From ed4e226da923f4caf49067db8ab2934d849f9ad9 Mon Sep 17 00:00:00 2001 From: Silvan <27845747+adlerhurst@users.noreply.github.com> Date: Tue, 29 Apr 2025 11:12:43 +0200 Subject: [PATCH 01/76] fix(defaults): comment default SystemAPIUsers (#9813) # Which Problems Are Solved If I start a fresh instance and do not overwrite `SystemAPIUsers` I get an error during startup `error="decoding failed due to the following error(s):\n\n'SystemAPIUsers[0][path]' expected a map, got 'string'\n'SystemAPIUsers[0][memberships]' expected a map, got 'slice'"` # How the Problems Are Solved the configuration is commented so that the example is still there # Additional Changes - # Additional Context was added in https://github.com/zitadel/zitadel/pull/9757 --- cmd/defaults.yaml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/cmd/defaults.yaml b/cmd/defaults.yaml index 8482ccec9f..55e14bbada 100644 --- a/cmd/defaults.yaml +++ b/cmd/defaults.yaml @@ -607,14 +607,14 @@ EncryptionKeys: UserAgentCookieKeyID: "userAgentCookieKey" # ZITADEL_ENCRYPTIONKEYS_USERAGENTCOOKIEKEYID SystemAPIUsers: - - superuser: - Path: /path/to/superuser/key.pem - Memberships: - - MemberType: Organization - Roles: "ORG_OWNER" - AggregateID: "123456789012345678" - - MemberType: Project - Roles: "PROJECT_OWNER" + # - superuser: + # Path: /path/to/superuser/key.pem + # Memberships: + # - MemberType: Organization + # Roles: "ORG_OWNER" + # AggregateID: "123456789012345678" + # - MemberType: Project + # Roles: "PROJECT_OWNER" # # Add keys for authentication of the systemAPI here: From ce823c9176bcd64ff3aacf73ce5fcb192dca76ce Mon Sep 17 00:00:00 2001 From: David Skewis Date: Tue, 29 Apr 2025 10:42:49 +0100 Subject: [PATCH 02/76] fix: update session recordings for posthog (#9775) # Which Problems Are Solved - Updates to only capture 10% of events with posthog # How the Problems Are Solved - Uses a feature flag rolled out to 10% of users to enable the capture # Additional Changes N/A # Additional Context N/A --- console/src/app/services/posthog.service.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/console/src/app/services/posthog.service.ts b/console/src/app/services/posthog.service.ts index 2f9630282a..17855d7eb5 100644 --- a/console/src/app/services/posthog.service.ts +++ b/console/src/app/services/posthog.service.ts @@ -26,8 +26,16 @@ export class PosthogService implements OnDestroy { maskAllInputs: true, maskTextSelector: '*', }, + disable_session_recording: true, enable_heatmaps: true, persistence: 'memory', + loaded: (posthog) => { + posthog.onFeatureFlags((flags) => { + if (posthog.isFeatureEnabled('session_recording')) { + posthog.startSessionRecording(); + } + }); + }, }); } } From d930a09cb04012cac96610f281401fe6d094d030 Mon Sep 17 00:00:00 2001 From: Ramon Date: Tue, 29 Apr 2025 13:25:49 +0200 Subject: [PATCH 03/76] fix: Improve Actions V2 Texts and reenable in settings (#9814) # Which Problems Are Solved This pr includes improved texts to make the usage of Actions V2 more easy. Since the removal of the Actions V2 Feature Flag we removed the code that checks if it's enabled in the settings sidenav. # How the Problems Are Solved Added new texts to translations. Removed sidenav logic that checks for Actions V2 Feature Flag # Additional Context - Part of #7248 - Part of #9688 --------- Co-authored-by: Max Peintner Co-authored-by: Max Peintner --- console/package.json | 4 +- .../actions-two-actions-table.component.ts | 4 +- .../actions-two-actions.component.html | 3 + .../actions-two-actions.component.ts | 3 + ...actions-two-add-action-dialog.component.ts | 19 +--- ...tions-two-add-action-target.component.html | 14 +-- ...actions-two-add-action-target.component.ts | 92 ++++++++++--------- ...tions-two-add-target-dialog.component.html | 3 + ...tions-two-add-target-dialog.component.scss | 4 + .../actions-two-targets.component.html | 3 + .../actions-two-targets.component.ts | 6 +- .../modules/actions-two/actions-two.module.ts | 2 + .../src/app/modules/settings-list/settings.ts | 2 + .../modules/sidenav/sidenav.component.html | 1 + .../modules/sidenav/sidenav.component.scss | 4 + .../app/modules/sidenav/sidenav.component.ts | 1 + .../app/pages/actions/actions.component.html | 3 + .../app/pages/actions/actions.component.ts | 63 ++++++------- .../app/pages/instance/instance.component.ts | 30 +----- console/src/assets/i18n/bg.json | 8 +- console/src/assets/i18n/cs.json | 8 +- console/src/assets/i18n/de.json | 8 +- console/src/assets/i18n/en.json | 8 +- console/src/assets/i18n/es.json | 8 +- console/src/assets/i18n/fr.json | 8 +- console/src/assets/i18n/hu.json | 8 +- console/src/assets/i18n/id.json | 8 +- console/src/assets/i18n/it.json | 8 +- console/src/assets/i18n/ja.json | 8 +- console/src/assets/i18n/ko.json | 8 +- console/src/assets/i18n/mk.json | 8 +- console/src/assets/i18n/nl.json | 8 +- console/src/assets/i18n/pl.json | 8 +- console/src/assets/i18n/pt.json | 8 +- console/src/assets/i18n/ro.json | 8 +- console/src/assets/i18n/ru.json | 8 +- console/src/assets/i18n/sv.json | 8 +- console/src/assets/i18n/zh.json | 8 +- console/yarn.lock | 25 ++--- 39 files changed, 249 insertions(+), 189 deletions(-) diff --git a/console/package.json b/console/package.json index 2d986730c2..77a8a40147 100644 --- a/console/package.json +++ b/console/package.json @@ -31,8 +31,8 @@ "@fortawesome/fontawesome-svg-core": "^6.7.2", "@fortawesome/free-brands-svg-icons": "^6.7.2", "@ngx-translate/core": "^15.0.0", - "@zitadel/client": "^1.0.7", - "@zitadel/proto": "1.0.5-sha-4118a9d", + "@zitadel/client": "1.2.0", + "@zitadel/proto": "1.2.0", "angular-oauth2-oidc": "^15.0.1", "angularx-qrcode": "^16.0.2", "buffer": "^6.0.3", 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 2d9942c406..658c205c4e 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 @@ -56,9 +56,9 @@ export class ActionsTwoActionsTableComponent { return executions.map((execution) => { const mappedTargets = execution.targets.map((target) => { - const targetType = targetsMap.get(target.type.value); + const targetType = targetsMap.get(target); if (!targetType) { - throw new Error(`Target with id ${target.type.value} not found`); + throw new Error(`Target with id ${target} not found`); } return targetType; }); diff --git a/console/src/app/modules/actions-two/actions-two-actions/actions-two-actions.component.html b/console/src/app/modules/actions-two/actions-two-actions/actions-two-actions.component.html index c22b03ef76..3e6c31fc0e 100644 --- a/console/src/app/modules/actions-two/actions-two-actions/actions-two-actions.component.html +++ b/console/src/app/modules/actions-two/actions-two-actions/actions-two-actions.component.html @@ -1,4 +1,7 @@

{{ 'ACTIONSTWO.EXECUTION.TITLE' | translate }}

+ + {{ 'ACTIONSTWO.BETA_NOTE' | translate }} +

{{ 'ACTIONSTWO.EXECUTION.DESCRIPTION' | translate }}

}; -type CorrectlyTypedTargets = { type: Extract }; - -export type CorrectlyTypedExecution = Omit & { +export type CorrectlyTypedExecution = Omit & { condition: CorrectlyTypedCondition; - targets: CorrectlyTypedTargets[]; }; export const correctlyTypeExecution = (execution: Execution): CorrectlyTypedExecution => { @@ -48,9 +40,6 @@ export const correctlyTypeExecution = (execution: Execution): CorrectlyTypedExec return { ...execution, condition, - targets: execution.targets - .map(({ type }) => ({ type })) - .filter((target): target is CorrectlyTypedTargets => target.type.case === 'target'), }; }; @@ -81,7 +70,7 @@ export class ActionTwoAddActionDialogComponent { protected readonly typeSignal = signal('request'); protected readonly conditionSignal = signal['condition']>(undefined); - protected readonly targetsSignal = signal[]>([]); + protected readonly targetsSignal = signal([]); protected readonly continueSubject = new Subject(); @@ -112,7 +101,7 @@ export class ActionTwoAddActionDialogComponent { this.targetsSignal.set(data.execution.targets); this.typeSignal.set(data.execution.condition.conditionType.case); this.conditionSignal.set(data.execution.condition); - this.preselectedTargetIds = data.execution.targets.map((target) => target.type.value); + this.preselectedTargetIds = data.execution.targets; this.page.set(Page.Target); // Set the initial page based on the provided execution data } diff --git a/console/src/app/modules/actions-two/actions-two-add-action/actions-two-add-action-target/actions-two-add-action-target.component.html b/console/src/app/modules/actions-two/actions-two-add-action/actions-two-add-action-target/actions-two-add-action-target.component.html index 422ed7991e..26d9e3be2c 100644 --- a/console/src/app/modules/actions-two/actions-two-add-action/actions-two-add-action-target/actions-two-add-action-target.component.html +++ b/console/src/app/modules/actions-two/actions-two-add-action/actions-two-add-action-target/actions-two-add-action-target.component.html @@ -1,4 +1,4 @@ -
+

{{ 'ACTIONSTWO.EXECUTION.DIALOG.TARGET.DESCRIPTION' | translate }}

@@ -8,9 +8,9 @@ #trigger="matAutocompleteTrigger" #input type="text" - [formControl]="form().controls.autocomplete" + [formControl]="form.controls.autocomplete" [matAutocomplete]="autoservice" - (keydown.enter)="handleEnter($event); input.blur(); trigger.closePanel()" + (keydown.enter)="handleEnter($event, form); input.blur(); trigger.closePanel()" /> @@ -19,7 +19,7 @@ {{ target.name }} @@ -27,7 +27,7 @@ - +
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 12/76] 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 13/76] fix: Improve Actions V2 translations (#9826) # Which Problems Are Solved The translation for event was not loaded correctly. ![grafik](https://github.com/user-attachments/assets/3fa8d72f-f55a-44b7-997d-0f0976f66b85) # 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 14/76] 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 15/76] 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 16/76] 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 17/76] 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 18/76] 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 19/76] 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 20/76] 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 21/76] 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 22/76] 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 23/76] 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 ( +
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 }} - 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 04/76] 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 05/76] 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 06/76] 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 07/76] 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 08/76] 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 09/76] 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 10/76] 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 11/76] 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 }}
+ + + + + + { + pii.map((row, rowID) => { + return ( + + + + + + ) + }) + } +
Type of personal dataExamplesAffected data subjects
{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 ( - - - - - - - - - { - 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 ( - - - - - - - - ) - }) - } -
Entity namePurposeEnd-user dataHosting locationCountry of registration
{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 24/76] 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 25/76] 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 26/76] 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 27/76] 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 28/76] 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 29/76] 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 30/76] 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 + ## Subscribe to our Mailing List From fefe9d27a0a5c32592df9967f07729c113351cd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabienne=20B=C3=BChler?= Date: Fri, 16 May 2025 12:07:02 +0200 Subject: [PATCH 31/76] docs: fix typo in email notification provider description (#9890) --- docs/docs/guides/manage/customize/notification-providers.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/guides/manage/customize/notification-providers.mdx b/docs/docs/guides/manage/customize/notification-providers.mdx index b94ce7542d..3bd1b358e5 100644 --- a/docs/docs/guides/manage/customize/notification-providers.mdx +++ b/docs/docs/guides/manage/customize/notification-providers.mdx @@ -74,7 +74,7 @@ A provider with HTTP type will send the messages and the data to a pre-defined w - First [add a new Email Provider of type HTTP](/apis/resources/admin/admin-service-add-email-provider-http) to create a new HTTP provider that can be used to send SMS messages: + First [add a new Email Provider of type HTTP](/apis/resources/admin/admin-service-add-email-provider-http) to create a new HTTP provider that can be used to send Email messages: ```bash curl -L 'https://$CUSTOM-DOMAIN/admin/v1/email/http' \ From 38013d0e84862dfe379c3ee9b54dfcde35237b9e Mon Sep 17 00:00:00 2001 From: Juriaan Kennedy <97170019+juriaankennedy@users.noreply.github.com> Date: Fri, 16 May 2025 17:53:45 +0200 Subject: [PATCH 32/76] feat(crypto): support for SHA2 and PHPass password hashes (#9809) # Which Problems Are Solved - Allow users to use SHA-256 and SHA-512 hashing algorithms. These algorithms are used by Linux's crypt(3) function. - Allow users to import passwords using the PHPass algorithm. This algorithm is used by older PHP systems, WordPress in particular. # How the Problems Are Solved - Upgrade passwap to [v0.9.0](https://github.com/zitadel/passwap/releases/tag/v0.9.0) - Add sha2 and phpass as a new verifier option in defaults.yaml # Additional Changes - Updated docs to explain the two algorithms # Additional Context Implements the changes in the passwap library from https://github.com/zitadel/passwap/pull/59 and https://github.com/zitadel/passwap/pull/60 --- cmd/defaults.yaml | 11 +- docs/docs/concepts/architecture/secrets.md | 2 + go.mod | 10 +- go.sum | 20 +-- internal/crypto/passwap.go | 56 +++++++- internal/crypto/passwap_test.go | 155 +++++++++++++++++++++ 6 files changed, 233 insertions(+), 21 deletions(-) diff --git a/cmd/defaults.yaml b/cmd/defaults.yaml index c13d5337b1..397e9af376 100644 --- a/cmd/defaults.yaml +++ b/cmd/defaults.yaml @@ -653,7 +653,7 @@ SystemDefaults: # or cost are automatically re-hashed using this config, # upon password validation or update. Hasher: - # Supported algorithms: "argon2i", "argon2id", "bcrypt", "scrypt", "pbkdf2" + # Supported algorithms: "argon2i", "argon2id", "bcrypt", "scrypt", "pbkdf2", "sha2" # Depending on the algorithm, different configuration options take effect. Algorithm: bcrypt # ZITADEL_SYSTEMDEFAULTS_PASSWORDHASHER_HASHER_ALGORITHM # Cost takes effect for the algorithms bcrypt and scrypt @@ -664,10 +664,11 @@ SystemDefaults: Memory: 32768 # ZITADEL_SYSTEMDEFAULTS_PASSWORDHASHER_HASHER_MEMORY # Threads takes effect for the algorithms argon2i and argon2id Threads: 4 # ZITADEL_SYSTEMDEFAULTS_PASSWORDHASHER_HASHER_THREADS - # Rounds takes effect for the algorithm pbkdf2 + # Rounds takes effect for the algorithm pbkdf2 and sha2 Rounds: 290000 # ZITADEL_SYSTEMDEFAULTS_PASSWORDHASHER_HASHER_ROUNDS - # Hash takes effect for the algorithm pbkdf2 - # Can be "sha1", "sha224", "sha256", "sha384" or "sha512" + # Hash takes effect for the algorithm pbkdf2 and sha2 + # Can be "sha1", "sha224", "sha256", "sha384" or "sha512" for pbkdf2 + # Can be "sha256" or "sha512" for sha2 Hash: sha256 # ZITADEL_SYSTEMDEFAULTS_PASSWORDHASHER_HASHER_HASH # Verifiers enable the possibility of verifying @@ -689,6 +690,8 @@ SystemDefaults: # - "md5" # md5Crypt with salt and password shuffling. # - "md5plain" # md5 digest of a password without salt # - "md5salted" # md5 digest of a salted password + # - "phpass" + # - "sha2" # crypt(3) SHA-256 and SHA-512 # - "scrypt" # - "pbkdf2" # verifier for all pbkdf2 hash modes. SecretHasher: diff --git a/docs/docs/concepts/architecture/secrets.md b/docs/docs/concepts/architecture/secrets.md index f8f195114b..1f36802754 100644 --- a/docs/docs/concepts/architecture/secrets.md +++ b/docs/docs/concepts/architecture/secrets.md @@ -71,6 +71,8 @@ The following hash algorithms are supported: - md5: implementation of md5Crypt with salt and password shuffling [^2] - md5plain: md5 digest of a password without salt [^2] - md5salted: md5 digest of a salted password [^2] +- phpass: md5 digest with PHPass algorithm (used in WordPress) [^2] +- sha2: implementation of crypt(3) SHA-256 & SHA-512 - scrypt - pbkdf2 diff --git a/go.mod b/go.mod index 99df9ad86f..00f65c1331 100644 --- a/go.mod +++ b/go.mod @@ -73,7 +73,7 @@ require ( github.com/zitadel/exifremove v0.1.0 github.com/zitadel/logging v0.6.2 github.com/zitadel/oidc/v3 v3.36.1 - github.com/zitadel/passwap v0.7.0 + github.com/zitadel/passwap v0.9.0 github.com/zitadel/saml v0.3.5 github.com/zitadel/schema v1.3.1 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 @@ -87,12 +87,12 @@ require ( go.opentelemetry.io/otel/sdk/metric v1.35.0 go.opentelemetry.io/otel/trace v1.35.0 go.uber.org/mock v0.5.0 - golang.org/x/crypto v0.36.0 + golang.org/x/crypto v0.37.0 golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 golang.org/x/net v0.37.0 golang.org/x/oauth2 v0.28.0 - golang.org/x/sync v0.12.0 - golang.org/x/text v0.23.0 + golang.org/x/sync v0.13.0 + golang.org/x/text v0.24.0 google.golang.org/api v0.227.0 google.golang.org/genproto/googleapis/api v0.0.0-20250313205543-e70fdf4c4cb4 google.golang.org/grpc v1.71.0 @@ -226,7 +226,7 @@ require ( github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 // indirect go.opentelemetry.io/proto/otlp v1.5.0 // indirect - golang.org/x/sys v0.31.0 + golang.org/x/sys v0.32.0 gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect nhooyr.io/websocket v1.8.11 // indirect diff --git a/go.sum b/go.sum index cb8f0cf4bd..f361fb87fa 100644 --- a/go.sum +++ b/go.sum @@ -807,8 +807,8 @@ github.com/zitadel/logging v0.6.2 h1:MW2kDDR0ieQynPZ0KIZPrh9ote2WkxfBif5QoARDQcU github.com/zitadel/logging v0.6.2/go.mod h1:z6VWLWUkJpnNVDSLzrPSQSQyttysKZ6bCRongw0ROK4= github.com/zitadel/oidc/v3 v3.36.1 h1:1AT1NqKKEqAwx4GmKJZ9fYkWH2WIn/VKMfQ46nBtRf0= github.com/zitadel/oidc/v3 v3.36.1/go.mod h1:dApGZLvWZTHRuxmcbQlW5d2XVjVYR3vGOdq536igmTs= -github.com/zitadel/passwap v0.7.0 h1:TQTr9TV75PLATGICor1g5hZDRNHRvB9t0Hn4XkiR7xQ= -github.com/zitadel/passwap v0.7.0/go.mod h1:/NakQNYahdU+YFEitVD6mlm8BLfkiIT+IM5wgClRoAY= +github.com/zitadel/passwap v0.9.0 h1:QvDK8OHKdb73C0m+mwXvu87UJSBqix3oFwTVENHdv80= +github.com/zitadel/passwap v0.9.0/go.mod h1:6QzwFjDkIr3FfudzSogTOx5Ydhq4046dRJtDM/kX+G8= github.com/zitadel/saml v0.3.5 h1:L1RKWS5y66cGepVxUGjx/WSBOtrtSpRA/J3nn5BJLOY= github.com/zitadel/saml v0.3.5/go.mod h1:ybs3e4tIWdYgSYBpuCsvf3T4FNDfbXYM+GPv5vIpHYk= github.com/zitadel/schema v1.3.1 h1:QT3kwiRIRXXLVAs6gCK/u044WmUVh6IlbLXUsn6yRQU= @@ -881,8 +881,8 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= -golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= -golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= +golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20200331195152-e8c3332aa8e5/go.mod h1:4M0jN8W1tt0AVLNr8HDosyJCDCDuyL9N9+3m7wDWgKw= @@ -970,8 +970,8 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= -golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= +golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1016,8 +1016,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= -golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= +golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -1038,8 +1038,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= -golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= -golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= +golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= diff --git a/internal/crypto/passwap.go b/internal/crypto/passwap.go index e14c2dfaaf..e4f3313342 100644 --- a/internal/crypto/passwap.go +++ b/internal/crypto/passwap.go @@ -14,7 +14,9 @@ import ( "github.com/zitadel/passwap/md5plain" "github.com/zitadel/passwap/md5salted" "github.com/zitadel/passwap/pbkdf2" + "github.com/zitadel/passwap/phpass" "github.com/zitadel/passwap/scrypt" + "github.com/zitadel/passwap/sha2" "github.com/zitadel/passwap/verifier" "github.com/zitadel/zitadel/internal/zerrors" @@ -51,6 +53,8 @@ const ( HashNameMd5 HashName = "md5" // verify only, as hashing with md5 is insecure and deprecated HashNameMd5Plain HashName = "md5plain" // verify only, as hashing with md5 is insecure and deprecated HashNameMd5Salted HashName = "md5salted" // verify only, as hashing with md5 is insecure and deprecated + HashNamePHPass HashName = "phpass" // verify only, as hashing with md5 is insecure and deprecated + HashNameSha2 HashName = "sha2" // hash and verify HashNameScrypt HashName = "scrypt" // hash and verify HashNamePBKDF2 HashName = "pbkdf2" // hash and verify ) @@ -125,6 +129,14 @@ var knowVerifiers = map[HashName]prefixVerifier{ prefixes: []string{md5salted.Prefix}, verifier: md5salted.Verifier, }, + HashNameSha2: { + prefixes: []string{sha2.Sha256Identifier, sha2.Sha512Identifier}, + verifier: sha2.Verifier, + }, + HashNamePHPass: { + prefixes: []string{phpass.IdentifierP, phpass.IdentifierH}, + verifier: phpass.Verifier, + }, } func (c *HashConfig) buildVerifiers() (verifiers []verifier.Verifier, prefixes []string, err error) { @@ -158,9 +170,11 @@ func (c *HasherConfig) buildHasher() (hasher passwap.Hasher, prefixes []string, return c.scrypt() case HashNamePBKDF2: return c.pbkdf2() + case HashNameSha2: + return c.sha2() case "": return nil, nil, fmt.Errorf("missing hasher algorithm") - case HashNameArgon2, HashNameMd5: + case HashNameArgon2, HashNameMd5, HashNameMd5Plain, HashNameMd5Salted, HashNamePHPass: fallthrough default: return nil, nil, fmt.Errorf("invalid algorithm %q", c.Algorithm) @@ -296,6 +310,44 @@ func (c *HasherConfig) pbkdf2() (passwap.Hasher, []string, error) { case HashModeSHA512: return pbkdf2.NewSHA512(p), prefix, nil default: - return nil, nil, fmt.Errorf("unsuppored pbkdf2 hash mode: %s", hash) + return nil, nil, fmt.Errorf("unsupported pbkdf2 hash mode: %s", hash) + } +} + +func (c *HasherConfig) sha2Params() (use512 bool, rounds int, err error) { + var dst = struct { + Rounds uint32 `mapstructure:"Rounds"` + Hash HashMode `mapstructure:"Hash"` + }{} + if err := c.decodeParams(&dst); err != nil { + return false, 0, fmt.Errorf("decode sha2 params: %w", err) + } + switch dst.Hash { + case HashModeSHA256: + use512 = false + case HashModeSHA512: + use512 = true + case HashModeSHA1, HashModeSHA224, HashModeSHA384: + fallthrough + default: + return false, 0, fmt.Errorf("cannot use %s with sha2", dst.Hash) + } + if dst.Rounds > sha2.RoundsMax { + return false, 0, fmt.Errorf("rounds with sha2 cannot be larger than %d", sha2.RoundsMax) + } else { + rounds = int(dst.Rounds) + } + return use512, rounds, nil +} + +func (c *HasherConfig) sha2() (passwap.Hasher, []string, error) { + use512, rounds, err := c.sha2Params() + if err != nil { + return nil, nil, err + } + if use512 { + return sha2.New512(rounds), []string{sha2.Sha256Identifier, sha2.Sha512Identifier}, nil + } else { + return sha2.New256(rounds), []string{sha2.Sha256Identifier, sha2.Sha512Identifier}, nil } } diff --git a/internal/crypto/passwap_test.go b/internal/crypto/passwap_test.go index b872b0e298..dfcafd1406 100644 --- a/internal/crypto/passwap_test.go +++ b/internal/crypto/passwap_test.go @@ -14,6 +14,7 @@ import ( "github.com/zitadel/passwap/md5salted" "github.com/zitadel/passwap/pbkdf2" "github.com/zitadel/passwap/scrypt" + "github.com/zitadel/passwap/sha2" ) func TestPasswordHasher_EncodingSupported(t *testing.T) { @@ -78,7 +79,9 @@ func TestPasswordHashConfig_PasswordHasher(t *testing.T) { HashNameBcrypt, HashNameMd5, HashNameMd5Salted, + HashNamePHPass, HashNameScrypt, + HashNameSha2, "foobar", }, Hasher: HasherConfig{ @@ -142,6 +145,15 @@ func TestPasswordHashConfig_PasswordHasher(t *testing.T) { }, wantErr: true, }, + { + name: "invalid phpass", + fields: fields{ + Hasher: HasherConfig{ + Algorithm: HashNamePHPass, + }, + }, + wantErr: true, + }, { name: "invalid argon2", fields: fields{ @@ -357,6 +369,59 @@ func TestPasswordHashConfig_PasswordHasher(t *testing.T) { }, wantPrefixes: []string{pbkdf2.Prefix, argon2.Prefix, bcrypt.Prefix, md5.Prefix, md5salted.Prefix}, }, + { + name: "sha2, parse error", + fields: fields{ + Hasher: HasherConfig{ + Algorithm: HashNameSha2, + Params: map[string]any{ + "cost": "bar", + }, + }, + }, + wantErr: true, + }, + { + name: "pbkdf2, hash mode error", + fields: fields{ + Hasher: HasherConfig{ + Algorithm: HashNameSha2, + Params: map[string]any{ + "Rounds": 12, + "Hash": "foo", + }, + }, + }, + wantErr: true, + }, + { + name: "sha2, sha256", + fields: fields{ + Hasher: HasherConfig{ + Algorithm: HashNameSha2, + Params: map[string]any{ + "Rounds": 12, + "Hash": HashModeSHA256, + }, + }, + Verifiers: []HashName{HashNameArgon2, HashNameBcrypt, HashNameMd5, HashNameMd5Plain}, + }, + wantPrefixes: []string{sha2.Sha256Identifier, sha2.Sha512Identifier, argon2.Prefix, bcrypt.Prefix, md5.Prefix}, + }, + { + name: "sha2, sha512", + fields: fields{ + Hasher: HasherConfig{ + Algorithm: HashNameSha2, + Params: map[string]any{ + "Rounds": 12, + "Hash": HashModeSHA512, + }, + }, + Verifiers: []HashName{HashNameArgon2, HashNameBcrypt, HashNameMd5, HashNameMd5Plain}, + }, + wantPrefixes: []string{sha2.Sha256Identifier, sha2.Sha512Identifier, argon2.Prefix, bcrypt.Prefix, md5.Prefix}, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -723,3 +788,93 @@ func TestHasherConfig_pbkdf2Params(t *testing.T) { }) } } + +func TestHasherConfig_sha2Params(t *testing.T) { + type fields struct { + Params map[string]any + } + tests := []struct { + name string + fields fields + want512 bool + wantRounds int + wantErr bool + }{ + { + name: "decode error", + fields: fields{ + Params: map[string]any{ + "foo": "bar", + }, + }, + wantErr: true, + }, + { + name: "sha1", + fields: fields{ + Params: map[string]any{ + "Rounds": 12, + "Hash": "sha1", + }, + }, + wantErr: true, + }, + { + name: "sha224", + fields: fields{ + Params: map[string]any{ + "Rounds": 12, + "Hash": "sha224", + }, + }, + wantErr: true, + }, + { + name: "sha256", + fields: fields{ + Params: map[string]any{ + "Rounds": 5000, + "Hash": "sha256", + }, + }, + want512: false, + wantRounds: 5000, + }, + { + name: "sha384", + fields: fields{ + Params: map[string]any{ + "Rounds": 12, + "Hash": "sha384", + }, + }, + wantErr: true, + }, + { + name: "sha512", + fields: fields{ + Params: map[string]any{ + "Rounds": 15000, + "Hash": "sha512", + }, + }, + want512: true, + wantRounds: 15000, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &HasherConfig{ + Params: tt.fields.Params, + } + got512, gotRounds, err := c.sha2Params() + if tt.wantErr { + assert.Error(t, err) + return + } + require.NoError(t, err) + assert.Equal(t, tt.want512, got512) + assert.Equal(t, tt.wantRounds, gotRounds) + }) + } +} From 056b01f78d64a57d1f877a0d97f16d7f9295705e Mon Sep 17 00:00:00 2001 From: Daniel Fabian <67621761+FabianDaniel00@users.noreply.github.com> Date: Mon, 19 May 2025 09:11:54 +0200 Subject: [PATCH 33/76] fix: typoe in "Migrate from ZITADEL" documentation (#9867) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Which Problems Are Solved - Fixed a typoe in ["Migrate from ZITADEL" documentation](https://zitadel.com/docs/guides/migrate/sources/zitadel#authorization) Co-authored-by: Fabienne Bühler --- docs/docs/guides/migrate/sources/zitadel.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/docs/guides/migrate/sources/zitadel.md b/docs/docs/guides/migrate/sources/zitadel.md index 8a8dd60a3d..99b6e1d64e 100644 --- a/docs/docs/guides/migrate/sources/zitadel.md +++ b/docs/docs/guides/migrate/sources/zitadel.md @@ -40,7 +40,7 @@ You need a PAT from a service user with IAM Owner permissions in both the source 4. Go to the Default settings 5. Add the import_user as [manager](/docs/guides/manage/console/managers) with the role "IAM Owner" -Save the PAT to the environment variabel `PAT_EXPORT_TOKEN` and the source domain as `ZITADEL_EXPORT_DOMAIN` to run the following scripts. +Save the PAT to the environment variable `PAT_EXPORT_TOKEN` and the source domain as `ZITADEL_EXPORT_DOMAIN` to run the following scripts. ### Target system @@ -50,7 +50,7 @@ Save the PAT to the environment variabel `PAT_EXPORT_TOKEN` and the source domai 4. Go to the Default settings 5. Add the export_user as [manager](/docs/guides/manage/console/managers) with the role "IAM Owner" -Save the PAT to the environment variabel `PAT_IMPORT_TOKEN` and the source domain as `ZITADEL_IMPORT_DOMAIN` to run the following scripts. +Save the PAT to the environment variable `PAT_IMPORT_TOKEN` and the source domain as `ZITADEL_IMPORT_DOMAIN` to run the following scripts. :::warning Clean-up You should let the PAT expire as soon as possible. From 1b2fd23e0b6fe21e144df85e449cf45b59bb4ed9 Mon Sep 17 00:00:00 2001 From: Livio Spring Date: Mon, 19 May 2025 11:25:17 +0200 Subject: [PATCH 34/76] fix: idp user information mapping (#9892) # Which Problems Are Solved When retrieving the information of an IdP intent, depending on the IdP type (e.g. Apple), there was issue when mapping the stored (event) information back to the specific IdP type, potentially leading to a panic. # How the Problems Are Solved - Correctly initialize the user struct to map the information to. # Additional Changes none # Additional Context - reported by a support request - needs backport to 3.x and 2.x --- internal/api/grpc/user/v2/intent.go | 8 ++++---- internal/idp/providers/apple/session.go | 4 ++++ internal/idp/providers/google/google.go | 4 ++++ internal/idp/providers/oidc/session.go | 4 ++++ 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/internal/api/grpc/user/v2/intent.go b/internal/api/grpc/user/v2/intent.go index 8043a9bdae..afb34deb83 100644 --- a/internal/api/grpc/user/v2/intent.go +++ b/internal/api/grpc/user/v2/intent.go @@ -167,11 +167,11 @@ func (s *Server) RetrieveIdentityProviderIntent(ctx context.Context, req *user.R var idpUser idp.User switch p := provider.(type) { case *apple.Provider: - idpUser, err = unmarshalIdpUser(intent.IDPUser, &apple.User{}) + idpUser, err = unmarshalIdpUser(intent.IDPUser, apple.InitUser()) case *oauth.Provider: idpUser, err = unmarshalRawIdpUser(intent.IDPUser, p.User()) case *oidc.Provider: - idpUser, err = unmarshalIdpUser(intent.IDPUser, &oidc.User{UserInfo: &oidc_pkg.UserInfo{}}) + idpUser, err = unmarshalIdpUser(intent.IDPUser, oidc.InitUser()) case *jwt.Provider: idpUser, err = unmarshalIdpUser(intent.IDPUser, &jwt.User{}) case *azuread.Provider: @@ -179,9 +179,9 @@ func (s *Server) RetrieveIdentityProviderIntent(ctx context.Context, req *user.R case *github.Provider: idpUser, err = unmarshalIdpUser(intent.IDPUser, &github.User{}) case *gitlab.Provider: - idpUser, err = unmarshalIdpUser(intent.IDPUser, &oidc.User{UserInfo: &oidc_pkg.UserInfo{}}) + idpUser, err = unmarshalIdpUser(intent.IDPUser, oidc.InitUser()) case *google.Provider: - idpUser, err = unmarshalIdpUser(intent.IDPUser, &google.User{User: &oidc.User{UserInfo: &oidc_pkg.UserInfo{}}}) + idpUser, err = unmarshalIdpUser(intent.IDPUser, google.InitUser()) case *saml.Provider: idpUser, err = unmarshalIdpUser(intent.IDPUser, &saml.UserMapper{}) case *ldap.Provider: diff --git a/internal/idp/providers/apple/session.go b/internal/idp/providers/apple/session.go index 9395d84b2b..99794d18a2 100644 --- a/internal/idp/providers/apple/session.go +++ b/internal/idp/providers/apple/session.go @@ -60,6 +60,10 @@ func NewUser(info *openid.UserInfo, names userNamesFormValue) *User { return &User{User: user} } +func InitUser() idp.User { + return &User{User: oidc.InitUser()} +} + // User extends the [oidc.User] by returning the email as preferred_username, since Apple does not return the latter. type User struct { *oidc.User diff --git a/internal/idp/providers/google/google.go b/internal/idp/providers/google/google.go index 221f2b61ae..083d4aef62 100644 --- a/internal/idp/providers/google/google.go +++ b/internal/idp/providers/google/google.go @@ -34,6 +34,10 @@ var userMapper = func(info *openid.UserInfo) idp.User { return &User{oidc.DefaultMapper(info)} } +func InitUser() idp.User { + return &User{oidc.InitUser()} +} + // User is a representation of the authenticated Google and implements the [idp.User] interface // by wrapping an [idp.User] (implemented by [oidc.User]). It overwrites the [GetPreferredUsername] to use the `email` claim. type User struct { diff --git a/internal/idp/providers/oidc/session.go b/internal/idp/providers/oidc/session.go index 430a14e5bb..9e1e55baf5 100644 --- a/internal/idp/providers/oidc/session.go +++ b/internal/idp/providers/oidc/session.go @@ -96,6 +96,10 @@ func NewUser(info *oidc.UserInfo) *User { return &User{UserInfo: info} } +func InitUser() *User { + return &User{UserInfo: &oidc.UserInfo{}} +} + type User struct { *oidc.UserInfo } From 968d91a3e0a745fda5b6dfbffdbf1dee8f1306a6 Mon Sep 17 00:00:00 2001 From: Livio Spring Date: Mon, 19 May 2025 12:16:49 +0200 Subject: [PATCH 35/76] chore: update dependencies (#9784) # Which Problems Are Solved Some dependencies are out of date and published new version including (unaffected) vulnerability fixes. # How the Problems Are Solved - Updated at least all direct dependencies apart from i18n, webauthn (existing issues), - crewjam (https://github.com/zitadel/zitadel/issues/9783) and - github.com/gorilla/csrf (https://github.com/gorilla/csrf/issues/190, https://github.com/gorilla/csrf/issues/189, https://github.com/gorilla/csrf/issues/188, https://github.com/gorilla/csrf/issues/187, https://github.com/gorilla/csrf/issues/186) - noteworthy: https://github.com/golang/go/issues/73626 - Some dependencies require Go 1.24, which triggered an update for zitadel to go 1.24 as well. # Additional Changes None # Additional Context None --- .github/workflows/build.yml | 2 +- go.mod | 90 ++++--- go.sum | 254 +++++++----------- internal/api/oidc/error.go | 9 +- internal/protoc/protoc-gen-authoption/main.go | 2 +- internal/query/auth_request_test.go | 5 +- internal/query/saml_request_test.go | 5 +- 7 files changed, 150 insertions(+), 217 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c5e99b95b9..f06c4a959c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -72,7 +72,7 @@ jobs: with: node_version: "18" buf_version: "latest" - go_lint_version: "v1.62.2" + go_lint_version: "v1.64.8" core_cache_key: ${{ needs.core.outputs.cache_key }} core_cache_path: ${{ needs.core.outputs.cache_path }} diff --git a/go.mod b/go.mod index 00f65c1331..ec86708942 100644 --- a/go.mod +++ b/go.mod @@ -1,10 +1,12 @@ module github.com/zitadel/zitadel -go 1.23.7 +go 1.24 + +toolchain go1.24.1 require ( cloud.google.com/go/profiler v0.4.2 - cloud.google.com/go/storage v1.51.0 + cloud.google.com/go/storage v1.54.0 github.com/BurntSushi/toml v1.5.0 github.com/DATA-DOG/go-sqlmock v1.5.2 github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/trace v1.27.0 @@ -20,15 +22,15 @@ require ( github.com/crewjam/saml v0.4.14 github.com/descope/virtualwebauthn v1.0.3 github.com/dop251/goja v0.0.0-20250309171923-bcd7cc6bf64c - github.com/dop251/goja_nodejs v0.0.0-20250314160716-c55ecee183c0 + github.com/dop251/goja_nodejs v0.0.0-20250409162600-f7acab6894b0 github.com/drone/envsubst v1.0.3 github.com/envoyproxy/protoc-gen-validate v1.2.1 github.com/fatih/color v1.18.0 github.com/fergusstrange/embedded-postgres v1.30.0 - github.com/gabriel-vasile/mimetype v1.4.8 + github.com/gabriel-vasile/mimetype v1.4.9 github.com/go-chi/chi/v5 v5.2.1 - github.com/go-jose/go-jose/v4 v4.0.5 - github.com/go-ldap/ldap/v3 v3.4.10 + github.com/go-jose/go-jose/v4 v4.1.0 + github.com/go-ldap/ldap/v3 v3.4.11 github.com/go-webauthn/webauthn v0.10.2 github.com/goccy/go-json v0.10.5 github.com/golang/protobuf v1.5.4 @@ -43,36 +45,36 @@ require ( github.com/h2non/gock v1.2.0 github.com/hashicorp/golang-lru/v2 v2.0.7 github.com/improbable-eng/grpc-web v0.15.0 - github.com/jackc/pgx/v5 v5.7.3 + github.com/jackc/pgx/v5 v5.7.5 github.com/jarcoal/jpath v0.0.0-20140328210829-f76b8b2dbf52 github.com/jinzhu/gorm v1.9.16 github.com/k3a/html2text v1.2.1 github.com/lucasb-eyer/go-colorful v1.2.0 - github.com/minio/minio-go/v7 v7.0.88 + github.com/minio/minio-go/v7 v7.0.91 github.com/mitchellh/mapstructure v1.5.0 github.com/muesli/gamut v0.3.1 github.com/muhlemmer/gu v0.3.1 github.com/muhlemmer/httpforwarded v0.1.0 github.com/nicksnyder/go-i18n/v2 v2.4.0 - github.com/pashagolub/pgxmock/v4 v4.6.0 - github.com/pquerna/otp v1.4.0 + github.com/pashagolub/pgxmock/v4 v4.7.0 + github.com/pquerna/otp v1.5.0 github.com/rakyll/statik v0.1.7 - github.com/redis/go-redis/v9 v9.7.3 - github.com/riverqueue/river v0.19.0 - github.com/riverqueue/river/riverdriver v0.19.0 - github.com/riverqueue/river/rivertype v0.19.0 + github.com/redis/go-redis/v9 v9.8.0 + github.com/riverqueue/river v0.22.0 + github.com/riverqueue/river/riverdriver v0.22.0 + github.com/riverqueue/river/rivertype v0.22.0 github.com/rs/cors v1.11.1 github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 github.com/sony/gobreaker/v2 v2.1.0 - github.com/sony/sonyflake v1.2.0 + github.com/sony/sonyflake v1.2.1 github.com/spf13/cobra v1.9.1 - github.com/spf13/viper v1.20.0 + github.com/spf13/viper v1.20.1 github.com/stretchr/testify v1.10.0 github.com/ttacon/libphonenumber v1.2.1 - github.com/twilio/twilio-go v1.24.1 + github.com/twilio/twilio-go v1.26.1 github.com/zitadel/exifremove v0.1.0 github.com/zitadel/logging v0.6.2 - github.com/zitadel/oidc/v3 v3.36.1 + github.com/zitadel/oidc/v3 v3.37.0 github.com/zitadel/passwap v0.9.0 github.com/zitadel/saml v0.3.5 github.com/zitadel/schema v1.3.1 @@ -86,26 +88,26 @@ require ( go.opentelemetry.io/otel/sdk v1.35.0 go.opentelemetry.io/otel/sdk/metric v1.35.0 go.opentelemetry.io/otel/trace v1.35.0 - go.uber.org/mock v0.5.0 - golang.org/x/crypto v0.37.0 - golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 - golang.org/x/net v0.37.0 - golang.org/x/oauth2 v0.28.0 - golang.org/x/sync v0.13.0 - golang.org/x/text v0.24.0 - google.golang.org/api v0.227.0 - google.golang.org/genproto/googleapis/api v0.0.0-20250313205543-e70fdf4c4cb4 - google.golang.org/grpc v1.71.0 - google.golang.org/protobuf v1.36.5 + go.uber.org/mock v0.5.2 + golang.org/x/crypto v0.38.0 + golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 + golang.org/x/net v0.40.0 + golang.org/x/oauth2 v0.30.0 + golang.org/x/sync v0.14.0 + golang.org/x/text v0.25.0 + google.golang.org/api v0.233.0 + google.golang.org/genproto/googleapis/api v0.0.0-20250512202823-5a2f75b736a9 + google.golang.org/grpc v1.72.1 + google.golang.org/protobuf v1.36.6 sigs.k8s.io/yaml v1.4.0 ) require ( - cel.dev/expr v0.19.2 // indirect - cloud.google.com/go/auth v0.15.0 // indirect - cloud.google.com/go/auth/oauth2adapt v0.2.7 // indirect + cel.dev/expr v0.20.0 // indirect + cloud.google.com/go/auth v0.16.1 // indirect + cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect cloud.google.com/go/monitoring v1.24.0 // indirect - github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0 // indirect github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302 // indirect @@ -130,7 +132,7 @@ require ( github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect - github.com/klauspost/cpuid/v2 v2.2.9 // indirect + github.com/klauspost/cpuid/v2 v2.2.10 // indirect github.com/lib/pq v1.10.9 // indirect github.com/mattermost/xml-roundtrip-validator v0.1.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect @@ -140,29 +142,31 @@ require ( github.com/pelletier/go-toml/v2 v2.2.3 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect - github.com/riverqueue/river/rivershared v0.19.0 // indirect + github.com/riverqueue/river/rivershared v0.22.0 // indirect github.com/sagikazarmark/locafero v0.7.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spiffe/go-spiffe/v2 v2.5.0 // indirect github.com/tidwall/gjson v1.18.0 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/sjson v1.2.5 // indirect github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect github.com/yuin/gopher-lua v1.1.1 // indirect + github.com/zeebo/errs v1.4.0 // indirect github.com/zenazn/goji v1.0.1 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/contrib/detectors/gcp v1.34.0 // indirect + go.opentelemetry.io/contrib/detectors/gcp v1.35.0 // indirect go.uber.org/goleak v1.3.0 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/time v0.11.0 // indirect google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250505200425-f936aa4a68b2 // indirect ) require ( - cloud.google.com/go v0.118.3 // indirect + cloud.google.com/go v0.121.0 // indirect cloud.google.com/go/compute/metadata v0.6.0 // indirect - cloud.google.com/go/iam v1.4.1 // indirect + cloud.google.com/go/iam v1.5.2 // indirect cloud.google.com/go/trace v1.11.3 // indirect github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect github.com/amdonov/xmlsig v0.1.0 // indirect @@ -185,7 +189,7 @@ require ( github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.8.0 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect - github.com/go-asn1-ber/asn1-ber v1.5.7 // indirect + github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect github.com/go-errors/errors v1.5.1 // indirect github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect github.com/go-xmlfmt/xmlfmt v1.1.3 // indirect @@ -201,7 +205,7 @@ require ( github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jonboulle/clockwork v0.4.0 - github.com/klauspost/compress v1.17.11 // indirect + github.com/klauspost/compress v1.18.0 // indirect github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect @@ -213,7 +217,7 @@ require ( github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/common v0.62.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect - github.com/riverqueue/river/riverdriver/riverpgxv5 v0.19.0 + github.com/riverqueue/river/riverdriver/riverpgxv5 v0.22.0 github.com/rs/xid v1.6.0 // indirect github.com/russellhaering/goxmldsig v1.4.0 // indirect github.com/sirupsen/logrus v1.9.3 @@ -226,7 +230,7 @@ require ( github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 // indirect go.opentelemetry.io/proto/otlp v1.5.0 // indirect - golang.org/x/sys v0.32.0 + golang.org/x/sys v0.33.0 gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect nhooyr.io/websocket v1.8.11 // indirect diff --git a/go.sum b/go.sum index f361fb87fa..6d54730acd 100644 --- a/go.sum +++ b/go.sum @@ -1,27 +1,27 @@ -cel.dev/expr v0.19.2 h1:V354PbqIXr9IQdwy4SYA4xa0HXaWq1BUPAGzugBY5V4= -cel.dev/expr v0.19.2/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw= +cel.dev/expr v0.20.0 h1:OunBvVCfvpWlt4dN7zg3FM6TDkzOePe1+foGJ9AXeeI= +cel.dev/expr v0.20.0/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.118.3 h1:jsypSnrE/w4mJysioGdMBg4MiW/hHx/sArFpaBWHdME= -cloud.google.com/go v0.118.3/go.mod h1:Lhs3YLnBlwJ4KA6nuObNMZ/fCbOQBPuWKPoE0Wa/9Vc= -cloud.google.com/go/auth v0.15.0 h1:Ly0u4aA5vG/fsSsxu98qCQBemXtAtJf+95z9HK+cxps= -cloud.google.com/go/auth v0.15.0/go.mod h1:WJDGqZ1o9E9wKIL+IwStfyn/+s59zl4Bi+1KQNVXLZ8= -cloud.google.com/go/auth/oauth2adapt v0.2.7 h1:/Lc7xODdqcEw8IrZ9SvwnlLX6j9FHQM74z6cBk9Rw6M= -cloud.google.com/go/auth/oauth2adapt v0.2.7/go.mod h1:NTbTTzfvPl1Y3V1nPpOgl2w6d/FjO7NNUQaWSox6ZMc= +cloud.google.com/go v0.121.0 h1:pgfwva8nGw7vivjZiRfrmglGWiCJBP+0OmDpenG/Fwg= +cloud.google.com/go v0.121.0/go.mod h1:rS7Kytwheu/y9buoDmu5EIpMMCI4Mb8ND4aeN4Vwj7Q= +cloud.google.com/go/auth v0.16.1 h1:XrXauHMd30LhQYVRHLGvJiYeczweKQXZxsTbV9TiguU= +cloud.google.com/go/auth v0.16.1/go.mod h1:1howDHJ5IETh/LwYs3ZxvlkXF48aSqqJUM+5o02dNOI= +cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= +cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I= cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg= -cloud.google.com/go/iam v1.4.1 h1:cFC25Nv+u5BkTR/BT1tXdoF2daiVbZ1RLx2eqfQ9RMM= -cloud.google.com/go/iam v1.4.1/go.mod h1:2vUEJpUG3Q9p2UdsyksaKpDzlwOrnMzS30isdReIcLM= +cloud.google.com/go/iam v1.5.2 h1:qgFRAGEmd8z6dJ/qyEchAuL9jpswyODjA2lS+w234g8= +cloud.google.com/go/iam v1.5.2/go.mod h1:SE1vg0N81zQqLzQEwxL2WI6yhetBdbNQuTvIKCSkUHE= cloud.google.com/go/logging v1.13.0 h1:7j0HgAp0B94o1YRDqiqm26w4q1rDMH7XNRU34lJXHYc= cloud.google.com/go/logging v1.13.0/go.mod h1:36CoKh6KA/M0PbhPKMq6/qety2DCAErbhXT62TuXALA= -cloud.google.com/go/longrunning v0.6.5 h1:sD+t8DO8j4HKW4QfouCklg7ZC1qC4uzVZt8iz3uTW+Q= -cloud.google.com/go/longrunning v0.6.5/go.mod h1:Et04XK+0TTLKa5IPYryKf5DkpwImy6TluQ1QTLwlKmY= +cloud.google.com/go/longrunning v0.6.7 h1:IGtfDWHhQCgCjwQjV9iiLnUta9LBCo8R9QmAFsS/PrE= +cloud.google.com/go/longrunning v0.6.7/go.mod h1:EAFV3IZAKmM56TyiE6VAP3VoTzhZzySwI/YI1s/nRsY= cloud.google.com/go/monitoring v1.24.0 h1:csSKiCJ+WVRgNkRzzz3BPoGjFhjPY23ZTcaenToJxMM= cloud.google.com/go/monitoring v1.24.0/go.mod h1:Bd1PRK5bmQBQNnuGwHBfUamAV1ys9049oEPHnn4pcsc= cloud.google.com/go/profiler v0.4.2 h1:KojCmZ+bEPIQrd7bo2UFvZ2xUPLHl55KzHl7iaR4V2I= cloud.google.com/go/profiler v0.4.2/go.mod h1:7GcWzs9deJHHdJ5J9V1DzKQ9JoIoTGhezwlLbwkOoCs= -cloud.google.com/go/storage v1.51.0 h1:ZVZ11zCiD7b3k+cH5lQs/qcNaoSz3U9I0jgwVzqDlCw= -cloud.google.com/go/storage v1.51.0/go.mod h1:YEJfu/Ki3i5oHC/7jyTgsGZwdQ8P9hqMqvpi5kRKGgc= +cloud.google.com/go/storage v1.54.0 h1:Du3XEyliAiftfyW0bwfdppm2MMLdpVAfiIg4T2nAI+0= +cloud.google.com/go/storage v1.54.0/go.mod h1:hIi9Boe8cHxTyaeqh7KMMwKg088VblFK46C2x/BWaZE= cloud.google.com/go/trace v1.11.3 h1:c+I4YFjxRQjvAhRmSsmjpASUKq88chOX854ied0K/pE= cloud.google.com/go/trace v1.11.3/go.mod h1:pt7zCYiDSQjC9Y2oqCsh9jF4GStB/hmjrYLsxRR27q8= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= @@ -33,8 +33,8 @@ github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0 h1:3c8yed4lgqTt+oTQ+JNMDo+F4xprBf+O/il4ZC0nRLw= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0/go.mod h1:obipzmGjfSjam60XLwGfqUkJsfiheAl+TUjG+4yzyPM= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0 h1:ErKg/3iS1AKcTkf3yixlZ54f9U1rljCkQyEXWUnIUxc= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0/go.mod h1:yAZHSGnqScoU556rBOVkwLze6WP5N+U11RHuWaGVxwY= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0 h1:fYE9p3esPxA/C0rQ0AHhP0drtPXDRhaWiwg1DPqO7IU= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0/go.mod h1:BnBReJLvVYx2CS/UHOgVz2BXKXD9wsQPxZug20nZhd0= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/trace v1.27.0 h1:Jtr816GUk6+I2ox9L/v+VcOwN6IyGOEDTSNHfD6m9sY= @@ -157,8 +157,8 @@ github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yA github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dop251/goja v0.0.0-20250309171923-bcd7cc6bf64c h1:mxWGS0YyquJ/ikZOjSrRjjFIbUqIP9ojyYQ+QZTU3Rg= github.com/dop251/goja v0.0.0-20250309171923-bcd7cc6bf64c/go.mod h1:MxLav0peU43GgvwVgNbLAj1s/bSGboKkhuULvq/7hx4= -github.com/dop251/goja_nodejs v0.0.0-20250314160716-c55ecee183c0 h1:jTwdYTGERaZ/3+glBUVQZV2NwGodd9HlkXJbTBUPLLo= -github.com/dop251/goja_nodejs v0.0.0-20250314160716-c55ecee183c0/go.mod h1:Tb7Xxye4LX7cT3i8YLvmPMGCV92IOi4CDZvm/V8ylc0= +github.com/dop251/goja_nodejs v0.0.0-20250409162600-f7acab6894b0 h1:fuHXpEVTTk7TilRdfGRLHpiTD6tnT0ihEowCfWjlFvw= +github.com/dop251/goja_nodejs v0.0.0-20250409162600-f7acab6894b0/go.mod h1:Tb7Xxye4LX7cT3i8YLvmPMGCV92IOi4CDZvm/V8ylc0= github.com/drone/envsubst v1.0.3 h1:PCIBwNDYjs50AsLZPYdfhSATKaRg/FJmDc2D6+C2x8g= github.com/drone/envsubst v1.0.3/go.mod h1:N2jZmlMufstn1KEqvbHjw40h1KyTmnVzHcSc9bFiJ2g= github.com/dsoprea/go-exif v0.0.0-20230826092837-6579e82b732d h1:ygcRCGNKuEiA98k7X35hknEN8RIRUF1jrz7k1rZCvsk= @@ -225,13 +225,13 @@ github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/ github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= -github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= -github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= +github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY= +github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= -github.com/go-asn1-ber/asn1-ber v1.5.7 h1:DTX+lbVTWaTw1hQ+PbZPlnDZPEIs0SS/GCZAl535dDk= -github.com/go-asn1-ber/asn1-ber v1.5.7/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= +github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo= +github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8= github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= @@ -242,14 +242,14 @@ github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3Bop github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= -github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE= -github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA= +github.com/go-jose/go-jose/v4 v4.1.0 h1:cYSYxd3pw5zd2FSXk2vGdn9igQU2PS8MuxrCOCl0FdY= +github.com/go-jose/go-jose/v4 v4.1.0/go.mod h1:GG/vqmYm3Von2nYiB2vGTXzdoNKE5tix5tuc6iAd+sw= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= -github.com/go-ldap/ldap/v3 v3.4.10 h1:ot/iwPOhfpNVgB1o+AVXljizWZ9JTp7YF5oeyONmcJU= -github.com/go-ldap/ldap/v3 v3.4.10/go.mod h1:JXh4Uxgi40P6E9rdsYqpUtbW46D9UTjJ9QSwGRznplY= +github.com/go-ldap/ldap/v3 v3.4.11 h1:4k0Yxweg+a3OyBLjdYn5OKglv18JNvfDykSoI8bW0gU= +github.com/go-ldap/ldap/v3 v3.4.11/go.mod h1:bY7t0FLK8OAVpp/vV6sSlpz3EQDGcQwc8pF0ujLgKvM= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= @@ -298,7 +298,6 @@ github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7a github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= @@ -342,7 +341,6 @@ github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-tpm v0.9.0 h1:sQF6YqWMi+SCXpsmS3fd21oPy/vSddwZry4JnmltHVk= @@ -378,10 +376,8 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/gorilla/schema v1.4.1 h1:jUg5hUjCSDZpNGLuXQOgIWGdlgrIdYvgQ0wZtdK1M3E= github.com/gorilla/schema v1.4.1/go.mod h1:Dg5SSm5PV60mhF2NFaTV1xuYYj8tV8NOPRo4FggUMnM= -github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= -github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= @@ -418,7 +414,6 @@ github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerX github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= @@ -441,14 +436,14 @@ github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANyt github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= -github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa h1:s+4MhCQ6YrzisK6hFJUX53drDT4UsSW3DEhKn0ifuHw= -github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa/go.mod h1:a/s9Lp5W7n/DD0VrVoyJ00FbP2ytTPDVOivvn2bMlds= +github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438 h1:Dj0L5fhJ9F82ZJyVOmBx6msDp/kfd1t9GRfny/mfJA0= +github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438/go.mod h1:a/s9Lp5W7n/DD0VrVoyJ00FbP2ytTPDVOivvn2bMlds= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgx/v5 v5.7.3 h1:PO1wNKj/bTAwxSJnO1Z4Ai8j4magtqg2SLNjEDzcXQo= -github.com/jackc/pgx/v5 v5.7.3/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ= +github.com/jackc/pgx/v5 v5.7.5 h1:JHGfMnQY+IEtGM63d+NGMjoRpysB2JBwDr5fsngwmJs= +github.com/jackc/pgx/v5 v5.7.5/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jarcoal/jpath v0.0.0-20140328210829-f76b8b2dbf52 h1:jny9eqYPwkG8IVy7foUoRjQmFLcArCSz+uPsL6KS0HQ= @@ -498,11 +493,11 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE= github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/klauspost/compress v1.11.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= -github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= -github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= -github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= +github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= +github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= @@ -553,8 +548,8 @@ github.com/minio/crc64nvme v1.0.1 h1:DHQPrYPdqK7jQG/Ls5CTBZWeex/2FMS3G5XGkycuFrY github.com/minio/crc64nvme v1.0.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg= github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= -github.com/minio/minio-go/v7 v7.0.88 h1:v8MoIJjwYxOkehp+eiLIuvXk87P2raUtoU5klrAAshs= -github.com/minio/minio-go/v7 v7.0.88/go.mod h1:33+O8h0tO7pCeCWwBVa07RhVVfB/3vS4kEX7rwYKmIg= +github.com/minio/minio-go/v7 v7.0.91 h1:tWLZnEfo3OZl5PoXQwcwTAPNNrjyWwOh6cbZitW5JQc= +github.com/minio/minio-go/v7 v7.0.91/go.mod h1:uvMUcGrpgeSAAI6+sD3818508nUyMULw94j2Nxku/Go= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= @@ -614,8 +609,8 @@ github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnh github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= -github.com/pashagolub/pgxmock/v4 v4.6.0 h1:ds0hIs+bJtkfo01vqjp0BOFirjt4Ea8XV082uorzM3w= -github.com/pashagolub/pgxmock/v4 v4.6.0/go.mod h1:9VoVHXwS3XR/yPtKGzwQvwZX1kzGB9sM8SviDcHDa3A= +github.com/pashagolub/pgxmock/v4 v4.7.0 h1:de2ORuFYyjwOQR7NBm57+321RnZxpYiuUjsmqRiqgh8= +github.com/pashagolub/pgxmock/v4 v4.7.0/go.mod h1:9L57pC193h2aKRHVyiiE817avasIPZnPwPlw3JczWvM= github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= @@ -634,8 +629,8 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= -github.com/pquerna/otp v1.4.0 h1:wZvl1TIVxKRThZIBiwOOHOGP/1+nZyWBil9Y2XNEDzg= -github.com/pquerna/otp v1.4.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg= +github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs= +github.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= @@ -669,22 +664,22 @@ github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoG github.com/rakyll/statik v0.1.7 h1:OF3QCZUuyPxuGEP7B4ypUa7sB/iHtqOTDYZXGM8KOdQ= github.com/rakyll/statik v0.1.7/go.mod h1:AlZONWzMtEnMs7W4e/1LURLiI49pIMmp6V9Unghqrcc= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= -github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0wM= -github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA= +github.com/redis/go-redis/v9 v9.8.0 h1:q3nRvjrlge/6UD7eTu/DSg2uYiU2mCL0G/uzBWqhicI= +github.com/redis/go-redis/v9 v9.8.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw= github.com/redis/rueidis v1.0.19 h1:s65oWtotzlIFN8eMPhyYwxlwLR1lUdhza2KtWprKYSo= github.com/redis/rueidis v1.0.19/go.mod h1:8B+r5wdnjwK3lTFml5VtxjzGOQAC+5UmujoD12pDrEo= -github.com/riverqueue/river v0.19.0 h1:WRh/NXhp+WEEY0HpCYgr4wSRllugYBt30HtyQ3jlz08= -github.com/riverqueue/river v0.19.0/go.mod h1:YJ7LA2uBdqFHQJzKyYc+X6S04KJeiwsS1yU5a1rynlk= -github.com/riverqueue/river/riverdriver v0.19.0 h1:NyHz5DfB13paT2lvaO0CKmwy4SFLbA7n6MFRGRtwii4= -github.com/riverqueue/river/riverdriver v0.19.0/go.mod h1:Soxi08hHkEvopExAp6ADG2437r4coSiB4QpuIL5E28k= -github.com/riverqueue/river/riverdriver/riverdatabasesql v0.19.0 h1:ytdPnueiv7ANxJcntBtYenrYZZLY5P0mXoDV0l4WsLk= -github.com/riverqueue/river/riverdriver/riverdatabasesql v0.19.0/go.mod h1:5Fahb3n+m1V0RAb0JlOIpzimoTlkOgudMfxSSCTcmFk= -github.com/riverqueue/river/riverdriver/riverpgxv5 v0.19.0 h1:QWg7VTDDXbtTF6srr7Y1C888PiNzqv379yQuNSnH2hg= -github.com/riverqueue/river/riverdriver/riverpgxv5 v0.19.0/go.mod h1:uvF1YS+iSQavCIHtaB/Y6O8A6Dnn38ctVQCpCpmHDZE= -github.com/riverqueue/river/rivershared v0.19.0 h1:TZvFM6CC+QgwQQUMQ5Ueuhx25ptgqcKqZQGsdLJnFeE= -github.com/riverqueue/river/rivershared v0.19.0/go.mod h1:JAvmohuC5lounVk8e3zXZIs07Da3klzEeJo1qDQIbjw= -github.com/riverqueue/river/rivertype v0.19.0 h1:5rwgdh21pVcU9WjrHIIO9qC2dOMdRrrZ/HZZOE0JRyY= -github.com/riverqueue/river/rivertype v0.19.0/go.mod h1:DETcejveWlq6bAb8tHkbgJqmXWVLiFhTiEm8j7co1bE= +github.com/riverqueue/river v0.22.0 h1:PO4Ula2RqViQqNs6xjze7yFV6Zq4T3Ffv092+f4S8xQ= +github.com/riverqueue/river v0.22.0/go.mod h1:IRoWoK4RGCiPuVJUV4EWcCl9d/TMQYkk0EEYV/Wgq+U= +github.com/riverqueue/river/riverdriver v0.22.0 h1:i7OSFkUi6x4UKvttdFOIg7NYLYaBOFLJZvkZ0+JWS/8= +github.com/riverqueue/river/riverdriver v0.22.0/go.mod h1:oNdjJCeAJhN/UiZGLNL+guNqWaxMFuSD4lr5x/v/was= +github.com/riverqueue/river/riverdriver/riverdatabasesql v0.22.0 h1:+no3gToOK9SmWg0pDPKfOGSCsrxqqaFdD8K1NQndRbY= +github.com/riverqueue/river/riverdriver/riverdatabasesql v0.22.0/go.mod h1:mygiHa1dnlKRjxT1//wIvfT2fMTbfXKm37NcsxoyBoQ= +github.com/riverqueue/river/riverdriver/riverpgxv5 v0.22.0 h1:2TWbVL73gipJ2/4JNCQbifaNj+BCC/Zxpp30o1D8RTg= +github.com/riverqueue/river/riverdriver/riverpgxv5 v0.22.0/go.mod h1:TZY/BG8w/nDxkraAEvvgyVupIz0b4+PQVUW0kIiy1fc= +github.com/riverqueue/river/rivershared v0.22.0 h1:hLPHr98d6OEfmUJ4KpIXgoy2tbQ14htWILcRBHJF11U= +github.com/riverqueue/river/rivershared v0.22.0/go.mod h1:BK+hvhECfdDLWNDH3xiGI95m2YoPfVtECZLT+my8XM8= +github.com/riverqueue/river/rivertype v0.22.0 h1:rSRhbd5uV/BaFTPxReCxuYTAzx+/riBZJlZdREADvO4= +github.com/riverqueue/river/rivertype v0.22.0/go.mod h1:lmdl3vLNDfchDWbYdW2uAocIuwIN+ZaXqAukdSCFqWs= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= @@ -725,8 +720,8 @@ github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4k github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= github.com/sony/gobreaker/v2 v2.1.0 h1:av2BnjtRmVPWBvy5gSFPytm1J8BmN5AGhq875FfGKDM= github.com/sony/gobreaker/v2 v2.1.0/go.mod h1:dO3Q/nCzxZj6ICjH6J/gM0r4oAwBMVLY8YAQf+NTtUg= -github.com/sony/sonyflake v1.2.0 h1:Pfr3A+ejSg+0SPqpoAmQgEtNDAhc2G1SUYk205qVMLQ= -github.com/sony/sonyflake v1.2.0/go.mod h1:LORtCywH/cq10ZbyfhKrHYgAUGH7mOBa76enV9txy/Y= +github.com/sony/sonyflake v1.2.1 h1:Jzo4abS84qVNbYamXZdrZF1/6TzNJjEogRfXv7TsG48= +github.com/sony/sonyflake v1.2.1/go.mod h1:LORtCywH/cq10ZbyfhKrHYgAUGH7mOBa76enV9txy/Y= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs= @@ -740,23 +735,20 @@ github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnIn github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.20.0 h1:zrxIyR3RQIOsarIrgL8+sAvALXul9jeEPa06Y0Ph6vY= -github.com/spf13/viper v1.20.0/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= +github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4= +github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= +github.com/spiffe/go-spiffe/v2 v2.5.0 h1:N2I01KCUkv1FAjZXJMwh95KK1ZIQLYbPfhaxw8WS0hE= +github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g= github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stvp/tempredis v0.0.0-20181119212430-b82af8480203 h1:QVqDTf3h2WHt08YuiTGPZLls0Wq99X9bWd0Q5ZSBesM= @@ -778,8 +770,8 @@ github.com/ttacon/builder v0.0.0-20170518171403-c099f663e1c2 h1:5u+EJUQiosu3JFX0 github.com/ttacon/builder v0.0.0-20170518171403-c099f663e1c2/go.mod h1:4kyMkleCiLkgY6z8gK5BkI01ChBtxR0ro3I1ZDcGM3w= github.com/ttacon/libphonenumber v1.2.1 h1:fzOfY5zUADkCkbIafAed11gL1sW+bJ26p6zWLBMElR4= github.com/ttacon/libphonenumber v1.2.1/go.mod h1:E0TpmdVMq5dyVlQ7oenAkhsLu86OkUl+yR4OAxyEg/M= -github.com/twilio/twilio-go v1.24.1 h1:bpBL1j5GRdJGSG+tCdo0O94BwK4uDOHQuNT5ndzljPg= -github.com/twilio/twilio-go v1.24.1/go.mod h1:zRkMjudW7v7MqQ3cWNZmSoZJ7EBjPZ4OpNh2zm7Q6ko= +github.com/twilio/twilio-go v1.26.1 h1:HazQUV+BCuW5CaJVMTjqV22V32LirwZNQBu98ADPQzM= +github.com/twilio/twilio-go v1.26.1/go.mod h1:FpgNWMoD8CFnmukpKq9RNpUSGXC0BwnbeKZj2YHlIkw= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= @@ -796,17 +788,18 @@ github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913/go.mod h1:4aEEwZQut github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M= github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= +github.com/zeebo/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM= +github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4= github.com/zenazn/goji v1.0.1 h1:4lbD8Mx2h7IvloP7r2C0D6ltZP6Ufip8Hn0wmSK5LR8= github.com/zenazn/goji v1.0.1/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= github.com/zitadel/exifremove v0.1.0 h1:qD50ezWsfeeqfcvs79QyyjVfK+snN12v0U0deaU8aKg= github.com/zitadel/exifremove v0.1.0/go.mod h1:rzKJ3woL/Rz2KthVBiSBKIBptNTvgmk9PLaeUKTm+ek= github.com/zitadel/logging v0.6.2 h1:MW2kDDR0ieQynPZ0KIZPrh9ote2WkxfBif5QoARDQcU= github.com/zitadel/logging v0.6.2/go.mod h1:z6VWLWUkJpnNVDSLzrPSQSQyttysKZ6bCRongw0ROK4= -github.com/zitadel/oidc/v3 v3.36.1 h1:1AT1NqKKEqAwx4GmKJZ9fYkWH2WIn/VKMfQ46nBtRf0= -github.com/zitadel/oidc/v3 v3.36.1/go.mod h1:dApGZLvWZTHRuxmcbQlW5d2XVjVYR3vGOdq536igmTs= +github.com/zitadel/oidc/v3 v3.37.0 h1:nYATWlnP7f18XiAbw6upUruBaqfB1kUrXrSTf1EYGO8= +github.com/zitadel/oidc/v3 v3.37.0/go.mod h1:/xDan4OUQhguJ4Ur73OOJrtugvR164OMnidXP9xfVNw= github.com/zitadel/passwap v0.9.0 h1:QvDK8OHKdb73C0m+mwXvu87UJSBqix3oFwTVENHdv80= github.com/zitadel/passwap v0.9.0/go.mod h1:6QzwFjDkIr3FfudzSogTOx5Ydhq4046dRJtDM/kX+G8= github.com/zitadel/saml v0.3.5 h1:L1RKWS5y66cGepVxUGjx/WSBOtrtSpRA/J3nn5BJLOY= @@ -820,8 +813,8 @@ go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/contrib/detectors/gcp v1.34.0 h1:JRxssobiPg23otYU5SbWtQC//snGVIM3Tx6QRzlQBao= -go.opentelemetry.io/contrib/detectors/gcp v1.34.0/go.mod h1:cV4BMFcscUR/ckqLkbfQmF0PRsq8w/lMGzdbCSveBHo= +go.opentelemetry.io/contrib/detectors/gcp v1.35.0 h1:bGvFt68+KTiAKFlacHW6AhA56GF2rS0bdD3aJYEnmzA= +go.opentelemetry.io/contrib/detectors/gcp v1.35.0/go.mod h1:qGWP8/+ILwMRIUf9uIVLloR1uo5ZYAslM4O6OqUi1DA= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU= @@ -834,8 +827,8 @@ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 h1:m639+ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0/go.mod h1:LjReUci/F4BUyv+y4dwnq3h/26iNOeC3wAIqgvTIZVo= go.opentelemetry.io/otel/exporters/prometheus v0.57.0 h1:AHh/lAP1BHrY5gBwk8ncc25FXWm/gmmY3BX258z5nuk= go.opentelemetry.io/otel/exporters/prometheus v0.57.0/go.mod h1:QpFWz1QxqevfjwzYdbMb4Y1NnlJvqSGwyuU0B4iuc9c= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.29.0 h1:WDdP9acbMYjbKIyJUhTvtzj601sVJOqgWdUxSdR/Ysc= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.29.0/go.mod h1:BLbf7zbNIONBLPwvFnwNHGj4zge8uTCM/UPIVW1Mq2I= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.35.0 h1:PB3Zrjs1sG1GBX51SXyTSoOTqcDglmsk7nT6tkKPb/k= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.35.0/go.mod h1:U2R3XyVPzn0WX7wOIypPuptulsMcPDPs/oiSVOMVnHY= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.35.0 h1:T0Ec2E+3YZf5bgTNQVet8iTDW7oIk03tXHq+wkwIDnE= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.35.0/go.mod h1:30v2gqH+vYGJsesLWFov8u47EpYTcIQcBjKpI6pJThg= go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= @@ -855,8 +848,8 @@ go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= -go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= +go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko= +go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= @@ -875,19 +868,13 @@ golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= -golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= -golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= -golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= -golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= -golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= -golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= +golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= +golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20200331195152-e8c3332aa8e5/go.mod h1:4M0jN8W1tt0AVLNr8HDosyJCDCDuyL9N9+3m7wDWgKw= -golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw= -golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM= +golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI= +golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20200927104501-e162460cd6b5/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= @@ -903,11 +890,6 @@ golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzB golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -926,7 +908,6 @@ golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200320220750-118fecf932d8/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -938,24 +919,15 @@ golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/ golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= -golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= -golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= -golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= -golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= -golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= +golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc= -golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= +golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= +golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -964,14 +936,8 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= -golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= -golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= +golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1002,44 +968,20 @@ golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= -golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= -golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= -golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= -golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= -golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= -golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= +golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= +golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= @@ -1064,17 +1006,13 @@ golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= -google.golang.org/api v0.227.0 h1:QvIHF9IuyG6d6ReE+BNd11kIB8hZvjN8Z5xY5t21zYc= -google.golang.org/api v0.227.0/go.mod h1:EIpaG6MbTgQarWF5xJvX0eOJPK9n/5D4Bynb9j2HXvQ= +google.golang.org/api v0.233.0 h1:iGZfjXAJiUFSSaekVB7LzXl6tRfEKhUN7FkZN++07tI= +google.golang.org/api v0.233.0/go.mod h1:TCIVLLlcwunlMpZIhIp7Ltk77W+vUSdUKAAIlbxY44c= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -1089,10 +1027,10 @@ google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEY google.golang.org/genproto v0.0.0-20210126160654-44e461bb6506/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb h1:ITgPrl429bc6+2ZraNSzMDk3I95nmQln2fuPstKwFDE= google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:sAo5UzpjUwgFBCzupwhcLcxHVDK7vG5IqI30YnwX2eE= -google.golang.org/genproto/googleapis/api v0.0.0-20250313205543-e70fdf4c4cb4 h1:IFnXJq3UPB3oBREOodn1v1aGQeZYQclEmvWRMN0PSsY= -google.golang.org/genproto/googleapis/api v0.0.0-20250313205543-e70fdf4c4cb4/go.mod h1:c8q6Z6OCqnfVIqUFJkCzKcrj8eCvUrz+K4KRzSTuANg= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4 h1:iK2jbkWL86DXjEx0qiHcRE9dE4/Ahua5k6V8OWFb//c= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I= +google.golang.org/genproto/googleapis/api v0.0.0-20250512202823-5a2f75b736a9 h1:WvBuA5rjZx9SNIzgcU53OohgZy6lKSus++uY4xLaWKc= +google.golang.org/genproto/googleapis/api v0.0.0-20250512202823-5a2f75b736a9/go.mod h1:W3S/3np0/dPWsWLi1h/UymYctGXaGBM2StwzD0y140U= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250505200425-f936aa4a68b2 h1:IqsN8hx+lWLqlN+Sc3DoMy/watjofWiU8sRFgQ8fhKM= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250505200425-f936aa4a68b2/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM= @@ -1107,8 +1045,8 @@ google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8 google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= google.golang.org/grpc v1.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= -google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg= -google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= +google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA= +google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -1119,8 +1057,8 @@ google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2 google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= -google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/internal/api/oidc/error.go b/internal/api/oidc/error.go index 781a1d3815..829a15d4d2 100644 --- a/internal/api/oidc/error.go +++ b/internal/api/oidc/error.go @@ -42,10 +42,7 @@ func oidcError(err error) error { if statusCode < 500 { newOidcErr = oidc.ErrInvalidRequest } - return op.NewStatusError( - newOidcErr(). - WithParent(err). - WithDescription(zError.GetMessage()), - statusCode, - ) + oidcErr := newOidcErr().WithParent(err) + oidcErr.Description = zError.GetMessage() + return op.NewStatusError(oidcErr, statusCode) } diff --git a/internal/protoc/protoc-gen-authoption/main.go b/internal/protoc/protoc-gen-authoption/main.go index b0e78c6990..1d6ae21b77 100644 --- a/internal/protoc/protoc-gen-authoption/main.go +++ b/internal/protoc/protoc-gen-authoption/main.go @@ -88,7 +88,7 @@ func main() { } // Write the response to stdout, to be picked up by protoc - fmt.Fprintf(os.Stdout, string(out)) + fmt.Fprint(os.Stdout, string(out)) } func loadTemplate(templateData []byte) *template.Template { diff --git a/internal/query/auth_request_test.go b/internal/query/auth_request_test.go index 152a032cd8..12288d18cc 100644 --- a/internal/query/auth_request_test.go +++ b/internal/query/auth_request_test.go @@ -5,7 +5,6 @@ import ( "database/sql" "database/sql/driver" _ "embed" - "fmt" "regexp" "testing" "time" @@ -22,9 +21,7 @@ import ( ) func TestQueries_AuthRequestByID(t *testing.T) { - expQuery := regexp.QuoteMeta(fmt.Sprintf( - authRequestByIDQuery, - )) + expQuery := regexp.QuoteMeta(authRequestByIDQuery) cols := []string{ projection.AuthRequestColumnID, diff --git a/internal/query/saml_request_test.go b/internal/query/saml_request_test.go index 3a062ac5fd..019054d4fc 100644 --- a/internal/query/saml_request_test.go +++ b/internal/query/saml_request_test.go @@ -5,7 +5,6 @@ import ( "database/sql" "database/sql/driver" _ "embed" - "fmt" "regexp" "testing" @@ -20,9 +19,7 @@ import ( ) func TestQueries_SamlRequestByID(t *testing.T) { - expQuery := regexp.QuoteMeta(fmt.Sprintf( - samlRequestByIDQuery, - )) + expQuery := regexp.QuoteMeta(samlRequestByIDQuery) cols := []string{ projection.SamlRequestColumnID, From 6b07e57e5c8caab0b4d95ceef0db579c61b756ca Mon Sep 17 00:00:00 2001 From: Stefan Benz <46600784+stebenz@users.noreply.github.com> Date: Tue, 20 May 2025 09:32:09 +0200 Subject: [PATCH 36/76] test: fix list orgs test with sort (#9909) # Which Problems Are Solved List organization integration test fails sometimes due to incorrect sorting of results. # How the Problems Are Solved Add sorting column to request on list organizations endpoint and sort expected results. # Additional Changes None # Additional Context None --- internal/api/grpc/org/v2/integration_test/query_test.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/internal/api/grpc/org/v2/integration_test/query_test.go b/internal/api/grpc/org/v2/integration_test/query_test.go index 86a2bd312b..cb7576455c 100644 --- a/internal/api/grpc/org/v2/integration_test/query_test.go +++ b/internal/api/grpc/org/v2/integration_test/query_test.go @@ -5,7 +5,9 @@ package org_test import ( "context" "fmt" + "slices" "strconv" + "strings" "testing" "time" @@ -89,6 +91,7 @@ func TestServer_ListOrganizations(t *testing.T) { Queries: []*org.SearchQuery{ OrganizationIdQuery(Instance.DefaultOrg.Id), }, + SortingColumn: org.OrganizationFieldName_ORGANIZATION_FIELD_NAME_NAME, }, func(ctx context.Context, request *org.ListOrganizationsRequest) ([]orgAttr, error) { count := 3 @@ -101,6 +104,10 @@ func TestServer_ListOrganizations(t *testing.T) { request.Queries = []*org.SearchQuery{ OrganizationNamePrefixQuery(prefix), } + + slices.SortFunc(orgs, func(a, b orgAttr) int { + return -1 * strings.Compare(a.Name, b.Name) + }) return orgs, nil }, }, From 7861024ea2913a871ae62eaf9bc2e1a82dda78db Mon Sep 17 00:00:00 2001 From: "Marco A." Date: Tue, 20 May 2025 14:21:30 +0200 Subject: [PATCH 37/76] docs: fix Go backend example (#9864) # Which Problems Are Solved This PR aims to clarify how to use the zitadel SDK with OAuth token introspection. # How the Problems Are Solved Reworked the setup process on console needed to create the JSON key and a PAT. # Additional Changes - Closes #5559 --- docs/docs/examples/secure-api/go.md | 74 +++++++++++++----- docs/static/img/go/api-PAT_creation.png | Bin 0 -> 54389 bytes docs/static/img/go/api-PAT_view.png | Bin 0 -> 139720 bytes docs/static/img/go/api-app_details.png | Bin 0 -> 213443 bytes docs/static/img/go/api-create-auth.png | Bin 103599 -> 0 bytes docs/static/img/go/api-create-key.png | Bin 31504 -> 0 bytes docs/static/img/go/api-create.png | Bin 182604 -> 0 bytes docs/static/img/go/api-create_application.png | Bin 0 -> 135770 bytes .../static/img/go/api-create_service_user.png | Bin 0 -> 43016 bytes docs/static/img/go/api-download_key.png | Bin 0 -> 66369 bytes docs/static/img/go/api-expiration_date.png | Bin 0 -> 65018 bytes docs/static/img/go/api-new_key.png | Bin 0 -> 58790 bytes docs/static/img/go/api-select_framework.png | Bin 0 -> 85996 bytes docs/static/img/go/api-select_jwt.png | Bin 0 -> 119147 bytes docs/static/img/go/api-service_user_panel.png | Bin 0 -> 116026 bytes 15 files changed, 55 insertions(+), 19 deletions(-) create mode 100644 docs/static/img/go/api-PAT_creation.png create mode 100644 docs/static/img/go/api-PAT_view.png create mode 100644 docs/static/img/go/api-app_details.png delete mode 100644 docs/static/img/go/api-create-auth.png delete mode 100644 docs/static/img/go/api-create-key.png delete mode 100644 docs/static/img/go/api-create.png create mode 100644 docs/static/img/go/api-create_application.png create mode 100644 docs/static/img/go/api-create_service_user.png create mode 100644 docs/static/img/go/api-download_key.png create mode 100644 docs/static/img/go/api-expiration_date.png create mode 100644 docs/static/img/go/api-new_key.png create mode 100644 docs/static/img/go/api-select_framework.png create mode 100644 docs/static/img/go/api-select_jwt.png create mode 100644 docs/static/img/go/api-service_user_panel.png diff --git a/docs/docs/examples/secure-api/go.md b/docs/docs/examples/secure-api/go.md index ab45e2a6b7..90fe7830be 100644 --- a/docs/docs/examples/secure-api/go.md +++ b/docs/docs/examples/secure-api/go.md @@ -10,26 +10,63 @@ At the end of the guide you should have an API with a protected endpoint. > This documentation references our HTTP example. There's also one for GRPC. Check them out on [GitHub](https://github.com/zitadel/zitadel-go/blob/next/example/api/http/main.go). -## Set up application and obtain keys - -Before we begin developing our API, we need to perform a few configuration steps in the ZITADEL Console. -You'll need to provide some information about your app. We recommend creating a new app to start from scratch. Navigate to your Project, then add a new application at the top of the page. -Select the **API** application type and continue. - -![Create app in console](/img/go/api-create.png) - -We recommend that you use JWT Profile for authenticating at the Introspection Endpoint. - -![Create app in console](/img/go/api-create-auth.png) - -Then create a new key with your desired expiration date. Be sure to download it, as you won't be able to retrieve it again. - -![Create api key in console](/img/go/api-create-key.png) - ## Prerequisites This will handle the OAuth 2.0 introspection request including authentication using JWT with Private Key using our [OIDC client library](https://github.com/zitadel/oidc). -All that is required, is to create your API and download the private key file later called `Key JSON` for the service user. +All that is required, is to create your API, create a private key and a personal access token for a service user. + +### Set up application and obtain keys + +Before we begin developing our API, we need to perform a few configuration steps in the ZITADEL Console. +You'll need to provide some information about your app. We recommend creating a new app to start from scratch. + +Starting from the homepage of your console, click on Create Application + +![Create app in homepage](/img/go/api-create_application.png) + +Select a project from the dropdown and select *Other* as framework, then continue. + +![Framework Selection](/img/go/api-select_framework.png) + +Add your app name and select *API* as application type, then continue. + +![Application Type](/img/go/api-app_details.png) + +We recommend that you use JWT Profile for authenticating at the Introspection Endpoint. So select *JWT* as authentication method + +![JWT authentication method](/img/go/api-select_jwt.png) + +You then need to create a new JSON key. + +![New JSON key](/img/go/api-new_key.png) + +Select an expiration date that suits you. + +![Key expiration date](/img/go/api-expiration_date.png) + +And make sure to download it, as you won't be able to retrieve it again. + +![Key download](/img/go/api-download_key.png) + +Now we need to create a *Personal Access Token* to authenticate the client requests. + +On the user view, switch to *Service Users* and create a new one. + +![Service User Panel](/img/go/api-service_user_panel.png) + +Give the service user a name and a user name. Select `Bearer` as *Access Token Type*. + +![Service User Creation](/img/go/api-create_service_user.png) + +### Create service user and personal access token (PAT) + +Once done, from the left panel of the user management, click on Personal Access Token and create a new one. + +![Personal Access Token View](/img/go/api-PAT_view.png) + +Set an expiration date and then copy the PAT generated to somewhere safe. We will need it later. + +![PAT creation](/img/go/api-PAT_creation.png) ## Go Setup @@ -119,8 +156,7 @@ Content-Length: 44 unauthorized: authorization header is empty ``` -Get a valid access_token for the API. You can either achieve this by getting an access token with the project_id in the audience -or use a PAT of a service account. +We need to use the personal access token generated previously. If you provide a valid Bearer Token: diff --git a/docs/static/img/go/api-PAT_creation.png b/docs/static/img/go/api-PAT_creation.png new file mode 100644 index 0000000000000000000000000000000000000000..b8d2032f05fa4d5450c3e5063969573c2a68f25f GIT binary patch literal 54389 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 literal 0 HcmV?d00001 diff --git a/docs/static/img/go/api-PAT_view.png b/docs/static/img/go/api-PAT_view.png new file mode 100644 index 0000000000000000000000000000000000000000..c516f2ae2f5d68508c1e8b890ebe976e44c76baa GIT binary patch literal 139720 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 z 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 U>ZS;7r-O%|Gdo*gc;oK>0Q@Sw_y7O^ literal 0 HcmV?d00001 diff --git a/docs/static/img/go/api-app_details.png b/docs/static/img/go/api-app_details.png new file mode 100644 index 0000000000000000000000000000000000000000..730152e937bd8d16446fd92b8236a835d7ddb9f2 GIT binary patch literal 213443 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%0A;f>8 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 z zJGxi}n?f9oMr&2MJ=^TXoA|K|ZCE=AD#`(;l}!E9rMLcqdb5Ckzi@92M0B3=qKLWR zjGfd}diO^3({F77iE8LU^7ny`4H|5~Sowkv*L*Kq(Jwk6-U1mFQlJ^JPKwPeN0&65=bYoCx(PQx?pR;6{Zc>;bMQAs+Hf4;Mx*^C_i#7nJA(#`(5#@>+YsxD}@spMIrm~dr|9=|h6ac@M- zxYDC4bnWx$uJpo(dHqS1H8S*5RF6#QZZCy@1k0vYayWS1i~4^NiHPqNH@@%G%-!N0 znC`rgsz5YPRo)le*evxT2_8F!ecFRt8nO;BbMUL~weGeKG<}3>H>-zoJwf?%Cp>7C zHK~f1XS>H-d_Gf|whw0rJ#Tnqepy3#2g&a@H;s(%+Or;u7@2Fuo%3DRcmk52S4$t( zy>Rt^w25?#^wH~S`mU7KTt4`yY~bo@yQ!2t$JD#`HhF$y`pxTQ8eWZE9XC5!tuZuo z@;wrTUNYu`38@`D;&kHD{PI0J?++@m*c|6dOtJn5Z-=#Px&tz;*Hg*G`-Om5q2Gvd zpj)_@VtH7D$uE&s?FdNrNIDFtTp^)Gv2-2|*&`kd9!`y8Je!qgo@u`moXFCquuxHK z6^Sqki|$?QAO|38VZ!i$aM)W4*6v|yP($K`*Lp1EsC#(v6QtX{#uGXu-ayJ);uDx& z5jw$too$j(RKu1*_&pc$bFS#!L=mo43^l=L95At1+p+m1D5SRH?#k;E&RB0RaaY;33Iy%i=#Zzs zOE9e(%ld)3myYeOTrH$wpJPxS;vW=7%i#2$9N|pZ?0?%&5X#Vk6H*k@Es23lt~`9 zg}#an*=DDkz4vzE6M0A5X7$6Rvwc4Yy%55$_PF1wF{R$$h!$Ax9u*0(C9Rg`3dJm9 z&<`)^12k{QMTa#0IeohxMF3;TPn78n!^>yWH8bH08v~2o^q*U~HwCs#i^_%-p1k|M zIQFnV#G;+bGBhO>S+}t==SNB_Ajz)~WCArdc|#n=+f}xc5MBO@S{usXCtv+H>DRVS zM{cq`E_a3Q9_tG;9r^KEo&>YF)MnX|2)2Z8GJ#Dy55an^9z=_HTz+Bh;xmaW5xp(+-cWuGedviaKlHMN6MnqQjaoRWtnk9`TPd706tMJy(G%?Hmk+FLE~ zl=toM&W&|jwM{1~rd)7sMq*_GX@}gA2IJ8O#iWXXfGo^ri1J5|%@HIvtA3L4eaH|c z@h54G_FD$&do-b7O*`!;Ck5E#bC{cq>E6@cguS%n!sGad3&+8#|8(wC?hsQ5xIHoG3v(EFtB5><1Y6Y z;^VTRMs6i0O^z^_14P^TN@dSpz|H^Lu+Y{Is`J<(cB*ZU)gLy~< zKHv0$1QsI66F^z^TD;BlR;)QTu0ydyev@suR@7HZOprE2-f0%0`wVo%F{$ zqW)f^*V9-5r2mL7?`)iotKtA|slIwcuv)0aO0L=U@})c-c=WoS>H3UiZ0#nA7*OKA zX}yQ>#d9BQ>KrhZUibKP51ZPXTl5{|bi@`m}h#N>3UrickV`l|9Y4LkD{K*^mnQ;|hN7 zNy`wu;m1$c*KUni<(}q`enhMU$!Nc&3TE?3=9Z<^`K#SS=Np6+re*tKPq%6}MKS)t zAsGjfYfV*kGt5KQpW@i&KLb%eX)aTT$;edvAP4-{0YPz;pPBkk{r_X|!F9Nt^48K) z|F6$6Q;8eqaD2mJC|HLOm2woiY?bh+TD$?NuLg2~Z9Q4(Kp-HR z3G9~`1R0vrWTy~R1z3&G9H|^N((5XI29msh<#Bdze%0>%xH`|;ugoXuzB#+-RkKe4 z^L8UX7(PM(2`Y|?-G9v=kL1RCrX8yMKRaeGr(KY%u)Lcs8D#8D$eSZ@@SrCbS`sIreqO^|}9l0-nVRt!t)xEwY?VYNW zZQ@*jcC$E~^!p!PP4qli%Ongez+C-mExGxJ{lGSecx*lu5Rxot=4r|z+nL_Way=(#+TQ3frn}VO6z3L#B+6l@uEu?keHV67=N(2_Nv_S~A#9ankfbN6OJNI@-m1 z3!oXmLuQkUwGuk+E7LaMi$Ek49|&auT=QXTSW)hG|5*r^V39f4n#rkARsQp+SaZ3^ z-`ktgjQCIs69K8!2c_RU(HStR$h@4#Pg0iMe+vsSLwbr=apHE(3FyZ&Qo9EUAfN`Db2tb%4mQs|(_r8OQdhO2>B77>%*$<}<$xxE zad-gFvXCDYmqz<6^wh=-`&yD?(jw4@m!nE8zcU6c9a~-nzE9pu{$ct794+-{v-bK< zndyjXcEQUE$W5W4se@T2FcK=j3WyN{cgCa)%2q0|IP~fmHlTB8?zbDZKfuuQq9NR( zE#7?S*gjR>F4ks8H)T1_hCsIUFKMX{zxEy#D+uA3F0jjV+l@0bQuCq2a-FwB(8rmNqhkq+IwX;W2?_gSgEu5I8 z^^1z%;T0>s)}v)o5thvYq$Wj2Z0bt zahl$s09mQJi)P1q=FC2mHVn#{!*DEVAs2MbI$;}m=*W{-LA+K$Nd?VH$k(Mz_1i6p zjaTR>R4><=beMNEzO32kDiEc&L8M`O^i$U~=l68ZJkpD~!<&Xjx*-64{3N$u_Jpx4E5UaSK*8f~XifA?{n!L&bd-8M4hxrHw7 zyUm4uT`q(!))Hlk4?K!2YfQ5*D`!ab4J7KsynNRW5qV|pb4|WVJ`CMGT6W_B{ zm!hjF($@!zO{wkfnmY`q43jK5Dw09ln{Obint!%1;%xn-I8BYcaB)Lh2={Ti@}= z`XQ-`kV{tntD8<=L-Ldl(1hN-$|Azz@J>U+P5*vfe`ydCkjh?GoV$++{uYcs(i$C5>7el#lR#=$^i02_9(;^bjRGBU=Z zP|P0NI(n;|g(FRGmnG2sgvB5eE#K}A9Ws0+%nv1-$~6UBKbejeU0a?NOvfk~BUzk} z?U>`3qKl9(YSCi7i+IUG7$ew@DAzNyqEcB<_d zT)97KF53{N4NYHH{myOIE8(`xF6$?#kY28aFrq0Yj^?Oeilc((cG?brn|DZ@2{@SQ z=7%oLBH%(JJ3NYw?o*hVwuzy%se)5gtM(2|?L*?TrjKhg4GPi}U=jOg#$}x^$12u( z90~C_Q**r8!54elR%pQZ#7fv^aHmqUz&*2fy1J#pR zTWJ{L3I91n;Khi7+HEfwkMdR)X?77)FTuC)5Xu{96X}qXos(6az+Bw>=*KB71OV7@^($%e4`AXa&G$h8)uiW`wVTbwXk~cJXs)arxi_O(F;JLs+ zbo})Xii7`$uD6bgvTeVHkyI&B6p*3hMkJ&Kh7?dxQjnBVhmvOK91svChAu%+DQQ8v zLztmKBu9Ga?igU+6QBF}eQ&Jq-*GKj*L9ue5&PK3-XV#b(^ft~XC2YyYPptT8cS8- z%>(BhS5o|3d(o7o=a=W{^9TfqCR+0@Mv^jKIq#DS zanVY}+I*g8<Q<(a{MP>3uXtj8 zy)OTVj}Ry`5@7aIr4S#M!8kk!+-}}S;aiJfn8XB^^mTd#sE}He6BQaIL9;nX_pQW z24)!=ZnNH&B&cqBdfVo3HD9U9WABjBI1WbRN{7fjumC^W#uOskqcE-fd3qY5;F4*x~IQhE?))oE#jK<>T27*N?jg8K7CQ) zo~W#=x9PC4Ua>PJdVg)JSg{l)Z<0uSAluj}qUnGyswc`fO*tT_t5XV$c2MVMUkj;N zh%>95f{Z03(J{C#YBVax8CvQoxlc~G zByr@oZmdV2irI?L&zRw9i&Gnrdg`XAnJNYoAD8|9#c!cemP_7^uf8Y+%@(c7^fLD zcTU6gq@IT>c13&6z^>M)+=jn8ZAJmM6%91++M+BRwO!Q@C@iF2NGaY`BA6V5m}gm! zz{Vv<6`a0@Fz75CZ97kw@5_eeB_Sepsp)eK=68(IGg6gUYjvlN&fCJ~dsEPkX zny;OzoOI@onAQ)qPu2C=&etr{!~-!r;aAQ^L!*kZu+)a5OHyaE#ZeRKvd4C7;Hqsd zk>r!7pFK9uvh7yBEYd5;ozq7cej}ma765n>$S=pl3>i8L{S!O)6#w-nfy3y5;<48UmNm4)?23V#o(n~?F`ge#bQq8Z{!qj5% z*!HUG!G{XgaZJUCu%^ki#>0S8^uyhxtGu5T4A&Zc>yOFYPph|VWjHELd$T>H>YK;R zYS+qLrYc>#VLdb{R+6PaMGDF>VGqEkJi{t z8t`n<@R?F7_JHntpwtP0zZrL zyNtjgj)MywhH)Yun|ITd?a3nUm#Vq;#R^X4F3p$4ZxPeP84WxG5Lw2i=UpWWclX#b z&~RjgnU;?8;$ySJ`TAs!y{LxxsoFz4jIyob& z=ZVvLi?)__R1RL+*eu$Anv!b2a_OO$ta>4ZwlFk}sm|Y$SQUDI!NY5xxRPq)r3tNv zOF4S7N;13nh~%NkV@&!_AM3s+6uhWcX0idfPP{_bMfKm%OMbG+eQDWf za`M}B1?E)1RA-tf*& zpKTVd>{0YhTW3tDYs_MZ&m6v2Vp{3^Qqc@&xtr}B*Ep4PE6EWNuhX(n+0?rHfX@;G z1Bse!q)+VpuzS1F6_X>3a~Kt~tgw8mk897aD5YqJhAgQfWwtZ_^r?-|c~Ny%KDS;> z%1r+;pFUOyHz~P(|B-r!x5GXqGnl0tOk2!VEZkD5vX)(c_|gx z`rA|hu#brbRU&@gxew;k|Fcwl-=vF>H8;gj$VFO zoXe=Y$yHf~@vn(9XdI31Gd#wb;)j|~d#`M95GMHe1dQ?UzqBRT#X`|~JW9AECR3y{W{N)^H`_bs6NfUHa;k4Wri3%Jo~TAiPIs03Jy#MbKMLP1G8~%AOWSd=jfe2Bsr+h5Fpz)pb^qr_YRqNP7y9iQ zkycEGvDU|5&?Uvvsj2m3gZZ3E*U6P13%35KMBL2bM_kLE;r9q2OhJ@%SAR{I8kTg>yFjZo2Chh_{tC3-YOECc&J50bR=lXfW=i>HPhqVx z0!3plifZ#TEpMyiHkjjnxpH@-EhBPj>vb5CKikLb2_ynaNcQjVfWRCi%ZF+C&O%J)(lc(dPjK3&-{dzB<(O~5x z9f^IBgQKTsuq7ScQMm@5^!#M%6myiEe#Yq0!dGo9o%7a0e&-F=t$*}R&wMNAb#FR! zZSd0tktv;?KAbLLxm<_LNyQH+sk%{nCS2@kN(YTPKKg$esbteXl68@w=Ir3l-Ra7* zx6rFf9lJ5VML3Z*xWVM+=ZmZjAsgOo-3VSSE3G`-oIPF9w6o~Glxiy79J}guWs}Cg zR*C;vepn3a3V!XEw~UI^b}OwJ{O3XK7ya3YydGQRvM{s;_AD3v^pcX!V!2Ax+}hH$ zr0{?@XRn-gunZRb6QpUsl4(S{p6{Nfvd-iV<;9A{>5I|}1b9+1${87*U-Q$3jx;(bLAV|Ph0s! zsh`{R#c&VaXE9%5Mp`haQaf~siyZ$yn7&IeRRLN?t_%Tp!s9zccsR(7?ErSa|9l_S;n*MvnX}%T!AFm=;yFgE@I*h{fBSq@i)y^ghva zs-T1A^}a>1N0{IPrn#E5zU|?a`4|!A6c1-?^V;=IW+v~W2pY=*=)A0w$Jh0HSbFbT zY}HXWjWteyc0c}Rm;Py@nQhpe5l_1 z=i)Nkfp`h|!)15;7unEse6VQ|g68a5VI-noHHw9qnhfdcs&{!zT}4BwP=ElGn0Ky* z&&fJo$aFl!{I|=Q-!%*h)Hsb6a&W07ha#13K*FgVAJpdgLeqN+A9Wn}#6AhI?^|8~ zhvf+^J7_vFvR8@6$$VMU0yO&f>!_NcF zKX8I}B|Xt%S?YZ2tcdk0G*5f`!F1469vfGt(S!bU_|Od^RbKNv%f8}gs+OQGoOb*n z^M{rZ5$ z*@EH$JC`X~aJ{V;4j*FU$X^Gh?VZ4ei|_tGaLf?5j%|ESJkY*V0eLuPXg{XM#VG!* z_t&bcmPVOqL+xFe*o1tOV<)t6sh6O-uuM}S-8z40n(P^bFJ0PE^5@6V>;KcGoCtpY zg^=W9KhkNmUe+ufw`s6_Z&>;<#yK6T&9lyDeC5sFs}da((1b)&^|9{z?#l|} zY@Yp+7ppzX^9zG1{SJu>aZ`|)u!_M*_|QXd^X4oJhTC8Q0cFma|G%3Pb-n5p&^bgR zskYTv&RO&ARDDsEeqB4yzxsasS->VklA_Ey2^|`}gW+0U{uW=Z-!SNw7&?JMph16y zSqct3$kc{-Pj=m{qqd^$JwMWf=9<|*c93BG7XyVW!Hb2Xdc#6 z)cS_pd>jzQ_;q}yqGLKqQGB8{xSmGVa&#gC$1~+bRm}!8HnU>QqqZ-MQDd#wPI=JT zv+?+os`e$^?S1|X_ED<#=h~G^;@9VM z?F4Oc{2QrFQc}g`pJY9$oU2T$)e2*zHzEWjXU$m(PN+j53M#re9oY9D`3aI}Ni4+7 z0vu>znkAiC_ye$0_@Y#z%&go3u1%@NgD$xiS74ETU}IPmGZcriCT-eLsGsLXk>lZI zJ1hHo4i98|aNP+VF&l08u-+H;jHMa@9W%y|^m;P7NWZYM-L+4qt>-iK7MA9=G(u3m z*Y0t?_VlvxgQz4zlHHSr(20|gor8g!79|8IzpJBO-Iqa^&{S*Y>f12d?x2}L4LXN| zi`CLjn1P#pS?~4aE=`w0{c*u-mT=+~XVa|N{0Pu8VC+_uI##^BZ%lu*CO@t3T0H6> zcw^^1f%L{fND-Sg1>?^m&unc?+j*W%_biJ}R~;?bmPQ_~v(QY9A*=0J7^OZ=Op6t- ztX@z20cF;3a1`a@VR#bxsuEf4<9!6x2GVu5$aCk=#(HX;I9_lYGkMcf>ieVB`wwV) zQvQ4$bv@N*l-^Ek$eq-j`-07@xt4FX&j|*>Vcd_BU0qz>Z=M%9pft|+`s8<*$bUM9^d2Wy2Z!aK?8Xj|AFIa)k&}-j zX_jQmYOA2AKk%3ak3FHgbJp>a1KJY;xPvx20S<1aRL~}LoZrvMotCU(nA@1~t#jS> zv>c4>5TsHJd4Qh@@o5chdYMw$A)=xFHfHE?XvwbPkQ>+x_54p)`z%+>dKy8Lar@&M zL$`;<;|kSxMiJpp!TcVsC3Ezfv3JtTh@Ug!v8Oekl>RQl>vH%CbxuYKS17-9p##gM z*mt(ke8CoX;y~{zrXJf7eV}7B=s4FBb$|C=SL2oOkOS4wxTSZ3rBJwU)>b-b9X=;R zbZY;BNeMwq7v{zjL}FnKbUEf6vFtc0E?uV&iM2^iIxns{-8Ac)*KHU7lMJCdpxX3d zIEXa)K3?6BsRDmi(do-u{ZT7%quP9LRsEp&n~_R8ZNc@a(Y=-Ejsg0?RL$tMvUB>F z4yS>sS&U7CM|kT>4z#E`+T8h11ziPJ9t(VH(!ma_*NJBCFla_f)~1ADt1p@oEk-=N zmE+?R+D)f>fZkcE-8s@|s$@g;Zjwl5si%0o!9gJg0V|D^%($`7|d3v1ig_EY<+$geD7W}7~* zej=7$MVi#5>_~t9w;`x0h1Lu~77zXU+*MjW za}he!=^)WC=%W{!eff3+)2n{K)Ew{zYGj|aKSuga7rb zB6~%Hcx7%_5UBbWa!p08dXuW`bt3bFt++l<81El&bEQ*YPwpz&o*Z`EdbtmSl8G`aRgFRDcC)-FlJq#juF|p!K@;9p z>rx~nL&PY*X>;jF+9a0tC>VOD*jgs+g|C7eNHo2_E#rrRQ=jhjkeq+%U}_H4J1=EbuJxP7;qNu9MT}TurVb~In|L<8 zu29S1hZS9}9K4BXrQQA*?{mAFNu&bpu6zRBJzA&dGe|XX{}OV@X6c-%)Ys8mZij~X zwy!iEr#x(Zd$$#=>Ax&bfRu*i>~dMOQINq5+N7=55(gKtbnGwzhIN>s*bg3~;qxZD zFNQex+dhd7RN@9lw@_f<@;L%s*RA8b&Zrv38doC@Fn*H5=Kx+=P^rZ@=roXuvT@}ys;_{5AfAC;XmS0NKk&q+4QKPl=+{78^ zhoPwDe<|(y*}R3=l~jwPB+3-&nbaJ2X2~4SrxR0vL$e52S20GycJdX>HKrw;Hto&8&wpHL`&JIRRyW%>7L zH8lYX`)m0PXA?Lw-KK50Xlt>lbSsV;DX{Y%@q+f~_kOTz#487p79Q^dI*yTD=;TK` zRK<7v$&ur#7ym^1{@KCG`6WL~FHH;;0kuV<$Km^vj3*zaih6Rle200*dj)_4Lj|r0 z!@3qE{j`h+KN6~G`a~ik%kybFbD-7tZPOu9Pr_@@hJ`c=0BicC@G#;y@=RrTSaz@Y zlwRgISA+ackgJ%HvGE)VzjuKVv&$Yioitu2v|<1E;BBb*!uZemD~LW_2}SAfe)9pZo|x;X?qj_JP2QZHoL$u zS>r)KFv_w%7d)5f*n}3)->yHEYNObc!oNNv=M|#Ul4$%gg&=&H5Wx&gj0D@oCncNP z)RO6~n5K{SVW~`i0%K|jQ^bIk!%onZ{Afec;>w|x) z6#sA`H~bnm@r-vHWZ90DvZ#!Uf-ajhb=0t&{Hsd&4vU)nOq%w5o@FKSeWABOlut?O^49|IqX zj~QC4>U>OMpH9irJ*qqP!UvFLtWRC5m324Q(`T{4hqaU?It4!OtNIJ8ZlMaA39BRX z&no9);_^*AZzU9nwjcE*QU=ErxNQiBa5qr0nxL)zTjBT6H(f)uAv&@MG{2AAxc<&J zD_6dmDFeKZr8NZ7g}E!vI#-y(6+P-N37xL>+&n+D)Yx*7u_^mee>QVU+UU4V@1U_9 zN=dL)dw@(7ko8q%))%rQATgQ9IcL@KD#-KGP`Z(d8 zg!TN<&Zi2=GkTQYv_tuQhms)0Q~c-VLri~>)T$jo_Y#3go$>J^uv>_Nia)5JKT=d+YXs8nXT!jF z&HDX|hmTW6jy4`5hn`O|Cqm#xnB5aO`YZ6VGLx$J1W@MGhS1j7M2=o2pp;BV^^$rE zsD9BLgUw2DTE?r;7l$d-*n7rL`hZmq2UfqF?(JOwB&-ylP(Ulp5x!?0ea$pfw|n<3 z+aQe|;8aW+T~Ejx`2xLqIy~fjnohS^W&Px_vv*y{$bqP^lQ!m7(wv%#MaSSav$|46 zR-=HxT0{q1_1UoxQfZKp8mBK`C$F9VL*l~37F>K$H%R|6p#_hl)l z;N6u1gLLQt5{Vk~t+S~^C4tgEs0$fM2VZ%Ni0NS8Sw!PwQLv-?ljXF?q0&Xo<+Qe8 zI8jOlcn0KaL}d__GajX^H_IcD8+G|_z|8)7qd|3#l{E#Ng22m zfgrlIrzVv{AV+ioFNow?E;d$qhE~sutpV^9UaMw=-nN1VX_`AZpB^p`T~+wv>7<@UoJoZr26}sy#lhR9_#gv zIzlchleB8s}{-i0a(VmnmA`iI`g)Ns35u*pFSN*iB&wU1dyK z2pw1O9U586l1$nd;;;Yr?-&!l1KQWS+{PW&X#;$51q<={q_!OS7+$?Xtt>5r zQZJ{+_nTe-ipzX?VK(iFXyj1L(0qtjmrk?vkD)l46g_MF_)*GXa=wwfG~f45{*kMc z_HC|vEjTAOPn}g**(yG$G?Lr-qbhpcWQ|l8jg8*(Q`zUSN6$!7G-TrcP~A2(OIbG!u$M}fobePfZ2qxO>7TYzE#D4ao7Lk1qebwI|M^=s6n`2lXo2o=#93~H z>8`!X>IrpiPvZG5Vv8iFF?ViC$CRYqUELpd&^UWx=5)8+1K46v_@HS7P|oh)AcEai0z>e)X{HqVXQ3w zRjI0m7NXIIitec_5>>$p!q)FCk$t^OYY|O^@&(+aTNO!?H-y`HaAY2izB*%6;h^)d zsY5s*&j!9Vt6Tf`+y#-)KIgZK@1<9q3kRpD%w20AzHP)Y?Rxpm)oQLD6`=~9Xq*rK zo^D#5QMPBHY6l(swB6yz>rbspamKkR{*ZI~^w>P^Nc3>Z0pDx6wO}a|B4&apA71m_ zJiA)AQjz`U?lrW9!b#Ih_4`hMX~j3n+I%2$z^;>Ii_Sb_I@0_Uc(G4H_Oi$PD|B~vc%hsPcio~%kD?Vq)dt*1ZK>D1oy z2EnjKtJKTEfe_-*7(Qz4onyi`4ipl*=~Xu%2Fhm)1f=h}-|6dM=JU++$RvekIP$q7 zHd6_UCoJOwnZY3Y=$vvnObY>Gc?CduN51sO%y8>)$Is^_B2>_8Kt<{MBDbEJXJI;+ zVwDTky{!t5b~bTksi3hfyLPOP8qVAo%tt*KlWboOGo1@paRVL4psnJvZvCD(tVpoE zfr-(bm!~-UdjauiWR76Fv32e6o3j$PZeD9{gY}((%oU$){hwBQFoP=qwo525fuQmV zzt7(ShMq|Q`suATM;V=5E1|Dy6`hI&_#`*HUrZGDG?;ly;fmD{w)d6xr{=UIMb{$~ zkFw`5;wh@PF{XWsM=g|;9(ARNm&bPZB2X2+NIg#N;E=X+pJzn;9L{VYKAt=)8n+MO z(_6oxQ2+|QlJqR+e}-KP$x3AskN8<18GAY2o+~DQ?8;+*&mNLFWU}p+%&7~ob->1! zu~^^b#J8qjCbu$V+&-%Yvwwq>Gc>i|pCRdp7^uL{99H2E{|=opnhhH4&tul(+!8-B zpb$+CHS0=w>oG$CoVPOaYoCt8hw28hl6{K2aodwuOrX9B2V}npr*d$WPK(ogG_;O> zyFY)~L@)DqA6{1D$VX*dq3VhmGeoVOo|N2(aseb_sjD^gZOKoE?)(JhsDas7^R{Kl zXCv2lmIZgqjZ4R~wmkOC%efm!Pe*gc%`ZeBF8e!WbyyjsNie?@ZFH_VLSV=@4egs~ z^dBPGK4-XiLRAyeL!vjTZwSk8x2q&I_^~{1u6u@#-VD>CKnoJPK$7JNBz{%S_OC|C zBlGz3qzZ?p8Rn$6{UOGmGwv5H=(0s+FgE@3xG4Kr4(Y-V6i%`^N_*($~|)grixVn?u~s@*+WXf>>Q@HMnZ8zZY9UtNU4uViw0 zKZZ1|ZCJf+6^!qAh3t!(Hcup|(|68$Q`=Mg2-xODjgq&=g9uSFovb}!6+F`4017K($Tg{S~a7^iOie6{)8~}u(9Hvn8|iqtUOX}3^EZK-63Eqs&tYQ20kZS-c=|e7Z5lB z-cD&dKy~P5?Gj`+2=Y|ckNk!vSXsC^(lO>O0IdqVo0J&@X&ZyE%OQa&8PmU}Wc=96 z9`YrOSA!u6rO@Foqu^cqE}<|u3A+R(1cR#aXDqC!yN&Av4mlY#epA~_r)jM)h%V%NpNH0Cl%W`02U^4*dr5;O*>;JwhzGjmASJeRg2)Zt~Pd z57(7bdQO|q7X`Uf;(=55I*?vU3a>a75JwA6wWykc)n13xp7@eYe1q)Uf%kr{`_J)b zeHGgp^QZVtyAirU%i;2t zLGwoaQaL0l_IY>(Tg$wlx0ga|B7Eu#Y0E;3`a|STb#>sgTMAV?Y!vCfcr)LV+;HcG zp*$oIoxlVpAanc#1ph@PZ+ezz28`c!ev7=kdnvV3XegdO;Vh?8>};P59(TzE*r~o& zY&s_vmuWiM;0hhtfbC7{y_!7a?yi6SQr_6&*!epElVsm*L9N09=DM=a0dfRBwXx=v z8dpj<@jal!vDPvB!rP7!p6SGAVj+JO&f9&t()2_E0%_C_>q**oRz2o!q(nQ-OuYiW zYJ5%N#W6!Y_gALq71!dMo-wFg##Sw{T!9ly0RN`Hjo4M_@CgrS#e!)88OGt+bUdg~ z@f}P)6Ejy#Fc?(Ye#?vcNk|e+gDw0$HB;JjiiZ8nxrgLjFjG+-w?$Q|noY@6a`<*Y ztM>elRgCGBu zWUDh;2pf#=*`_`d#%J#15Stoj#9FA6&2utY`?HN3EI2EzDc*Hp#_jw$3MP@W&i|m2 zB|x7>f%zXSdTr7dEeNLo+Dex|Qw6@SjmQ2$t<&L_3{|_@*y+eeKv=0qyt`lvc5xQJ zj$Eo-J~`ey_L|!Xixw1@tO}Xy;OfeGM2odj4WNrOZJs=EuzWrZ_xkYleXIm0eJ)iH zg!3~u#}iWM1z(P*^*$MM~Ep@+G)FrfTJqW`4Mojg`ju|F~w* z@)*{K^Sc7=C6^`IE%su1x87I>)n>#T${7!im=c>HN=oghUdlIQ%`-4DzO1m7V2t#q z_JQ&zQQ~2JXJn%&*;-d(pPt$e%=_*8+r@_VG8S5NL;X;FLca=nBW^a-n=`hLZnG_^N~4(1+ruJQ$(%p z@&yiDB~z4O{wrJ>m8mcWNy(z}!q>|RWxq9R*M{cCC2JxjP0u5BYiXPY*tu_IMKjIm zM|cYt4G?qk``ynIC-Ie9)F*H~?XoVpb1UpE{&|%jgzo#~H@1pwLlsZ+B4*)986wex zYA)XVO9#$P^nsq5FaI&PJW8*!oqD7gevk_QmUwAqmrI zL2Omwk9Y4n^Zf0=TDm)96IHgG>db+$$IL}RiWJRFRlU=J$KkJ9#prEvaTe4# zGRrDw$9D#Ttv)hsw8Gul!rg*_f}}9vc67KPgPMSo#hEFNpWu(I*OkM{EVDSbQ4ud z#(T|e@w$o1O0|AThA8}43bug9nG0o}h4eP_0qx}vX>=buGTo$JaKnBRp!ei=jQA3H#78GFSfZEYr^UZGBW47KV_ z=JBQ!2x%~165Cndt;p&9&d^$Hr+#c(IhJVkJ{jUW`=NXyn;z`9{rTWzyqbGEUFDQT z|EtW3I6%m&uHO%6tGr$CZ;`rEx#A8=6m&(p_ZEpyU*pzxVd3)Z>13pzEVJ}i6=T0u zrSs_V3NhJTXh_?ms0@W&sPBF6gEfOd%^&+6&;K0`7Bu!qLCSY91JrIdO46sVm;}wn zQ>6>ZTh%gzcXcSn7n$ZT#48p04=e*?$@~D}G96_hVn5C2LTTZt+SZD|8$fPeIZ*iJW20sc?q-bMe)7<>IB z@wkvN&hTs;^Xad@WtdCxJ|*w3knuqk7O)Qwy5GXhdgE^TR4rRNb>HR{N5Z6wBY9(C z%Wr!v^xtSw^2h_;A6PyFhsUw^Y^AzX>U|n4w+k+n=(}*2H#S4ym$FfpaA>$n)=Gtz zg>%_orD9I@RIDvBeu&>STV)?p4;|{2vEht`4PFd=laf!W_Oyo=uj9!QzC}v#!nRq8 zMuEM^t_<;QUWC0zb@z?c1BW5ma~N=-IYpiST$!@}oz%#8Z-7&F|BcW7=2^&CHewln zRO_+t?i+P#KOx;L@l=6c7CiP8ev-Mlaw%-|d=*TM-}yP$jJZ!= zV;F?-s&(`>?&_|UIr&pWTBzg6S7 z=0FApp>E1vU{X-YKJ}(n*3SE+RpRw%SuVILHI0<(K;i(?yRa0k6MF*~aQxFiWa>nE zLT3gKS2YcuB5xIfwvV;FJH59Qubv>+4s6${?hcI}>qV(i^2BB+&vSwjQ<6&(?CW25 z|JQ?e8R)S8Ww-VACFdSr@JO;8A>5^ka^EVY&YUu3x$Y z+P&5dSbhG3$OAmdgHg#Ve z*QA#k(7@q8?#PO|cV1cOLJ2Pu50xz^-g44HYx z2r{At82}ld^1@BMm*m1rz0Dt@@K8z8>#=(#8g^rO8FF-Ho4npT@CM- zYmL!@R_~32Al0jxnm#zm(9K3Ww9}hEY**poWH)O#{V$T{Q-F55y*S{7pXNK#l|pwO zO?{>XZvylz@4Dp}%&fxLy8S8O;j(04A0A$;<*I8E#lGsJ-{|Nnhm6yE3KooPc?;*R zH_Aj1L&v>uJ-XXf^CAF}==V?YuQzA_NRBvV0_lp`+=oc4Z!>eE;9YX;J+OKd1j%0v zMZJZwW1oy-Al@ob?6$z%c~Iw5VwpB@rbjWSxFmGwXL zWZ&8sV~`J#>aaJ=1&>r-ZWogBK;R%oMuFkfA=s9)SbgrFo4G=cU0N=PpF#?uH4)?X zT_sAT50Ucpyk4R}>^KAUeYvsihJzj(Di|Nt%LFWZ6@RFS1w(kg7GYnMz9A@QA<{jSg$~ zd;#=;`0Sm>?L&1_nNjRbfshGUBqj6~kL-1{6Gnp*T#rleGJkS#h5BQLbXkB~?gZyu z>}7p6lzF;3P#$i$qP73%`Y4a<@WT{ZIHYLm_sf+3yo2XpMGHmf=3M~L242`3lSV3_ zM<~a46CVDmAswh>I0|5s_O3nlhy3`IkcI@`!TfWE^>YB|{8ImsVhzZ1)2!Y=xI-}! zs2`khHjcl8jD3dDv7re11(~r{dH8N1Bl8rv2w&hzT-a~pu3XdKwC;nW7KJ0l1DQEu zmsEyW!4&q1XFxPz)hLR6{V!Z(Muk*mBVby_RQ?4Q{>6%DT7n*e|NDVI*Y)a;3BUmH zeF^z&i~kAHX3NhG}>^~F3sC>DR7)eTSLkK-!%S0FXgs1*# zx8~+zp2;^5HnJEvF@%#R6pRNQ7mUt}eMhIWiU*0_d6Npr0iYh_MhiFcT!mtn{Ho)V zv{s3*NoGet@$??4QF{uLxFHBoSZ#0?c&>h-TPn0e51iRq=`myJuSBwz8UuOKvpW^Q z+C1sAnQKx25He+6(#Ky!R^vcOi4i+;g9XHaEV#-grFd%vH=uFI!AyvyC-fZqf!>aA zV@qMz*aHBX<}V3KEWK8##2$nI7_Vg_-h&SL_Sd{9_I?g@iB~5ESlO?Ab^d(={?C~U z0ibI*jU5_lVo-c-fsjv4CJIKNVU0x7!HK25j8X>6BfoIu^Q>o)BN@P{p@NF)TRsJA zIfx-LbP0F|{$lB>iGme8*wMO)Y*7MGV?OJt`|gkY?;;*8d@b&}9CCo!osp8rOg^pQMZYgfLX08R*76|q1Av&~^((fvh+oTeTngD)j+Hco>5pd0fZ?|pi4Uk9m zaXlov9;>E){WcJANsx*JVtStv<&iso=U8QwP(|dcR`(jA8Yw)sokFmj5=u&05*Eci zXf=%ar-X}g3H|Ip>E+7B<`lRzf3pj>yjxQ!HS~Zph21w6Omu!p&|Qet{u4;Sg})0E zRjL7&fESeRxH?v);p1yJ;ANEsAUqNL5MYMB4g!0`DCWn$Hqx=?#)WdoySK2TFWW9b z-vQYCD5}v)tQ`%oJ90Q}VgX24FD_P=NU`^TEb0w-5V(MozzM$U02ifGvgv3(g7gH> z48d^C-y>HCXGiP9QaVs_8Pvax7IeJAt2oE{FO`sr0bbw^%%K`i2%%mQtCz4HjB+me`>MyIfZ9V3Ta8{q&;#aO4|ni(~xnEnhsfbfjt)h`4I#m>w* zF3qV#sO7;}j>w}?$IE`TB002h+k}Mv8J?a0asdi9;=HHiVUO4miFM)LW>w-`N_q^^ivhVVI+}<7-|WH&cR?_Y$CE)7TkgJ>)dK&7du)rB z4hHx1Au{`EO}9!;2{ z=6A+#At@CYTA2EAck~uT5~w>S8`zbBS-N2ZJp`NN@XGRX6b!UMMAQawv|#es6A%@N z_%0mOH@lLw&`$5KDvyAOM+Z> z00aXB%AzuqN$!DETVhVu?V&fz_YiqXZ`H(mL4h~{oET3wrL-Af;?CyWZz7cHP)0rm zAt}6qm)p?EQ_l;!QVZnD0TfAgVCGaR95vU{*!uP| zJhNFm1*~;ycB~Yl!k?i9%gkKMHU5us9X#<5mLI8=E%B`+TP=qv7ofY~aOw(9X4qRO zml((mJLG-=Gm!s*U;`l`z6r+@_OR(lh( zALx!kh5IXCBB(CI!)Fl~Z%V*?FkYPTp(nXk}e~i4M~yMi{s)*C^pedkzfnvJ)qTi)5>lHvuLlPV1GI z&ev}njg`YDdm&F#dNd{IYBffo%;rV|#}>sZZm(}zc|^_qs#2iE(o?&*Xb=SVYv*PRup)onR0{lpJz<5r`sRoQvL0a=6(i>Q%ZdK6LaB_d7_}s6Gg%K(o0A zrv}|)jcz*|JyZmTiIh9Y(-$UuG}L2H`1 zMgg}%rD$3>h4w{N=X+HkUfVYSBK@gco~9GBaRPVsJvH_R1nq@yQLsIF(X&dpvHr;| z1kK>#duj>AcM!UB1&PSF4^co}qRFaaIXepW_;-Ar4fs~vxh^KT+G*pUNBi5)UNpIz zM7QCpV0$SC3_$*;cxQ3RWK$wxx(KW%(B>b?oJIQ&u=jcvl0Xv0KfKuzwA-+T)gi@VCfAcpw(4T(G}yu)vn4tL~b9B{!K8Y8d0zoefR6|g4Tjg$!##6?y`Y0%*jHv-4^Mh@PEcrdS?M*xOVQl zyq&TaftLRf$Oj$b2toD}pmpMRu?eW&Za(F(qml<+$&6(Hzc?%E-KhNkTdY@NINw3U zPM)wTDS&p5l?E(|?N^(*j`&xywY)Rl!(@RCduC3?JfOOVspSlYWVHi>EiX`=k9vus zz7?q7K?`Q81Say>3xVN zL<`DwtOyXjih@b?mz-<%^L*kBw55dqghzFnL5t(k~4%P-O}E z;rw{4aidw{{|Tb>aI6xR{Qto_Ks$SI68Pwww&0K@TCn^KLXgtaP$&&%J}H%$-ytzW zdl13h7D9>kkI#LlP-g>xDrp6~NGY332PAL^ykkWIn{?Sl)v62w>g51=!EcIlGv z1|rHI<73{wmFmGU}4W{!MmV1^ts#5cx@dW?SpxaELKc=j_0vU+e5sr*e_2ITUh zGkToTOeY8je-RD#E+B^ourg6F*w%7qMG5o^hQ0`bj1>`$Fg|}nA`ie#y2XArB|(Du zxqrR@wL@6Pu9-HvWH&E;jzXUQ!={#W2qbyJodMiw#l*$*KS^xaAf5h=m1eiWEK`+8 zLMRS=SkvZN1*CnyX4ci{UbcAdm^I+g7zIO=LVt^7uoX}DZ}{d%fhT+*4`%v z>a4t1f1@OFm9A(eDx*FN(Xn#<#SP`l&;-9V&EJ*7!Un@bx3N~QLC`Fz{@bmSv+U6A zwkPpkSU)_t0jB3X%;d<3DE3zlL~x)U4)C)qLTuK@n{eC7$;E(R*a(?tD%(6I zQwaaH>ALQ&`?>Gm=Xu`zU;TP>eJ+XpJ-_F<*0GM`SnE8O;rz{L47NZQ5}!D~dRLi5 zV`#xBR|VTUw3a?X+bBFci}%Xe-B^>A?3&R%Ffx13`Zz2Sq)$_sm@pdb?$`KXig5xK z3?9k_?3Dg=XKk+>XZ)(wH+Pm8)$3^UCiAo*g&Ro8BC(jf+_U18g9c+>dn5lHhwGiM zWz$xoC&Svseo<_1G|!F8or>~5Mr5!~?NKX;YZSVPhwOSgQDrwOYxAi&zzn<3P?}*7 zk8t2&1)?&4b+in*wf6CZ9{&F~kkn5-n|9buDwCegmqD`+T zA!nDQk@2Ccj7QFyhi+S3LM)Zw>Y+u-`^cr)%wDyn7f_A{-BerVr9LEvyf-A+m<9&) z%ENFhipmdQWhE>v9h3uePybTmKAVD1?C4XM!%<>pfWe@0?%yO@b5H01Vpabv^cc#s z`e(C}-0T6!=zj)E#aDag(e>aQseCYF4%fh(G-S47qry@RRU7N=rhu&T@!AuK#%_9O z-_#}*Bo85ngs&e)ji8R>{PXe%R5k0R6hAU0w>2a>(|_s%Cgvm*g+7_q}vdk0-~ z8lN~LPQ$9H1fC6y%i-!COM=iNvdH;&wvbrPtGh8jebp>SLYe`jDY-yP;Px3gtUDaS zQr)R$9Iq@7?XZ`}Y5sCFo;X2Nlu#x|6`JN6L5}#II*a-6!5iT<~3Y` zhRkGe6x|FCp2rrGTObul|$s!HK|?kx?p(kFvvte!fuz1;RFyqpJ$|KHf3T zG;z8HLI6R?Zp~r-;{noI?6AT6Q64Devjrsx(xqKcg_ZCD7oPqRxSMTy5Bcx(UMQ7t z(yv%hWZFLc88L;}(Vh+ai6?iTIl$qQEN~wQgL+OD7Jc`H32iykb9+!R26&7g{azAs z^7yK-`C*WO!w*Szn$QA$9txUl=nPi4aT>x;a<4Sh#a-U4xqLPL{9I6;_hhT($GIx& zNj=Z7d1QFBY;U@>-y+;*UAv5;=h#CR0v=NiRN#CE522;X zke0316wNhBA}*W{VHe%6x14KzbxggPyXro$S(XDJ zbyeOC#wsjDxOCKi&((Q-$3KE?w9FtP;&oQ!R8|H(;noED2VYzOG8W^+Tzg|1WdqCC z3ZTQOdiZ%a@M_D@6bSa;w4_jZ#k4$%-ff6o!&{D|b=M}eoPI9zE=lWcF4qzi1!6|@X|a0V>_u>&{A5U4jVcV z-!4WZP%xM7Ls9tRTG-KdTkP7@*lQoX;xGP763}5RkOIDj_OHMwXO)YsbFOlRNkx;i z#C^p3aPt-3dY{{3|;NOW+uULI=H7kQTf$Y_?;%!4FUjj^Bpa~@rS4wd=NSWEu8XMHBiL2`8@u}^FIxdXZI4j4;q8Z>W1lBNckJ_n%xRH zN^IMlMX47A(Zm&-aGNyfkeX%sUa0csYOHr3;(L-)W1+OVSo?q<((@$~k>H2TOrO#`Mo~y6m(2QXkArF?+4*}G0nm~LS%=Uow4`2~JQ9;*gl9f8JithbEHFoo ze!qLT(9JtN|4D0v^Ylt9EuHV|%tK|p>u{T5>K!LNc|#LhPczxep5&~Up=h8`VaBTNmgE-cwI_s($+6cut9!mT z;RO6OhDy}Fu;oIHzy{?W2WK7!Xho&fN46PH$M>7agG7evt-|oE^SGnv`P2x6jQaKk zh!9JABS_mrWrqj9LlVpPqm+%t?jfzs!vG$E@0M9ODa?w74^(k#ZK%4)_G9zOJu}|= z=ZxF~%4YGwQDbdr1=w7!InSS5b5n=OZ3W3OK zL|E`s|4X1BaWy~98OCEyhb_YiiA<8m=`rS)I?$778@EzC=JP>_8Q3nR z#GlJG7e_Rbe!NN;tpzpp_g2swUnK z4yrC6y})Tgh>K*F`P&jUw6Rz%Fus$P?!`IbpJOh|6LJFjI| zWKH)90rX^WDplhpt%#Q|5}NOUfQQc09?VOwH$V0KENxhm$`?LLS`cyu%b`iDXN~R( zU)nYqzTGgujL$Vc0h@dO)i+>{ffZw^O4-uOI-oN2?ew)Z2S^`(0dSE%XKg~xiAm)w zd}2B^X<1z4*nJs4^ZZXNg#oZD>9Kt5XdZXH32@^FPKwMs?S9i_M8a{?6NFn!@xN@2V=eq}u&K%AYJo{f z+dP}$Bg+H#z%V|~i2?YOZoj<}0BIg%aI>F05cwA?-Oq5r3P81XY`Vp}~t#9o`E&HF(VuFTAmc#^;(w?pqP zv!hpvUYOp71ai)g;F^Z#SOI?w@VhWE<0{1mwp#ZD%tm5@z9d+0KYsI)kI5nVWGhHv zQj+s;UgB=qG*{^k6~RN2LZA?#z*2#Z<3WRnY1B+se!)lF=S=x8kM`lOl&DZ|Noa(O zcssqG`Dz9eNvaC2WU~u0{pKZ}U!D#CvIeU3@a5J$Aa(1efBFk{$7-ZBF!b#J@8e+6 zu}}~taT4IcSTQLWJGkOk_fJ}4+F7jjok9c>t7;{77^0rsTfmr}U)znJV@9r2MHsEb4P!3-F^Iq1Kc~263B%J6?YTLCKdQJQrmbFS4Sg0;`iHS z@$i2T3rRY`fHvh3*BuLUNMlYFo?|4CrRAC}lH@J`i!z{{`;LKo4iWb;48u&JBUrld z*Ml%F<}UKs2GC1jg67j5gD?qRi-z0$CKE*iDeMui)#DzvZa!00VMpJJ46`vgO-;_6 zc;in4%C~6|BEyXeV76QINO%1b069wl4&xm|MJyeDf#(?OlN1dK@=P(jNUl(B#3wy`9w&VaOnFz zBn(Od=|oQ!8vV((v6<5LgL@IuyWQUA+SZn-;1e~@@vh{FsMs`j7!z7nlPe9=*i&;v z@Eoz(RFE9FwAa~Y;&?jQ|01zLB-XeNky5wZ$b}5+Eb$3w_N!;MY3%)|OTky$d@78c z2f+RpRnh0S&DFY5Vd)s`k6=}YckCuFX84Qw*Nc_uw_iOCf4<4hzeKNdaAOsfFsXxa zTRVrx1_8wvfpxyFnqRI?uYj9=+gTXYOsn%d04`-NVFz3aKmb0EuH(P?o46(^fiI1X zNO}gzr71cj1DuREkhBzlPSZF^aQeL%I4PzphmeL8P-6(qfO$k}8$ z=y+QW*K;1-b8M6QDI@Ih$sO5mmXmag0~LxpPWez)nkn2mQnH#spDV0b?uX>4wh#yJ z)%d1ZK0hrs$lv~;PZHc)Mhb&q{MyBQ7pb-sZ@96JvW5uwBTn9V3l3tsAWXmuPu~k~ z05rckhySYi=_PBm@M-YhsVX8BDM)(&3d*J6du9_Bj229U9B2w7Ro&Z>;5mUNYJQdq#E(^>&%TEL*acuN z0@ze;Au;p{O0u+))W6*l9I=VxJBgqlLLrYd@y3eqzcCNvV#K#CJiv7c7@E#JrG-b= zkzqPP?2hna^%XcO!4m>y4lq)pUZ1w@A~~ZVC7Gp52TxNF8SJV2?jg%1-EFtY*}dCP z*=2?2Wu_~n7}RCLt#0gd4Ai{ZeILf@T6|2&eUX214J8NvCy5WJExr^|7WCkh>i@Wn z0scq#9VrwtrL2*WdSKbmSM5-2=vdaZrKM}OHh`hTCtB5T;I;+55np~VXaM0x@)9|Hs{ z?${4JyP-A}wt#H#cz6Xl21lAj&R{_?(M&=*mg*SRZh>BxBuxy>z{ysa2EU$2FE=r& z=1+Xg7qI=<+<=caO<#b3d;4Yl<^N{11AT_~4|rUk!GJx5!B{g#{-i(67zu?uDF=8I z&;D1Z-OE^Tud5y`Q$X^8&j( zPNvTutP1UYg90pnc>%bRnS6|0c4M8HRPt(nJMDl^oIDAI)?HP6Mnqp;2G09|iqhsRy_3%4cb@&6>_nUIDIZ?8=Hz#nFzO_>C!$o}LSfVE63 zNjr>nzIQ!b-DJu(>T*n@5V$arw?aVr|9_D09zK!Tjs+^aJd-K<>>xg_{92O>5z3_k z!KKJl*GK&u8EW8*1E8#260HCSQ4J5s1rs&L^E(_$UjWQOGE;My;#||+@R+2i=@cjp zD5!G(Oa*GtN|lld;QSDwMQ1CzqcZ-f9(yfSvw@lxn;N~gWV}u~3uJV*73$4ZJZ9gx z1Po-^aR&XtCcxh1(`5=z?PNNKv2wDow$Jc{5it-0v7!R9q^3)r;oLd#RZCqO=(Wsy zcdQreApZv>cJ(CTK!=RKJ#+#4j=Q!vD!&yyYGU1&g&$6U;79)AS@)zgkzpY5(={~6 zeMQeff;)zN$Xg&Ihf@lG&aa?r?<~uD?&g6ltz18w4Q+MAXG?CKGz}BxnGl2yx1QIz z_LW1~T3N4%6144aREu!O^bxpI_3S|?{K%Q7Sof5-rrF{-j?Nuv4FWw`*KEQ>E6qcY zgot1KH>FPDyh=E5ua+s&q;vq-deX9ON2SC37nP2@+xEfZfZsDj+((;toDRw5&hMeO zF`O@N4S^C{ojLsd=-Ia9zcso`b(0cKA3q&9FwS7B4pAbx_v|MJIb2K4cAH_#%zw1b z1q$#Gqju2X&eZv<%vN=iV&KA^0)F6K!fz+?A#A?Mhhb#|c%5G{`8u5$`@9qRXz zlsH!c{_wz=e>gn^N%oJ{-+XI2c+KR_<8XGkm(^`xrrQC(r0_SS1UH@n1HU88ZM!;9 zjK95cWU`RO(O>t6^HtDcgKTAMHtWdrYr3H#9aTqcud0TG*-yyJ)*ZZ-!EKP@4V)BNBb zyC&wGVjKqUeKv7cIY6=mx~|@Uc+96e#L3(7tb{WUoWjdPj+1uxT2J`_FFcTye%jFa z;BU?tSb3(;LJkVIhS@Tg9~JGywbR7z2T$r$xpa3Ea_sL_bzJn>E7c)I5Yke0JquhQ z71Dy14LvvxPST7mqCSN9-Ff?A!9M=C#wOXzuAscb z8f%ey%eOzMIPk@`--DtXLy~t)806U9K#%Eq5{oFoz{Mct6d&$2QbcOof)=7aJjO{3 zy^gvPMEU%59UFT7PKZ3uIaWRxlGS_CaE3fd>B^@?QNUt3N|B+sg9>Qz7~#*i)}N+EZ&&pN4S0-Pk*U^pEX#82)!82HyA#YN z&;JU^1$p4~we@45#VP9rGD44bmD_v-{oVI*{>U8nc)ZoL2Sz`cBo%bXL80~E#k36N zsEtgCTqE=yqEQYE*FmCy!&+rN$D^ zqk6)?XeAc#{yc6KHw(pmCrrU+$tA)nCs>I$#0!q&IcS}O zI2{Q>GT2t=t{~w#A5Q?z{>a3iLxrsu2`yO4VE*^F;T)%n7LkKotdTRiuzs`Q&iZPD4p0Oa~^po&m;+I3@2#%u}5OEvu}k{W8?9 z*))SYX5aHT1wlj8fsKbZGC+m7O#{aI!e?U!oho3NefDpH*$#-^Aw21^i`XzL8>oHT zV@vs-lduYf;C}_Lb!-atm32{#LXF5B?7HpmMgHOM{h{9rBG;nAQu>OCofA*+wC~1U{!-UZebF2<7EaFR^k+%{ z!>*wZ)01!-IVyLxp5$;AE}(54T$Mw|uF*hL<0mqhd)-4yKbx4lk~(=tsD&-Z$5b`e_Qgj$x3^r%p|!$y6=H9%ed-(gr|fEItbEcPqB(iw=z z*KyjRF}tNJ?cax+N{yE8Mx`@>8(!%B@u^ps8)R5ILuO7QL%Sv?sD0s7SP|p+T@cu| zNsLwZVY3Vnwf-YO6&nZAMhM{?M1$HNWEJZ`s=`l@7|R*F>tB&uXvf_y82wMe>J2IS zl7ia*n+QfpICR4oDey%eI1nH%I`)!`yDhIZ_;gpofxoA_l9xtF8b1cEvdy0}z2IEd zEYO)jJ={xl_A8w@LS*m>hMOdz`(u4KgfLA0Z$Tg5T7ZlH5x2C*!@GbICD^}62R;3}P%)FTd21_9t$f6&uZ$`% zYnv3-aA58G4Nys58d+UZ$+FWWRVSw(t{kp1l59==Tk)B55n9GkJNUd4ttMp_RY|T zT}~^>1cj@KtV}7Fa`v!YcM4b~NNv_V;6pHZZCRV%V_^tFHP+s@^WpP437VG`az!yU#* ziC7Gb^B~Pwpn~}R1MJZ^Pm1l7>Q6FQ6a1A6N+-`kslE|~h~52in#oS$0zoKch++N7 za}Fyr135G+2%<&nll{$`MslTOe>x?G@hW;gxWWlcO1pgO$>wrm0+vbR(M=A`)B;V=ZEpm>>wgVjdz8d$i*USE-G2^8cKo|ZS zLo2kbft&XA3fK;CJ&w0>QOaXRMeW7$8~@iLmXmnFY~5{~W%-3Ol;xZiDnK)nq$~?M zJ?FR3z(wlg$R!Cj$FNjP*T9gQvV+&)d6zV{pZC;`gheu=AK6xR{|}4p6%wnERvDwLT0TzaSM!F%TNHG0s=LGsl?q`_D(N8O-2 zRnc+f&-RTuhzb)iKxRedH4U^g)FT~TjzPnUFw%_eyg4v97ylJ=^9c3B_oUL7hlV>e3MryP7b((}w&n1(z>8U6${( z+Hc@mAh2T!`?C^1_K_#Z%Y?mlsF&#B z`a_I5s{_1)z&+w*|NisTuuabJJ84UXd3|*hjuyI)%-nG_*utCVtP0*oqGHbxS|}y0 zTHXK!N0AP6e(HnGyMO9Y{y#Y^?<0%NKQ90G>e~Z*K#%$|X}hO~ldzi@j2MEhl)T94 z(c?4Q?(*B<-Ai&|B511UW$08vB;8%I8>M$Tc4t@+miVU-^+fw&j!wav-KJG1%D7eQeUURlXplqKw2JqB=AI;ic3pwUQjN6njt}F5f#R+rML@ zPdAZ9E6#3S$nOBv=iJ`?sFX5fAeO@HfLb?v0GO5kCJ(v=;xivlD=1iwcUh~yWoU(j{ zPjSvvPUj$RtJMD1pvL-$%+)vHGn(k9jgK2H45 z%h($4{1GY?Eq%&XXz(0v%GaN=y~x6UK%LMsbtgH#>$qR_X#SU}mKPJYmuOIutWfxT zZ-t*q{lsw6fAl%Lgl{sjvH9|Z76m_mtSRgVBi8u2hz#Fll5X@h8{hFRn(M>K6*5=g zG1t~Nk#;%wMB4BG@D@B_d652(=6dc*HB<%vSh|1!D!M6Hz78kz*~$ft3%pGsLx(0=bCpqT}NY zKjpI&aA_-zZIzgJ@;JfMCHKXNC-)A;%ihC%r9lM{YrzdIEVK)U(1O>HJ*sUj>&%Y!xreJzi<~aG@;&ACvice& zO8Es;w^Q8s=irck>UZ4?H##0p#3yDaq5@99vz;K`$Z8%jV^zR~y0N2EyJkl`Bb>|T zQn>g=ukH;D);0E2;B<$DoIX=&%w&4>kLhy-= zWlXKhZJmnnm%Et{?b?kbbBE6d%MhL0w+^G97#mxb^e;e5L>LWR7w%$=XbnyZ=8jK& zz5K}Ss=SW^E`>Lvd^U1SliT!%kZP3C@lAgumm?el)>HeSRZXy0>6iEdkU7bYuq6gp zPA-Ad!oS#9EOzKccY;6CG@qP}gYZCGFOdIxMD&M%uXbI^vf336w4ZLgiWVlGOs;}A z_~IF!oj7j(#2>lw4fgwT3vz>s^FC~aut?T-&Fu++Fmpu}w*0}uB%cHPuz7wR`f9CW z^eCy^i#N|7S$;s0LNP`G_dYsT@)^8$$6ISX6|~SpzL&X*8B1mIx#f0CVB)>n#>Z6Z zgpd~GRcg{IRC~SqRN59sFAtykz012(?r@5EHvEyvp97HP84?`uVOALz-Rf^5I>SU- zpM&%}8r;~9=32}&uUlG08aegg#_)+5uJ>56&O^!{I=olUS-QY>eK_O!>U}CcQT{MC z>=$kI+HvgS^?Dn`gxV!46h-N#97XpX>ZW3O*ex@&NrAi>rdy}F4XKSdu%jK9JI|pq3h{}Ru*}~CiBaxAm9s6dkpiaQ>gK^L)jn6ZRs}+fmvlw&XcXB&?4m|`SNbn_ zwHFv5Ac@b6E!AAfK?x*Hj3+dVgQb}Kvb8nmgYd&xs<4B}z8^k9#WLTp_|xZ966+ze zGP1uz<L5G}kZ$b~|$j$d-3YHDVK4~~Jj-;&YnM~wXe4N$Xi90Vsm zIjcxgLuap}^55Qs4v>8@^|Z zd12zP!57=5*e$TvEQ)djBI9T^G+^I*LA0RK$FN(f%)3d**9fe90Y)^Lkkhu9y zz--QzhMtjK$6 zRKRp)a&nV8S@=(}03PBE2Vo#c$=i8WbPeG_Y9FiaWk=7MnZg;XF@JGv7N6L49-RTr z29N!b_M;v6!uY~VADFP8-lp37laZkj)|YlN$I$ixsdbMP@Q2hWs7PpuYz5P#JI_QS zFUoy%Awr|F~;bq4Ewb$}F?F7N2O6e*}J=wFtT>*~GcQ zYC($bco;tMu`9x2_r%qD8*?;RfW9=EZe&@cym>+2SJ2uVmfZ(?BKk3B zSgwnK59YZc-Cl~v_@3Yrw7NDg12BO%qlyu`xNtNzerEk3mg+-T$ENNVb7;mtj~=_o zirdcu3lr`x%Z`aJKEwx+;^$7})VTzw#3e!wEfJ$$0L zr!>pt*PC;jhG$2^I=)h&O6+@W*wMVuq)6VnSx{ggL>wRGIQQnxKGq}H3y6a^cYhII zAl`lVL z-i22>SR|rm$m|Qat&hd#J9x5s?5v}5C7(cWOL<)}+C}99*`yY1lftyX31!MgwcS3l z9Vdk+!#Wrl*wI@_9_8(o2^hJgKz!mhlKhz+lESUfrJ({T^Y!4b<-NT20D3A9vs0st zW~u1U6ed+xeHWJAWIZr}-c8CBYlg*<7lj=NLeJX9*KR*5K5TUn{zb;JJv*|o#pOJ7 z@0loVhsdXL0my+!k^4w5swZ7U2`apsO|pb^`uA>`A+giK{x*i!3kao$-Hp0sg|3w?NBmPY!g3kNx)SOvjp0 zNNxQ+_*@Hz4cPXt{|9WXLJ%T8xgRD_J@geI>e`XxKk$ixq|mr1L=_PrXE2!^2HfO2 zK6m5HX71E>;-rD&kmKb;Q)obF2~>vTLZ!Zbq50sQkgv0Hujuc1IlV1KAbefu1n6d2 zqL5o(-`{j2Lm*UmujM$oXro(5|0kSqW$*IAGxMVzCvrP)OP;h-4hh9_I_}r^$+Db{ zDVs1=T%T*Ne7DF6s2F3JR`KAA=#_yg zmm_#jOykU>zHgljCZBe3pgc?sZ$e_TBmJ_Om@Ou2l;rrXv!R%S>oF{s;Oe{^xbs_b z2i{#zA5mw5AEf9gJg^V<*N?Q&$+@NZb#~W5cRgA~!!n;V>V)g_S)N=RWfY`unH9Rn z@z=NR{PztKkoZ6Tyu(6`@{g||8rZ+=`{!3q1wwlM$DjZGLI1l(|JV@!yN~|&i2P$i z{Qn$Ao)K=kA>?jcywL69w#D)&=)q-^J7q`jRGOf1&%aN0pE7*eo~Lqrq&{4G==7@L z7R$MBOSjyd?KAp|os>hp4N9~;SC|zSY!xTI3{^5TaG+%~S08)tS1d;$c5A)gwd#y6 zDpXdyd1Ka1cJk-LtviwxO2cdErir1P*6X+dqnsbjSDf2y?;oK5Oe}id;llOUy61F; z$m~P?5524ixi!6vOO^xeZ=Z8ceCk<im7y|M3(z`WDXHB3*Ix%SiYn?J&tIDCp3pXH5fJZ;1?3gs-Udk1q$8W;zT*7eABx>CTM` zO8RWR?5YiB=`mN!qLA&h?`B%|9gd7I%i;^KR|;Jl>`~&;c;3qI=uOutLxyP07B}}c z3Fiw7=Y-EV+;)HKD*2pFU}58vrB6q1eu@!&EaN`K*fUf2H2;~TmtNkOH*1X!w=>Vajj$^)j8g zR;FJoW_Q`R`P`;&j?A^2jveJ`d0qDA4OT_A6ZbU2`LEMp`tHp|PQ81Pn1Y=vTaz_4 zFAU@4m0S~sx?~@sRZfmO4_-%{w5Qqah0zGkk-DcUy8Mw{%atoh9wrz32i#jPgyvpv zP7xDuZ26X4>0!5!a;Z+E<6})tfOHrcLQZ0Y@LX-dsv`9$GlhRCzq+14eCEsh2PPX( z2SmT7b@fM-R;co}dHB(BTqi?($q1v(dN000GiAG9T)EYYW6i497ZcsewK(h+f09pF zOJUASRYm_701#Nnhe^@LLVp!|Kkm#LnqHc3bhLiO7v4}h;Ir7RlxesUNy98Ns_O-J zerPYGJmFM|JY73J++4X_avFi~H|h~i&cD{;eDUJNWTPy(&9a#FYAK^!=VXc}&%Wdg zD%01;e0UP-Y}yG?xfqsu(? zTlM}_>&fiJEXxJ%`e=?)&a`XybD?fRn0a37S6rHoLEb#KI%M5Zk@FNbrPs{|lbx?8 zbDmXw{T!0WBeLm}J)Rn`Uo5aPxiG9#b<<3m=&0a5aBEH8DI2}STHlo`ak0}@pdL-6jDEd1Zt&OWFo}+D7&6CNtGu)e^Cx0tM(8)nemc<; z;=9yP4nweQ0^e_VzOs1uvS!WP*frhA&vo*%S5&~@mcQgm2|9dgFe{M7Q%OJTqhhGp z@;49c1%K0`f!^+siWlUwicUUxR*f%@J!hI)xb@U5DuJL_Lrd;^^Xu`$c*;!E%ATKl zv_c=z3%rJ)TOz=E=&F7uQ~0w&^rcJb8#9es_8Aju;tUOxl$0^+1HsngLI?z7TYFKw zZpeo{RRh#YT8)!UV;*j}_@5dB9>ML@jU`hrYR&dv%Leo>HH@Y`8!`NCxWIc@3{$QU zYh3j?@jJhHp);vCPz)?_bI6?T$wS$*%6i<0nRJpst}O+-)Wk0xFPiusGW_U?XfU1b z3lql1&FM^$0y2cJZvKtIj|LT3u1j&APd-9flMX%9uu;HhlT2_j>DG58?{_Zsc86o28OU)hVypo3aC! zvS(*3i+H?EI5{~_O0MPR%+2aN$Mc%J^4(AW$?^4Ef4Ro|Jf-!FFg(Ayj!xLorqwh3 z19(o|Gy}nYQ;2kC^$-S`#9w1xXRkUvm5Gt*%aKCt)S}Kq#)LZ^py1 z^{E-_%3xBeMP3gl*C791gg*;rd{=b)*x9i(>!%bo9$mOh(bdFg!G>iGA?YPtoTtn9 zXUzheTX~#m=i_V~`WU)XGpxOu%Bme23T+2ei;BuF7TD@(hRE-sJAN#+94@>G)}30c zH*O<-{~B4{Dd4DAE7sWY##?~?BwhnfL0f=Yp9swJUz&T*Vb%p_<_uEWRo>Op{}G+~ zGp$_GM*q>7$Kg!j0d$7pup~@GGTjCq8=a=@G;^Q6FyLQfV8;nWhpL z!6bZg{zKbD+jUWkc1((9qUl7-h%5DEp8`#+g6(F1(si>tUHO$aU+iLPFN$?|rLRrT zAEw)R%Fy{oyY)8_HR%{P!}%c3z?GKi;vr96^QSdSq8=7LaMw=KkI_Z}B}uQ*XL`SKl~0$;=%zM5 zTQWc!4W%~A+%a4|Fxpl>)HPY%b(rpZCl0R?AplJQBb1^Y_5(S6`y?(wY|}11nsW91(|pIaegjtsJii)N)h)4OO=-~pf=Wa)Bxm?_&QvmW zX1pNtDzW+UfO2giMw2HyTw;GGT}PU3b$+>gv2gn3te_HCyX5yAtR1cNK1oKj2*i5; zd6y&2stSJ8M%U0CpXu#SHsS z7SmI`IpGs2%Ar%r^j(iNf<^76u+#n#g6nFb?Frs$!~JnT(s~9wS7Mtcf@OuZG|sID z^vU#1yOyv0k^n%U6pCY(vhE$qvo(gVaHwbVYCJdL(=W{LEhyF<`egocZ>@Cm$rc2n zsABV*si?!{hI|3^vK=49H>Qs4w9<8#E)I>d*Qf>Wo07}P*ID(3q~flhBSeY^+vsE@ zJU8tdk3Kf!f-3K{S392J(eWrWNvgD{O?I7TGI51Frk;-1%(_CyYqJ(>^Q>p6_H4IfOH%{Y z&J~eK5Xxh%+`f-_l@u$HTh<*;hF3B9w0UgOPdr{|;9O{uaI(rP)rrG4J!KDByr;Pu z*SJw{S6e)fbfk<3s4o>d+|Dy9D(f+DlR%R2g?f$YS?Z-D&6UX%OSX)(T+1^Zs01>! z5-FE4$y!An>Q}UZM7(%o?70`8X4!+Jk8{4kG9vOv%;To@h>OGIEkKhfTwDnl+2~_)9 zW9#_t!lg@-ZM5ck1HEvXb|cv6XpoZW>nt8k`7_~TESmSRmi@?(l2voH&W7e_KVO=_ zD(jLU<5;oQpE*@QOOIPx_9qGSxlWAd5D#7qZYoMO^8R9K`s8LJr}Wm3^_m|LcgCMY zvWS+a95{A*0^&u=L|KW8O(tU-_nw#Q2w$r4xUS9X`^C9AImg>4@>1qf4fCv88k%Lk zIZr#vHi_I{M$!Z}J(Es_@WwbK&cVU4^l6y(#+%1;)w~_)nue@pOF<1@#)X?RZMNyg zr7`=DDbvU&QCU@pOekpgP-3*>b8ScSyHvl{@atb&Y_9N=^qBUakSb`jI=d->q^Yi{ zVO-y}DwAon9M0SCJ~JmAa_EqU{Wo{|Q~2iYh#zaa5r~RO{5STE@^zZ=@th-T*Ytk) zt=->0st*8-d~mkObc>+@_9h=**702*aH@2An-!h;GByUR8~Ik4- zU~fkP1kF*0uFSWZyYN=Ky$fqI@EnGB7W1nmvc?0@e|rIjzi31X@O6poMsHsm9{$YIXE^eYUVGL-(Rs>FRJ2DM(G^ z9u;(be%NVJ>d2F{JXpfP)h|xrX`Lo6G0u7;B}G%qf^JjOJqw|S9aB`dz9;LL2M;*t z%gf&bu&dJ8x1^m`ucn)-blv_}!@2yQk2PeU*+@e%@!{7++2~8GxqUM+Dr(h(jFMoZV3 zo@{Rg7U2!Ua;rpruhs9vL>{XJVqN&;{nuCBcx0JNp$wZDaFf%zwvTf}`^{cjX5Bn7 zFWutVne?`#yn3DT4v}4LP{!&*4lAEzMX)VA2ny1Pq!D*h0rd%$T-t)>Yz1=pE zLalV7HePSCzxaR$@nNJ|tqjz?ZE8XE{9UiG1AV$10n6#$3ZA<5pY@E3=7g7Jg&dNw zWAzh-r{j7HF2l)%h4VJg)6|xY%gnAceF;C_I-NFAO%XPK9fu+8A2p9L4908gZk43_ z_^X`3uak7KS?!soYLN5Kc3yAZFdMNc{M?x7nXxG_&<@8vB`xjrMf1+55L=*3JUKf_ z9Vb_*p+qb5DSE}PcZDIPRW~Nhr17ZHe1lD5(11hN)M?c-ovhx!ESH$i>A@OW?6>q@ zS~}4-CtObP`g3W1iu($vUQ>)iJUh!UIRg5W4^*>vxp5c}vH)5tHy3S?LBIHWu zH`}8n?-1Ww8;2{8h+uj-XACM0=E^t2G-#J;r`sBxpZqH4HIG_OEgLh*y`ZU^ez`Ms zv*PC(U7MWLX_K>0HI+}Fi~A-0<5Q25$CzKU7X-kA5+pP{{kUf+TCiQj#-KJ|*0%Fh zx)$$kk4Z*zuj{8OWd&9zBA|+LDVE%$7iuOg71f#QE!mep_@nbUfF3T)#;X2c*MV%W zD|~sDeS!s(=LqAN)v0V1(XxY(j-PL^e%3x+mKj&Nq7|FA)bO6&-s7|8MeUV^1 zfMd2NEV7#G%d8@u3u6r#(QRf6#m}Yoo_m$t;N8}ZHg8wU^srscwhi*~$y!%QtdX&Q z4F$?%z#6=}Ov6&825-a6@do`M`_!gqZMo)pv707Rhx4T1ZCm=Ya9U4qD40a>LpIFf z*X=j(5wEU(K|7?*GVlXVY)P7~ah_Z{7{NtW#wFwZXDtm$~m1HEWYDu^nTxIVqUt_{vwEadTq3d^I&S zJ?``qbk?7?2R`?jYa?xwbyjaZ7#q>GNgi14tp>qS^C{iXK+#OM{W*pxi{9(zvGptC z-#jji6lTUSHSlanW$vwwPHo06Kj=C2Q)S-RCf{z83Z zmbFD=^~b1CC(pUMQ}T#|>f5`mKAd6W=?R?>DSh=e!hsD>Hz5ZKF8GI!d+Nw)b#29B zQqyMp26WsuVB^LEs|1|knVHJ1=VUcM>bBIUHqy?TF3*4diZKem_VHDUcrmSn%xb@J zN-nST!iT<;rRB7sYry54bScC5XPG<{>Wmf1y1xH_J7%teoJ2BN>r{nBoHDhJ=`1#T zwBVdjUCVQ$=aFuW$3U-c_*t}TDwY=={|Fu~$nat1brQfyRylfhS$cjtHszg#@{07Y z*EuFH-8C!MX%Ga1T+0TVQ+;+~q9re~^lPcd2dWtaJ^*@scX=fbQMS72j44)cY;Vp! zYvq;da=g%^W3L+t*E7ve#L-$wgs(#B+ID-Z;?Rakr*5Lh4~0Ofm1#FaZcCrUmoLMl z+VmJVkz2Dmqp-#)=gDKlZC^jwH+1bZuzT6$yjHRds71jx(P^MHMb-w- z*Q{0U4ZgAYGX9PYIpUx}MYGRdHihWQ?>y2HHVHZ2Zk4lZLr#v$p&m;&9PKnv(k`Jx z`kDHVN7Xo$3bNI;XA~7L1Mi@w?fEPJ!)j#nlrm3nxAWR3l(unK{ZpRE9Fsf`%c2oM z6KC1n_8;b5Cj|wY-mYrB=y!2w*o8Q#oT?tLyY+Q=v4bc3cwbF0W6F?TW)~EoyE>ba zAGg$?3V@>}*URQ_uK4vF0HUGRwFKhb5OR_Ej=CD}*nzChB2l5vV@XkZ#1uMhOJ%Lj zQ<`a;&+(ip5hpF1pEoWdVAY(AL#|!GD8(#IOvO~*h&FR~$pz?EVAGXubIQEp4p+xs z;v;%qi&K$&h9B*VoyG)j^dHG~Z<%Uenj5G#z~L&Ue^98Hm^|Aw$kQz9kNW9*g|yB9 zi&OW~iaYeCScwm?De-CM3dSOY|5XcG&*L{6)CUTC^rEIem!{j)A0B#v!>n*_zyN=}n^Q#1{%F1mb%&sgNC@ zy?&$pjZElag}Zqvm1!R0F7qBX5Eg5LtW{>oLm}!ICyz%S-|-lk=JWw16#h?RIA>0BN^Gq>$z-a=3}mAuk*eN z9qg%#wwdhAvf7UnG+WE)%grtms@^A|-1<|fY6-%2bp0Jj|I%lKZ@J#jwNy?6u%~{v zE?CyR#X!bGO-`6$PTNM%Y~i5a0C#mMFH|W!%)KJw5y9~i$umB4xA@LFzSmkR&wI+5 zcKQ9Ubn~JT{f{X>RQ~b;452tqoLB_HG-#=;*lA*IN+knyjBt+XcZkr%^_V9yo7H9c z6;uesDeH;cp?v2a$IzrfX{j5(qOS5m;hVFCbFdeM(?O~_avH6QGDb4RnWjkO=4^S^ z_2W@8PFc@A7R@kMVqq7pA#a+LdglSB{gPtIMlrOvz#$#hdHh4QN=crlRAN7z#J8hV zh@)VkA`myFTrSmW{7OA>|A2BRXdCIk0a{;)&q|p{lijkZXv?Y0HcACcZOx4*Z0=WxK75UZ_Xj8-0N-q(R1mRwRg>!9rOA`v5Jl)o~GX1$26TRgl+6Nm(rY< zMAbf?(dI~f@%HRw*{%&PB_^S_MVby{vtB8Ol9i8&2Tz8586l|Nx;5Xs-h94!4;doD z>04#nB?CbS6Y-ZuRaU>pWH?85pTT`&qi1{XSpbEdN@_t~ZaDsCvKd zcrN*?+e$0YbaX&yL^L|;RFvHg?c0jKy<0h>LIZ(tZ-t7Zjp{#lx{>qy1PWAyE_Dt2Bo{ZyF*&K zyE_M@8{TXF_jA8>e}2Eb>zTD&60Ysco@<}yasH0u+T+_}%j0;c44z>#jO6aZNHyzz zqKF3XC7E)eLfgzzg$veTQuA-M)*fDm__AqbHFuQ$}hER&B#ddMX@bwcKG}hRsB+N3B%7IOAdNq*k>jhFWJvjo!Ba}k$L&~$u*A}q>j^lG z&?yd`GC;)S6hlcr$1ntnCqRF{Qlf&q;RF-xNR+H00pKWmv(?4SKEC80%x2#TJIKY4 zWN-Umd`;N+gg`d&!t16>E4BxQJ%g~Nn=q9#O}khx7je1l3^`&J(M-^R)g~gQt1|3) zX*N2o7{au+^1x?*a3x;b69sCx0Aosek$UAP3DmcD<*vRhDXqdVlJ(D=yfulU^u}wc z(7@+Ts6?a8xgL-RdYWs|k?+hMA@{Kvj6BkQ^`81C_9QT!4d%ajM3DKx>nIT5Fa@tT z9q-rB+LaP!c{V*zkAZWbT%DNOT*r z2FN>5(#pkJIuSrIVaA^O&HYJt!Xx|iX>7J~8ykyY|F|wnQDd2>DHU`*(P(5^dAj39 z7ch%|(O-|Ekc?WxdTD>S@C;B$O&CA==pW8C`LFj>7Kk|W5JGx!$3|_QDMm+4euJ`h z%i8@1%=@#xM3U^eAHWPdsPA>??&>kyKJ=>cTvxG}yW?24frG@4fr%pe=g-gXwaw1Q z5#^vQ)j|!&%Sbt5X^y>sc-rT&%^Ts8*fBk}l z*A$-6Do?lnXnI8)O!3kk308|OJUbRoAU|=uEUqCojm>)cD&66aBsh@9)j^B7UHtb~X+WE}O zLj=|CYh0U6nypG@TsH^vxgBoI3woqoZn?&HItd=;4wc;it5+U=z{mrv_58#x{F4M> z?{T;2dDY2f^^IBo^4msK79kEuN)$#2%9-NaV-ggHcWcQx|fSVc`bupx` zzsAVk2Zgm1)lz!plwZahQC!(~Yb6VKjpjB0q^y6b1utk=23`b1Z7w^|&SqAk6th*c z_W=aKIE)-IfVpIkUZS5Zh+~{=*`&m<^j3y1ThZ|lqI)#{rK&Z6gCNz?kPMFnMVs;B zMopaM5QJ>9nLXMPSV|?(&JoO38lE?pn}!kcrE9xIh)EN(HIx`y5Dm^heoSsI`JW@2xmz!X2wW|L&UiTKxA@udJ;aX3k&`#3dF zP<^S4*L;V-r`a6%rM*-!cWYm)#bx&$IHnMky_uT&@}!?X(*S^Sxx*sznB~m*m;7<% zY(R&2Z*B}FD>ky<=Xq$xngAy|KPLHNDLCgm)l0JJ!J=}0GDBTo|2DpIy9s92H<=7rMG>wblOnVqLE3*Xe!Hxx~$~P(`gY ziIb&$*?fs?+wPk&gVtRTSXr0?b_n(v$xWHegVUUTy<}`g=;jyI+&C#3r;;R%6DaGr z^usG(?fRlz)6ME>72cQ|N&xiePjQZbC=EYo`8kly6vxR>>?jzqugkMmv+Az|VIBuzG zZH_M|1Sh$iCa0blNGwN7t&IEm7_;qein%7&Z?L$96-ZxDb%!bems#3D6Y3H1QfxqC zEJs;Uzut87MCKk5@|9nkr$zyc6d~t$x`V38l*xP#Cew?&d+Pw12`xGRWxMNJo1zTP z$}4Q_Y$IYX!FMf>3np7F5fnkD#|tI}^2I*o7K1nRqG{FubW4wa9FE`S-4EvhS|2cx zjafp8XgWi!kAIa5hkUE_P8}`g%QeCE4?R_yhIp@h2{MI5WC2bA7W@=zgCeN%EpeOt zS#e)CkIbaUm8gjJcX3DbE(OxBbt%Hi^lInNcRrG_nvW#U)f=@ArSRnfr$=>DVl#wB zCVy$}H0?|q2K0y8Rb@hooMmLWk5H)dk zcOULWW}#NPYtqTx3igNUMWmD1cbXb30xsIRgX(!RSI&!`^Mk;Ntlxd|i^uJO>+=K_ zI1x~@*69?l2W4o2GLUWdnzs*gpz95n>Vw(+Yz}1|j0j*RcijxdSR}6ed%HQuY+5>+ z3e>#udC=^Q(eeiX%XGnXs{njj=}dLLixDL0>zUeJF$iP|6TGQdDX!9``~pKrM4}=h zyT1_qcfg-4*Msg>$2xaTt1%QTSVAQt3nZ&dj>B8`!r2I-S3ZqEYZr?QYgt+Q9zd(# zok8r8o`BL@GPi8QES(3PM_)(iwdx}=XVzL*P@ZY@NzUm!0V7{d3=v|^?s;P*P*aN& zc_rnZCqR$C7}o?Ah3asL1;Ao+GtG8p9{13f;$ykG19teUr0?G=fXft?LqXmcE`)`! z2CY7s{)DO!`0PMF;QejLj2FHY5z75vL5e00<{nE6M zKb<8Wg6-o<?+Vlj%fUu5_mTE7~|+Y5Xbqc@=5pW>XQxG=1m|z3VF5rDE7WjDGDCiG{{eMk2v9 ziyr|%+bTW89gAT&&mi8D2Lje=`rWcp)dO~ntybKz>=t8!M>4Jouw@DhapdJu1N+4k zTub>=r+bNt(&H{0AavzmrW(0u89Qh=t{A?Ux;1?~ct??Q4g(_~ho{JwBU$=OqnY8_ zDH0fCAUh;#p~*Hm7`h+d9t!v*KJUpW>m>%{Gn(#@dLBBT|Na@?QA;HkA$s%qgMIZ8 z^Tg%@7_#xUVc{MJ<9V%N0WJ+jUN)6lc8jm@G{Y%;r7a&x*ooYL2%57eDq5$K!JCJr zzBJfq^FPjS!6s&!DyO|O_4CG@FhLm20s*$zBZWkowq&yog3HiPs7_Hd+Cm)5YRfa8 z5A7l_hq}DH93&2jo*aO228ObVG&emYPTHy%rT4*v$U+?VCz{3oW?`1b*y1Uqs90^Z zJqI%xwm2STl8;V zSNcKU{^3qU{2v(C0Mc=fQ&2Z4MG-fBaPk~h*8p;_(IjY0Qvbb2s(`lvfxxyWA*Xtn z5FfWE?&e2IU3c+?t$x?YXKsY7x5j*q)6Mw7+E%`g*U zyru5SnO^hQ-<16@k1Exz1IeYcyNf3prlcu(*?M>3{JwvO_oy|FM?fdpuDFj%|Ky~T zaF(8*#IYK3ipx--dGyQGdO?^-z^2b&WDVFm3Fm{C1*2uYFoD^_4F{R3JuZb7%+^J= z#5MnhZa?}3$HI#7;#^Itj_cyR{UBq@MY^TwtfXcQlwGAYW?tgsbuP~Og`3@Zx-@^}wPi=- zk4`dP3hcP4cTtTppkM%*<1c!31f-}^whn=E{9T&Dp~q?SZne|l z$e5DXo8{c#ScdvVr($^ARv)Pc;~c}``=xvgX6EvQtG%z%vY0Y(kgp8uqKPB{&Q^W6 z{6qpEi<7e~d2?>~odN387}+MYQsT5~Z&LBu@`@+TIIhpg8Gg>bqS5#i zQDt>`QFh^b%KGB92r}*j+PNvUWDMPn(c=dTFc1@3ihj1G0;5HO^1J-6y%}?-!*I}< zdX7)c=i2oZ*~*z36hvH(LXDRPm#*I#-|($#p^=BN&Scr}K?SUZL8wLIROAt;@`B#b z^H&4}g#y!-7uURBD(97R4Y8xCRn+{^iN-Iq-NvC4QJ|=d`^=Jjl5khc5bZ$Ek4e41 zo;MvU3nMq_8EQ5i)$^XOP;pL$UFkOtX4GO~<~L@i%iOL59(gbSiWv`&2gV*-sFrR~ zWzEF2mb&1##$B&}93gq*G26T=X3)xAU@aw8R8qQ1=>C^{^I9$fd`NH!a0|sFaNO_H z_ok%olDWVNGbmuYENn&DmXZf^IynUbzl5sEma2!KXl(X4kelPF7!3_ySwwGbX1zDq?`aOUWelK_vj86Oz@$aj%v0O>%hh~242SMAwbieFtT}%h<>X*3@*-U z(6&wZmwx?BgV~%Nl&y^~9YJkni^B z$dj;t=j?w5(rKJj0&9Z|knE-aLn$}uOwh`Yu^!|5fD@?_spG0-AQAgBiZdH`ocAeY zED;nX#a@ZpEA8PJi$tVP33J1!BWIO#lbE!*cTvB{43PUVtQ!t z76zik$BH>PCVnh$?SjI=?e^e9df*64`_|iTF%eyk=rfvJF5T%pV>{F?=Y;jIe=SJa^Y8sVf|OXe zxVa_Ghw|kaer%R!asZpj`=M+q9xl|sA|WZ&?~&aDmy+3Nc&qW&!LwD^wjE*sX=}NC_Y;5fKi#A{O))#H& z^+&^*Ztv&j9eOjL30T<8>)b-0WYb5>o!pW^d!-|*#3PaKT2i-nPss*y8O2e zh}eG|oRpN-`t{1E|2(?zGxVo~f&Y2<|9UrDEl-RFYMKeDrD7-IOZty?FEm;v?7Dy!Vuzp^vEqJA3C1Oa~MgLKm-O(>Xf$4s_!XpJtM7JhBecmAOxtitcRN%M8 z*UD0r%^?Og!ZHG-j>Y);6C+CyZ!D_E4xg^V)0!1wh`UGAXuh@`L{RDM<6YhU=dcv9 zY}$|(zPXRt3ajD^itd#b;e&nD0;)qvjo)WcVl=C~KFK|oO6DZ&YnLc`+;h9TxD{Aq zmc`&OzuMq~2-=^0L?sdEH{4{8ZEi;Py>-C8tbM4%C=XBBaN$BmzIGamtEytvYIelR z9nuk67{B7Bi+5o!b|Ybsk%4%p_)?N`aZ#j7Dlye@@}z%Q#HYr^hxZa7>EsXU*ev9* zzF08OYOp114Hi@S{K*eFU4C&7>76POthe-4S}>iy1X_O?BaYTNU*kXUc@Sg% zVn&$X?si$M^W^ON@dE;}Txjus>hC#dd;8GLEtsv4?g`zyYQOb=>^DP#t+{3`^T|h? zms&3e38tSx$SJ7S`v(OQs+a$~ZI;WEdz+P&^Q9l$YBeGvA@dTvt1KW*))kD)Sav<1 z{K-9roH+85NK9B%zbB=G%Qf_%$sD@tXVn@OwD}PZf)e8%g7uh!9d5wJuON5dT9q5P zGuZ*@iIp4gb{H+wajuyM8*$bu8C$w^sfIQ_d|#i;z(RZ`+Iaq=51K|8n*V@ zA3}AHSU{z{Pj-_}lIPfZNW4-zuY;#gn?iiZ)3aI`6(&tHpJEBZ(N%_So}ET`EfWQE zZs-LD+6#S@*u7Wb^-B4neM^VIkjm#lN2^Nx?r{G7Z+XuWYA9VP{!juxPRgMr#G7W_ zA8g}5!Bq*`uBla>ORI|6A8$@xGFnmVgd-`^N@X;lL3-~+Rhc0p`B6%Wj$f-f{WpB? z?Ck7rOQ{4;@KZu4RQWH>mLHYunr?irAYWtEhvgKavpvVQZ-Rq+(GZ9{GY0d~qH>)Z zBWY_;oZCPi6{bNRs0=wR%rC>xZ_e8#^bAIU`AeRiwUQamv35n$uw1^olla@#wRd6OMhSkHs=wmpm9>PMsPdp_G>> zF~dGjo{p3niP^LohDyA?nbW*h%Q%R?S)z`1e9e(TLN4x;+elz2?S_^~)soC;FtD<< z_k@GPNLZAQ^+r?oMSl{(!7Jpm{Y5x$M}^aG9j&{3B}#L8d!4I!ij@URkG@YOgIaPG zjTk?V-lpymi6y2^M|=;|M`#<8(Oi}tH?}^QZml4+=5s%jg-49}PA(qDZo-)}pp|6d z@=td^38ueYrVJqt_#N@E_TVQueg0%iYIh_j|Dam(^VA&%y$i}3*AI+6nnT?d+owHY zsV^=!J$l4xXzE|CT)nT!-CU275ji*~?l*NO7bxSC7A}3yAb*d1dU4TNl&{aUTK(-> zJ4$sdv&SYNc-+9dKsDF3=5``aM}C3fP!m-qOdZj*#%%J_0&tGR20FyJsA<&O$2+8t zS{fVO_@jl*&PJBh&fDgly+#u$YiW!Ph9n<{kCJs{d~Y0oB>TZ_eaL=t=0kvjkBF%4 ztCKDV*+ske$#5BWb-mo5D2b4zaJi<&Mt*fk6O?@<)f4sn<6CUk>qK;t7LKRyVu#uY z))BUyIK!mnXTXb6N zC3g?_ALu&_52_12npTM+2T#6!%BKriTAb4xpSzS-ix-H*KN(AF(LOtLb8~(3dzm9u zLO+=lz2O@^L_LMdWiwcIxuY-JZY1jieP%dr=lks+EqpwJ*-CJblEu2v+~eo2;iWe# z7QQRp0|hFy;q?1UwRLDu;=FWf%~f>aIi7e6MT}(H?bD^YO+Ta_-__$oUTAP5@pm=R zF_2)+%BCz;B1i>*79 z3(eC5eEDz~o6fV z=jK*6@!(nx{+ad?2TF!b`~8ClkLI05%>`|B?l&I}{`L!2%;vthwqpvXM^E@APbX{$ z^xzvrfJCWSfjD%}FL zW`B5ZXQG9R-K%kY33J*GcuH1`glcIQUzKl(|M@;ny333i+lMs;@#)^hemV#UspkErkRBRBnf>5Ki@<45j(ZuyqJ4>(h($@3i3 z1aCM*Ge=j#TV?gSs0hwueh-Xfs6WLlB6_Jrs;ZLdI;q-v?e#MjZy+#W!+i0Py-b$M z_wiXYEu4{&SZ5EFhEh#FmcLP>LfS&W2fj3gd^%S&O}ELpaltlx5qngi^i0s4I^S5- z4i)3AAia@3eG*%`#Cr7@s5mE%Ekby}40K}@Jr=4UPaU7bnl+v4&ig}rA*mIDyfMzV{8L`lXo;=!fqG9H!Ip_a9LDYY_e{Ad!XOP!VRM+9r^w_! zpy?bFMMu0`eT3u)g#6N6 zj!6ZAW67+V&KcZ;T;9H2#e4ZOb<<2;$VW1vTKfa7oh3D%oYsW)`Wlw?UTd&xc&(|5 z?!tr3^oOEH)5JQjhc$>;b89|<|I~;apzc`H(U0i3yJW;CIQAF_z$d)uvA7?FN z=XJU+>_7Z_JuyCV+kI6M0#|9HMR=WVQJfR_@KTw@1`iT(@rL+ZX@W`idW%hoxvCBP zP=tiDSptz@*N)lQ(VYvZ1_w@Uq=JGqw3~<^DsJFcfeLP18z&WR?g58T8?Bwrt(WJ~ znXSU7mpKXX(Kc&4p7mPz^8s%7sYjug0|!uLYE8;d3#pe&w8yvmx(kR)I;bIiLzP)y zrIM@R5z!xMA5?Pll&XaYGy@rDBHd@7#M@4XKbGs6{Q4Z3Q8D}z%SLV zKE3(dI4UL*@~$s#1~2-O;~yZOK{ zy;e(Dm~4^8vVrv)Ox#O&NSYG$i2TJ&Kjr&9y>G8)ke;)8#aH_%Y%Pow+r6`KTmGhx z5Z>6Z@8iI3c|E#gCoJ6kc~{KL=JwE+FuE=2)HTiArQU4Xa&16e`^}d3(vMk_4dQP8 z?qZ%xh%r^V($C^ALc%URN;oQ#gGsHZ9X*4ce{EQpJ%$wATUIr-8f`RrOXR4ToVMU+ z3?h}OIbeNYZ0z=F-4j$yO!fAddn*T1<#FlXQsSeDSrb+1$D7|PSqz1BD{B&29j$BP z?@!tb=TaD`*U8Lfu|^BS@CbGFR1GTMLZQ+|g~#W|qNm*s@(ir6fimwH&wXBJvybk0 zBoJgaZI!rCswU5fx;Kvp_t0YP*e#|2Q$cQ1WnNlf2d*6>Y(wS}Gb(fnh z>t#y*kRld%1$h6J>%JwJ51*zGTN*EAF|-#df0vbK6c!ceoVANRo~-4ePdLt0!0y(J z6#Ed7Mc3*JN`moJ9(rFi+K*N%W?tz}or9W})_0|ml(7o=iiP;5N@yQH$%#8RG_H+C z=GM91<4jpC7?ueK`!~Mx@s((ADRVTD%T@@aM6+jUB+VdR8;d5uiLM9I0)GlcYpQzW!WxTZtCP4G z6g1K4F!~pA3yA3G7XbmT0s9}p#oIUEag-VC zTQ>I;^YsL5Fz7aad;j(us8rGKVm``!1~X-oneAUVgIfLA)qW@5 z0iQK0t+F1gsT|6=sj`TixL0Qv)yhdU!IkSgF>i>6c@=8C3$$vYfK5}bRfjWZX8tK8 zM+QGEy|mfDejfDPbkj|yD0JFy?H9BQUN8I}hqSw$<}VYqQaU;ov7g>Lisrw3_-k#~ z2D+xr`jqy8z*TV9EFWPo=jXFBers_Icg_22L*bsz_Tk#s32Y@~HK%9x)3@5q*4ICD zE;3Mo$OK{n8HsKa1D8r{Av~aa(~QGr=g(0c38ukWv)bum`S&rI>8N)q6a5(;EYlrY zNy#H64*C-X@_6VMw1oFitAiPjPg*QNu5MqfTN!p8uqcRyyH|#>a?8nrQP*Rp`dRv- zl9GHb`L&)72IKBb&- z->-;>g4njHnQLMV?iV0TAexPwUdE{P+UZbaC6}zHQcJpR%(1%Oc5Y~x1{^scBn&$~ zcQXU> zt!2nx*zB8Hay$3cOK|*0wOqEX%Xj$b?*RT_kY?Ci_n{rk*v%Lx4@L|x%A^bCM|kGx zNaG6QYEZuD?QOW(Kk`b&k<3X@+)A1V4JF!kbudk{9Hd2}$3m*U;j&z62rjLKGIIe~ zkS^G&LnPm#h2-AE_bVzYB+=;*+;T-Wk#F}6E4kbPd(1a^e*MMy`HnSyLc6a{Iv*qC zsI;b?fKBao%yjYV&y8A(J6@iWU!~TT-V zkCtahV`JaHz-7?xDDe;j3zbNUkmWUG(fmZ1-02=etWX3lH~4 zidWaut<0Wh5b9{Z)s+>nTbp(*HS*Emme#!xfmiMCl7aqZfO{$Dhhm6>m}a$ z4Cv;nrOQHi>TYDEO$AUByB;w*3t7#v+5`fq$!~U#^_Yw3*$mjo%YeYy((dDHFMl)q zGKA50w$Y8}3_ALOF){lGSxF1INcAN=pLOj+q}uv<3v&Wz1%oarugW)Aj=rd3AjD#|Zy`%qI< zS)*kM)WK`|#L1rhaok<{=X+aQq&i2(pbXkw!h!j;q}J;+=ez>ddf{|jUY=Qr7KH$M z2VkdPoyW#bDgaL3M>}pHkObB|gTMUB1?_($3}WW)$K{)Qps$bz64((YSq4*_oTgi* zr|1dvpg*k2r~lABqQc7f+PcW!za#X$tIyJuo_kdpD_}^f7WEtp44^3N&XrRjT--UQ0x;dByd-Cz@c|7v*F31;r7f4_&3N4(rT7`_h!uzgu;Hyxmy(e4O9}x@&Ru0t^NX+bodm*Fx=H3-+~c9CF9o9A?VJg$a;ZITs)P`*f!p_N>eVI1 z*2W^@3@1LZvXZ@jbaPFU=(63JY2m_Ys_pwMZm#cum-xKI;&3b~^m(8zM+?7rcL8qSN5|RMCh5=dzi1vD|uSpZZW! z%1QtO!$}DhMWi;Y`0xEt>fcSn`w;OdPbWH?ZJqC&OCa;uc`P8Cken{AgAp7SDAKw_ zl%}vpDSQqcg#+kNv{g@04umE^X_-k|QcQQpt1?}J=u-`I?63}qH}hsnLF9_RsM|vq zkfYoC_AVS8MFECgSzR@h=L_6f=rRMM1?)s7&m)Je*N`O~2c{w{8e z>uL16D3Ms1;=i+V^jEuOCDI1F+bst7hzP9&gZPu!+l&xruS%lj`?BGp_uEyLbOJ+w zv3GbcB@6$RMR;7}&(q9Bm@0ZiaC?6~4W^B5A9bvBgG$_!Q9*gQ`W zabRL--h?1R>OA*b+Sq}?G>tMA6c*N&&$4ZEdcnsmH&P_89YQ>(Y1O(Vy`QdK*!`t7 zD~mko`iOMx>nz}{FTF5DD^_PT{Mn_!@+4U6(M*o{0d`+>@_Ulj9ABLMuA25wbPu!5 ze#)HX#o_xXCSsud02cJhwOw`HGH4UN7PP9!&(hE`=IW1-&4I0G(C4^D_nYS$ z?8S^>rQGy`X+n*b=R%mM8KcJ5fD$C}+7Zjk&mjjWr2jVSxXl2ND{}L2DZd)YGDKEN zSZe|6xc=QMWT{lvg`(L{cFXwTZ|@reCB7<1AQKCVlJPUkn=2OEz6LioGd$rklp!Fa zjSv?X4+wT64a0z(9Zf#3{;CeQ0b`}z8}f3WUXHQi^SZputu+U4C&7~LnPJUQhn6B0 zy4a&ma_9CbK$JjjSPPIUMAh3{(;RCf|Gn&*Q$`W7&IHEUdXMZ2A|kRHRT||uW+>Y+ z{;$z;0J_F`5WF2l6}xp!3$CBynyE01pjb*tNVGR2{tAfACN9-Vrhz)= zSS)X{Nw)nl^v^FC4;5dnk3ZFY4|=c;qu};XGa~2xvWT#lS!*t`-xFv^5~E8x5a^_U zWU6>J$XnI?q#t4?)sHECcw(|kgyh-6pQoDpV-zZiy7v~}CY+dWh<#l1zJZO&>w=uw zJ27YU4dn&qmoHz^WxV(RH&j>bd3~SUC&$k!49`Ic?1SD&PI(={Terl?^96-^hZ1FE z6u;oei&{2%dNiJKBhLrm$$b(ZMfg$}MaycY#52EbI+hoPB&S%YhKM@SXTeerN|Eq?+HL-&HvHLJO?M@N}mx7~cX+2p_g-t1vpsGU6S*is!O z!E^O$(qSr9QrGWIE>y%UOxBXqjKgV)(-MRtiyn>f zA2m_A{sjEG)pAx$lnngDwTBB>t)~)xPf9}%7-NVAd$%8U#FN^pwaFT5a!ptbrJW(> zsvIf&Cf|`PrKBW;K>_=eqU_nd4lxJXx@N+nIYVh6&p)0C+)9QJJTYIN1zR32*1XAl*B2zuAuBnn1<)Xv0=J+ zDJ*>5t#WU5@3ude2jnfOpd>#Yb)M358?@n}U%C%3@4FlwB|roJv*W0@=_Aiw_X=Xs zxZ|t5PfuH0T*hbN>BddvQCMRxJ6~Y6FxK*3vl?cpcB6)srrD;q#j)GO#q}nG@v1{v zcgI4pAd3WlVv?Pc4Id4M?)o*=1K&Gh3{b@UgO`0I@`dJOLjMAimQgVYjOX&jX-%hH z`dxe-HdMQ%F+<5fS@$2F9h90?yw)mR;G zy%ub>+fIqApUeJ;;$APp(Vs5wp-t6uPH-b>bbh!bgA;d!8o8{I#Zj4Ib8jt`l2$w+ z<>o?SXPH!WP zSly85R)YN$euM#oJFS*|KE5^=u|Sm}+$)r>*z`F_53v4pAicln5m2K`)K*Z-?~ME9 zvAVDL>YczmJfzM|;N@_H;>v*TOxY)VKLUK{C`>w0-yo4DBsYkQX zT9zLoyN-H+52E~vWghYw8oRu@GTa(`Tf^HE@Lm!OS43_YnP!O4&nq*(JkTmOxp8bp z2#mEsyo4@QkyZ%_OY|J~(rTe0*rYNx051{D890NNL=$L;Ui_979}?rDzSFQQ zbu{1pf>b5%kuaptAUL@>nk8;3U~csd+7;^_g{@uG2dkbNWmDhoFBQ~)k=!majz)k+2ve(feNs+{S@+vRtiAJwOpK%Npmqfp{5?=`CPKx zQ8$U8X5B2Vp0A~*0_7{cHnGxLQbt*T;oPOB0Z z{ZW!HV;JKDzbwrK;(_^_dF#A>ji+TTG8Fco+8-@w47}oZ*lOzK9@114{Sg1)xrUXh zcXL4D%HofBzVCCGT!j?~*rAZ|uAuKgl=t~^@&O_D4WdS^G4SXA%M~ix+_Xx<0K1KJKMiPCAjjdOxNCt9I#FTF;J4ILur=e# zyTldC)HYKa$oOTA%A8P4ICY3MsYis4%zQ8xAAh|IDtEONgXa-A4Z9`K5d^=1_!8($ zsYdn_;Jysp4A{YcLHBO&qfoUWryC1#ziHJuk>M{UB<@2;x?i;lB(S z*pkGN!fF*rEn4*PK=dllYpN+B=;`EduTq!%%XGmExp!;={mcBblY5kof4*iRGT|7a zen!R@CiiypX1a6`0P!ubUNvV)xqUS*%^oW4wK#yosOeie)y(L_8Qs@zJf49$S7rPi zNZf;mU24ZeE^}o^9K~%1QM}D~`c28TdjOVQ9jq|?$lE3-DfSI4+3@9i?B!@etv(9Z zx8ta*4JzmTWEd9R3N)H67nyo2Qt>VqQL)tDn=by=yN zry~L4d{gMx$~~}^7F^Y66}Z@-jdHUUX1L|5#GMX_xmxgPHiajZ47?f6bGY;*kD*V= zzjX0yZ2Ul*)oisZAqINMAm}CFp4G2voeua&EteZf0^?@H{$8AU1A#N2>8m54sS;D7 z77kJbh>(d=Z6aNqdrbLwN--JY+}ma@=N;A2+Suut6!uoW98!GXLksO1M$7-TXLZE;Z&tL9Coz^B?eoc1GmIt&c2-j~OHY1%8vSKdY zhIRcYZJ92-y0beGSZFCEC_?V95>D3wd6^%fiGCLIm+PLwuYei!mb<*tcBRbHu>pjk zK&|e)AA%+Z&-;5me5?s)n6zKNxv6CL^^w@S=#8o_RGnsDyJ!-5hdL%oau0=<$z^od z-8=XW@wy-KK?NP@ch)XY@?>eyt60^fcgnO@_PX^%Zs?guWgH`BE5gkmP#|i}hQWWH zkn1L}R08eEWPTte{|mnA>95MA(PzZ3==Ut%XKn<^woDXuKKbn=aL{p{=+t=+gy7d( zor=>NwZ)SV(44| zTZ*8u#=Fys986c6IY(&xnIJj^c6_r3--6}K!A0OS<%5A1cG?g+-)Z4TzU#4^lvEW@ z28NjCZ!aJL0U>Qcvg~V~!cl%lf4DdMe|(-gD_^q%m4pxBn??mea#W? zn?sJMXT!LiW=21Zh9kP53KaKnkhg6VB8a*{+|R}h>6wwHG;2(K|I8?<$Al_A#6KkD z>w|>fGqctOQx&nXwQEJW_kuiT+{`r3^_SvuV}FgleEWd{Ht8&QdE5y38pyW*oBM&! zL1w|LLl{TYtd(agj@ykU?BOQ1?M-@RCK;?0NSiJ9fT*7k?bn7`r`0iKI`tM6`_~jy zxhuhgH<3h!jA|v*a5tk=1R_MT`E*6Gvt26ZCmlVWJyG$&HfOb1AhY$vKkVU`mE_Jy z|52iaTiI5ymB>~Ns)T{wch$o0(URLM_kQDcN!N5D$v&&=hJomsg15JgP%&+{*~LX= ztJDQ!Rn76C;Xsh}tFo9yA`sA|LzsYrv3%i?&;P5+R$0(mKAy}k-x%Cn(nXB1;DPLr zz4`Nf3Q}P4}%ue^}4`GV{U6GNiFFu`anjED45m%&@k1 zJapeXySaV&^k_MAGbv^~-d=IKkaUVaqC0i5U@#o8gT)ll3AwX8=gpkns5KNkwfH(?18145BaE7lW zWn@J6>@2(ufg!-;l*8aAujTIZM<+@?X65%wpzI5zm&IgM4S(9%+ID8RM567L(Cod+ z%F5a@&L$(x@cBLWPz%Cvfq~+CRji=K=Pj+fd2v%(CRVP={g70(TxmD3h#(e^V6upK zFr|?un0h&V{>mX}_FH8AH>kLmjx$H!$Xvv=riPCW4(Zm>8s_!E!U5q1G_{fh>W;#hqdNx6 zk@GblV^<5*a^iEjFki@NEj)nuf#FfE%drsSPo={<3T>RHy6P8CHL3TYH193)C-bjB zJ|c6Th}%|+mG~9%GodF>QENtxS#RL*cT*@R)*bU((w0uKiRfb zHv+JR{tnGs1<2GiBZlD(2-R+KiGGa`^<~p_r#iei7}J19yuNYXrpX5?}VOg3WZqO?N9M#R!Xm@bnmkzNg~Fw&UC-5cPRl_UIm`Hr;Fi1#-S9;0jbV;fmCe6XK76yfo%(CiOl469Bnr~Sm0)x`9?W3a0?@4Js58Xfz@ zk+sJ^BtJB2S89kHw~iYz;N9~fdQ0x6`X(nUp#BgR*+=Nj1r}kMZOpS9$)8NygXJ0H z6ciNC)|cXpRJw%}+YPptLp7by4Ec<%gjF#9vX9OhK%IIBK|K3!Ti^L>URUl5k~bo#}z&4`z>aJ+3{aE?MjcZ7C!EPz(SadHqEckRb4ax@h{B| zm+{NAQ9&>dWs03)g+piTkna7Zy9ZoIzU&oI?C_YwaO6|SUGQz10S;VVOiTE{JC-v% zZahUp25{eBl8nc)-m7a|ixrXXKGbebZqDDMZE%28&bY0-?v_sRzdkDYeQ-?zH&Sd5 zZ>*}kBjApV5S9?8w{ED{IfJ+6M5q5xMQaBYjel`IXx`|t(fDVwbx+F2wc6>nHaNDg z_rY{inivG5n9{R3Q8J0h@NW#C3 z0A2d+`}g>?*+fmv&BDF{r0?-zzK}gh_LvM^JSt?qMI#=%|0sjMQmVCQz~^ZqoDUIDb!gPXb&jH54)&w$+8Q(CkK% zEn)ob<@x^9ZxBPl)M1mltMY_9NKmGk$}hxGL@3}B z&cVSvWH-zV=$)yP2SNkh2VRsO$Pk^kPV?Z8;&Tv^pJkbt)C|5V(;jMt5 z_Pu8Y-(Q;r3Dq5$$RCXlRbyL&82W$Z3~xL+dbv&$*J+P`%`+h`{GH;HcG~`Ff4;Yj z3CNF%YGs2pOSnp-n@^zVLCZ@~F@w)mke43UT>|b2@*m8b`J zmme|74jCC4`#Ye!gVYd{asbW1T*+t^kd?ekZ}bDPZ^V%3NOZKD z?J_6OtaP-~j_MCiYllX%FZ+M}6ET_?D*FPMqPJ;vze|P|G-7>6%e32#9hwWbLzD{g z#C|A={!q#a4QQ3hjN)1$Gb9&^?%w4}t@Ic+xyC!|`tq=(RBWPx^ZH5j%aqE~0i(g4 zQ1Y|7T||+q&Fivy1J7u(LZxVbPa|P+P%*R7j590n=O>E`M^NrSu~2s;1Ikd&cc%%g zKXDOUcp)Jn4JENZw@p>=T7C*szoezmXXv&KeJx#>ng%q~1;FkXTu51erddVu9rUJ= zW-baMo}%E?A1(7de^YtWtPPYrZ!FEJWAlW29>+(P=~OrrsvMVl*D~ z`CPIgwRC-ozqCQ>KHa(UcuiQ2aex4zVBpxKF%AN<;hO#c+U$v=8Iuj@dvhu(!eQ_L z-b&QXQ7K{;#xdz^ef7@@3^O=uOmaRO`<3V)XH6;-9y#TEH;!CB12w0}Swfd#va$dV z`pxXPP#dK&gG4nzvT3A0jBG{vyBXJHNYiBO%+sSGTWb#6$zlfG*m&kik9iJ@Rb}{Y z+E-KVeSzLa!<L(_puAN63YcBWuPRh$;Awao$}7BUi=LLOa+?6ttG>c-iW&=d!IhPmq7ZB zMKS%)YV0}@pJX%MhxJ>oO8p6}!qG`mPBQ@4UFr z^kCS~2_qdb2{+Vlc;&Dw{K~X*RPAoT=m6c@ajr?*+FD?R6!?J0wDDRD+-DhoZifXL zye?jW<7>^2&4Ki<)!;`27$+#*r#-y8xiJ_F-dFP_S}aE+V0_?RIAE`?BePT>;pc#^ z^qLL2jPyQP43(wXq`)52q}o-97U-s1<=eUs8bm4#@iJ3#f1i}7N6dGmTP!-Gr|!Vh z!X}iTKgllNCN~mA?S-sIopkq{7SwhZ>nY~v9s8xF&u9zOKC8z^6|mB<6+EUh6vc}WOMMFpJ@rf0P~RF&Ll#X8n)+DxfNrVAG<0h3{OS3!*BxBMeAScoD4N@O z+>TMJihsefkgIBdlk-ol*cX~#lnnK3r+CIatoViFxKuYk7GsK~x5#}gnEEZl0M076d9(jiX)DBN+tx!`9 zL5y$J^gaHLRn&nwQ^dl=&&;-|mR%B_KGg$rDvw&WjuZUTnJ|tgF%n=N3H`Xek51a( zt(D`Np4T-$Qpzp*B&0=x5ctzMe@HZGNJJdq;f)FX3 zK2d_&!C${*?d61ofT?7pj;CYi6QyKLrnTDIPLi(#_n2k*5@~VByqqnCNd}Uud ze@fYgcW_}eP?>w{PDO%OIhudEM`4Qw|-%d}s&fZLO?y@&h zJSuDM!3rqiAw}SAYHA8y2s3fv{e{n5vS$>_a;2nHmhRAvc!^yPGWRu4g0th;pVH9Y zO^V}IggIWwYQHv;{V|Av_U8$}Uf(lTLd9eZ!#^7B^TU}yPq2aUtJU=h0S$pWhllY2 z<2^{w8rGS+QbQlmaPR0euz97C;l2CEaFCr!<8Fzq9}o;6UM6(f$x63V?&_2GOZf=d z{VSR9My0rNqr3Ln>=z@%PdRq+N2;mGo=a`*ftTZVVMTn6wW0WlOM`b$%xb_pfFFu_ ziIV7*bLL{G)yg5XmZWc1{d=gZ10|O}j5Ak_vI;aVDSnA>DI!UA0Gj{v=ckDCwN{~Z zrO&(D8)Dqb%gSujd0pxI6f+WyxxdmXI%TG2+E$XruW#GZ0&%%zEK|PM9Cj{DbV9~V zZ}={YC4=l&4e#e=Wz0Yc_jB8V=-s>drm|Wnx1>=<%SabsbpFv1wcVmcsL@n#@3og2 z#m6rs`6{N%pggWY{9*$<4gQ*@I$_B@dlHZyK0wQvf3_4dHb3sI2ZJGs64i`qIB0vN z`O6_(3>-hrIiCa3>rfAQ{@>oGGursF`Jq1e!g6PKDMd2%B3ZF(#C;1YP5YW7Q`GTFF0EqG2ZaHDD z4rTNPrT^!5m|C#5?cuBq5B)E6Ek;0~6A#@Qr5 z^9F;f-y+C%nKBTD`uzkDztxQoRy6dMaMHrM_XKdi=VYJ7aS_)9f@_#0ZHbG5qQ zqE^-Hv*T|F-_)D$T};3qXz(k<5@IIM-WNme2N5}V#sO4>8eSL!VNa?#pgjlmNHvyI zL=aXBH(jkZ7hE2jW&D4#g>U-5PHWYhEwYy(<)6~S6=#w~ice40d3nFFf8y;R_qI2g67mLmY<3C|}>UmRw}5BIg$@0B6*vbhqU0K_g+ zAF8%c@lb4ieCpI+nB6LgD6eK}Itga@=yhR2NN@fsl0q+ROYV$b09*l7DYN=cOGYFr z#x*-{2@mA^#@vUSk|}*TxdB@-LO=!h?WDa2IPG-7PRRsxO$Ou(2P*lc)^dT6OwC#M z4COjAwLy)Akx_ir8bU`0FNZxM0A@-q`dqUTxElL^>-z|FDO9aXvy}?CPb%P?ng4j< z>IGNw(A%%R-0czKUzdE$0ebT7={yVog9XuK1BSp& zB*N-otg3`sN9T}_S!O?%HmCj9Jc=AK;;)26c{$n6yg?1qQDhWmx>u)|+Y=}pTgC&^ zRI*gL?%^q*zlD3}G=LwpTD-K;HwwOgU%OmFYVf#t#jMk)Z?9*FXj%QRl?dRh9K(ft zrPp|7b0si9-oLh@ytTTm4v(=H60x_|h^BTP9hDovyyM50T!{PZS@lvM>2%4^7u=i5 zaU7iYHtbW6sz4+%#yoDu$(yba+?=yaPUUDHCtW*?X$S9T+rAPJeUQ@GD=YHl5E{Q( z`Yc{^HlwaWDC+(|bGW$eq(pt;Nf-7MEsKGI+ukh(0?}-xj*)j^y=eJswX(K*)@SFk zuq~~%WT9}Lu3ezaIu&{^aqEg3bxeFF(!<5!yGk*@4x{=*eR`Xl&wOZ}4v#8J49LO6 za`RKQHYw}L9?xh%&m<@9iapx30G`z|_C3~7I8=e%*zUwNO0n=A7GYwBEb@!T=6 z;ep!f1x+D5s2VjaUW5Cc-!ihT1$wAs-~BuvewqQyWg*}A`6>+Wi7eiR#xd^&zHlF@AC%s1qGQ^c$U zLL~eLA7N=EpnY7rh(1H0mM@qc0M!Uj)a`7|p*M(WCBD+YU*BxZS*0q@ct`yn_Xny4 zlpvZvOYX|7Mw3os$w`D;M6p^xBj|H?rpsBFi$;F8dDm)iJVSa#AsG&{c4?imI(vdE zUB5Tk7SxcwbPAh1w@{2tqj~u-ZoCJZ?YOSBH&t3%CU9nQQso-W`8LEmdjtvXV4LK$fFflLM7@oz+6hola5S7=B2c%B*K+62oZ2llXY1$| z6%L9cO9ry80qse^48?O?CXG0@nD6*iW&Al3kr!4JR7wtMRKOk$r^12=h>Z~s>ft_-X3DsZS=>qPJxBc2gkGRxeC|cp;W~YIvdJ= zFPggEO(hGB4DSS#3ur1eUhR?m>~1?ONpFX0kdtl(^l?nPzOqUfv3cAjUwD{fbSh4%_K}iwk?S=;_CYCNh4fYw&?xE4>y47cnNyq#5S!ty8OL(A{o=6hf3*O@6cn~6)VJs6 zEuC22Kq)mia30`a^Ev3En%Ho9OFUUS=i9PZ#(1 z_vX(9lAO5mh41=IZ-UNQU_epLTcRo5#Ydjha7VK@Sq6_?+Gn7S3lY7`9rO5Dz4v zbO9k~xH0FVR!wyt=kuts)jrU1B-yLAhOjBTEH@y48!ik1=&c)klzFgup%*xanS5Ek zX-|@CuNG^Q$0pG5qp*aH@@lQ|uytwBq%WaVA+r@v5x0hz9~Ac(?sV;V#4LaWnSBPZ zr&P?X6Ss#@WUs4k;fTwXD(CtJ?Yg(DK)r5Sp)kiTR`^kjr*YMCPS`;#@BYtUbGM}o zc6}neotZ4q2{9G@%lxgD`vDeXpmRP3WoqyB|)zMzaDT>8R;?zL;>b1kt_=IrK(arngBgGG7VPcqxv_E(| z%|%4tpC*oN4g7L7-T0!TUg=aP1{J8TaOg~8x92rcAPabjmftx%#j5NG>JHC$JNkXN zblzzW40<%yUeMe=W&yF@*BemFtE6akdoT12h0eG_;g{bU5l2aVgYTx*G%>!_%YuX5 z5_LdtVoU=+@8MNl-JdfB)E=l?&i_uh1_d!dhv-Xq zu>UQI)gs_59C9{*>Y~J3%uC!Di_yUDlc#2jc>6!@1nXjKo1Jk*F6 z@ZGG~$U)cFI~;3kxY#)OQ8J3U20JO%L_E&1lf$L<_CXpr-YpD?pELt{KHE>04rN8= z#78shv__pw5M+CS}La_WZ^AgrK*CIg!>-CK}hHGr1K>B#0lL=b5;gp?8wl&JC>(W&l0Ucfed{~DN`pa zx``r5P(0}Sl9U+oTv&$)=ps{XbP^^fom>_+?ReEWO<@m3yrMJjp~suuw0Z1NOq$hj z?bh0u!TPJ8#!!%4OL}%3XO7uk!uPo{1e0TBd*@mLNi2@w zITAM5YBNMe#S91w!+8}={&U$n{mU2iU03z3tVQn}e}5Pk>raum1uW~m#jX|A^06_2lKdCX$4kq*;4Y-kWRHQWXsIB{GPx?aN68Ft@M1%giJ2DGN%b)3TABIygYlQ zzumwm=j{UdJvP=muKP+}u%NsgNkG6d={bqK(KG>S(Dl(81~>`r+XfXr6Se?Kcm~PR zZ=Ij5b|!vkwuAG}6xXvf3_ z!y`KfN2T(Jt@xC6L7uJmWtk2SA#Ka!F-lAZ!cVO_-+u0WClwYf(#6{6FZ6McBSH}i z#1TF^xW*2R+zTrmR@MCA938B+&yZ~MR(L*X5<@MRK|IZz8I{z)f{YRc#+kmN=NEVb zuVZRrVz5_6oj1i{8WdZQ_=Hd;-~KeMXkcy)YhEYeZ6@>9@VL1F`=S5nu+%Is5-8|lre6*^Pe17l zKMRLYv(pm@W}vjtH_LxZ5IXOt3nTeBIknWzN;1&w>hCb>8{q*~Gldhcw$UtkA=-ck zPRQ%@!#ejfal8o^g6XO(i|NNIrQv4wmg3N8<1Lou4$Mj;yzsz4sSb{I59v{gg+C*$ zYcYiLSqB5?r{9e8sKt@rhrC+1h4Z*0R6Y7N^wC4>usm*y&9Q15(E+PIoRynh@>A1k zUcwgB9K=(=(ko2y;}5xSjEEi{vo+H8(ht6)+Mqha^?OFaLyZU2T%li;>p;=c(Mn5Y zeqRw+yiFT#^%_I7#sLj;|5G=Zxgg>xh!~wTg1Eq>)rMl3aE}Gnmnc{)l^|*?U&`>u zU!${X`^WeCjT?)k!VZo%zsLSmB_kQfC)Cx|^%UiCuvrZ;_R!M2pyoh(d~n5IY6=~I zXlQ=Y8@SGXc7%e7l8Bt6G`+_|p|m99q}CT03&-js=^Mr!mWdu;)32zT?R?gC^!~m3 zE@)P=)O#!zzx7x#ILlJ>*0PYZSm38 zzkvm1wrP*G5B&%UGX#!q3)Si|*^hWPSkd`!xnyKSc1oIIY()&N{M+%$`p1@|$$nz@ z6Czg_47<2g$fkRnBBXcitC*O)`f$e3rB8HeLL~JJH?G32mmsVJb1kFAEdW@vKm8-t z9jxe9rKPM!9g0<3-p?V(L8}hVdU2U3bMxb{XZbPld|;ZZFEzDtBRk=s&yv58Ws+mRPu1bcki1`xa)gTyT&G9Q?&)~F+W5TFUMdkw}53XQn@f1-75O%AQLZl z_+f8wXrwPEJ{2Evx47znh=fGGcIypUb2&BX{&4T3;T~xajLeb>y<^7 z-Z~AQiSx8o2>ugR(nhqc)pWX1e zU9Ls-9nb{+EU+P%wx_-e7@RV$t{|uDOr;KAb8ueOGY$As@#e?O$6vR)^H!xyvpbhQ z!+$o{LVs7h?xB74f{Tl6Y^?H{2rvBGy#+EtG^s!oFv*q1rJ6jpug_j}7wM3jO|`tZ zq4Nd+GM-5_AyvEu6G5W`9Xv++cYbwKwyXc=Zj`97pAD!q3F8 zcz*jIdx!rYd-pNtmf>5RvfB)oh}y#vp_dQyCX2>s0tV;riS9J>+}WAjoOM>cp0`>p z4rs(0L^Ea;n5tS_LcY4r#Wx?QotE__s;qceG!bU0)Fgyf8G; zfB1^Q>wQS?;3x%$P8uw>M-Vk@d?@5ZdKYsK=Mx(ty_1~G>-g~e)jQqKKVQH9)vJY& zSy~u0$RodKu5On#`g5SmKbMvjBo5qp4zjuxw`%(|r46T(2qW4pbno+TS00TL3z+SW zI+gDQ4G@=Gu#DuPp+I2SnZ2TDuzlV5Obq7p{dj_x4`y(5dc+k)0L=gwYM?k6Z?1nD z`H~$;F?;|Ynbq^*c~37P;#WMcD-@J4^#&_yg0e*_w)UaXvtwe|WuHLNeF1FLpf_@I zeQPS3_QzLo4hxicsFe;-8zU1r%uV0}PR^wuh%p&?PFWG`;EJv+9FCL2ZrUFQys4Q{ zPtoJ=&}1vouCR&3CqElD+Q*814GUwlDIK{{pAMo@c_ln?0s8hFOBR!u|z`ls03UqYy619zvjPq+|zq1KMDM`r684*_#6qjJaF1cPiUKr{Tg5 zN<|$k=o8A9=$x(SoA#*~x+XOkX4x$W2!J};hV_m;Gc!Vkw?--ps7dy5oA{Zz!4XwU9@v9wLN}2Xm%h%Tvnma_FgdAUKymw4Y}y;YK*hnsITR% za%D>GrNCY)^!YQuba?_wbStbn@vdu6q%bI0oDL?RYtArr?7rmq-KN07sL3j2X|2<~ z!{gGF#V+CMD>8X}@>D5Ud7k)dw%+^Ye7$+_)L{M~BZWrVcbfO?L7(Mh-Xe_s{RQ&K z*-Cqylue{dT=6&VIV0DaTt@?}lQT$^yGW$B61AytBRUNu?+A@-ktnBRJ|yNiLaen% zvmTsYsNDY7!otlHlc-5ft0!~c{R1c%nr!8z=j!uq?a~dr!zIb(y*Rvz9GnuH8X(8s z_)8^7rD}57)_e9U+XcfGBKoL5v4IKyxLClDdb4w%mpa>|*))_GRr_*LEB%;%H9+-{ z6CNS~=k2EA5)vJa+0{jj15LkgS9lw#^s5`6?uR*YOsJ~*Gfe_lHB&#B&zublQ(b8x zav5bmfBm6;&08|NAA|5CA7*tkT}Hjy9t+a(YYa30RiE4018%ClY=W=P3LFr8GDjy&Z4Ug{J`u!&8apCmM#EQDRi6O&) z-Z`I=&*tp7Xumm=A(2~?S|G@!T`2;lZih8KC3TK16O=FeFxGxJ{rk6m^Kf&>MDgvL zH$7Qn)ewlFGU#H!w?5n~8gzHhWVzMdJBI_Qa2n=*|AR4DEMCs$&FIgM{F((-cOz{= z0=c@Q%pk#hnNULC`qn#qc3wR@Czr{fora2&s%h5xjjk@%RIvvYH3J$2tOO@>b`ExT z$5JoKrXX&z28U5)b?xWYDCf$4Plb9iR`h6!MPzMl?Q;@-mAno~W;LWK@0KsIv0+#K zneqp&42tF%iI!)AKLm3OYcExP>|;tuz`?gb3*)Owi^fgbn|KG#&*TH>p;FeqB7gr1 zKsfiFBB5yl@IseW1Y?uZKJ4>F6|KXsHE5B!sW^lvz3%AmP#TuYvG}v7{#Qk{5Y(YA z=^JH*)Kh$(YMRpn5!~_7>~p_u@u^5%)xq?c$sQu%<#-$d@|!968w!ys6c4R2g>jW2w~C@fMx z>yM$C+Vw$4Fe7Xc4a3>urefmsgKd9bFEux_PE^7`Vr=q-c0!G;fgD2snbAtW6rjJD2%pC;xrqwz_Q*zbB!XC6OK$z3^FevyY zT9mL~y3X@<3ciMc-}tk!0dx|cspx+lNRub0Z=vk|So*Tydy#L(3%n{q|8IFiZ4*5` z$U?qWNZCrEP??AK<<>7jkAw(0!uqPJs=o<_)BM>=e;qTolUSZ3Y|}L#M3OB(rHpQ8 zhq(iAJYGIFjknNmo9^zi=L|pV6{et-b>`K&+N8#}H+f?54>@)1nWv{@KXc7R>S_Y! z91vjh+7%W0P_FN_4vbZ%x*977iiZX#8cOCmR}^z{bvZyJ_&%>nAQBy8W0^ATYs#g{ zMdL!P%>ZH%d`O^gZD&-V7gJ4a(A2~dg5PVjJrMIiLO)ya-JG3Fwb(KHIBM!-D6QT2 z1Qh6v&x(QJ*aXd^uYoW&ytFwwVZKdKHUEVMNF6F1*QvmrVw01@G|;B2po+@4hO-MA zxhA#r0Frzzh>HvH(oCVT66qyArfzCg)x0 zZ%M&6?H?AvV)NX6qyFhrMV=JQdrAW%Cv*_f$Q&5nfw&ETS3w}MpYt09j^c_!xI38X zuF?VQ%U|6pJ4dP5;&PhqB))gc-h%*D=u@qo&kWu)TrV1z_D#V)wa$UazZ5{%IlAF6sOZN?BAhtO2O$t*o4+_Xx zMzov62!q=lrd0{Hf7kH=q(QyO#8ZPO1pCA1Lfuj#Z;`-IfZl+hF=G2nnU)nj0MlpK z{(j@8$|gb8>DUrc6pyvqqx+EE2IIv_8+em?d(2ZDVuVTTmW@MOzM$`;qJ|~+p?>vi z{CEazy@Sox4wK=k2N|jUU&FVAc%=Csr8|TJUNO{`VOo0ugcbp^FXAA+w%PP;k`r`R z{{!vU(`xe}L~;pdKT9DZkz?9AoqCcXVnRA5TB(2+z#+zGxRMsP_KxnzQbH$T8G!#` zWgL9y-A~ZG3@hMsN9K9?NLN2THaW*&)K)I@7$#R90A9e0!2iCy^tw8=jAPmKmGETqyALaOl*=wKkMe01ZU;YUdq3*; zQA(lq%7odux{l}D^Oyvx1Xg@7q93VVaPc9?Jn{#iso5mzOUSwtW_}5bEMbInBnC>x zuVkjA!Fo%;ZEVu})-!_-%ZW@Q750n_hkngEy)#^2)3>%pf&3i$Ic~8KK3{vJ9>$zm zC>>`_%l8srqJNk!%mhb#fCT()ml(bx|AK~bKD8 z4%U!4r2%e6+$h)Z;>!PaDtzop0-3H{?uwV`d7tU2%-LB_1_wojqQOX-uW7uvC-RG` zNb+g0GPSQ=+7`8hHyJ<0)E|k&d*veEU~b=*e{)N7b9)1ZilW{qi(j_UA-;^d{S7`p zwC0q3TA}UTx6W8IMlLRBYI9uLzgtKgnT~0)Ts5+d;%Sq3F}d7d3LGx`hIhg>bx%zZ zYhi|XD^TK52?+u8N~mW!pg)!Y`V4wIS50473*oJ$9x;(tj`kAli5iYFzi?R!|xoJ)fa1~8EF z6`!7i=9rf{kJ^F^6M$2e@nq^P3%DJ^!(+ir-GBQ6=mMtlnusXUlx5`w z-^*G&&HU%>+-+c!bBs@LR2Uq*3zU&ebdCe-2(fdi>YuI!SK_>9BjRuut30ApgyQ8i z8KdQAHPsVB^nUGHyPah(Nb4rr?QFz8odkDDYzuI1?=73PL(GmAQJSZNv zaCx;TH7D@Udp3zHsu6Z(cAso6$q4$8`vDp%enz|5L#4)YFCu&d6JQ8WL?^eEP}Zr# zn;S5M0u6LXM}i!0?=UcG)Ye97{>O+sEnu4}-s+oy$hJ1}eXG(h-@b)^-@q$ntX8YP ztf*jRacNuq!dfzSO#(L4cb&cQY3qg-k|9%cQ%3GV!wDz*eJD=MzEDtTAjm=Yep^mY|NTgtW z2!wy}>k=84g%XmrCN~UZvcx7B$cXh94s%~Ny^~7ezJ&pY6aoPSI6<(Wy)dhteU%Jm zW=!FBLly8d8w`K{?hG9IMsyP*V-pg_TxeeYSqjPeYMq1%BHBobF$zHFjW42~nM8JO z%l0Du_DbBI6+@;o?voC-q6jILq$p^)P!8P%fyC@%V`a8aSed+<8WMW9pR)KtMQx?g zXdFDO{}4n5WE_lX955r%UwINeR2*dP>V_$?QSH}O(0J`O0>}5#1prz#UHtMSt+ka2 zf!F7+lN0Ygx4Ds-QeWx+IzPG`^zRi0h5#yF;Xds3mQ18!a^?WAqSku^KyZhd*JdN- zCa64=%#Auo@OI3MuRG-2zc?Z{;nRh+T}5?*n^hkNQ*Hvba}@bJlIApXNBN*>yt6X$ zixm`*Od$!7lxXndKBn^Gy$w3G-?cJRBI z#n;i@tlML?Lfoav1(WOow_J&!$I&|lDLo}*(6zSQ-t&K(g%!ksr_B-+q!Gsac>gz* z*XI-#((&^rguHB|@Ja%MvqUWd0~HKeU^o==VCIN3lm*kJx<2D;LLf~|S`8na0zr2` zkP)LYs}Vw!x(iSNHx{LAT3%R-iP^39cWsA=*Nflb+N-n{YUu(_>-pg_3?d>THaKr* zJ&jd1&)&z9ARIgsuVLg#i~-@8oS>ILyo9-OS3;ND^TuLOt&?;~^6r0G=$j(ohv_%~@AI~vhL zQ@_FKfEgU83d3##JKmfRd=szGdpD8?(Rj$s`2Vim2#col6GVb3FMA=eW@D5+hG;0$i5gjID4*%#n~=IKa1abgU^judG3-n~$1BWnW(( z1kxrd3gYVjdJV9tlLutdg-(|$S+``rsWK{zvFX~a4ZXayAN#6j90aID7I~hkm_{w_ zq$DEqnz!Y0heW>@bSLt?1p>tbgJi~}y34;+{;=2J@y9+*|MhdI>)gHA{sf>djnWsE zxkZUG#C1hVPD6yKHjId50?03P5uSFhpCZ1i3n?uhpA=QY`^8*Sm}}<=oxCDha1;Ny z_Mylcrvt8p|Mifk3fbt^`eHWIU#Z@FA$$^|xr zrDyzO1zw|zwby&V7cSECe`dj^q~U}G;A$UmMR}X9HT6sn z);P}-F=FoNgk#Goc$F5xRXy8)k`gO;gpf5r$b)TUQ+af>JwqxoS%#>{W4f`qxi+z7 zm)2}2@x!^e$(xSdM$&h7R-H;fK*r><6kz<@hbsglm@cfWzhyoS5qhCwK1~XursI>! zb2gXHMRZoGufq~t($+H{kr4lYD(rI`*@_fHEkup$xBbXrW8@1FePc#*NzPwpmkh)? z0i{v+A3ls%xSZ+4&lF$)&R_H(MYwQ72dC-Q+7$ zxPyNkUS1NvXVX1Cl=w2qU*xVP9T)7ZG&g}iov;}S{rYsM0L)J!mf2l%sR0aE8&h*k zj(MEN6Zi#rn<+>NP|gIta`rnO>-~BGFp0uLWD)||zdmr4lw|&hwPpmes#w zqNq78#R40c_4kC7nH%1ruv_rz7rS_ofE-Ynh$5kHVG#+&VE{Dgv!0=K7&p`NY;cXb zh6W`+KW0p3Yac*JRdmXI`&G@<%L(?aOFRD9efje3^Ic5F`jIkW$JQ3TS9wuCD#%I!$8{(t1Kf_3U1& zmMy)+i7eRV&I=pg^)(O3$PgANwJp?;!eo$l{CHIcPfaax$rr3Fwx*t*mJ$teL_PhH zZqPmsFbpRPo$DYl7f=5rOf*~1P{KGA8`s@m>KRjcbS$#(>C8)7W1H%Wxv>jxDB&&H z+q3@b403OOU-HotbviaZJpg31FPMaI!Q51_vscdgwx$85qj1MFzs^FVZ@xZiZ|CTa zj)3l>{6KXR`{i3?{ZT;0)o=Ttb}|6h)R>PE0cvM;RWIV(H%@qO%--a)8Jugn!1QvJ zYDBZc1&DYycEPAlXB?sJ_k_IeKYD%Na9G*dJJLNBPgbPn3>kBSFZ}~rF+gBxa274` zto#SZv~mR&1}bJZ;B>MI3L?fN?y@|%Shn|esa`RY1Cf#B8};vtM`Bv?E4NQKK<{3K zhES41_ab{t_g5AumQ{rGO|8R*h8W>Pq*ouhw;JrXVm}Ph{*T>TK}5B?kDe;hLHG~f z4Bvv*GwR*c)j1+zODjTBrdq znc-vE6e}A$kh1~hi(qhMM%3iTDGn-7>I(zCbuzA|$Fp;(kvYr9V9lx*F=5hT;Hc=D z{fSMYI^thiYLxJngv1g|wjoJQ4riGyZ1To0Fpfa{y*|)}eKt5`Uzkbp`a?%o7wMDI zL%EsY8hTAkdUq$1H=T>2ny{(PZ~F|eE-y2-PTm6MF>tf(qQdP)wFTWRrr}!hW4S7V z7A6vSA*;QEJnSrf5K&Nk{X5$l%(W92&-t`B5m+0KpvrVh0VGu~GwW+aOLtZNtTbXI zlq_~7L>^4%F$F&?N*L(C2?>RHOPjcGdq4$ay-UD~FGp5hXS;BPT_vZcb{2mX0QrP} zgqp44*+}!c(Pp8ns<;d@e1k`$~=1ifT^*turX`eFiXAZdK?ZzLo#jNO_GeI4F z-#^C`1)#{t@mV>KC|Jmt=9Mr_Xe4)g?KnOH&xe>Pt;+5c6TX%OujR<;e1;9>?=%V1jrQcI_`%o$s+r z7OE6Wjk&-dBY}8NbCUL>vT~!6p98lQQ6V@vnQnLT90o%3Dvy$r@xw0fO1=s%cSj`v zMTSy^rJXp6iO3aeO0R2e!s$2?srg>Udn9>iWboyO}#|nL{A)-_C@kWTew;wM=w=S8aRJP~7e zE+v6s@AWTm$Vc&XbjalpcyC>;fu#hA^cX*P;q>SXSh6Xp}Qx-EjB1CFnYbs~cFZ{Nn zPlFuMkVxW`Cyv=y$(FsZ;e|zzAgg~=af%c0FngqXd&DU5LG(W`mtpXR8G(pve$+#g0#}H(K}iS!Waw&LCr=NXVtUrmWmXKmtL(?(XhP zb+x3Pw3!=CM!QGEtooR!a7ecpZTsf)av6=0cbBu(#>AeL@_>b9V?(K|w14(E!N<`T zsxvy+4KxjNS64ezQioNZE=ND#WeR#qm0tW99PdJJ=3y~r2|fjCg-14I7M*qp2cIwW zAkbtWR0wEm+kPY`Bf;_)?WoK3yB4OVrIlH$)z(aq)4CoS8Ui*K9w_f-x9)+E28*l6 z;@lm0{zOY2ii!!4pH20GqrUr~FadS3|8zUuZ%}DiDEeAbT%1b0n;Q6-^c8a4p$vBM zgHfgb_ncPy&RB$r)@)320wGP6_mf&LOHmDE?1Nq+z&=(xndtjc^P$Mf{@ULI0s6IZ z)A`L2Ft;?HNjNw-a6mB7Bl(^pte<0QV2fd3dL9*E5s3flgil|R1MSj2mj$?v|cYwJCB7ZE1lQu#PZ z6?Ne$Ur9T){BhtV;%K3;_;~iUQOD4bno*Zifj)@=F!uXaW8Aqr+<7HWE?W~blLe<^ z0R$nW%dLn@O*KK$v%JCVI*2VYa-ULqxRLNgxIfl@P5r1x1L@f?Row^Kc*1bzKs47e z0&GujFSQP1!zhsRaO9&WE^x0%JL_9o_de*3_u34laofLEWlS1Et}vJu*COiF!RQ z7I~|luI)?{U0)O_Wo&DfU(-Tj6aVc^)kK7vb#E?hkQnWy%k=hKDIDP9#SZ=!(=$>% zC)St)p1-G>@Sk*tTR!jlX#8gf1WY^a0-~l$pe5xjp&eLW{kwzc?Je{JO+10Xq;Pu! zNGY=mD3zqsFMb$a*1WfZ92Kl84W+dQw4#722C)Yw9vq3RsKKhy>(R7-S;*; zhzW>ArwT}i(ybsZ-7VeW&MEFg^_>y+(lG7ISkO)7&0MBpn|E%PwzcP=|Z@z?o_&LPX}67flA~{!Js=eqC@9+zdwEQ%kY!ON2_N} z>ymC`5!_hZu;-a*a284=+XmtzFlat*LE}9-z*t>>Z*#_6)qP}E)X9mtN6`1c#I>@7 zO=2g0&Nb$0x_EnoK{0$GWrVPFZ*YFw2=LSedM|VtmcCX2u?d-wGnV(I^4C}P-*Peb zt(d0>xh-@E0X?Bu6I@pb{|mO-bigIz3%S^S1`nI`zo=SGqO0q>K)k zCU{4+m8G)u85L*xGzP!&3s0OTnW`uUmf{T82L##7TjWp9dGR_gc?WLh(LV%Q?hAjp zNFKGEY_rt)FZ5XR>i6+TNJ;%2V(pgZKAP=27Jv3}c@Obf1AT*mL%-LO#~xd9)ph6g z!EdnP!Sp;`^azftesJE=IK*CF=_9{8pit(CNffK8ogFj(~nVnm@X@GiM~yYnIB);MS( ztPrLT$gq308K*Ebd%S3a(%uVgj{a^1XfV^2Y@tJWvDlLY3F_311hii|jv$ds9 z?m5hMl->?hx70TqFdlQUP%P!Jm|k^)fG2lR`73i0#Si?uGS~X` znQ&GQi+*B&rj~)|SOGHRJ2fZl^jZ<@{bZ7pA_VKxTURvm%>R98|6ZNO1(ME0&%Rn1 zQ$vIW*{a$WwXDkxc9-RiQx6=`XTqKWc`D5{708e?zu6EUfXiPOt^mEV9kJo7`$a72 zLEW3X^Bhxlj9~L+S}rR^Aj-|4W&Lv7_WEnWnoe2WPy3(ELxeCD$~qO$Xd_-+y}2Oh z-x;|0Xtngcxle9Gfvdfam;`@q4-agSkDzFx_vQ&k%g7prq#QGoF5_G$F>8z_+ro}S zz=HV8s118y9GjkxX?nHG^q$h)hj{V3CMK=2;Q+rI)Fdl%!H1aK;6F~5^l&!NqmWCY z$1y+5(+Wr$TNTno3=Gx7ZaKRT{gzdtaR5xhr?pe!CD&&i&}Pi0mp+S2YX+#~jN8Dm z0|3btb@>QY$5ln26(w>7&-A!`9z2OXUP8=|4T3(P6#dF>g9l1mZWKT4Eg%FKv%%8Z z;t9xaKL@q~{rW^{LCHSdAqmGF!LyLICQ)yhM1Dc9Gc2Iw-!`HweQWYAE-sEp=v~wI zyE$;~?U>xxP|)0y3+zm=E9lBlYW?fwy2T?b%u`?rbPr}U23I|AiLF!;`@Oo=Yzo$3 zLkw?O((!0ld3a0+c4y`9=@ng7sCH>7Xd7Qjr)&jU5@~m9j+7>+s})QKWWlSSEbP!W zpzfP@_m8&5&>i8o#p(=8tx=m+M=2<81tqx-0uRO*QGNI^%(8p!Lc zPypjH9bDAbUW*%uJvvfVfCjd)O_V@GmFDH89<8oC5u{RHwZhoLV*NH5bdhq&tNQf` z+y8HU1>oT3;JwtSame5V+~;H$ZBN3Tu5a4m&s!b=J~j8%y@= zr2cTnWXLf=NlogwSz4)OTAql_nncZJBBUktK37W-B+Sf@Q^qVt@=FF#v(L~3mYbQH zm{6%dh7~;F5C}{Z}zO%^->`^=^t^8I@fc2x&R zt32|Z^QLQXZL!DVCL~Sx7VN9saW9&VnJOqPZIuvGvEg9Oqh>epk*&1VMziBheAriw z_ADpJ5cw&y2Pq!E{<{d}krJoz2U*g5^s;o{D{A2-4hKs1yky|3K$q(gro=!5@hBGP z;7*{kO?cZEt2qZ?=s-(iet(w6`~G~-NVKQmlP8+H?#uK$J3#nMyS`4_eQg8ecChNg zmv3eWXKIl5y;JH^74z(EUT{A5^M|wN!w5~;7ogbQv4ll49iZz&>&oT_Geev`hGf@B z{d~Avr2F<22Nzdp-&EF*GnK`S#cy)lx!R9%u4eMy+J*z@)|&UF3D`JUgF%S?iPWwb zGbllc4G(|7PrkoO1?fAoo0_x3^eNQvSp*3hey^rvWO%GCq?P- z0&Q*KEsAcutxp!s$6l)_smZJ3>EyI6`t%@fD0oojRkVMnDWOS3B9Zw0u$41Gz@XlR z?C*_|n6c`_x_Z8yr<~j{W1TA-$tsnPLGoIj;LV+PEX6TY>d7AmDh=H66#;mu->?mR zYN3rZ@zl0=v>)IymViDS=qz7OtH};l!){g8-DqR`3aVPp?nSc@76p9&PMwF7+zp_O z5AYx=>+k)Mhlh@;TH}Loubh1eFdHOey~il^tY<1 zJQeaEkZ?8TNF?8BH6Y@!I>fY{al4)^a-(JiZ0m+|9_%fr`$_*Y=wlsu zvvR_OAS!3fu^=b6oZ&p%BVX;CA1Fjg2(_DY`?LeS#6w1v_zys!DtbO#NvVjK7*CIb-o3hs$-<(n{Bcxf&B=8S+&i}*%PV&t zJRlAUdzI#jV!8k8j#PU?{j#yC*^<-C#kbZov9CRFuv$^~UeP#*Gl3O*%yueBQ&}7W z>Fw)dvpXYV(ruJoJufnh!zn8-$7kVx!4Q*FHE{0@9;;dMm#B0oOa_LUYFeXX>XjWT z(X%7 z^cv*m;ZbLz*cJpL*xp@7E=oEM9DQRqW>W85NN_%qO6OU5=oprhy;o z0XajcFK<{M7bavtKcEa|QRh8k+{_&0s+Kok(4C}i{+n3(_g`4M4qn+DA;POx7QW$m#tv;q<06qM$7xB<|@_C$jHT3lF-s^ zdSP}l1wNFqW@2E}N40EkqU>c^jIyXzF4c$&R;Rp?n_z(lwW+@7wC?BE_vX`OcOWgJ z6GVPD&rG0gZ)f=sLya=pDo2hcm}lJ|M8k0syRDlQpZju8>eYJP`-pP61*xz=TnEUx z2;=;jFY$d=9nse+I7-9AZ!N8ZwGeQ~{Tp^#+CkT7lt)G;NliG&w}zrlj^Wn>0g z8fdmk-;P};A|ir7YC^^^IR4QB=;)3XErv{SHiQGkA^w(gX=~!YVdCX!3DEU~c_U79 zA?~xSE~NnUW<-SQJwrwd&{ry4`!a{g*I&ZmS9f7n(;WXX#gY=U-I1XvP(Dls%ZSl@ zzTpGq4P=}oCB?s~l)0u~({-P*GE1%_plqTkrf?F2R)YU<3vt8A>a^tJI!+r1Q!xPn zX0Z7CXE`%n&$q-*IWtpfeGNdkQ8s#SQvV{;fBaG{#$4|a)ubllJqp1eH@jV7>V7P- z*hWU8H9SEtSoTkOd0aNK$HsCBK+TlB!voznlitge=gev`s@mVH9|2)$A#2M0-=3sg zCA9o~gtxyWMqs@>xJal23nLVIV`0^d@C7%!DnrfdkpTszzeB)0-4Rl3(~w*K%3o#W zf&7J*5j!qU)B4#<8!7pLw>gY-yHBHG8PEDUFUc7V0Cffmv=W9&Us}4~GzC(2McaCg zqaS6T=dXjB)yJlnW!c}yRO6>rz@j&5Vxvpc6_b$TUW=5SzLyT_Dys@yk(PQ4(QIQx zZEq1juUe;jlz&%H0Y{4qLE@;facgiW+>CC*a{I=8U8N~oj4}o0gCyStNRW99s=PXK4x|Z-< z8Wt9ezeI##E+#Xz$2CBl5mjn|dgZGA%O|6e4!fhv!1IZfB>$7==n82vzX#CBr=3K^)vnc#SUL2%%F2+meA3Qdyj5#M`xMCJJySQBTk40e`^zJ-~l-3kWKR);C&b`-r0jbTA;4 zA4MiiE*#FDYH>Z9Wg7u?ttyUXX0f0ioyHjy_*2LN*c{Z72m_b`t$KnGaW+uS-MR_X z@NZUOpWNKgut|SnGcG{GfKh%jd zNZLI$s()d9M-c%S^6)-F6W;~4;F*UiV0W!;$6`v*tXFULJV*y}p9t?7#M;6hQc_ap z)6}#bq2OnjmD?T-gw;DJc_Mz3ClShGgd#+K?;XN_n{N(}jQ9r07+BnxMZ%UN8<_UtHj5Hr9M|(c~zjJL!4#~R~EUmau0Gl zEVu*kB+sqS1#rl@Z#fVa#uCrmj_McEi~+*q`>VI!0?s8`!`{TgRyk|vN=Y4vx+XRz zo@%Db$_5Wro0$dldWmU%J$M^`MGAycw{ftT#hHYEn*aWmR!)x$V7=H zi>VwjhM?e%aXb%nn*$c!+E(}!Y`{e4&;;T`BJMKbU{Kxn==SyPijWYKp=t~WSa$aT zaR>esmxMEy$|2cq5^R!JYSG7v)>*)NWPwEdC%*RNRf@nGD?!Yi&7sDRuccP5dAd6q z%9yRJ{+=8bS$r_m)z5;HPR8GmG777AH1`FfF5@EY$eo^91To0jTDrxqK4vr)ejCte z8U3#DR6C}oVdk3+r$BQ3prNbKcue?s5dQX{{d)a^rp{AZs zMu4@8@m!dQnCSBhS-dB`J)-uKi|q--k$yiK_NS^$j1KSDe^Gvjk`(ExMst-D^)|k* zrMh?OLxC8=-?L!j@~(nsb72>NGbSy@1La;E4*=;X+S~!MO5*JoA>cP$`tknLut*~V z0A>x$OE%7Bi%}Jt+A-ZHQ*t#L{@mJ_dBg}b1R(d%92HN`DTx%0!>Cui~a=Jg?`!)9xF&^CH13(cb>hd#z?3PqxcJS~_Hpk9hbQFNE z+FJYlJUvdQbOXlKSuQC-u=YW;UL7qPSK8x#!O;S$6DD5{)zq-kOfNj$-Y%GQ0bz6#YJKmPS?PA*YFCWrI2!&9s63jYLb$k5j*g!Vm} zRoK}ZmB9E;cB3A9VQ-|IrCWS3(!8fvf9x@h@Zhk5KBZMmUsQbOY@g<8t#5pO6qJ7I$~Vzj3X?dhC8xm0At# zivEpk@GsFF%k3k&pl8$L5vP#`mP~bTD-0J^aL8x`6y8^7U-Ms=%=HjqOh#2WB@%fIU;VbayEN^ zbtTO&jIbkgyEBGJeYvCWmifF|+2PppQXGRRjgY^R=lUPLz2A~h@-oJhQnyd}*4Nh!H&`QSl1IkJ zKPDw{fwPl?y=qWzhw*2G@&FLGb~gR;=C?>;TyasFd*S zxVzZ`ddbv~;gWC{e7qFR$-5vvL^{eG66J5W$N+IplWvM=+vNQG=l6p5_g68Mlt$i~ zS@{B?MvTV&kt0EgQm+*qP{Ut|e+2aBpf9ULvcl&nPWZRZY541FjFkZc17gnK+F;}Fdlu1x27j00R&)oJ&AwY8hELTa zr{u8GQCEQyzBG0u6T0rDWAtk)HG7|&ilK2Au+Ts&u)PI(Wz>KQ zS=0njpyT4EJ8!JpKesr1i9*VuQT6URMZdR_P46-t+S zwmBIK8*aRN@M~>8t|uVC%<=r>jw(E6HqAJ!=WtZB>B1|{{dhaVN>`4?b8P9Zg>(Hg z*|@@SJy#JmK$C5a4PhS7WUJ4eNJco0?~^+A;bTU{$LGs8pX&)Toyz9rS!|AFee9Tp z)w?C)dlw-s8zFza7V2;FBroNw-gSZL=6s==!tZEsS%n*1+^+0Ryq4>k5Mw!?ZBhjF zGcezP9w%QVRP98}CO=DHen}7{Hm>u$WyX0=w|X-cACABMLM#MfsJJu5>3AX;HNLsm z!(e*zu&lxsF$2etkUF!!tlk_f%8KXigE4f_nl%*UP>>c6Vm%`jKNf|5a^w zq257plUB`ln_1cv&z#G~`nU|eO4}ioLTwJtYEr+nd9}+zFHeE6evO%}9wc@U*mOIY zk5LA08nP10TlZ=!rtfgLAdh0;*GX`!ZolG*)j{B^MtlN-B7#1WwJ8GF&rCh9i#DAt zLgq_VD!Y2Uh|zLwGtcA6tyMzKh$z=*Quv}D4ifcrB81MY(^$v{rD!pMR4dNr_tRag-99UJ@!iXKm`IwbSsRkYa$>uMz&ajlhcw~YM(uEo_Z&s->3bOHQJ?-u7 zTR+V75KWl=g0`$1d)#^E&DGd1tai2CEsj)Fi?zfeM(aC$H)oebs$cjStox^wNnB^> zhtchDRn#D%V$&Fj2~DTAd1uEPPlDRhHxcm7NS72B6bfNLBrPMOZ2xvQ{8HL*ph;8< zIdBuXJ|z{QQVFfGtv4yrj?-5!QkxH|TR~O3Yhsm5R_wjaB(`_tAzO{AF0!tIAh$+z zTU+19KVQ2mHD|FGh}JSX=y^y)c7#SXH_N_%|Gv&^-^7+`%QPTFFpVmFNKB}6vLU)E^F|fR^IyRfU)f~YDJhHh?bkkgM>DAP9#CAgoz8Fi z^nA`S>xr?cA1#u_mKw%%xi|7vQFKAGsFE-$_Qgf?kTx z``k_i1z*hZl5D455?b_(C7n5~2wTn$pKjB!u_?IV)b}s##mQ>Bm?P%5WU_4k*v-D+ z8%591Y1qQ&hXaYv^{@610-x^e@m1=}iHrL^lS?X$ojXyu6OiK|>t^l3Q(@C-J1cAu z@1^lO0mTKYGi^KxRodBYY#E@Ywb~UhTi6OJ_EvGPo1KZ_EPLq%5`n&zQ4W|8&4JqB zXkT*XWck2~#Z^VcHofCEi&JmUZ+v*~Msw!3Mi>8#b)=W$6n0>k{Fmk0IiNin-f0&*|qFFe3s#7#wj}wea%~Cmtrp4XN2vw3({pdcW_62WbI8U zo!z}WVAw@gdUo16xXm`-@E75>o-3-~O(@WX9rf%QT+b7g2;IEAGvc_vujrtwSR~ul zb&;i#FQ^(6ds!5IET^Ufl`h(uXVS{2j5&Yi-vg`CU@GG$)2g*B6rU0N!E5+2Gr0Fb zDU>hTRSR}Z`9i-~K_ zE)EW@6NSKB{e&)lqaHT;58H|cX|t+Vkhj!sEG%ZY|EfHm_3G4QA|Uol6GY)!9%3q2 zuKVjJZrvU5W?iFQYYc$L4K${m>a#%WV>xj|L31AWwU4Za-x&82MzXUBf6fh%Zg2QD zA1Co|DLU;9E8?~d${bwgv{ujLz-`G26a2yun2%D18SOl`$0(f4=cjD0BC1^4Jt^Ur zdd1V?vcpwLFyrc+m8s+}_R^GnS87z9Z+^LkseT!5%GPmP@+!_eKZgqAR`8Qem0A`1 zg}H0+JMZ*VUhId{&2pU?T$C(xwkl0z!TQEE7dtYe@I3c7xa&L*+g1l^sM$ZcPH`iUwes<(=8N2Qjk~1C60b{>tOzBj zehM%$U_6$0DUl^d+x$^hD3RE3qDUpB!=An5E4lMNlM7(bf-&LJM+G@_!&t5?$7}hO zS|w~KEnA^Sk|_9^BWZ zZWLAHC!3UU@STibuY;2pWzFSXX*D7eE)lk&@n)mxDm!G0k)t9#RJm-2IZa{IC_5ssos=-q}Eyv||wH6}tou?{XO;vHIv%=2% zaxp%Fq5o9+%&x*T#4?{!QTci2k|W)Vsl;y_!kqc)sGQ4`FwXL z>$3ZgF7)IGgoQaR@2ACc=3A{5HS!V1Dl8j~7OCpqB^@0(B8LSYa*JDF-@#Q1S`As* z+beF92xXY+^s@VF+#3SOh&T8i5@&o;4lvPAF%;UOw*kkiw4Bqx|A1NPIqsF>$`vy* zX_J%Wt*`y!s<+=>Q{8O>WlJ;C1Y2ery#;~HKd?uZ+ZHbgHBmYIFbqA46r>Q_2&>tH z9}hwxQ8fB)VM$>e$;#tk#$gsvSR1t zl+~`sy`-loBv7o*Rwv<=>7HdmZ+hjJl(US*J(Y3hhmG9=j3-5 z&J`2q?Xp(hD3y)b%&0B@*MnQoaRvdF`YvZ6)uKu|mo6 zh0?Wjcx1=D?vwJhq&>^={30&qgv~Q6R`&5EA_Q%`UyoRWDtlL2u~m;W+P)s zP#&@}twPJWityDwPo*F)ouVYz5hJJ~dEjJep>UcTe;_dKp69`5aZ7^K-T!L zh3;$eJMXC*8%K2ry(lk$L&vhRChvGu}`qN{@D-D4Dk`6CdFOrPL<=9bwO3)HM9>=Q)lKAwjmN%yOQ) zJ6XU-(vhOc}6?}PRDW?1v&$C z*YTl$tdJerPBtFQYsaIn!4#OQDtdJ2*#144ZaVOvYhA#=kbtalfVzUA&d^vVPV`^3 z925CH%?OQlVn+WD{pyO;Zx#FV_Fv!oS-^S!>(#P$-~Rf4eF72k_o4dlPxg7Qll^n@ zKR@vqc(QQ&zkkUi)LT>i?~iz5N8#Gq#e!F#sOev9e%&QMe*GSGempmgsB|DD=fTUe z7-(Xs7i?77I{8&Ukf+oh7?GLgBCBBdGfqj6^6!J--J;^T_SgG`#D^4WQg=yT)6vnD zyy@331vMrFLEy`}>H~`KczsM06NOaL(5T_7u*y=TvSmW@ouN<(PXFAPe*5jeZ<{CQ zd@sbzq*9A>Z%6)cWO%-(j_*dkGQ4hjYrjO=MCH-n=iX-y{`>xX9K^E*1Htq)(G7X3 zS!SID1i>^?xc;L09tXr*Kj&Ht28!rdC;Y%pHjqrIXpaB$D;`+jF{58T&;sui+s)Cm5U`I%u}|tt zF$%%#K5ggCAUi@Czb8_b({;7BHAgR`l%UW+J=Yb_tjV3)P<+Mu>|HXi#sW1sdO|f+ z3p4+&6}WQ;cQ^~v9WDzuwNz;i#SxwIrc(l5Yxo;wYyxU5S5NhLUQi{X;&Q%F>!gj9 zB8=POoKE%j9W~I-Ioa(}Rw|+yk?(i+spG4)D$`nmK2mxbns$$iap5ZlPR>4~*{Leq zB5@>@F$gyH6*9f9tO_qrtZb6^q$2oStTUP#>+{9RK|eN}!89txv#q_mQ{MZMO(-|$ zRc>0oVn#kN1CE1Q?wKlC*SDDrs`<9@@8#EczZV%CT!g70V?5J|5#~2#PoA*@B#$T-**A_)%^H-0k#vUu9Zy*U(1$?5e zOJ5erM72MIT7K^L@9byWJ$scu2RLm}Hzus>ewH;^&X(noAi}=1KPBfO%h~}3+)2|+ zPtS`oQ!Tc&0XBY9YPh41{}AaS3@bR^sm4(QE1Kh>d9WS~^(f4Qg>ph-w%$1^++h7w zeI9I}M=Yfj-EB~JG1h9S(Oi`ONqzXoiSm@oLJ0xug>L;3*}iK*x*n@4gTKYLXPXI; z-`s`$dtAx@rL|BSM-=mTu2yunv`o3xN`9sL)l%{75&b_2y^jO_zotm8dnH3W=gt$= zVm*YXCxT&fqA0qk3F?zAjLJu^MId<1m+i^M!oi86zH#f;MgTEuJ>{U`5J=5cfaOqc z#NvD7Z|U$lZ4PQFQ`B_|xWvSB!r`#k&^m*z6?f<=<5c-^YCMN+YCrjuB-F3rHx|#) z_LLMrB>ZG@e~%m|tJ~X`XxGZH?B;=gak}#ZO$b8$E90i7R8cG0BL%Rjt!1vnxLn<3 zybSVWgmpvZ=Z21x>vC&70*<<2yTzc6tM2by81==z+o5 zwKKzMsiHoPJ+-w;dYQq?+5~SNw~nGt5+F|T48^rMTpD16HWmYKQMWkNLn8$aG#atu zZ9Xvfao4WjaA-Z8J7HYLGLU_NZ+Y`oo#*;}OEdPb7 zlPArp)Zahw@eis8kUJh$Lc{9iyU+uR%mRCiyNVD zi(Ux6Xe)^TR$DQ=?ObU-CP}3u@`?86PvqX}4sc2(Zg-t(OFn+k{q~inRI;RJN9gou&%MxEQBWHe3(%4< z0oUr}#M8jBg6T|z6<01SRxTn_q9bg$kcyUWc2S{cHH0&_nf)bDSPGtv>NrfT|4fvL z0LbE8y)9=Cc}VEcrym3B6*72%KlxEpt!Z7w`3;GpzXHk09L|-p&Ino!{nIUe+s2_pLP(MjENU1TK~7vA(`C zebzJ0J@3Ql)4F$U?(u@)fht}$w6o!Oo}Y?Thtq-i^9F4F!u*4GYAG>x?uXT}fM@fG zdLy!#)!5ir5*o^IGCx%P1B6*3_9egpb}pGsrfV8W4foHY4lm?;Dvw(pRGLpIG#8#r zL>!b~?yVb`o_gNGwHh`m@3ZQfw?y>P(tZ^#4{s|T8P>mwnoTa6ztYOty2YQP)_js) zR4abiP-8||J)Xo@)vwyzbJO@1RqWMeHTA|>YivB#Tb2u?eB=3PvLsYBN{W zLl#gU`R;B{W ze+EKm0-mT$LOsuUGGEXVnPBxyleNJn^n{R%tiZoxjy@S3bj49H&vOH4fwZ#jUU!Uj#K+JXMub z;;rP~d#+>=CQNtK1b?Y>L#zc=OH2 z*8S4(8R*iTWviiuT3pe~IDTH>!An6=9%`4BV}%}IS)Dswka5Vj&NCjDkt;2wbzJ+3 zMi>mm#K}hjg~!)V0%{Bqpc4oB#Y6c|xJf4g8poQkT-Cqx2L>3n;=Db>C6(TpJhTn@y%zSg8;f>wGo9KxthGGvyS30v$v3b?6n}wMe zhNX<=jr0%dR$8j|BAvEI*<$t4t1+rh&UZ%^K-wcQNws6NG&$?bKX?wDM#PcO=pXWG z`>Z)(!l-^7+&-hJ9pxPPgpoMdaFeX_I?|tp@i)_PlFZjqCq^^6=OFt!ZL(H_v#fs+ z6j?%C{YXwe`xsU(n*Fhu>@5qot;zt-vySg%>PpZ+>J5$FJLSFK-?`)gZqaQ_hG7!< zvzrY^Hcp6p-~g}k-_wph|4&r?XWI45vTUb{>Ej7BIskjFFj(S}8V=P(Rk%*>)mgu! zl$un=$;p}KG>M0uT)WHTs^F%H5C5lAP|kmmSSB z*X_i#Mr48|s}JdY+4xFbcTPXTaic{@E7mPos4;OwnKmbT6X4>UhpTWrb8MAk7V_SO zjp$e;d)UD=<<-|7Z0{4XBz_*u8ZdR%CoLi!HxDfewVTCKylN#u%2JOgtN~L6KxF{s z=yPaJAN$wrVEgbV^?1pvRG5!ix5sVky7I>L2T#{=dabfrNYT9xWghHZpjE6KH*y`e zq=V0B>feA%c9irum0I<7KQ4seBpF9EN1i`t!FHmgs8(M~ORH!p+2MtEHgKmJDy1LR z7WBMC7}VHe?@3_^@rn$e)y(98dJxP#XBVuEux_I_D-0tn-gRmy%IXrqLu4_t>{>6;7-1f zSLAI_D*9rg$rqWJUZP;O0>D+NUw3k_s^{aV9VYh#Hn^-Swfa^@SoWGr0G4dhBOsn7 zjUR2#Fl1D#5c@cDu`Br}|IvvQ4fS_MCMF&5L~=nay$MrP2cV|Z@c2A0=9l@auYzZb zoa#F1#m(CJ`}Sr;Y)9XJ=q%3K5ilc$SG+eno?Iycs-Px>WHM6`v+;b!sLp1TnwXfl zy-6=HgAVz3%F=&p?MH^jUxx1E&<042z7+t?5xQNG0Ak`OO|*CX&Yk|383;?nVP}Wi zjt;HvhX`2`{KjN1?G0p4NJt;9c9q3!!yu|wj+Ja-&#imaL8^}g(YvxdI%-Hx#lq66 zV>^>-o4gWV6CCQc83iMHhlt;%?=NmBNIBD;nJpLrKqifp6_+)q1kDa&>DtBGa8RG; zrB>tI60dxU)G&;ad2lC#a*X^~C><=rU#Gu`h$4_5oFf9g8}ZZ)w*Fbw@7?}8^Nga> ze9t1a2{qt9tq(3yCmIlVt~b^@v^Q2TpNXOsu#pSUW=*dus$W$qPTZ0-uBhE6;p!PE zHJZfurp1|0c%~!fv~QnT>(RC#b3h-LFuR)=KLr*^?O0rj9u-I8kP-n>_a)WUGAo5r zx~E~L`nS~e=2!=y<2t;9IzpQdsiQOP2$|Vq>vbpe9#mRMM{wE9EvngSuOhX|-8qpP zh<2SlzCI_t(3H<305LG{JKJV60kH&4^tL;F-ub_xl>fM&t*hDmPrVz?m^Sa-Pg)fz zdQ=M^7NPbi@;mNs#clb`yBAs--@FwW8HwyYuwwvj56zBgu2yLmt0FB`r9Fyn%(HOC zl8zO_DQ5ZBrdS0EB6gc$xt8P5F^j9T?ZY3xi_ap(`YOiDxm5M*Q2 zV$De-$640FebbJhRzB|X;wSzpbe6A@TI$!YJGLW5GW|oZNJrsIrA-zV&JJ#SUTe3r zL6{eqp8M~qoW(s3nz^7~((N;`^N2|04(W(GmyJvpy@WK%*n+|2##^f8;;OnHit3ei z(@KCOyEg2FFEdX`R{jqjsTK z98r+{4R$c|k3M0qIa>cTgl13F&VPl0USnvo@-E4sHWyq~u-;{CF#Hh+wCJ8a8^bY> zBXh|SI<*eRl@Nzd-`GLG*KS}Hez{uztk`LY$W~>SQUb9$K0Z18%*hJg<4ewA@SB~F zz(J?9Q{M6iE2Gn-k5EMByW{z^OHY`2dx{l4Iev!t-7U*O8p1p;U9*V-&B&W1$NO)j zhPU=S@@og=!k5YD_r2`|;FN?{**I+qIk<(R*-qFFpL%~A0y=lJK}z)E_q0CWyQg}D zj<#?{)-AJ}QjZ?~tlX|vZRa}z1666;MxZ;GS4eF!HCn25I<8fybx@2W3h2}5n@3wF zegxG!GZ}z)TjlGoyQ@7~?BZO)#>uIwUA^bTSiZT31c7~7?e(|ng{R!(rK>`yT$0fs zh3$YybOdxbP}ldn`fLM4#`$KZ_s0#0w3-cqsH4~2=>z{`Y57;+VFqBNM9tB~(OBnX-(Q_P8;qycIsrzojJ!Eg>BWhlW5HfUHw*vUT z9(g7Ek~PKOUY9HP?ygUd)Ox3a`lrshJpb(82XLlGb=D&wY+RB>$aKoklgzy$j)l) zFaV7`fCTL9aPub@pK(ma(DV4`D5tQ_6X&59)jmrd%(bFlA}U}5UVwui^|%1wM!ik- z8B?}*gGf=(wM;?LWaLA00s7-`1v z?3o&_f16E$0a0LZ;20n#H{#ajM@tfl)KrSjyD4n4KsM25Gu^v3H+arU zNRLRqNCvYU@FXCz#ctg1Mu9jboEp@6tmPY7l;lifvn)T|S|cJR&MJMB!+7*vs(PTs zR(_-^3FZh0N2mS$p@#@&K$qVW;LzRkK92^p@jL!7klhMIX0kS)N@ZN3zRH=%1tgDv zH8MIe(R%klj6&Iz)m}5@i>nqoLb9oz9&y_G{OinSW!_e}dE4_L=fNpA5(Fu>TQbl_ zJn?t`TC2907Av)gZV)nFk#O12NCp1*r2smk9%zQd_dgK9-JaCTMy4j+gtV|vS>lO- z)l@||J=-v}b3?(n0_WahbD5C3@0Yig)?<>edt{0p-}0{Ys2AK%=PFFtXla!s52Bw! znw6iz0c!UG$KQF6H=9Ml;xnwqYzoo2%v=7MJXmN4o{9^rpL()W zhKFIGyq9*O*fQ|Swuvo?NXIUtgz=ZDevM*jpYN${s+IRtfy5oJppa0 zvV4FB`u+OWIzDPrQDtG7VrEfJH$0-U{)W%>{?kdXd|G_>JVkYlBTP{|NKxSq`j6Un}SWien5$AExc=r)A;=BL+Z_U4`vvoKc>J7@OYgd{Sr zxlHS@O&LSz7_No3@P^G56s`ARLG}eJ5Sr#W7?37^=P7&}PDrXXkuNqUiCcj%tAl*jbwzem?*-_<3lE?%eWERl~RQ3v%SXZABM z#(?N`TsU~ugx!SfJzlt-=&;_W#pYgq4buJ4#_1A$g(d;cK#Prl_>?~}ORyF{G{(Li z&iPow%gdvEW!XC6g6T3MrMckpRT;esG4Td8lcpy*hFa8*T2x%EOR*bAWl#+h5*UP9 z`?e%R0+9}^7hXtwWjFRMjhhjYS}>?a80<^}QBX6_!S=5rf!K2qQA{uU_+XXYP>W)! zVDw#FTVribRhYcS*q!XIWnp2K*_)dI)9zcl1EIs(O$qWtnM3w(qKEeVT!F8Iof_n& zW@;e|*^~wPO%jX1lqKY)MGv`cg~kOw9^Nxycp(#`nwKV1*$v-rOKc@+=?}!YY`1x@ zK>~wOYw74~YsGAWxQsp0NgtW8uyEDm9~xKF2BC+9XGS=dWH$pNRpGq54qRT3gWcE{EY(xd zVeB&ZaxI%zaaJ#2y~bC77QX@Rn7LvE;uDdP#3GA6^v@pK*gM=Y$vz8g)!<-04ezcz zxa=wKLQbi`HOVBy%5UEA527HOTi))oEWcsaw0chp^3q^PEmjCUa_@%ZQpnNA`3AK~ zV3+vxbVg(I!-qEGQa)Puj1Vvc9Tx|a?VcQvWnUjEg`%MZq;)F#d7hZVWk|uC1aEQ% zZlZ_&J@h*5hY#OgoBy;fG4>jZfcad5_$1bN3;i`hGr+7?a2qvyt~F|6Kzt>%J*$S~ zFda`KwWHB$bglpFrjGJCS&j}&+UQ3s?Oz*%A?Vk6bN9Xfg zfywvD1kTgmJG`)RE#v3kCGJ7GLaOR;2GIuub2#|V0dLY8hclO?tA~+mJ%3H;w3kbv zJC*O5kPRSa%dLLQGuRG6;fcdXxx`@BeIjK_l03djxYhi7Nsy9s)>h3!FS0ggGcr0o zmzEw?nznIWl)7gahIQiCUaAQT1CCTgu)a!p4VPIJYK`tg_ zDp%DKSe8Tcql>IPe3cmTH~_Wwq7w{b%~SAH7!OUEWi%T#M~3?)S2HLsBd$Yg(hFG_ z(GT{+kX$ScdOU6YreD6U^11)xXQQ*hPs}zB%aga7P_lB+>y89!qAxWp-*Y*9^21>T z=yncxMD1$cTwbar@x(1WUvTa7K3o5Q%X~0%<<}Z388JM(9gqAi0YusNeDl5cD9DkU z+W}a2?m)HYb6~$pPajj73JnS+IXMRjw9-?M?@P_m*WQ}__ z#S`Or2=XNa%!B?YQ@b%x6p$}p+`sAkiWG>n6r`l+&s=(J3$@BMy^H2@m!n3ng()i7 zjdebo%bTjqcbhquTz+1tS1$%1w^dY9k`CXZeE2XkS$7&P5nBZW40!;FtAOo2D4eYk zWxUkUFY_0OLpxA9NsCGZxHYGZQLV;07X{FiPPNhU>k$P{zW12jNVh6!h0A>SwXk<1 zU*6Z%sGFJayx=e$PSlwucDe?!FJkP2ZK^SoY@}0=k7gM$au(=oD|X6Rwq=u_86m_A z=PN{siBIKe@WHic$_Ixd=JQixi^r;k3+$&U$nbL4%z_VUim7JPbYb@_5yILP1|$6* zq9ks4dWB`^fwLv7Py{V$O^Ar6>WrE%HJeqNFG}^^{mKdS=uJc~7s@OEO0g|D$Im838oXOapZSnwU@)LF>=o#ZMtrhqaMJ2>I!dt@>PncfeQRH zALrqLI_p;dt>m$8)hn?7!kEw7AIt(65T1EA)Fp3);pg4D9ZM(kaSd1cEw3adFT+TyT1WM2K4 z#{|^mBPN*c=9{1R4?8zQ>g>K1L|nX+LazhK(#3@oqs{etU!Tjp$JlK&Cgm=GzxS+{ zCP`~V-LpFJlm~>&4}mcD^VJ?ifz(HVYWB_RkcAml_Z`Q!n@)#|Tk^Tivd8@%AtbMJ z&BmSUu8K$Iw!w1^ zWA?eJds_A^`YElL(XK}X)h|zt<%?12ddqs77!$c~eY86~ho|7VHh2sHZwF;-d!*)l zdg`{u2*0-~XzNEhGD7Zd*X8>q_2>jqhcL0q7ZS$4ckeI1fWU z_x1OchT@J!vM;Qk2?$bC*Qi_zMTazz`am1@QqirNdrNGVe|=AX&IOy>)R>6R>xEv< z0CmFF`8kuj@wxrQ{^v;AM~y$4X!*%v<=MPV(-Z$$;A ztqX#5lqTI>RuK?TP>>GNYcSLhLU3JRT?CZgBO)C^dI=;bN+*$C0s*810wF*kKoS&7rjp%CIY_Iq5`Jj>57v(kVVnuw%cG;UVm*6) zO$Yh_axHUj0Zj2D0~rWzml$C7kv2Otae zTtJSAj-mO-(&3hsZImkFLz11iG>1LT9gK4QjVAUYWzn}BnMcwHcx1l1dIp%jzvw8U zjH{(`a`}EFjeKbnk6o0pReuw+(X=Z=-DU9;Jx3-o0TK+2jEn}whaD`c`fiuv&KIP9 z-S%y3nvUd-IY0o4lNP#x1nwYs>w#>V1(HlYBY|y8o`F{{ex8 z2ea1S;ujkhGidMk50}y}{@_cf%O^x^-Jb~`A32+=bw$+#&Q{hOr@uEkuR71R4W`~Go0!2+VNBAK2x>eX84o)>Tik&h~P4y(1v*VXqi{yiy^(W z#tY##iLFY9aALPW4ryUT`>C?g`O9m-sgu>pYUP=t?9;%+3mops5?6GfB6X*#*P;jq zp3cv)ZE#|wy*l%i-P+n%X}*Aaf>m+I0Faot+r2-p875)V7s-&anw)!mvyHWVu9p86 z1X3n>7k=M3Fr}>o#xoE}!jw6v+8L*D9=-`TK6K%7a^&lfn0HZ-XzKo3t$`(;;?@j` zXQ#ARNW3t$E)Ah>c6f}-e#Q*fMmNmAeN!P}fB%ki8)|}fa?SEC{JqKwY$v$ZD%p;N|rEH@b%V;Re_3mS~|XJ+EHnkIyn}p2fVlX5havRGLbs zk=9c+7gGXAb)tx6d3(Kx8V&lVdMnz)KKJJ0;a0Eio2uZcdr2udNo7Cjcdjg{V**a8 z8(}YZfQ7GFb8*jB2~omEWOv^Jj%<7G9-h?ly=oBNBwZ`fU$AAVvNVf1;?sQF?Z#Q? zU9Kv4djtqjryXn@a0CkSc;p1==D`D_y^&h6>*RAqbRpvRRje=;}!V^3g-4dbu!@hVRS-)bIGj(rH*C3(ObMg;WtmY#*(l{z8 z59Tr1>Q^J>Dg3m!mNOKC-Lre81zDv#9_>${V)c5MuE02R`u?4CN2YUnKfkm9gPr==| zqIam>We)n)V#IqD%0X;kDm3D82zVOsD~3OG3?7J5=jK-;W?k;Pa!&6rd=HxIh57B4 z*NR?;HAL>tRN0`MVdy0(z1^&{rnza0OmiaO&89S+f%aSq{;_WY=5Ki~fk*fA#o!;y z*;j690hN^cVdR}A7+9^iOcM}ZRqL)BdI{lxwn0=7uJ&l0{X{i`d6bb_BW`eb{AQaS zlLF866icFTufMe15~pU~yd9J8T!55&pZt@dL2Sq^Akyyc!6j+EaD33#Qm>vq?l*0{ z!m?yG6YS9MUCPgn7yN)=wGG;Bkj@62e)k{WGk5-T@%%#SnW$Uy@yH_pAk4QhJhFkD zE(+0#>1kxi1M5&y*{e4LyxgA*v8z=b9jDaecYv&xD;k*4$lx)3Xxb@0zR~#cGbs`x z@w)yo(tVe{vAkPIN1A7s;;w*baWbqNOwrE7I6Y7BFvWc@dcj-QS6V)hIA8^6p#)LI zw(^qd3nd(ehD!B3$PJlt=$LAZiyBOo?C zOUK0p;=1pGK+|Ch1bHC$U83}-wkc7MqcV5MSr12>@~PI;1`!5uLem=D%6V5Cc_FTI zCNMBRf&XIgv@aD8c#@~Ipc^Bb%$;iYF4przjNKwV?E(-&=THvg%$Mrn<{`f0AMG6I zYqQY=FayiA3>nA1Gqy|ob$tf$D2d89a+_DYf48#j$@n>?HxIoIW7;3|uJ}p)`t0a{ zv=XpmB%?EPzl4=0XX~b*K5$wvP4adwaE8Qy;3-8KFTc{GL7xYd`_xtPIWc63#d8k{ zL+8|!>K}hG6n_5VK5z-UL}_?^uRq?d;`Z?=2)+g;;y^j`q>LiJ=mUzRH!|XkEx0u`tF3`9nZ_hj^+z^o%}WY$KPK= zCQPGiojYoegcpmhShp)!Vnfulmo`SU`pj;b&7Uj$&L~;o$l1%k?YJ`fne2)6b^nlL z-x@dfP$Z7W7){Nkgf1&5Ki!K!Gw2bzh!(%izThCmC#e!$2|u@Qp&g8!cF(Aa&iJTo zh&2|lIyyZqK@F~g-ocbhjLo5?XS!AOZQ>##xrRy2DC{L0aqbTl{Gb;a9V5jAD7{Xi zht4b1xYlYQW}7KdS}yB~JPteEb{YTP+v3KX($Z?PsK*$G@nimj#oVu_VoDXJp?i)p z_*mf^`QfqY_5;k22WE{kwv&X=bKLSY<-4=VAVDeMYjzQ< z0)aTVu}q@}F6$K)2fNSSTmvskS;61n+7F%vRo(nDG?eCO7wF=~;W)r7l3b@oqz|%_ z6E8lnP18fmaHt%%qG1iARU@^q1O=xLok|Llk7kX9^7+@t+%FL0y&G^eLFp%xY)uWr zae+E==0mR;I&Ui3S~*md7UJjM(V16R8=9?!s?jMP_nhuZs}kM{0kwNh`4Mt@L-x*! zJ{#4+yTDo9h*9;mV<7(f78Wiiqbp~(RT}I3$D4&94-is309Hr!E9Q+l7-Haq!*<7$GspE7vqt>S!Z7vG2CMfUucy62zle~6R8H}0HN zB|Ndh)l5>?s zjay*>l3ha~aUq$^Mp--<&NAknNsH49yXBS?wP&~4+Y1?y5=Mlbv1v6+TAW(*?@97L zJ&8g4rBwRP z!Z;nNH0|EkfwVmw|;CciyX zxdB|dIU+9~vf=T1XLR1X^Vbw+Hvy zGz_hDBD-LED3L1PlobOm{+uZB7e94eOWLk8_yNPlTaJ=37>w(UZ zt~E}8fg37xV(a}ggT0^49v^a1eMEC>F3C+g9aSm1o5%aamNpgW?{>S?4FQi3GT_U# zmI4XJ#T1xUggp*Ua_wC-EUgk^F4b?0em6CxH8eD(wix1Tyk|bOHnwMP#6a_;SvEA* z3s2m+SAJw=a5Ppv_ze8^{1`1^8amEp*y3&nR7|2aLb0+AA?|KB+exZx+bAJ_A&6GP zvItnC(Hxns%^Z)c)o1OKW-S}^IMtyEp0;Y^)X*Pvj@(RtwStmott~(~2ACvqGfgQg z4n3xqosQnD)9oVbfPcDF_x#(8~%Xbu6f?LZBB?-(@7>W?jsC8d1=@8%}<%G5#O!v}|6zl7YYQFJ@94-eqto`2A+Kut1F-N&1 z5PM_yQ}2+CteS0_j|7utjJpGe!;==Pvfb-fXvJDEZpoa`4Qp{i+Cb?jNFrKSqb1?% z<}r>VqoL1SCYV<+g0LzDD-tYHn3zz@w>gBIWhH0|5~K z#GSnvLeQXrY%q+P`a&UWxK9=j+&loJ%p9$LKo8*`PDujny7`r&Um>>kwvVw^*&bh6Y^)GfxM zwV9p*rfIqJU0&cptm0jJ`|(@xvrKlPemMKX{>QG_`27mqRk(>0^XY6@#9((G0KQk~%1;izU);yf4Jv~ywsS?o9 zK5V}LhW+dvcJrQ{nQ3DU!koAI+bIiY`he;E(;tU6%74B7({6$$z2sR9WNE6G ze2YQObxF`x>Jo$BamQudpG17dz`^Y&j{Q++PGCJ%T-@$c0Kg`4g?+$M#H5uYg5i;f zVRADO9jlDMHEU4oikHErx@y6}(d)H)q8bY${M6*~D*Sv%x>2sQ0P0u%GG$Q5B!ZCk zxH_YyC8ok=rqK*euQdf~mV{iOKF7gfaYW1<6xaO*PV@jQCzNiHh*ib3LGR%hR8E^N zpKhJpDS>tC$-Py@NcG&@dJT&=kt#}qnIFFI{B2%rV2VI^6^fWUm5N6mbl-vwTH}N_ zCt_mg5%c_<3@Ncd&!y=`=CD-Ebc}qauAdQ_I?A^UD2~Kx)(29X7~34PfW>SUq%LXU zw8^Ch>7i}@af#3&i-dj?3_V;SoI2+|HgTcA`pAmUzR&KGRYYSAb7_ORyojk{db+Q- z4o7wAE=_c7cOW>wxH+|_a<(`%LC4Zd)BscL(ziLjisvwIM$-CbrB<`Z^8&LR-!n6H zUq37_59FpI5ftS~FAI@~xeo^0VtV_ZU*g|iVUST)k-H0scNIYafc02b$8ao=E@NO% ztOt63rkC8O*ydDTYu|bhtk^p+9-%Cyv()YERbaz=)6qWo^#tL%Tq3Pmkz+-mRhkvG zwaI?>zOO$waO|R4U!edJX$D^*AD{$sY=F%oqos&#>MZs^Aky_w9>YQ`9ELuqqLef3 zwiB#~8aU1Tv;NZ)(@9pG4xa5&#R!kM#4SLwedU~aE-$auvh?JvOVnu(WQwt(uHGzT zIX3XXO<*ylSS-Dzq$Q2m;=b)E_~|%y4#1$o}CRc@NZed zWoiV5C%}e294c5P^G8WT_ELc(xz=tcp`@z3&muvm0)ooV*98ppRRfVorv$y_FTLqw z=A}7Qn28wy-^Qn%BgR@+hXTkcW#~ZrI4q-l-yyWa&wA?Bp-h; zh0&9BWOpL0(pb~q->yCS{z`{^`D_3nWyjejIRFV`h3gz65xZ8vTy0$BL$u2BDZ=Fp zL_=;SKmh{HtFBa7RoEr-MXb-p(0+bAb^_FioUa@faA25OzL!Ia~9|X?F5!-f$E@RSGhl^>e78DgfqXb>jYvCs= zYz_MFT(ZwZ)zlgnnQ^+Bb1rsU;8~opW0(y9>QcPCy9X1D*(56rv$>fTis+~KH_3lC zmj+NGz#(W%dJ;~^k?3(t2Nqdsua}R4cCJc9L#&>f4h<4=It{h1MCV)mj0*(W_$cDKz=!Gqkw3w7$Lu?yPrs0o_f z{;3`z8-LdT7Kro9KM{2T+(gGATh>3y*}eC0sCB|=0~$t*bL3F$5DR`@-rl{&Slug% z;gREuS^%ezfHGQPVGHHBN?!y-_Sw;-#bsN{p}r{xcJBP;ZRMax&tC{L8z zh=Nt@SU&&Vqr=f0=IS)Y)r&x*rx#ULzO*N9?babY*GV2BSq^C0k z2Try(oOXPNCSw^8dfaRpIC8!%6SY|zXjH;Ol_L@TU~f%}PiH0<5X>!sJ%J_RD-<`@^iu~k6t#JT5!oPOhV z%=HITx=$46rYqXBH-r&8m!;30gl=DpwY1cmn1045-zSSjAVlHo&oAqMlK`O*)fgj~ z*5%ID`tkLLTnE|2T%@e-R^s9OH2u@P1wsNPaMT3o-Y)$5?+W z@mH%e5z|Yv>-AZK>+6jHk${`Re0GAHJ4>_BKD`-y~I;m z6#*t!$T4z);xi6Gg}LH^>8Zv=EWPfCx4krAGi8(=_xZ6U01QI@XytS}T47E&&6Zm= zoc2dmgd#3-&^C~fyXWQ-IO}%zkI#>Rh)hP0wM2n1rK@_8u}Yef&j%|&zMs2CNIdKs z6)9at{Q)UeufN8?YA5^Ro91TKJpx9e3QFancAnC^UXuAkgR5<=X@z~`<;Gbk0!<@} zEoFGLJ}#f{^q;9UWdcf53mKi#omK8#f{azzBeTJ6fDv6Y(RZx0$mMp5yIo|euHLwu zGj5-BiVz58*SLDw*f11kM*)j%kS32SMsJe;9D#_xT&fX^7JPw0!84oF6m0W+B4L48 zI16@2NzG`0%b&i&RHH*yBXdD&drrS^s9|!8OcsHzyfUCpD%z(iJ3yQP8%g*m&YJ|7 zLcM5RL_;I^ZfVmly}m$nzi)_qMtv*^MS*!vbi@}vHkiVrJhEkcX;L8m;mutnxNQNg zePCUMJydm{ale&Nzp%*ySqQC}(yz{Db)~Yv5xb{k@K#Lx9?!0}<9;vVVWpqm-lQ2h zaCR(GUFU%28gcN&phtfT%2<)stFl&A14C}lZ1W1zR8&TYQ7jse_bwFM#u9nACJWpinyR>u{^feez)ao9 zbP|J7=B+j2)tNY0I$G;7(Esr`irAnmBad34y@xIHuj9>{w+BTJ za80@@F0$-r*KbFi7`F>fC@XKqlfiQE~@s^9(a`$;%jP@Rj*#9XV_`>4I zsQQ_;+7zRu>F_Ri2R);pw6^xfPUa;ZCm{C3t7+DUFO{iUH+v7f*Io8(VW4AUSl33q zKn;FDq#$D-TmRMrtTViSur5+R&>t1JUv;8n6y$4tSvtjH;v7UyKx=raMlHFxzXCwT z`mwb7?2qx^`yX>s79U0^r1m!NJ&lbmc*_54q{>Y`;|Cp&EHclxuFbxC=QFI+koxKY zVaGuVZcux)-F<{4bFcV_phR$s5#We(mzUo>r)4uts0k73swtSV9V?m`Y*|fvbGjY0 zgDk(Rcj>kI5d*=%lWpjGv={Dft``9uo+}+KFaf5;=E$ly5c^Iv{EYeGmVtq>jqTVk z8INzI0+dRMh{$LA+h$#gk{?7=O0qno(1888tDF_Aa){&!7Uc$6n!LczVuNQkyzO(l zd>R)3GFqRw8GTOhR`a#HdR}V?_=IecQ##TIRch&6QVQyJZJMSsbo&J$L`&3D7n{i;B=*MkEsR4<8?+LhL!b?V;-cx$>L&O6jM=Uf*!EFCF%3 zO5y9QV>@0&8Puw&niwy<%}x4MQ5#=qXKH|t0a{zH9~QU+3Q4iisTlDB#iVNLtOF8? zF|o=u9cr`$pVTaZyPVwR^{POow8V6tBV97LDnY*9Ey_trHgC>=lqwfu6N1j+!;ed} zoL7UvB&Qh1+7pHmqSxqypl3jqj!WKPMaKsqO23_8qw@fW@Wc&rMb7{>NZiGOykb?J zyy0{UR;}z+wch*5UZM7Vv^gGjf3Tznbowcq8ZeZ|J)v86>cy$swP%2aKo6eT=-dDr z8GUWamj~dKYEbN6?+n6~`rc*F+1vDbKuI-@8?^odi%2dO<5dy{L^rLJ6}$<=r>`%K zkUjuBE#VFh-vowOR#@y{<(~>8&$_;OxbiM#>7B?Ckk%azl(A~SbvhMT?9sYi>n{dG zE`omEyK|+(pciVv;S6AYJkYPwv{(_#xEFnvaug(P#({PL>qG0`0V>=0!d>KdfXPh(1T>nHb27Q zvAG>24-R$YeH%df&s%OMK;GFlyr6nxAfXNvcUS^E)LP|H{L^IRGqz z|M|~{fBnDM?f+z5slHR{hJ$Vj)boR!TK_&76?N^kT>23Z-h_1wB#4*1+g@G!$S7&# ze8jWkQE!VSL&m$k&5Mt`)~mk+^0ty6U4_5`k@#rC;qsBQAVGNcc-z3*W7QRXuA7ea zBeCwjSxQH`%smW}-lFr-z4Aw*roPzoqkLRd0ge2EJk*Wyso<8qXx4VjdH9Cxx;8jS zhzsp0`CsBbjG} zqfS&-=IbVBWhL#5{KXr^KY6oD3bJ}s&}!{o0G~wCH!^h9cVQwLMnLgsQt9kkw<~>G zm!Vb_rxP5w^K&Bo3+PM1SYhO*7Sq}I%<|mL+uUb{8$R0=XGp#Z+zwE&HPbS`% z1(?_XF!63uu#csrVUcNO4scnLOd~g_tf0F3vPJP-(;*8XpQoCci%)sA zF$CbDCdh2MIJVL>5ae`jMC?fjW>N@6at=-}-YDV&L^u`eE|+!hNEeMZR^eDHV|X1- zH+|^O3wmG>%4>&le>jD=EuYR31rR0Z{-K~AVodk!i&_{*5yY%QFVva`JfXrdz>aiA z)9)Of8&puR@p{-CGuNccnA)IQYmUp2ftydz-g0I$CSrir_@zriLxi!kR5Q7%kU%=XuZ)$y2INqj4MxipKIGe-9DmPI z?qB?_y~!N$Y0_(ga}iEUwbV&j&aNsmk88Lqb}8z4#Tetu3;EsT&@3w2Tm5leeSL## zuc`9T$VdjT!T@}YkpWNLx7)gzMbprYrg!!Z1}pE~Wr@a0<;qJ}!F+dsvf&-CBdf%R!*=@2)%T)w z8GYn|$*=(r3vYYmTQzs^IxzUC=OqpFFE4obug^V_3n^-Qr|?k|y3p9v&V> zh356y+@Ow1K%mMV8#zUWCaZF3+n-#FhsVau$?kry0PFutfhE^Xa@m1qn{{{fNQ=Vf zE{fR#!HmQY>3{20if4Tpp*#uewFoiIzjx;k5ySi4N28`5WT@x_*V0U#KU8~ZuOF1+ ziTkcYz_NM@KD%~n9BD`_<4Hb!&i`(iNWe%ex6_ZM2Y^3l>AdV8*Ej0_OTCJ!2{s1` zd+RmyR>CmQ*e5I7)|@0~+3W@aE;r=eJ=F662K8lxpMs1#UrS8HCiG`rquC;K?4;B_4--Gl0u=L{RZh6>@1b1#2v3q$uyMq`MCNgcBv-KGOcXH=x5_ z=!3E#9zh?S>4|Jz70|+m2{Hf{CvNx;ji_j7;c{|wLkP`NCP$*2Yn^9WDr{>Z{fP;O zUQ*Rx4Huh&~2Vm>PLl+d^z$1yj?XwGE?$?C2M6Rgne6Z7#3Lc>& zpb1^x(Z6fxDm{n}iWTUK&U@(+`rX?RV$;A0oul_;8-&fLBmDK}Z-b(q|Md3>y3T0y z<*v7gFV(iQnIh3YgGzL4Z7Q0lt9(mc0P~*tx2NzzXy$Sv`lqh9l%Y}ElkuRN@+2|u zyT^cDCAt505(Go(+#<M6I`@b0YKQeIm9+`V24N@=E{+ne<$bZk~ddEa9SpFP;9BOd;UbACf^&XGu z#NTxnL7VJFgOx>O1q4)3pCW@k*3;Gr(NEV$C3|Kcz^%GU% zcXE~RTcbWqMh)tBhZ-B*Q@>0ep^3W z@DSyr#|WXG#V7N@9WDvzHir-VIuN8C2DhQ?J6~DJ*l}pF>HhC{1SRjZS0kF9Y_rY! zMi#KD7UHU<_Y?{bI>CjzXrJhBH9VtEcpZJq;QmfsG{n6~m~(RewznlvEJnHu^J6lf zmU5v9BRtI#3CeW+Q_&6y`Y1#^fqClTp9A?cIqGxPH&)spC%1>8XY#M!13IAqH=C+~ z!Dog~H$U{_^Q|iF%FT|sh~oVZ z)`TE4)vRywQ^GPj0&@=jj;DzAiqT&a4l4{)k zuHyuRx=+Bu+`{VP=h-p>lI_NBc54TxPpSM<(G#>V+hoQHmS2$lPiNTu29-Z6w;u9X z>GBK4zan5I+Kav@7u@O4q867{65Tg@;G2vAI^6zs(p(V#zl)WXH@FfCf3CL$b@X{$ zI&Apx$@uPYbPvef?cY%^0(FxAdOB;L2>mM*?o{}{jI96s-v3!NZK_`{bThq!HYF{! zp{Lv>my_2#oDJbD3^W6$G9hFgpY4^b25)t6tO?Q%iO{0Vt?R%bkvm;d+aJVqACV=$ ztqv%0e|N+3zKg!SE@I<|#?suR^f@{qbb^_?-DrR_N%HlSUox>Gq;gO&lk+ysZo$#6 z7^ubYpml$e6!#ft2iluk8t%y4WQi&bXd;tIOKo2L5L_?`WE}qebpCLpO9ndi6Ke5~ zzzZI|Z|Q!bsNuzyh=o>t2zjQQ8dEi!JBA&)S{HyP>@_l$r};y0S<6`A;2vHsQDyLL zi&?yuI&Tz7JVJ&>j$euIQjX9Ir(ioYPpQUdiF~5;CiWv zQ>w#i(Lb2kv>Lmc`gFVa;L8WsBhL`vQ)-C^{feSJ<2Wq|U^u&-3CwNm!-6l@|LFnL zsd=R9z8V|?Coq_B*~Y%5#bHdC1QJc3NAoD)GaNb4U;@>VZ!w9e*dLdeou0_q$}l*7@!acKarfVwL*-Bny)D>40TMW#LE`vBov2J3AWd9AZAddbLX z`X?Zz|Jk+Xd<9}o6D$EA8h(MICLjGXgyS;M?A1fJI{=pS(Df6P91r1EV+ZJ0QS0qr zj0ucq7zx9?!2O(@$o+NU=DnS|^Ty3SlbsQCfyA9tQX4IS7H5zciz1Z}`P`Y2d)?M% zS+>ob`BeGlrTksPB9 zhDm$uIkp`1&AZuajQ-3N0pVKP1nV0*r$Td+MujvXL`U^yp>droyx8RX(LK?BWES=6 z^4yI-Om%`R-B4|{DW}k|*_UF}Ph9THeN{J&9H#$hnyaG!X$WH8jAfsa9feXN{^tn7$i#&b^LIx#Ae2h zhQGH3Tz9vICEJ*dkJDJ38J99#9`NiRJ~MsA5V9#cOg|<}Gy(Vb#Cb>>s@0`kuP@at z7Sl!HTqwapLh+_3V7dAAn7}o6NNVAS6 zfZ=1z*4>ddi>o{9*^Piurd@0t0{W*TD13>;T&tOMb7*_)H;{4qZ3I~KrSyw0FZPa$m;=cOy*&j!Q#HQ9wwiJF&s&RPS|YYmd6t)?)*C)|9jrIU z$v8F{;-;0%9!H>Dmc(-I_90OmnEo279eDrB8<(K0CXiRe}4v z0v#|L8ZtjTIY(9x^>$;RyJ8wtg5ma;#Ui)f^pf=yeXytWLVax1qE88tPBoL_W0wpW zLD4dbP{K!MS9~J)+JgfC;&ND%`0a@)nncfrveZ%*BHTRd63N`^4uRDg4JAI@TPkj> z>VFf;RtPLoUoP4cO_#79p3W>#Xm-9+CAE4|EpM+LwVe$cd*IM9Dj@PO^XZm7ZpNd% zuq^b`Qra{O6$XY?Fibc0SnANg+xZ$V*_Kw`9i4Tq3OApMefuVy2hUDx=b>(op z|LEdh!x|uzR@cDsL%jOo6>+9Wq*t~(kn3UDe-TM{xiXz0k^33qu-S}1oGgapcM~XZ z_QxY)Yv?6;C@GeG4?q+9r(J6s_7=_XZc#1X(l-1M!MfK%v90;6|J@dc4kDty4gw=! zO*&{1$jH^Ny|nZy`za});lrsG8(Ak|Xns{f5z(9G(f-YJJMEvik?v{V$rcSzETLA4Z&l-4e-pay08H{}6U>1$ zq3r?V%pHX+4GO4-E3Se{Z|Axn=&n6>_1{Psq&FUigf{b z+9<4jHCk#NI=_l%Yklj$DF{%I8e=pky{_)OY~ySQiOJmgRb1J}W+AEzv)=?gn9&o< z77$Zc)N9);>3x#!$O>4>D7bJJ_nZf~#%m)P`0YeoI1RBZka&8$?@skZ?ey+W&uHfH zCw_q*ZVZaqy4ZHac;=~^fN^QAJVvu4tyQ23HsxTg0wzUuwE;aeZ%!V4`Hi50cfgid zhUXtZu2a~O&Oxs%pOHS?N@lZNr)Y_fqNU(tt697FJgs8-%PWoHtQyXS1pv2b$VN=V zW0Kk}b1=eHzp@P$oEI{uV3-7MqrGG@H^^Kst0+DC?R zrkr6S<=I*m3Ze4#_KsHF<5(>TJ&cQ+17}Bv+I3}(+CZ7s^2OC(!JuKm4ngbF5QfmmsJP>?1s-WE=zmvpo-Zma)4780T?c10YHp)h`&RVw9=$4%tP90w%YrX!#A4* zVp+dwF#;w^cPVn;MS-=IG|hPwn1zpnZz!gt+zpuv`fJoNz!VFi3F-iJt=YzDkdqr1 zhr|?nrqUkwE}@B4KGMslFyRtroXr92`f3*xegbxp^t5$(E!6d(g{I5Uj9X%^RmG}L zKa$~XWe59sNSn3oENeHtp#+{cUIKnzDTXr>o7WADWhlmhwxzfy#(AE7)f|XRWs5Tu z>DGa&uRBlGQ?Kg2MnPR`RGd-LR{ZbW{Ym;TIvlg`burb5hL}&)*_&z@YSdy6ZU<+p zerIuM4Z>tA*FfL~{s>nF7m%U3__8g;$i@CLB0LO^xw&{}(#O(*maQ(&_s$cWBsF!L zXsAAX&#`-pG0+_|hMlHwCPaTPBt(L&>kbd!mm@05wxpe`QZ28HxRvztNofh`w+e>M zFD1BQZ+-)~P8nbqQUAZONYwL>XaBAT2dK~g*Sl^DYI8UI9R+>ADxdjBSzmZuH~;7N z|FM7UZzpbC=q>bCKN9oF1GrcGuP5UU5Ls2*tzfD~8X|Xuj&^vy#%-EU#!3y%fexrukwYE=2mF~PG)=YPKH^kXLINLQt5 zgqA3}x|bZ4zj|=mHvL${IDFk08r5&r=cjik-|iBtv;9BkdJF5OY0n}VJoFd;rAPo^&BQy|W0rT!?>r$F zOonx+mregTp(_O5?<@lLa&)_XjsKgtnyE{9u4}R_igxI%a|n` ziD4a+U6-0r3z5ZtXX`6ok~>!;X#{bM%kH@w^XPZUiyQE~AC>ncxTNOaGPXpcR3CZH zH6cuPnbTI7)q^zw4cNi@Wwqm+smVSM4Fu~)!%A#`2y?g$*t!1eiE^{?oV$TH1q{0`qsF3l8-=rru*`p7h+jy8{YE?LN(LS@Y)xjcZ zaJBM9X)@_u8;1y(V@bc?_UyQdrcSDl#%0;G>5S>tp4bu6EZ)0}ltv|0&)rEgr z16jJdJ$0!$^*JQpWvH~$mlmT=2+`Vodu>5auu-_|nQhr{YS3^0C?W_Xds?^=c-4GW zs6&W&_T8CZ>jqf9FA6n1&l5AY(!k7xZ&Rl|yy8Z{^b}tYz0=00zsc&%x0t*=t$pi) zjM!lStz-DhPY<}CUi?Om@UyKiLf<}tZbCcK$PA?QBjaCEkHo3l+b%ekWNBP z=uzo}fRxZe;BC&m=bn3x=Zx`wyfNONn=z8?WUsRK+HqCS+R=n-{rr1OZpruE32fFrM0-$ zBgKDL2YyN2vT<>FDJ~%3?(WX-F31mY0tws`6B84-dtc!GeLkQBpR=c<%X1GtN9WuB zYUKBJ9$7hCIN82*v4uFY9=H4a1;o`w>ej8}j{fud*Kt~T*#6Ozqw~K{3phc6<2?fR z`0on*r){9Bid58nhr;GF-m*(G<|9j`ZD@qC+pZb3d#lOb+w|{|wmOdvb@Slq&eeMY( zK7))*mQ4AP+%pgIm8mnyG+*hO*Ok_oPQ8}l4iUO`>FTMgjI^1mYYHJJv>wUk1!_Ht z;(r`<>zqi)E!vRBm*r%mOdmyN`?EG|VnrADmHFM{>rQpP#U98gMNf^5jjge*A3_$U z*}6a8KC=46>Q8p!?B9O8OgVkUY+=*c>(nJ#GV;Iu_&`Puo<95c)q!@O-;`|@=Y1QX z@we?CcYLXMUg{qY;1cbLQw6Ort}mYYha>bq?*8Qey8qkZ`QPCGe>J^GAzm*zPT-nghj2!ol*_8icKFkD+w@ArxT9R=OOKO*uwi?qD{W zTZdaYRk~qa%=5==m_{q%_FwNZ|DIDZIYPQh(EiZ1wqSX>(EwF~f2?nL5iGv;E2t%!J7A zcydXWwf?jaRcDDs$J`1%?WGIWnZRL;#036&7O(64^G~7;?t`K^nW3ZC-B*A56v%<8 zbiqt~ULTlM<;VhlUhO|kD_~mR*}Hs+Dgs%qtLd8cH?!M!-fzqPgzwPs$FU4yb6?(s zwm-;Ds{j=n;+F4u6)>t6CONn5=8)cBVZTCpj~I3JVX+YB$lmse*zs|wnycyCtZ9&` zRI2k+b6p|rU)QEA*HG)2n6QutM!$DZlbra82?w$GrcQJ$XEa$z?c|ivj;b!IUnkbX z@g{2vwu70MP0XXNh?5hZ>7Dz0%PrScB*9jF%mbm^H%Y4V+p*2+8Aeuw{MP8He zmdTQgO@imDfIewPQ|8Q|{pfHF9xb`CpFhQ>v~3@K=hd*(TN+M{?so$-b9JX>68yGt zLpt3b8?j59bE@%{YmFy2+S5LjTH!8&h*f6D$jq*2@gh8DkDoDDgDI0`lTbuMT@xF1 zJP#j_9wN$73j1rnLzg0E8A>+BS1T(EWK~F>UPbkcpI^UON7<rhRB3fmRF%Xb z(D_OJXf&yBCOAgPh)jbU))_*U5Y4#MYUlvY+@_C!;j8oc6|KwP(H% zWJGxzFT+~3(4eD{Ds2G|{=tAl=W$D!E#2`NGG8d18XRUQ!@93ttX?uLa*2-XW9(d& z*-=SU;uh2X(N9(sX<{2A4G;ds!Niw&5yJeg8 z;tgkJkE(;!dNj-2WX)!H|Iep0h-P0cE|a|1R~vGQFL)DOJ!_q$IA!y%MHsbA?c}Jr zhYxV#MD4e|5M_=%hje5Devr$~g3a)*!|B)5Y$zx>uGC6JrRBulY<>}4=X3*6ov}x+ zz-j#4tg*)25nQOyOf~0fl3rgDI+QlE0(WF?Le$i~YXZ@qgqq!3GCvJDlJ?NoEiiV_ zi}EFI8_6|lX1@G%u<>L}E6&Ihm}g5aFy9}~rRiaF$O&JF*$r1#!w9Y=z0vsjpw3k= z-RUc*)xtI zo>U(=Qftxjh23d#f9OaPAv@LSzOQRW{!nYIo-?G+)=NySMF- zfFWpQcA99_De5GONZGZQBHK;3ErtUO%B)4mah|%(Uai@D1BT~kjT_HMk%ldFSAPsS ztsZ!K9t@nZxS$W2EY}fQ%$JGy$aA}));$`OFEw+mSlXtK)YMx6xN>VZXdE?#iVN40%tNLVMxoyXY)9s;hSZwK z<&)>Pq!_Wl8hs_^^;h?q)#sFPO6z$1z(vD} zC1#fXk=%^w8f5CyS;X#n%b`KwlaBbrm@;>IPdA#Q~kAD z9wz+BO_#vSA=Tp{sKe+TERQYX&G&gDLGz|Vx2R0DL@j3DYWrk$Uc>s{;jV<6z8_qL zsoG?e4r8x!X}9Tf7Ba0f#k@AvG$z^T7rkw)K@78;%+MKe;q^j9Nd3TXuQhcPjI|Rp zpLPAv$}s$D)A6Q|ZZ8W{!1_6!XhFciY;{!#TJ^e5(D%hl z@$60>SdKmLju*XTT9ue(XHrpDo|Rb-B%iLUMT*h#sL)lb>)dPaIOc{k9$D-C<*ab5 zjN8;@`RTh3M`h5mmC4e<@{F=W&WH#VlXB-x6@$BLGMu>+_tb zmgCDc?|UZgwPjW`HZrb}tw!I+Ym{P}FEf%aJu^~Iu`ZKn+Y=mDhWw077RyLf z#_Qbkr+bysQ$?9lk{2-gK1Mp4YpB4d0lle6XxLODGG`Q-t!@R59pE60B)rIk3vH#T zl5xP{b;e@1Hg~lvUQ#6lxt%E&ZC$Y!VR*nZA?c17RKxaf#>P^QmzCQdg~U3}e5miw zd!IOhuMseY<3MP$eo<)>ZW1rdQ?&PLZ?^T-#=^Gn{YjOunBMLwQ-n2*pfj{qIF!^_ zs@cWQqgkDOWq+abX_wwExo4gh)4)9*SLiEcFyE8Ud#ywQRLN=DDPm=ukfVI`eT-Vl z5e3bCS!&gD3t=SUw$VL7FqFbPS;qM4!YWo+--O1-;3wR>UzfBMdErRwFkatRKV9rr!n?b#y|-H?CWrOV)+79_U&MXs9+%wJQRjfKs_66}!z)Ii zSI0dI@Ncg7U8FmRLBebc6zKFXlz(9MvuelZrD9aDhk9B!M;&ZTjZGf+^EZ+kr}*Y6 zIO`;Jyk~h_6Re^dYE6}?9WQNdly=nS%8zoppQ$OZ=h)OYyI4VII4pGg&G~D0%U)13 zi?0}%z=U&os0tF>89t5}WHcQ$sw#$gV)KzEZ_alnqq6l|CT2N;%M2R_aos1PO=C}b zTu#0BevNSIrB{y16`U_Zl;#EZ0y0Ig&gE?_va&KXJ9KH+2@=!e1(Isw>1LD# zAEt>KA8NqW;IG`30n2ruKx#sD+Bq6l%tnS~&#>1Y4C5l1bp5?>l}zai`kOsoWA*HI zig_URlz@ZQ!E3txN3ScO!HImUH8PE;89|Qq2KesQT<3sAG`lm0vwVA_*oxWv&&-#1XObya zV7#nPJ11S5MT&Ug?sv^f_*GMB%;D?G571=YO64 zzll>mdZcm*+G5v7vRf!elU35D*dkn~wBm~NtpeS23eqap9=;NHmW;SIO3;%|64ukz z$3S;X-ed;EhOkO2(CH12b;BrTLu;_&GO*j?QLacu0%EdDFZW}Wb+TP;2z_d=8!{i| zO5Psa9T(z;{wl*8|6KPQp_N{c1b6}dQ_xjmR}OtH#VuGM-#UHFuBoQa{&t`Hc|@9{ z*3!09olUR~;-EZspDm^>j@b$sk9`-u#HVX6p%lI{FkTI3beZ=kwd@L74xEQG3vQ^# zWbBtHF@LNp_)3L2*hsR~-h6_99R@tj(&ErRVkt>&=xV5!DXT5|BCdsFM6um%(&zWP4u|R zo2MmSx0?jv-i_jqrQssT*%=dXpEP=y!A;p>Zhj;!8#GpLRY$q6o3GW<+lwD^FQ&}% zL~|MK`#Dd)qRH`e3@3QnFKkFY*;EDZhZs1|cM-L-y;tv(Z+W@q87=S&nE4S>FpW>@ zAE)xC3?p>5HVMkJwE@~?A+^=+slurJfW9H?HklByC${mS)lTcaGJ|k?yE(0lA+v#EDxtTZdqAXRNpJ^NYyl-kzYkT~}a^|&+4cWw^lgx}lC5KehF ze+6T{mlZl)R?$a}mhmds{{B@2rA(~kQ`1;OAsC;s`0VDIu**g4>(S*Lsme#EQ9L;N z>jYKZeWLK9K3L^c%A3z$HBP3J+hKn&mZL4&!;@ZEKifxBRbd#?q<&(sgAJpioNI&3 zn1nt8uU--8Qh_*VfVJvLtz5mnov|h+Wp*a|j2L2WAl>sMsx{f4z|Mm$flyki{cx6o zq!vME#6mOX!tZiGJfkAM`yaiF(h*x3=i&+ z0TnO`FCoU-lo1M7XkfIKi|=+GLGK$1#%dY{Ku`t)i1B(}6U1152f}B*5|2NvssaF~=(yhPX+~bh5m7x?U!3AFPrY)}( z^rq|5bY0VIrQ;nNjaAv{HB+kH`X?v=zUsOhu}OUPo&A>>gZ1Uq>0>-e#_Q2)tvk=p zi;FMpg3fHkEmy!2ud8K60Y3=LiLo)Vy7gti1eEB+c)(Q ze#@WXSAU)2Stn1Q)ek0~9gsQY_=H^D>{XizW%NA@iUImBA9v4xPTJ4S@Y4DfTMu|i z$IAY=I^q&Z{u>2dzcG3V@s-9kpbY?B*gS5wGroUHr>~YIf?!jQZtE@J(D%I^CXEUC z%LM0N2lQR%eXuBXLiB?_e_H~FI)kA0Zf{~4dPc0q<9oVs z?if`Sp0!GfJs{BQIuUwvAnWokr&JvWAo4|7A`1`60@8kiHEUVZDXd&6O8x~udZz>w z3g!y$Y$pzZ99^yy{yLn-+>ygsr0j{atS^Jk+1!ouU!fA;H5U2Bo*vbch4L#NTyhTh z6)ybkRQ)AR0VimYy!bb^_#4)FX&pG$+kWqAWPdr0e_2a26Hs94p2pUdUuT=&O1$$R zKY_k1(`L;2%i;YC^80oXD6o2+)9d|TM*sJpz*zveEz1l3OaIWt%V2=QaQ{Hl@aP{H zmRqL&{;qA*_vqgJ-sWG>#%g(g{|brr_olc0itzrs!es$~g!vJiaQ>gVm;_jwai*C^ z0+)Zii;jmb;2j;SIn%m)@9BT)V)YWMIX?VVdi+0JE7`X5mx{x8Z_c0ihc3w70Vc?; z@S9;WJ_KKNC;~D_` z-I~Gr8bx!ca-X4Qj?*yNf32_+GOevo>$jP>q?)unHJcFuN?aJn3V<|XecWeb-8(d* ztAfKQLl5mZ^K^1qPnxaWR0_Wl8#2Ps{i*fnwE~u+aa!AHuKRnHz%jqT-s5*)1|aP= zj5w1;<{S7Td_;WHGu5IV8dmrk`03YvaS_PgQT@DcdRXG5Rk1PTr1$+Gx^o}VpRG^GBn57ymtoI3wR-lTjw4Z)V%WHTG%cp+>U*7~V<)RnN6rB)`-20&_l`&Go z%y>B2s5qFZ%(Re8keaOC3%$v7RNeby<)h}CL_rP}03J$0Kv(v)SNU|NB_ozdKb$@4 z&Fo(4p*$fa-UrOyyQ#Mai*1br$}G`+sqiWB?ncrX%az?+zlb_h#+?;?o?Pvm=&+

?&9B^$RGoNO;l3$X7HO(hF{HUhYByr|6jfvcTOk^Z&@o z&tO5tvDo`P!B4lygf2RN30qZH?l^FV&4IliGsIK`!gVWorXtB;V{-gYu7mB+b zL(|ScY{YN;k*R4d+5uf)KEbCtuXdk6igJtZow10Jyn{Ej;ZHHO$5)UP{RTdnsM=1Z z#nBW@m$^4l!Y0b{gga#x!%PPnA+6t=C`C7_U*Z4c1~i9p$f{hT3)`*5iag!}*_n+n zFCb%wD6Nf0Xub|qBgd2P<7sG&m0?2f)ow^U%fr~x;_s-FB1Ie+%6tL3N!`oeCiaU@ z(4l8zt#{lR66!#)qy3J`ACwfySZ~Do!2cmAJ zbkM$jTXDFTs83zpE^*A3yVF70#tmF`J&IOSGL>7$=LPk%2DP0whHY&pzIXQ$ZO)vo zzR6!kmh>vGVyWLm!w3^K4cnXCi_K4Z`*=wAWBr<`vm@13yk}l%-G82R>W5Agk7PK8*+-NkYQn3NB06jzPj7A<;V5 zM5&?V`4Y`{Mr}BIKq?TE9k!g{ zz~FCtFd1q}oZ3jQx|UfUnsVOoHOzk4Ujkvivy!v=ciPliEuB1)k70`4r+j8b=O)VOedXR<)k`*V-<%RIoC z&%D%eTwI(53R7{sMmHH=Zt;2^#;bBmZDy7!gC@tUHsc)917uO@=hLJJZMU7=zXzi2 zdjzcq3ZGhVbjzS$xWg4TQ)w$se5c`rvgP|7emy0-rAZK0s^Aw#XOnpRPKMGUp3$JH zaLWg|N13nW8?3W6QtoOA73XIiCAHR{FsQ{31ow0gX-eDi_3Jj5H+l7mClbD5O4~J; zuaX3JFVGK)nR?ZO=nuDX(UJ;IWsBc_u%tVMOti}s?iw8Pu(Q2UQ<=m@7B@Y+nv%9fSmisND6$8!D6f;C>hWVYBct|wc`z$2@f%*RBbguEba;{xp zXT!|)ge9rHkHaG;bk~#C`vwIyy;b{0Ks0-1906Wk~ieDi`kpDkWaH zsi2DYt!Lks+H=02?x?BL?4hw36IJAj=z9|L2-#ZIW|QcI>m;Fei{mYYj&Epo#Am&n zTARkXWU(@%^WLj*bLi_D71mo5TpTMFTiKo92Gcn8q^hpssF_C*2#eEEsU8Qn%ji7rtc)ggYIHpR7_Qcp#((K(Oj>ovDV=g_yp;DYajsT8LS zN9I12vd@Wtw-3p|>xL;-l5SsyZLq48dy`*B%%+OgEJzpVI-$Y#)1@ZXKKG4i`An@Z zBA$J!m$V=)F8m27Se^5f&s@=imF9{jH_5863r&>kuPzYW zqd=*i;eQV1zWI^Unq1htw`J_|iU2b^sX>|E(l)QY{C+-uWXuIsWq$>)BJA$?eozxu zPQ&nVNY+-(3Z3kkUdC8qvm9uUs&NGhb%FxKsx3A*r+3$-a%i|aYg3I?hwk!ZscW$E z=JT~-pT;pKx^UX-r8~HpK+n|2CF$-->a~|<6JBZe2107jCef$8Tpem_;}Y^Dd*7rNWHre@6Q}7?Tiw zi`?C&>#Udwj&HE`QFW@qe)g6w-o?g^@dSW#Sv+# zioLRxi#y~dhR8lK-Pw`UfZTajFx`C0+tu4XbaBkLpUkCVyiYPI3Lpm~_gzy7p7`e8m;JT>SN`EwcmkX`NunkdO;l{b|S9UFIh%ONrLjjjy;?AoRKHy=O%(oU9G1V z>gSk?Sj)FKKvffwV2 zqSk)K2U&Z}#S_IW1c{li;Zn0RsQJ;PdS=(Sz0c22_K7s)W`qI=_(6mo6N*liwl~~W zw{~GcYbeaevEgWVxXK2;nkSs((f6nlySWamK9%`Jcm+ClK%}t8Tq}sh59BRTecVp* zPE5LejIt7;fZMp$RprzuvC*xm+IT++ggjL70V(6cuXe@s9Gc+Nm>Lqb8{CHUweS39 zhpuX!?omq*j{brrOkO(ibVwWWPPO5lzXtp_A zKuowj;CkSa(O3zkJrteLtU9XpD5NPct+7MY-+TF)@)o#R6SNw}Ed9jA8V(td;lrG6 zDz|BVbkbMcsDCvA;!AXu;(2F-SsE%UTPzsZQKYJLS8dI9NLkpo9NRH_&|>Fsv}xds z7;D9LHFbn1dREEc+#|B^;Dz7dBM6d+m$9x7`1-T2t&E)E=fyJ(4a#; zl)}agH!{}*qxA0odIE1=5x#!#MyB(%GK+Ybi|}VpXXNK{W>q?*Ti}Rzs9uo6cAy`F zd6Lj-_l3eRCj42+62|wtvw?tujS+#IvVT+xw3}2b`xZlt3Iq9N-E%zot+GN3%N(5nR3=G!paXDdVXZFP2R=CK1b_%4-JRISg#wZk zA<>Dg3S@RFX~`0r6ZzU_^*}gtO&Acrv6^mv!S!9i7sb6LfDNJR8?X0###7&yX06eY zG5Ww+fcH!$1^bUUPR)2!G=zs?>%O3i$*}s# z_$+2G#dgDhZ?F-bMC+{z$bf7Z#hr2+h7aKaJ8pT4_R~y^dhaM+c7*vE!3(a&nvEBx z1z1zvs<0zEeekWoc}d&#dv1*?4{q;Sb3E8>JxGk^13&wz79V z(V$|5QP8{Ec1C<~b0h$%eu9`F%Ur>v?vSRS-TIN2SWe@Qc7R0~XseI5jA9T;Ogh}s z>`ZlA+pw;ELUP3HAu%M8EKA&$73*{`ZN!PLiQ=wZ8fIadtBts9DU>g%} z)iu;p1_}iyTzO@RnOW}W4UB~9w(4mTl8*}(+2^4`(GxYY z$l#+bR#XYnrZ<nDH_kWhsRpQGh~pLFJLU3Vj* z^r(gJF0UyC`QM8L_%iVpJ48l66<)#(w#;B0u4y}+S(sZWc*#{H9W)?|Kf~f{En>^B z3(bmfRZ|Oag%a=%Ui=cGl~NPWKo_Jj`|Ncgp7vAeQJ%xi9x=4|hHYD4l!J+Q#ril) z@A(w^F-UWyf6#~*Kd`8tG4PC(3Y{mS(_3mrY0ZZH>i|h<#DITB!zI&zgFTlRy#b+n zBRG$opZ&%YZWnE+RybW+YWW2!gyqbno-6UB1V{-EuQtC0)@;WsHms}fLn;~qM+`B7 zQm>I;E>udcdx&v97M+PP0E9;lMi8G-PnfW5p{T1*Xm}CiaIM^aTY4a zalstdR6nS&t-g^Twp?q=TG=P$P&j~=s^2R+V8FqPMh#>E=Grm0-CW$$d?@V>@0dUl zZUC}`sY2fpP}KtRn((Hv%*;0L=39c@XmFB}`dsExK>3zmA5mh=ers2$h_L;T-D+@{ zp2Os0gK7lkje~G!k!bx7?Z8*{Fs#CQ4aM?bQ0&+7WP$~)V96KwFp6ug0iw{af>yz6 zepLKK+CH6vGMtNIE`MN&B{Q3}s|_eybt{2KHK*5747lkE!}W^ZaR>0coB5jq zN7x&7m0y=Xjekjd4b&uK{S{%70tUpwI^A#{F$qf^#G`ci)-ua%Tu4^gXaM7&eK74F;w>C za3dbK;>gD^S`0ecLk~1hi-ASTAn&;zd^Zxv#?)4)B1_R^F-Vc&k8E&-pk{Fi{m|^; zceytFV5;OXTZI>SbgB3pjnr^WSKG0IfU=)v8&|um`a%|4OeE5>NyE+2Aiw2mjY`za zKBR%Y%EmCEx*I7|I9xLmKJg=O)x9aZqu)uKnVBI0x1q)8`B{ytD}gJBvUCEoy2C~} zs$boof_$1(Y3~mAmL99byD!W&=&}xrn&VftJe;~LzH3DE-Sk{eRGom6-{@TsDAdSO zjn@@-_b`Q*IMw6S7C&-nKaiWM4i5u2P4^F6WjuX;RuIcX{gLKUO5K{e}Phq6_d{qZidZ+|)n|w#a8G0Xd##MXn{F zn(+wuLX*vDYS%gn2|zo9uj#>gZX5-U!*0!bUGpwckZPj132B(?wl{?O4t}K*KC(|^ z*KWU{F+yT)0fRD9IpKnd7Uy?~V#8yWXI5i4BWg>&ue|&IKIQhscH-{+4mNteUto0vsk85#$lDwnyLDgmVeOS{x9-bygslFEhB zXVgp)tlwrUdR6Fw$}}P0Y~e?*Y1L(hHr~Wrtrk$L(d-y3IZRWfmE_M|4_QkmtJS>r zmnH1(HGs>@K1owJCH~@cv12V)eyj!?MBi6^#RaEGO_@b7XXRkPyd_$*WOK{4N^+_njxY=_5xERte`ilzZ4#=)dqS#68}`+w$?RNuAv)(zf$lJCS8I<2$0D(uv)e$* zQUQFW`M3&7l51(mEYXAo&*mq>HtW2QLl07{TafO%=6EKZnrXkoIo@Dv zNQ3TJ)Iaa7EF&Xj#x8Z+UyM)ym>vz$d#Uj=)%Wirc8l#{L91!?!XzZJ+O7 z9g!!(Sn6d|mJjK82REma1%uUqER~D+sXMo)mssreuDH}AFOHCf#|ab|WLPm*qimIk z`-VmFePH#c{TZlO)(EMLC(h;(H^c$palFd5glWn$2WP48yKRS&(jqHBVXa#mD(SS}OJAm{T|G+f zHua>S)1-89BEiIkwm1&Zi-Y$IC>TD3A{H+=N>GYIw4kz)DLI zhM?Y-rb@Fb9ylC8xx{NY%*If6bfK|v?~%JB=`GEQedAtX3J+zVC5fKMz$Rkh2{%!7 z=|%SQ)=8*rV?H_!!8>F6}~shT3N3HoDl^JoJqoZ7ho| z?b+d^`@(s8AF;XuPEJ51u)$boyJ>UAjZmjLFtm{;}F4#6n*6%aAFXoeJOdoS@HkXt69^Ouz6?K4rMH0Pvy(53mWcE$lUzMAB zl&7Q>26W=DC8mrBT}ky5l5P(jGWb%nK^yi8$|}*`B%^MM9~(QRCR7cU!E8}!J}Yq8 z_}sYNK4D#ltYj3nPow+^Z+h7>n^}R1T5Fky_W;+pJH+dB)PcY z_TG~2Pe&{dXSed)x1GGZ$-F#W(NI4)r%?7dyMZ-3o2x4YXPvm-WncC#8E!e2E{6UR zS*KP@Yw`%$xb&^)BI208)pf{lR2-WS>I-H%z-*4s1riRuy+^OdD9W2`)v3!EF{Gi#2!@g}H139S zR#5ii1sDHWy>B_T;3IQ=kIAGrO(saIP zQlNEmu2BJdMi^K1bNmJdN+ZOid^cPVSJ#QV^$XO&O>V6py!X=9oU=bYfE_wFnDv>y z15R{tD{4xYTU*j)9pGnR8+3@t@wJCUU(Vn>rptP!eTnMkPjQQ10t|)T$Q{YG&gOQz zrtCAE8u(GfqLFn;dcfaw1g74b{)%CDn6>cma|8vOFzGdfbumW=JiF&+^40mKqLePk z^q90NIPi2Rkh-un4in76j@7=z^6e)zULv9PQ_7yuLveW7DZ0{kZ1erD9g)+rNZfrN$}g$~Ui zydg4W^Jp6V?I^`PR)kAn8f2 zB1{%LV~8oUfp2s2j9gAxFIc0ex(L2x)S=NN&#enytPhro7+SE)PVKR)NHTDBOJrn$ zySm)qR8P5iI#k+b+6+##Mjmn-){WBvIha}24YY&nbJOXe7Xjoe=x{WZM4>*>nO3DV zK9bS_z-Y-qQt(ChSR`P9W2p*EXQ(Rk4b&8z3E+V@_W)F8AtBGxA@#xDZ7#1P3l5nE z(q0MAB2iOD{&eWa0BTCD-=78GI#@nRJh2CP_>f7SU}CuwtMnJwuJh94k6cQJivzu# z)gpzr&XTLgqzPgx4MG#}6Hr!koKSOC=rzZx6FF|*IMF8y*ERa&e1RN6nu3e7!^cTX zYV?BLfXbjgFh1ahH)8pKv_L`gZIFJLAsO@Wgu*HpZgxYZh zGXB^s-d@<)gqfwE=JCbtdn4A?1NSz&1aKm!Ze;FYkc4U4RFs*q^}b&xj6H^upNL$& zIO4jzVoJgV3pYLLdl><#gN@(D^oglA+F_gXn<|BLp8D|+I+rVO^|Uryi2ODA2HW(t zF#xFE_0?nt91CxFf+^T5+@eDxq3Ol@+;B%|nk8k-7x`4MVTL<4DN}RC#O1;{+G;^G zDjSp$a4357QU`9`WY&{UQt3C1$$vi1LIBaaA zD>4#g!rRAg6Q9jL;4>j!Cy8-TXx+{>w5RI;;#2Vz`TsQZo7#?*{p{MmwSmU0X`xEv zq4Itr&V@WdBmCysp?~2^F$Vtr1XpGz z?pTKF*tvN3AFkD<#A6w*vvCdgKXlRT3KTH;pE>-$Fo(y>wai1$n@)TUnPi`15#Y4d z%Gs`yDGcM(Oo?8b7l^B9DvZeICjOJb<=+DI82Mkk@QhRd_17>WTU9A1_Yc-vcnq5s z#rwQNfl&pkLy(t&hJFZ44<$S$C5^`E&y&p`M*Hn{^93-=B~uE4$Or3TdzIaAsU%C} z$AR-dfi*e{NY{#%_6436upK&CJ=RS?DE`2G`f^C&5XIjZVY4f3^}XAn7<#E_%_f)cH};4q9?xPBGU1L9 znlE%#HBRMPcMqHiY&9gHYDsLrHt$KQMZv)QNBUw*Qw=d!xnJg(1p%>UT&(KC-V_~1 zk!rBgtMHRv#j%fiDjpZV>5SSYLfo==>%Sm7sodBYU}Tm$)xbM2G?IM6|n%{ zA*zo(IuidjW`}Ci4fFlGn~DE2a5F~-q$hpnN)wFZ{J_K=&u=Tto%;1LCtHzZ+SvMp-^Z}Uy zte@k-#sA#Ve_YOQ6tt|t1S-j0%#T4-vJ@xqB$5kDxyQNBFOy!4gn^;POahzDq-Y`A zD@&s0?>;0w=zGQ@=%`{GEJn;9ah#Xj@lCfC7P`c&ef)rkd-#x7#I!2KkV9j{29UHD zeFVS1Za1yLa%HHZeb$_Xt|;ay{ofn~Yr*M+=S=9) zaAd3roN{Bl?&&nsy~q38t1q%HkFg%DV0jtQ{Bb~nyi(oIt6W;0XVxO3Pu4+Wg1kjK zr?JrWB#JN6N~z2OU292*u}0k~67+!EE#%@<(ZVty{9Kv5QqiQEBQo!!DS23Izvqy6 zW4fwqOI_QwE!J$mb0A}W6krxZ=e5K0hd-MR7`H}>nhEOA`4Vdi47bCaN9WDry8-65 zb3AoEsWE@xo%_B75zkpBoyZ2`5x01|RcbM$S^6^xwPF&@_Ug*seCX0V!S0piajTQ3>$3dpXcB|*w^NK$AGs)ztxdBx+URvB(HK+;Kk_ysBplKgbjHxe6Hm7 znb2(VC4dmH)i{X+q^|BTjUYUfL+k=Z9)#pAyp|IX$ zQmoJ3NL9L^kMJFdnW3@lI)tH6w8UyPpCLwgDoMFmyZz6*$R!nUf_vCPEs{?lA7|ybQ>AWnIwg(N;3b zK%AP4KZF-xi^nK!j_UX6_MC|fxuDa*O}onyIr~3Y;*4raH4VwXWo~@`kcf2iO>bH5 zMv2^?0H!fV>jYdchzd!hZJ19gwTil*wHCjmo9b%p)ANvrg&?RY5$#PdF&qMw^X>eRM;D7)30@UKpY`Mcouhy}LYC-$o;JyW;bmp0sb-4d z2yavRHit9(3`_p4x}!;){SdBy-FEywck$F=7QA$IK>_Ip|HI4tW2@T8pB}x+ETgw` zc(8X~mw#CmePMA3>6$EQd-{3IZd1hlp9DC^o7dT=2h1ztfafnf0BDqfv^*1`=G-~x zQI8cQ+6?3H5{`?R*qnZ`p)ld18Z9JT3LLH(b*l#3YE-^z*{3T5Ik68l zC?pZxB6K06o$J|a>Wm{%6s}->E>n@NEluk#lQ-uQw)n((4$BgqGnhb-{_dyGKwTv1B~_1oT<^?>x+ZJJH}alMz6B z9824`Y_xP!I`W|c^9RhAfDy!1Mne(DafXbZH3|15Nfaz%j6XtVJJPcjz_Glw%a!^N zDL{fZs1CV3UF#6MIqs*`3hN^+zK`D_Ef*>`J0D%MuJ&+JWk{ixe_c{CQbG|?K z{(Uk_^D49dwQ{sm)L z(IC&N)!)s0U;60!N?`Q0>AluNg*P5A+2o{qPSg^rV3jpm(@OD(&{aGtmDHEV#^z>p z9D$EVO_L?+GHB1=njkye(3EJtKyv&YoSA0yl{mQ8{R>O@hUOr%lQ@Kw&f%%9cL}^? zTkiu2F=oeJ2}=kdHC;TnZrykM{f;+o z3)Wi}15^eW+O=lZM~kj9U_Q9v$Jwj3=kpfZ4hPc+_xT96@ajuuw_Qzark3nWPE~>} z*Y4m}>;`Uhy@x5q+n>iXPDE31FPXOIC%)2$I{vK^VMCxl%D^h$CGvEir-8OW+o~6Q zvhTu)5^Du-OS<4Lh7`wW^4GQ4RHL4xj>Hqes{0^wyjte8Qoi9^ZW={h2&ZaD+MBO>fQ!c)a z-{&-4(YldE@FiPiP`)3W?@pjbb+p>30LkAE^77+%TMKy)O_t|x6IZ*0AZ)qE%j*8N zX6Vz-a>6@zD_jax9Fa7w{4E?sp6qa*5Jc)Yypb9zK0T*s(|)Jk`x((mUFpfI2~dIh zX%DEO>$DKmYYCAmD#IP`NHAT#5dLm4NyX_2L@O|YIpwGnROvLUTWsN2Et-<*t5xW! zBE`!ps&pOscCI8xIJGQ+7yoSQEMH5}6undV6=+p488e&C^wg^SlqLOj``_7F<^mh- z9^pgZ&X`-Z#tygih19xlT3eP(JNU#7VbXb-6#V{IQ z6DOlt!o1E>T{%}3BDp$asQWQ^Rp-{DIk>c_S8vXAKc3xJb$dJNh z^wg2GxODiXN=$B)O|-J7nB}*ZV##XGFL}KpFlI`c?^yc>rqC<<(> z=IUO>6E&#q{^`6boz|aq15@4Rr$sZJnnO0~b{{?*Cqt_{6B6G*4ewubRl`}1=zmJo?*r>T$pE{FFJv9>4N;MT}z4LaNZMkU@*(FN}C z{12Uv)c-V{{&KZ@XINvV%OlOd0N@>z}*#^4utg?!Nu^I^Q2| zkT5%*_0O;Pb@6Yfw{iR(;u$T{doM`*Co6IH{|51Mf&V9o|4HJnoArN~_DPf7f8LI3d(uf&ZG0kNXB=l}HF{L9th4VrJ z`Laim=c%@jd?omo*u<}q49W5IT5HmI@PBd~KH11BDmvTt_&<57=l1fPTLp`6%Kugf z2Opej-0)QQXHI6`8Qg@hUgC3Z+NO_I(_c`he!N8)+d$zekBg> zF52)^2e?$K{D()>TW>Us(6CuP@E_Ldi3RUK-J@4xCH5a4F`~@7;r|bCxFk_z-u&2F zEv#|M_tGc*COKC+--{z>l`p?kP1j*es9`3?(4iAwY*Q!J=X!jm`or9M6^wti73Q^O zV%I7JXY3_2G8)Bwua0qH^Ob(&Br9Esl~7#eFo+h4>#5H%`ms&|76$_h(w6L!Z`>Hq zIO9WA;>O2_t0p-hI4sn95!Hn0KYZc#C-=uQ1J$4r%@0rNOZWaGQrgxxeJ@Sn)7E8f zVRlXRizC&gH&2NMa1i@_$mPIQkk@9{>pT8f>E~^Uz|Xto`L3fGH=mD}Z=>@@k-ip+ zbBE3Eh+pjXJaVNGkQnjJtMeb}c44_kUWZNdjO(viB8kG2LL0$y0A%@C81+HPd%t|1 zc4@0PC7@@rq$l&@CPS+PMnh15oL=stVXe={G(6|*qbM((KT!N=qy5LE(Y`MGCB4?m z(=TH8E{EbiAm3S_87GXty6VU`@*sIu=!)+@ZojLrupE!L%;L3I8f#pP?%{P5y&MrM z0i@O4-W||)F4FT_*pUxiPSBO+8ovVFOul0~L9!pNeD85Rz6k!JD}Q_L&EZ7l)~0a$ z7+nA2AN1gLcE>`bS6^s_DdE<)wb~q;PBg=6HI%wi_fnFx9<_I=nktOi{txDPV7Ya= z_oFxFwfCG8Qoau5cW-TnQ8cK|!}6 z|Ep1?fYxTVbQACQ*D)nZyoKB&kNrW34`g5K#9F3n>CRwg_G z=rbjrr}T=SZt+w~7?B~S>*ed;P%$kbl^Quoo_Dyrwv)$*E~#xua#SB`=?z`60VqEJ~(yj zexj4?wS%`a#&*;l7B+(u=46U5kcJJ+zE05+YX-Z@a0N(eX{_i^+N@vMdnqI5Tw0Fj zP5B%@lwa=KOA;Df(wC+-)$!EfrMWK#MKZY_$lF}UJpcY*jc>LdUa(8902k(5dTzBe z{M`J;yKAHB!PDP)ndXkwR#oHDNblnoCMB~xui#W=6h**Fjh72!t9CySd6Y*DJE}J6 zS){99J{(Mn2EE}m-um{aEdBK^?_Xi0-I1rP(5d=nM~iFBlu(oFZ$Gp#+DZT-Ne8NL zR-BhKCEeHS4IlV5q9B3aDs!G;6uBmX2fjK#?BSJzK4Y`EFN&RUSEj#z$U8{) z;E1e(-41$S1~=*L5jewh;hi2Cz7#3n@=TGCNEOeU(+kb1LEl3XHTQOv5hMD4wcvSl z>Y#V?P*GXeY_jFVlH>EG@V?f$24@bA5L`cSJ;7uBt-_JrA?8!B)dio_Mn65;e`@Qt z{nbYgn>}-y7qHm0|7PKvgU18dCyt)azMMeN+adSp`@B9SjZ%_a85&r8S+g29Ue4-B zqxEvzJ?Cmd=d5x<>0GB|RP9|w+B@e0FehXp&zKbk36!{hX6w0AKR(C#Bn`;ddoBf6 zG*9Oy^J>OZLgy;`tOD6ao=dMR!CEGeFJ;>i{8OdEoTzdHPf~MiF~VKCkLG=_Q~IkG z`|{KJu7j^5R=Qs6MnE#w=pOCyS~g8JOa0MUIW3m$gK$?fM)Z9$fjDh`E9CG&z8{~? z`1(9kLccHUb&J~^q#|?axwV#$14tVKl6jk{@+9n{hxTe`=y=GWM< zN|7Y*?i6?>t09|_lH;M-4Sjz&rMG`!b99iCKMM0;Twv5P_2;x72*mux7d_(^zo>)? ztymo>F!EgPv8)S7A5RL0kDq%~Pb%hzCaUe@sm_sXlL@ z7(1Pt-)kO`iz@qgJ9Jhfea?y{Mdb&Cyw8z_xa|v@IDbl*I&mJhExOu^uHZfxHva0xD-LzMPycSF;Oo3B0qd;BR? z6{)QS=+Sp|lD%3)pVZHy5`C%eOJc=6uToP5fzz;AKH}gxyWl-MxdcDA{XC{zLE5S2 zJxn)2@|trW%_S!EoZCd);!$h@rJXvSBKikp!meMmTO2!|Rq$?q=0Qf4b8SBVwH4n` zPfwd-(ja-s5};ZQW1cDg3foOOWeGxsZePj_{u6GS!lISy+VvL~@%~?uR2@GhYkH(4 zYbfTws$VGmFH`Ven1c{ulb6y^QI72=QG5!T%qIQ zeN{ls!cd8O@mrW3H%k?N;^-jX)%5xrNv?)n40xtI!OhIpj}#8o${j5M5VvekeE8?0 zTJXX3SZNtjn#1>GiwE9GCMTi3z&|h;1~c2>uV9=VUY3xmup)iq92%9h={ga zpL71kE>C6lAsysBmS1*UG2yU0(tWM>E$rY4^5=d%XZLn@y<2>6U%G=lc!*O)sRj=Z z>ib6D!rE8ASsP^v3L8F3!S64}dc*4}-pZqV4BlkL6yPEsb$oYV6=v;}h zo*1JFv-Vy7PkA<68hHsw>m3Mc=2x_V^GF94+heXqEvPO_)(`$v$>s33CzxAzr)*2< zU-)3O^_4=HcW!>QBftc2nQ`nm{nBBT;9}=wSG`%i6UPi7bAjzOxm?{7b>ruL!1;pg zW`l+hW=2m-ssTQ4`6W9-E=q@z=5EVaceH@Cv~&u@NamF4o|RQl{0EG}o*R$I0YmD7 ziJQT*$?|5mOp;VSx2Ll%nbZOSp%0lWU~a$Z#rVU8B|EMqX*ytKUj^)D0t~huAcTgW zj6N_ZVh5SoyI+Ix1(_Ud&bb=}`D*cVB%satiWV6Z?b4|F3pjB zyy{2-_yBeZ6-pjn1=h|@iB771H*E+J|6+FZk3mfsZ;GWl8?ui%v1VnQHAC97+CQo} zSEV^&Gdy@CDDV5NDKQzH4vmxOX=7hI7N*G)b@}ygNq;f|FYD~S1>!;c>)$R2^Coc- z`{CxHUtF4%_Q3FX=u4zsg2JDB=(>|XBUaqj>A zvp>Z4Kbie`vj0B;jYr6lMN+lNbyVdZ8#CEjX9LnPCnA*Y5|h`BZ@uCVa!gofF9k_80leZQ#!XIQv#6>X+gfCMT=n+I3FNacNx6x67VgL(y-5 zFlhiS1nm2ErQ$ZH$;UW2{RRR}mdH&_oL_sBeP=i48F60c3$wBBh38e;dhPW7>J%W1 z{N!$n?`7)C+8_J)gd1O~N7ZvCp&X%xW>1J<%MqZ#b#gl(sT;OnM2pEW2)>=Omjq=! z0;Yncxf4No(t9jqwtg?Pk@?OqPCJPpbf!VZ%(uIij^!fP!jUPmAm(|-%Iy4l{eHi1 za*0hvXLoVipW41X{zHWh+jC}9EY=cL-8cjz!LecUFzU17kgk1YYBCRAfIYuZu z!zouu3)r04{5gc0??4Y)@qA28tQ+4(#^hwUbZ}_E=tVi5pnOq&GdQ7{*~|z(8xC87X?;s`fMngGm3LtB|Y~x~12vy>1cBxUCdP^m2+E_1%0-C=~M!?tH zWWV!t@B3bck)M?2X<9GS9A$q9NAYl&h|IEaBE4E<{XLV|)7QMWlfmpLkMl=4cWBf4 zoI27XNkcDYVkEUXYGow8Vj@M@jT?$^Shn#g#k@q_F*%V1oG;f=CRqyy;dDnP=L5Ry_bIHcFer)8Vz16N7?S2Abxt1!u)3R#46(S7@eK zgL0d>I+%a+v2Q@gsJCR06hC?K(%!YFR|1?VqWYMo)A|thR5?h)<&?wObyH(rYp=<5 zkPaY_;P-(!{u~hCyIO|Hwej?VO0a#J>fu^OT&#?( z%oG~=23QY=dUC&5+mOcAmzPH^Thdu5hDixr$^~!y3G2sM%=D=1>t8a2*ZOp){n*4Y z2l(*%o{GiO?a5>SWj)u$EUS*?gw+hfFdKL5!{!FSKQNaup!*&AvwBWT_qZ3`CiQ2J z>4`1-WzfG%V?(F~9(BwWUKruu{m|t_XtAOvR|^wfDX%rQd>a6W>YiV=GtMb zs00o%=(VNVeo}xoly2*2mZFJWAIl+nG(zU98CvF6{-wbaZp0Z_vLUQGADam~{lG$<;~Gz{;Vt)l#6g=d!}FAA8K<@urOHF;#Uz@X6$x!kfHqFChbB5qMUaFY=iW7$OQ7?)nH5MVsndl57JUCmE2}+*k>JF z7pk;%TjA;o4sPd(9^LCpoHeh!=p_aF%1WONB~+rB7~6GPTRgqq)VULp;{z4y}@8 zs;?Bt*=qC?tsP6Bh17DM@=eQ-p^Q1UV2{iBP9dZ(>&Xur25J^2|Dv#fWh}0XN z>duj@QO0{xF)3&Ueir;FLL zfFOhpqlTN5oSSD~R$hD1J#@CH+Ol!=3kB3BO~UUo86;T28;lk$^bU6dc^WfIt~70h z6F{#dlaQQGpO4$G-^wE_TqU*Of}(WT6zAl9Xz0gEdX?nURJlnsbCy5ZmQ5Dv5Q#$v ztY7A_JSTO4p~Sf`(W0mSS)X#-q!>P7*<`guY{MSUZCAmCkWJgqCt6Kse0U`9RafiB zfVR(}25{>WD6Utm^U!aj;@5^ap#%HLY=G|J#$~MUy5rnyM%}PRDshE?NHVR1r!W^y zaO#Qu&ZZ(xp^ZFG-ueXCDmKVUKyzsKn1{OgpuBE*B09v@cMu12tqYw-SkoZ2{(au) zQ%L4=diyYA$ZZbI-AsEUYKYXt8;x3Z!y!iQ^Z2EG#P*o#^@mE7tah4c5@T;aza3L@EECF0|WVn_FqBK-+_5qj~)DDBwmvfa3JToo>j+Ej@ita_-rh z+_h{dsJW1Kd9+BnqNwqBc2wE*=oHR693BXl!q7@8A3|@XcE7T8Cwrb*6pQc1>9?^r z%V;5khWkqIhVj{m)Y3dy$^N~Ua|cZfN5!0%BDB!ecS8sRXn$dkv~b4Y`7J@UjIeOR zgxMLTDW=}aII7b5Ix(mlOEyKao-&Et0u4my%mK0$>*ChB2z@q2VW7_%ry*jv`i!}B z5>rj_BllUz-WC3N;vJQHa6z>!QszE<2$L2`sS2*)e9pS=OoXFH-Q@G$!Ki{xT!J(AXs`9}o55n^JRKKu;?`WA3&6yoqzYW+t@GXHDRYGFgDYuS`1y+N z0mFs*aN{?w(auI=O462(ZYi&hNJ38SEf-X-X&gSWB(^u!K5zTr7sHUP=yOG%oO~)y zx2+T+QV+U!;_s0zVk9qXX*YrahRgADCuNZA#zO^Du?dt&_&P0}F!u?EYAl;-maL4> z@sy`YEQZDPS;MosziIvO`H3lR?VM>e!Wb|J&YZPCq8F$4a!yJ(kzri@h8#{F=7+rn z{-#Cni`E%|0{mn9^yX^XX8R)Mi{?XgC-`$J@2(?RQwV^)O(p&+76oJT`>tjJyig(; zoFQwhbl}j0-+=+#_^Dku7}+#SQ9Y9}M9H69r7A!$`Cd*m#)t^OH2~VuQE)8AsvvZMc{OM$U-x@lL&xHD0Tx+KyVY5$A_Pp2*9weKx2Pa2u4!Z|L+6|IWbv zp*cs0s=u9%&|N=UAg_;;gdfF}vD$3xjXwe2L6=1h?*>5DIy{9?4P}FK;!eFY19Pj_ zSRI;qLYOPAg|2vV2ec6~B73n?o_kCIiXHJjV!^I0sEumk*R-}$+8&f;9rc*3azL>r z1f`{y=+f(OUY)Qla9sVmNQK&d%h?j`o$FoV? zhDka$H8h_4a?O&i(JnYkxoy_~m_V{-a;l+_>Mo8qw8q-vfpGNJ)xlQbey}x1+axLh z-(Oai`;o1C-$5p5nLRq?-t)$MqRBCs^$$CeHkQVhYxxW}BVV3uWgmKK`jn=@dF{FGJgS~ETEHe`7ZRW&U^8%vz;`GDH- z6R3h)JFhiHY*&>Li4e{WDy$vkELIUCO4Y%NEx z+UEH|;*1?QVDbH{vxZBC7zb{k)OVH8=I#I=hoE&qnVc^kDyYv@9RNaQgfRGONBKN= z!MWfxy0-Q?^dq!aUNdjHLra}n`XiRZ)8xJ8n4%S!w{ul(f`CEL&y?$u8vxq% zL$2l**{p|Ghw%P^B{=Dj&&r|a0#n^TJ-1ALWNS>pm*+sn!{@!hwy`@60*p(wfI#BP zd2&p*v1X(c-L;x)Put20u&K%SFO)R9kq)PWhm7VhaqI34>%PdfB%Ln>RWOz%qZH>6 z^0Km%u*trmxOCCHMRt~^gXU&+&+dGeK4hfJIry5QsG)NI53|D#W|#sz&m>%$`-%DAJs)r3S7xIa|H7YV&5 zA<-Y?CvZSzEZxJ}G7)`;dod^7)#iTawIFwV4hSCeS+9mppe8u=LW{A9Go)Hx*Ca63 zl2C&PHKqe+49mJwPT9`w+WB-x6%(HR#+fTlSXejp6pE)1+g-CXRtEa zgq|G7cF#+!`np-M?AuH;{vlNuXWUo}w57EsR#HOH%{~qE=5iSqWN~moFhh@U?{`^P z^4OkZL3$-WgOI!vZsFKmP@1UBzM{b@%inBqL4-D`2*yCOlBT8xQi|~A>B;raH6Ena z$+F6VqH2#~3B{-1+?y!Z$890%7dsw?5B}^0kT`3HSyWbZXVt-Q5mU!=^V|uHjB;^x zvp&3wC2O=DcygXQGAe8!By>~3>?5j{p3+oYA6Tjdt9Hj zoFokr4@FW!9U3ByDegtKXrrLlRcTo>_yMjqQF`=j4b5Y|)@EWC@RQ(njspM~DU7>- z)6$RFWr^AjD?mYi@}kn#h4(^`ONTRdu!UHja(Nle&hD9L@9#NvF2)=J;^__ao|Nx_ z!_aaTIxU`mpzoNA82~b(Cv3QB&9QpMyDu&T6`c6g?ospL-s#$M? z)HN8zT4pN|xXyatD3IU6W~We=`%K@!{(81MpcLh>;m=6+<%GO)s~YUyYIjnJvGm&ze|qOV^abzdN@j4OO?M%c>}a zVz*OKZe_5s;%?iIMa?^=Lj*7zsHpcvvP zy+OZ7WmB}I3~f>vYXV>O0CX435r+(WjedpRZf%6#YJ(e6cX>^qYI{@WhMlYhH!ot? z>46@cB543$El!d+VVEccc##gOBGre3dZWtPh?0aETIM0cJaOAqmGk=f9%XE)5=bbk znoXKmI=O6F5>@|Vcjr#3T&C6+_r=jD|KgC#H_n-gqE|yaT$X#W(>7~NDNnn(QusEj z7qfy$!!H^i5rts*>|nhd*o+e5;^VMwzt6XyEAJ4y$?&xZN_v{IGrqyRF60f~V7)UO zJ3&{fnQ) zXKH9%lHV?#_2${5YVS*W>IHdC;(_-~vLRHCmHC1{6#*ST$nsPtnVGJz=2X}6Z8MwFBT&eaVO93Qo*<}O5NXU;wKgTcQcqTU+E=*4;iC_GI)aNs zRLaQeIu`cSKG-1T{0Xz1}42}1$*YyTXvy*p8q#Hi05P-cHr< zyG_TOe}y9d_TsD@FT(nJON;S8g`9r+=KqWX*;#B1AWwVZ+JC?8Juhw|b6jWO&!PX} z&G^@!LW(>OPvribwEx3x3A|aIl~d6Baq#>~QTmsQu)7;GC>(pK_TO*I+{=rwz8}B$ zU!%2$Rq{4wV3y|dbKCx{&64GY=6w$v6?bOzYjTtmNHSPF6+;)Yh{ARw0 zPyaLY{KMh!&vN6fC~pQ9-mS_1{kDI{$}iCT(}kGX#tamVb^e6ZKYja4!eYlamgNdECn0KKbi5L z_tYQP^_Mo%O#Uabe@O8E10vgZ@y?l6=YxFF{5Lvlct_#;!z!LPZ)Ms4J?vhK@^(V# zH}UJfnb)mXa&sH@Bow{i!Cg#0>_TXW6uebZEj&<;_9`x1y$cRGEoC+#{ZL`6UQpAr)P#B-tjMylmGBoP04r@`?jQEndh<-tqp zybLK!%A#7Ys?++TJ?XVCcoZ#mIPAP2_6`5Ms^DD3Pu&BU^$Jz0Bz7oV=-L|!`WbXeu1zzLh0{%h6?PbyiXk=U~$?V5;oXwc1`oPU#s zU4=l5rN^*Rr8~F+(u(uiQ>azmzW~inw*kw8IaYx&VA27gYBXj_O-*+9?34PV@+$Li zxTbFhJ0r^DUim)6tM^MhS81ODzHbAdv}EKo5axRH^&kI z)r;-U4Th-=%4AE@)>>`ifyiK?W|IZHoCMR%oAsgG+#m-3q#^2HXm9!youpt2p3$7K zoNfZBKdq9qxl<5-MWNR;mZs8Sz8IW-y-MHZ`{69)D;*&XvfkK=DHudy?vY%WTa0hN zwa-kI3keFV9PQB4RqZbPD>ubhZ}8E$FBqKWt!6+9=1-2If*s_s@WyLs4#;UuRwT8B zq>y@fUryBweh$_4X(XE~tVMReRLDI$FqiG!Ml3jR;VL(C@f-e2$EW?L2Gm`A#H?(T z@&!fTPL;k%G9-G)IG4lDf`vZ%*I46Tuqn-UH#Wq!k1HP_x-Xl{a4P@!<~l ziF&-+Mr?`}O{t6~rv!JKYtS?a-`bI7pKg0m;n;Ba+6(XB@AP2oUrv=X1XLCQ-BJJM_=gtm{tuhXt&O zpQ~-UHGBgICt~z1&dKwS5p{@rfz6}A1!MOjV~0DmPZyuGZNj0-Edpg*Rcyc>i)U+c zF-9kyfhq1BZ>ooqxXY!#o+{1CVLQ5=@`4-tmEiZx<0Qd(c^M_&jGYE+%ab%v==wY- zb4McyP*V(mNOe_?wWRl+>4y3Zy!O1Myxo^u;A_?~=t@6SGm~#CCdyx;xP1V8^ljg3 z!+YFRuv9`Ume3m>9k2DS-eE+EK~%p)QSzo6x@vhnx`7_=nMJ~`LgNBjs$i@0wMB88 z_ot_JfjFDG@Sy`wC6XcEE~VZxziVt=*cpQ+T`Bfzz0#al{8$|1v0sh*DmpUTH8!Cq z^qRGMnK=6Ord}X)dVjOFfaq?VAVM%%T-dKlm$Vg-hJXz!+qLauHrigh4Vmi_K|~&p z*5-)XjUFf8j(N9*{0)&&+)A~t`5jmwi!rDKEE66C_phGZMY=xd68<5F*`3stjU3ap zPrD;u+)Q||ViBDY{3yf{r$#F@T#G7>xb0&0DW%*v)zt~K{JX6A&rs8^Q>xxVt}~aa zW0o9~R5h%YJL_<&@KZ>SrLtOgPTrhPgFz87NYIub0kqLqUUoeC!DMP>3Zo%8bMwXb ztP>=4Hs(>6y{(Y*-F?yF}6-6WWSs_IkSC6$fXqh<2?1QKvw2_z=0 z?g?X=s(iv~D4)Fx1~n~l&(DF_f(_x5gKhg1rvi3R@!@d)kBT%*qN!(Z?9@_rh__Z zJ=GL}HqpZz% ztF2wKAMK{+^DRx{E*X&Ms>t1u@=yBQs5yiT3PyKc1J|GK&rdjCD-Onl8|O(C%H*oK zj`I1AHj0x+bwr$A4etMoW)@a-m6t7baiJ1f(cv&Wt*Ku;crC*{bZ-TgUK^!4yVP4% zd8Yh3y}Wbkl>svw4vfDwhm7^0%2~Rm`8`MW0p-*u6T2+4VOP!1mVXx!`>1_>`)EC0 zj+P^&@I-{pVkDs(Ab)w#@4B$g<6wJP7hL#~)p z!5ZG-puQH9=q=`AxwunuT4Tmz(bI4fX~lR7X7Lc}Daknd+5habV6ScN>A+b@@1U`d z5TDRQu$Wz1?}Zch&%y#6YU~ccuNKx_koo+M+`F)E+F2$h5i@FdPj;7232Ehu+u%W% zWo?XkAJ*1AR(~pIOoFy!Gb=yQ8$YeIB)s@F`BOo;BJuDpNU1eAQ}bo#Lia7On6pgg z`n%d#{tNCkS%gxIlDcV*QBKs`e>?Vq`A8W3usuR{>oKM70nymy7PaUT170g#FqJH- z!0#cqK;-$RQP~(LnD#5->xyDI+6yE$#o>2nDR2sF+|aQrTNru>G-n!dQ+F$*3;a=! zxl-gcgLxl6=BK@@fl!WpI=K7j<0lT2%oGeg%@obnA#015zFRi#HN7t&6BH>907 z{f;~2<_id@^qB4QT?uwjc0Q*E?&b_rPZ-o09(R-0Cw}5oMKmU=8P$Eyyl|}LD<;!Z zxd;d3tp@@A^*9{((vUw;-FfVu+GC3LR(X_|w%o`I@7tz<54`dUjfA}?qr0Xi>M8;E zR!2R*2=?^ZoR8|GA3A`whPQ0&74lgNA@~CD) z>HyGtb>hYTyNZ^EKH62zLy-3?^HXtSy3$g3RMM91Zqhr5%2VIJa$eIJIn^@2AiS>3R}w(Dwx~Q zUkaWH|9%@NUhFAD0cpLjCWf^173~~K2ixQb?Y>uYr2%-tC&v$K>=q3vJZaK8BA;Q~ z92>m#v6zBXAN_n<9^noGCgFVsDuw8?yr^$3Lb~_qYk1S`fv;OM1N56ejA8UYu6{Ea z84k%9Ra|h|nE`8+T@v>^ma4v;TfoFfmsU79X5f~_^nPdGt{RvdVfIRmoInxvfH+Zq?Zkpy z&T2@tzHvF+#`&vSQ)c9m=W3e3tMD{I3HmdTa}6w5NSFbAYVSTvYeQM9g_{s;#F%J991#ZdqG` zl^biUIxB_Qd*u)!sCIuuuWF+lalN9WGM`XZr2mNiZ!3_uVe@R$Cs{=$+_OH%Q!3{Z zi>7~L3|)S8pwiH9eb#pgfnS9vu0R`J<3aWgG*~SPbN7o z0i4cqT3kOM*K@Q3M4qQ#whq*NhpFLw@YEn+ubj|RXM3G zP8|6_(%T2nFJc$1t+Y78FG+Q2+!0(jr#+9A5Vq{SnoJpWe{OyN*xS7)hu6LiSCeka zid|9l#Fl@?Hv3yXd%sWCzcyCe3=_Mp!+NC(@}A0|@W7s3uGcn?Ew)waKMk+qIQSpBUX^yJo7iI)bO%-im;ms&qszs3m42+z+Si8-KMP z4F=cAId@voA;G)aeCK^HQ&iJUr?nh2LO<#;qMj1ZwGo>~lTW?{>p|hd4rxoCiX^)iqD3+X2kzDKAYgMi&8*gjO z<&+_sx0mlcTh$gR+E*_JBE%`$PdyHK`N4CojmM+2NGTt#Sh%z%2-#=LOU&e#1t)d@ zNWowNkR1(V-jMGIXtZC#A{*W8>f|VX(3Lt^u+0njd?e+$ja%}%nr#Lch)3D>nubcQ zoy^j8@!H4d+jNxr`rZ+n34Kq?=Q||>W%76$TA3<(?c_D?%V3$47bj2a;NKt@wzTUX zWeQ?#MOx~V;(R5i5wt^QG5g{qgDZ#^u_2Nny@c2v?=Rj~QEg0_#rh}yH+omL)8w`K z2V2(kP0_go3zLyql!M+=hw3Kg$kDaC1_|vrUr*w~zOkaZvXHx^Vi)qaCk^^%GJb1M z#GFKYB#c#UZ~GlId%s{YWT&@|30o|3%U?BR9%k1%o4)xAuUdK%FD*c~4#~1uu7nQC zeurDB=pKrJmSYtg$PR+f*3~$JE`pAgAxzJee{`XrjtrLP8nHR+6yf1n_UCagVwNa{pAJdsV>s`eT>_M^?(|kIzL6c=Pi4(Syp&f>X zq$6DGl~6XBk&P{H@1|HTO>-&b$?V@R^gnQ8p7sNs@~n$=NUT+Ad@iVPaPZOM+I`x~ zaMS*N0K~RDQ7Dmm7u{|0Bw8ZPDBzmyOZIoXI7ZhMn_WYL5uqQENwuHz31&3Gmg;S^?Q*p ztz6thn1o7$zH0bklj(blzNx*CaMTyx(HE?(L4MvuVP>Bwa3epTQZv?kYyEFPru_rR z)_fK03&2NRq7?H7OTB4(8EXOlNe0)tR;~|0%gUF_yV$6MD{GwX!;HO?pnGHIS7o|N zbm}`W^e)OgUe@#D>MN~HgX@d};&}lMB+ZHKerQ~EV1u|w*!$8Ku{9X2Fsn$>K#Y-~8dj;!eDjq{yQ zq)2+(3Gz#?_zX>)9lrMEH%nRLi!wHPJy_EEnk&heP(-j3qwZLgpd@d87(>bV1lGg- zo(XTYBTqM3SO<0tWBJ1e>qhSTIzw7(2t%54feRD*j_u{GSx+3GI-K`8UD5QfduWF| zYci(wb!Vn7-uO2!hw4UE>dg!&;Yw%}XQwC1cZxCV@;jEYo*y(Ni+Slj3E)}R#qv|* zuZ9?Fj#3>xI-}Z5V~)7qK(+zMe9f7i&8_kz&w7kLuY18LNu-?Ow)bE%(YnS<_+{I{ zdJ(@D@J5vX4P#GDsgPh*R9=7$fdE+|L$?KpfDUIA_c4lY5Gon;A>V*(x|ej(-qkjSVuiA6B50?_0jj$x z^sz$XeZv>?#c@Nu*z(3xLrLLRwS(Sx-a2cUCWh+DcRM3R-osWg*1!x+=_MA%(wXP_ z5x&V_t;eKwLD-<*Yj+GQs=>n8tj>q%UH23~>g<)f19K2cG}D=g8-siu=$U*ud+mTo zNU$+M+oHQy_)hIX-_Iv0{QKG~UpyK0JDCCB#_O~xDn>0@Ndb=4^V23rO-z5CWIrxc zdIek_ue|4J32J1=JEIfP6HkAK<7ayxZrf>t=I+m(PLS3^z z0vtH5!nR#aG7_6Q7rR&@uO8dq86nf`)5gnlIYV&H*N61ANCf!(;UgJK&bPNLt}w5s zzVKIof6sdRE^!M@jvs|v6ITe%f&;r(RIDJ$DC<#|vq!k?ug7ooA>;5X@u0Pww{?km zdQ3&Caw)44R%^iSp*Q#sB{(|Bc_v)Ctie`qs^QbNx~P59v*}~{J^kr#2)*DW=d>%$ z3AvvOM->naK?~0*pI<+TpLHlNCg>vUE{3-1?}7uHs**1r*yrz%1+~_quH3ndz>M?w zK{r}I&nGSCkmLJk|G-6XV){Ck;zke=wu0*GYrO$xq8dPaQ9ArkBskl8zQ zH$1;1%cH5(;T2`nC<}Vs1S&m1O0smy`FE;l+i+bsIim+$O0;I_9b$~0Db2KUk`%4H z*Cw(`=SZIWHCPRL=wiWDq-#vkw-RHWx53WerqPx}{HoL>v0OprmDVRy!fa8kA$fYu zx)>Xhb?(}{!$Gbg5QUpJ;-=ko&&X-yiM|U&1?Up#+Igv*#Fs#-Q5sY_hD2dA>n&Qg z6z8SBkYQUbr6O`FyO4-@2w5{5-fL19V`^a$@5#N|tc55pp%3dE6+wrQw3)7GUiEUV61SeLySGsi@AWMP8 z<^u8HrtOso@t{wU8T!`}0EubF8qR{Fla-9|T@C->sMWtKO+N%Lv(%+4x=RkqAHFJn zkaNjYI8|-`SU<-Kv86jhD=W>!BmqX)(PwzH67`L_19cbeeiu%tsZKpl)@w}^ zq3dG;L&g97cpQPC<3l|#!T5obJn(YdW@+R%a~*L^BwM$nV1Kqs8NTBCZWGk6Cz1fo zN1f3iCCD-gIF~oIX2JF?I%VWDiovUenFmsTZyNMYB>q+{#tc3D6Ep>mNuHoPxaBwp z)(&je9Zqx4PfY2?4JNk*f`I2N%ft*y(YjnT>B6vkWjJrd-XN%Ga|Ge_C2E1Fj4uR zs1+#{t;L7+b~X@2_k7LdY0Z*A`wBrQ83Bpj;ge9b9)D9wp!eXGLUPu+#)FbWLvh=? z*tcAslF5In6}T}-iA8E5zM;#F)YxZUo&DKCg&bp!W9;qJ@OyX-BtSg1#DEDk>hAbUD`N^G|#&B}0QAEh5p4@5NS4G_yfydQe z0(5SM${f!BfThNi9p@fxc1kVFky2Dlg*$09L?mp%T>5NZh>7q0-fzNrKZdnUPbkNX zq(-3+QZ`M&DTjfr^lpP?RnUm}*gRQPLd!WD$97ah2?lF_#pU`DCWIOyEDn4K9qQw& z#LnCqeFx$LMx~dl?tHB<3m5e-ekrn&mGk)7Ae2Yw#b$l5nvMFFsZ4igfa68K05Z(| zQm81Amv5h&vhWz2`&}S37fzhLoK&1hRWf^1-iC&TyP!G&u1}yt;9ptI3Z|2`PqOK#>e8gnzB`;fqcvl*AtA0IF zNbeuu;nok9iq58ZZ1NsKTxVTwG*|DL3VZDP)cO{4xtoLj1Lg-?ae)KdQBD$#qs>_6 z9soWPV-9!B!HoY%XSt3~?tcJ3E5P<)%tT=Uz`#L_&0`Vf#M!cs(Lp4W7x}s{&i6qX zitd+}D7{P#=neMXM~aq7t+n~O%{*CU$*C$K({rx9Y2Yub{bBQFyA3vZ8TD{; zi>!Pwmwyd5UM47$hyhkc1SVoKbi!Fr8BPGE#)!q670b^lIZ3+*)^J7JIbMH7p#a~Q z61&&!BucX{XwEFNH*9J>db^4IZ7h`l*m$9k8Yr$G_%o)5mUV6J(LWt5FIhJm;tINt z+d6SmJfpIO`ZOx26voD}1OGeU1-UQzU5J(Sx)o_>X^j5{AMe&O5bB&WkoGK!bU0-QYn<8T0-=#)0q zTJWZn&AON?*-M%@cY5-wl=-U%snO)u5Bw%x^6sn6Z9L;q>0^&(yjptB@fxon^4vh& zszJ_q#KabFkD*vwSeZ9|T6NiN;M{!U-rhdh*sHaea89>j#tAi8-j@O@!=?8Y-^n>nk9UUONQ28xF!wB$BQi*f z(T#U+LUj)x_+?vLCk}up9!%`Tw1${SQ}C6)dqV5V)Ipl_D1}s z9c6SsY4p0)&-#~Ix4itVYKDisOxp|F6D$j!1(zmlZz?bE+FDsqLihGO@2W_Xd!+m`*rk{^iLDf-gVEkUkuS^KOtnEB^JhA^qF*?@I~W-+QIoq; zQ&JztwNC1>-VqUET|}X7WiN_txUa0CCi1>Gwd5sf2rQ@g*?Tl<7t4ssCHZluu0wHrTNme z`nx#R&ih0R!zQTZoCcAXlN^}2CtKiT#FA>Ua(yG5aqMz;R4+eucF34{aPfX*9&=sW88(=QwIlB_wR zpLNIh^nX3_+grGsRhWJINb%CnTez{t&>vuDNyI(B@jJ{(=dE?ZWs#Ky-F?of4+okW zJG+tMQqJbFq~u^6K3z+oa=h>XXiN=6WW=xz1A8ST8QKk;6&0z22g}f-}@VypcB=o%44_hlvlFKuqPXSsEsthvE1o zKo{*Q-jMU%3Ek76YPB{ooVwRVD>1KoNUaP68S@qOFzh_AI;z4-$@03r!6FFb|Bj^o zpJx_v%+9q$u5CIqx8-S6NVo7Es9#o@=mHbwdz4A+{yjf~F;;hyNIJ64Q|ps`IV0q) z_Zq14#$A*JLvkIjQTs;!5#S!qq@&}5hd2gDUw1mI^H+|H=Ocgq@iy1*pqx(KxvvUk znFoXNJKfDG&Ld|}zcO^n0e?I&VYX3LzvsYZqi^byGUqx5R#<}`VElW>hi6LHHCgqX zEX$;5RpqvaF>CRT-Yvxlzv=y_zQx;Ir)qxxJdbIzYOWMt-mE_><9N?Zs*nBnxMibs zUd`@kYNJDSb{G94u7ONuSGnWD6d+*8w+ zNq(ii0ee$N!@Sd^p8AmG1PF@S#5Rt2Fq@D{FN6N!1mZ zE`D~8*7eZk^}HAcH7w5=?Mul(kNd+)b;EOHn1iK#-M%kwEZ$S3%@(w*SMy9Y=Km>t z_^vVUu-0zNp_^$%OL7S2Up-igS0kkFdBOvw?|$Z94|+_EnrUICLDdOL1og2#{N5Gj z_+1BK*|hm&ci-nxc`@s;@&ib`SM%dr0#*YcDM;7CpXENJ;^5SS8-=e8tOt~DdI{oC zKhr7tnIBFIgY7~l-%u@fyWL+jgk(RnM&|`=WMMo2h#QSe?1#;ZLdROci?C9>6FyYaPA1ss*MG-eXAx&u)vgO%&H9H8(>HIHMaomNTimi(?b#OA+heUa-H?a~C<0-!y&QgeW*Eu+_b$Sv_#Dg6UlB=+ z8tUcqFdpe%PzY;HIiJuU0m>=5@DE96-`=!5>#sEzF({C`r8UjmGROw zc%~hxd+70{{V_A|*qs15%h^@?5oe$G@;yn zZS!xC_{zugNrhkw|NGmptI*4MCVI}0CEh=(prl*av5c&gV z;`B;}X~}B80`iEVSO(;f>43@;`*^>(wVUzHn)tp?f@I<(D(0)=Wt(-cG$Qnj{{Rq# z1r;PzMCO%|G&toR&rZNEimMaJ$phi|Th(exJTikJ)uemHGDA9xeD$*eY=nmVZ`97l zv0EjSq{*8O#2a1v%aJ1R@6I*>RQ}1Ez-(M;qzKsmW}yF*NU@k@FO`vhnI2feu#(UG zEL^;Ze5&@MB4%nlEVyS5FoTUh#U_L^e{VDaExj9hA&NfeL1ncoOO zjgqVfjKTNert7{4%^L-YshB$uY!5jKN4F6?r4KC`rO}DXC8VE7_BZraTF-j*=WRNa z36$PW0c?N^Q%zPVJ$tmU?c|EJB#2Was%2Nje4K7G9iz){`11^8(|>QaCvo-( zTvij~kaAa5GD@-R`>*Rro2{R(Lid(V6u8-ka5%HljDugHeOWN zVYnJztp!0>V5r#y@Ei8la?f*;*o=Xt=RyB6G-y?WsV+*C)sUMG1$cD<(FGP9RdRgk z*1DBJI!jd%)8I#Bu(qbDznR358YD<6GlATg9l0`=rDkwdlG|O=mi{XmLw^(7bKLl% zgv{cp*#9%|@_(%6w^=?POsXg5L1+4FAQUibVy5%uYCzKnI4f8tFu@W+ep=g{*2 zEmYok^+V={CsCKXFBunJPiFwSK&*31i)7->LGN0iRAPdIOL8x%W_wq~Ie5onFx;8F zOmXWrMKUVcyQVKi&SemKMa?)TY@zs&dDNa3)zJ;NKB1c3R9^xqHuyL*c=w|`X$`BBnUs3^KFHBUe!-n}ixx&<4*^DYq|)G`z_DruxG zL@1xW(3M5X*mi)z7~#-Sp&4srFS<-=9-*pl zm9t@w)`CGp0!-h3n)g{8LP>&)-m$f~ER8jb@wgk{0%y?^Ip6a19Pi8oJ`uKR9m@=l zqGe-zr40_929nc|6ssx zMpwrnb^!NDJXC^lQhB&~5|2vK7e}NZ^{1P5JKOJ6myDwLK zKwd*H^OfzldAGljD`wdO+z5B3A3KoVVj$q3dfU7Rp?I*6VqHAf7TOB4gl#XvtQR-E zjGb)fK^(TE2~US+humt##;Wwii;Y{Np&~r$#Vl)%JM#766O!f-hH^IppEQ{+c){`mNs9B!;Ko1|Yqyvihw^c}zc^MdEVw}2%P^_oME_v^o9 zJpOGbfY&U!6h{$|Y12Wkk2e9WwDJ)mZ5mC#(?G$Ju2+Va$vdn9Yl-w&1&#)7n`B)7 z*r2Q^fr0vblwyCeJ~>jf3ycobwX&31NNY%H;#~90O!neZ@)AX@PyOpJR9&x-OeU`L z7iSW~j>RHBZ8PMyXg0>$4)($Q0J3PNUk zgz#EnO0=wXfwrGFl%46X!CG^gEOP?6?^(abWm=W5H3uR$fKQ^P(y4AH-yX2?k~d@v z=%D)$m}Vr-8G$Zhqi1Bk@R8c-q2j`u;T2VcCK3dDvU(IHgq3X zb`yWkP5CSE^55eNE?8a`oK=oF#d-1wz8>23KGb0p{r+?BL{u58xN=v!)ojmT&(|H& zUk5VW;WaPt>){4UCBJ?20v44cj1RFmzc9S>GObji^$v3y_L)3>;dg0WOfmlsqf3!+ zF{~BpG({yfj?x%is2p$P18ZOW>I~_9Jxisr#G4)lP$k(rPKlrH+P{ySi~+%7n+%!Q zoRr9Sx@O_bArY|w%OALVqZ}!6_osN$6x90SD=Ae^Kf5jX0m?S_$K5p~{2@0$NdQ~% zUx{?3nMwAYsn8BU#=PRokSu7=IM2QSi=aZgX1^KrG0(t&i+_fSky>o)g{vZ2AH0Sr zo~pk8)fooRT-^`AYR$ZfBb>@n=B9kQ`KCCtT{-#8%pvlD94dn+so1~c)MmuArg^HM z3};BEY`vsee}RstI3}rLT8qao8Ok`d_Kzp4cS4?(2?-UFKYDb=Z9`*s2R$J^mpw@{ zkDQ8gr+(ov$q1ZaiZUaX2$!>fp6f5+_g6}-e&`)ctTr<1)Ua=z?U~;Y1I|IL$2vg> z*$XNIsqY6@2gZbKoNOl&ahQ#bJeKt!4Q%DG)Mr3Ho(%_&g2Q$Fp>&}l+V6K^?z7Ek zN@kXKOxVBz&FOz;0od>B#j`pj#cOyh7WO1qT>E|j;yqM+Q^5Yg_D*LI->3(b=YgUl zANBce#o8P1voeVd$nO!w8h6+}n})4~6-A2dt1>T{Ct6ceEcAK4JHnSEi(o3M0*WIK zvt|I7*N%vq<;ChPB}V<34IC6^2DG4%2GPA9Pq#qBeRo>d3Ufs5q| zv(!wgH!e|OWHOY_GaH|wNd6Rl#~HOgTs>^A)BP5c#?0MszA!OMB%ca#L^;CfkQ4x6 z99yQEA||F1toJrd-;yV)w82}7#FW3jkFFwpj4SUFjy>|>5c%`j&bu~KRkL61H|p+S zV;NjgA_AJ1#Qp6kPjdb|*?9*Q)!?7Q&|NSrglV@NcWPBOHuC`w;e;*80`pl1il_32 z+ItdBn$nF-fQyf_Q^HvKmMFPr;l=`%C%&;%=HHyZ*l5O?0t(RHab)7(23!(VtbHu& z<5AK-ZHKJRlcirRXEj#8+nQ_t2^v{LS=q@qs}g) z;iD1Mfz~3YWy@qt0QC~hAb~&Q6k&DdM>~(dB)8b}#yKb_CRF@E(5~+{jr}JfBMHK+ zcQd^HtN4B8zco-wYX^5Hnat!l5Xa4^`6lM{0P8#nEU}2#G3SJlGmdqe)Zu|vWEQOL4MKRx6?~IoP&s+|HY%)%L+!2iOnI_g+ zXml9xs)7`0iZ(8#@Ws!^9tVP^m(6RPWi*=!ugGu0YBjV?h>7IbZ_yGBi z9M|Xo<><(_GQE`1QJJ)pwYIRH$p+$4GU3KvO+PxzCHkVweC^uMH?ON7yGGxnX2qT$ zad_i)2s?WpRK3!Dhd6SfqF4K>HrKK>aFZG_tx%WQn*uzL)MC_PYB-06cOJ}!QS(qeL^MYm5U6u-8){{l-lF1y#Dv$ui|q)UiWR*r0+6& zg-7@&G)a!`tZ}KP%=t4Y9LqXCwyNeVTbxP7%RMo1VtF`%E<~b$hu`2aj>-sB-lB5CkubSsWzBny636U0t^aJ*m+EFG z^`8lmd!=yY4#xmf@2QMBi@UwC*QmjaT~>K!CfX96D&l zd7R7$0Dg&Y^zK7`Jou#)S@lGt~!|f-u{h`fOF^$0t z=YN|Jqzuu2`O?_9xIt(v(QnTe$RwpfwF<@bd;G1dX(sE-e*cSaoaREAesy4-0R%GB0U|s{jE( z37Wg7)B~CYbCm`BzbHi8yyEMTn;^=ymY-uyN~*93FAX?w+WZsCW;(AlOW820D1E%x z5m0;7!C$>EtGsMWFY%Wowznc_t4AW@hgy+lJW^)iEd&>!a>YE}k;=0{>JF z&hYlrFD_bn^!R$w)VsYk)Wzv{k3!Y03Y{YknZ&+gLv&ooUYh_p@3Lh$rx?89hbLk? zBab)S9SOL9IhUZ!RW_0F`@v5Ce2sd^Cw{AyUq7|$#ciDq zb`{DCveuKC+Lp|$b{ke;!$uzKH(_k2?yHJ7&)cURI?1^ylU5f&B`6A5@Th6x*LI8B zWwe->rtXy%k{Q@7O(4JzzC+=7c6dIX@y2}0x2iBX%Npgz4q2n)8vzx*=k`^1`V>dC z#^oLAf9+Eyi+W9*o1;a0!ha#$9d&qjKObK9HL%T23pt2OQ`E#$Wde02N~^C)5hJFr zwCFx!PtK2FP#7MDnj6#!r7xZ`c;zx!^xd2OAm#X*2&5=0p zq@s!le&H!#hl5c!F^uc<3Jt7sxAE*H-E+^zpQ1d~v9h8F_HOsChKl6CR-LK=6rt>3_O`9asJ!5rltAJc^^81BJqI+k(<8X9U(%x;G zEZwv8(WKf=csdz~bDh>KpSh=@&Ui%VVO$g;X^a|zww=2G>tFIBW|f!^?9j^Gu;+w* zV7oNY9y6;fX7W$zi^anDxh(Pi5;17bF}E1wzb)xw{Ap>)Qy0PfD#19`delRV1b$Ss z1)!}H<|)&PAr2BhjX}wyrqS4oVGs(#8%baP;>u)sn@7}(4|(_Z01c~NnZ=$GCsm{* zR3-vjK5GXiv^Um``e*q-dTYYR-s zsJOOus8kOf=Y0oP(K{tn?^6D-7R!r!N>m>#eQikDJ36t_sjs!hT5krl>sT1y+T+5# zjk{J?;?EjzhNOsY!d6WKETKANcHD%aeMW_A+WMe5SnN_vChtcaEaWf$kxFWyKeO`e zYA`4FkE6hma_Xl|L_MrFT%2cbt>8FNABl$65;l>_ZFjUld^73hf@LwMDL~{k!gJvF zGiyr3OjAQp;ZghdMo3!KjZTD$*)>(M)xW>OA2u?dyls-HaxOwcIKC+3ksCb|jHgzM z40R5xvdE@+yheNk@{68gjr_-d$v{2t8)Ol*qb2txT260^eaw_-4jO4EPVb2aCv!gS z^UC~EaToQji%}Exb$C>?GZ$FCER4%56#*2|HYaq(?;_K!G!J%o(#DbL531k{)09R* zS?W^3hc0zINrJn-=|+H)v}5@eBkwsIv@5@)Mx^tSmc)#fturXR}5$ZWm@f}|R{?GRGoApZ#SBk5|{ z7Sot~iF7T7ctfT zXdK)!l=dG$uFyy%o7b_Uay}jP*P>DUeEJ^wL7$OE_KP-e&p|&uOlh^~M+@Qn6FQ!$gP$H@1NeCHLM;FUlB>W3NLYA zR#)1x7i(HmHB$)Cc{$9>Q=3~-x8q355DEy;5nOroR5^1!(**HoCNu~9NOJ^MQq=nQ zF^y?cmQO`1{huF z4rh7@wu_%5C53KBJ$-S!dq6Q7S$qYWo8(+sqq%F6Xj-t9;qEH0oYp18==^|RF=r#JpDDgNI-lvYkWC>P(6agi=YO}Fval`UjFnyiR3^;<1!N*E4sAuGy%S+VeA zwp(kkVJ?V0={bjCHXN>rmNFL@+w0lm;l$wHn!0#)@hha^(x#nYSyCnc?qp;US5>UZ zwU&#bP%K^3NR+<$lSIG>`o;pYITIALH1B33!*tMNpsWK$%4~&Wnr0*RVlaEflww|) znEUh$m7lo51JsFLHH3cpR%2~YQfH@nej86eEOY_ zk*o^X#P%*?u6w`D?URW!h#4eXX1)2$UYg`;#y3u%y_8nr4pV`5v9 zLPz2RqruA&`2_Tugzg^@{W@l;cV+n~_3y3+y%Wz)k3|0E1vt7E1{tffHecf`wSt{#;96!_TD1lNmMl(3sZJ189_bCmWDrjjz{ zG-A2<2uai^X zK6MBK7n`S%^PeZtOr`@#^pV!d7+9T6%`H^(8x5wV2I2vDF;h;|FyoZLHA6+?>Dz=C z6@3YmcQpZC4o7R~Ax4iJMW?~eKs4dNrsG8i8Bu>+n52EUy4KX51(=pZqZ@{cuo})? z$6MzB!V3Xld}UI{W&;7SaMJB*CRiYG2h!oDA^dt6ApEuOxbm)D=#~eiOKpY^QV-zo zdyA`4Rz!jvCscIv=oPmQtjKO_pX&`1Ki}vRqqxaqGjrbY?A0V|A?Qy{w+xJD-uMtv zDf}3jjWvpF?NS)!c4IWOzwX-)YaGX>e``t$SSiO%gA(Ykn;s>G$&VhHCL_PQhW`Cc zx)h!&bkP~Ai}@noy5i9;t}*kRm?AP@x5V%TIoO@Ru;Du#0}L2u8oR|9moC8CzwQ`{ zTw{r;NX~(B-^+r~6LR3<*@^e@52=t$7`gqG>Pu=7ey`KZ0Ni zAljsBmQ69*b|i74!+7&BVQScCGxA$??nm&wNr%dD&t3_y>!2>SW3{^#ox^7pSBb>k z_KJR3G?v`lw02sc6S!`IiL^2E`q-QY!(MAq=5d%q2B?dkRPl4L?j3IKH%bvz1!ZZX zG><^r_=^lMr>6lsU@9VDBWsNwllh|pv zB(QXqv5$-8iR*FyylZh@wF!Lx?plBCC>RU(ISbjyoS*yVlBB#+9vG$-9wLCiyz(#q zam+OjN~)O}caT}osaS-$PKHKX0tw@8R^WQ4ZXi$A?styn{TTj2zXLhvF?FJM^gL^$ zt+ZzHcpSM7TKE-_RNyCKLXKCk(}<&YI7Eh$dYvoP94T{~{@l7n%9M71P9?(q;LEyJ ze#3G{6@8wAlIYzS`A;wTUk62|$1WaIeSc&plisJZ9}j$ed3QZ3(1kKIuH&m4?-;Oa z&aBL=89g7J*Z|rs3HwXkFTd4<;fgdIz6K=9SKZt3={Hsj?aI@LqmH!EhcEJf6yU7F z>DtS?5z3$1{;a|~x1l%V2ztPEnRb01dc}nv#k!ZhsDm1E!4%#+&eXXvt|TcArLz^Z z!hM_6O@*|SS_$O8o_)8afK1@o5e&NA3YYlS8)Zic6_@`KN}Wm}Ux-Y5Y0TWAe>prS z&|;O-Fv^+sTllM7T~W@=mMBfgOmk+sTwg1F{XSG__lSB5uH$w()_mJFyfFIlM!A~# z@zd?0QJ4Ba=Qw`S=I$=DzBQ21lrTp-;Hn3+P}^E-{J1(+ps6T>_jPaBz7d~5WO+OE z_kLS@W-f|vZY_Xv+)>xYc#d79!ZPaHE+L7h!~I5JvcX!W8`8+l$%}WU^tHDGP9WSE z7f&!6XiR6%i(X29K}sOp85K_?SF3&9E$^BskWH4!5J>v+yjL%lr}~`{q6eH4`hK_1 zyo{$K>LW^ia8xq;IvLdLVeJ!=dOnE_>JG+--#&;Gn1BU-T zyYT77%Rfvl_3J|vpmS&RgpR5`wHs*~f^iS_gMIl&l5@L043IX~Am6O%;h<1Xh9`G5 zSYl%^xg!5%|z%BYFLO?%8A=51le< zeEpN47e=o=n6h@3r=&xc7~b3)IJMsb9#dtIPl3Vy(8!U6Dbj1_aZW}tp=0c-KwVq9 z2Jwi5R7xb@=fBW}Qm5|4T|G+Hl~hk|*!~i@+P?1k^{#V8!k2PBn8kKd8I-_b6fA#^ zqZ9YCbDm4U?pI$#9M@^QH709>7!MxUjd8z`scHK9#geugn2xaV$nQWrYF=qx49T^C z?F^{i7mjYBbRa*N&|UCq?ww|Ovr*MlyH;80?d4}`aV19()DFX`R@j*YMRY-Nz~|#Z zH9VOZ`$ysm%N=C9y4o6D*h59cjS=QJ`}Wvq6%_sRa%f61L@g5#2v^Esn2HlGK{2qc zQyI~&cmL0Ju&*d)?OS`@V`^Tk9Q{jBk$F!+soz$X{{_~v zfq*Iay@H&JOv{mmYF*&WVqRC(;`u}DdtE7`OZQU0?}`0lo66>$J^?5WJmF6lUD&+p zv)Z&qn0$2pr7t8(p4Oj2$u@nvJoE1aqp85oZd#0X!KJ4hE=g-_(cl3BFg2MIb)!J8lct$hQ1*4)60% z&uX%tow|Kt)sWKS!`gqf6CjY;RjsPP8vd<5G5TR|^ZP(RG##lp#%$mjuLsr9xFCVa zx*ts(I9ne#vx^{3Us2z{05;7jWvxk%T!$Zis6Jqj8Oj&ye}FGc1KENjcV{s_sTz}S z(fSekKz7AjHIe)4EwQ}JhsiI@Sf zG@Os>KQzPY8s^R{yaazA9eVoQeB3K1R1}LZ^KjQqQ5f=*Aj))-cZ8DF-(X`a5#E%0 z##z$bXg4IolcH1c%@SjNP3j+RkHu4qoaE0jC#0_(S%J65tTb(#`WBwtC0<`M&h60Z z$HW{!3a*A~lFsdH6DoI(W;N20n6D@}vn!R<9alFQT()4lALVopu=&p9XS@|mw$bV_ zoSc~>LPVxwo&v@D0OU%5<#xvUrVI->(@K+i18W|poi3YjizXAeK*Wf)7l*G}4yehs zsr^|1)nM?4^l$jra8wL!mVvGNHE#5KI~x~D-}2uotu=Tljjc)!LYa!OM{kLd>EB!U zn$(?v0^92Y6yyLYt%8y5+-Hn1__1cN_vAVc=la9df=RWQIeN%f#mp_)m$U}p$r|=` z&@PnqB*YN?a1NW4@R=$snac;$XmJW(4-x@pn;D4JY? zi$JgWk4p9d%T1Xp$0!xEVy!E$-7T(3Bn!}K(X@;{edU8BFo~uD#;eo z-;vS-_jX?S5iP#8gAT)=BH1tBl@kdgD{r%}L0%gf(u;kyo;)`TZy8pDaqRc0;5&eTBcjp&!uRJBHte#GkV&il#exL8_enmedK#AD9dRLpPL9xzv zFOe&w*mSNWx>uOPlG3YPAlFIrJA_1eT9UL(0Bg?^K7K@p9nyaK$4ht?Lkx2KG#R6m zsw@eP$~&Inu}jeGPISQM5BzUD)lIX;r618Y7HqP3Aw|r(;>8|j6VFa`U!}>Ca*X+R z&lP2tOs}f#iV+R>`V`Wu2>xEN%lr=dZ?R)kSArrsYz908c<QMEP>aPg7FSYYY4RM*mjWaeSgt)u;>28PWHJ{-%N9I zF<$IpteAdEaJnMeL-*IoXg@J~#uJC@#3v48rAgipLcCN)YRRe8E&(P1@UDo!W8uW> ztR4^TSZbaONd$g6pV?VVaN#`gul&^tNaT2!S#~*Eud>2h*)_$fMj^z{zRH*!o4sUJ zIZ_f}DscSK-?@zo#|-sO`sw^-e(_kVVy{LnSm3)!;5|Rj-?DqF%Pj$|+aqiui!Mf} z(7Q7Z8h@>Hl}~9q`Mluha}4Y3T_IjlB>*@6NY0`(X0k{!rdPG4lK~$wPv@6LH$}x0 z!>AP(YNjL*sQ$T##L7VQ*W^onhbwr$fao7Q&i!j>S@;yY4!`5L-c9fB-9}JdQFQYB z48(vFSMaN}26{Voh+5(Cab%gkXVZgDn&Kx5H&E#^@#84kY{I4?O*^>5HT&E8t*M|u zpA+*r)WV~qIvOpt3y=qpPGFca!HBbA$|pGy9`eS>-Jb5DG4WG|ea{0g;Py~)bVM_Q16%aZ`^V#$*1607y6bt%nSKpQ z*xtug*k^~M7zc$4VLWE@T)%>E==Rxm$q|=kd-uu8sTL0S0Y^|LjV8P79>D4bF;eo9QrB zdb%*LbxYat>-53a+eDKk)xiYC(VCK+pstu7Kd^hdjWh6QlAXlKn>uODhuCDYx9Bv! zR32v?>Tv^p?d|W|2Rin;$=4F#wjkZgrf_fF?SW3wzC5GP zAGjiuF#?FLA7BZeM9r8<$E4irf+Y3xl|9Ejn?49>kX*%ICx-~}C!HkAS+)sSti9B( zAtf9r;1+pts$n|L0-TL$0_o)o$-DQi6gZcrLFWO!pAKNpgb)i)<^}3| zjU0<5l6KzZi(sHR(B9oZs3*0FSz;#**}oSrj)HE6s=eDo(hU1KqIu4=wNW zw!=t1zO(6nPBecy%W-5AwQY;JvT)?X$z=l<+Lts=mqB$bA84)Y8lZMu5&pY-wkjvW zpnfl#y*-vkN0{H>#yf)9?zPn6lol6oyuUx(Q(yB^}ju1kN*u0S) z-kPo^AcETZZm-}pGv3Te@O!QjdL@-$c-OBr1d;B(xe)CEzt+PfL@x4H?kXH4I5}Ru zY9+b(!f}RSQCv!>mzgXYiRqeHm&99MlAPI*0Qy+MM#dNMEAQZ(}n*w#Sw0qT* zxj`J|74V@tIIIX?emn8iGmY0f;SZGs@rhbNS8B@yITm&|4JoV6N0RuzEC76^N#`LI z^vg{RctoYde9j!_q<644a{PwQ2SFt#g|pt(ag!F+rFi>^d7zvO6{c2P`VUfG;V55m z{LTm{RG?4HztH%T#g}po8;0Ilwk9?|UoU!+w)axOuKCeQufG%{54v5mp~P})HDr@F zG878f*-e-Wtyo>_e{slKET}jM#UFS5e6lBJT!OwwfqafV$#aKC$s}T<&8VGAV_N8g zqt|Cq%GX^<#7hjI@8}@5&GRAVTe_!4gXPG&{=7;JEQ5#1g3yZbW@4yhr`>Ps_6^B3 zC-IJJlFYC?`{9#$*PMOc?K`06SE0&%HmpgsYp)J~tZ-en0ZPGn7#C!1iFfA}j5vL6 z_o-ODm~~hkG<9zBNzPZ0w>;X-nliar6CvNNuBe{sHS|BxJhhl z>I3{g_P+cd%D3&mlC&XZD_bQYA}KplsZ>IJl$}WuvhUk0Ns_&ykY#8g+4p6bk!`Y0 z*1=$y!O++S!z^ZeukQP~@8|y3=Xw5u=eKLF*UX&PbuPzw9`ECQypQ!Tvh#?uFHe9H z=CWW+Y51{-g+W;pzTt+rleBS_Hhu40b@l_Y>i(|Iwkxz}rIYTIs1380xWnFihD6;{ zMAjyI9tyg9Pi(wbnLVp=djAYpD|PM&Z>xJcls8ojf>G-WIf{*LzMG7YvcmO{K1 zPcJrX_Z2g5MHb1sj@*?w7F@n?9S&iiAVde8v4n}MHix))(SC!3D>c8+O{E%>A zlcucC7m~dEE}$gEYLg(U5G~PHw7S$l^C(UosV>uJ7fqrRMh>I0rlhhW9%gU09#9_J zJs*1JScKnYUBNP>URG1Ej-uJU!nHY#7Jg;agY4CWd%;?xS#n)X=K{1rf(NGM9+M2j zdfnpM8=3dTAxn0m?A5hokj70=jI#ryNli^~mfEBuFPBM*FgZyyj`n8cU6@(29+0q5 zmfDi@jql~#Y_gI$n=q}~os<9R7&nouIc&yZaNi}6+jc6wau8HB=AXWMbXsH3V*Dar z5bZhs&cHqEkJ#4BODT@oGUgH#boFzW(*hO`(!v#2&m^;`I*1X&hSk4Dm zD7e$`nrJKg+KA?8tc{|LRG>E415{#h>lA4Xyw2LfIVctklx0dS;3$&X#_iRD@jTFt zxB>4D2vyxe)~B$PA?!I<>CN0q$RYIb+6~(HPI=rZJeIeXS_;`N?>YSrV;hN5xJB^h z)BNln@z#u=_1rplV8NvjG-PT?pzXODa-2eT&bnlEG*;I5v)&Jh?;qE~N_0L91`%U7 z*7;iOPwP_Ktd{S!+Si|23(=w?xt$B(6>7`{jT%~O@vivqg?*Q+(=g_b!CE$_d5K8k1|!iY?6OJG+R!1$3yO?V)Grlkd8aMTE=~Kwc}h&S>EvQh%r}=Z)jtF zg_iLmyyPB7rO{@Wm!lK3{ehicxufI`d z2Z9Q|RWnIwzoO#U`?xsXlqEGi!$I^w+OPUN**k#s^UlOi-m#?@y(d+xLQuWUHbS6fx{+G2ZoBg-(0per zZpXNwh)?4l2OIMw=$5P4Ix_f^U-X0YGfJmb;p?Q%txLJl2;YdyZvJjkE`(5WevKwy z_bY3mV$X(qZ-4g>b_&eNWnb6J=}nb`h#|;PrxIf|clwa)eR#*d zB(jDs&phM$YXNC9Ku#Na>z25VGX2ezB__#iZyZStJQB!02q5f?*0*HS8>6N}-fm|z zAZ~i20U&k-nMn!S3*5KaPtUU^gxDMEGP8~BOsr%{rn5R9F1x3(T%a^^#VnRK8Z4i&-djl^@qlkT%9y!m~{Wp?7S61|uu3g!kd3|^N1Hr&r zOPNL)Cd58Z&!GLy&QmODF8j-0<4xPf;yL~Ieau)&(4WHX>9u>N=b1_D5g>tNUIg9R z0YtETbU5P^{w^fF2R$McZ;+=9mHh2w(yDN?SAY}(#62=MA+4EU)A{>(h!dve!k_GQqrT)2P#J%=_Ax|Yigr9Ls!5_{`ES- zY@czp1dWPo>J8_{a9#YTwJ6j6i+JV_F~e%vI~fM?mh%_Ae?rbk{22!t-sc=iD*^2^ zX{PzgimCVA4Y~PoLYo{T&Dg~W$xv2u=g6Vc#%k02+{sY0qyi!SF(57a{C-I6z-CYOC+jF;*50v5b`_} zcIaNokm*W=^c|5yUzyjeWIOlJ%1UE|W?r47jCi1F;P@TRv{8*O$t4qOeG9)YX%$W% z;%WGFZivqory!F|6mj^XTT^i$G@o6*^3sYgV6#rK&Q76;Hpyqp{OqSNaAV9wKIoH{ zi4Xp#YQstB_|2oQ#gIgFi?>8IDI~_vm$|(96}K4thD+5sSd*t^qh2m=oVg9A_etZH zagJ;UDj-3Cp{c?{wh4?X?`SYBM`ouUHzU|keIU9<1jF_uXhwd^~qe30NbKswGYW2 z<)h!E8dw*%W<#x<;={#Rt2dZ}U6B+A9lP=!?zrABX-)un2uQml|BVgudr_GgmVJEk zvk-wBBTxO{F zPPKwA2(iFawj9)DK0k#VLX*2xe%_WKXq7ShGcSdzdmHxOFjyV=E+szk=#wDp@msfJ z_j`7O^+^L{L$R#5kJ%Pg&6O16Y7x`=_xb=7 z&nPMu4Ah}~rc7w0U+wxzuuW|J28m6heNF!U3wHFdnbtut-ThM)_ckbH?4Bm_vm?4FuIlE4U71 zya>s0!B)pN+nh*6cfwF9(quFA#wSjN?72%w9Z}%|q;v{aM)fGAXk^oX7DnWo9!1r% zGUyxju^n8&KQ6yvn7)=QS&Dz4&~=@lSjJb72GgRlhVaw)?q*cNU+HuCNV~79;38$r zO=_uKOkz>0@}hiuGUapn8=9K;UpljI*KX_tbUY+5Lq9N}QJ;qTgf8%(UhgF*3(8T^ zUAycW9Z!@CI*)!X?|R;AbL(X#kAkfQZ`0#4tECb>0Y}X%A81C1Yad>OX{;wGs1B{ag?8%uFPNP>)1PNfwqYSG zA$Z!y)2$boOdjDM1>to{6xt6@1#8d3U*A@HWAp?Sd?#bx_&7mkOkD4H(tW?VxSrBp$ z8q;Eh&W^ikax#%+r0RvIkH_2GpFrn=7!M&FOAUjwpXtiALGA}}(u+GbY+|@65=Qp< zx0_Ate4;9|gytEnFWsMt6oj`uNY_NWXvJyb(dhT)NDuNYzKSn2OmlUmFQ2>klS@^U zBDSf*PNL(R2>p?7OQr*XmRq?a0s$*)C_yxdguiK++#hubm7gItIxb+Sn{yY`AWFec z#bEcwq*mlkb46>Yy(5yb_v<8ICLW%P;X#P2w?F#$98t&Hax>U#JQ$@G4Q09zkK@*# zuCbz8$Lc4hWs7AC6TIb)Dq(JNaly2tzMyDLr~k2$At@3sk3felqA~t~IBg=-n4xN1 zm-O%AdjGz7x%;sYw}7bLF1xP}PVA)ZNm91a;@!Y&zSFP)xzv4`0L)C*V-g3SV%I63 zv(9h18M7y(6@-~eD|N=y>fMz1UbZx<%!CMK9OiM5R!1mkUM_Db&3|Nvaf9}O9HY$K zZlzPHpZSM-m1W4p+sDqlsZFN&7FqnVFa_?mJXN%O6CQ^rM`A9ar0W?H?PLjc9J9XN zqU}bVuHkxao2*aQt>UO~YSHqA$1p8lOOeMv#6AzM0i=i0WY$~{oK@LdI2sCXF8waUe~jxi^|5e$B*#&ko$ixBCPZg&&Bksq*` zlw^rn6&*XZ&sBQJjaLCt_t@E&oM+;S=c=+>IYrDB0!K%yaP= zD{+t{qnpL4y~RD7lpu6}UiEzdVrU`G`$NIn4e%vPUWrbT^R0`potBa}`I*kpT6Kr} zkm@f)uct6AZ;H-OT3Pw~bS-ew}Ginyx6WCZMVW>2A*?LO`bFZ_{@R-?3)S1sp z`p^!y2N^&&P>|boUbl3UKNh3o7n$LdVnfv*_WK^|a9p7Y7v~U~LFv?DcnFo|KT|-A z<7)Pi(gbDF`VV}f@E4)!#ehGhxtksgD3?U1*7%wY#Gc0n_K|X*ORe7xv}!iNWG0Vm zXxcHG+Sl5%D7iopq+=m#FRK=FYpl%GjREs=1S*>BAvqJnXT0dkB{XV!y1eUcJ+LW> z$!dzaHUxk^c%VS-O60qRIAUzcM7k#im7wf@!Z$tS&c7;g|J#0u`s~tb4*4#%<1G60 zq|7nGL5wX*Nu^Chu}nT3 zGa59b4VG4GLwYm)45AaC@lE$qU*is8&VswlmE%*TMo$weOBf+p{pujme9|Y1RwloU zdt3e^30c6f2A72{)m!Z+I{^rU_+$Kd%J!J{5qd}OIGV^ zjf2YR4^PzH+7(aXUwYXpZsjgg*>6@?1jk7X7&LIHwZg}EU2v_t8mP+}0osCQufh#C zmsK+#sDp~oEjJIehjOv&rmb1Z+H&o#KhOrts~-S>KX{#z&|O{CTsNNp$Ptl!7HHK> z0B^3lh2o_9k?s_KSBoz?y=N!vSW5Rx?V^5OfWQeP9z)JC#Hn>7d1CHs)rPCqdgkq> zLj2iti1QJfSI#JBnd^yCPeM&vyj%3eN)oh0JGGJ8jNxSAS?#J3lV}X>N>fg|^k|v? z+GL$?x*&q7X?*N~uMcQFTf*s>W_s;g1z_^3X3j=>Dt6 zkF5OlLd5uCn2g;AwVm@b@2nn6r};|!58Fno9Gi7v4Jv|aBN);z~oc|t{P4z2Wd zV05DTdz;%mG4F^7P~UA!LM>~H#)7}K;IHi`t%MPocA za_s_<9AYwsy!R8HR!pGbX*lmEpr0nEc5TE~OEu1RIUou1ORAm|?lzkfGwWGto~F@Q zaFSd=fIDO73i&IC=cFiK)hNtMO*c#)#fZOEX>>%k5Fb5Q5`}|O9MjuO#Jdog-bJ{r zPa3p3ZSd2Bnm2m=grWyi;986Qyx1CL_-y@w0UuY$WwIe2GxJhtP0*H`;7lWxL(yj%K`H z;Ou$0GUDL-0d84r8pCTmN&ctc^0{KoZz4s#bIIr9n2z;d79o7grdV}trJgRSfTz_E zYrZ(92EEFA52Fu3p>mF&KDJDlWZGuip{?hjCj}wL_8oW`E7WzV2E0xGgR_Y9;As*S;lh%&9F}stX>E z(>Je@Y|A^P85`K3Y1|QsONH;(cxAwRC~+~tS^H#lX3*YNjU;&pXM^Ow44wV-Hz2dX z4URwQGz&HrewNv;Lnt!^QqfA*P1 zSE}nusaDT-j;6l#Ei5|j{w&e$tu)ln5$UlxqxXY+z-(NDL!fd3SX-D?B=d3?3*ps9(60L)^z4_sULey^deCMk;vSsj(?O$&X4F* zKgAmKgL##mC0@$z2`@!@e=*e*S26C(^A&yRU~k(zWQ2}2y&2Q&`tyESyeTh#z(;`I zJgMFQzzgO#(@gBTO=Ndr%Iq9nzs_qFi{NMnZc#_K?o=PqPyXHU0f_Jw%^|~x9ffeg zT`2Q-?S0x}ln)ZO_2H`Wzl<>Ydh-$cb%0GE=}}}mXsZUaG?%0JehzSbqMCK# zw(|jU1zwRdY}W03@X#{lae5iY4E+zoa=liz6zS2HYK7{GgaO@=Ktf%wPw?A5y$3vD zuzDsqUtM6|ifQB6YCK_mk{Ud%^BMj%=>GWP`eh+ZyNKD9v9QWwU^&$=^m1JRx2QV$glNM4zfpP7YPP~Y-5Yx?OKCD|<0>Ft(7`k~lVVz~;JO`NKT5+fHrAd679nk6 zXcz^Z;HeI_1uO|y;KhkM3KPt@ZvNX$=L>l_hd9Lg}uv1W9-&ix=j_+Ka8i;@N!g0l1I<_ z9=7b}yL?z(7D;Xnw?(Exoo_ zWR5!$wPX48=kcwSUsAzXx zSf5fVmTh&CwJj{rnT-$CYAO3yO9N+c&X-~r!bdrq}WBo1d#cZHY%$)nzG*ew$-U2{`D3T94eIy zPr%KdZ(uxJM8T4~vfs{kL^R+Ot;U6E*JKc34rYDZlyB-Ca@ zP9I;>@uQz_e&X?5h`;6NWqz1%`ZWHYsZQ6R=1lD;Z8Py60Hp1wO*P+4qqMiWj?=2)m8^S%K+${)$dh8UL-vY zu;8DM!7|aXH;)ULUb&S^c$Pw;0k4b!h{YbuIE-g$q|8?M=I3ghJUuAYI7 z$KwW;CKBE#2l`c0vSESjR?mEbkBrlb<3ynz5Hq!Izt<3D@-#5<4OnpOXcuLx{*%~* zUgeBwQs8tBPg^vi_XSCMaqsE8FpZM9#P2+3$u8zIyo{`i-VaP?>`~slx}gS`P#e$q z=AfH@=>pE}h)LaS$kA%ghLU#J9K1j$We4f1OKCr|#-pteggIxQnMA8|hb7S-i^+Zc zlhQGF&{db~l_0zh=be1EOn4$H6{xg5J*%u|+v z_Y?oF>|skoL?k~pf5-r#5>s$dr;4>UJl&{K88GlN*T`1P5)5m;(hPISyZ7}x@Y8p%aPgN8!vaqj*e!9~&9uRPN`z57DeHl48{ z^S+HH>EBDIRP8=GyF19k8YUKad?ipc|6<;_zX1|sfr&x6d?-x*RHH=nwE&6f^N~hL z8R)TWd@gu@U8{229hR46L4n_!{l#|Q*O_|(%dpr3o;wHnc{k+wU!L+2Ylp>jaZXm( zLhM>Wg+P>uiml^Y^M)2x-(Y61YbeU{t~%Dk*uX(c)CfJXQ`doUp7l=IKnC$-_o}Ix z$CtAwmNkqIbcD>;vHe=Gx5-@3cgeYZQGXys8i7Q2k_pjYUcPQ)lhHt_@ zAkDCeo4t$H+v}GJHIESok(yI=L)qZ?UMIx!o}hK6+eNkBy(`AE$PV%cX=;6nQX54? z_#Sr3C-z!Az)qTvNyaF;tv-kRNa3lQkC;NeQ|xM+RgkLbLGaUC#KC$lr`7h@qJKh* z*XH_NiH-W|3BThYUm)c4>(pHN-**Gi)4N9Pb#Dh+yJjfuuK8H>-6JK(Mt~yZn5(3Q z&-S=H|L#Y%Zg7C*7L)I?U9wyuCktNvk#j@4D28B$2$1`F2_v(bBl+3Yhs$0p((br5 zJA~bnJ=0nr?M4ck->Wz7;l+;dq=MIDz}Og^B=_<~LI8WI*wzH;G3JVmfyu|fmw8t| zU#B2GzA-V{RiBo$F_mEWjn5{QccG{Hd zT$_X#oEL(fp>q#S3%u0;y;8=RwuYO`Ube*86V|oBqBQazMGSI6Jh!7cCC7EuaiKm_ z5CKRf@Q|e90LiXj?#Qwx|Ct-VM~4By?SB}86*S<0T({M@Y_!Wij`Kp|xtzm5S-xeg zAv8xY7CCAsdFG=elCG?pDH8oUU4n0Dc#gDBW`s`5+_{V(&?Jfj^Zak|_=FZ)DZ z2<7Ly#;&D`x>RngSMq;?(;pnd_@;M+jeMdoec!Hq@f(pCg@YIrPA}kf_Hx+D~bQxi<>KzaIyL zP^!{o$NfpuLHG53eAacWT3(s0OVjz_)1%Iiaq94pTvz@d&4J(X#=jyA2h{JNyZ|*R zPshT3=9ff8wRL{&>Pn)(2N~av(FsF&5*2HcnvD78j0RuNgW{^z$r~$Eus!}d224uF zC>+f{i@Vhph8{AI$R4hVSM|pq?M|*8VB~GDpK6r#dEh^PVzNAvKaQr-!}Cenb-W1F zVz1a?l?wzx^a$PuLn@0HSsp?kfn;XNus<4^BhG`GN53g?luUwp-!9I40AiGBh~`7m z_7{;lA)DQ_;p0O$+>i4!#oI5Den0M2I}+pIi~ z`4;S!=~fuTm$+C*(RmOa38+qldtxEc~EF@d3TP=PUUlDuMDG_$_HI< zzgxW~p_6>)O+3D4Pp8E2yaA|xwaanVoaN*pqZ}_dAACagLUJLjJvkWZ5nN1bsOV>g z69gL{Ci#E-eow^juIm}iq`eag-gEKitz>3G*fPdA)7k)S=26Wma>PSjzm(k0H|A9Q z#vEE=aIpXNQ(ej;SD(%~no}5s+#f?h<;vq*c=9|mVYhR5D%z#xV})c{Q(S4p&-^l= zbW7fh)5h33m1V;IR<|yoZtUZ?tCxD~Ts!$cwMJ3TA`U;vGRN~9pWe=({{vx2zDC^% zKlX2k?Y}O8j^ZCd8&?$xzdnZAOmi}Fi4n>LQ@3Wr$j zD0w8eCGv%&#Hm!aAL*0pS5|J`Fh5-$eDJRPY!A`>7u>iU!b7|oDrQK%Zl!$SM@4QI zWxhb2Y&v6T`+X-%R7y=a*9oZQI;KTk=uG^hx;Ze(uRk#J2KpdnVzM`ThqH)uyfYI^x&m8v)zm~1DN2RdV+TMRITSKu$;vVfJ z#dGF{k0{KK%Nm(#+Rt7epqziX(7kqaIeyYjNTL^E2dra#2xEMxuj57_<=t2L^?eE0xVl7+r@CNvZ%V!c~c^5T4? z_EP}R3w%HhJTJZyuKdoqS>i|1M#K?}Z)xDE(BU@CB5m@wtBF@p&aozz7!~Vj)A^BW zw4t~}(tS$NBH-3E4{?q6)CIc}`0rMydA@!2=PR!NO5X%#gVzBH$Z;Mq)tbXG!4PJ}k&_dV$slV|=H zemx&=u`@?KY~*p;#V$3xn`T#%FjUww=-bK`FOC1DN{BA{i1yFs~JN}*z;5{hMW ztn2*w=CJ2G3#Nd?2uhr~OW!8}@+(TBiQdsgr}@g6$s;B@njiRo-iU7O4{OX(hF+$R z>?AyS_YC_zBXIo4#@Obh*W@>}%WTf{Ql}U!W~2N7j;hno38h2I7Kz^lv>=T+1+u z#aB`$*fV3W`2Yg_t%k-DtS#zR3%;w`Fm~SXMQ0DFcZfA=?%CI6M2S%~VSaA449e!- zrD4m$nl`hlLOU;|crLF7r?KdfRT3$9_griW|1fSduNdB5i{Tq~y|gKq{_4y+Wq@Wjo^dJ50a**cV zjmgo=021i5+p#s}#O@v)^72$2#dANUpz?`@|68HvG`r2{k_oN+h&XU<_ZO-sYNo1W z;7ttocbCD95t4$pf%jgq2BLd^$7hD6TW>^jJ4Rf5Hp6LY<#RC{1ZbwT_Yc5vBL#!; za|B8MhZYW%Z=2PuJ&(fH`@!m8iwVS;fQE53&%L3Go8^y}D$~$i0NG}Exv3M(Y|`2M zU;*%0`1He3fSI389ux?(Kkn7~7WV=RrKSbvfk!#bQ~JwL>mZ!=gdiV_w!B6k z@XHJ$My(Y1tEU|^9r@i{14g^?wlrX|Zv%ze``_>2Jm-7@WjcCz>$JWV$rLmTnO!Y) zZV1uXZZb4DSlyZiI!7EEFjlM@J~{Qy+5=&=W{2b!P7ycOUixv4P`~!v6SnzuB~c%a z&sT5Q!_Q#|Oc~ceqSTE(mW-Ats@(|TGmLJ$bA;YWP%3#w)Ax3|AQoT^pHh{2 ziTKQL-)j(BPs>1bAyfrx2@(g!e0ww_7jyJBmqj81;g2w0%MF{6!+M^VK^rlCOSKh;K9d6X@ zlpD>m$IuRBA~Pi{!yON#%HC0BeSeKT10xtNl+v`y$dqRA6h<)mW}I#e>X$2HQILwCjRY-n0Fj9kO& zo!Mf?5*$IEp=-iZM6{#>wU~u-nKCKe!HyMr*S#j~U-I!->l4AWjfw3aB{+V5mwTnx zTdpZt;JL}-5_XK$xTZ4-pyr`)lE%*$Hr`u%+}iMWhU(iLVUX4$=oLlzJ6Q6bJ_q#hGYowjG{SKel99)#<#m8TrVK+0X9;PdzYb`)|B1KV~E(R^k8 z+79jse^Sqr3RNrzxv0}pL>WhOMoPN=GJKBau$#PA^H5@9pt_f}BMxC?Or*HSssdvB zM&c-o#2BJqu5d8XxCo&`J^4KoBFG1nndbB!JDvzJ&$`y0{0T-UV*hpn!%UmFt4%|7 zBMFpo#5i90f?y~5$boT_ zzc~>I-&(S0n^nJoa23o-ptEmIi4n$dzp6dOBX7MLXa=R6t^0muYQ4ON7hg|SH5-F% zV^D%ap;HuMg6@QFR`GAN)PHxlak`r}Qt}Vn%5r1rwpe zp_8a7ar}{Sb}Q*#qp9%EPo%+Xx0H8$+zV}z59fX?I8{msjVxXg;A0Q=J`U}0fmdaW z%M~p%7o}dXQE1a)&7}!Qx7l(vgTFqMvSJb2fDgnI1CP{@iMf#+rG-NYr-2>*?b%s8 z$(VApyjwUv+Q|jPp&yy5KfrN8_9Hga{o`0fs&i84Qmyh9r7S;fQZ*8*a|~8Om8KSpKL}q& z`}XL9Anv$^W+8K1*EJe`P2C2o#1C4fhgkf`2_u+TwA4Gg3s4A{5;1r_sd1@AWVtq1!oxvBnmGqbe~6Uzez$qvP+7o^wkm>O`k82=tF0q` zfNt8i1fTro<*(y;thG$Neq*LeT5tn2ynV>m_Yza3%>)vJUzrF*jkFna`bSRFPVYK- zPHX4Z$HA&)!URW)3*oyZCl5O$Y;ZsVHpjmpg?$%~qb6MNY36@h#IDD^kdj=Fd1^bD z=C-vJH1Oj@fcPD=zy7S*0-P$Y0DZ@l_bX5OIk$U)^;&sW%2wRgcYUN`L;DnjsE=Uc^oIqEq=3voteO?szz?2g`q3m`Tazy1Mom!s83GJNY>sJi8K?q=FOr6EU9$u1i z(oYy#34cTXR%y~kWGutk%_~jG=!?sJZ3*Sy)B`*M@kZ2_hbweR0;jYWS5)wh4Mf%C z@=|3eJM^?csDl6p#gh8|1g%M=uP{s+l!ZSL4rf;O?cZqj)PYT=&`ItIFmmro6S+_d zey5kTz>=2Wc{7zKz02WHicauE5NG`q>o!UGGT&IJh&JEc_5^Fh>j}`)yiNpCIp&4* z6)(ZPb_m8WvN;#Dp``tuT&7Tz9o>#@lk#f;$JWpL7av6H{%m<{{5Ry{ljrX41=1U? z)*k6_Xgbu$ua@c6VOfQwihvkJGnIxqX5qY`zL!(bbfr0pMuyxwA@`Nh*LG7s|Rih|^ zG;sKP51n2480%kp4CEHFB}1SyRVJO&8>2#~?mg8?}c zY!`VPk?`8y%E8Ltb2ot_SeM+cGZt96zUZ~2lPhuKo51R~d8mT7gl-_Vd#6FxI{Lz( zNAwS5&qH@~+7i@;ctJHaZi>SXbEN}-V)O3RQWjyG1E3a(F)5hSut%Eu3kO&^S}5ku zRY{ZP)Zt;r+LbE@o+rk0qEZ)X`U4j+d}vx6He#GqdR)#X`-}e~$b@$pzjStF@f!%# zM0A)`puT*KdhtQ12g%{62UbXhT)lkEDGMV*tsnF^A{-w5Y1?V>yibz+{toj{ech;d;B4(z&xZF=H|6|5Vcb=k-%m?wG7qr#AIosrUGgzx zMku*DBYN0-3^$wNuu*d)fZ3r`rze)k0=g zx??__?jIFg%z%UU3jqrq7}*X;TvS!e-1U2qy7B5S^57B1l3evZ!yRNB2c8XQUb0?7 zMJP31dzo)%vCEhwjQ%=I;@#e`T`aNcv95s71haw{O8Zx&g{D-BflcHO)aRdf`eUB% zAL@@O*oto4eR3r_XaLi&se5Q+n1VSXr||jY|ML_fCqD8Xc-$k*4LQxa^7 zDVFmo{Uz zXTqC5V}AeLXJJz(00((x%(d{|-vA^3ex=hFeu0zgi+6qg*Cus-0lJx?g>V0ivHrJn z-uV1ilGk&T?ybLtOaC=eFCu{vZ}u&&{Of-k@suAxlbJuN4cdQSI{!Dx{(F@FZ<76M zR{eWM|MTMipJXZayC0`IzHo znZBc&-U}VFbmH|KTNSd8n0c3(qmZ(9)BC-W^WF^i^=D3zAK{f?jfE`ToFEz?&nRE- zo2GstU^xg+X#(&=Lshl2%2i{K#9yGmqh!2WgX8c_K4QPr_K&JywGaZB?K z^av9)VgCUrk^@^S*KNMcTGE;l(LQ_N@(K+=IM3gAg>56N_?9O&j7%N1B|mPHoc>F83w;UJp9Z1M>9yq4;O|q-c)sTVE0-# zEWK4-m+F{U>rs!a9I}zg4epiM=KBYDw)_k}Tbegt?}RUG5-x^Wsx`1Tw42XXPYCpA zZ^PHpB#f`+BCA}!g$|9ZZ{NDzTKFpJ4^GHJE>dDgDFQ70#3spU7$=k0L7gzJbWnn{}9MAnz zN@^AV6s=)Fvs8EyOitAv+inc6dMCBSs9afTPIzJBhAy}L*a-chhkSux6ThDeMg^0= z>h$>OWykl0NV z-YFOGZd2Lc=gDXe5H{YFjp$vH7(#zPd0@0?o$OX(7Bv0ZpgD~|rLL57p@cY4#-M$x zlcI>QwVyqU@Wvq?bWuEL#dDQOSQ+hZ8ly@{OTxFJytdk%p8s<9OD=8kvF16*NOXE= zn8f%6>w2f$3@XmcsUA^`hxDJb9+QW$BmyI_3pL1g&BqAc1^Ng-fD7AoFD*lpl}n=)i$LQaVU``6nWyQ?_Dsu zwX<+;b?#$AghTwcDV=&El7I4wcTVxEI|@V%KEV`@7tzH&#=PM|ma`XbeDj6!@)p~x zyHdTS=`LI#JR%FcH9P$D!?3q(rX(M|y||15MOT~tf{~jrAf&J7>wQT1Ku$+)( zhhWQ{M|F&M_~u+xNxqP}9(BsSc`K1zUI|_WJRV~t=eQ2=BqQ+Z@TRU!H0_$g7YN-{ zn;R%f1A)j0H<`s;s|kcnuGM~tB&i3`7kHW^L)pXoAFt^CrWmw8~qck45ueG5l<_xH*P5*dvCqwszC#dUxELhzbR9)1%esLByPT;?I1 zMM7Lbp%a-66Yn={pM8++i2#20sl*eqq@1w1?y3#ziY~*W;GqguA4Kb?e24ZC@Z?dE+n!WaG2~X@0(YUkF*ACIRMB(jXQn7psGA$1f;w0Qj}yn)t2V553S8?@mlm( ze_yOQo1!Z`OD^ZQ+A;c8Hc~okw`=6Yp$k&(wn0lP64lMdq0fUIzJ)SUIwPnr-z?Ql zB`$(`H%Cj)^}sI#4|amx7@jny`6$#P__I(`(}A;x;&o;pSs%fvU96|n#Dd+uf5#12uW%Epp4y7 z$>L{g-QdmlM$O9!+JjptpZ>UlLr}A=NG|c-_4jnPA5jW>FmT*IL8wdJB9J#!xbYZT z`{{>`OqPT=+ElolMQsn_M~kBmP{(7X-B-l7 zQ`$cvXRTe~@Max<*rSWPci&mK`b`*#MXkJD9!6||Rwl#EYC>Ig$~T%~wuSVl4BIBp zcpW`qKRZdgvR+@YGjg1gS1U7~3r7!t)U8ecW((7xnl|w=AgnmKMcOPS~9Ev_REED;G7yl%`z&YZ~f=@xrZRAoZJZQZXQ}})N zcyoe=-k2{IIyGe=xsQjv0D#CkZLruyAReZnv(l(Pm5*Ey3Sjkc?BfQ_s+zdjfils9 ztd?DjGDBq-HQ!d^&<(vqN8BbPut>*HAe{FIxu+DFSg5Qn2tgt7eD--3&J37h8+#=S z{GQ2&;5-!?cRrvwTIa2}E81eRN6kc0@u4`kz90b*7im@Z(v%lw4BxraveIIyobY zE{RcT>FKSDYZGoQ=pBx0+pV&aPL$etr=TsqQyK;@TbwH-@ms^olN)}dqz84RR{80m zhmB$~x=Ai_KkWjWjdDU(5k32ITST@GbG81IKyQq%;Lu>vfVhaq7a)ANgzX3Ce+3cq zi)!^IQ*8qa2bxS3gV9hEO!b6N@Uxc z_y4i?pJ7d9-5)TlfP$k4=m^qLR8*RX^lAg?0!ptUy+nE^A`U8DkzSRiL$85Q0wSVx z5+D#tlomn=0YXcEj&uL#9-W!@+xvWZuKQcAT>I>^);epi@>^@~O_zpk@%6K!_x5HP zkMV8Kr1rlFKiotf+}|8HDn-rkjT$8?6pH`LaVl6%pom?@JXTh&H+_URUS^UOpBEOs zjswH#P)Yg5ycC{Z+un=6sBVvRH=W#Cstx%~Fwkq?KlIlVlXvgzPbjFn%A4Cp3O=i! zk>Pf)iRZ_M@>h8tVqSdw>Fg&Un)E@=LhJ>jy!L^zeB)betr+!E;eiC(fXPeGm^kC~ zy>OL+!EC)6_qAUhIdoh=`pJijar$lr`x`1w(bp!WKA(Ev%Z+bRS#QXj3>po+>3g(i z_56HQr*y=Q$~XHATe;sks3eVF*1n_kHAWI0^A<7A3rAbWa1$BRWZyE8c}L{gUo_nu zV)HhvVVmQdcVdKHc@#YyiUVCsxRft;L|$IMPGYcDUVzJw63nyJj8y8ozcj~MU_xbOMp&=@_KDzGOf5Li2U zG~avpMBKQWg?}^5s8z6AMkTm+K6Li(q@6N%Adl_CH(`_H9Vkxc*ZECS1h89!m3mWX zyMsn!?w$*e=t&Zv5?R-R?LzsrXz?j1dx(aK&B{0Rul{4+`>f2@evPPxE<+#f8;d=a zR6w6wJHc2zcsqm)Ydi&T%@VlvhuMRu3*o|h4N+B9ZDfsujCY0q#L=$M*pAV8X13$c51)M9 z*}fO*e8{!rZS!4-&ZfqeC3V_6Sva=(lg7(A<@M4}M|*Dk`p~!{oZ zTgq*+ooA2DiqY}8qWW{v7t&b{>KBG!@T_;MjEsDG^35pUd;5#pbG&oZSo7Z)AcFeW z#oznX4;0+HBlemz>{Rg`Y2mXE4!ykaJfPSd+rD>PmmmT!(E;w|dcuC`>_tKMv+cjV z;=Dr}3V$_~C()WSdoDF^O&vq2Q}M^7`xmIF(Dw^u2{ibt_%zOy-*KRL18eUz{?hxy za|v4qxmbFU(>YDt!>!mj)BQHTya$H^tUok=vF%;a8hD_-P#j~*qQfd27>2bBztNZO z1#6wicDDC!6c<+@F1#pXAO_%7kc+wv18T^WZ4^IfX{tOCee83VJ?a|LbZ(0km(dwE z)4Z8F5Rq>M>~6*{yY11xfw1XRe3JXD-H>c(kz_usWOEp-Cii8%Xy(f*1M&d6`$?s* zuqO;n3cGYk=8JBn>I>dE1JJkUC&aElkB#kp`R&c>*%UGlJ$QwkAW-7&S%f|!f6C^&GC ziG__*MDy|YAEo-54r8Orumj+=Ret(6$Dy#xAcJbd-WII5&7|n3TT~HyuW@+ zwAI*F0b)rt`Q`3w8B{GNlylc)P}5#cv;GtUS%>T#f4%dVPM`Q5DAjn(I} z_-Ni;G>2Cqbf9P68`P}$MBYwuh`bkx~mzb;h(bP+gxm-=tQ8+?|j~0G&-|Tvz^`#8c=lqMe-;q!_ zP#Q?gD;su@Ge%tn!`nArdDU9}@UIU_{a#PIRrb_AC_JZIf2=>|*5=&vtWWV6=!S8J zn_uOYLVI7F{bgCtqK~%%5qv=gUR1G;G!+(?nEp+xF;nXfT?EO z^%ytLI1?9{Fr;uKfjn4V360>Bp{AO|L&MOuwliy+&tro69Vu!h$NgXLh3;yklJA2M z@P+;Kn*&usiiP%2r3~IU?j`?uS!I{&Ku-uUIxxIFM~E?BKvxxv>Z~Gt$`V^r9tHQ8 zq-vFd7EfHMKgMUF`|ehz5IW_#6+FW()HKUmEo^(mOS!JisB{@6hu2e0#tGpkR0ul4 z7^_v|9I)=RXtJ7dhj9aKg1_vdalMxm$H|hUB$XZN_Of+j%R&6>-SMEml!lFSMW2ZY zHxwXOwqf&jXzh5*!%uu9PkjAS=hY{f@ARl?<7St4;-x852F}AOOAbqTfl2z+XA*$> z4T~ea-&A#xThHltxy2hNUIjhhqmP%Aw^XR!>;2Wyv7}Mq&ZKwmAR-M(KkL0e3rgo3 zO#9ka2m39hBJ?ji%w z1{CfyeH&b&i1eH)^TWTAx3eyJXSm+eY8GBH`rXY zrLto|Tr-UmrMFM>5L!aew@&lek0$qa`)?rUQtFhZwTTjA6KijMyc;I;tyCCQyH>A% z36tiqyFaHljsedm2A39b(2P-GM0VY&oaPsfRNIe~YaZ8o*Vc3^S6ltmmiJp7K)-;lC+Y-Gx+a(ij_HiQo$;r3(CBUIx`!wqjdh)sB6QWNucPD5?gNI zeIj${m5uWABZ=AAP5iUfAlE4SVYPreq=*+L%HOyO{C#gC)2((w&8truemhlQl;-)! zzv`4=^p0N3Iqjw8wDIYW3zs&l=CsGwllUt?C19ItA2cYMjg41DB;Ed!{7xZ?L=sC! zIvx0$K(*wnY)4jkjV)>~wV#Z4dhAqbb7Cdt}jP&F%q#IF!i2Uh`SKP{vFs^H? z4t;_Sd(Sk?mhbc{h^s8uwD+0!IR*JNWQK;!T*743DC7&LyN*^i^=&!t`l!M*SWlHL z=5TAp^Sbd@Q7W&Bs^n{@oU9-Dz29ZvB`77>xC$?za5{ne`i%%0EjDNUvck8^M~TUq zUru*{lz`gBD` zh5QsDC7~c0?<|8*xlyV(%y#+gLn|3%$gB${MvqN!$Bknqyw^`khH_=q2OK(nO7}}r z^Qc|JyRNX%<6DHR5dW6CD~x7(nT8ZCDQjgkN#gJUR;s# ziQ;j5WL>o&iOikeh`N;bS>u7r!ce%NrHi-L9bQ7YTUtt8%k`(<4Ky-aXDJfeOYfB% zvyv8k|1EirUq4fZ+SNFXDGJEhhbV4xGzs?FI=nm&pAL0+D{4LFmNfigwWb!T!A1Rq z!@C=+U-&TESApI2;;)BkFY>Q9a{Fk_ZjX|t*~aCPi;{Lzrd-9lg8ipI4XP~AgwJ2F z^4ye6Q$+8#VxP;OpEIEF#^@SrT~=?+@!W*TZYrHtX!k;Ww~({us>wMj$NQIbQCkLS z%T3jG+~tMM=%YAjKA9 z60+z*sNQu)>uyMCUbXXSWO|{eUsL6j!B8=_WnwGT^_L1y>_aq*%e8~iI|)HO+Zlfa z0nt{!Ts&f;H_IDdcIk7xO7XEz+#+ZMFFBI7&}q=(Z! zjlNcq6j*d&b*w3Uw^pa)`osN3ZnNH6>s9N4iX?Dlmsk5&I^g0Z`rIw^ef zD7ok_N2ut&{*rgK)C>y4*v!`DeGg*6NG;=2vam*r!SoJUpnf$r`VL%vx*~#!kO1FVM1HA%*D3~ zHl%Q2s<#ItkRhc^Y1T;(z42HxlI>RBPC+i%8MS~%tiJw%C7cB>Q^Q5%3h#t4s(28ZxD_orqy zm-&=?G34N}Bd%bdtV1kp4HBHun_R5;{N)1B`j{GHR_l4hzORN(b;qs?>2Nm(glY$s zel~!nqC83}hAG|Z`apJ}6^6qH>y1QwFs79DaNAy4`ehuO7mNmxGzZ(Zw(F&frK_P4 zaVF|FpAF>iqP%lr>&{5GZR;w3-7xLPj7Uv`(kCo&<(E8egUM6x0wVL@L{3W+36zjhyLtY7ax3ACSeji#aHU*-Q!61@pOmY-nj%~his7{yViYAm zDKr~m3W4A+w~`%s=ir+6qYuE@g63Zmq7w9~2cZ?TI`yGE}0;kU^+ zw6Ko#Hq@q?ZJ2yexy9ZZh&ao6otz!sH3|Po(b6$>xO) zol+{q7sSjABDzTa4ulv(32^Wxq<%}czqnp%-!fbJ8FW*jk&!>W1jEbrqiH;_n!6+w z`VxqqDbJt>BT}deLKYGuFtgnJxS!0ZCXzFAi=Y(DF6^o!66w47TfhF7 za!|8c9(6$vz7k>DK#OXyefgqO@ku!-B5*ul{Wh|g=+AlW$}?-OnOnFFS0O1yhN|=7 z#Uo{QDeF*5vir9~7=v&;)F8B-+^Zj|!Nt@=aC6NIvyeJ^MO#_p*#fIzs8!f%7KZ34 zn}$Tvm(Ae?^sT$ft55y?GIzqPg7I>Q4@x1>rKVPdwlKRSzI-u74Rshza|t8RIJQLYU5>H$B_c`7S>m*X zQ8Bn*zr@SK+XV9=aosfPYCZ+-uF)cKJ1})n-mc6`SmSNP=%`qJYE|1V58#-?hg%)B zg?{>s2cjba3z%>nhfAmmLxJF9g)y<)<0^*3!c8~10z!Dew&DWhh#p;HZ8 zzu4=hL93knItA;cif*57ST;8siXP$VC#FY9)NjwTM~}9H+h7$zt431Z7Ll#9$Gt-$ zMwb+d5k`s^C0n<7^pqYOX+>E*A)o<>H8${fVXp={M3?k;$3 zl!uG1Bv2FRQU+I-56if>w^MJTq-=?HB3~Yz0z<;lLi?`W3Rm7%d|p@aw$%0CUY{+0 zjd>0VyJbMOUT+p-9Y)^~TLPKgLJzfU{C*YDQnDm-RN2K|`~AyF!XN~lXJ+Y}wcV(y z5~xiddDjZu$(uaN9V{ z)oo8j_YN{n_!BdwjGcq`aogbfND{vlp3>+lFoew+a z){|-izsS02?DDQf{yb)OCQ3r|KiS!zp_cBG5Dw1Fgw8V*d@jxu-2R%l|E-EOqxuV^ zNoU|*Qr2@W7CCQUL=RA$lZ;fQrj|+@Y*CkNFv?Z~>t3h{YTspd0+7(NW$mec-ETnT zYhSZ18OInKsocP=Hm#gJ+AxV(-BG0k<#Kp0Q$RHM1G@9&&&T3V{Q#ri7*sp-WU&8q8y3TIo1NbqM_4%Sja zglFO~-W?*SV8lfhszDxiucD@rF!o;5%aH^6b3x^aeaCua4ON?2?!pEYbi69n=!^bJ z5#(#$&{5^{8&+3X^~Ho5LGR^SLprjo8>|U!Vv$s!9p28IfLjo_(`x`vw-5TVjjsl` zkW01{heSOG9G%i#XrOd!s$dqSH*yZ2sA&BtazIW5iUkvIcfk8i=)OLh6pi0mX4 z*k^X6G4j#;yN*YaSeGnEjAhANL?a1>J8M~^*>#|IT+6Qdiuk58V*xAr!@2cgwNn@Hm=55M4?^OmC;Fl{;0b60;}kDN@i zhdFH9N*HDr#=4kdnxJpD9met!cj8o<^=!8`WQvrYth^iME0LMkauSz<+(Q8=H^Qpr zTCcpdS7u+u(m`6h(SVKW%4dXLroh3SdD3Bb-;*Dkm7JcMa?lrQ1K_+o8G)i+adphr z_z;I9naM!z9S~G?lYgg7^nk|tMmwU@Mo+;?V4>M`NLsks4BZTeJ zCr_CoVrP4>L|}!Jyz^LVmAc`YCqxi(2e+s^GfUI~*mp?g|J;+%zv4gh*?XaKH{rfs zD=qdY0+t_A`uhAk31T?;==R*#F~1Rxs#$f$y4hUnW^225^zHOJAWY7Nd*#vX{xjYT zo5L4O;3kFq}z!du7H`_Bq1i`D~DPu zdvV8b?7UaQ$%Lau!Lg>k$A65lGd+Mr^4`e_A66zDYN{!CSVk}G^ICn{SC4_oTK1gY zV=HVGdy|aj?Wp6=78-|t_Zct~q-#At8EBU?(cm*_d6eUA5`IO->B1DKLfO1ZNo_4A zna^j)tUYwBOfZvqdzq}QwIBa0@n$LZ_NpsdOJ;0ZaqAQ~l{hp$qnG|;rvGOKIdV(WVV32i zDHEP6gx+KBN5kN(W{Uk{>bcD~n0`tD|Ia_OGtKe~h5l?7|KdpbE-+p6{&9mp=l^+N zB7nVW9ZmkplKgjMAv2J52|g!`{59Q=14k~3)c7=P{2crLcfkK%`~!^hU8MgL^?!Ba z`;Y%A*WVWN|Le;M&Gk$jn@NM8$K;Px-IT7~Kl?to+KzpCCUl|4Jr4wK{6-p6&vdO) zLaf!Z`%DgIi3z@vQpQcjV(_6?95i7K8&8OGUUm3%{*s((C50UTQgEDltqzn}B;luc za&uj>-|u09M$65I=b=cS>4^2l(xV>VLUDnZ5^s5Q4fajx3h5;);bm3JTEZ5gj3@6^ zlA_z*wf@3r$#qYUD)Y65_uBES+50za&@1Q&z(UrnzKxeii3jVJ%Sm;gl^SR%33f&SHF;Fp@BJzq%l>c zwN6_E8BNpbmWqwsFeZQPPUg>tV7grR!3uZ?CWt*JV&j{(`^V=-@XT5#{&WLu3;?2J z#{w_`$`e_s0g1P5nyzRbvOo1U%BSQ7AeSl0Ks_~oH+4xXmM*oh03#R_)3FT{5=+v| z_f4QHYYQg8S>J$EwjoN%6GKHnjH09|JaUCi=8+~Nz~gudVt*6M6HISvG?=DZv5XTj z;OaKA7T*ZW{yWwo=cV#6X$lV4i#k)elQ_SV2hB zHt{ze`%A-4p|0MCKa^7YK0O{jqK%(kI}Y@4cB$_BhLKTxh3~G6&O;@C%ars-YdEO_cRQGsjGYLK+b4T5JjFk*KZvThu2=7jtJbj5BI7cG6}VMf&eKTk>#?a#>Ff%o!N14tz$X~<%GHay^^Wle6ii%OTiyNMvQZQENcj;#kpIR`@Ne8X(1d+A&|!nH`NgecWfI$-6~SsC_d=398ll&f&=amBUQ zzn%$ic%-tqLW6%eItal3`~`L)*tCGp#;(QXemY4ZjDnyEXXo}tBE=htSJ2A=LF}`5 zZMo%%3wwLC_BIGNDK$$Lh-PsJ4Oa#d^JnarOE$XP!8A$z?VE1!m3AeM)16tzUxoId7h zG2>DE5um0=y~zSIex_vgv|uwr|G1TrD(!9E+M=wq zAq8`w6|PPTxQ7bLxCy_n^wc&8+oKmE@P-KKXtGk?5z8oLSD037r>#?zif1alPEZoy z>;@2}t^ImLri>Fp`0TGUz!g>#(}eU5*&DrXoA*gZ8vJR@m`J!(xNw?K$V#nE)m!Z_ zLc8O6RSf{SM?|#T>y1@XY@1)T)>WjFb3i4tdn)p?d9F=b?btb`{e&Qkb-mYNlimeI zi0j(<>Evx}D(q|rI8>4ex&M2J1R9f)s;k)%C@c3y^g#P|nTGWFGiNyy4+M>;rRyPQ zxBIV#x35Vsp3|aKBW|HQ%9yv*jf}p*zA_P;`s*9%LOIM7w%>z+GGT1jlfK%r9zwGn z*Xl=+-h<(UyH&=oo7u|ln_D%pXv&=CViu~Es6tk+cT^v{=T+acr^{nz9Z0%{`+6Na zp|Y;SOxZI65?XsKtHw;LDq}gjJPilmrpW~iO(Bwlrj%#*Fsz8Zrb$tlIl+)ZXo;*Z z`!$B6U%K@nbU9W9PHb7zDiMqVu!BCx$H(0hDuNb5gl0U4Um^Vtd5ck*Lf*}4sen2Q zHH5B{GOXFloqEC<8<49-VR?-mR)>Qh0JU5g)t1~_WK>+T)p@;P+{{+Wx~)K+m*gHg zcR%WugE=o-D<&9xiI8=gEuI@j0E+0bCyvQ*au4&^y3qq>Pr0TCKKNRC_4&!84@B0Q z_@(XyQ>kySuG}^2(|90KAu&AND{{vM?FKwSh$)@E$MLl1>v-->7bCId;a3;AxFiq1 zexTu-I}K{SBG`r=T2hNZ9brvVpW7rJ?H4g)$qHXo;!!h?3LY=dX*i$P_$f?x$Od4gvLjA3!a0u(tqm3J z<1fu@9I@N$t=;epb6dwc!Ri64NJb0?ze~+0Ef|ev-%n%{ExOIw_Gwi-!$Epgsdw;5 zQ9d!{%*hYs7G*8o4-Wee*zxDPE`F+5Ughp`c&eh&zMO6X7yU5$j#(g58TX_hEk(6M zOL*r!CNF$w+fp6I7Cesz_SRg{897Nw9C|V=H+}!~B+k z=x0Pqw(r9ke(d;U6BeGq#?0$vv-SS#l(pBcR)mxo?6>nzD=YYy&YZm!S9iv>Anca1 z7sIw+PkixFqguY?jzX1VK^w#VAtm$t={LAcnZ!3{CBmk}@S%u=qr>X*f>4GOHZ{Wg zxZ3hSti#@W>NuJ0m1t*`k1>q&vN!(@dLV{-bpAq$F*A7(GG~yiMi00ovz1s)w{I!k z3~STK4vUb3J@Y%9ZDteo-jDbp^N=9(<+KA=n#vpOF;fFF7i)kzres%$GwDIR)N9wx zV545aj$mcXwrD^5P2g0fZ&YLcmOA0o_pd z(SFmj&M9RBPX(1-$>I4l^mul3jk_!-cY*h2KFR~J(~mK3{)9neUG3GsSbJy*v*j#4 zEJdX5Uof0;Q@pX#dUx-B9P8 ziKyhE#v7uORaF{-XSN!AWR7kW+iZQSw1F?QFxqlKO~_r*1b(!|f}%?nWXvjDvPnsl zRmZu><#?ZwBL0I;km>sjLAiKg@K#GKGS)e#`8eKva0i0?m_1+JF|nVvTQ_VrRHoLO z5Gcx)JT*Po$mV73;X9PE{KyTFIB`i9Z8Wg+xyT#3sqEe_Tv0$L&V&7RUVegKGT1vw z$WTnYne1NSD8$T1SezmoSPwM87OK!`3wgoIqwdetj*G`|FZc`MwCS{a&^6aYxuRzn z2bHS+7!HPb&)WLwV+94r|0NWH>I zl)Uk-8*qNLt9{7*P@|8OvrG%$jZuul(IwE$Yl3Y23RU5}Y~=!wt=2GyOMORsG%d!I zK+2rlIEXyw?t(u!+f|pLh=+DBV{t7NCIir<69#D$0U{N3s)s+UQ*bwz+&!7&PYtJ& zt$EqvLErB6IqT!1Yjg|BFogZJ=A9&T`oWJE%^Mdu6uz9<*645aS`X8nDg+$F1#p!> zK@_8mm9Ou`J+ck&9q)CTspa1)8=1;L9jR;x6g#QxF_3;jq*Wj{%3_BQ6T$rcof5@R z%9+!Dh()CChTI=eG|0D(mL-X>AcTDR5;d zA9q7rCux=Yo1p-hQmG(Ci{l+O4p+HRcd?x_dUY_g6a?U#4Wml>i8<(=wPjt)mwleY zYQj6u%q5&v-{$sOk+fdOoNR@fc{|U3p-IU$`17W+rM=bi4~*v|7OBR75Q#8r7fx*p zz9kd#@`-c!O|SQ`Y8WZ#2;zZ@@6!jh1e2kXzBf<}1kslz$k@hg! z^quQ1QlwNPgVQ1}mJHP19uI1eRIsgXdj|AWox?48TYJcegJCu>CM@_W+b3{KBy@ZSVpKQXSF!(2Pf(V^AWf#S1GfO> zXr%Ynwry2DvDna5hg}RjdC<%MO<4DF>4nify90S#fyM_K@ik-OC(z!u+C(|%m_ z48Df)l;7JdNk%fNS98+9JTJ8gMP|XHba&ed!;Y#fP(&Z zlRE7_EDBGAW!^0aeU&CgEG|0y;R2#EyG@{A(|AJ6=WOdOEcOTZ!XE z$7!SI&IAvHR5bL-1BS8hQ|&NgbF^iMsG4@akMPb{KtU87nb$#gzeF5wh>2-3^9xTycxJGA%A#FVsH7X zeb(~y3i8OB6*X9FfwknZ%8S?0d z?Xh8K1GTxq#xJ7x?EH7`n*jSxfQFhA68AZD6ABddI_3rI?!_uise<*5~HX z4T6_|td^wmBK-8n11?lhuTZO(rFzbTk&2S&;gWQVYhd+61F<|`-Tmx%@UAxJi&0h0W(ON9dncFSD zMMcdgg{Rb)xxcj6BiU7tmdL4~Yu*r$Z!p|iRM4BS*DPhM(d6zqBw7_ZHk<@MW(xlx zNi9$28Fo5w#%JtwU~^8B;__!B4cg3CL&T2qYAAu-Q1Ti5D+*5}_?(Z3N8`PZO4Qv7 zQ{lK}U=Ys_SIgF`765xJ6dJ67hz1jqF)Y*gAU9Ib$83Acl4Fl=9Z9>rrNFtgm*mEFhfL3e5k+(H#gX1>aS25X}C+QRE;>Ox}bJi;oQxW0TMyXE;)n5`d+Nw!=lA`UtT ze-8XI+`5P|(YoM6J#GKsc^1joJThK&l>U^lp-}>seG|@5GR}r$vWvgQYJ!@Z2YrX*eA^`q4 z=iDhZk$C-eOtNy(vCB>4L?G+)tc7iQC%*e8fFT*uz3{0FyQmyjvEBny36me1z_25M zWkrlln0GLJm1D`JOI#8bP>&41*TMQB7-?5e1vygP^L%$tIX}Xsdkz`+GM+RW4z`00 zHE?zB&+@%uaMLLt5T=bB{3m~~E&pwxBy>WPE(IiZUgMgS%&2^Bas`bJtCr^|)lX$M zD#{x(b(ozny~;UZ_P(jwRipxMR#`a(?{qzO_>+O<0>4OQV=43d%1V_MB+EqV$t~p{ z@=aTvP6M7+_;^qi3$(d&xQcQcx`ncK{ah2K)gDk8$y}(0Mm;Ac`yIo(8}xfzn{DzV z_V#UE%o&@3$j|R=4W^E!$!RcDn!K%outZOTob!@dO}+Ic^iPlnRwzaTHj=hhQi2ma zoO-l;K_a^Yyowpve)?m-m24BxOZcfHD$MAyrV0yGmlJvXMEOtl_50{mU4Ts7s^7}7 zA87xpumi0EdbV(O;jV^11$6&6AR2lCsAr8Yjqv`oV*iwLfbGHPKxF&wQ0|X({de-f z^FS{{dH);P|Nisij}<6j{Hwz8c}o9E*s89kgRObSP1m2&l0S(Y2?R=1wtFZQ|9>TH zEDhkQO6u9r&%G@F_o71FFF?tBy)k(CM|%Ebiu0W~z?H4-@w_uX*UbJiSQB}Q(;|4* zlQ8hNw$cAge%Bn}3JMia{!fSJ--wT{5*Mjiha%^)e$>@JnbIt*0(w^i1C;_||5cv0 z=0Hs^u)1AV>|Y791VmO;hiC1oLp!k+5srLM|2X_eWvhZ`jOs2o1Xl*;uS*0Y~8_QND1nhs9@ zeNRFkaheN*ZdXxe`X_v)0}~HKW6#_r3A_WQ8I;iwR64t}e!eE3@8tjL1xx6<09=cE>U%zBJCEEbz^cbOKFK_ z3CkU4mmLbBO-V8sWdEaiTKV<6Vd+Rv`aNGUmlGB?RC{N&eIvtkSSjPqEM@qmN&auZ z#~e9wXse>%w;QeF8`Ja7QqWmbS4*%=nZ0{8Zbn~rPlvYUh0CIB>#2qB>dr#t1S;#| z(12!^bndINskscS_^4RBlPf$PD^k_rCY*kV8tkWzo9flF|Uy7g_hXl`QI{d)b%8N6xD932tl(JRI ziao^6ak+Z`AB_qt0RY3i%dZTNmGhm%!Tq@=j`<%9eBJr$FZ<#JHxzSz(p=!H$O7HR zVX>`lAIf>xUtzEOn^W;Vu`*ZMSaItoh5Ad#9ufeTGBvaRYMUQxP6Ni-+rN(c-)3fE z8?ylXm6Jp3zuM*l(M^DHo?d<^`F8{IA9Z?H3#{Kyok2$bPMP;fpo%v5#p*9oe_Z`{ zoq$!F7>&>WQ8oV>in;)_YviOh{`kmW{-f{{AZ0%0X|;dns=)woIr3tje;4+Drq@ga zte4#3rtp8~$}I=r%G+?_>|d|@fxl?JqihwP$GrCbBK1cSN8|ynrgJN9|M18UG|B;m zMdazp^?!B3nuXo~S4~Qm@&7K8|9$BH20MTL_}_>AQxg9TApXC)q6=N8&FcTfibW4e zUFoPV_!^)H^Fe$&eS8$y%rcQjT2)r~$MTMrXIm_UUM{Z+n~e3YJi(MG6F4Dm#FxQ& zc0>qRw&4xRk7f@tlgm0H$;EcE~ zeG_j)tv_dy%mjEf{rW*=#qYa**o9@z0(%;i6O*4pPB!`|MH6OQ%~#RY_Ho2W0?lzL zLS^)l@~OXZR=CdM*Ks0i^|6u(OfJYs&BeB6JxsAE$Zd@xUB(4&xtPe<7p$*z9gClg zrUfB-j(-I8zY(oS%If8skJ_DjXUJpr>^jSaua9q%Zbfiw*^4zAbVX9Sc{7?d4q;v z=1_kH)R$lG`uIftokGnr&J_*+=lJ$d}IBEoaED;V`Yzc^%h4jE31IqY@1@*$T%NRu-240K#|O7N7h?|Bi_^Q0$%+ zizX?_%`!T>-8Xx<4T5Kb4EP~U|4{qKn*KnnuMseC*}d^LYe^aU^vfYACM{sgR=SJ_ z&6^d~Otx`P>~V4TsSlF04(g!0L#l^!phllR^f!S+JS@V9-NxA8AR6w-jtc$4xkCdr zReR#s63KhtfW2eTz%p{YaS>%kP=I51!I5D-PJ zdzm*RVq_yeKELKO(;QnvPGR2F4`1)Nxr(d?#l1RXZX52Tyi&8!Klj#b#{g-Y{rwci zu}W*m_K*sp1Rp%L=dX==xUd5!xfZ;tVvwZabXH+v3sUkQt|ve~8YH3Q83x*4(fP32 zr)2hQoKhiYk6Q917I(CV7t1*HmQe~ASMa;_Wr)wC3-B}*$*g5wRa3It;$#DriP=QI zIBG2|8F2x_P!Vh%VsdYlTb62#SZuzJ*r&QRE(ja4R>*=;baDSOX#qcQb|biY`>U~d z8NX13tame2z}=zh4eiUjTI^$fdg^)3%}`fa7ua{8{8GATn^p@MKQ8hgI*+~@6A>t7d+<@qNCY?8{#3T%T6 z!?wH7CBN}li3o4J$g z^XSyIniDpJhGkXkF1RB#KO`{3lJMHbV{y^GdOpT2vPbq9vC7!n2U2Thcvq{6t=0Ng6y-z@jG}D9PR(SW6m{eKe&j z$T>^+?-ua8^KZ7V_KWM*@Rop}W|Ro8U8Q;xFoM+~zqe5#jEIzVm4Kdxz7H{cX2~^S z{qY4ChZ*Dc6(i-I--6L^58so*4PG-PG>n}9t{(>O-mvIy zG7zpH=7G{p&LC~T{)i0KfB1s{t7vTo#a9X9jNuljF$Yx9=@PejQ z+MdQ8%;E+VP7h<;bl>0La)(6*NjhtsP3gdwiubegiQ^IkRiq*#D-#jGX5IAl4r1)J zM-I56xHLTV!rxQCffXi|#jC1QjYjs_{c9<2&-g)+3e>9Eaf3@_O-ZDQi7Y53(gyA$^L;`rRX*}2`}7qVT_GcKDAt7l{2 zr&J_flD9n)4oUaPm5UB^sP6%HlTk`Q(sG{UrKtW=LiM%c_GfkK(eJtMo_P5C5!VF4 z0eRfl0Qywf>!Bom1*vxWCu{fNNDe)qbMG*R0|q6wDacbd9%TWqX^?zWg2;MPHM94k)(A03kMM|m{1=-Og4 zEomWAN9tysjha@js+(m`R*`Z1?nr@q!jRDq?%R`_Scp-ZdFcBdw6RGs{OlF9U*jA1 zLFwaCjJLy}$ZMC4TykqZNpiq!eaOuV<`_d53jE>YCkPQY+$Qt3?kMbtzQP~%IJ=$Q zJfGgcLV30U1;R|pm=?cT-i30Bwh=0@0?`JdS`qLyx(n~ME0OXZ_u_J{O+XC@{?$9A zNsYhpcvqTfLGMx$u-U7$YuZL`P(^M}LN@J+qKmBp@~unY(=N%jnVAdh{jtY#YrMOk z1gW3S^}L@v#Oi~;@NyKEyk&4sgH*A@uhXH8KvvlFL=l=i$5P%0BkzwKiWp`a;@aM*B9OZH90D+HKHcxJt=mw!x)WH zFD(UE;*p-f)fFQa6{VHd7C-ke{}n!rX#yAz^S#z{f7j+ea>nRg0MM8Qx%}-6gFnZ< ziv|MB!|#H0{vFW3^8m3h+6(F}qmI^ZvW8Rz?~N&96odafspijVMDChK`=-K1h8&1i+ zcz3fhB<9QrHxR#CzgBk3Gtx@Zr%`rrg+;+Ljtl>qNZ?tZC8N?B7Ok_>#9Q{hjgTAv zIdUVr$`ef8+!(8O9lqwE$-suqB&k~Ddj9{)CejWxT9dcG*q6x=+OOd`9Fu$GBK*6i zU+|@&AF(w6UchIztlR#3uJ#^Opzhve$y%1>vO0Uo(@J}ARS}}ttB6!|{ik$fqjU4p z?@G9y=w?Jj@e$YK&~~PddWM%l+tYmtK-%?s=u6m zVAtdrkL9R~s6bWn1-V9!)!GZc?vRP=KYR1*jZHdB3s3T#-xlpRBYNiZHO~`vMfY>8 zR9^EgPBg51)6MC*%Qrp~;gHaq#bh!YK5zkN01H{e&*e^;6FzD>UE_!;zA$U?s~`4j zYnd!pos(|K{Qk%Ie|)7?VdU?!)VF%T5P$b5pJ6#h{|7ixC%D0Fhm~Ea^z_BAgd*D8 z4(!_dzLT@Dv~$g8jfMrg_SRj?7ZHdskE*~NS-qgjT6AER^R+zT=2u=nZmDW6`?W-Q z4X~NB6F6FX>TM2YZn&_LrDg5hS4CDcC4&^ipI<~|Ke)d@42R>u+-Chx3T+KATulmN zx1EPS6YEtQBSYg4=#ejXgF}I8ha*XdF2Ew0mP>}s$+MRL&95Ict%BKlqv$- zHQlEd2&1>WAf=_khHr8h-2kww8g~f*yPii=FL0uF>>z`i0uj~xD9(qEc{;>`dc2Q} zUa+8*$WWJYtl7(s-5(3|KogOLk|ik99SE1RT)W4N;31n4AP$5!IEx>35jyE~-2yYtfVF6#IF-+FI7 z);a5(dv?v9nLV>7HX#af;wW$M-atV?p-4)IC_zC%&qD5cgx8QS^!Ah{kO#D*lDH65 z$q2z7Di-FgCVvG_!SDlfj>YG@Le90XP99a+>DGM5QqW9%3$kY%E-*c#l^_P z!pOox52-=#=xXC+;6iWXNdC8z|MVkb>}cpux{rd~2 zv5Wctjb!8aZ(5KEGQN~BGBYqS{_O(~^Vbe_Q@f$^W~hs-v-ku&p&@NGJaPqs+g?|6BO4Aur?0%>M@yf6@Hs zD+JH{Z+IF1OEdmAT&QLct`LNh6#1;;0)3bY=cA&EAJ}Rgm)+Qizo%sN_MHNrCydq=?_45EBsm6C+&BB7G7SvYMgV_r%wbN1Tm|C468jB7GXK z6)T5~^!6S|snJ<1h>c(lY&PzNS<8?q1Ys!9i(pfhLQ!rJryD|X$naNT4vOCT)t&)Z zF-V{>5VT=xu8&sVFG9ToEc75QrsGP>!MwE6Z^?y^oXt>!CM(;1eY!Tv92x~SKumo1 z0?Q2tbh<^xZO-9@e<`A{02Y9CSElTY#f{3r=-r=A`CyJ0Em^Z;xBXdT+t_T81l|Ey zRj@sH5%$R_b*Z3V>h#gTj2utdnCJfR(ekYYS`c|1Nq=dpP{z=9 zaF~-rcKZS80tylK6?FWy_cL~orCB?-@5ZJQ(}N8F{sjgSXbwaiH~pCmpxf9mas|UI z8wTLjH`#6imj(ZU9_3bA1@`GwjX%90%&OKLiW@FQ9AUv@AL;fzIV3qWy@s(=kYW@C z0Q^Nnx+Pp({Pl#S7(YfrXac!P2z}p4%E?7#`4>h-MHwk0KV!~W5&2a!mi!pAm(u

Sho|pMezj#x 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

K9W-%Q$ZI3QdJ+Uoyk}kETtM%~sWQQ>j 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

&fbyJ+n|P~C$se-EHzl0mg4CdeAD( ze?f>FIaxX}bpZ1ea-u6;GjR()Zo+Y;{>k8)RhIX;b7;@)a>*4QcKoJ+Sm?|XU9 z+I@og`Qlh464~utJ&0MZC~(u_W>%BK*Y3S~Bf1YEyAI7ucUc%DX_0D8qV>7X7@eEo zH_56#hdtPvulunnrVe*UI6Q+iLCb&dIJEMc<7 zeI32Jv61E}6SQ)VW3&c5KJDAk=D~C7?*JU2v6!+`c*6!v|4h1yYjbGZR4Oq?rncxBvf3s;o*GU4{nl?xzX zTNU5M6~i*sZ^~cakieD8$d;%(3)fw!6@xB3LKl0ah+SX6AwNNAzC;k1f^iUpzln%|1@_do6zXnXm!Z zy2sJLV-btN6q%4WdnJ5&zwwZCI=b~P9|6|#nAA}Z#p37+IrA-0t3jlN zceOnX*4*awJxA9dV7m?=p(7ek{+e{cMGbE*eUB3Z<=2FfTcrw&j;ls|b?$;fBY$|S zQfxgxnP+PSpOCqu&MlvGP5X2ZeoH_gA#6lOU-Scy)4Wy=fIxM)BByJ50@rph^E-gn!2n|Q za7Nuav_Ki0{wND7v8%9R`w)$*7n*^U z^~-WNmy2PDNgNp7+Vr#n#_!kv*zPTAjI}y{_t{pWFUwE?y!_)*YyhExl(HZ;U-Q}F zV=b)Jd$=eForvcJ#0*tiz`-UL*SjYo4_L;KmX{B-rCeu)sO-pg0E$AX4MQpJcW3f| zMJE9ED62lmN&1r2iErw!WXZsaHg9=9ZQg1_jh?eEe7r|^p(24s6puG8Lq~nh7?A(R zFOP|f`)%vOmVYIM#|7{i2)R@3*W+ssKYhAivfZZ}RZ`LZ_99oG%5*?mbC4oO-dWj{ zSYO^?F6o{uQP;`w2l_k(^nl`NSNeSoU6~7bF!8Vl5kP2)q9%>-2q4|S0EpUYQ%>!t zVq7N2K`zN@xg6$iAjfb0?y4*%6~1U)DInIQ$i8@y6aMH+(CBCG&N8%Y{B~$*wtO=RpL}{#lb_SQ9_LKStNpyau$)a<+3S``1=QN=|gD`3;>M zUcX%uKnOWbJo`q}tSTFgrt6!*PlDvlpHl$CEL8W+6zl@xx&NJtgiqZoZv|T5&c37P zJ+Rka{9><)cYHtCtM{it&iu*t3RU~&jc= zj;{-La#y=g^5Z?Z{^m`U1lzvK^HSYWfc6O<9Qcz%?_SQ8IgTG3xQWki)qKZw;Z~Hp z)?2v#B9lv)Q!!nS_8CUc7WU={;AOGZ@%25x5cNS(IE&l21xmBA*zcrOiO1UpTj5Kx)xSmeHH~uLuv(^ zBFItefBf~%bfqiZUWH^ZiPvIdWYC-1d8%9Df{%HP_SG|zpBQsJAHRgG1K14!QW9`s znF;LJEB%2p-tU$|oRa?r{)2~?(J?bd9a1{sL(|*uztD9TEspb2{fA4_7GsQ}x03t` z#lefNxF|DHjN#d!RQ{ow%ST@SFZSL$s>vgjuRH;#E z(mSCCbt@uGL+?s&q4!W!1f+%*Iz)OW(h}0|iu;^9?s((<^Tv2*ygS}K-`EH{`SNA0 zx#pT{&fom4xghep_giLj9Xr5+3V^A!ylxQo$-4e7ErX%#t$WhMZ}fl9<`6#Q(2~@w zU-~R);l$7U6uL}h(1QRMcij4z0SJi?gHS#Gs**v(ssVeTAzDf#VrB(-2F2!j>z;z< zwdl5R@|`J*lqlX)Prkh-F+ac|W~kpq-^Z?;70*;;Jmfuo=Iq)?JEHwdi>mC9%M5ia zv?+JYarnKCk}ghhDpvSXuKVHnAv-|_j>$qCXDh~Q5+9q^MqrE1nI5ElZxOp&B>W4X z{y>A1R`At~g>?bwNL~%jZ)#M@gOJj)dvwr->gY(>w;Cj45kfkf$|3&yl%+mq)MMCV z-{w-`HW=q$KujHW8g+DBI`MPm&bM0u%SY`AKA8{O=eT9gWdQ3{;5ze~>BO89)09`a z4~n?#4MxrJM4~KezK!<&g*gVH#s?B<`XOy3_%!$Drbe{fu0?znE2bLTov(Br!3KO> zOMN51AAb$dA{>w}%AV?H`Wc8&EYps#tuMK5W`4QKO&CGpvfhTpou_PWI-6`$%mVkP zO|{pIfy6|}ILzu`VRFlss2Jf1h$3WzPJ#ja_3cFj7{O}ishl_+soR8H+MRnfSFUmd zDVmiuNImLeREZHNyB_9D7-~*{6l$a8DYYI96d6#A<72Oez%t&=r04Vu#0ncl(4J(r z+jqyOL(7}j=~Y%uy#S*bM#-JEj?i@-8)!4zMOsyi?Jn{t^bZTZ?2gUiM0@Lwi1K)? z4%zjj+{S$QBw6LGqPm*SQ8=_r@Dv#0`#-xn<})LHP#C0nNi4&{=X z)li6k=B5Pw5E(iA4_{FIl7F=W&fqBV2l1aG-MS1`Jg#78hSQU@ZOw69=%%!Ed-ItP z1R=YH{`nhUo>Ove_~En6Y4pZldTqSQLDqLQyxgWIBfxj(?wudpVBtG_l2N)_5ldPdZYwq#9( zfUqhmMxOl2#(n3yq&a9E4Z1eR!&=0_{$pfpk z4EGLj16~~<8Pt11=SIOqx1T81%TSp<0GuxUj@eKo2Ga))ulr{(~i6lR7_Ci#R|uIYiE6y-*3`xBnGb zIm6zj;pxY82}`4Mxg${%r8l$`wnWWp^V)h$1WkTP8!2nY0yq8g@n=98r;oPW@m=kH zNX(}!&xPg)cE*V$tH<6fK0w+5t^KZ@ri-fA+N|q^kE8Lye$CYSmNIS`s(!Y+%Hens zCw>+tz}FRjHD&5Pl7=CUSM4)w<>e<0K!A|uUyTB$bMf~QWdtWYl(FZbjsNtCIm<`1 z()YsNMV;Sp?RJzZRHg?AnHM|+;XfGQDv(%VZ3iZ~7Uvcb5`u0)^dcbuviHl^L7d{E zz8ds@sH%FS_JxzvcmsB(7L86;kGq~DTjh|ay}<>9@3y7Pke-I_{Mq&`0pfG{T@a}1 z?|iyOw%&9JaV9}Ge*tmgiCm!^*;2S6auwzNT zPlj%ajj=;6N;^I2D93W=#6t5nZl~_GCb-EW)LQbQC$dd|+8Cfn35t|iVasBg94H5% zHE7%WWb8wg*Y?oL!zD{gOKgUL|EZv*2dN71=@o^d;g>xr3PvjGhMwaT(dU_slCrY> zq#o~F{Wte%nva|SxOAD+NV#EOS79EiqzL+w(~{R7;k8deDuK51?W>X+jJHPL$ZH( ze;N;My>T2QCMr#8HkNnsPdn&D68TiJLAL5a+P>cGyf-k)y5v^L{u`S6z)G9fx;_D7 z>)$^fZGbX-K$90-*-Pj5Hyc>q8urV^B?F7*J5nx{tW`K%G?;lR{gDK3ZSv?p;vcbo z@klV|zP_>C?$Q(DN#bQ$B75Qi8`OrHb(D|@g6faSty-TxeS$WXsuL`peY5V$GaDQD z0Q{qGEovZLv#BrrC=SF89KvxUsN|Z;fY#23ne0e*;5Gtl8Vf$G7WFo3xm8JWeP`M5 zN$3j|FRD+TVadeD)wDfd_oD^(-@1di_wniYtCi0O&!x(M9oK85&&D^;0d+@hf3rSo zS7kq!BO>c|FFVp%^&T96 zxJoAq1hE9q1Jo*T?^e^Jg8&owWNeWIax@TSb?dnO!w_W4kTQ_;pR4xP8D^QP|6l%H zTXN+}F%$`$pYC5tOu*@;sNwrC9*HwsVDWzx+I_YeqU`?Y(66Qwa$Y7#{ep*Oe-(or z&_BCpU%agyy?pg_Gx$)|BW>3!jarz_0PhkTl)2uaLt>7WM0X4Q#u@M}Q09IoH+`Ud#oWcd71x zdx3iEw*6#Y6JX78aCw^`ik~L;VpI=sCQTjEk3UJL-xpO;Rb z9y#m1TFJ_Nykoazg{eouyRNNb5LdzfRdvptWLD>8USHpS`j>0=-+%wN9R8o`K-Qzo zBVGn2&Oq0h`lQZT?VxAtj6;Zu08VFOuypUQ8JIz<{_*Fm>apFqPzArtHCJ%wo9X@cL(#?(B;D@5U zH?Soq=|6vQS+j0#o@-RdOYdGB+7p-cyYo=bwpR-)c{QZ5P@O|~82}n4icb%QRP0mdcc1cr?NZ!}W z@DNFRnc;4&4n@{``o%GGLMuh^DA9tam!Bf_EBvf!)vlo$UN35mKuB)TD4UPJY*Fs> zNnh}Qd_#i(^jw$#Hl6sn(kCOdp|L^ND?+PUGxgZ{(dMYvT`>cFAe9)&(VcFDZ&Xlj zf6#Kr@rTicr*hysvJL_7>GAOLOY^3Hu~E2(hn_bd-<);v)BY<-q(L98BniFdZiYf; zL87QAtnP&)uGUM~d~+;!>7+!*{GHQ(2hC)Z@vuM01nkq5j+Pelg|2vU!qiP?`-#=i z5Jd|pTba0cz2AmO#@zI@g?q&)4A6cW@)HuyhwS8Prv_cx^Y80Tk;x5)AVMrRot?Mq zQ@Kl3)z*f2%D!Oj1E1}oIgLsz^ypOI-Ko(}ZDmk)RShIzQz_vI78o>TA+Yux^qT)E z-bily_^{n$sW%%wy`Ed1t^O0RX>5tTvR>ry;UZiLOO+-cEc`+V7)R?4qzW!PIcAZv z(93ZtS`(TEt8k76Sy0r8h0d34>%Mk$pgMn-%vCs9Ny%sj4(1qmoMAy0TcC@9E(*%h zXFk4a%j^60;GI40RSR3a{e_Gr;WuY^9ELs@A7Km7LhiOBn|FKQ(R`j&16JR&?8qBL zfu#MON7h@_QJwEYL!V&cj|%N(Ib35_H#a*KcKE8^U4KFYxl#P#oS?RNWTWJ7gF3YB z^mANC=!f#vFmc?1`!p1_j5oH(ff^=N3&uuzC8GLt2zwAZn_F_+F7pIsVrB@+|(cJo>YV6F^ARsNs z!hK9s!j5!IO__s@t0M*2wJN){$cNTbQzG`%(M+b^f_5xZ--eA>5QERrw{k%e;WBg^ zP%h%@6-q!jap(EK9bZ4c)qKIE<+AqGPWy=d-9(QnyDo1Mc|Q-1;pB%_1f}J&KhNcd zVug$zXTFQF-%HvvsR*jrUh2uriC8Q2!>ZSFaAc>-5nnE1b~s|6z4zIitlNDwwO=?A=htkLm*Gq01S9;T~<({lHDA9f5Wp8Nkxc|}(DMJb?_i!g@KjYaf zub)$zeKAttgFU$$=C!?h!2qRivB!r(&9;UuPu86@DRV~|7MUdv@d-d6qE~A@p(GMn z9Vz8BowHJbFzDej5P(jhy@vex0)s}@91IJKk|YcAe6}*mRaKe#VnP@>aaHP3o=^WJ zY>APnwMP6$bZ%}Ww@eP!LF#1}!928+Od%E6%^eZqGc!?a8uQwox4YWpn)BpMy~cRwcW&pR`t^i46(kAMUe$$QG}JnA$NeI)+I zjp)`4%l1fz{#hlj`O)Jc{y$oM0p4v-m^{4QwV8P>Vztm_Psp9P+xP8_+MU2fGZnMO zBwm;~a#yu^+TFura5|8XFqjCe*IqzKu65bdUE2JGdY>(%#mbZx2w!ouTL$9&;swM< zA`0Y3BEZn-s71bg*-?(D3|!*PC2DL`4DPgQZ}+5`dn_@EO(Y9IRcH+jhFS4anq78N zBTT=>f;PCp4ClVm?p=W-_6oMBI;>`uApua(DU%{>+>obukz5>5ZtZc`qR>Xt%cnSE z+K>qCH%_{9 zL&^20&sd3RVG{F3F9)NMU1JYy=EM%G9z90P)P!lgn@XB9D{L`^OJSI`#A+Rx032J$ z4>lQCnSJWJi1SyW_R~dWfPCP-_#1Ez_N}@Kbd-FJvNg_n)an3UkzbCJ2FkqV2_GBZ zM(MBTVS;={U#PwYmtYnYykWsVZp9D#GYU)#>$$Kcg+#>*puU2cagFFk#74(pkm1kJ zTjuov#xhi@K1yB1xZ5T-?1*holFHSP=G-$XJZgIe*ZGQUCls;|pNwCgIIb5y=d!zo zveDx8xNe$bFJK; zb}c!mwz^j||Fx~Q$YzH+iR_`-&}$}@`(lB68*FV`|F}(K3rEh-+N?EV`=Wce?7o`v z<;Xty6a%1+YfzC3ra&PD`_UKxRwmb%elP^xO*RY)@`rA9-AkWETXEHgXtIBLB!|g z+SXH<`TyGH1g>L=y(=JnDJ-FjjJd5wJsAObj}bfynu=rPH^4W7MEBqizTCAEgrU&z zIbi_n?vZlw7Z)mQdR{dAG(j2CnqKDTXeOh0hz;cOsq_jy&1 za~@}Hc7mQ0Y61@Z1HeQu9I|7`EN4}^-&vp&m!{N5@VRCBnY=W?|?kdy1#qIN7hQq^}_=s=a z_eo$YA9>N=a%h%<|LYbK0{9Sp>#(4~%0v%<$y`XZ(hXbG+RchSuY&~HZ#|nZV?iK$ z&^Q(2OlL$IdYWfa%n7FW>Ow`3WHoGc`bNJmjE^r?$iNH<(uSK4uihP38Z=>%@wiU? zE`Tztus{=eZuKgR6vw{Sh`&S7qpbf3t@Xp6KV0V2F1j)8zzAO(aao>i)gY3wAj(Bx>ByLOTa^zU7e zRNH8Yd1irP5JL+(;DMrz>#uX~^?y9#RR5qo*!6fkc7i@_@7q!LN+DEv~sm4^UH?+VbmPnm7O&{`#d{)rp~)4@4KAVRrbra zFfK}}tbFP+`J%M*U49mRyQwyj50Rm9{6K{tdGN46eiqH1J04lpYVZ77X31r;KKtO& z^;183&z*9fAVcTsFY^gnJb2c!=p-9RNZ|*`0B_X?_=GZc9?%X?;B{nPZcwf2+$L-z z`B4_={+B-^Zd;SN=WxqO*o{m=w~hbL^g)~soi4-ODY)K~nlyR?HNchrgS5R9rO%%S zb~h~5o#dXaC19$hf=2kRVIFxpnZ2owkBX*NfQ845)5uxWX26gN_sjoKxCkgWG4mGj@o`$!|!#oIDz?Zt0Sv;2%X zK;3wNb}C=l9rHNBeDzq3vsBIJhtD#`A+H#}rxVN4vJ?zb62U4M^n;)jn51 zJ*_kJSfpdV%qY5SD$h-?Nz1}6iQK4I?AFo4)S5nd5*mpzK|gVs+WgHYt%NZ7A>w@A zZ?!TbJ7NDNfV+XpCo{H2ArpAtJaH@m#UhBKcYdC&7K2PuK zu5mM$9%*lRy#LELkO!yL>*e;YlJ_pcX({(>qK^SmfJv42+|-(WbxfIg-3ysq@lVOE zytDB)?b5w%@zVzj3@VZu8XE4b<+_cdfqQ<_#6~lnT`;>$&+m~#!0e(;eMc~H5uI9_+XoKkIH;Yn3dUXyk-T#M`Bhtg~2 zu)DW>$1(PU1$Eum0CRzs)nAIsyJvoNI7!N*c+kNzGqq&w?=xo#xMQjFn~T>fIy`kS z%oW(|Xeh)=eXQIjmfsvbW!mIZcBM^*4bHZpoxxeJC1qYW6($Op(y9oBdo*$re^~1k zB^a#AdCUxDDmBt?dD#!K%qa^(sm_>4d{6ASj5ca`SwB`M^ykknb^a5N=Wwiiorfd`I`sr7O%9}fTA!X1wlnK_Y|5xqw z9;a?!Fz@+i_-e^W&KXR;?!B?lwF?(a1oun!(Vkh*gi08FdNu#o%tE4-3>;1&R;%VBUI3gq zOIhw3KVq6JLu z4kunOG!Wb4$?N3kS|Kf&6grMcxWhf7L(n1h&3#YGI}&sS1gD zrdya>OC?IqZ;W88)Ra;c_?%^`Ztap@D^x|aETZnCbDxP1`vm7~&SWc=w}I3+SQX@AZ$m!Xv=J5ihCRJdO^HAknN z()?y33*ZkxXjAzpka&X#5DhEGc2ro5?u7UH@K^Y|)zD##!)%tFDjqMG{&M!QXx-=y zg)B+M(8$Psmu4~Sd$IMwiCW{yJKnpujJvl=q`%b2A+3PJ&s(E*$;w7hC;iFKjnNYQ z{Ml88Ivh&K8ko0J$|VQ+?kq)H!Oq{6kdV;&Gv#=L_z!%vI|QifcF}EI4_a%SC-SvS z;g8BOdWeOBMtDG@tYoU_a@-kPOxj4u12ry+3^jm*!y1d42*ZIW8k&{{C2DH8SZMow z^FoL#o-TKOvm<$O*6(NbkI+&*WL65D&I)~ZSEnXu;QaAVzYhbF8l(K2&M4>c>W-W* z>47NNqKGFM*DbT-HJjCVS?QLMwk4Qw2t;6otbL8a)4@lD zG^!m7%*s->U{m8*Y@xqxAc>@wba(S4X{i*m;5P^M`rL7hX{mxCC~x>Fy9#>OBA;w! z+-n1XzQEqpNV?t1cVqkCFL|JZ!?QW`TZZPc)ga)b*qUCP0F3x}eywCl>2HnAy2`~%D%>^R@yC-!!FHPiCWk8)+Qb-bd9UYr^p7X%6V@{cJemT z%0;10X@2(srs=gZsvV@g2-jK$TSX-;$f~Re7)MvUWG`^hVG7QIyhVnEO_b;MA(1vtsxWw3U!ZWUjFi-){mG^UZmH59o_7&%H+4}9$^nhAe8)EJ{AZpLt!=EULyO} zA8a&v1;fUQ#+s=%-A@=k(MSB60!=d=AAdQJB>y~|o}gq7;Mpe+e{j&y_Nn|gfS1(7 zC8@7CHk*6Xemt*fQ8WGX+h%Ra*~FS@9~DcrQ<5)Op`EF6@h8qz3X04EV(~Q)JMM<- zH1SG9Ln3)&`3;Kxb{s-%l_Smu`JZ~-eo^{yv!WQ+)gvJJjHSJ0HMxUG%F(0>nK$YA zjh6Z*Un#!Zi%Fj(iW%j^kO`|o6&;FIR-koUjJ;!X)RDP$@!dvwChF~A`svowJqnq$PWl665qxzc{z@~lHxEMTAxA7O%?gBTu|Iw!SBQ=wi#ELzhq-&D{N zZI4x7CAR%-TNBU!HccKS*7pK0oXR?r<>ED^lK(&|yL11D*K?o#vb_9sp?gIzTJr<&Aa_SY zk-D5rx4d1DcC@KvOb?u!Q0)j#3`+Ih7%Op-^IijxXJ6iFzK*FLEs?YUGv*IKsq4_K z%|}RN^7XLzc;`vp%SruA4HeeFXu?brZxaI@}+UKhn!;ONG$^A!-|fg zr=~^PsfwArjYpR;3x(Xpp^daDN2w)Zcw`k{6m${82^MXwx{0;ZDrR*V9Ks{lOcjT1 z24P){SSy!#m*IJF=ztN9{`qufllbg8>9FLwOzAZi?)|4H|siAtsK2Hc74DlSD!~T78EP zL66(wZU`XJn?A4KG16_ip@g8LQQUf?8%iy;{9jR~HFqotC0yHEORUyU>XFOmRLj}KUciL;!-;;G1zA2XG%`Y_^uXEsld#4yW3S^ib^}8gWzNU3P6X0^-{)_f|jy3#UBF4AC z(Kjk`-kSr!r|1N+^uw(Tz~-t1$&9oNjCxN6m<_=p7xfO#e6g--TOQr&I~&^=M0+RY zGLbmzulCRk81vZqlR41XYKyP|lUt@&eqZo`cyH-A2zhxa3!b-kQ49UTkO zF5d9^*Aa0B>|QV!kVIubWfT}&T1l5sAlbbFuLQ?j0cCl>b)g&dwlhxjGSs9w$fG=r zza4cdcF0c~yrq1<=rlzBS(Bn-XRL7Uix;|zRm{(r!2p(UmYu!u+s@3^ozVGJ*{un>QG&OilwmE(OFKA(d=sh1OsK=n-*gS6uXzzzg8)j19#x zpO1csf$1h7q4%Gp4Y2DrnZG|7jQ^y8S<&B|a#s9*I({bc#=n%B|4@Se11Szn4F$aq zW*~!uQ&xw*m?M|K{v{4K(g#q+paAU6Quvw##tq_Xz|Wj2!AD>IoN!qNn}KxohixkC z@J$E(=6Wuhpq=Lei$Nh}2*jxM4+%;6nzckC*i%s%M^!WmqZ2(;jhKMT*9RX?GasjX z1neOUbDplZUZ>E~8!4<66{5fC44FaX@#8CR|F-A)`~H1#-JqDmyf>phJaRjHnxoHP z65EPhGQPN69%p`0vHV5$``4WXdFbVOVJA+OH;>rwACs*7`1gH3iNqa>rG#K&H-Gd$ ziJIFBd-bBlDIS~dP0R88y>z;xiK%JL!ry=e z#FLW$3KV9Ns{{;!2K+(3UOpRo8Nno5x8w3rzR{3OAUyAf_d(Ac+knJy*izgcFwy0*46G>EZUOht6!=u_<#;P5EziEu8ND@sk| za{dl6#>NO~z(^Dt7#OJN@vYr`^@gC$^2f`@jeZ83XvVh`Z!&s&HY`lm39(s+?|fdO zc$cD?789hU)NaW@KIXHEuSG1j+uS3Ui`|qAMu5&lgMzq%%m;QRoeU}z& zMqR*If8TE$7#)pyE7p`C7PP`xFvT@yOr-M!Z(v~D9#-_(giJpxnDnHNhjZ;#9Pb$C z*GN@BstMj0-D>9>oFQpR7elUh{l2TsvTHZM*t*Z!g}NjMtv1c9HfS_$OdR zIDWo&!sX|*|Aw(?F046+o4op9U}$vo`NVDdc1*xVAtR=(Es}BPg1pZX^D)!ZoROt= zC6e*9|1=2X#>erbJ4q9SNUp`Ruv`zw}C@m&z zR9w&~<;9>$>{MUWH5%DUXDN}H8I|{BbZejfoSmKBva`6jXy&nJovJ-vQj(t!Yw}oW z<+-Aa#ul2&K75ZLAzD~jkDBI{c+3g zV^hRQbcYkmXw#JIoJieFTQaPTa0L>~JoO{+3gz#pkbrha4# z_?=nBzV-kd!KZ|v5AqQDhx!t7a;jZcYbq<}(eZ&wbA%-1^GsrI-xL3ZTqTBq(-fUX zUPxCQaig4S_ZZI?1f&cWiT!+1q^dWkH871PR~VVXMSg2*>$j z0=Bw}%I{d;8lPFQZT>D3VMj|8@S>=jshW#@LCxJzZ~%_n&9LU-Bj? z*jBX5_!F*HwbLj>+^2n{n?C&Vlo@>Q7%EC5AW$W6OZUoch^QLjpsH;N^XF$ zG6tH-tZ>{I!L&U9-`MOw2DMRSKyi`j_$r59v(UCH9CX|xcfDoq=d|2IbU3m((8!2 zy=pnBI%?mAqi`QEH4sh7{l&Mz#IC0fGxT(nD#j>~XYDm*=Pw2^dl2Bw$PTP9J z&gX{Rf!WDGJdI~q;boh+?|grI^=hCZy#KHaKF}8K*Jy&W*;|dPG&CJ-hBc$ciSKNB z(xOKi3H$sVA7X`X+pquN9TS6;$gidEgEF*Iy*;m4z)C3X^5)`xwGPMb(ovgi6V$MU z61C6B2}^Gd+EVg*54Kb^d~`t&KXl)v*8as?rlz_sp@<-U(s9H#q1~6dm4DvkmLhON{D+Q z3-D<#_{LVh8ZWbwELw%4wD_|{&!}cI{V+ZV1B2I1|GF&a(jO#k{p~epIUvN0@0?U5 zA#`CdoFxT#H?n8IDi2(Mc3$)X8b>u@E=h6?K`S*)BQqp9jABVkk2hmyM5#+$jd$7( zk*ON_(~CiV)38sqBz0ipM=a87?mZoar$bY#74~AJm-s}f`@_`p7xzZ^sE;{o_ zkMKxibL35M-;Qcw`0%yjVv3BXihZpLpm<*JW@0#SUC4dr@W~ka;hr5BR6t8B0+{L4 z;l>R@B6Hzi`8`f0o!s6-khJNG2l3DWZ(SOvQu8$(d2#6q-Xx%2_SZB=h3AOVh1ER{ z+IOV}Srd?Ge4P?;ilGdA9S5VsRO)!)cAZ!NIGT_A&>LrIHoZltvEI%)u0Z!-?A9Z? z)acY_%;|u%!YIHofrxrZ4_@D!Y&}7D|w%A2$bSO%>~*40j3<46W(F&7<{Z^w5){>Y!@>wEw1YRd6X~bFLix)SEH7V2%86 zoCjiqL=2N(CrzP^tg z(bqgL@EVplcdX#=*0fJwgg`8y|E|)Bz?_3kV2fMr>D?v8jHq?FJ*>cu4eJ`h4%UHi2eH^)=P+a_PSY$5j^2V-jv5q z8U~G#5fe-cP~D`?rQ6R<#I%ORJNHH_4y39Y1`OVXCpN6;Z2gvKz^~T`glg2X1Upw@81x9d#Z>Ox^@W|T z_orJtetfxI?z1u=f5%^Yu{W?_!inqRMTw2z{4tkws4sbJ$sMCWw4UDCsrmMDB$4SW zDLqq9^IIJ2_FJ9-l_|eFk?D|9f$DgS7-ULZ68HjVU(%_E>o$!>T>7O7jcR~<3AYA6 zdjom(#NR1u+u(=1PGf<5e)u2pbnt7YJq6d}Tg+hv*NJO?qJ4<+w}1Y9;d;V<94&U3 z9ZVW%nla7%aD?2BZ;r~GVuthRa0?-_t z^i@+Yh;qDYDJ>b-es!C47&2P9vBms?RSFxo+jAJ(>v9G9{NRP#_77DfxR_=s zI;XAeCcrtm;zaOkUsxc4R`c&?WL}OBaU&-CS8&a+v=oYF5U5%ms8Ga}4(f)Ua>9dO zQrQw#-ShU~fky8#S(W(p2&tnZEx-caCMw;Mlg7zbl!WvPFz+vR#DHJ*R}PhGMer(I ziEhUyx>>?IetkPg@w*FA<}3= z?!Rm(|0u}+7E1NsE&0zu|KD-S|M%$pmyAy7yCz-;KNkF>%vZR($CNVDTJL^}NT-%8 zA5fktNbRMRAbzp`ftJz)4wXiD$#*r&2+~ab?f4-Ro$q#jdwH#!avmM$Ji#>o%9y_o zjH(W&?v4LGl)z3?Ua6Ps#b(cym5H1<_CMc0GOlfdLLdtzp)J3}?M7)I?}R6%y!$ez zJL2-VfP+|a#NvVwcDSsI7p`x^Q3YxwQKTRxhcf8Kw#1B6`aWt%^)W44E5XyqI{FnR*3ZcfHCa`@LG^Bv-M zTV26(f_Y*pDt2r4n#;y-`FL^>Wh?PV4wk-gm|W|-^B1qV|7HR6+n#BFr+w}}(`mz-dCX~W@iCU&yuI9_JpE`O zdb0B^$lI6aWs3*50L%<|<^i7g@l@{Nf}ASk(B8!%qk{*otC~08vqJ$+|E@B)`m%xk93XlCyKO&g7Vl%m* zsr*Er|9E5>a|as)A{GCC<3u!OR1J(=ga`{ zr9+Sjeai>tqB0)3kvJD0q1ec-oK2})f;{rzOmQm}hhFsRt4Zpwu)tfL@wvswa6W}g zg{H&vWu(`Cb_ND&9D`IRW)gXlTp9x1lPFj?dh;vYt8O;sM%~J{x1_=@zU~ik!%nxp zr@75Q{g=91;2mLIChos-o$+fk%qILkAqd*=<3dPOhlq>N}hiJ*F~A-Cj}7VKaNRU*u#enUkfI z=30GX*OBD!g)XP z*D><5W$+(*iD&Pw{OXsn-~P3(S1I|ol1Ev5;^&pQCwh}|@AtU?$td;am^F^J#TaSB_Z zf(3RUX{RKSR!$(kv%5O*7}ha;HTkyfbUAg_B{#RY+?lkTnyhP#Cy72FF9wCw4L$%mYr;v zQkn+c!5KN9k_qo=$JN?~cpbfiT>^qD0%5(qR!+crH7kBA6~2+2NxU%r9NjM`z3^4x z$U=Ba+x+)6-VQeBK<^4h5VQ5q9(V;RKt4MTdSBG{r~dX3F86!mvg(4me2~kdhr+h} zHw&_Qv^2nL!aw}NWQ?hzx2wp3B73`@^t7ibra;_pyG)S$502iMR|DM%u96mXhPNNS zE&DJSys^DcS8^MYl_&=uvX zB>COl?MeByR{64LD#+@`uKlKMOTR|gXc4Ubg;;?R|Q$N-WPyT`z{%z>w{ zj^-2@89cQ&-Jy#&z|W2~`PN-9>YsLs14^Y*pF9W@Xr`kN-VU2yz>tj?c9SCh#(Ss9@CG2gqlm9)CLn z0(UwiP>su$lH=29wPoQn{F+9aR*x2^n# zAdRcjV2an$#&cH20(}_klH~~Az#x^9X8dij;%-IDiDKpb@68Hn8ZHTFl?gr>-$$Zy z{>|e(@1m>O30+30j#&-|duj3%7n?#&1Hos}Qfy(U1c$~_{x}GmwQ8WPL7wd%`Ngz0 z|7>KyT2Ww42c=jkiZLXTf_%AJ`--^&R2}w`$d~>KIbDjUSz9ElfxU`;j^nv>M*jw8VOIm+rNwr0vcS!AA@s1YgPYZEuU4Wx z9l;c^-zVnkphMgJbQ4yWL!dqoT#sABq^CWXlk~2m+uc@O8rEqL%I!Iyx~3HHOLb%- zCYkqX!XLw+b;_%5z8h*@EN$beN8gIsN|f-PGC0VtT8xhWrp4yX3K49!A5z70mfG~x zYaBkZSAtvilg60T;9MFVaDOcu;p|f1k<}Hq$x|M)udbq}|D4F~c!UV%&{Wc}{WKg~ z6tI*^8E#5!#*C&)?0mmqT-hKH)dNdAtYK*TLt87cR>R}S9hBUSCaZ;{At&o{Y9E{J z!UqpqFM`Tl}%=?wyx2%kemdA__bI%{Jbq zbA06=MS4!7JLRg_!rSb^3d-*wQa5zYV^{HkG&${Ld{vf-Q}U7greb7QZz2*mS#t@m zU^6ngH#!@zyc&Y!M+AaNx;0p$4GfIs5oznix3}F%B038MHBQ^yHZnIF*Vo`iet$=@ zlJ+}FD^X5{9^P$NRQZzaCf}yW{INp<`G!cV zo!my|6tGAHfT82#XYz}(_OxX)h|@+LWRY! zmT&YtAA-&~@4Xj_dkwZVh1ng5w_?``oTCoCfBM*0>x!@Hm)YX-6sh6Lo2Tn5-bqmT&$4@~rsE9^*1{s2tgc z8zR@_)vmk4!Zfp{+Q&YJ6v@dL)J)GwhNX3^s=2I%v9O$dzL!~J-IHN!k6Dxb0eQ}B z?{eDT%2BJUi?LmJk}(Lbos`Z8^O$9iCoK@EMT2Y3XPH5=<8c6S_Io1>kXcCP^9CkL zmgi2htoy`G2epnDvW!ROGJEiEDAq?FJXobrQcOE#y48m-rxrUvj8qrOAI1vgXDN2n|NZ$-bd#)e>@#O2P2HdXS^%oN_i<{XHLR6Zw#Xs=wHL|-C_9wl#4yyM zAc*~yQG&VvbX6CnuWOXgzorsgO)08W3(hhzmeyjINE0O=In5fd#T(CmR}*753M1;eAkx|OM4VTeeEy+FQ(xllg8SFFC!n$FRf zp-^Ufl-d41u&lyksT^2^l#I16@bb#9G}O!2k0#rj6k>-^2=}f!#OG(Jx%)7GN#$|6 zLo)02;y(z#UpN3v*0>KAK>x?uS2Mr5cF%diVKs*J5^ot7yp4?A8r03I1Tz1Ea0|n> z#!^FtsoQ?DZDDT=S(UlAR#@!8)RpUIh3w*C$Of*20}h8s0#!}?CLH;?$sx#hX_V*R zDsko#Q|*A&#R&)a)~)MqzRk9o87RIAT)8yNQwI(-t>tF&T>5s|%&o<-x#_30q~{o{ zzHi3N{K8gx&gJ#j-u2(RdJ?oo4Vbngoq&Vqe}!YO@~sb@%gh{CDDwj-+;C5sWrJ)1 z6L|P%0WeO#f)qov9bljV$xuL_(+xvkF4(mW`0q8&3G#XQ&o(M4849cm% z(byT$vGc#ax=aBy8e?$c{x7-sOi;~bpayQFFeLauW~vX&fCiBvxN>1&NCwwj3=GFy zAgQMZTvIYINC<yUWB~?jURJ{XWjm zdG}HZQfBX?YqMHg8-yszOCTfQBY;34WGTsy${-LFH3$Ux3=a!@BEH4!4E%ayE2-fC z0wH3&enWs#(r`hbcOa>cA}TIvN6Stg7`r5JrwXx+b@3l!YB8ym9qXG=%<8|^v*{z@ zF6y^-;0}+kpPY>1^4=l`ZRqCd9!Q#wX|b63^K(9ij;Q8H}U^X$o|7^yv6BNHC->$S3Y4t#^O&CDeja;T{)c zyF04$b-7les!hcIzN5(pCl)>K^bm|VJSe*GoWbuISYyd`*s_u>N)S&+01f)Va=}%7 zE%ouEap{*C4^%mPE)b~M4J*)OXx7+`L=^TWC@HC6Vs(*JROowB5F9!>#@PcH5dst` zABoGKg_@R@-sAY@?2V+PtY2|4;#VuH=-8a@BOV8cAB^6&4>)3D#3UflH4(QTT~av& zXlY5C#qd_x|IX|3_RUQBDlH?I<;>gZVpSN>@UxYma%`+z>kUm@AH;Y(!A!|~-i=!j zij`ukHXf*5fz6AfkwnDwTv5X?j;(0~AC|&+I6|vVDPI`|B)l65HhO-fz!vHqN-FPZ zdkTF>!ODtB4+L$qyhNU6~BArC7v@}W0Ao_ts`qw^SK$DY`3%`YiPGEf9PTbuuZI;hVU_BZ2 zkd8N?xdqyA;!oO4>U%BUz$@?1u4WiglZT-|-Bjcs-a@-Hy0=dZD4nOsl8PGW=}m$& zgM+CvC8cFM2fRmv+pLHd>O>mWSj8u2i`6kmqVQl}(7kvq1CYE4O#F6k@QccaFj?!H znnd10Yqz2KO%{wv5tGqwg-&@&-6T!0K#TmmX_O`PFSj9u_wyOYDYxC4Tb*lgAd$}$ zEWk+vwdeMY^|YmOS{Yxmjbmj-!$bH05=NMwClU&*Cx#wcGm(C5R|6RV3Vjf>#>evk zHP`5Y69iiA6vSn-XSQB#2$G1(*l@bFtZ!<}_PDE}R%HdT_OI=)N3xnolagOH5RH67-v}A`wra5^!1d9jY6o7-64@6(>e0jW>b5oh(E}M>i?{ir?^RX<0(U z#)c7|)Gjfr8{M${(AbACFfdS2T_biz)>=_n9iSj$Q{Tw_o_ve3Ot&TS{_0D|bPpcZ zV3L@$Gy)0{4@4mhAX0_u1=wFy%0uVoxNa@aq(SWgh&U5@F+YDj^i9lzrghCcJ^eq-s z;2_pe*YpSsXuMX{VOXyMQ+Q!4=WMliO3#)hAnVW1t4qh(pK$}@jj9fqv?_{bZ5mXC z(1j|r)R+kIOgP`qxBarzinEoAvXp7j9w>=P>ufeJ=MjB0g+F-kTdy=mRGCP3jZDLM zK3uo|9N~+O{{aswe5(yBm%{$Nw3GpxK_i5V#pV>;_98k-nPT7NgX(UueSqwO@c{e$ zEH+UqM_1p_@MT33`->R8QgQ4d1OgPs;4k2C=c1dTba-n6A|eP_Aw5oH~LH{(LL8+1OZ58~LJ5 zMn+~?t$$|kcrryyA#)1_LU6urN09 zpV!VY1MFyG+L{fdsoU4T?Y&K*60|KhdQ7#A-$W7f_mVgyt(~;M1uIC7y9=jVaC)##6ZAv@ot~~^ zr15#9`MndSjO&;j!jPAb3=R$H`NctSdN54~^2tiPe$V52&#tVTmp+#vgevBdwjoxsnktQrYMoS^QCFju$_$aKqs}!FuMhu^SWr9m6}bGqIG9)nn}A z$B%b0S?Co`L-Jo&Yf#b9lmH7EJF7~IMV6amdq3RvxzU3NaAw`J@kMRrU*6~2ij-=U z&)gxe)|jP2C|!x@_- zqWshsj*Otz?ms5Qr$jw~`Pu=7{j`Op_goEa(_ z3dlOwjAnWuVEagyesFP#@3|>e>|fj7hQQl`j|%pYHo5gvqc%Q0JM+~>0i^joqu@lz z6At~}8#s4&epF8j$5qhU&JHA;I~-uH*G5w7kpSN~pqS;uXK1t;W85oA44jz_h6J3J zi;Ej6rRKf+*MWj_#CM@4m3C(kE;PfJF*=|5-;xW~x7IhSg2P&M=?4qBd>?l9jF zI~&WaXmwq@J!VrH+613jQ;)Mk703^wdZunKJ4XZ{L&KGKur|Zebnt$2IDvo@)rpJ# z;Rg-XHjqf~-P7GmO3Aqi7ihtXfjeNlK)FpJ9nXxL#cr*l_vuqE_UYQXk^h4f;C*D$ z7jlrcMH)I(CYi!!vzldm-#{S)x=2=)C)m=M^?4!(~X0NRf5ifbrg+A_i&L#J7B`oi(HKEXX7}HA1M$?P z2vmGVTU&c*fqmt`Kb-NiFE_c4DUWeg2t6%^w2cZ<_`( zEcHJ_aNuVBzjca`g{A&5y5;wG1@OrTiqM4)j3j>t|5(A2{_&G#=i2wbjrJ)a_?WO% zf1Bdn!>6*$A5xgP|JmGdg z{qy%qT(t(yKiffUkv}gH_-CkQvN!+jqRYyn^!{%br4uwL%>Rs{8wZ6X|Ib15R)qXJ z1~QMO!atvBR=@`Q{i_5P3iIzWs_>B%|7rWeEc|yGvSI_HZ_=c24kCZ|1MOg`2;Cy| zw@)zrsH0EkkAF8U)eC{(kMVbuL9t$_{~o{)5*#(GKZZE{Kj%@+JoI-F1w;awLy z;O9jtXE{}aBRu_+%5}}^N7CJI7-EIU+_?~$76#M^Yc4k?m^#YU-Sq=y+_n=Zu(%RP z;O2Aq)ylLLb!=%9eMG+1FAlUPI)^vD#QTI+? zI_KwU%By2-xaa1xb&v7Q|5WJK3f0B>KAS6+WAiUB%TkR}wi$|zXIabDH#L~L>!F~a z6zXgW{I;0K%!hgao_IACM}Y zy3dEvOu+CCYM6eDrIMirW>=sJ7oX1(cByX)JA|phUq_4iaXm7|?Rny-@ld5i&Fpl} zJyoKM7l3%z-Yf1-t4d4J+4a8CWnS~^QsdADxC_??4EgzNw)~V~>wQ@ho;{o=Z1{}K z;pS#U`A;vutUtcgYuhw<@$Yze@WwI%3>WHz086dqwn)5E4FQB!AW(v4M5fso+k+FoMLI`rIv*G@r6XlJO^g;up&M#;L7uOGEpY-K{ z;9^LduPq#_U&|$Fk-$3-~6pdpn6qZ?oLQ z^7JmRGS;u2^pNYgU7`B9pINSfHNSFg3@?@+-+^umMLaUyFSyYBf;DqAd9#Teru>HQ zs;7D!$8CRUS-aA5NVg;5wwRv^p7hzDZdpXn z3|3Dzs@}mDF6Kc2%t*Mk!}FHz+>}36ZU92O)}=#j~_oiBN3my zv0mliuhLdp?fhJ4-@2uZhjG6_(Hs6k=nNKOPo9a6Jk~V2pE`@p>iz+sO#S{ng|h?I zAN7n+Ex+~8q|;t*gy@rB!Y15{zWLWm%E~8}R9^}hgus0)SIZs-Ms^pP4ef# zfksj+2Vf4naX$4XKUTu|WGwUMSeG8)HLtv}R3B#hJdW;&aAq->NE2a|nz(IVjReQX z_f@Cuqw<=Zyu5hbis+SJ+`N!yKzZwrLYy>sKYivGq7|DE*S05W4X(pkm{=1kDha?G zOc@Kh-yD(gSQ{J153E6ht_itmHOf9+v>;8^eVO=NMGrv9masF>FX?=JDO+?RjQ4Nk zVZEmanrcg!iil}!T`zP`$p1U7$qbcm#D++ zIQ3Dx;hShxM4dClrEHxZ2$#Qt$-@I7bjVLLLQF<2S2I1aP_2&R*VLEDv4WTHR7G-g z05SF3#)~t>+2~-lC}d{@UwY=v7mgXLrO5SMs%*DqHS@_zt{q10NPwR(Oc8j9`nj6$ zF>EJ-96~|Gk)2r3BfO%*wosM!(S5y&+*5w(^?p{Lnc=jYkGg8pAJDKEN=qBC?BN!_ zog_VKwdlGLLAEDldis3`K#-nEf7~TZsXYk&<)oSOQsxs{sMyZSaTQoPhKOngg z#mtO!ibG(L3&@M(Hf!c8Mbm`K4@fmtB{{xT10jnh z>+#Y_0_yt$Tl>vtV?v6-9Bl;JhD2wU4ll`0~RSxQ39Sa^05R2c3KTud659>yiHM zoWaLOFe1XmC@th8X2S-*LS}oX5z$l{^R_&_*d6&93+UcL!0sgRoO4miKlQHJ5loea z2<@1sD&^bo{;5^{^mrQaTTdd17|QPD6`UY4;(X^A+6@_!&PT5yXtlZibkwq_E|Y=H zq*;Z3fz2>0!}VnjgoHOIVbhYOmTTI@2|G4@=C4KW8QXY+M{9wi!dZwpL;r$d-vDVg zrqg{pphwEWqGThqpV}&b>gijXelPu}MEj9|Ruu_5$vfqWYQ|>4`cwe!7E9E_(C}wD zp75lN1ebY4SeV31hmkG#`F+c8zIuCyoSjLCU9&v+O;T~=v;L0BldS@vk-L5<2qxue)SjxV z05pmAs}qZk!LZv8eSRWbQ%3?UPtv7=0R|HQ#Gq%gwg3eRA$|%ZK#~XPZ_elhN5*{9 zE>)>)Hf}J^YRk)_fQJl>O8)wr&GDlO`?F@Pns(xVvH4<^hT5v1NZ9U3EG(6u&%~w> z7j7C7!IB<&T6cH1p)@R!mqLbMRPn4$=n(PA$?5qx_Qnk{cW8aR%K#@vFA7&igId@1{*z94k?cue~ zRLLqf$3abRa8v;eQ;xFNV_&pxkWjdsyPF$1x^~iIw;OAUJ46bYB=F(j;8eU7v~V$?A=(~mg_(53XG-EY z;Sf)m%D6m=i^JGLu^7l0!E>I9=y{H;;|f^{s20`LY-Y$&8$Xqc<-ByqKIloNEOT>n z?_K2vN5;Z-u6lCxY&^v+)W%-JHNLEoS}!#kRX&Z~J3)NVYi3Pmqypo~q(>c5r&~i; zlbAiG7`Ww22e9qdc$N_o5mDS(ym-k--GBcMU9w!k(dGFwuOb-$Mj9Cd2JyIQTai>_ z88R8V*z#OlqPC4b6k-W}&z_AY2e`fzk5w5RDiF^-EOwoe>=vKZtDc)Oy?UNS`@*f& z>2-n(|5D#KG}KmFdPRn=11#9jHIO7%$CTGn(R7%v7V}xVLZxcCLd2886ZXT06H{j&0an)ba zU7rlYL&9XD9er*t&*PhCExByER@>_9wQHnd2n8zeif61leoYg^#K*>#ZbJFk7+8>` z)esgDw zHq60{KLK_~(jU4Oz_Z?sv4(7aZV~Fgtc04B>rRS~^)sRx%Sx6fZF8T!c7u?fl?Tft zn!Us{EX1(~MLqfJF4u@i_Muo&Ku8DGE<~Qj#3;F(AMt!U;R$|Y6a+C2t;mqd(b@5y z3S=%WM5m@gP)|Wlj{B83DJG{i0I;d#2yX5ji%lRx!Xq|WA)5}?-_{e7tSQM&sRZJv zBtA-Af}LGKQI!^p#9x=1-w)-=y5AAqC` z9qN0Yk=3uW8)6J6VN*fkZgR37xekpRYz*DxI>SD1On$SBXMISP1MJMgo= z>~R-*wV;HUMAt}<80*www_TUr_JNasbt{zcQ7SjQ-#eOcKmomeMyAH*uygkd?}e$? z1J2oGC?0z@f|x}ZNl_e`s;U;15ET{aEbhcUm222EyQ&Wedp|x8Ih&5P#Kd3#lB6`mcx$>q z&i+eJizy4lUL)Y??j#Z??lhi#7-R}Zvv2tf5=Op&B9s{%aPN)XeNU&GcjzA%8|$~a z!#Fx62#^(akKoCTDs4sq=d-i3eJ+6V7{r2w#N4mwG$sPLR?G(>P>AjCSUNnu^6L&mK1DGzL#wTB^dTz0T^GP1JqNb1c-vtGFtY?jwx#7gtBZ{2yZ zz7W(0TDWiCyrITWk=8C(p}pd5GMlMv8_SaEnLFL^z1e;}x@>#)c7Veqzd;KCJ zbDM4L!cE7go?lqVF}*b$Q}N z%4X8b_6DO4yM>bA;6y1zJ!U59og|-u;akQp@(WGGBWaW^;bq-$A|tk&T_pVnZWu|Ej}rTn~i)+*eG=XN+mL+y{Fj{@9SQ>X^^Qf1CzFBO!C#*P~g?h zVFCbM-~rWW?70hI69AgoIh^=8CNHo={`kNYj5>bVHi3);^lJ1rUxEN_M-p5xIu5yN_@=WkQE$+q=^Bp_0ocdQzT`HU znY%lzt{XjXQ|2cil}JAHw~gqMSKRM>+vPM!l>Ht|drv=J0x*rLTIIiDIY`LdSYA#GR6BEQp51<~I~< zzcK5jJeRSPsepp>$}hDteR2xmDn~ayM%;B5P41FAe5r42)W3~>cKz{w1Ue25MLG!E ztIdhc@k_bTYTvpM6~z$8FLe=(^d0s28m~>x0ZDUCPR`gJ+F9$R58r*|uQlur!X3+E z4gBvzya5A2kY;HF_BuA#1Clg_S~7gTSPsV=zPE^6c=qCs45(uZIzy zNMT$xv)}o`D3j*jD_+*`_bLq0?S z1$7w+rvRQjdtdFr52&!wB_u$2elDm8mONT=P^_}v^AR*(C~S>kU=Kt-iHa!3*|-=W z*_CXi^uB9ZvCW$-kQXj;6omxOwdg5_0ks#;Y1v#%{)}d3RKvH|4qtU2a29GT4A_aX zEPnlVkMg3rXRr3w`muhUTuG}V;SO{=17lDjj}5mnE`N7!?3kRk+(GRs?#}}8UNkWQ z44~!O?rk<3ii|Bj$i4ua-i|+>Sbg^AxBm z^D>TzuU+J>1=8P$w;_Lxi03LTcqjoa?f_D`(L)9{mH=S%I~xiwKfTE=HJ`3}Bb`9+Yg6F) z<(%fx<^o$L<43_8M8rPtB;=d3{?JrX=eH!8`lmGsmYn$7BzQ#r<2X#nt+jSTD7k$I z4Gf;Yp5_aT$RW@`DhP~0PkggECSkpJTbf2!RSmdYrtqBQTHyiS!@>Y>5<4pc1FOG$ z?ix$=l2o?B+@SZW7eXT`bQ-&Wnmss|ijG;Quch-UgjQE$++QDrV!+kqYD~gXs}#Q5 zdA@(ozdS-WmD=^^COm&kA_WeWSSoP{8pcrnMfcRvxvSgmz1?-UbvQmHQ4Aw5CAK$% z7M;MifOGC|wuo*wdwv^5=}S5~X%i)Oo+X<1aJ40RezXbxN-Xt+i42db%A2u2(!yKz zzkHMdjsuEsHuuxR%;`0w&jQCv7uO`^djGQ=Jfj{q05*9<{zzr4hd9YdDXpiH?5qXZ z2*zQ)Bj7BI2t@vpx};k=n#@M{K#AfXsL-AQ*xKv6(NX!&4Yyp5;No=t6Vra8qd89y z!0MpS0?^|U{Ny_jLNM)zU(1Qd7~CPyS<}}>J(tGVx7JuAOLuVt8pX)N`f3l2Otd6_*-(s2|CImX{V)M-KPSTAo>UFPXj!1^Of#G_g5#O$`mrehet<8%qAP= zvdP@|ewtRcj&upq2?nJfO*QUcf#LYoSdqitd}(eyecTYsx2bpGL>!XZTrR>kM{_kK z3Qk|ha2{*Wj~CdJ!t1y9_JXA_#0~#^k8$gAKUAhoh#r90Q%QN`cV}Fv{Yr*M-Z3eIrmK;N|T<-+8 zT5!lt0>E-zw&eZQ=WwRh6o`D~Hx6Rr;@Mi@?0Tn%vVq?hFPfwpubA{cVsTh7u|chD zI#XeLtLOsHIyqZhZTnzSgzgVn_Rt%w>*2+ zreM@}0_9dC3tS@ghSp~$ZYf~MLc34Z{2jZ1LDvUbVfENB(*ViHaftGTAL_2LLXlZv zOXMeMeCBb`Eyt!1NR|yen5I1}wVBNq;eskk2hY-|72%kcYnPNMup)=`$e@rKCs zcc9`d5Wh?`>M#vtp}q1N?H~#qataC!NAEi8Rk5%ecl}8KI0px_&xL5bg$5Fufmp>{ zIr5t~l{f85Q0{}x;gmLufa3wE8%7#xr^7$G<(UH~o4``!VdBfGrUE=Hd3+eE0zQ%4e0ez`h4!sYPe}Q6Ociq&7gP zgMc6`KFKCM%Tw<{ASvp~&|Z#FvO$rIr`w$%o1s)wQD#fsIEpzjs*{#bv?aUVIs+3)GnKv_!!`!sH!1B#_atJ-#pf z+9Cw7uCFvb41x=QCm~;S8UpL<(^5s2`uYeUyn6I4jl#lSn$iwMFAfGz1mZAup~jJn zPjwTdZJnKaCe5>+tF}caX_>_@`x6M;57ArdY&nEml!xN;Xyp7 z{+kAkS3UCoU=sk4W->e2u2%;fW!i1YK|HU?!km(5s1m;HN2VvF^{~ki)W2!QJ19#< z>Q&3BER<$tsuw7q?(UC3@`eu8;dPde8YV`(Z(^K2(Wurt$*@UC8!DskFlZ+B{X1SP zPhRAPNpm)i#M$cvWI0P6l)8T(H0Dr-!u%FT_4m`}KK%cY^E74^Hu%K+_o+di5|Mus zpdFz9ktI!rSDbtKH}Q#W`9D&o|9v7b%~J-@Eg-L^`9s6M^Bqu-0DRdKr@H)2ZvHPb z*39pP*Wv%!LOkDR|NlQZ?yjFc1wo@ZpqebsH05Xr5V{IaCe7asx^Y$hC;J;HXY5EH zKIW9uwlX?EC&Bjjus(trXZ|-S^Q~UK{=cOHXxsn02mUV;@m;<1xzr$1CncYEk{IyO z`Oo>VphD$XdpeuH%e{=O95_V!%+=wD{`Y#dso2uVGYx$peSH7cNo+Iq4wl~u*}C>Y zVM3rpcSunN`6DR`!{Yg4z4jlJfbP%o^Z5u7i~~*nLD&mLWn}^+hVk8HV9PdUdo@nt zbrNzYeJDI_W1leDKrTNYc#+bJFVE*FksJAMPkvvLQ^+)+uoNm z4DxXdSKzPl|5d%qL>ArF1d&g$qY5?XcQoZ1?@IrvA8=FOPST98;}@P#PL=;FQkQCj z3MT8$l)@7gC+0D)g4%z6u8wzGimEu*4CLy7GS>G(@e!-{|LVhelrx#|fP&S@Yg4fO z2QM0owjKy+!NRuJ)sPKh%xI~3h5cJuFlh7Ndv^(f#5^qR{il=0T>dYJm&?Erh&vHKYghGMSvTaf+{(}nGZw>o_-xP-fM566X@4+?aE zbdP%$BITNr`Pe-_0dYYYC06+Xrt;>}^|9*9Ctb6FFUUvZ2mvhub{8EOU-`Zb8a|}I z%zvILRU`&6J6utg%sY1*K8(sGwkEa`&!8gzQtMo%8e?(WA(50<3N7DXxrhkz^y}xc zGX>XWCGnxWut0%M+35+v-jA-e=g9l}1GTAaN%k==O&QZD_?`KvngVh6LT$ zSwwEW=yvvKIg?uqH&Cv?`192SRQp49=Hvn;G`c6&r{4g7p>B|j5Nn0Ca5yWTUGdq= zA(dw~wjpKK12r9g8V-aB|E(+US>SXzmSf|v@y5fHXJI~Uk#}Ly#Db)u2RPVNuX}Vc zL^qikzJ+-y$oLXn_m7F4pn&u64`n`jq$DM`ZmicCfV9j0i+vgkdY=MiUsuXnrgUAd1-DuWnT;TU^4GE!m0f(M=2e(N1(@n^BV_R}7r z+NP)CJG4F*PITHCM4q}gM+(KJ4SrC_B#`a0?EJkZ{L(!7A>(!P>W=SRD{rtwvMha^7Hu%4B>Q&*M_oq(h$e^Z(lkIBu) zjr(;|uk5=X@mBx6u_J>?qUKxr+VV$W2D>wGACoSgT%ToB2?SKU_T^KdFSSld;l%7s zI&V~7e7yE>jMMG6GtJ*+pD^UFuR%U|MuAz=do-|FH;;J#^X) z51_@U%--B_oY>#q9=r&goCwXK!M(F#YWPUcn$u;G)*}RDKs5I0S=*26?2VhnuXvR1 zXhtwl=V~xN3dQX$04>T}*W+#?vp(0BRC#v#hMSK!%hP|sT%{CU8jCJ8_{V4MmIqMv z?AK9yI8lI)x-_51ogPdSr)kTHhN!=}HfM?n!*32}KlUj5+S*t2wG?D?dz^UJKW$n3JO_idaES<} zv6B#8T{oWGYPz>&oKH69i*5ZEd4V-qPQn(d^Waj1-I4JxG>=V|SD^B;m-zoOk?vTX zg^Q`Qu{~TjDiN|JoCOOFsywr80CN{Xxphv;O-4Q~_7tBzI(__k!IJH)9)Hb*P@~!Y zhvBz}q!sz--?jT<#+o_uMNQ0~7E4KR92G`D;4L;*#NiFG!OVdi zY`K##hH&`d^cxYcp7rDPz=}fLCAVAnRFur_8h2DV;>nH`lm}0G z#A!>iiqB{s3t%WvdxK?Ng-6o*L}2gr(Rb;5$g{m3M2{wy(hhc#-{!%l)5XH2*rz>9 z`LPozRrX^Xg_>wBT3j8uqS{S{?KO8tL!+Zs9OmHwRdexkjt) zL(^DjyzlW}n)*TnyHh;uVnEr5MQ!1^vr+2y~> zWV^m&MZfB#Sgx>{z8@6aKDwzsqijHG23jt&V+?x-1U-N$_jRgT9d*Su0qW*ZoBr~5YEBBg3Z z`ax=YbA^oj+B$Es2!dd=z8S3Ud$T|`cD8?GsC2v$d91ZAzSYeC<9ljr2LaMtxYMOjrX_P(Njr-%P~Dr>e82E43giH)TB8L1K#e*o6_v#3ED2*xzu2mF zV{_lP>bYm4OoRdHekV!x<#wv^@ zZT{UrV|z~Pt-+Kq@XE~c!ycDa;7aW_)YDnlo^MKwoif%X^OGye;R4G0rr&Y==}dd; zzF8YR%PX(A;><@qjolS;befB=BuYcYsU8Gj(#(7zug8k|J!l4CT zIUdhq|E)cF(>}htS$&I6oE&7 z9RtmNW4o>FySLCxdhc;&6zH2yn)v87c=RN&-(_8huOb~=t6U_kB5kFM48pNq7HT(P5P*^N2>>XBu2Od}XU4||`pBBOGow>NTQjzMt&=xD6nzw+)qdiJgh{%u8ilWGG*SA-3 zF_bGUD#Z)am8aEqh9Xh`-4Y!9(cFbw<%?XCg*$=Pa56LGaw=Es*Tp8MPmfo3E@z%a zQ?w)03`$CdDV@3*x2{KTTQ(V!uS0;U0vXxvV=4tSY$9ZM$Y$*klB=ElA@#S=BH(*$ zXpu*=5=vvqf?7jc!;@Veha>&)qXpf^Gw*9&B(t)W@u~Z~oorG6#viO%LG$2_w?LKK zqSv!Dy<`ippgNk>;DFHKoy}Pet)44lM#jQVMa>8C`B73W64N@E+N3PjXCZUeQ23Kg6K#8~9T@f#S$ znplJD*PCV_AD+!1QPSD)ZIaZtQO}d!iR=lt_3AiCUsn-8<5|*TD)g$5>R`e4P=TQF zqL`flh5Rhxyc>fi>{t^Lg{Y|mljFFI>xhG3A==sSX}^Bs+ud^x*&i*!^rurm$l@B; z7y3~8sfd46yTRbxA$n;0hv4tbdcyPDH}?veG-^eQ)R>=Hk1A~2{1T0FTe3z@Un|kC!dB*Sn((?j{|{i2k&>~ z=1?p;|Hw)PIrYrObsKp#d05p3y0u*Gy@B<72%9^SuooIKzO`4>JLB))w)bEutgAmB z^bDfFAm`AlJ8F zY=wj={Q*2EXg)Ds;$XNBE+|=AExso1{nZjgakJ8JSjT+%zylj)Fj`|)beLx-C0AVy zD2V=XYi|A3Ee&lVLVHFW-Yoj$ViX6pF^A*gaN;!`r10c7HRr7u;!Mwi*~o`Fpe}f9 z+j7HRGcE?eHSak^F1ChNdev-2=)?@@*ir^^jMW;s~eXPuF{?Ep0Bh2rYfTb8{D$v{WJ(}L(G|L(H{<2}O z454sh^5+)AF(BQ%2Nv>-oG<3G#5QIHFaWpAohIkgr+E6G4UC)U7!$4=>akSM8Y4m0 zq`ykM?c}KJiGX<1g{j6h@E!aQ%PgW+#+r9o>(L5|HmBT3FK1j*!f5!|zQ{rH!9%J; z=HSv;G0&d1o6MLQ%w8(W^(56)Bu<)P2D)ov>=#A?MeXyHg0Si>3*Ic(KgXG6Z)B%g zi$=7eAr4Upp`xGMZKy7V4N$7C30(E(N#5Ztb*`Hf* zYd7y^*HAk;UcKq=gl)-eBjoyv%iZ0>F!rZ@>OId5vz^dWrHFeAXPK@T*a`4)Q?gB& zFc1!0N9=bQgNGhTZK1(`5YOdxS|h=UB?I<)F?U+u)(f@|DONKVBhx#j(pQsxk2RBP zm}I<#Os<}5*=2O8_!Kh4SDCy!S0;DuUiKZ1xV`Sv2~8sMaI>PP$J0gldBiTZr%nZq zKaEHCDTIIf^=yz_EBbZ9Z(#?az-fWx#=OWXWgS3IC!?bvA2|v6;DD4FCrim|EkP%C z43W1Li~Shs)#}!Gy56b}~phMFVVuQIYMHE?Sr^FeVNUtdgFakAPBG5RlBPZv2+XAYo!flfy9md9s+ z9Sorv*yBH=Yf1TDva>nUMHRavlXv8$@|V-=<@a6y3F>MSUq1tbbcG+%CNPzeWNF>w%8 z6uNAVuDkwI3((nv0#2;o{R03Y!wOX26Z*6H@&!cbRu@AMX@-p;l&^RP@724sc;-1j z#C&Uy%zv?9-+!t!Xu7RSO2!b_>3(%!HZs*yPUu89LHmemnu7lE z;Q4UeaXjf7gmnX@dblLpbv#vSL@u_jOR@XhLbr6z$Mr~d%*%MZoEq{JvsrO?k z_G8(8a}?J8K&E!WL$6joTU)PU0m9v#xIw)SYMiiW+K* zY=kSp)2ik)xS3oGdCus(XUl z?gJS1GFwI`z3GM{dP57-XFbi^h9Y&v^4?(piW*f}adNG1%=#*9r$SeN%zyOtcd3Uu zSb*%Cwv&T>=N}6UG~jFiW76?B;(mHLeMnETsEm^NLgH5U2bu^7?%Mb1QsCZNpkkxP z##I>Jh=d=kKwNXynroN-;!aU7SBvO*IudZwedH@R;n7JtS;Bd#Rib9Cv62rB3B!5t ziod$)It(tRKFCcsd3->FI(-U;z&q)ho|xFOt0lbCB5c^l z%*A0^&(s8grwSQ)IZONjrK(&XfAdx7S|Ji?-kYjso(066GO@(L)tdR|M`Ihqi1O?} zGTrQI(LPqc2j_|P=v!zA-IFqEoht4xt?I<7wImaVC56!g6P0qFUl$Cxl_>`!0bVFJ z#(z|Il3#50smkk|Y!gyh+b|Y)>opQFD_?n~3(zv-p{d;6U7DIgJZ16KC)qjRf$-S; zGc+fOK!^p@6Nl>22)^O;O4r`}hlj3)EA!stX9lr4sN;01UZ{YFu@60MI)s;Pw*)Gs z!BGn}9|M#jC?6u?&zLBm8Blnm74pE2#j_ogJ^%$H`*R`zI}PL`8%-)!^nz!?MdB`> z-nHxR)-~S&QHwxuX|+g5cSHDL@<6qFMaFdNB(xZKluH~q@8HNVHeS0d(x-y4?!j`Z5a53l zP>O22Z2SkwzJWtbwpPBX-FIide*rmVSuX26d zqS)d@z&yY9iHLv<@N6D0(2{wqk=DfGDYW6M=WZj?8#MN$%pbJ_nP(^$Ao~*Jeu^m7 z>M{&2ji`NhN2{zA%v=Zj45O@g`y-qOwnfY#@l z8|GhdmWyIGLfM`hNpZ0W(%cIcD?tUR#U0r`5bYc_uJ|D8>4lxw=xykyH|>5M6lj2! z0^l~*j`W#k5b{H&@4~mwAEys4)-RGaR?noQ3fc5adZNxQensGpXLB?!F9VT%R6&~(G3X_BtRgzyKLOu1Hs*0g9Ht_vEac8!QI^*Hf{+T z2<{HS-Q_jsobTTE#<)N3c>j>@>h7u~bIrAC)v9AP=1s_LFQMX;+igLa`xDSBp}$I0g2ryB#E>U+`#l6!;zfD^pGwC-Ne*Hu+w>O|AER1l9_ z%d}|W8dnkPAx9_1e4zy#oSnpC{S5%1m!QR?Tdm73JUH(y)iNi*eA3rl zxTYw-dL5%tjDYp4Nw8dNE2AQLLMDB*zI;z@K&j!Pf zG7Oi?pV84q?u&X*s8$^FW1hSJ*D~=Lp?g>p5Nms}+!CDf7*tef@wlY*owNE5*%hg$ z_;)n6^6N~O1ZSR81)!n(!+i}nk%83FMLau5t9}uTYPw| zXwX7^RryJ|72k7rGPr<*UZC|_&u6_nX-U{;DF`oDFnPQwp`>UDWE;?eP9*|y+ zY7LhNm*oN5VYH9|KD@q&-^V?<1gjl$BN+laSehgb%~GsjI`Vhl4y47{dcrQJ-S`Y1 zJ$|O+bxB9h#~^{DEG()rE^Q#yZ2sQxN-bR{UgQTRl6#Zsuf)tyMUl84bV)2y{5{80 zt@G8tZ3vPnU_qYs*5pSNOds!h98b4DpGuKeNgH#N8@%_kNYYwTxaL<#gU_;}Kgk{4 zbmV{H5!%lVH_sW_APWj=$JVk!>g9pT?HG zL%7x396`^^d4CHa#~+87AjgcV3APfVc;x=;V(17@B5?1O62N%=wy!0)VEqqZ8SC=n zCgiW>QWjT8KH58_=E=|hc)L7ye&S$%^z+xqXUH;S)MG5Ja4KcMeM3brd z#;^D%?#gItm&<tB3$=s9bnUFC*|tNsXHuI;!Qsx5=b?uugY!)--W zN{b>QUC;hlY+`0~H@PIHF@=YD?_hV}KA)=Y$X%W>zHk=KXoY@P>e|gdW2=X6QIui( zXk<7GJ6BhT>eL^|ypE31vJFN2z^9+NzEZ5JwMwLcxseZ#qbr-1jl2$5Bo(mu2j%WM zgEg~m+a~XY^ry;%vkr9T=sVHUbvTq4XefV2CtA2|bZvmJHe9fLPuQK$Cu^Wdp%S~; zIYQj8JIBImKkf0<(|C_OK&!307CeDnr1a`fqe?^(w$iC@pZgd)guZ|tu!{YL2m8GT`SBt~@qG>oS!>tHI%@2ppEriy&tuidSgoU65N*>8EcM0aXFsVqUrg?Qn3!-ho+MiuCnT5mQee1*%0dp9@e z?Y&2S{@S8nH!nG7*NjtpUWSLL3v!;u*bz;&*rlZ4nkLIwJ5GTv5+8lfe9+z=7YqGj z1O>jnHT@OwtBPZ$ZU5S?FO47cTlX{zaka5xcW|ih;|2sMz^-`Uy@m74#8Dc4qZXAJ zLSXQBF;@5aa@WxM{?p50bg8aq@&S}o!d8)xc?hf8mGJ()dbBnan zzuTl96Fn!g`Zr=KHuGwmg`G95qNUWVmevWvWi|LJy*1QytA#XPCSR-Cn>k@54GEOQ zQN*nC`euE1%SKuO5;ZLflf;%s!i^5iYQkk`C{-M-zn}E@V%KH=bnN{WEM&IAn8V`O%0cVl#hYuD-GG1T}FwUX!yEArHPdojjPb$ucE zQwv*PheNWJI@04K%9s3k$+3mGH^+*2;m`+IIbefPsVj-neS7Kp@7tjyAzKhMEu9n# z(b0WNg|O{`xndbv%10Svp4%z{qpj6xBMPL(ml(>`kG_MFMfj&GuwE-5vb`cFK>mf z9j^ud7zURuw&UO&FNVY_`B{l0WcdO;l{S4z)OH94V=B^{E=|7SHj+r4 zUh%qZFeT1a4#T0IH>vGdR!|Gd%ZsOyddwsn3-Ya5S@S(C>W`P|Gsx*d^=t+&&-|eR zC)e}O#yh`i0cujbwBIL5$P1mjAlh6Tyi_E2o{|>(?G;Xik=#qav&v;3qMlZ_et<|^ zw{pVwhYGTmxN+amsd#grpZDJ=Chn^xtVCsYL0Ku>H+a~boIDt}T0Sl0u{JPykbsBQ zcZD^pEml|s#zwya0yxc>GUTn<*j;)hC{M%&l7IAeILNe-_Ra{ZARA%z5Fx2p9CLPd zyjbtxOCS2(ky&zgUXG@J)RqEOM1%oR;0=|S8^u(Ed#TQZLlxdOEbiZ8WuXhQp!8g_ zR!DzPc)MMu6@VCDcvJG$uqlkI3kG*tpOwMOkA$1Ox`f>E&9--|f~oAvRx285Y=Tn& zZSDCvXMw!n)fne6No88ddY}y71kP3uXKM9EI2y+8+FKSBc$XtT(RBWz!^D`B;M-E4 zwKms%N340evp>wv?zz)vFUKM62Mco6tmwNk58uT5h6_*O`K2$ESbyq@fi8Zv zdqfG<^7oSe)V||-;g*AQRfzPbz_uabzD_q_o0>VQO4L*$^5qhNDi2F)z^-n~*u%}BV*M=M8HQN#M1?QMl;k*(QM@4{J z2*%0!alEX)-4(}U&>ec{n=##F=;E_&J(!KG*$f#_Gm8kqlyY9s7)|~5OIBm|3vqQF zmO=y9qF{E|i@el{mlN!sl|#^Xx1uz{R%#DZ;T>Ns4yA*nnD#P6^3>@2;(_|g-1?e@ zo8><#JA}p3+OjM?EVy&E1Rt&r3K|scjLEQ{YxIwuU!5`49QqUF>Q~))al`fX=~x=g z^v3`)aCTnKtbg40;$2`sU5z%b8?E)`&#r3=oUK)RJ1f;!nztl@}Eu&i^1_o4o za@(3J^r9x7@Jvf@`of4H<8D_WIstKPveU<%!&}0nRP3^hSxgC`j#L+9o}*rdhjb++ zzoobIUPJ2Sdg}ojVduaII#B=nJ&U=z$r_UY8|%I4aERf0$O;;HV^xgJ2Lw^pX?2TcQOD)WeyAZ)=N;k+ zl7N)9FWe#sJ$ewl!xBx;Q)|qFG5JU*^k5Y?@2#w)7eVEzK}_G0@rKT7m)!>Atv+G7OR{d04H%G$2Jvt>RsQbPHf&mxr~by6gR74a z_EQr~M|zP@iU(k4FiyV>cKrs2%ZDghFCNcO49?UdBh`&=sibG}>Q|{Jy}vh*;AkM? z?_);Z6#Z@;9ztz?U8$uL_Vw7yA7Q*&4;Eg8IEk{r(rA&!&`o<~N zX;z@9BxAWzWUfv{w@>oRPNQ{@XtYPG1%C$idF{J{ZvI}2CpCN3_je!wHe~%!2_@zF zn?Y@C;vqp|nm;RvZzNzQmv^vbWgP)G@VM<&be)7I{^V0KIL3XAr{)u)alx-Q1F57w z#uUSqs+W;UDpDU8?y%NFl)tdmB$f8sXO zv^}ag{kiLxul;HVkxlc_N1}qZTusfW#N?1T3V}2t2WGarAlI%ERM=oqOhe|-K(R&N zccp*wG_WlSKH=>mm{L$k&`|FS{>ms3uPu@XFl} z)fGr3(+Ul9tNW2X?<*Kk0tEZYWasy|dOl1T2nBoC1GOxNVb;xt!N*C}ZSoMbCHe)N zk)LmBRCu+Bsfwh8=+O-4-mS(Hi&T?~;dzSJHjY`)Cau0jF#ck3%T z32G3C&aZAqo4G{~)d1Po5M^&MTjo{t8#2^C>)g~%Ly+foEeIT0Ai9`!tg>`1$joA= z``7qe#?;%Mu2FopWRwa8*V*c0EYRv$oaRaYW_G=vni!&0)4}Jm@d8H>$YbR}UOlZT z3V3+&aRmJ3eqMq_lDYOGn&#WOkwJ#uc$@6qiE39%^onC2eh>JO$2Lwo1~7g0k$xbw z*R#<+NURzcJY+Ife9p;s6!0<%T9SIHMzuLim>RvfiWB^kkS`Hln6`>`g?$Q&!WEaa z!pAA6l9qy_NYR(KMkL#mEVVaPN=o>AUqxL&{c~_sQo*2op44Ls1P97{cZucUqPm2v zw5^C6)P5*lXGc=MiyUAkNB+;fd2-&!N)g}x)MgE@OIoW`axpTbZK>0uEBOHf16LSW zTWdrA`3aFMuR+OAMfPo4M?{dY-Rj7Z>_311ITQu^L&u3<;N6bhVvyE{WY2#ur!!Ss zfOFUOX1_)SVq*M=po{l%1biT5nh01BVXG4=U@VL3Ya)(jh-#%!B@IL|u%D`60IXH7 zfi2!#vxUIH#u*^jpL6+R&XdT-*dokO$PYf;7`1G^xg%IcyPC+*D>McKY!Mu` z_vwwS8dz*dxf$J?5Y%42kNeQmD-jqne#+zeSLZWM2ry5qJl3McR46@44mn0vcJM4H zoh(&sjYaiNxZx31U+yS*=4U&{mfj0{vlLDb<}mQvFmPNsg)|p`oAw@-mjeZV70L1~ zh%&dA7X@^RM@!duZ`B7-6|MAjjpWZ1vPZM;M?^N84+;$pP!|M(eSDA1 zl9K2Zyq6`Q{}6p*n5BusSb9hvgJia~O$MfZJM!QK0s%yR@e-_hcR?)G>+Tfq{;IX# zj%Clg`Yb2`2M=C)P$He&^`pDa9%czNsiTu@Mq)4M+tHm!(!R_pthV#|dRbr17PDT~Ez;!;Q`J0*tC^-mTkFHHL0>s03i9g$NAp!BkLCV68m{{Oj-`)~} zl9pahHJF+lV>y)vi4oRoZO;hdV=-3oSS^6m;T|rJ<4

CVHO`Jt(}F-vERxBad=i>K^s}=ZP!MifZ91c?S2beA)fIF`|G;)U z+lplwR|wtE{*fqT)NuN**9S2&jkaqp&-%}OkfpA9e0gsuYk{KTBBF1E$I4@w2A!|> zEC|Z&gM}Oab@J`>G~(4&@6U`Shf`Nt?DDx@bwsI)1Al6vvxINhPwmjbqt(&Fm*9eF zBz0X`57tM&2EBmX)UGwI9M!)~khKmX1-;#(5A5qbA7rai3zn@+~ibKFFKPj0s@IHXIR4w&PzIv=eo!)+bA@goy+g85bUEp zev`)^R5oz-C9&1M6FT;s*VXP?=|SyjafKhM(upqL#60dW;KUU~uQ< z&o*qGeo0092%c%?o2#j(LnfAf5knG~gCm22=BvAwGm_XnaXaaSk2M#rS38q^FX%}C ztyRfX#%4BAigKO4u^DkPaQL2{8C}$C`I8A~LyZV02KIJ-$ESbN;NJ`UHK z>G|Fuj!m!wJ+!kym8iSwe?9!Y1*-3dqMP@%H5zt%DY0aG(GlU$k-oHQ4)Qmy@dKZ% zujB{7iqQNpn<17YZr9gPy|fEHd-daw03b*=^eID8(83=7_7O4sTYy~2_dZ2Ok}43n zN#>q9*NeGo&&9?vQ34pEU+UE1R3fZJkM(Bn5^}VYqjlUJ}Tuhy89gSJi7Jc?(M7^h8W_WB@EL zrm(7p9^=S}gU5!o9i&p5gp`3>abFkBunEi03hFWX74b3Y z_7aURy_WCV>^f$;v)q*qQm+?<=QqW+EK?!Jqh4Dw%QuysNQ?r z;>H7J-io#eE(kXQh@t0S)6txzUfniYn+jAcp$*JNw@-M$AIzDCI1iOx6K<-%hFP6R zzrV2_va|CZT=hYxrEh&Ys#x4FtXSG6($v!PSLeUpcqoVP#pgo3Et!pu2J4|rTAJcL zG3k{ku6$gm>j}EF7NQ7#F1~~t=?ooJh>hgr&6f%z0i|lx`oj;~QuP&vE5`zzjO-|{ zgweRuN6l@F--sA|?C`pVvDX!o!53O-!aV);er#`fc#I%YP_XIp4Cu4sRb$8S?tMAi zUCQaD^@XPmS@5voFdPjC=pwtGTc6tOA{-CNXPauKfz(-zw)x!MF)lw+#Qk!I`M9v^ zhcuL39ux^oMDa1L2pRmJMsLJM!o;+}Qgn@=)Lf#7_xd@#6Dkj`V&{KudT=Uoq`@TI zWavof2Na4O_j36v_lK?QsC>wGm-i_MB8I-`qm1_@YM!#P@#T_kHpa76T+FJ?+ zi8YDTek<-IWSn(M$dErCJM>4yFw(WWO7U#)>tDJIuIT~;Z5HNsQRTA2ox-V_rnhM` zYipZYhi6vrvcF>t_G_`RneZ4F;Jw@xw?NmjZpRyvvV%$n!tBnN=~1`$ zDQx>%RgE+X<+mhHI_Id%c-*kq7A=wcr`Ku?)1OMJW?rjeng+hUTbo+eZ}w3UW9$;~ zm};YmHn>ZKe4bcsw;q7U?UNcLgopBo8CCz`O%2z$G34aN%?Le)EvkTkAWi z#hY8e`VFUrF_>D-@t$aYkw;Rg1L>in_Ur-0Ov^TvP88>Zxk)fvyx&; zGKgoNG>M6;-H?6<)rpS^ZZUL0mca9 zLg{Pl6rMTyM&ZzPH|8Na+TJ~VgeuP&7@mbWppto`x$ucAAz^o}0FG%9sod3rq4rY= zTc5qVtpE^-DU)MUkWrx`j2ZfnnHRkQ!U(O4IIm6*=@?;vF$SpL+ndx1#uTwm79CyX zjC@On@^^MochSbh(yE3ewl}-_OyaX6Cy2K9=k|a^ZCo`sLIMmJ>4DFpLEYZ^pWPn^Vrb)P42%L#^R z*&ZE_$lK>y8b7R5HY|pR_WwjnSeqzNud1$ljSDYyR1x}7ZlcN8%Za{mF9&J>X>QleGZ+5omS+CLP5;Xj~ zibo<*>~BQF(Ug*gXg^~X-)`Cq+dnFwbv*#Wh`5gqLBaQWSr;8~(tb`Id3ifw5&LK8)EpmMl&9 zw|yCcnoDy1=X(?iPnYqQ z|IimQ5-VN%4LbjxU!SKs|#bVa=j69;T%UGA-}`tJUCynG%Y_@0el@`C05REgl8O0pN+lc49mWHvXF3CT7`e zJ5KaQM)&jt?9g&Xf8^!Ip}`6VFGV?%cjWKhVQf;3ff~0Td3jEcecewe-#!uB0g95p zydg0tK`pS=f9u6!j$$VmCgExzB#=gU`s?C!ce=!b%JO#^-*ujrN|@zeJO{TgAks`< z$v=c(6Dv9e!`5$o9kx4d?FWZ+xWB83K&wNI1#5SYQqm*CB@;WOfGY3NBPoT79`d>n z1BhvZ-dElWT4h_{DLYsn5*?GE?$JljOMz)FM9L`XD%=lskN+a9M}V zoNcgz`CI*_Fbu&KSdg4#zZQ=pN8{%$XmS$NEC@yz#&AePIP|p`NMyesX7+l-a*w|n zY*AU{_51nzIcBcIuYI9H!bOspD!hkrzZvLzG^ZiGggq*Xt z=^YU;=feFPH-$0Mw*%7VD!l@-{lDR4-Q%{u{V)17ec!c!o?n;**>^F^C8w?AUHH_B zEGhcx>|W)nnN5KIx=F?+d2RLE>p;vw%qS3b44ZSxJVLp540RtP$Z$&oLwnaI6kdN5$PK2+X%!ehYu)ocg* z!I3va-G3Tzh>Jd0#hT4;04I3o=XhZ~0&DJ@Hh1PiRJB+(eP)NR^3glFP3uV%x1ntF z%*8&tYFg%kjqM%MI6w;E@p|YW$m|aRx+4GJbwbMoqs^zFVQFXW52Vm&!AxBNyISc1 z6ymrul{DBLtQepwuYKYN$E)xCkANp}d`h&30iDtn+Um!9t0NASC7IX4hRDWsIrxjFBQ0qXtLTpL)pzJ=}WPY8zGnu{pW?%CI@ zFCWg3K|nAl0`lA9b5!%Tt4fj3he}7%$ICcUfV;DHh6kBDJFa`^SCl1<9p5mml-^cT zr*NFAzBOvBwaatqIDLdT4KB=u@{*Ki;I~?^dS!YW1*EAJ=ADmG#c^8L* zO=^=fB#2k^y(&AlIeGdq<^H}?Yux*&IGB`?P7w3s-}hrCiU1gyv;^oQm7XW)7ouEG z7X;8Su^!*`$n`x7`%ERG>a zwHpTO`(_UG6lq~$Cw4AO;pIZyBr6-p7I~8EA<>gg#V^Z^6W!vDaR%#k*Y6IUu5`=4 z|CpDbbb7gmZzV}uU#ZxN{!nFvjpO&Jujap~d?uU@7q}0;yxrv#aK$k=dE&LCP;O%^ zAV4L4IBKmFr1w--vnRt#i{mB zHiClsfeAvEq5O_dR42cOfHwr>Bs>LJk{* zrfmrvki)i_WptQ3UOYW8+(lvAk*-^-7w0NS^5dmAb}g`m|J;!nV7=7XuJ*7m%Zpkd z@&V@Gm-FDA9B8o*0QQ?Zm237H#WKfFDH#9zTo2K+d^W7a{dM(rJ3SoE8&HtqQ=~u$ zN*@@G?w|M0Gp4sNcaN&Mvl9%JQG;^8$lRi((8npu{yVgl18{%d;LYXYo!VdfRjXh; z5LgS|NbAYQ4@#9UGT04^A{T@b7+*k2{qJx<+74(H3^rE=HRv@mnICG*wUye*Dp%+gcE2&(Nb({^r326FDS>Q8`l8hj{8pC!U6|p#8raxYP;FiW&sexSm>=ARk-Yi&dfo z>F(DDTfB=^(6shuaQ-FQ@ww#nvnN9+COi1bV(*Mj@2<%%21p<|cajP9B~S_Pm4D_z zT2Mn9zVfcWh&XWxG>P!*52^bPuc*afbzfP7aorG zpXUU|yy(_H?NI# zKw0el@;=f;Y(5924pohCAhWGaGP^OK^7{sWb34}bc?&~fj}jdz+u6^^Zz#=sc{)D} z9Mcx>TM$uZd$0@%ZS;Odj3kx^*K&MZe!|Ju+c#gKiXPj@q%b;dq15;3JSw_Iw z&1|C~Vn+G=-at(+qH!DSuSH(G^<8~s#gcio*U+-r1%^UI#E&_J6IDzpOL$RF9|Vd7 zGgfUP|*P6MxXW;dh>1)sP`~?MmSP?<6F-;sd_zn;HF>+MqrIq6us)r zFdiPgnlIy5D*FPe2I7flfNp@miC0LGuO5u8ts{DS%@$ipg4^z+9bS!iLj_oD`qe-p zDl+ub4>kGS&ZC)+@W4q}F@{>9m6{J)k&5{pX;6WOhI=$9?`LvQ=hzH_K7ZrDlnuAx zCtg56C;t(%JmMryQ+hrZ+ktX>;25DDCWG=5T%b1CtHTLTFOATnlAn(!%S9SF!p8+s zNVfqtnmmJ)j_N&Gd5SnKRt}@z&m!D3K4;Ml56I=Nf3`S0EU2ai*i6$^^6B^~7nk1} zw+x>X7WCzKKK4a?tw@W)aezlV<_4qn@5YKZU3QEaD9KLgX5_XJxjwW6BP&q8dqXTG zL3Z)VZXlRb=f8V2>JVq1H3%dR-L?**Rf&nSBvT9X* z!jqAaX*win6UKS;y!QL~6H!)3+fPyGnZ#!q`YN5j9s$(E$KN%xfLVR2=&|GC>Kd3> zhycvGPFO&QqT0Q)##CGqPn``xcYHV?p*WXMD#BVC9)D3O9h zz!)2CXbERPqhzh-JsVMN!>c|o_{y1(r$2*6!9lE}!&*r`6{OWOO9k&(^j$#qw-R4+0psSo5W z^P=YKcSwB}YDs|BqcRQtTu+?-uv^KN)=2Dtyv$V22{Qth^}?xxBUOPmRzHfRhf0i7RktE4e|P0}Gg$-VYi5 zywVeY{;VBuz=8f#|B@L9k&x#*dxca`tn_PVcJ_&l~Uph??;5f~_w7H`NrASK(1n8wsyqy`~y8KlIn|K-SnA zh0G^-pqIf;4i6*<&y)9Q!B=ww=gnYquoj`vohD;qD6pQ{|3I3{pE=PTA~HJojZfQk z-*zY4QSCEX`c~aQyNlI~66D#-$F=4Q#8ok4Y{y$iVH)K+pqQ~jr<>hhC0I*FAEF8U zH5hdr_7TuBMVzd4;9m0j#ftx#HTpQ?X?_y&jx0aO=Q&G~&Y_-2-42g?Vs1R33<)<} zSSRaLFY3zG{Wv`t@#I{2At4SpQV^}X*##a2wEB==8dF_HCKR@OU|UvgJc2Zw!Ws`@ zWZ+LF=mkJ-o^clIi{To>!ooHlZ6z@s>L(#(WAj&{?VX<*Vm-a&rKJVKg34~}7~5~p zR6!y$3WwFs`#KLm{|@q8lhyTuz;@ttD)mxqqs}bf6=Ga2>k^&SYM*m^tqu#S#!>&i z?a!^saexMY!ydtR-bbMb%*_=9x+NQd*|rZ;)8hirgda;j42c6d{XQV;3OLT}%}EZg zi@%y$mxpXDMn-maQHDM`U#-{yMYCIe)AzM!fc0*Q%&4=o@7EjlLd9wKif6m!sag6n zO6s@2ime=65)`p-Y1iL4Iazb1SBA$?=%vwT>;%DuW6~&W6kh7u)PVufwMK9`B?la` zgi1-8nlcz$vx??~^ntZq|B~IT%oSDDl<(hm<IriHTT9K;|S0Xk?ud1F@y9O-fh|rrVlr5P-OM~bFf?z z*S;iH9J<&~*{Bz%^i!6cms7sdR@2#1ekbAjP*@LB@};O@QVE9{7|(i9FAW`c6#;}U z+sothaIC|G@dDo9{(&EpP<~7JMz5ppcpdOd)RBn#G6Cp0{UamqCHwk*j*S(1d;mNO zREa}60}wv|@&IW4pK*aF{D0ue{~69l9Hi^PRGpokfJrkowHBH@u;GHfo(V*j=flRy^&xST2sm;RURT!!EAhwbTrJjOi!<6UIxEj~hTkhVhYR)a zLp3};XB=l|$n=8!b8{G<#dSJ+dWZn{5*vF;c&do$_}EW`g@dzy(QZI&_i3IIQAj9* zg`Hh|qSj2{+c)vuN0uKl`SHZ|mZp)|j49n?0|c(F4L^UnMU$?wc&3|_nk`vN{psBzE?2{}dqPrRl@vy3XhcQb1JKS~ zbff<%zI~v6zwrdjD_M935{U*{mTvBXKuylqLMH3)=oeQeXK@*Ixq><}nf3J2l$4Z0 zo0?t+3ZY0@TYKgoYo^;M6*L?N>=^`{>FKBaZvSk9OAxN4r4|@hIC|PZ5FWu|`%{Dk zC3nVyoxRraY2rOdrB5fS|jL>;+W;ED@9;6Tlur174n!HMJhw`=|75S?po zR@Sb%UmL$Ed6YI>HRyZ(Iwv~p4a1f1RIzaP40BWWEG(#QxH>w%X?SYD!B8H@Z$!q0 zH~E$hDdN450X& z=|}#0`+(})U{NE=$)?ArM~8#OT0@%{r6n7sg=-k4M;oP#%0`;)k@T*38Zt5r;j*RP zg@K_SROIckiN9VYdEZsMPW~o>{H7QZtR8PGf1bq)lTmE&Hrj>SNF!Rg2Nqzv4qfp2 zVSIX`l0o#~jUeWI-R9{EN1>!at(CB)&UA2i_`9iExZ>HZwc9z8kPERAE0pdNf$m zXKIX;gH5cpfegSr;$guwvzcBzB%-1)P^VfIJU4mV;1>x4;*B1lv*YCQ^M>Sc_tKK& zz9k1Ir)YFVIy&C_$b&S!`a}CRUbu9=hOM>&9iI=RBKNN4%6fYK?u;nU4K~MDsQ?p9eD0P7hCu|_6YOnyEI%X$p8oWPmGh!`36aXJ3dTaO%26o#SKxLw;9mZ zp1^8~@f=A^kY8dU|9m5A*T_POuK02W>-#QI7|*untfw|0hf}5&ymEE0qwX&iP1qe@ zX_!ge$rEp8%mYW!pYgU`6hR~%nBWprN5xj<(;TiW6#GTI7yoa8-9Z!6n@x;Qu!VQT^ z6R~i83l&^q^`4i|(&dH)%8FTS(T|oPev44AW!^wo7zyfrkl)3{4o_v#mjs+kO8Bwb zntER{B*2W}{_aSukz2!Px*6-xxxPNX{ID~67=MiUJ|G=(MZw$w68j1QPLb~Qdur=s zL9sosWpNiFd2qRdidKq#UO@nsEM?f|-sK1q|I6&gYRSSNc_=U6+-Uzw5K!dG6E6^p zg9yoykM{-7cZl~0b4T}7U-QI9>TB)~LkaRQKXu_{fiU3nR9kRhOz=P9179kK50 zxX8Gwt_}nu6!bv?nlJP$JncGSrNAR}&(9;F^kg+PCC!Ab@9anVczZe=-;NBDU=R_E zxfnCj()MnZK4;tPT(PjS2LF~u)PNWGrjfC6h^N|AbF|?5dWYHIs~Yu2MOQimZFPHn z7*6Hrs6tQnd%ZeEgr^Il+rt$R#PRY@UOHa|*jPbVY}JH$hrYyDk3fOmIja@8GsYhK zBdz4@NtNu~uh#h|w~e8%?wFG32UT+{oGjDcE>*)2Q-_=;qclTO8Q% z;FsEXw6@qc1oWxWxYx|<(IPgD#Srp*Cc^!;Io4WJ>wNlWUOt5n#o40ck`D~tjNhoL z8klV|rvZYt>$VrK^x*@wPD6~QEfBXl+$~ZC=m*TVqoFvW4(_iMBFs11jFcbGJA)KW z*6WTk6Ed<-`FFgZB_(%PDrsG$^#-h6 zc-`2j?mEzXcZ?c&U!)=7-PY#5kiz+9J)x(EA|^RK_mog7>amZp6kcU?Gzy)zrG9g3 zYojC8jgz>;=Gez>PWcpz7}Ou75>@|EUVh+_<&ud34{ywIBjF6%6O6K&uuyLW=j_Z5 zI>XL;M2IdXVvd!r-ihv6^!!sG3K)QmL(JVu!uxkbxyz^uB()apZ%tJ(L1&I6!ew(H zJY8k??q_1(g~yP7Z@ZPYXaUL65GAwMs-_-8eqAN?Z>NMsc;Mo4av#cI?;*Jc`-7ZiYfB%#+LpFflCyp(C6!6&`G&TeZuN zZorxIoJW7M+iFr}y}DJGxvC|)8FjTeC+>t4=oPJl=##_L{@^6q$J17Vdt&!i4Jk}L z8YvUZ9l0;Er%jQOkYZCy%XT3xt#pHBa{Hc-ANut5b8<|Yw$A2S^xnsRF3!X1Ghr<& zFJC?U`!-hknhAPT!Gz0~C*0_>eLz({SD_Xk7QgaUN$r$Hnq5`ZZmw6tfS0qEv*_?X zt7S%HO4nX)ONXwvkLt#8X&Y&*u=Lx8!Bzr^fP0|7*rSf(-NiwFec9I5R_W65zeX?; z{uibgiWn`r(a+VVPg5m$AP=gCpP7Y|S3>}jTv2aX+1=PQv}Ap}d;{7HSUo>GYi2SB zjPxJsj(!3O->U~<=!U83M)TL(kyOqu;1nE=_A!QlJIA3Q`uFTpqTO%bx&oH@kCx;9 zTpRn0cToIb=83K@=Q_RC1X4y);;%7gzxsOU*}?Xn|3IvwkyS<6&-{FK0pWh<_`->| zSFX^{=I5G-k{`4l4{@yHcYwwVwo9jwAgd_L_F_Wen8ZXh1O)!!O#WB-`6>ePa@~jg z+4wt(z~(kmIt+0$6BMAT4tvD^l=Pr_-bX>6IjK_~i57T58sth<>kxqXch zk8=80MU#{M$y2~viuRw&0cUP)%=}r33;2}VV`%=>MdRoF1YDsnq>UNbMU<_rbbpX=;!j%Jt-dE#+KSR4w+!kS zfz(p;W$PCr$_E2I3;O$^yxfDr@my@!T>}HiLkVCp{`k7C65zOiK{!Rw@1j_$`8vBd zBqYqQU*F`oRaV>>tq~5pdy_|S)}d48(;_@7CbPf;HVAqMNMn+)8+9eWk@gj}xL&|K z6m3v#2Oo4sYV?qV0X0qGPE>6FyS4R|t6bM)c-Q!5B{4InIDt2yF8N}f){Yl^z&fL@ zp!s-rv0C#DkV6|nG}C_|Y7eXHi}@>^%kZ0SQ(jEO(jHRcGZf9{sQWx(|)su zCW~AJI#1w~2lu$oPuXN|L?vIN1U!YISowJg5q>U#{EzXEvHmD6=bxI2EQhtE4$1;n z_R~@go<`YIGWW5l%Po*NSU%wPU<3sPNz&5M2L}c5Y8n&pWO4J=p>_?D_$Wi31tDl% z^Mhd!J|;HS;1Go8zb?r@m{(uekd4;uaJ4AByHLol89T#8(V8a_f&BcFp`m08{S5=j ze)gS+A+;%)@<*X8SwpX{2_G*20=CLypQ(I+m!E&V=XzVji1IZBmeF7@>ujAlO{2HS z^D=aw1QRn_Uf0brb4<%DkPdSo$HUpfCZVyBrsa!mi0`wi29xvoS@q`~1G#jb1RIR% z`o^I0w(@>p9}z3tBqzxFScq123J5)QEzN5{+QnF{*F@{}Y8-EndN1fIMFD#dY45`}qYCZX&zc|?jD}Rg8hmPAYGOY(5Yf(*WiSID(Xw{s@LWkz~7h+_~@k<$nGZ~={T05-+Wo$ zVXG|56MHIvi4m*#yVz70P#{xNjH4xAEn_!EW;AXT!r!U)i^BG^!SCX;s@MQOV=teH z^3_D?KAVE3Zq!Pxpk*coeS$?98Huddr~){?Vslar%6=~oS{=F4*MIr)jNybZsvFnK zFrZuDD z2McW1AE_NCr&8z5Na|h>s*4(LM!&1UN2G>I6h&CO7Mt}+L{d?iFqy{X1_bm21#4;u zL6&IVm zY4jr^eM_*@etZiJ+lF6(^{Z?QQP?p&8P(OB$)B9OK(aiXHhY+jbx_L7kC%~oA!eb6 zK&czIoftqyB(Z74TN^21FBV0~o?eyQIP(_2ez#RCto}izL~uxJ4&?|uL|*QHUJ~U- zE1V&onlO@*<-BvXcn6TWWU0ATt ztNl{RV_yx`+=!1gt3?EgT^vk*(-rnvuQM1Z{Riz&`o7S1UnVNu+{zxTSv4Myo$DjS znf;q(Jmo&Pf?ax3cZOCQ!r_{DwheLee?cl8;a8h4ZwQP*_ zb&4kZmGXkY|HIx}#Z?`3U89@sM!H2ny1PS^?hXkl>F$)4P(Vbwq`SL8LJ*`=a?{;; z7SHp&@7=jNcjxzcDZAExtu@z}V~n{bUFTT%$5)^l@m&-F{#0!yt)4`Gnn7W&cLl02yvnd5 z#~4QqCYv=~Y8?o>%buK$zZ)6q+dluO2_1V8X5rxEeyyoK`6uV$bPc*%W`}L2Ar$OP z;YgVx)6e0%zPKR8X`O5;zCGXV$G~P|WPIjH1buN|O-+_>w!nWdHwSMNst@P&^YMUw z`;pY*9BoO2z{~d9mmLMPv*_~&p>ikMcbe-45Apg9+t!}X( zqiHLdLw9#SPnZ4pxOj9?wB8g|RH~u3YCwEh&-NdP=FQD5sEt0fc6DD_^mKQpdH5z| ziQ8v!QDv?gYX1Ev?t8pU=6W;%^W#|^3JUTc;xumUUhtRNy!}kXsbWUh-VfuyJze** z;c+D<>Ti!yDmP|*{i(e?s8H8+gH>g}L%x-ce~&z|)ENOy2Ru{htJ$Kj3Imxml*%|+{p0fX!D zp%7OSswyCu6gzfjX8h(iW;_k8cfEhdKn{>WPr7{Ca$cjlTZnyWcz*4=7Tj=EoIo@h1BNb7Lt$v2meOW_x8l6 zF+ePhlpc=^|JQFxOvt-;uNw^{5UVf&Zfpee$`!XRSo!-bi=w_A$fV`x^;@r19pU&+1nc9tWV zc36zn<@yO?^QSLW5gu(r=9U)gcRZw?6-iOA-YO}Dw6q8#b8>NQ(lIf!v9iJlUG2{_ z+MRt%*I^GJ82SAh&}u-8?nfx!9IiAu!;FDKARt{f+A1uC>Rz9jZ-$hVxL^#Gl-*@1 zXjVvQxH7XEH^m-C1?|*d)^Go9NuBox>|%vFwD#LOH{M%%Wz#SDmV1LGFT)+X3{_Q~ zLc0oi|C?EcZ-LZkd-HVaNrmxCCs5WUUp^ke0pD4Cd_A~0$2-@)qWZ@lc=$GN$t*(RTW&@%epsfZ^30!0@&X3AH(`-W#+ z8L_C27M>1UQx?^40ll$o9+ngt8zbxMO9SiMW@2j7ogV%{cLPMO^OR_Q&kcBPkK-=| zBM(Q@hVZv{T@#DwpXDm)S%LKtv|n|9Ac*M0U3OASL4Pvph^(!3^=xYCnjA^wTcsYZ z{xv*2P%eUW3?1asTj>Dz%@eX2hQ`$K7^IUmd9i5>JKpZUvVE|t&?DAh`5{JX5%s^ccUTwVMu!zh4qX zC!`lu_4UCmC0seV>nb$kM$~KocEf8ETF%bTKf6Dyt#371hZC2O0QFTc$1-+MpFa$_JG# zb$TUr)AOs#3|Uzb4At7dMjsw&vML*yrs>0#KK=oLnbdi0J=xJ#t|GKV5=W-i!aY zVNyp|;D1{b`G>&#w{z0}{h?DzrGUCV5&SH^v%{Z#8pnQt2UbM zy0&Zz++Qt$li{<2v9ByPYioy!HAT(Mj6a~9_G#S8wtj}1MljPUf)J<92JZHv-^18H z!X)IbDKz0P0=(79$a?h2q*-*f7NdS)sA8_qjMDXai1x|h-k1dI*}bxlv~=U}!*N9A zH!Hnpm0z0?anW5RY@$JZSIdVNT>R?BzL&hqeh&hmv2>1Ztg^K9z65oSmhJ9FWaGmY zC?aBFW$h_%v!1PS5aZ;k!6wFu?FzXy*)KHtW5WXpeDw3C0Zq0;`(9b*&c#gBx!ANV z_&#oWc?R8#-cPyHWS zq~>HRt@kkRS4fY{Rxz~KyCcnxPbM~@8vHPU?gzMgOBP@@zVrG~tgV@XMNmaxrTG^} z1_z0AG2Gs6xfw6e%>Or6oLB^Fx;g|x-6f&pyF%`{UqqN%ic$1>P|=;4hUPRjIXI$F z)B8T#C!c9Ihhc3?CVHuRaA(>7NnIh;3B&*TX6BofX@Zz6}^Yacw!50uvDClrlP z-fdTEotjJK(x*L2+TK3Xah91}y9IcVz?Q!BJl#n)sn#*LOl5bcy}dXHolG>h;$|ch za)%YV%3IxY2nhade|77fF6iC~RQErxc@R*XIM|6k{~8_bny6@ZNrg}nWyuUAn$yzu z%EiTya=~7z5AYFvzP3y-V@TF3>wk&0i~%mmPe}=lOZMM9xiB-$aa{dWwLSgn1qw

?3dV)Sn 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%0DrI#`xv2rN%&4t-=8nX$ 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 z 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 qP4a)PsMgUmf>6?4vyjFwwzTcf7hpwJG( z9>$xn{9C0@ubzaX-#NA6jFO9py*HiL7|WR z4TEafgA03J#>QBoi2*oba4ju;@KtTCoO8;{Mwn+~J=fk9ls&aE znkIA6s>@eI>&5kyU%IFt!lbPw5-B;Pvi6d{!iuEy*J6!nK4VMiK{jy%>az12@~lCp zBoscV5V-h;ovLYCeMlMdCuN>-9kW>15Kd{PbBIwmeT~w~MD5A`l6Ap<)-M!a-Qu@R9&9 zMdPB|v9*`UOsr=nenaU4p%Hlj4Xw_#kuhd)QF24~*ZhzF^wUMl6cv}=PpTi(97SK) zli=dEU>8n2nv{B)aNA^LES&ekpQndkn(&H>7}2v$mJ_bhC&{pZW33jkxsTE8EJfF~ zav){5Ai8y#PbNK`F~E)|!_aewRtb5&k64z&12mF!yXGx<{sJoGvf?nF??w6zjOt}X z=9S)Y?tXgLT&DJP{*h$^A&Met_@fW3BQMzBr6T^=C4&t zxZ7Q%1J-|q9{YZ&?daYah=-EE!R%FST1iD0UlqHu?5OD?aWOqLq2_i4{Vi1lUMJdK z5?2AiC%q&jE!q9H%1e`S%YgjWIAcplWALXRrRQ%G7NhQ%|AVvw4ErEOYAOFbCt>k1 zUmL~Wv5TzO7eLX_swf`MlSuv;paH{qasRnYZS6QIV9p^X`#qubM04dgk9*ANOv6t( zSsO+Ja&l$FJJ0aPnGf5ood~`$@nDK7mldC8^7G5H&}NO%!cX0VY}j=0>m>Wcdu?FS z9LEPtD&%>4JWCb0Q&joF?>kIr0_H`tH=}Wm+#!^UL)Mpt8YX+;>5vFiuDK7YxO2@g zM^^;`-@1SU(T+R9S%tNGuKk(a1KR7*D{x}qXv%e0o~Lr&Mjcd^kw)>Edp*w0DLE06 zs};>G2%|?Au*qKiPlh(oE0XuQTUngWtN!&J_lt-A_xfd86zi;!0yp8?d|<=9@WuUN z^9HV)nM;qgU46jnr`o+kuvI`1Ed1sHI6N>_^E$e<50)1Wt{A-jyK4;=*vuzFlgNi1 zP6{rHM^B^-!!!Sq? zZzh{r2D}F}hB&=<{_sAJ<8Lp(w})W%5uovWe3QJ|CWMmaL!>+TF8`%04=R7$avY9# z0I?y>f>fHvfRPLk57lae3@IzOT)YPe6&G;f7!%B$Eada7~yRmxb`j3w`L15(qN^0 z`SOR`H@O*f?|{TTrxos`arI@1g71j)O~+kIYg*`Q()xk+Z5OxC{=MX{)}PYkCr!6A z282zSY6p3`nPvEowD{dWsooX4`R$x)OEEDH3j;IX+RM~KXl^kc7B~Kzqx4|(!=K~- zAnwB|#bI^V(z~&l%O=MjyCGry-KQ*{>q5X={tR>2rSYms)I?;nx`gimTy4`o|GM$f zgRno3Q{GKk-dqf)MAtjHD1VWcvZ2KmNplkK#_+0~b583L^>Q3|JeevfXp>{^BtZUw zQuc_Si{-uY2LRueDq&%;dx9h1cKLg$Dji>?{R_M;Ra*OayUxAoP4se8X&vZit^EA2 zTYF3;d>Bg-%#4cVZ2QebU#7lDFJ>RBR!V>f2oVIF>DZ+FDlB|$Ri(vpJuN(KR~p-_ z4IDk6BDBgXaUEyX=Op}_?_7EO$4_qF`6=e@Hsdy4Uffq3{F>7ul6LIjG*_`QFxsgV z`~+*I_}NkMZMgPUl`s|*j_Dq-zk-T6^M_>Q4}9?Q9HL>5ojh2=Yq#A{usIvxqX} zHzIf+dnk6Ry)TDHdFZ1#{*Lo%mdH`1XAn-pO1vI_0$x~fDWHdn7jRtvSNTYTJ@?`U zLg$z9O=%51dImv*^ueo?#sKlZ&ah~B5A4e%jQ#LgrkC1Vr@AO<ey(>Xs@@`E0ul`yG(5PMxQ!F@osF>SfvH=J8ap zFN%@tPh|i1MP1KGgka4thuxd#1SkXX?aS;XQlcOzS^i6{i<(QgT~g(H4<+=)zjjx` zrS3wqFx@hITh?Lfqqekh9!d`eeOyM|4-&uEI?IM)P2M|}yaR@u|8Gc)d9kaAg_W^k zUf=51zE{>bhwFFWURrEop;Z(=AFI_I z=jHM5Z;x-aRy}nY4(!=e#Cy2s3T`8eR~0AwAHFDEqT*Jd!;;A^mfS_a^CCWpoTma( zM$O04Q&R25e85W(S}IOeIzKz^?;hR$VOdn30&!G0C0mLSIZj64m(0b-b?oz3Cgbng%&xFntj{!dbT6 zJpY1sl1^-i{r@|x28=?@`Sv0xv3~vCH?GWp5Nax)Wte&@*Cxm1-@a=Wi+odI6zbj0 z4@Fh}s{6gC4^nO{36I2j8zj3J*|}A+WPnZuIwpD5xAwj_RdOZCy-jDyFbDqPGKR3u zGF3%I7qDn)sr*IEEWoP$Q`?cY1OTZ66l@k4ti*EPe%5sMq< zVVP2A&eej73li+y_7&;G18T@Ne_iCOBX**bW&9~qJ-V%WX+u~<(D7Vry@t>^9`09P z{>l2~Dvitc>`Q|JCnxV5=lyWm`Wj8uh!X$l>HE+B{5J+C0(c@Fm%}uigb3N-UM!3K zBPJCo(*-1cH(N;BG`4NlhPAq(4=2ZWOBp&89()pNqvmZO#($AW*lw0vVd^oO%v8E( zqb8P4uhO9}5!)m=a0Zl-4AbkCI@4fYy+n_m?@8`jobHnU>pEUN@vG1eb;z~eFKg{z z>{j*><6MZYp8}krLFJVX3a{&isoRCAs|@f;BS8=5Q8{zQyE-?AT<&o)zy0M^r}PWFIkBD=mO=;WXIy*F%jk7Z!aS+)dE$0mM{Pu?CjD|0>* z)#h^(TkQ`#Sl^!G3~kBWvnXk_YL;+uL6Rnkx0&(U z1$>gk$=0Ln|H6gLPu#|-UJ@MTq5u8u+NZY}JQqyuTv-Y1U*E02{33<`-E_F)8@;OG z!F2xzCA}AQt-F+AWZ`+}!@oYmCRSLZxo6;3?;LnL_}RkwdxXCCN%2r_>qy5e4*IvE zz`K}kZ903}Q2Iw9K5|n-C|-paxLj~miC*cyYk24?=h^ByhJ>kOKEb@6f9h9H*p`x3 zUE}G2=Gx!g@3Jj_EooAu#`qbw*mcI}t4~H@!lR)4e1-q4Q9Wk6%OGbvcWj7?_@(tN>^9i@B=mW~$ZynospkyuR;I2Bbp*$=#m!#dE$t_wW7j-HhRok?gFjJ=ZMHoby?0``o^WYd3pl`Nq4d_zfpF zrkm8O9D*P(!&M`0%E8)R6kd4V=%qx zmS}^CLOwbXqtkLs5w*rjs5Q$WbvmA8P5cEU98=6P>wyC?!^_+lRI2)uT9M$)ff%XY zg>5Sf#sAL8qokmG8VAbhjv-k4swSQ4+c>TdEdrBWEK_w4la17$sId97`sPl=9>bc7 znP*e5&@t0!16FGoE)FjH$3oXWIZ)az?RkH!b*dIV40%C!o%E9KqVgb9;|puw!hvjc zB0lcWXD=^)Im75afbZb=uV1_U?4HOegTxu?*ilTu0#;c13lUo*Rq488dey2`OD}~+ zo_;QfM@gqufuo;}|GQs3!-|SJYx?`ZE(_^Qlp2I|h%|fi_wHmeLdo%$vqPXxo;ZRb zn3Rsv@5>DP!Zo`7?+zIC&-X5dzuEPbmq(`3UeEBgTj2V9QsqRDGXKkxQe^x_tAv8=+p>oi(OOnBcMuxh$H*Vj~->H=Wr(g9yF|hf;u}x2gwQ7@EaCz4XRHHq_NwH`Eng zmU;ajJSsu*+$W`^ro-_*FGIlB$|@c$_4V=YIpi9^EE?wu^S3*$Ck~OeO7}?J2Cq$q zcEnL-^?vt}_xpJq)Y-TfS~B))ccWF-0%PiKp*!HrIN?4dDa{BHFDn^lnZFc_KGFY}jy`99Dfy)5t2wauB|jJSEhJ#mF0ZFN*3nTNIhgIe*k*C$DPPa@berpSp!Z3?k8=TF|ZRTyeC-y-n6J; zzAN`=iUyd7QSl#*G5a-_kXmAb@Ky?;sncofasVkRDi0!j)rNFv`^S{j=1Q8T?Liz} zXjAF^Mvnehpvrr~x3*`0S}Jfsg;iB=tbYUgAN8H|G5-t)K(f|MxQUMfAdsdl58n`| zxCL?-vl2sIy^|i(C|b|K9oWbPQmVGw?nRqZU#DjutylPb@1HNr0NY_kfS=XE{c6gn zZEu$&dl0{d%w?bQw3X8fi52j1^yst9y`T4<(6q{u7zmLa|nG-FPVML4(~{^jD7l<=FI%-;LBhHtMhx z?RMH!u`qsTYW*Hvgxhz&D=3+c+-KTvGHD^?VJR z*iZJsS5{H5J+ID>*i)2}I%dE9{z_t8&}oA;zEIQ>oQ{a{PgN*eNc zN|5%IJnJ7?bJrp~hhskoy|IsHuvz%6*>m;y=X`;XHyqIEtZtmTiO}NE|npj@*VQlriohnrb-$5Y7wET8z3Cq)w23|QGC@m;#TxYX^UuS(T zgd~9>qE~6SW<+jnuB>!xP~bRKKtE(Fk<6>fLnBQ)VVp&{tzhU%jK!m2_4_WGBfrMT z%5bGR{?EoEI>KZ7zFyA}h+B3k9G#i4>@{bqZi@a@PBj&#blyJV1LGC{K~Q(iLui}SQaZ|-4>MQ5_h)-2b(!Ep$0E44>tLLL(&w-D z^OkH{Dw0ga1A4zLNw}bRb4Q@MSq1#P$Sx9m;gucTjB_U}h=?Y@?C@vTl{hE`bJ@V? zFcIGeZq-DQ^1ejk#>}C>Db5_9;n-h6x}H>VGm3y^B_l)KDd;uZ^!)f%<4zl!orAx{`wBPjkcZhfe6Z9XzXxY<|I^ImbXf_wh$QH{XAz^`aD-D z+0M-u44c(|MGwNbbum%byGBA5(UdQq>orCyXt|aijWEVCPfin*wcTPZ=$#dR-EwZ8 zF7%%uBN1M#KT@0eqmxwH`~hh70jh+Yw)e2S)5L@uogQTS`^0JqMY#SwrbG8=->Xle z<)@dgE?=WTQNi(bOE>1WNj{HvmF+|}ENa6dPtWG@4mU+ss|y0%ex9U$TkH_a7U^1% zN(=j6fFZ%Jk8p8M>A1o(@|0)$z~;tABo&^bE`rJ~Eq1GrKExfuj#B#AST>; z&+xm>Jq_I#S7%^(XF%*-5%JDu4Xx7UVOyo?N2&IJ@Qp)@m!dEV>>Pe-2aY9@}cU%KQzAtF-Vq znt=VRL0WA@(1uMNVCP;3ut3-oG*lbKZ{e2rDmmJbv_V4T(#?Wnf2*Ktmb896Wqpf! zR_Qs>2A&Abu2xSIHiII}-GJhm491p{wF58otvCj%t%9BSb2<-4Ki>#{BP5P+eYJ&S zIQexz5^2cH;0^LfWJyQlFrDyQn<$4DZo85O>mfx<>nrO|-M10aA0g8gd^8TvoP4*= zDoa_dzjw=vlW@xJtyx`5J1NB=BE-rYT(_RM-B*>>Dq9Iye=YU}H&xZ!?OdWv1{-9U1AfMc^qaAH+w=Jr5W2JoP^^!1v_?Om^fvZ9^QDy}Aqy?X4npdQ@Nv%GNU7jXy31}1CJp3b>44yUH4Qy-jrnF(`=m zX%jssNO-(HaH`fOf8FdO6<9L6BhQ;H(UkhmfAS2{gi%MAMy`wW?=GXGXPgIW#|wPW zzS!MQGYI2g_I>YCdBb{R`+h6s=0~McWCs*HLT!5eltbpDZwQ7Jobdt=c8g)!a;{GIKG0T49tFsa`lNjpC-sS9L-|vIb8fG zn|zE`o2n}iv2^APQ-YvYzV;w%uXYePW!Ksk|EZUS(_g`qWk$GnA6(~J)3b547Q9GV z?s5`o*(R+mg%Fp+vB?y&EYm6uMLu3;CfDPA3wCzwJIIM(#;NS?>MZ}~@l{(B*^5Gs!GSa6&=#U$*S&l*JJ?G=K{J#8 zOPllNa}|}E6Pcg~7(;YX-M@wWH(~MZ%teEw^-$M+Y2{$N?OSDC3fyLF1qT6PNH^l-WK$)1+35Fj;a4` z@{JffTNpTsj~W#UjF8t;k7>y|vOgI%bMtFY5~JSWtg*l2?s(*Wp^0~E%VLH#*Fx`1 zrLXL-{VI$gMMdf9-L0s2uMjr2>#Z;*mx~ekM;bybqJT7LE~N3Pk4?I)&IKsI#yYJn ztOfVNAnlgx-JFrv+@LFqBLPivWyGHOu3Ka}we%;apN)eqZb)b;samAx?NqQ?;+{?stSJs0YuBBh|9eCZup_qd1R3*BcJUARi|18~9cg|KCy@vEHWgU`W>$ zc9bg1j{SPDc9LW3SELbp$$3SL`K1|abQErJ#9HW0Hg6a<_V%XhW?<|k{ngN&qw{0U zi`yPFjS5NR7V0x9{*aF047cz6D(*2zLc_yncO}!I z#*nf?g}x5!lqFmK{v|QEeOjvJ?-bFV1qg2$l#_Gvzm?G5ST~@-XMws!CJR(a++3Dm zK}D@|$>M&N;VrXP&BdsJBF9}{h$+B7 z?^&#AOucV*bYyGw5$G0rE~fk5xX6>&9z>b0mG;ubJ1}cKu(dWfN_=`PfrUE-f(o_= zb`u=3wY)<6B0JB_{Sy|i|FFPuo7C(WWnMmA07z_*{RZO5;9UiFmt6Jta-eV1i|X;s&A zV1~XUI@tx`k(u$2UEfAYN)Nx-3>Y)!vygPKOTCpWLfR z=*c+Vdlc+QTOPN~Y4~uo*3&%+_qs-XCztT1IfJx=;@3yP$~8vq1LYKZpgnD}E^JQ< zkBSCFO$_ElFJ&K`15+dFbc`{r>ancik7L?phWEjE7#(DBMDbUOUg|T!8P|_4_0Br} zt*aXCC9}!r#XAiGJrqd?Wb9e#0AyIpyefnn0)$VCWZYZ0O0b{YCIg%$#fS$StlX#u zQG*RIHOhj`GF~XRel@k@j4S&JnDb7V#C{`@F&c)zL{fbDXSw@;w)6 zC`dYI<@DEY8ZE@QSsab~n4^DW)JmrNq_xfY{JZfMw*uQT$Q#m*L<-*y} zFJOvIr&?^Y@AcK*vI2TK!H_s1kKYr&nf0k~3~+qZ5Kp5v%x`aC`=#<}OLzNKN4KP* zjdYunDB+aR!r9u)co1qPX8I+=%&Vr?#9@`fHNh(UpUy#{xWBDrdb8%I#{5=9V_J+w zAE!KuSdjDE_nNo6x4(sQe73I=%$yL(ZY=op`EB)!fRt)}savYAQfyDF-;+L*9Ug`^ z0?5SeQ=^QAEgL(^v7Zul70$ojzzj}XVh*gYHu|D)O1*T1?jiO zj2K#>ta10=RQYE$ydeFIaIB(dm`R<8&-tG2W293#T#3%Rnn0sQJ5hq&DUpKW|3S|Z zH@q#ZJ+_~DhyQx}CK#C=ay!1;M%~}E=Pc~}R4E~UecUCA3a&77hCij{qjgn@YKR!& zJ$&{3b*`_dqytUsU{HPb5_;v458H)v2+hxR1|`P`{si{J3BftJh&WA$>H0wOt=8ZK zDtPEL&@PM&NGKil3R4qR7*6e_)2B>$P^0TZ^>=`}#uveBn?HGbYDaqbfjBgL6W#Tz zo#O|5O(>^OGcV4m+&(2C5!U#kc#i$!-=4ZmDnO$+`ZmN`+o4xGr(SJnz(4w*9x}zy zt0Lhq@dmYIs71H_Z$t6R{(~PcbfQ7FLR8g?wX&nyM|Cza3vok4H`gu^g7_bn4JHOt_8g^D z25+R?W%M6>co8Dqif-`i9h$npEKn73vO>8{Pr6AjRU8H^UlW2=PE?`;my|^lqiL5z zeYh?Jv8FBdA8J5N|J9zOKZ~v43Hnc`A80O>vQ^-ge$e(`HTnvVu+XE`{cqtw$JBK# z{7;{{;!Qf8jp{rP+N7BU^Wl=Zo5Z2qj9#6gc8}ak%Ky+6Zl^rma6YuWu)(`zcNR6= z^p0-!ylhk!)hyR+M8?VsvOW>=C}TlNUsJM3u$lA+KIRBl`~1Ij2ARHq;XSvW_nGky z&M-B#2kb*9LY}eNmm83(?vA4B6x1^-+iWd5txmRG@1{_=(P{L};(6kUO}BUvX8 zdOY%MHV{AN+#Dx`RvNQ90QNE5CT*MJOht91;ddu+#ra$YTGQ@pe8dBP$UkNtDhTh( zTt78GR58=<6vML{m85*p=_IB1zWMV@E3doj?roz6XZKDq6K0qVK+>sqh+MliPV!e^ zQLg}1e8*ygwp{PHO5xS@s0*>!2t$F2=Sz(ICG8+SDk<4{=WnS(OJOGsYTx25QQdFRYn2pCA-cl z3ipq85g}3!CkE?9=1C5enB=f2>U5bV(_w+jiUNbTGHUuiJ(K${Xd^3h@aXHO&UFRU z(xED3i@yeDNQ>W%AueH!BW*hwgg=4+e~xtooaGeoEz@D^9X!&~`1Jw$O#v)z@!rhe zAm0KYCR>Q0AEf^7HoB8#VoSnaGXji$2;wwa6|{4{LBpRQAy z59ZHElSlsB)AXqmN5*b$|FG!Ztu^s%9Niuz(rZ0in=wxm8E~B7i+)I_7|5;#R?TQw&)}{naoD5-SBEfII}~Axp;yjy|>zmm(tJX|-Y} z`LW11P+D_KcXhm2WfO=)?d2G9;RQF9@ppf~(Tin;;tO9x;BzZspeVo|Bb^uLFkv7|EblU@1%KHH34cdJii$MXYS%#)d<$>Z*5-3zlNIN-^q zvUT%bXGH^NP#w7fWDRRa{nFWY7DYc+*4ozwwQR(q0HnEg@`WBC=iSD7(=uCaG!&ca zkIVP`?=Y_yH;Q-xVj;CInepUpkS)NR;Iepctk5@>0J7QZUTeQe%Gs!VUJozv;_#v04&b{4j+6YC_3a=JqggZV^ye@L}PT=gL{J zt$C4&T9CGGrLyB|uGH<&d}x=EC$_bk1etZQuu*Nl%MqcrjUez9_&XFWcV(vbs)K&f zQ(!%iYmP@oW`%#|g<{bw+_+JbV$RaGI!33JD^y#l08l@L4-Cr)o8yy90B~gubjbN- zCw%|P9FquCXM})s6sGgA%hZG2Vchx=Aa4~YgVjb%4&gaXLgzxP(OZS7hD>c=r2X*h zm_@Ro0iD$ie2W*MG#6}T17P)(nAU;`E&^6Y6y;U{GJNkcwT4~}yOVid`ycK?UKa&M zfJMWEITt8o$O?2LScVR46jkbvN?AsLF{cVEN;MW^r_unS=&0q8i~_y)PuW}N7txmh zoZ8A;<8;VGZK9XcT*O~c#j@ek3iJMKgZt#^0vi{&pExrERI-XIw0(6FY|1ux(Aow= znL+dh0E7crWXUP{C-1 zJG+|T4^mJ!5>J{>dxPOEEmagAtFLi2$^{zbaf>!W(0bTcGD`8X8QncRJ!%L} zNo)^4A*TNx@$x!R?@;*ts4qTdzOFzs@UxW(UDrPZ;Jep2q4EGN*&QJ6z(%!Y7@Sd4 zKJrc4?TmoZsRQ%Z2UcdIP6j9u08dVu1z14RRGYlTndA7H3e{FhJHD(;?Lg5((wT&g z=e%6pDnOlSok%Bsy7`Rgz3!i%hN%g^LWMu3*?i6b>d`J94l6^DZ$AGAcwpiHrYpe6 z8vdX~Rb%C@Cu8XW?{IL<@0`~JfVXU<06_TAH1jMRUU?NXlc|^@9rv=4)q!|yVhhN$ z0g9#ZY0FDwo<|}q6~@wdWfO`(T-@B7lf2-<5=UCa3}7ozN%)!fLX<1`Jda*S)!r z^#!bw`BW7^f*bp=oFhQhi>0Nw{^Hv;%T4Otn74gQPv?CS+sfA>hU#v4Hl%5OOG_jtv%L z>tJG1f~G8tTiS1bIl);76x;>G^uiV)hjxCI{t;?J!zT=5hhMMp3}G+WHw0$I{pB{2 zQULij=W6Zb5_Da(DN`FLd{?`hmjv^V+IpFnT<0HWt4JZQAk+b{l#f-Dw&495R!;R6 z;rlgHNf}zsp8*!E&OqqB|B|6HZtQ`8avV}fz6mi=-1jwhVV`wCqs&k(aIcA|d!P5& z%%(a(b*hGy;jqSxiDH7v&;?};;v|=3*khGjE>Wf$*`}ROrHO*+fIrhrHG*bJ|D!@7 ztXsIUWsi{C)XuEc9R+|rOUXAC+Iu7o;pPcc{<@RqaF#BIsvhcuoMmTF1@Vng3ZURD zZUslZg`*fjKxG{KDHJs70SxY}ns0`;`9aD28zfG)oU8?SC5)!WYLdg67W3U&)^zrhWxS3A{C#@$3K^=Eq`Z zhI!C^E^1-4#EgGyt|VAxMAKcRtB#f(x&b&zVSW`j`+#|hI#16Ncd_Uz z{BsuIADSfc+ zZdP=_NjPy7aO8+PN>nXB>jwwuPundwx1JpvIcd2m;o^JR%@^P0z;c#!H^o)pF(vPF zdEad@5Z-r(!pQaXG>!5`GZLn<3>@Bj3du2I4j)1Is@84+M(5V!POh{!kS~bO+Wd$~ zgt0&em`8YlGoD{VVt6f(X$R;$uJy#HBwLz(!4U~7=}RZ(#6tu7vxB27_hXZj@14ju zGf)I)h9B^zxwhX*#+xGG zZC2hc5~gSOdDj_zgmRv))j#PoNESm&{85JUrs0v-?wuce*O5DOL$OAl#LeO%(q-ZS z(8rP6(6D($(71=Wob8c!o-K~N%(nNXfzz(w%;^haa^&rdAmDTt+RfVsTl-E5dr;)@ zjDL{D9QbI0@9Q+q1IOjF96|CC}o-*$=hj8#B1Vu z@7@9m-QnK6UsrlX;^288m4w1mI87mWdXq-5X?|v=I+j*U+R@YfRm&mOmp{t~7nt=X zH_pzZt=A%MG$NUHufYi|(n^A9nvZ=zxB%T$7l4VI9Qj;D<1{<(I*ssSVwL3bz@zkr zFjag@_OhFh`&5^_-Dsz&5H7aG)5at;!k5-ZE=ugwEX5&Ua%Z3cJ01WSdc!YP@L7wu z!_?-+=Hc+>j@>-cseAsNb$>~M9rp8$8xi2vIQ%P@*I5kf$tl|!?kWzipwfBJ0;s<+ zJE$8VEx1~x=Tk7N#sH^CJ0MMp)x@y+RQz#p?|wEXzYIWWQKwFn>aGW*H`Z!54J<+U zot~Y8-T-4H`M&=vz}T6#MO4Y50m-nfhM3Aqp5+&T31fP@SW5J6!hOcmxkQxb080a| zJK!0!u!cFDs^E`vX{7aITJugZY5C#4l!;~G=BE2Cz``vVLR}p->vz2Pt=5J)m2K7X zRxdJf&{ zpoZ-ROlkjN39OYc*Q|bE4obCnR08D<F%#m?Ag87=f?uAsZCf0XXVcJ61jsFCA+ z^6OKJi;D-pW^nt32!tea+Dnv6YJU>~o>x_N4YK6IXxHJ4s-5(h46@n?-{&L|S!w78 ztQjLbOMmqZ%ZsNfI|xkZW8x4$F=*u3rl5W!qFl+`|7?;Uv)v&J$r1HPd33-mQDCWCGIRvH?YBGiv z+S#I}0qWruG#?+IB-*%4jf~83Mrf5A^x8+ikhAf3@5@Ev-kx!jJX%(eew9aHy4O)0 zNBe8TN_8DWetG1yACv)4#hRAZ7BQiutUSnp#8~~t`ib=kV4*_uqmw@PY_7}rH|PF1 zDQeffQ=zlED)*nxRtEmXjWxl+a?RN9R>_!zmg{2e&3OOlbAi2L*q(4oH>+ktg)+nq zH_3dXt>V;s-p2k^_jJW!o`bC&^uvQ$aCfMJa|9t)hpesdamXY=|~~z1Y>XCC#8U6G!)q6r3j`2YU<1U z;u!3XXl~zen|t#$q&vYH6aXqW?~$*Lb~uTXB{mDWj+5z^zXQp%i=A%;bT1w%LTC_; z7S{t?=LZjUFjj6lTF5mBn-u;YOXI0Ny4G_v>JBfZ!myV^E8;LJ#`bztx1sXrJDqUDM0Fbcd1#2 zmWX}Lbu8j|ZH7cFY+?fNK%0n=^f=&oo{8Bb_?<_fVHKSL;@YLXC^rbIAPzzgWpb zZ20oPBZ-sJ=2QO?JIvC&vpj9_E#Y&}@yoHL(&FObIm`3LR&$SzO>DgJog>UXLoN)a z0ND;$66DaW=6&9JQapmEjD93F{~xE}b00`2F1PQLr_FldK76x}$V>MvnVF>Q+ww@^ z86NrQ$eD1+RRW(r%vV%AZ|THuW>xXJbv~i?tZuo!_d~XaKKa!gL`@w3@?F4j*Zr(o zlLNv#4>^c>d($d6Rj0hqj*>?-TN-O?Rmlxqq(eGHR-KryZU+Ls!H;^G;f_8t`2{5!&Ry*sN1lyE0O8-n%WLU3T|D?9hjD3;&4UYnUNS&E!cD|Vs(ckRhT)=**Sw(LEe7- zusQpOH%FrACNBuL_oy$-v>$xx0E#xS=Xp@NX^6a(!EYK3B7mH|(4F&N&-}I+SA-k1 z(3vA0cinfXs#$A<2i77-$8AQ5v3g>w;d(l2HTdbGI)Oag@oW|Fd>WJ0qvBEy_DS`* zK0M>QfQ9}5+p0uDOh}cLNlDvW%bzQa{a0w(+5{Ov35L#gA-GGaiHR9u{o*QCy#m@0 zJZ&}?;>h?_OCYW?Z<#QQmYjV&bHvJHJ4HfIO^0{{F+%}4mr-U%y8q;!cd)6hu@0lns<_&nSfq`|Pde;%+y}qoD{9wk=Z=3@ z`TQy^VkhtYk|TM=m4W4X)$_+evbl3P~qzz9B9FNasDT}wB!gH;tRoF=YRjca3lGf{GJ@) z9P2AFhQ~Bz{A&O??|vm7c>G!M00LtSxqPclKHZP|nave9_|%k;5qPl~q954?xEhk) zSlfY3*PW3tsqLziBpEQT}vYGM!gz<% z2fYBrw-}fJ==&YZ!-vcRO#;6Qpd+ecLY85dI~J^ZYc5+44bAZ}+C;&UQGGDwS;61> zbLssEM96Rw^5f)cI&1d&;TlYs_LhBFX~7r}w3$FtPSBYt_Lq|e?~GO(hzX+G2^|4d zY_X4^33y*iEh@Fsf=dL}yR>0Tgae26?8936Hx3xJWGciQi8QogQw2BY^bcl)T_(_l zfaBofcgJ@;;O7UF;M#V8c#Ao^LiEoIF2D<)K~Is>=^~-cgT**;0X3lD)t)pDhJ2g1 z#`5R`5`YF6!K%3Gq^<>xJB>H(wcki~AF7FWSfdy(<%%6x{R0CN9pv0>iVHU=o3V2P z)raMDA7s1y4SyGLHt!vIK&_Ht!H$hqy4cPxn!C?9YTnF4zMWg{2N)O6c+RktztMJm zO$T}T;DOy}Xl6I%SJETby+0HV#$5CO@qrIm$R3k~S!k^XZpLQ|N5$(L71^@#$!_qQ zJqXtpn$q^Kh?C~{21L*&-xfX;A&W=KGf)5QplNk29b&OMubNr)SUGGKP4nguILZf} zc?SR+#-ANE_1jJ7IOZwm>BMhlF>3hLL*kBVMnhyh0N9;?Z9o!KO8y1~6fd}jMxv%! z0g5WL!*Oi%is~SMp6C%=@J9VHEj~phR~uVcm(1&-UjlMnQRa&~}iLD+|XMV^_eoLl-bhH=HbT9U)vA3+&TDaf#;St;&gIE-HeweUimX$%^zB zHg1WV6U_JY^o@&i9VGZY1Biw*Bm$C9#D2A0#G}IVqe&wm1q7Z;wTJyW5OuKWD{pc~ zpNlgoe#Q1U*>%OH!&B*F%BIhqKaqL=5HaxL(wYa<4_Ct3vcd_10Ke*6COXAT4ur;p zl0ZDP8B1)pfoUbc1>b$i^Ohy!eP|uojgMJ%$3L2NW*TkB#tYqR+zpP48}*N8#}8e1 zK8N09{h}Prc(-#W>RXV)4??r4<9!jaf#&+vSy#xL!Q*s4HGS%)wY5hw;P8Ij)hw>7 z2;yL9z>~8pA0Ij5dqlj}?nvNj_PA|KOgZy$(#(q^dAbDONEUBoo1IqL);nS@>jB5= zdL4^tu0xlW-V#V)U#g4_uO<-FigVeQVV|x}+7vzPyNmD%I2#QzJaqoOrne>Pb(`Rg zm0jTc++inH!nNFG$jv$zJcFP39Pp{5B?y@q>f(f4VH|uIMip%YU5`G zl{kEY-m77|oB!61%2tyh)F0X=$mKl*iw z=T-;I0ZTvtV`_WwFyoB>nWN*Ez<`o_xbMQYKe%K?=;<->;px0o!`1r8`|X4>=wvOe zT56FDyV7xX-~?VdIBxMtOxKfppUbT_EA6kuPj@y+jB~Y=%9LDBi^V0q2;d{QdB+ks zQFDZRcT@-Q#42KgjN8`QDb4|Vv_-Ik*(O+eak9s=|89%K_3pt6HE?xxLC^cyw86Se zW&&@)z&_!lD<>Yt=JefZ(b7Jd_DRD8?^}*&T3b8ze8!wUN|)Q7s}j7A&b*|}nQY9k z*t6~_xzfg1X4q1+_ae`9Gkj`LT;&!1a?qLK>Z%ZFG?VE!6&coZ5nmQg!}?nU@7$e# zU<`X_PvX}%HZs(~p1sL0A9|KOexFuCMDXiJT#7)zx*Ta#WGI3$T75{j#_cO?joYyS1IClw)MsfJg{ma-N`u3GOUui#0Mr$w=w$*1F3^u#k>FPa{cXriq z+a7!@e_Y&F_JWz<>wxkW|5Av^@d#}HE(0TDkwtGVS}7}li#Ap0gn31o_6C81ii#}s zJH;X<{*SbbZ&4{_9>{pfJn%p9{_tbhguF!7TK6hJhDuNHmsyJ2m`>~a2i)oDRlZYB%fyx#Ut#{W(1m{I!5X2>(G)j+yPCr$;!R5r zYZ><**5uvqwaFhewh!r}pP`()-trXBRNtuzFgYZXm&LoMP4P?T9rbC_z}CJxWa`Q#{#FV6O zV{Ny>R#;5R4*^>|pn7@DANRp~o0*4|@=CBud!$O(zMrFN0RH0~SYl^~>2{|2ifkw1 z$gX$bb{1j%IecZCLn$*iH}_TYk`nm3+K;X)RCIuLdmonN8|JA*$5LnA2Xorif8KXv<#$Q{Velwc4o>nCD~4Q-!%=oDI=5o@kQNd++xmf>|<2*+DY1T zl*Qh$^sA8)9)g=Mgj~$tT7M05Uc!q%G{Lp<3d$b3Dypisqki2@ZFxs@n0)xQ?wpG` zPI2EaQaS#7_rNFIyTy^1Tvg-Ii}Gi9f0IfYm8qkSs#UJ&ufO%?b)|k-o!q#Zx$_H@ zC<4y>Q4wn|7DAAru`_g&CXJllgwT|?hpfjL$g+n)HQLu$W z|LNJ|NQzQFCVj^MRjnGIlM}&0ujcQ9va-#8uXWuyLwHf!`g&^LwED8Zw_<2qW9T@=GV?>SNgpesB;~Wx%Jjf8G6(2+z%vZBgv8ZcBX}~1 z*CUO}d83>4DKs?nS;E4?{n-9jPsc6?&OSW68ycz*aRPWxnK?@T8^OQ@|Gs$dU2&xU z`x;gttowZNQK-VxitF&?&bK_FGW4eS9$DI!Bi+9rhT_lyfd(qZO@!ym8ui7c^+%I{VYfhrZQN}DqJ64}rc*t4&CU;zO?v)cGh$^(uVMXOn?oiB{_?%R zHG#t3F7bEg%}?@j-*zmXB=aAAbO5n=jTin*nx4DM$pgBuNzI$Z?7g2V83^Hg23~Kk zs-`s({sDrY;(`FzA3FR$>%e-oKeCo!$pZAq(GCF&E#h_ZK%(7ONjB;d|HVzK(>>FZ zGWJC6SiBh`{xr&T-mDzpRIql=X8TS!wpO|aX8gO*;0^|ZsdL_>QBAK8$C5yoF_a^D z;h)k=9T6SBN49dm=E7F6XFquDUy&$dO$7+v?0k=T#P4k!xe7p>2w231g|~=S{FjM3 zFA*QiWqv1730<$8sSbk&d;63Z^RUQEtRN+zuyVqO<+LR19mgZCQBuDPYtx^ou;FeJ zv3(|Dj4HV&M}iLY+>sv2T6`iS3R z(IgeWse(J1%tlqoziiC|yYfY>ZI)Nnd~es29tPd-hdduX?1 zLUp07Spj4bY2TY&x|B1=BY;?$wVYCJx=+L4rz%rqN%XtpBlJnMLPk+hV9Dl6cBL;> z98ys+-fZc(<(cS9A6?qI1_1zmXye?6rq~^_vc>5b8S$6k^+~s?+ZOzwCKEumH@Mme zA|Nm~e;Ui7K{;z&GnA=vUxDEsfy8lti8F42@3F9Z#j_B7YukD))(yJ9QGAG;m*-b) zC41>}S8QXnI7@7&0FV{Xh3~boM}M2{y$ayuRs_IlGggaF{t_rZz$!Z4mX~|? zU5o#|szu|+eCA8R}ju(x%Mm_{^=Vn2TIDEIvNAo2=9IOWu_3Dvs< zwoTnp>AyCd#by1rBxo)m>Dj1~|1OV$&H3uL4ERJd{!_U)I)v23!OvT(;G>c{37rXm z*vj3OEluY#HM_E?l<8buo@xD`BP|hb`?%ZPMujEd3o2)J!jPFd$lAM~Wy&kriCtgZ zmywetpQ>|ncOPs?AlVo#{^GKn3S@=97uwjSo3J+xJRVCZ+owwTIyg9JzT*fCo=yK@ z&R3`<;pf*>9&mQR$iU#IEl{S5EZgR35CcK}pYjo)qd7-6VJ<=HU~Mq$JSivwNRDGj)joH|zO0BnsNIdrCn?k>$du zBOo|+E9$h`E^>7A0fB^}p;SB-JKqee`H83KuPx2na>3Vc`t8$1K}9;g)SyTo(j=Kf zFC2_atS}|TyUoq@%U_YR{hT@B;a}QA2H#ES8eAqQ)O7nE#0I}@p?oM}XEsvk{hW7D z$EzvU=oYWz+UFg4qHm$QFUWojGQ%UkcMeKqf)gS+hH}l7Oy_Ese9kI-*a*CzAZ)vo zs~u-0<{vBFaTNfZOp)awcR&~Lv1LjFG+i-{Etae{Tg?CUl;v4IF7dff#`8@Yg5nbP zR|rE2xAlp8zQn2DP6}EitZ!RHf+H5o*9h`l{?RdT4q~Q|5 z+E@t(@Ku6Na)jo?17GiP8R@bqg7@X+p^vXPzRqeY!+Y zTY63%LFL2x7+z5;la{PC*a9bc$c4Y(qwUaDYjR<=^xf1EZ0cqfdWJU@#RF7(E5I^K z@}{VoRM*V#0;8h#iDvK5cOCO!u!R}yt0p4i51Opko`Bk$$`Oc2i&_K^U3_Uj7IY&JX=ie>GL%X;2F7>2!M1`Je-y=WZ2_`@~0UGkbxRZ z1f2DXE2WGnS(z;)O8)RY!k~Q(>s;;+q_=!hX1el(RndL7)URm;1V|PLdrTwHX13ysJB%d?~?d@O+r^&zTh&{crFg%8mI(KoXcy#XT z&OXZhH$I*xSWgpf|3B=#^;=b4)HQq%5fl*>0cq(jkuC+1Zlps*y1RsfN+>DaARr~( z-Q7sTp5D5hR*RTGW-ZQQglu`J#3@v|UZPIUsnSNEjz9wejI4AJhuFmoMhC6iw@X8GV}0LWu>m zFn-)AFZgYcX>@t1^_j2Y|@Xv(ED zatwJn{}CiG$)fuTdOb;9rs((W=ka{bc6gBHOu0WFMM&0|Y6MnhE}GD~``Y3Xo{EF0 z=u%Zx<+M2h&*IP2U`b%JDw?jAr98k`#6a~tfk@;Tyj0^Gj412A}=Rh7-CjbWY3z^{wF{XRQEy-O<_2nRP~%>Fhrb-0vL zZ{cm3>1dx!znX*Ez7NlZ+|O6_h^lCl9TPIlyeWv|NyyI$AoegFhwl|RNCscT?>i?7?)+%f&=dHj8< za4E_$4a{7%K#f-|!{g(h8VW*`6a_gL`1*oRodg6PZ*Fa2A&|I(t5ad(j5;8Xe!O+- z)P$1YXkE8r+M#-QZ4NZo6R3ck33Hjg*r+UY;Ov9cnydOo1e&XPT(-;#n(KMcwRlsfT0r*rL8bw~5)*Ip+r3vf$OL;^sH8rnj{n&-TPD+Cs?k3Dp^ZUATamTKW$#ido8Jq7I0FL*@k#sS-Dt{&a=f4uI3>Ls z5E#N?b0p-KNJJfQqT&%69&XrpsX~bId3K+K7k%v)uOwoR`e51kyIuY#4eP<>|DiBc z@In&7l6y;@z1E>fNyzs}(-clPZsx9c0-x>_av{X?yG*79g=}*kubq^Roc+7hK|Z&W z{1KNgm@Y$n^bCAM*Y}Brksapw%_FdKKe%S zfz;IW3xhbP4&{Zn$(Glc>Z6PLuz_-q!S zpP_8};tr*k$Fb~h?VSB%P38Fnqi@Opo&i+1-l0rs!eEq!HoxF^zP*=Pkvb8;Ne4)A z267>{Kqwm$w-M={c>ZTgg}_>e=aZy0udlzvV*iCKpY!^y_jt?L<|nT&GSblC25}rF z&+ea`zO(x9RahrBO?IK=o;66;_p2=MeXl9*F+S8k>7sjabOwQp<>)^GU}w4aZAS;4 zrCQNf59e9mYg%R=X3|)b1b4mP6~QCky)pJrD%o{wQ`X!xQe%cM1G&B|+zwdGjt@FS z?0`{v9-4X3r)c!VhUW$Ztq)zGK{!4t;qHh!X`2_r0BL@c_AX?3vBLNPAbS1b$@}+D z`i6dH{k#`s{>JNyMhJO%DtH6*Aqg)|`bQ+o=uU?-WD`G%F7%hJ%>8<+Li{5UkjKL> zDL|nsCGGp@2;P7)sTKNf@ixClFX?&3WBK^R*_n&nu{c0-cWV=tfleoPjZg91YejDk zlz%K+c2~cy-e~8~iZGKY=;{mPY-Q2#B~m^9DUs3Ih`ZT*`KuW2839)_oOh3H7TkT% z+UORq18}GH^Hl59HA_B(Dlw^7hbme^nz>lKYV*mlx$Lj~6)a5RV@ zQsncaFP^Pw6ah=Az}NJcyncN4So1`_+n?WWSofSUBES? z0m#ad!i%9EbJU?AF5*i`yS5z_LHV~Rs#G}`lN}=)*Sm~_|MLO> z*wj49{xYsZuP8LJHQycLb~R>DE&W}iOHr?RxcoU6IGxshC&*L5bc(%M=q+n&cP}S6 zakTx`ViwEf_nr@%++VTf9{-d5!q^UN3a`!@yW6+)|(x2d3jrTAmZi_Pur4EvR+3dSOMa1y=XtB&C_-s|fJB+%xN(9sT)_Oq!WEi7A2|PoZqeHA ztJvMU@pAs4EcC}W-2<@rlbn1IJKe7wN_;$$fKKa6!@;Y!UqeGb${{zyH4uF#JB8E$)1>LoJ-j45P=Zb$+MKugt9G@k(h($?jj1N$k+ zV};b&Ec+n>Rd`Yv8L6sPDf?HleR!&jtOv=v*hb>b0qN5df^i|3(MC#!*k-a8Kxk)V z0wKRrb`K5?9U;y2ewM?T`+W7D6bZ&l{QzOe)heN$GhxYjA_LD-vCn<~-dmf|#Opm1k$w3a-PRCY78` z`lb>vL~&m~Bo5VTD!TP-NcIiRZox!j)7uPx9G?a~X+zY=+O{2o0jBsH`PQPK7^C6t zyLJrwpJLHIiIKn}cs9ohqg$PwoSlAWYtS1SFv)*Xgzpd6d$IQ?Rbhc`#4d>EtL^^$ z8h)dr6O7@Qg+Cuj-Ak+7Q+j$Y#owEnDTW=}$Vhy)`dZ04&Bgm`dAU->XYBUJoO>c} zG211Zn^KGF1ym}k#%>3jM4&N^s&uq>c{yDgiiH!>yprvGFAZL$ASwAd!RGC=Xqm(3 z&#)ea^DnG{5BZd-nhUw@1TDHwPkxip$W-Cij7wZzM%m<6(0m0=Pj8(7H6lz*5&W}nIkmf8h$v`(63?pM}GziuSRK1;= z@o5OFa(X|&M?f}#K(s8FKw%p=c9)j!5xd^nIlsVU(#u!I10uL-?yHo@E1`vx>BTLA zQ%(Vm6lo!KMPW)0zUJrw8+-DHo_d`nmtG0}P!qJyhe z>3P1&UoT|&ii$v7lIoSp3~vo8q@9XWw^!gUf_cJ5d79@16{)f^g`o|b0cL?m^u1Qj zZEo=@xqx9TLs-vmq=6ojT0U#1|3IBm%Qz8b znxSUR-jEfHp3d0!db!-W9rDARnjMgjhR}h_9n(aES`YN_o71CXe>GYf&)cccJH(|l zP?thqzwMc`rcKDE*XGBp;b$UD-F#HcmNK&oq&WEHq!Z&|ta#B~E>h+3b z3_5iql%jXVL_YW~0Cb2PM{`wicE&R5Z`X})ygm~<&S(Sl!Kyu2EAx-KY2O5|lXH#e~ms5)<*sY2bJ zw^N}(L6U(iv|ZbCwFo7wY6+@GztmU&0hpR&-;RM>Lu7+;{XbGcIw>tJ?JENViq6jI zq5g^AzcC;;gyVFU5zM6Zb3i;s6T-(Rn~lLsD7m777Br{~4TbSqoh)l`iqQ8HHYxLma1Dr6m8RXkx9mBexA(_3~G2Koqi{sl?T^9z2U9J4Z zmA!6yF@;kg#z8 zmsxrmq7Ly4ShO3}(?VWHJi?`8c2|rJm%;Cg>;6!BEBLu-%|8d_HOD`N{3l#)!r+_V z>RJ2%RRVy!4D4iojoNCVuBb7gAL#W`axp_}g?eSBBRoK9V9*#Of;=R{%cyMRew$|! zQsUGiX9%8^aDV3fq;G`nINw@NFPWt)`ZwY|K~rfW*_O*v?_1q!C;3x*5K3DAJCvaN zqG-8l{wPfbb}A1SYwT~8 zPqT*%Itm1F$Azs(XB+Ok568+L((62`fQ@@!7a8t+^#)d|Z&6WCoHjYra{|yIK;HPj zcDQP3)*VO2!)fxPx(0}gpqIco_k42K8E}I3{VyTSUnCp(JdVT)hcJ9Pakg$+4Tw+l zw#%~7Nw!e3f#$5`TM2P?*Qnx>8^8oxg!9?2Q>6%b#_zi0Y)y#CSM?TGF@L&|+&-=b zl@S(FJ!jJ1y$&Ir#m>s=WC|1YB=@-vP&7rocFZ-6<+(AE1;xIas@zse8FF?iMu+4t z)MKU@K62mIr4h?^8Bn^Z<2v|~a#ljXOY(kl-so$$iL(R5Y;Kn1EpuIZQ~^%9(fh+? z^y6-QM0|IE2LWdu)?^0|x!Wl0yx= ziE&7nWes2TtwuemcRzk0I;0;6v|7IjR8_w6b?v>SN;M{l$O+-a?bK`3f?{fe&t0Pc zEQF{cS%AH$oW`_10Kk{0vwqibN{V}YM^}c$@_$~iErOZL1Y|!51imNQ(N>P}pmBB8 zEKFoh5CZ9~<4fBg?qMprxXwXi(CI{j*qwdedVh)$u8-ZxXT6gkB}s)!P`+djOJ4`u zESSiDZY;FvG}Pyo7=>cAE%ou#y^((R(NuZ@q9gK}F-JoR?&=NEqoTdTiAM=q9Y-;Ku_J-V>p2rjS!eZ@N{O+92-bg@MgSt^` zLg-$?EJ$l6E7>U^c~(R)*ScB_qez!rl@902r<7$e@??>0YeueX8O*o_?@%QG_2IOe8#t{@a;}sV`SKKS929L!_wool=2)A>#ddX z@r*2id4-g#i|=q>dRHlG9lTjc0sU>sc~z>fognCLamj_n)uKKUpN8>8ieVsT}yR%U>>_H&CFE!IxppH=(nJ@Hu^>^Z}y^N+DAd0)+1OitT%IGkdWqz z{EXs)9pR8rcwG$Ertd!le_Qdtizur@1O|2ZJ_01HcHMJksggm^|rlgPbq5d9ukLBNBZP##udF)9M1li~R| z9vvj5*G7x#2`PsNY#YgE|5V7L&(zR%wzle}QF#R`8m=p^Zk;_Z%w^D6CgZc^^sUoO zn}S5oHc;t-ZK9;;idzmlVPHV6*sz0CiblXpZ2?_Y!x}&IbZ?&A-`qga^I$XnUc!pFq^KUZlCQAG`yoR*ib=1923Ht=y4aeaW7ZsZf@+~ zCUg6#sMlUf-CgL=Yf7+aK-VLyqFxTJKwgV-@JUfILh6l*|wMR!iQ!<$kRT3s|TB z9kU6Vm3{rjOWBoWmIc()Tx0|rVB0k7b_gkF8{Ie1(vMa86lQ^K6 zwA&uxJfCaw^_)~>_J!Hb-v(S72{!86W2O=qLd-N5$N<bWBmbcYdGFU87as^(ZvN8X}_D` z6kXNf=)iO^oa^P7Sq5)JPgSm)K|YOL$ZxdHp$FrwgOh@RLJ6`1{FJzFAIt$Hm*gqW%!ZYf^w-*bBn` zoedj%nA7!y7pX|f=>|X7x7Q>$>jgxzf&O~1yk_}?+2kue6G2W$h@uZI1T65e)Br?H zT*LM1Gb*1OP zug&Gr$D<~!J*7VHK>7lLq@`aMQy-GZoqP30L!{EpMHgd zJFs3r#5)xvL5Di|Cip%Mhv}tTl)c)OPp-;ud@>kM%MmxsPDHg?v$gYaAHh|A@zV?= zb5A$-vDXVWyBw#n9j#Wz0Mck)!k)y%edzaKv$n-%4@+q6zhDQe5Z=w;QkRa=gGVi4 z?tAP_l^#O?9p?uYj{qwe`b+&v!_F#B&QWCCEV7GWcL_Bo}rMrGRSPhe*lEwc?5n zX}G5V(RassjaOPsOw6xK6gqg0NVs_5X`-&);b$%GC7fyfw}st=sXeuHpR0AHh_>YI zs63TbE_u)Y)VR0fMwJkuc|uaPv)pfenFVwhuayaOn(Uu1sR%NDhHwK77HGH-2w1Z4 zh~T}T^V##1CO5zHp;W^k*#WhCZ}D&KfjWN=7K@BkhE4#d7dG_z5h%Zm&xs24n$Sg2 zFZp6B#l6^FK}IkdJlX&4IedF538WY7B^Mc)dw@v%jGOkz&fevr3`5R)u+OuC#FtF+ z{-`|Oo3$SNvCkg{z2@NMZ7Y5yTx>N?kaqp6gX^6M9qx}*Vdys)oV?{{9A^U0iEm@0 z2;lvEx`wVEC8$@vn?E@mo*efBRUPO$iyIVQpNF31sOB*R;&b_HCfQfnb}SQ#;j95L zHS2YL9~hnN^7N(M^qNj!?08E%G`P>&+Nclu*&zy{XT_|B=sr33n z$jIonjJS38?l-$UZSu$89sr?+)8+%TU#I#GbXPc#vuzEV^TEz25q6BVQguv8N!kJE z_)FNA@kBPRd%nHS*9X>7pI0h3vFy1HMC{m72l!ZHRk_Z#_LzCH9n%XD?pdq94+2 z&2`jxGMKB=CC^~2(lXB9CE`T$cwr>?{JZI@I`e4u{Hy1QeAV>Q%PVLmIO6ml7{@*`>n z$kS-*2l=!}sw-t$9LK3fduIpwcTCs1DkNHo)a@Yw8?olO8gXpOSrPZM$LfVJN?oJA zQ1q!r^G85BM7=2-rwsUQ> z7(`qOI5;OICV$vD81A2ju+B zLKE4Zp7{$`Rm1u^EN?Uz3ZqC=#lq?*s>0+yEc zvQ7kS=Nr4$@~VKh^u|2thjGu*eAXWk z83|!6QA%h7o}0wUt3Yq@>_t_53sb`3ZpwxyCNNk`Hx??0h{$kkSj23!l=6*Y0(JlM zCa--?4_8}UFg+)2%WxAn2A_s8;d2p8hk&juX+h)en$O5wTHRzaj$#9ispzOMS#Q-n^G~V&nw_uf+UeR?y1#Kr2;Wb)GM}Z2{PUb0Yns9O zlK><~UqYXbn&TjZL&qMqG}?MNxSNRneaO4KJSw*KYjChij?V7ZGL29Qz{U`i&!ih3 z3vN-KdN`&Wc|{)fnkcfyjaAk?=!S)T@<+o2EC=+pBl#Xn>jr1L4S6CHRBpj$(9SMF z*Om%9W7MT)y6Lg7N**%~~CJh9-6cC`P@Fxtq%{npULGLK;X$TIuqyB3v>qJ;Ji z`o$$Y(M%)g)^?bE*ZBcB6@isK5FjWzY3DCy^`$XO>9vmwTop{Tsm<)mO6lgKRRz|T@lB0Arynn z-X6G7IA<~P@LR1RZYkwr2-8m-kbAdqxT0_5b8I&{zM|LKw73OhTM)P!u|>fIvjrER}|Q4UJh~cn)gI zNe^|2K{1+6#NwXrPzBaF!7g^>xk707e3M92>U4=U+xBo>3G&XZe8*3yfanqpdZsXFtEmz zBRi#u2GLF8+CooNDK#^)VZd(oy1wpZHJ0UEwz(%VE5Z7SBZ*PoTO3mk!@ z#AeX&#s74>`K2N*VEbA-%8xj9wcXJn!2P`p7ES$P3;Ow!1`zjcQ+$Z{X#s(wR6k#< zUOT`RzFrGF@_Pf4N(gAdm|*K&ss_;WMl2soE&A%(s8<&b#t*Od&=YtLZ$W$|UN85H z^0P1I27@Y5)k2x0pLq*%qqF!*zBYsviUSJ;XFJs+fl70#*~P;`4cTg~Qp=%mA^7A^ zWxe~LZUM6V`Yz!Al=#Srh;U$!d4?xqK6sKDQC}eFB6~`c-zKX<|4>nhn#Au?kU#R{ zj9h<*==9^@9;>vxX`Axp%LSrjFK+{4#V8XKU=b2%a3JK_cLS>{|VU?dJkW?-TOp7;TuRFZJMC z984znwE~}{j`X%ej%<@z3^L|P1pjk$8U%ITneFLOsrMwjr2~6B9KG2JEFEMs)V~j@ z{__I#n&z3XDMswIv}FVAAs{Cg*wQ(33x{OvQ7D;E!r@zN)2`qM%-Y9#nwpyoyFh8b zTh=H$9sPt{L&F^#axas1Br_lYfGGkd3t8Zeq><(_;Z;m5dva`|;0q8xVE%J9l<`0V zL?v)Z9g{!xSr9ix>}MveuAqv9`S+cto7*={SdMC)0AROYN=izuy~iX0&XTHFK~O_I zoUFeDq3ZmAH5Ln7XHV3D4b})&nE+3v$cdxo6Gcf$`mPNw?^TGQrysF}Ei)HzUt8(& z<5GoVfmPD&+r0a}kNIRZYz9xAEz}Bp&a4yDLCXl@3EP;jF)A+3w+>F87Ndh7myqz| z{l#;)qaE5>=a|^n-%wXXDBZ^R?2X~_H2Ti_k&{t?MC&@(H#NykMIy!H?8=&d#+CH` zm>5&@j-)#tGV{Ld3rVb^NH=e*1!(Zz+P8bE6--1y1%pXdLVbtT=eL170|QvOfC22Y zWBkYglqhP$Tg2CY1(?MObsIwPgR(Np8(0B#=@*{%skQ3Wvq-~J|Kxkyxc>^sKAwGN zUIrB(vHwyFkAR!O3K)_Ik*?ScwUy}4*yofpONDg%bW>>NkZq<)*)jEFuVNpt`aTn+ zV|%bK-s}3@M716Q7V+2lGMCEd6+nQ zlL!2Y=K4HZ?R)!mN4ppZ9b>%{ZJW0c2)c)B=VxOQzlbNMjnx;Ck#zIqKj03dv4Z6de>LU8VjNr7vZ1OwBPZ*rlSr!V{fe#)P!Aj?BUVn^f_Gwzdiy?u<*enF zw74@PmgtG`#yE0se-i|8va`-ANXfO=OIZF+>%uxA6Clc<>sQgD_iT#^t{rW=bNbSq z(0%yh+KgT%*Cdx`lscgvU?aeC>P+iP_|c=v{W*=s-u8tA!lRPC) zp*b+F4$gauyqP4Y}+3KUDR73Q;_Y3{ggFPsF^DVO^pkcBZw(+aiXjp}TI;r~YF4mwh-J&yS8? zf%fe40<=4=!sB*BOw*qBbd*KO&`CiUP_9fo8-Ta%{X!`Ix#jVDDVkne5o08exK;jE zB3dPFK6tPJ1>`7ET6CyqPX777HzD0N`VHnRV5I;Ex&@(}RaXZSpu_69 zd&e8FVtQ2UgQ6?%Q(^|T^{oCNRF&(-C+BhF`Q|42KQf8xGs9v!GJR~%f<9v{|DXzb zz?9fnn1L0#augWY$J`&x$Ml)xbKoYH4aaAOdu%y5)PdNy(r;zH3?GRRBV?G1YsgXaQ&!;yLO^&)GCV|%hu+Ry34%UktcTbKBd3SA5g(hZHILkM>dcogk zyI!5{>5~nS9L<`FGD{nmv>uB(YNcqFme<;D7N1U@3@|eXeJ79z7k}_eml=vc1eaIZ ze6Sx>=pwzYuSe`gWAN$d=o}Zqy~>C3w^P;!Jztb<^vg!?ef&X(ImpT6ru7F zy-$<-*qSaG8}wt>4+YOWQRQ=V8lrse?+@`B&bYZldgDU+?G|)hhjLgtXEdL<&*drX zltHD8jF1@q4B)&VI&BM-;~XaozmJT{m3qdQ&(;F8U6i^u;;@9q;6{mIn#oJ)|9 ze0SfO{puXkBe?5g-Nd!?<~h*!37pC7=zAz^5jsV%>k#@sM_(xB#>Kg%05s)v!pCxtm3-PQZ{c<9W;(SSen^NdDcQ zcoFH{aaq6JS1k48aT_-J2cTcJt2bpAq&zo1M^PXb1s&zWw3Y`}T>Artymh?Y28_p_ zS%K>jfiBft3yX7%wmWef@B)pYEkp7X(u7r3*~)Q_hT zVQ(cZPsSYCj!w)~^I-Dw84f+!sEo@NaVXL>+3x{`o86gDtut%}=C~ps-613yLK@%4 zE-Z?Eo*rOS8k^bdY*HN2(M6h{mQ`jZiurj^n9mkZQG#n*+Rn+_O$E% z9Iy7mrLOY;PR%7kw1?f>OIE~tDHGjWS4YQ~r6VUxnbG6p`-2EVQPCoSHRElsS!>t% z$jm;^p%&x}ZxPwBW#f73gCVVbH^!e;|j*c_Z^e2~|Tg+km_}wnoJ5MCS zhYaj&LcM;66A3s;xuiB)vR%(gKF@VDobcT9dWJ11y6DDK zc^Q|}=J#N-(Wp@c0m{_6#o4W6ugi$Ox+Yt8Au3>C-*6aTOC88l+(Yp~QUVj9P@NdI zg4@SM{JZT*NPpE|qMis*$1q*{KNm#YrAIB(IKm?D66^cgbrA)!4jq}P{TCGO2B*lI zv(C534c6wr%xQDn!<j`i;XxQEU4Xo;$(uWu>(h+|&UYty%G0h<&H4YYN*T zGIjx~A^vlpU@}2ELg?1Q=sXcvYa6!$bCm0@Dr%%ed0e8Tm7lneZYvBn)t~*es0PhX z0;RbBiO0^mcCD3rqw~14`|-9Sc-2>D{YG;3M!su3|L(5!xDu~4o$H{E$kG(yhp<1Y zWnCw^{LAh`JtMS=StTW$MqQz~LJLRu*D{-r5s?*r;Lt24IVB0+?r;r1&j;o;Ph z>V+xn~<&dBDG^^d_^R!VtqJEdPs7KcGN;GTRBZb2-`|2QJ4}RFmHB{*G8oJD7 zFhD;fC>RnF!oP;!wGpL3y0GC+;o+uVctvid$4!>5j@_#TT;Ycx@muDHI_n$%(3iOrg5amyPJ1yJni3V{DS!1MPPi< zE0Z&(A5-&ZvT0%Q5ZYvE;*97tD1??Y91o(z8V|;q%Sy|*>cGtgr^49x{N_@-M7_)# z9e2a>x=@>)1{=$=P2m%R{0kDwAFMq+$v>JY47zR9)a-x42}n1VoZe`xu)cOpH*jrZ zKSDaPcc*uy=G+<9TRJPnXLeZQp4|9?I?Sc%34@*`>Rv8rfHQh)&Jq|*Qf1T`N)}nl z!nM+<9k}X`d!JBdfkdo;#GhUB!sZ^J+HdTMfu@Hs)^p7 z*0r|OMl_&gfu(k9Drl)K!1QQU@A>=oul;csFXvAxagBFpdb`6zy-i|={yZ`>a32V} z+}R)UP42WFkxF(Jt-hFW^^OaJUXHNn)#gvS+ge)>utMvFP3yN8JVQFCW+!!OES%@v zIuVYeC}g96L6fXjj8?Ik{h-K3AI2gdK1^tSX z>Lw|8qvyUShiUzB(v{KK?3Sgo*7!(7k-^1vJp!F#+Zna%Ix500dh;g}xMy{XTM7d!4wobf@+w+C&+rKKYu^&KVt`0dHyib#t&bHrbi1^U3#WC<+Z9e5A|2*CrnYM zd*Rq3mn}5W67)s;J2VTVVAn(BquHxSEnL&xnaTn0l+&B~Q)YIEhn{e^p<;)_;k8Sm z06!TSD7|O(Xns)!$SX-MtF8V_gv<1Dz<7bZ_;V&c+${n6v@D-5j(eweoe(pUR3gR4Ub#O^4n8g@LFW(O` z^aSdIM7VJFenN?ShT}s1v{s43xO&c02*ihViM0so9XpjtztWS0+v~~xcP~B zkps3rrKH1rLH}SSN3MgetL;D03R@MHldG4`goxSKtgHozV`z=JDZQEztj6P8G2W(~ zMMHa|t^-kn%G{z&f2wry@^P311$c7TvzHI)Q%Hv~A7)7Iey?g+<2DCz=*)ZdKBLR_ zZkR|V1Nbt_=wj8Y6q1c)T{w7U9Iu6nEAwNz!9_W?&Z6nQmoMC7zm0bY+9I!adpzF6 z6<^HH^9N=&usDuGx*~8W$|;{wHl$xiFOB>NGZ@g_Tl!@QO%k0u)6g5f*$9S)#>;L0o3JVT5ae-*Fwx(Z zEw3trjr%lK=P0f;XJCRn?qmIDg*%%MZEjA_ivBh@h>r=*aa6zedAN4$%}yXX(L}R1 z{};b~Qo-g^&{;#qa{KQu@L6;ksia4QY(9oC8_NC>h}GMOnZdKW*;TVT2kN(euvBvAz&a6Y#p*-hD|D2z2f*{n8?!|swazd9em;%v; z;{GPieF^#64Yd=QoREQ)&W%O%v%Vk$w-5ff6e*JH?ESXhP}73G8LBrS^OVv@t9U_{ zx;?6@ax>dGr4tf2du=}3>27<_-RP-UJ9;$J{650Yd=-w%>f}}v}nBleB&$)*b4CPrQYLz#uTCy`*-~Q-$(x6xPV~r zAFKT*PNo{8!NinT0YCD5@*%nTU1fFYg8bIY-^adp>o5iX#*@{f>wuY~-OCZd1p*gb zm%q{HCew>V{y{yxQH886lu7LLQJz6s`cWzN|%#!$Gb>4dQD+$UG%EG}4~ z4F}eLe~r3!veu|1ezO}z8Hp>`c0Q*XI?YeSDIoT{rBF^={io)crT{rga)$iEQ$U?< zYgXfKZBU~*FE`!d9k6#FdG*54iaF}3c{nX&7U{@}{+4)nu$c^hI%9`#!}F8)uIwaN z(-{+FDX~**a+-^>e4XEH#edt5K&yqVW|kNq2+)cR?Mia4eZQaVVAtEE7t38{D?jNL zvG-A@Th9`wg6(EJAX{A?t-B3j()Po-k6diR0-s&sz1I(6jii;i%kf>NQ{`=fwx>|m;@!J{2jxev{&gMW8(!B?+ND1~_%@@SWF!jO zMiWUxEUU5F3~44;oVksur?N**Dr$r7Vc0D>5i@_MaWh>m&76d)V<2O|Snbow zlJ~uoj0p*TGhclF-n%`m%fL}@?#dwO?HljK#VH>E~6yu-Bd zUx#o9FHn<>>llYYU(dl3{%+1Ap+(0CZbZ#Qv=Vi+qk)S)_E^Nfd;VGHS;x@#p?z~E zMtnE;X>vLX!o|L9;NJ^t=$*%t@nEd9XT4AEHRLsoz=nPOcf((nLed4>Kh}E-i#nsm zqW(S7E9Eyi(ke00mY)*-ZyCz!u_NQ=vUU#l|6fkycu59=_*ECTvd-VgglzqL);_f~ z|HKRA2ixBhh2+SJTj<@4G^BWKm)|~s6xv)(TB@T;7fO^e9i*{Q84$Bp1s4vV4$`sI zBe#0=Z=wyoPd4(Mz22~h6#CM!2!R{-oQa-%Ht7R=)&Uji4&ya7d28uu9q7y2{`H?% z&QFETo*>09ePP=I7W<~FgJB}P;>1DLYWP-U0)In6(0;;RwM^&u>(;Kp{IMI`^*D8n zZOgA6QFFCHA&*$nI;dkHl~%Ep{LD-ru{yS5^5220#4F|$txP6cxC2tdS;Ew z+IRC+zCO*5@HMYljdiqCqO{Wb_skXKo^s{adMznZ?7`!}RUJc%>TPwRZ|g+=w?koi z3&m-BO%ryTxz!D~iNcKIUU8S+SE`bxGw0!2(=#bV#~SMqiR&io8O(WIUWXxwpXTk~ z^UcirP<1ZqF;)4{mR_hkl8xV_$sTks6z6;SEZUV>wYJ*F2T2~AgZxe&s#*Ilp3AD| z4+w)@B{o4{xDM!*^qd}_Q2m|GyX}#Au+o(W(cSwbrsUsaz0U^o^S!vS+?ynWxCeA;enc{B9HYLQ;OnYu9Pg^c`)Aan}Cc9|n#yvsQ%Tp6O z&P&&2GenfisaLvSE}4?mSq=)=SOk*yT(xtymG1vk=06?2dx6}=K5nrjmkLl`%9j)` z`1_`+%HXDcef7C{a46(uN!#(ED!lvrw>a!k4@hs*P}fW=!%f_zY|IDu+Vlcc3fECX8?|Q_jcG6e^n}ccGh7p&3zl2-c-uy z_r8(oU{Nqd{IQ#?zV~l~;~i8zH+%hJ=nix*^=L7DT$+gzPOkuC2GmA!P^pFdT7}Eg ziMUv@soDMG6c00aYi#PJv@zRxV_k6XH%yIC_P*8rqJ>1L04nsVPh-DFRun!-3Xd1e zYq=x|ouXCk#Mwr)Km0d6@oLWqIGp%FdxVeG&!&0ZyO#Jj+Rfwbk&oQx9tpyyQMG-0 zgJr1W3DWBxK?gYXT$rsT`*8|+kjfsX;Rk1gq$^vYP1AOq9ztkCbQcjEOf(%wsmN?=Lt* z6r7y5qV{@sK~#G#g~!m%8F4I{nuz?kF@Z6@YbtNP1%F!HO>>hYtOE8D>ZnY0MX046 zR#S|QKgI6Ge93mf>@X$c<(|B9e?_iSYtk^p>0l#JJDm~i)xB&AqmlN1VBTF!IpWKS9sfmKBzJXO?|xgXrRt2$5ML`D;#@E^o5j)Lquo zg&c+w3u~&~_?B<>r+f1e4iR@fC;O~z*Of_UM3Rdpd^v&;>E_~$NE^gpj{M+k>3Jpy zSJXLTJ$ga?cgAOjOU72Hq3=obF|SCca2q={y00V0L$^f-wG)bFucT}mU8PU$alJ1u zws$uh&nmkdF0tuor3XEF3bkPBCRQ8E5n&?J37+e4{c;C+o&T?|YYl4Z+QMKjSf?WQ zwlTCkqCiw$feHi%qE)~Yq9P_J6fgo}q#>Y`gm4Lxs2O5!Qs5E-1h^5UX^;Y;kbn>b zq*fl`P0Py!sk{P0f(Y_x2)PG|Go4P`ALq}SeP*At*ZS7_zVFZWVE|}pdy$5>F&unq zq^Z=U@6hwjaoZ7s!ouy}owtrn$7vUvzeBNI((j%(dRS4H;CU%=WE8-2THTC-_&v)H zNKL?VS)_hP2My8Gv&hP+D8E*y=`TrHd2-?%K3`>Kk$ZNA)r3;1E0*hPCCOKOBge>d zz-%a-ul2?~@J{y03sogMqpB2VNAIt7xu^|g#Spg$`ciG~L$RX!PVS+S&J}9+@?n(S z=(DlfslLxzX^XkUQkj;r%$awu)SlZM=;3ii{{OrwqJGZVD#OR*UvWFr{4u+@VYHvR zlLjI_a*o`T9ukQ}t@1v-NG=v7w5_%fL0#tyM=W z<6{Ns`|fHzk}}L27Shd9H2$M&aYCwEzd@xQXvipTC|tJ=8k6kTt`ymBHCcu$c>Sr` zxPA3{7`GtGvHdk%>3)L}a7iQQxlosTUq*hW7P%K}`bE?q()IAiEXTFejS17#0o81T zw`u&VUj*jzNGK|I>vfzKQ9)7LHzh%fk4AYht@0jCypk{WDFhAy7NPy}BeU0Qm$VIUSu!y2`>bUaaH9VUh~s-klE?Q1*WU0`O%5Y_1B?v17_!EbX^15DOKuN2Z2% zHoH^$e#b-kKK)n_KCJQ6Jauk<;7OxS&FLw*3D1Onu3n(?7Enx3$z%b{g_^@JOYn?w z!G&Dia6rBh?P(^L>+nyf^w{8MBu+?=KnGtLZ)i~9P*YcfsPJoL8q9iV8-BdNlfx~y z5#iTg*LwvG#kb|}ypB>IJ6@iEj=?EcH(UvjM+Z>e7!a6i4&C1RQ@w#q5U=^_feA-; zk#EO0W}+3Rpw?Qp5}j8~tMh=lTsy2#*b`>VK?>@ z6!M&O+y`t%3&o=k>izGzOiql$y~%}g(f zYQqZdAGSp>IcXLs4-DCrU&)CZqF32qT+d4uSw)seZ*L~aKyJkQ7A5&{UZ{$FL5AtF zm$m$U9n0AWm)M2{1Gsc<;`=6lN3XXD5QsM?!-M!AUJ62p%}QBcgKZ^Gl)}^lsWSTQ z?T>H^&a}cwyB~|{Y4ugb_IgepCIW|_(H)mRxhJ(^XO|ZdTgbRc%Br$#Df^vTo3^pD zOq}xX8`b}xvqcFh(FA9w6?ne4x<6=n*p4N2kM*T=${hPg#V|SKoiR2dq0d*@FVDZ6 zQJF`uL1r^!;N+pt$8Ji_Bp#xoEs*^wWpT-Ix`GeDkTz%YMOcz1V4zp~p0$HT+2AK$ zR>KgnuA-5oL=6}h+~A)M#1i?}gSMmi&t6-?w2yW0e+g;be(gyTj~ys{z_;a;t5ikY`T{ Iy~1z*A6)x4;Q#;t literal 0 HcmV?d00001 diff --git a/docs/static/img/go/api-select_framework.png b/docs/static/img/go/api-select_framework.png new file mode 100644 index 0000000000000000000000000000000000000000..aa7f112fe9454e3333ba9be39d1382ebfa4d5e70 GIT binary patch literal 85996 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 Q&3Y$X$KYzQmi6QR0Vv9hVgLXD literal 0 HcmV?d00001 diff --git a/docs/static/img/go/api-select_jwt.png b/docs/static/img/go/api-select_jwt.png new file mode 100644 index 0000000000000000000000000000000000000000..161c096b171a699c6eb2d765d37b428df7b588c5 GIT binary patch literal 119147 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 a{b!tUw(`ru*+m|pG~ns#=d#Wzp$PzXy)XR$ literal 0 HcmV?d00001 diff --git a/docs/static/img/go/api-service_user_panel.png b/docs/static/img/go/api-service_user_panel.png new file mode 100644 index 0000000000000000000000000000000000000000..d4e9b5beff9e54da0ada41d1209afb064e637af3 GIT binary patch literal 116026 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%0ABF0OAg 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 literal 0 HcmV?d00001 From 6929c680c4d99ab4a0700ea8a33e13fc49ca817d Mon Sep 17 00:00:00 2001 From: Elio Bischof Date: Tue, 20 May 2025 16:14:30 +0200 Subject: [PATCH 38/76] docs: specify deprecations (#9915) # Which Problems Are Solved We have no standard way of deprecating API methods. # How the Problems Are Solved The API_DESIGN.md contains a section that describes how to deprecate APIs. Most importantly, deprecated APIs should link to replacement APIs for good UX. # Additional Context - [x] Discussed with @stebenz during review of https://github.com/zitadel/zitadel/pull/9743#discussion_r2081736144 - [ ] Inform backend engineers when this is merged. --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- API_DESIGN.md | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/API_DESIGN.md b/API_DESIGN.md index ac7c4a01e0..9e77657ab0 100644 --- a/API_DESIGN.md +++ b/API_DESIGN.md @@ -48,6 +48,52 @@ When creating a new service, start with version `2`, as version `1` is reserved Please check out the structure Buf style guide for more information about the folder and package structure: https://buf.build/docs/best-practices/style-guide/ +### Deprecations + +As a rule of thumb, redundant API methods are deprecated. + +- The proto option `grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation.deprecated` MUST be set to true. +- One or more links to recommended replacement methods MUST be added to the deprecation message as a proto comment above the rpc spec. +- Guidance for switching to the recommended methods for common use cases SHOULD be added as a proto comment above the rpc spec. + +#### Example + +```protobuf +// Delete the user phone +// +// Deprecated: [Update the user's phone field](apis/resources/user_service_v2/user-service-update-user.api.mdx) to remove the phone number. +// +// Delete the phone number of a user. +rpc RemovePhone(RemovePhoneRequest) returns (RemovePhoneResponse) { + option (google.api.http) = { + delete: "/v2/users/{user_id}/phone" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + deprecated: true; + responses: { + key: "200" + value: { + description: "OK"; + } + }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } + }; +} +``` + ### Explicitness Make the handling of the API as explicit as possible. Do not make assumptions about the client's knowledge of the system or the API. From a73acbcfc304b49642b3cf35cda0acb020cbc4f4 Mon Sep 17 00:00:00 2001 From: Livio Spring Date: Tue, 20 May 2025 19:18:32 +0200 Subject: [PATCH 39/76] fix(login): render error properly when auto creation fails (#9871) # Which Problems Are Solved If an IdP has the `automatic creation` option enabled without the `account creation allowed (manually)` and does not provide all the information required (given name, family name, ...) the wrong error message was presented to the user. # How the Problems Are Solved Prevent overwrite of the error when rendering the error in the `renderExternalNotFoundOption` function. # Additional Changes none # Additional Context - closes #9766 - requires backport to 2.x and 3.x Co-authored-by: Stefan Benz <46600784+stebenz@users.noreply.github.com> --- .../api/ui/login/external_provider_handler.go | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/internal/api/ui/login/external_provider_handler.go b/internal/api/ui/login/external_provider_handler.go index d198978f1a..bd7ba7cd58 100644 --- a/internal/api/ui/login/external_provider_handler.go +++ b/internal/api/ui/login/external_provider_handler.go @@ -639,9 +639,10 @@ func (l *Login) renderExternalNotFoundOption(w http.ResponseWriter, r *http.Requ } resourceOwner := determineResourceOwner(r.Context(), authReq) if orgIAMPolicy == nil { - orgIAMPolicy, err = l.getOrgDomainPolicy(r, resourceOwner) - if err != nil { - l.renderError(w, r, authReq, err) + var policyErr error + orgIAMPolicy, policyErr = l.getOrgDomainPolicy(r, resourceOwner) + if policyErr != nil { + l.renderError(w, r, authReq, policyErr) return } } @@ -652,19 +653,22 @@ func (l *Login) renderExternalNotFoundOption(w http.ResponseWriter, r *http.Requ human, idpLink, _ = mapExternalUserToLoginUser(linkingUser, orgIAMPolicy.UserLoginMustBeDomain) } - labelPolicy, err := l.getLabelPolicy(r, resourceOwner) - if err != nil { - l.renderError(w, r, authReq, err) + labelPolicy, policyErr := l.getLabelPolicy(r, resourceOwner) + if policyErr != nil { + l.renderError(w, r, authReq, policyErr) return } - idpTemplate, err := l.getIDPByID(r, idpLink.IDPConfigID) - if err != nil { - l.renderError(w, r, authReq, err) + idpTemplate, idpErr := l.getIDPByID(r, idpLink.IDPConfigID) + if idpErr != nil { + l.renderError(w, r, authReq, idpErr) return } if !idpTemplate.IsCreationAllowed && !idpTemplate.IsLinkingAllowed { - l.renderError(w, r, authReq, zerrors.ThrowPreconditionFailed(nil, "LOGIN-3kl44", "Errors.User.ExternalIDP.NoOptionAllowed")) + if err == nil { + err = zerrors.ThrowPreconditionFailed(nil, "LOGIN-3kl44", "Errors.User.ExternalIDP.NoOptionAllowed") + } + l.renderError(w, r, authReq, err) return } From 490e4bd623c4c87cb8dd683d500a0bcb795f7da6 Mon Sep 17 00:00:00 2001 From: "Marco A." Date: Wed, 21 May 2025 10:50:44 +0200 Subject: [PATCH 40/76] feat: instance requests implementation for resource API (#9830) # Which Problems Are Solved These changes introduce resource-based API endpoints for managing instances and custom domains. There are 4 types of changes: - Endpoint implementation: consisting of the protobuf interface and the implementation of the endpoint. E.g: 606439a17227b629c1d018842dc3f1c569e4627a - (Integration) Tests: testing the implemented endpoint. E.g: cdfe1f0372b30cb74e34f0f23c6ada776e4477e9 - Fixes: Bugs found during development that are being fixed. E.g: acbbeedd3259b785948c1d702eb98f5810b3e60a - Miscellaneous: code needed to put everything together or that doesn't fit any of the above categories. E.g: 529df92abce1ffd69c0b3214bd835be404fd0de0 or 6802cb5468fbe24664ae6639fd3a40679222a2fd # How the Problems Are Solved _Ticked checkboxes indicate that the functionality is complete_ - [x] Instance - [x] Create endpoint - [x] Create endpoint tests - [x] Update endpoint - [x] Update endpoint tests - [x] Get endpoint - [x] Get endpoint tests - [x] Delete endpoint - [x] Delete endpoint tests - [x] Custom Domains - [x] Add custom domain - [x] Add custom domain tests - [x] Remove custom domain - [x] Remove custom domain tests - [x] List custom domains - [x] List custom domains tests - [x] Trusted Domains - [x] Add trusted domain - [x] Add trusted domain tests - [x] Remove trusted domain - [x] Remove trusted domain tests - [x] List trusted domains - [x] List trusted domains tests # Additional Changes When looking for instances (through the `ListInstances` endpoint) matching a given query, if you ask for the results to be order by a specific column, the query will fail due to a syntax error. This is fixed in acbbeedd3259b785948c1d702eb98f5810b3e60a . Further explanation can be found in the commit message # Additional Context - Relates to #9452 - CreateInstance has been excluded: https://github.com/zitadel/zitadel/issues/9930 - Permission checks / instance retrieval (middleware) needs to be changed to allow context based permission checks (https://github.com/zitadel/zitadel/issues/9929), required for ListInstances --------- Co-authored-by: Livio Spring --- cmd/start/start.go | 4 + docs/README.md | 2 + docs/docusaurus.config.js | 8 + docs/sidebars.js | 19 + .../api/grpc/instance/v2beta/converter.go | 246 +++++++ .../grpc/instance/v2beta/converter_test.go | 390 +++++++++++ internal/api/grpc/instance/v2beta/domain.go | 50 ++ internal/api/grpc/instance/v2beta/instance.go | 32 + .../v2beta/integration_test/domain_test.go | 350 ++++++++++ .../v2beta/integration_test/instance_test.go | 162 +++++ .../v2beta/integration_test/query_test.go | 369 ++++++++++ internal/api/grpc/instance/v2beta/query.go | 70 ++ internal/api/grpc/instance/v2beta/server.go | 60 ++ internal/command/instance.go | 10 +- internal/command/instance_test.go | 28 +- internal/command/instance_trusted_domain.go | 8 + .../command/instance_trusted_domain_test.go | 39 ++ internal/integration/client.go | 7 + internal/query/instance.go | 27 +- internal/query/instance_test.go | 11 +- proto/zitadel/admin.proto | 16 +- proto/zitadel/instance/v2beta/instance.proto | 192 ++++++ .../instance/v2beta/instance_service.proto | 648 ++++++++++++++++++ proto/zitadel/system.proto | 54 +- 24 files changed, 2781 insertions(+), 21 deletions(-) create mode 100644 internal/api/grpc/instance/v2beta/converter.go create mode 100644 internal/api/grpc/instance/v2beta/converter_test.go create mode 100644 internal/api/grpc/instance/v2beta/domain.go create mode 100644 internal/api/grpc/instance/v2beta/instance.go create mode 100644 internal/api/grpc/instance/v2beta/integration_test/domain_test.go create mode 100644 internal/api/grpc/instance/v2beta/integration_test/instance_test.go create mode 100644 internal/api/grpc/instance/v2beta/integration_test/query_test.go create mode 100644 internal/api/grpc/instance/v2beta/query.go create mode 100644 internal/api/grpc/instance/v2beta/server.go create mode 100644 proto/zitadel/instance/v2beta/instance.proto create mode 100644 proto/zitadel/instance/v2beta/instance_service.proto diff --git a/cmd/start/start.go b/cmd/start/start.go index 52d9c6fba8..6f04e8ee82 100644 --- a/cmd/start/start.go +++ b/cmd/start/start.go @@ -40,6 +40,7 @@ import ( feature_v2 "github.com/zitadel/zitadel/internal/api/grpc/feature/v2" feature_v2beta "github.com/zitadel/zitadel/internal/api/grpc/feature/v2beta" idp_v2 "github.com/zitadel/zitadel/internal/api/grpc/idp/v2" + instance "github.com/zitadel/zitadel/internal/api/grpc/instance/v2beta" "github.com/zitadel/zitadel/internal/api/grpc/management" oidc_v2 "github.com/zitadel/zitadel/internal/api/grpc/oidc/v2" oidc_v2beta "github.com/zitadel/zitadel/internal/api/grpc/oidc/v2beta" @@ -442,6 +443,9 @@ func startAPIs( if err := apis.RegisterServer(ctx, system.CreateServer(commands, queries, config.Database.DatabaseName(), config.DefaultInstance, config.ExternalDomain), tlsConfig); err != nil { return nil, err } + if err := apis.RegisterService(ctx, instance.CreateServer(commands, queries, config.Database.DatabaseName(), config.DefaultInstance, config.ExternalDomain)); err != nil { + return nil, err + } if err := apis.RegisterServer(ctx, admin.CreateServer(config.Database.DatabaseName(), commands, queries, keys.User, config.AuditLogRetention), tlsConfig); err != nil { return nil, err } diff --git a/docs/README.md b/docs/README.md index 92d3f8f279..34803a3629 100644 --- a/docs/README.md +++ b/docs/README.md @@ -6,6 +6,8 @@ This website is built using [Docusaurus 2](https://v2.docusaurus.io/), a modern To add a new site to the already existing structure simply save the `md` file into the corresponding folder and append the sites id int the file `sidebars.js`. +If you are introducing new APIs (gRPC), you need to add a new entry to `docusaurus.config.js` under the `plugins` section. + ## Installation Install dependencies with diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js index 1f45a017ac..6a4429cffe 100644 --- a/docs/docusaurus.config.js +++ b/docs/docusaurus.config.js @@ -364,6 +364,14 @@ module.exports = { categoryLinkSource: "auto", }, }, + instance_v2: { + specPath: ".artifacts/openapi/zitadel/instance/v2beta/instance_service.swagger.json", + outputDir: "docs/apis/resources/instance_service_v2", + sidebarOptions: { + groupPathsBy: "tag", + categoryLinkSource: "auto", + }, + }, }, }, ], diff --git a/docs/sidebars.js b/docs/sidebars.js index 92c7a00b2d..b7dc3fd8b8 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -13,6 +13,7 @@ const sidebar_api_org_service_v2 = require("./docs/apis/resources/org_service_v2 const sidebar_api_idp_service_v2 = require("./docs/apis/resources/idp_service_v2/sidebar.ts").default const sidebar_api_actions_v2 = require("./docs/apis/resources/action_service_v2/sidebar.ts").default const sidebar_api_webkey_service_v2 = require("./docs/apis/resources/webkey_service_v2/sidebar.ts").default +const sidebar_api_instance_service_v2 = require("./docs/apis/resources/instance_service_v2/sidebar.ts").default module.exports = { guides: [ @@ -840,6 +841,24 @@ module.exports = { }, items: sidebar_api_actions_v2, }, + { + type: "category", + label: "Instance (Beta)", + link: { + type: "generated-index", + title: "Instance Service API (Beta)", + slug: "/apis/resources/instance_service_v2", + description: + "This API is intended to manage instances, custom domains and trusted domains in ZITADEL.\n" + + "\n" + + "This service is in beta state. It can AND will continue breaking until a stable version is released.\n"+ + "\n" + + "This v2 of the API provides the same functionalities as the v1, but organised on a per resource basis.\n" + + "The whole functionality related to domains (custom and trusted) has been moved under this instance API." + , + }, + items: sidebar_api_instance_service_v2, + }, ], }, { diff --git a/internal/api/grpc/instance/v2beta/converter.go b/internal/api/grpc/instance/v2beta/converter.go new file mode 100644 index 0000000000..8bff682606 --- /dev/null +++ b/internal/api/grpc/instance/v2beta/converter.go @@ -0,0 +1,246 @@ +package instance + +import ( + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/cmd/build" + filter "github.com/zitadel/zitadel/internal/api/grpc/filter/v2beta" + "github.com/zitadel/zitadel/internal/api/grpc/object/v2" + "github.com/zitadel/zitadel/internal/config/systemdefaults" + "github.com/zitadel/zitadel/internal/query" + "github.com/zitadel/zitadel/internal/zerrors" + instance "github.com/zitadel/zitadel/pkg/grpc/instance/v2beta" +) + +func InstancesToPb(instances []*query.Instance) []*instance.Instance { + list := []*instance.Instance{} + for _, instance := range instances { + list = append(list, ToProtoObject(instance)) + } + return list +} + +func ToProtoObject(inst *query.Instance) *instance.Instance { + return &instance.Instance{ + Id: inst.ID, + Name: inst.Name, + Domains: DomainsToPb(inst.Domains), + Version: build.Version(), + ChangeDate: timestamppb.New(inst.ChangeDate), + CreationDate: timestamppb.New(inst.CreationDate), + } +} + +func DomainsToPb(domains []*query.InstanceDomain) []*instance.Domain { + d := []*instance.Domain{} + for _, dm := range domains { + pbDomain := DomainToPb(dm) + d = append(d, pbDomain) + } + return d +} + +func DomainToPb(d *query.InstanceDomain) *instance.Domain { + return &instance.Domain{ + Domain: d.Domain, + Primary: d.IsPrimary, + Generated: d.IsGenerated, + InstanceId: d.InstanceID, + CreationDate: timestamppb.New(d.CreationDate), + } +} + +func ListInstancesRequestToModel(req *instance.ListInstancesRequest, sysDefaults systemdefaults.SystemDefaults) (*query.InstanceSearchQueries, error) { + offset, limit, asc, err := filter.PaginationPbToQuery(sysDefaults, req.GetPagination()) + if err != nil { + return nil, err + } + + queries, err := instanceQueriesToModel(req.GetQueries()) + if err != nil { + return nil, err + } + + return &query.InstanceSearchQueries{ + SearchRequest: query.SearchRequest{ + Offset: offset, + Limit: limit, + Asc: asc, + SortingColumn: fieldNameToInstanceColumn(req.GetSortingColumn()), + }, + Queries: queries, + }, nil + +} + +func fieldNameToInstanceColumn(fieldName instance.FieldName) query.Column { + switch fieldName { + case instance.FieldName_FIELD_NAME_ID: + return query.InstanceColumnID + case instance.FieldName_FIELD_NAME_NAME: + return query.InstanceColumnName + case instance.FieldName_FIELD_NAME_CREATION_DATE: + return query.InstanceColumnCreationDate + case instance.FieldName_FIELD_NAME_UNSPECIFIED: + fallthrough + default: + return query.Column{} + } +} + +func instanceQueriesToModel(queries []*instance.Query) (_ []query.SearchQuery, err error) { + q := []query.SearchQuery{} + for _, query := range queries { + model, err := instanceQueryToModel(query) + if err != nil { + return nil, err + } + q = append(q, model) + } + return q, nil +} + +func instanceQueryToModel(searchQuery *instance.Query) (query.SearchQuery, error) { + switch q := searchQuery.GetQuery().(type) { + case *instance.Query_IdQuery: + return query.NewInstanceIDsListSearchQuery(q.IdQuery.GetIds()...) + case *instance.Query_DomainQuery: + return query.NewInstanceDomainsListSearchQuery(q.DomainQuery.GetDomains()...) + default: + return nil, zerrors.ThrowInvalidArgument(nil, "INST-3m0se", "List.Query.Invalid") + } +} + +func ListCustomDomainsRequestToModel(req *instance.ListCustomDomainsRequest, defaults systemdefaults.SystemDefaults) (*query.InstanceDomainSearchQueries, error) { + offset, limit, asc, err := filter.PaginationPbToQuery(defaults, req.GetPagination()) + if err != nil { + return nil, err + } + + queries, err := domainQueriesToModel(req.GetQueries()) + if err != nil { + return nil, err + } + + return &query.InstanceDomainSearchQueries{ + SearchRequest: query.SearchRequest{ + Offset: offset, + Limit: limit, + Asc: asc, + SortingColumn: fieldNameToInstanceDomainColumn(req.GetSortingColumn()), + }, + Queries: queries, + }, nil +} + +func fieldNameToInstanceDomainColumn(fieldName instance.DomainFieldName) query.Column { + switch fieldName { + case instance.DomainFieldName_DOMAIN_FIELD_NAME_DOMAIN: + return query.InstanceDomainDomainCol + case instance.DomainFieldName_DOMAIN_FIELD_NAME_GENERATED: + return query.InstanceDomainIsGeneratedCol + case instance.DomainFieldName_DOMAIN_FIELD_NAME_PRIMARY: + return query.InstanceDomainIsPrimaryCol + case instance.DomainFieldName_DOMAIN_FIELD_NAME_CREATION_DATE: + return query.InstanceDomainCreationDateCol + case instance.DomainFieldName_DOMAIN_FIELD_NAME_UNSPECIFIED: + fallthrough + default: + return query.Column{} + } +} + +func domainQueriesToModel(queries []*instance.DomainSearchQuery) (_ []query.SearchQuery, err error) { + q := make([]query.SearchQuery, len(queries)) + for i, query := range queries { + q[i], err = domainQueryToModel(query) + if err != nil { + return nil, err + } + } + return q, nil +} + +func domainQueryToModel(searchQuery *instance.DomainSearchQuery) (query.SearchQuery, error) { + switch q := searchQuery.GetQuery().(type) { + case *instance.DomainSearchQuery_DomainQuery: + return query.NewInstanceDomainDomainSearchQuery(object.TextMethodToQuery(q.DomainQuery.GetMethod()), q.DomainQuery.GetDomain()) + case *instance.DomainSearchQuery_GeneratedQuery: + return query.NewInstanceDomainGeneratedSearchQuery(q.GeneratedQuery.GetGenerated()) + case *instance.DomainSearchQuery_PrimaryQuery: + return query.NewInstanceDomainPrimarySearchQuery(q.PrimaryQuery.GetPrimary()) + default: + return nil, zerrors.ThrowInvalidArgument(nil, "INST-Ags42", "List.Query.Invalid") + } +} + +func ListTrustedDomainsRequestToModel(req *instance.ListTrustedDomainsRequest, defaults systemdefaults.SystemDefaults) (*query.InstanceTrustedDomainSearchQueries, error) { + offset, limit, asc, err := filter.PaginationPbToQuery(defaults, req.GetPagination()) + if err != nil { + return nil, err + } + + queries, err := trustedDomainQueriesToModel(req.GetQueries()) + if err != nil { + return nil, err + } + + return &query.InstanceTrustedDomainSearchQueries{ + SearchRequest: query.SearchRequest{ + Offset: offset, + Limit: limit, + Asc: asc, + SortingColumn: fieldNameToInstanceTrustedDomainColumn(req.GetSortingColumn()), + }, + Queries: queries, + }, nil +} + +func trustedDomainQueriesToModel(queries []*instance.TrustedDomainSearchQuery) (_ []query.SearchQuery, err error) { + q := make([]query.SearchQuery, len(queries)) + for i, query := range queries { + q[i], err = trustedDomainQueryToModel(query) + if err != nil { + return nil, err + } + } + return q, nil +} + +func trustedDomainQueryToModel(searchQuery *instance.TrustedDomainSearchQuery) (query.SearchQuery, error) { + switch q := searchQuery.GetQuery().(type) { + case *instance.TrustedDomainSearchQuery_DomainQuery: + return query.NewInstanceTrustedDomainDomainSearchQuery(object.TextMethodToQuery(q.DomainQuery.GetMethod()), q.DomainQuery.GetDomain()) + default: + return nil, zerrors.ThrowInvalidArgument(nil, "INST-Ags42", "List.Query.Invalid") + } +} + +func trustedDomainsToPb(domains []*query.InstanceTrustedDomain) []*instance.TrustedDomain { + d := make([]*instance.TrustedDomain, len(domains)) + for i, domain := range domains { + d[i] = trustedDomainToPb(domain) + } + return d +} + +func trustedDomainToPb(d *query.InstanceTrustedDomain) *instance.TrustedDomain { + return &instance.TrustedDomain{ + Domain: d.Domain, + InstanceId: d.InstanceID, + CreationDate: timestamppb.New(d.CreationDate), + } +} + +func fieldNameToInstanceTrustedDomainColumn(fieldName instance.TrustedDomainFieldName) query.Column { + switch fieldName { + case instance.TrustedDomainFieldName_TRUSTED_DOMAIN_FIELD_NAME_DOMAIN: + return query.InstanceTrustedDomainDomainCol + case instance.TrustedDomainFieldName_TRUSTED_DOMAIN_FIELD_NAME_CREATION_DATE: + return query.InstanceTrustedDomainCreationDateCol + case instance.TrustedDomainFieldName_TRUSTED_DOMAIN_FIELD_NAME_UNSPECIFIED: + fallthrough + default: + return query.Column{} + } +} diff --git a/internal/api/grpc/instance/v2beta/converter_test.go b/internal/api/grpc/instance/v2beta/converter_test.go new file mode 100644 index 0000000000..150678010c --- /dev/null +++ b/internal/api/grpc/instance/v2beta/converter_test.go @@ -0,0 +1,390 @@ +package instance + +import ( + "errors" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/cmd/build" + "github.com/zitadel/zitadel/internal/config/systemdefaults" + "github.com/zitadel/zitadel/internal/query" + "github.com/zitadel/zitadel/internal/zerrors" + filter "github.com/zitadel/zitadel/pkg/grpc/filter/v2beta" + instance "github.com/zitadel/zitadel/pkg/grpc/instance/v2beta" + "github.com/zitadel/zitadel/pkg/grpc/object/v2" +) + +func Test_InstancesToPb(t *testing.T) { + instances := []*query.Instance{ + { + ID: "instance1", + Name: "Instance One", + Domains: []*query.InstanceDomain{ + { + Domain: "example.com", + IsPrimary: true, + IsGenerated: false, + Sequence: 1, + CreationDate: time.Unix(123, 0), + ChangeDate: time.Unix(124, 0), + InstanceID: "instance1", + }, + }, + Sequence: 1, + CreationDate: time.Unix(123, 0), + ChangeDate: time.Unix(124, 0), + }, + } + + want := []*instance.Instance{ + { + Id: "instance1", + Name: "Instance One", + Domains: []*instance.Domain{ + { + Domain: "example.com", + Primary: true, + Generated: false, + InstanceId: "instance1", + CreationDate: ×tamppb.Timestamp{Seconds: 123}, + }, + }, + Version: build.Version(), + ChangeDate: ×tamppb.Timestamp{Seconds: 124}, + CreationDate: ×tamppb.Timestamp{Seconds: 123}, + }, + } + + got := InstancesToPb(instances) + assert.Equal(t, want, got) +} + +func Test_ListInstancesRequestToModel(t *testing.T) { + t.Parallel() + + searchInstanceByID, err := query.NewInstanceIDsListSearchQuery("instance1", "instance2") + require.Nil(t, err) + + tt := []struct { + testName string + inputRequest *instance.ListInstancesRequest + maxQueryLimit uint64 + expectedResult *query.InstanceSearchQueries + expectedError error + }{ + { + testName: "when query limit exceeds max query limit should return invalid argument error", + maxQueryLimit: 1, + inputRequest: &instance.ListInstancesRequest{ + Pagination: &filter.PaginationRequest{Limit: 10, Offset: 0, Asc: true}, + SortingColumn: instance.FieldName_FIELD_NAME_ID.Enum(), + Queries: []*instance.Query{{Query: &instance.Query_IdQuery{IdQuery: &instance.IdsQuery{Ids: []string{"instance1", "instance2"}}}}}, + }, + expectedError: zerrors.ThrowInvalidArgumentf(errors.New("given: 10, allowed: 1"), "QUERY-4M0fs", "Errors.Query.LimitExceeded"), + }, + { + testName: "when valid request should return instance search query model", + inputRequest: &instance.ListInstancesRequest{ + Pagination: &filter.PaginationRequest{Limit: 10, Offset: 0, Asc: true}, + SortingColumn: instance.FieldName_FIELD_NAME_ID.Enum(), + Queries: []*instance.Query{{Query: &instance.Query_IdQuery{IdQuery: &instance.IdsQuery{Ids: []string{"instance1", "instance2"}}}}}, + }, + expectedResult: &query.InstanceSearchQueries{ + SearchRequest: query.SearchRequest{ + Offset: 0, + Limit: 10, + Asc: true, + SortingColumn: query.InstanceColumnID, + }, + Queries: []query.SearchQuery{searchInstanceByID}, + }, + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + t.Parallel() + sysDefaults := systemdefaults.SystemDefaults{MaxQueryLimit: tc.maxQueryLimit} + + got, err := ListInstancesRequestToModel(tc.inputRequest, sysDefaults) + assert.Equal(t, tc.expectedError, err) + assert.Equal(t, tc.expectedResult, got) + + }) + } +} + +func Test_fieldNameToInstanceColumn(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + fieldName instance.FieldName + want query.Column + }{ + { + name: "ID field", + fieldName: instance.FieldName_FIELD_NAME_ID, + want: query.InstanceColumnID, + }, + { + name: "Name field", + fieldName: instance.FieldName_FIELD_NAME_NAME, + want: query.InstanceColumnName, + }, + { + name: "Creation Date field", + fieldName: instance.FieldName_FIELD_NAME_CREATION_DATE, + want: query.InstanceColumnCreationDate, + }, + { + name: "Unknown field", + fieldName: instance.FieldName(99), + want: query.Column{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + got := fieldNameToInstanceColumn(tt.fieldName) + assert.Equal(t, tt.want, got) + }) + } +} + +func Test_instanceQueryToModel(t *testing.T) { + t.Parallel() + + searchInstanceByID, err := query.NewInstanceIDsListSearchQuery("instance1") + require.Nil(t, err) + + searchInstanceByDomain, err := query.NewInstanceDomainsListSearchQuery("example.com") + require.Nil(t, err) + + tests := []struct { + name string + searchQuery *instance.Query + want query.SearchQuery + wantErr bool + }{ + { + name: "ID Query", + searchQuery: &instance.Query{ + Query: &instance.Query_IdQuery{ + IdQuery: &instance.IdsQuery{ + Ids: []string{"instance1"}, + }, + }, + }, + want: searchInstanceByID, + wantErr: false, + }, + { + name: "Domain Query", + searchQuery: &instance.Query{ + Query: &instance.Query_DomainQuery{ + DomainQuery: &instance.DomainsQuery{ + Domains: []string{"example.com"}, + }, + }, + }, + want: searchInstanceByDomain, + wantErr: false, + }, + { + name: "Invalid Query", + searchQuery: &instance.Query{ + Query: nil, + }, + want: nil, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + got, err := instanceQueryToModel(tt.searchQuery) + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.want, got) + } + }) + } +} + +func Test_ListCustomDomainsRequestToModel(t *testing.T) { + t.Parallel() + + querySearchRes, err := query.NewInstanceDomainDomainSearchQuery(query.TextEquals, "example.com") + require.Nil(t, err) + + queryGeneratedRes, err := query.NewInstanceDomainGeneratedSearchQuery(false) + require.Nil(t, err) + + tests := []struct { + name string + inputRequest *instance.ListCustomDomainsRequest + maxQueryLimit uint64 + expectedResult *query.InstanceDomainSearchQueries + expectedError error + }{ + { + name: "when query limit exceeds max query limit should return invalid argument error", + inputRequest: &instance.ListCustomDomainsRequest{ + Pagination: &filter.PaginationRequest{Limit: 10, Offset: 0, Asc: true}, + SortingColumn: instance.DomainFieldName_DOMAIN_FIELD_NAME_DOMAIN, + Queries: []*instance.DomainSearchQuery{ + { + Query: &instance.DomainSearchQuery_DomainQuery{ + DomainQuery: &instance.DomainQuery{ + Method: object.TextQueryMethod_TEXT_QUERY_METHOD_EQUALS, + Domain: "example.com", + }, + }, + }, + }, + }, + maxQueryLimit: 1, + expectedError: zerrors.ThrowInvalidArgumentf(errors.New("given: 10, allowed: 1"), "QUERY-4M0fs", "Errors.Query.LimitExceeded"), + }, + { + name: "when valid request should return domain search query model", + inputRequest: &instance.ListCustomDomainsRequest{ + Pagination: &filter.PaginationRequest{Limit: 10, Offset: 0, Asc: true}, + SortingColumn: instance.DomainFieldName_DOMAIN_FIELD_NAME_PRIMARY, + Queries: []*instance.DomainSearchQuery{ + { + Query: &instance.DomainSearchQuery_DomainQuery{ + DomainQuery: &instance.DomainQuery{Method: object.TextQueryMethod_TEXT_QUERY_METHOD_EQUALS, Domain: "example.com"}}, + }, + { + Query: &instance.DomainSearchQuery_GeneratedQuery{ + GeneratedQuery: &instance.DomainGeneratedQuery{Generated: false}}, + }, + }, + }, + maxQueryLimit: 100, + expectedResult: &query.InstanceDomainSearchQueries{ + SearchRequest: query.SearchRequest{Offset: 0, Limit: 10, Asc: true, SortingColumn: query.InstanceDomainIsPrimaryCol}, + Queries: []query.SearchQuery{ + querySearchRes, + queryGeneratedRes, + }, + }, + expectedError: nil, + }, + { + name: "when invalid query should return error", + inputRequest: &instance.ListCustomDomainsRequest{ + Pagination: &filter.PaginationRequest{Limit: 10, Offset: 0, Asc: true}, + SortingColumn: instance.DomainFieldName_DOMAIN_FIELD_NAME_GENERATED, + Queries: []*instance.DomainSearchQuery{ + { + Query: nil, + }, + }, + }, + maxQueryLimit: 100, + expectedResult: nil, + expectedError: zerrors.ThrowInvalidArgument(nil, "INST-Ags42", "List.Query.Invalid"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + sysDefaults := systemdefaults.SystemDefaults{MaxQueryLimit: tt.maxQueryLimit} + + got, err := ListCustomDomainsRequestToModel(tt.inputRequest, sysDefaults) + assert.Equal(t, tt.expectedError, err) + assert.Equal(t, tt.expectedResult, got) + }) + } +} + +func Test_ListTrustedDomainsRequestToModel(t *testing.T) { + t.Parallel() + + querySearchRes, err := query.NewInstanceTrustedDomainDomainSearchQuery(query.TextEquals, "example.com") + require.Nil(t, err) + + tests := []struct { + name string + inputRequest *instance.ListTrustedDomainsRequest + maxQueryLimit uint64 + expectedResult *query.InstanceTrustedDomainSearchQueries + expectedError error + }{ + { + name: "when query limit exceeds max query limit should return invalid argument error", + inputRequest: &instance.ListTrustedDomainsRequest{ + Pagination: &filter.PaginationRequest{Limit: 10, Offset: 0, Asc: true}, + SortingColumn: instance.TrustedDomainFieldName_TRUSTED_DOMAIN_FIELD_NAME_DOMAIN, + Queries: []*instance.TrustedDomainSearchQuery{ + { + Query: &instance.TrustedDomainSearchQuery_DomainQuery{ + DomainQuery: &instance.DomainQuery{ + Method: object.TextQueryMethod_TEXT_QUERY_METHOD_EQUALS, + Domain: "example.com", + }, + }, + }, + }, + }, + maxQueryLimit: 1, + expectedError: zerrors.ThrowInvalidArgumentf(errors.New("given: 10, allowed: 1"), "QUERY-4M0fs", "Errors.Query.LimitExceeded"), + }, + { + name: "when valid request should return domain search query model", + inputRequest: &instance.ListTrustedDomainsRequest{ + Pagination: &filter.PaginationRequest{Limit: 10, Offset: 0, Asc: true}, + SortingColumn: instance.TrustedDomainFieldName_TRUSTED_DOMAIN_FIELD_NAME_CREATION_DATE, + Queries: []*instance.TrustedDomainSearchQuery{ + { + Query: &instance.TrustedDomainSearchQuery_DomainQuery{ + DomainQuery: &instance.DomainQuery{Method: object.TextQueryMethod_TEXT_QUERY_METHOD_EQUALS, Domain: "example.com"}}, + }, + }, + }, + maxQueryLimit: 100, + expectedResult: &query.InstanceTrustedDomainSearchQueries{ + SearchRequest: query.SearchRequest{Offset: 0, Limit: 10, Asc: true, SortingColumn: query.InstanceTrustedDomainCreationDateCol}, + Queries: []query.SearchQuery{querySearchRes}, + }, + expectedError: nil, + }, + { + name: "when invalid query should return error", + inputRequest: &instance.ListTrustedDomainsRequest{ + Pagination: &filter.PaginationRequest{Limit: 10, Offset: 0, Asc: true}, + Queries: []*instance.TrustedDomainSearchQuery{ + { + Query: nil, + }, + }, + }, + maxQueryLimit: 100, + expectedResult: nil, + expectedError: zerrors.ThrowInvalidArgument(nil, "INST-Ags42", "List.Query.Invalid"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + sysDefaults := systemdefaults.SystemDefaults{MaxQueryLimit: tt.maxQueryLimit} + + got, err := ListTrustedDomainsRequestToModel(tt.inputRequest, sysDefaults) + assert.Equal(t, tt.expectedError, err) + assert.Equal(t, tt.expectedResult, got) + }) + } +} diff --git a/internal/api/grpc/instance/v2beta/domain.go b/internal/api/grpc/instance/v2beta/domain.go new file mode 100644 index 0000000000..439c6e5d8d --- /dev/null +++ b/internal/api/grpc/instance/v2beta/domain.go @@ -0,0 +1,50 @@ +package instance + +import ( + "context" + + "google.golang.org/protobuf/types/known/timestamppb" + + instance "github.com/zitadel/zitadel/pkg/grpc/instance/v2beta" +) + +func (s *Server) AddCustomDomain(ctx context.Context, req *instance.AddCustomDomainRequest) (*instance.AddCustomDomainResponse, error) { + details, err := s.command.AddInstanceDomain(ctx, req.GetDomain()) + if err != nil { + return nil, err + } + return &instance.AddCustomDomainResponse{ + CreationDate: timestamppb.New(details.CreationDate), + }, nil +} + +func (s *Server) RemoveCustomDomain(ctx context.Context, req *instance.RemoveCustomDomainRequest) (*instance.RemoveCustomDomainResponse, error) { + details, err := s.command.RemoveInstanceDomain(ctx, req.GetDomain()) + if err != nil { + return nil, err + } + return &instance.RemoveCustomDomainResponse{ + DeletionDate: timestamppb.New(details.EventDate), + }, nil +} + +func (s *Server) AddTrustedDomain(ctx context.Context, req *instance.AddTrustedDomainRequest) (*instance.AddTrustedDomainResponse, error) { + details, err := s.command.AddTrustedDomain(ctx, req.GetDomain()) + if err != nil { + return nil, err + } + return &instance.AddTrustedDomainResponse{ + CreationDate: timestamppb.New(details.CreationDate), + }, nil +} + +func (s *Server) RemoveTrustedDomain(ctx context.Context, req *instance.RemoveTrustedDomainRequest) (*instance.RemoveTrustedDomainResponse, error) { + details, err := s.command.RemoveTrustedDomain(ctx, req.GetDomain()) + if err != nil { + return nil, err + } + + return &instance.RemoveTrustedDomainResponse{ + DeletionDate: timestamppb.New(details.EventDate), + }, nil +} diff --git a/internal/api/grpc/instance/v2beta/instance.go b/internal/api/grpc/instance/v2beta/instance.go new file mode 100644 index 0000000000..b1c36e74bb --- /dev/null +++ b/internal/api/grpc/instance/v2beta/instance.go @@ -0,0 +1,32 @@ +package instance + +import ( + "context" + + "google.golang.org/protobuf/types/known/timestamppb" + + instance "github.com/zitadel/zitadel/pkg/grpc/instance/v2beta" +) + +func (s *Server) DeleteInstance(ctx context.Context, request *instance.DeleteInstanceRequest) (*instance.DeleteInstanceResponse, error) { + obj, err := s.command.RemoveInstance(ctx, request.GetInstanceId()) + if err != nil { + return nil, err + } + + return &instance.DeleteInstanceResponse{ + DeletionDate: timestamppb.New(obj.EventDate), + }, nil + +} + +func (s *Server) UpdateInstance(ctx context.Context, request *instance.UpdateInstanceRequest) (*instance.UpdateInstanceResponse, error) { + obj, err := s.command.UpdateInstance(ctx, request.GetInstanceName()) + if err != nil { + return nil, err + } + + return &instance.UpdateInstanceResponse{ + ChangeDate: timestamppb.New(obj.EventDate), + }, nil +} diff --git a/internal/api/grpc/instance/v2beta/integration_test/domain_test.go b/internal/api/grpc/instance/v2beta/integration_test/domain_test.go new file mode 100644 index 0000000000..a0e2011cfc --- /dev/null +++ b/internal/api/grpc/instance/v2beta/integration_test/domain_test.go @@ -0,0 +1,350 @@ +//go:build integration + +package instance_test + +import ( + "context" + "strings" + "testing" + "time" + + "github.com/brianvoe/gofakeit/v6" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + "github.com/zitadel/zitadel/internal/integration" + instance "github.com/zitadel/zitadel/pkg/grpc/instance/v2beta" +) + +func TestAddCustomDomain(t *testing.T) { + t.Parallel() + + // Given + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute) + defer cancel() + + ctxWithSysAuthZ := integration.WithSystemAuthorization(ctx) + + inst := integration.NewInstance(ctxWithSysAuthZ) + iamOwnerCtx := inst.WithAuthorization(context.Background(), integration.UserTypeIAMOwner) + + t.Cleanup(func() { + inst.Client.InstanceV2Beta.DeleteInstance(ctxWithSysAuthZ, &instance.DeleteInstanceRequest{InstanceId: inst.ID()}) + }) + + tt := []struct { + testName string + inputContext context.Context + inputRequest *instance.AddCustomDomainRequest + expectedErrorMsg string + expectedErrorCode codes.Code + }{ + { + testName: "when invalid context should return unauthN error", + inputRequest: &instance.AddCustomDomainRequest{ + InstanceId: inst.ID(), + Domain: gofakeit.DomainName(), + }, + inputContext: context.Background(), + expectedErrorCode: codes.Unauthenticated, + expectedErrorMsg: "auth header missing", + }, + { + testName: "when unauthZ context should return unauthZ error", + inputRequest: &instance.AddCustomDomainRequest{ + InstanceId: inst.ID(), + Domain: gofakeit.DomainName(), + }, + inputContext: iamOwnerCtx, + expectedErrorCode: codes.PermissionDenied, + expectedErrorMsg: "No matching permissions found (AUTH-5mWD2)", + }, + { + testName: "when invalid domain should return invalid argument error", + inputRequest: &instance.AddCustomDomainRequest{ + InstanceId: inst.ID(), + Domain: " ", + }, + inputContext: ctxWithSysAuthZ, + expectedErrorCode: codes.InvalidArgument, + expectedErrorMsg: "Errors.Invalid.Argument (INST-28nlD)", + }, + { + testName: "when valid request should return successful response", + inputRequest: &instance.AddCustomDomainRequest{ + InstanceId: inst.ID(), + Domain: " " + gofakeit.DomainName(), + }, + inputContext: ctxWithSysAuthZ, + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + t.Cleanup(func() { + if tc.expectedErrorMsg == "" { + inst.Client.InstanceV2Beta.RemoveCustomDomain(ctxWithSysAuthZ, &instance.RemoveCustomDomainRequest{Domain: strings.TrimSpace(tc.inputRequest.Domain)}) + } + }) + + // Test + res, err := inst.Client.InstanceV2Beta.AddCustomDomain(tc.inputContext, tc.inputRequest) + + // Verify + assert.Equal(t, tc.expectedErrorCode, status.Code(err)) + assert.Equal(t, tc.expectedErrorMsg, status.Convert(err).Message()) + + if tc.expectedErrorMsg == "" { + assert.NotNil(t, res) + assert.NotEmpty(t, res.GetCreationDate()) + } + }) + } +} + +func TestRemoveCustomDomain(t *testing.T) { + t.Parallel() + + // Given + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute) + defer cancel() + + ctxWithSysAuthZ := integration.WithSystemAuthorization(ctx) + inst := integration.NewInstance(ctxWithSysAuthZ) + iamOwnerCtx := inst.WithAuthorization(context.Background(), integration.UserTypeIAMOwner) + + customDomain := gofakeit.DomainName() + + _, err := inst.Client.InstanceV2Beta.AddCustomDomain(ctxWithSysAuthZ, &instance.AddCustomDomainRequest{InstanceId: inst.ID(), Domain: customDomain}) + require.Nil(t, err) + + t.Cleanup(func() { + inst.Client.InstanceV2Beta.RemoveCustomDomain(ctxWithSysAuthZ, &instance.RemoveCustomDomainRequest{InstanceId: inst.ID(), Domain: customDomain}) + inst.Client.InstanceV2Beta.DeleteInstance(ctxWithSysAuthZ, &instance.DeleteInstanceRequest{InstanceId: inst.ID()}) + }) + + tt := []struct { + testName string + inputContext context.Context + inputRequest *instance.RemoveCustomDomainRequest + expectedErrorMsg string + expectedErrorCode codes.Code + }{ + { + testName: "when invalid context should return unauthN error", + inputRequest: &instance.RemoveCustomDomainRequest{ + InstanceId: inst.ID(), + Domain: "custom1", + }, + inputContext: context.Background(), + expectedErrorCode: codes.Unauthenticated, + expectedErrorMsg: "auth header missing", + }, + { + testName: "when unauthZ context should return unauthZ error", + inputRequest: &instance.RemoveCustomDomainRequest{ + InstanceId: inst.ID(), + Domain: "custom1", + }, + inputContext: iamOwnerCtx, + expectedErrorCode: codes.PermissionDenied, + expectedErrorMsg: "No matching permissions found (AUTH-5mWD2)", + }, + { + testName: "when invalid domain should return invalid argument error", + inputRequest: &instance.RemoveCustomDomainRequest{ + InstanceId: inst.ID(), + Domain: " ", + }, + inputContext: ctxWithSysAuthZ, + expectedErrorCode: codes.InvalidArgument, + expectedErrorMsg: "Errors.Invalid.Argument (INST-39nls)", + }, + { + testName: "when valid request should return successful response", + inputRequest: &instance.RemoveCustomDomainRequest{ + InstanceId: inst.ID(), + Domain: " " + customDomain, + }, + inputContext: ctxWithSysAuthZ, + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + // Test + res, err := inst.Client.InstanceV2Beta.RemoveCustomDomain(tc.inputContext, tc.inputRequest) + + // Verify + assert.Equal(t, tc.expectedErrorCode, status.Code(err)) + assert.Equal(t, tc.expectedErrorMsg, status.Convert(err).Message()) + + if tc.expectedErrorMsg == "" { + assert.NotNil(t, res) + assert.NotEmpty(t, res.GetDeletionDate()) + } + }) + } +} + +func TestAddTrustedDomain(t *testing.T) { + t.Parallel() + + // Given + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute) + defer cancel() + + ctxWithSysAuthZ := integration.WithSystemAuthorization(ctx) + inst := integration.NewInstance(ctxWithSysAuthZ) + orgOwnerCtx := inst.WithAuthorization(context.Background(), integration.UserTypeOrgOwner) + + t.Cleanup(func() { + inst.Client.InstanceV2Beta.DeleteInstance(ctxWithSysAuthZ, &instance.DeleteInstanceRequest{InstanceId: inst.ID()}) + }) + + tt := []struct { + testName string + inputContext context.Context + inputRequest *instance.AddTrustedDomainRequest + expectedErrorMsg string + expectedErrorCode codes.Code + }{ + { + testName: "when invalid context should return unauthN error", + inputRequest: &instance.AddTrustedDomainRequest{ + InstanceId: inst.ID(), + Domain: "trusted1", + }, + inputContext: context.Background(), + expectedErrorCode: codes.Unauthenticated, + expectedErrorMsg: "auth header missing", + }, + { + testName: "when unauthZ context should return unauthZ error", + inputRequest: &instance.AddTrustedDomainRequest{ + InstanceId: inst.ID(), + Domain: "trusted1", + }, + inputContext: orgOwnerCtx, + expectedErrorCode: codes.PermissionDenied, + expectedErrorMsg: "No matching permissions found (AUTH-5mWD2)", + }, + { + testName: "when invalid domain should return invalid argument error", + inputRequest: &instance.AddTrustedDomainRequest{ + InstanceId: inst.ID(), + Domain: " ", + }, + inputContext: ctxWithSysAuthZ, + expectedErrorCode: codes.InvalidArgument, + expectedErrorMsg: "Errors.Invalid.Argument (COMMA-Stk21)", + }, + { + testName: "when valid request should return successful response", + inputRequest: &instance.AddTrustedDomainRequest{ + InstanceId: inst.ID(), + Domain: " " + gofakeit.DomainName(), + }, + inputContext: ctxWithSysAuthZ, + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + t.Cleanup(func() { + if tc.expectedErrorMsg == "" { + inst.Client.InstanceV2Beta.RemoveTrustedDomain(ctxWithSysAuthZ, &instance.RemoveTrustedDomainRequest{Domain: strings.TrimSpace(tc.inputRequest.Domain)}) + } + }) + + // Test + res, err := inst.Client.InstanceV2Beta.AddTrustedDomain(tc.inputContext, tc.inputRequest) + + // Verify + assert.Equal(t, tc.expectedErrorCode, status.Code(err)) + assert.Equal(t, tc.expectedErrorMsg, status.Convert(err).Message()) + + if tc.expectedErrorMsg == "" { + assert.NotNil(t, res) + assert.NotEmpty(t, res.GetCreationDate()) + } + }) + } +} + +func TestRemoveTrustedDomain(t *testing.T) { + t.Parallel() + + // Given + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute) + defer cancel() + + ctxWithSysAuthZ := integration.WithSystemAuthorization(ctx) + inst := integration.NewInstance(ctxWithSysAuthZ) + orgOwnerCtx := inst.WithAuthorization(context.Background(), integration.UserTypeOrgOwner) + + trustedDomain := gofakeit.DomainName() + + _, err := inst.Client.InstanceV2Beta.AddTrustedDomain(ctxWithSysAuthZ, &instance.AddTrustedDomainRequest{InstanceId: inst.ID(), Domain: trustedDomain}) + require.Nil(t, err) + + t.Cleanup(func() { + inst.Client.InstanceV2Beta.RemoveTrustedDomain(ctxWithSysAuthZ, &instance.RemoveTrustedDomainRequest{InstanceId: inst.ID(), Domain: trustedDomain}) + inst.Client.InstanceV2Beta.DeleteInstance(ctxWithSysAuthZ, &instance.DeleteInstanceRequest{InstanceId: inst.ID()}) + }) + + tt := []struct { + testName string + inputContext context.Context + inputRequest *instance.RemoveTrustedDomainRequest + expectedErrorMsg string + expectedErrorCode codes.Code + }{ + { + testName: "when invalid context should return unauthN error", + inputRequest: &instance.RemoveTrustedDomainRequest{ + InstanceId: inst.ID(), + Domain: "trusted1", + }, + inputContext: context.Background(), + expectedErrorCode: codes.Unauthenticated, + expectedErrorMsg: "auth header missing", + }, + { + testName: "when unauthZ context should return unauthZ error", + inputRequest: &instance.RemoveTrustedDomainRequest{ + InstanceId: inst.ID(), + Domain: "trusted1", + }, + inputContext: orgOwnerCtx, + expectedErrorCode: codes.PermissionDenied, + expectedErrorMsg: "No matching permissions found (AUTH-5mWD2)", + }, + { + testName: "when valid request should return successful response", + inputRequest: &instance.RemoveTrustedDomainRequest{ + InstanceId: inst.ID(), + Domain: " " + trustedDomain, + }, + inputContext: ctxWithSysAuthZ, + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + // Test + res, err := inst.Client.InstanceV2Beta.RemoveTrustedDomain(tc.inputContext, tc.inputRequest) + + // Verify + assert.Equal(t, tc.expectedErrorCode, status.Code(err)) + assert.Equal(t, tc.expectedErrorMsg, status.Convert(err).Message()) + + if tc.expectedErrorMsg == "" { + require.NotNil(t, res) + require.NotEmpty(t, res.GetDeletionDate()) + } + }) + } +} diff --git a/internal/api/grpc/instance/v2beta/integration_test/instance_test.go b/internal/api/grpc/instance/v2beta/integration_test/instance_test.go new file mode 100644 index 0000000000..5187bbc78d --- /dev/null +++ b/internal/api/grpc/instance/v2beta/integration_test/instance_test.go @@ -0,0 +1,162 @@ +//go:build integration + +package instance_test + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/zitadel/zitadel/internal/integration" + instance "github.com/zitadel/zitadel/pkg/grpc/instance/v2beta" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +func TestDeleteInstace(t *testing.T) { + t.Parallel() + + // Given + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute) + defer cancel() + + ctxWithSysAuthZ := integration.WithSystemAuthorization(ctx) + + inst := integration.NewInstance(ctxWithSysAuthZ) + + t.Cleanup(func() { + inst.Client.InstanceV2Beta.DeleteInstance(ctxWithSysAuthZ, &instance.DeleteInstanceRequest{InstanceId: inst.ID()}) + }) + + tt := []struct { + testName string + inputRequest *instance.DeleteInstanceRequest + inputContext context.Context + expectedErrorMsg string + expectedErrorCode codes.Code + expectedInstanceID string + }{ + { + testName: "when invalid context should return unauthN error", + inputRequest: &instance.DeleteInstanceRequest{ + InstanceId: " ", + }, + inputContext: context.Background(), + expectedErrorCode: codes.Unauthenticated, + expectedErrorMsg: "auth header missing", + }, + { + testName: "when invalid input should return invalid argument error", + inputRequest: &instance.DeleteInstanceRequest{ + InstanceId: inst.ID() + "invalid", + }, + inputContext: ctxWithSysAuthZ, + expectedErrorCode: codes.NotFound, + expectedErrorMsg: "Instance not found (COMMA-AE3GS)", + }, + { + testName: "when delete succeeds should return deletion date", + inputRequest: &instance.DeleteInstanceRequest{ + InstanceId: inst.ID(), + }, + inputContext: ctxWithSysAuthZ, + expectedInstanceID: inst.ID(), + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + // Test + res, err := inst.Client.InstanceV2Beta.DeleteInstance(tc.inputContext, tc.inputRequest) + + // Verify + assert.Equal(t, tc.expectedErrorCode, status.Code(err)) + assert.Equal(t, tc.expectedErrorMsg, status.Convert(err).Message()) + if tc.expectedErrorMsg == "" { + require.NotNil(t, res) + require.NotEmpty(t, res.GetDeletionDate()) + } + }) + } +} + +func TestUpdateInstace(t *testing.T) { + t.Parallel() + + // Given + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute) + defer cancel() + + ctxWithSysAuthZ := integration.WithSystemAuthorization(ctx) + + inst := integration.NewInstance(ctxWithSysAuthZ) + orgOwnerCtx := inst.WithAuthorization(context.Background(), integration.UserTypeOrgOwner) + + t.Cleanup(func() { + inst.Client.InstanceV2Beta.DeleteInstance(ctxWithSysAuthZ, &instance.DeleteInstanceRequest{InstanceId: inst.ID()}) + }) + + tt := []struct { + testName string + inputRequest *instance.UpdateInstanceRequest + inputContext context.Context + expectedErrorMsg string + expectedErrorCode codes.Code + expectedNewName string + }{ + { + testName: "when invalid context should return unauthN error", + inputRequest: &instance.UpdateInstanceRequest{ + InstanceId: inst.ID(), + InstanceName: " ", + }, + inputContext: context.Background(), + expectedErrorCode: codes.Unauthenticated, + expectedErrorMsg: "auth header missing", + }, + { + testName: "when unauthZ context should return unauthZ error", + inputRequest: &instance.UpdateInstanceRequest{ + InstanceId: inst.ID(), + InstanceName: " ", + }, + inputContext: orgOwnerCtx, + expectedErrorCode: codes.PermissionDenied, + expectedErrorMsg: "No matching permissions found (AUTH-5mWD2)", + }, + { + testName: "when update succeeds should change instance name", + inputRequest: &instance.UpdateInstanceRequest{ + InstanceId: inst.ID(), + InstanceName: "an-updated-name", + }, + inputContext: ctxWithSysAuthZ, + expectedNewName: "an-updated-name", + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + // Test + res, err := inst.Client.InstanceV2Beta.UpdateInstance(tc.inputContext, tc.inputRequest) + + // Verify + assert.Equal(t, tc.expectedErrorCode, status.Code(err)) + assert.Equal(t, tc.expectedErrorMsg, status.Convert(err).Message()) + if tc.expectedErrorMsg == "" { + + require.NotNil(t, res) + assert.NotEmpty(t, res.GetChangeDate()) + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tc.inputContext, 20*time.Second) + require.EventuallyWithT(t, func(tt *assert.CollectT) { + retrievedInstance, err := inst.Client.InstanceV2Beta.GetInstance(tc.inputContext, &instance.GetInstanceRequest{InstanceId: inst.ID()}) + require.Nil(tt, err) + assert.Equal(tt, tc.expectedNewName, retrievedInstance.GetInstance().GetName()) + }, retryDuration, tick, "timeout waiting for expected execution result") + } + }) + } +} diff --git a/internal/api/grpc/instance/v2beta/integration_test/query_test.go b/internal/api/grpc/instance/v2beta/integration_test/query_test.go new file mode 100644 index 0000000000..0828b006e3 --- /dev/null +++ b/internal/api/grpc/instance/v2beta/integration_test/query_test.go @@ -0,0 +1,369 @@ +//go:build integration + +package instance_test + +import ( + "context" + "slices" + "testing" + "time" + + "github.com/brianvoe/gofakeit/v6" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/zitadel/zitadel/internal/integration" + filter "github.com/zitadel/zitadel/pkg/grpc/filter/v2beta" + instance "github.com/zitadel/zitadel/pkg/grpc/instance/v2beta" + "github.com/zitadel/zitadel/pkg/grpc/object/v2" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +func TestGetInstance(t *testing.T) { + t.Parallel() + + // Given + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute) + defer cancel() + + ctxWithSysAuthZ := integration.WithSystemAuthorization(ctx) + inst := integration.NewInstance(ctxWithSysAuthZ) + orgOwnerCtx := inst.WithAuthorization(context.Background(), integration.UserTypeOrgOwner) + + t.Cleanup(func() { + inst.Client.InstanceV2Beta.DeleteInstance(ctxWithSysAuthZ, &instance.DeleteInstanceRequest{InstanceId: inst.ID()}) + }) + + tt := []struct { + testName string + inputContext context.Context + expectedInstanceID string + expectedErrorMsg string + expectedErrorCode codes.Code + }{ + { + testName: "when unauthN context should return unauthN error", + inputContext: context.Background(), + expectedErrorCode: codes.Unauthenticated, + expectedErrorMsg: "auth header missing", + }, + { + testName: "when unauthZ context should return unauthZ error", + inputContext: orgOwnerCtx, + expectedErrorCode: codes.PermissionDenied, + expectedErrorMsg: "No matching permissions found (AUTH-5mWD2)", + }, + { + testName: "when request succeeds should return matching instance", + inputContext: ctxWithSysAuthZ, + expectedInstanceID: inst.ID(), + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + // Test + res, err := inst.Client.InstanceV2Beta.GetInstance(tc.inputContext, &instance.GetInstanceRequest{InstanceId: inst.ID()}) + + // Verify + assert.Equal(t, tc.expectedErrorCode, status.Code(err)) + assert.Equal(t, tc.expectedErrorMsg, status.Convert(err).Message()) + + if tc.expectedErrorMsg == "" { + require.NoError(t, err) + assert.Equal(t, tc.expectedInstanceID, res.GetInstance().GetId()) + } + }) + } +} + +func TestListInstances(t *testing.T) { + t.Parallel() + + // Given + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute) + defer cancel() + + ctxWithSysAuthZ := integration.WithSystemAuthorization(ctx) + + instances := make([]*integration.Instance, 2) + inst := integration.NewInstance(ctxWithSysAuthZ) + inst2 := integration.NewInstance(ctxWithSysAuthZ) + instances[0], instances[1] = inst, inst2 + + t.Cleanup(func() { + inst.Client.InstanceV2Beta.DeleteInstance(ctxWithSysAuthZ, &instance.DeleteInstanceRequest{InstanceId: inst.ID()}) + inst.Client.InstanceV2Beta.DeleteInstance(ctxWithSysAuthZ, &instance.DeleteInstanceRequest{InstanceId: inst2.ID()}) + }) + + // Sort in descending order + slices.SortFunc(instances, func(i1, i2 *integration.Instance) int { + res := i1.Instance.Details.CreationDate.AsTime().Compare(i2.Instance.Details.CreationDate.AsTime()) + if res == 0 { + return res + } + return -res + }) + + orgOwnerCtx := inst.WithAuthorization(context.Background(), integration.UserTypeOrgOwner) + + tt := []struct { + testName string + inputRequest *instance.ListInstancesRequest + inputContext context.Context + expectedErrorMsg string + expectedErrorCode codes.Code + expectedInstances []string + }{ + { + testName: "when invalid context should return unauthN error", + inputRequest: &instance.ListInstancesRequest{ + Pagination: &filter.PaginationRequest{Offset: 0, Limit: 10}, + }, + inputContext: context.Background(), + expectedErrorCode: codes.Unauthenticated, + expectedErrorMsg: "auth header missing", + }, + { + testName: "when unauthZ context should return unauthZ error", + inputRequest: &instance.ListInstancesRequest{ + Pagination: &filter.PaginationRequest{Offset: 0, Limit: 10}, + }, + inputContext: orgOwnerCtx, + expectedErrorCode: codes.PermissionDenied, + expectedErrorMsg: "No matching permissions found (AUTH-5mWD2)", + }, + { + testName: "when valid request with filter should return paginated response", + inputRequest: &instance.ListInstancesRequest{ + Pagination: &filter.PaginationRequest{Offset: 0, Limit: 10}, + SortingColumn: instance.FieldName_FIELD_NAME_CREATION_DATE.Enum(), + Queries: []*instance.Query{ + { + Query: &instance.Query_IdQuery{ + IdQuery: &instance.IdsQuery{ + Ids: []string{inst.ID(), inst2.ID()}, + }, + }, + }, + }, + }, + inputContext: ctxWithSysAuthZ, + expectedInstances: []string{inst2.ID(), inst.ID()}, + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + // Test + res, err := inst.Client.InstanceV2Beta.ListInstances(tc.inputContext, tc.inputRequest) + + // Verify + assert.Equal(t, tc.expectedErrorCode, status.Code(err)) + assert.Equal(t, tc.expectedErrorMsg, status.Convert(err).Message()) + + if tc.expectedErrorMsg == "" { + require.NotNil(t, res) + + require.Len(t, res.GetInstances(), len(tc.expectedInstances)) + + for i, ins := range res.GetInstances() { + assert.Equal(t, tc.expectedInstances[i], ins.GetId()) + } + } + }) + } +} + +func TestListCustomDomains(t *testing.T) { + t.Parallel() + + // Given + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute) + defer cancel() + + ctxWithSysAuthZ := integration.WithSystemAuthorization(ctx) + inst := integration.NewInstance(ctxWithSysAuthZ) + + orgOwnerCtx := inst.WithAuthorization(context.Background(), integration.UserTypeOrgOwner) + d1, d2 := "custom."+gofakeit.DomainName(), "custom."+gofakeit.DomainName() + + _, err := inst.Client.InstanceV2Beta.AddCustomDomain(ctxWithSysAuthZ, &instance.AddCustomDomainRequest{InstanceId: inst.ID(), Domain: d1}) + require.Nil(t, err) + _, err = inst.Client.InstanceV2Beta.AddCustomDomain(ctxWithSysAuthZ, &instance.AddCustomDomainRequest{InstanceId: inst.ID(), Domain: d2}) + require.Nil(t, err) + + t.Cleanup(func() { + inst.Client.InstanceV2Beta.RemoveCustomDomain(ctxWithSysAuthZ, &instance.RemoveCustomDomainRequest{InstanceId: inst.ID(), Domain: d1}) + inst.Client.InstanceV2Beta.RemoveCustomDomain(ctxWithSysAuthZ, &instance.RemoveCustomDomainRequest{InstanceId: inst.ID(), Domain: d2}) + inst.Client.InstanceV2Beta.DeleteInstance(ctxWithSysAuthZ, &instance.DeleteInstanceRequest{InstanceId: inst.ID()}) + }) + + tt := []struct { + testName string + inputRequest *instance.ListCustomDomainsRequest + inputContext context.Context + expectedErrorMsg string + expectedErrorCode codes.Code + expectedDomains []string + }{ + { + testName: "when invalid context should return unauthN error", + inputRequest: &instance.ListCustomDomainsRequest{ + InstanceId: inst.ID(), + Pagination: &filter.PaginationRequest{Offset: 0, Limit: 10}, + }, + inputContext: context.Background(), + expectedErrorCode: codes.Unauthenticated, + expectedErrorMsg: "auth header missing"}, + { + testName: "when unauthZ context should return unauthZ error", + inputRequest: &instance.ListCustomDomainsRequest{ + InstanceId: inst.ID(), + Pagination: &filter.PaginationRequest{Offset: 0, Limit: 10}, + SortingColumn: instance.DomainFieldName_DOMAIN_FIELD_NAME_CREATION_DATE, + Queries: []*instance.DomainSearchQuery{ + { + Query: &instance.DomainSearchQuery_DomainQuery{ + DomainQuery: &instance.DomainQuery{Domain: "custom", Method: object.TextQueryMethod_TEXT_QUERY_METHOD_CONTAINS}, + }, + }, + }, + }, + inputContext: orgOwnerCtx, + expectedErrorCode: codes.PermissionDenied, + expectedErrorMsg: "No matching permissions found (AUTH-5mWD2)", + }, + { + testName: "when valid request with filter should return paginated response", + inputRequest: &instance.ListCustomDomainsRequest{ + InstanceId: inst.ID(), + Pagination: &filter.PaginationRequest{Offset: 0, Limit: 10}, + SortingColumn: instance.DomainFieldName_DOMAIN_FIELD_NAME_CREATION_DATE, + Queries: []*instance.DomainSearchQuery{ + { + Query: &instance.DomainSearchQuery_DomainQuery{ + DomainQuery: &instance.DomainQuery{Domain: "custom", Method: object.TextQueryMethod_TEXT_QUERY_METHOD_CONTAINS}, + }, + }, + }, + }, + inputContext: ctxWithSysAuthZ, + expectedDomains: []string{d1, d2}, + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + // Test + res, err := inst.Client.InstanceV2Beta.ListCustomDomains(tc.inputContext, tc.inputRequest) + + // Verify + assert.Equal(t, tc.expectedErrorCode, status.Code(err)) + assert.Equal(t, tc.expectedErrorMsg, status.Convert(err).Message()) + + if tc.expectedErrorMsg == "" { + domains := []string{} + for _, d := range res.GetDomains() { + domains = append(domains, d.GetDomain()) + } + + assert.Subset(t, domains, tc.expectedDomains) + } + }) + } +} + +func TestListTrustedDomains(t *testing.T) { + t.Parallel() + + // Given + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute) + defer cancel() + + ctxWithSysAuthZ := integration.WithSystemAuthorization(ctx) + inst := integration.NewInstance(ctxWithSysAuthZ) + + orgOwnerCtx := inst.WithAuthorization(context.Background(), integration.UserTypeOrgOwner) + d1, d2 := "trusted."+gofakeit.DomainName(), "trusted."+gofakeit.DomainName() + + _, err := inst.Client.InstanceV2Beta.AddTrustedDomain(ctxWithSysAuthZ, &instance.AddTrustedDomainRequest{InstanceId: inst.ID(), Domain: d1}) + require.Nil(t, err) + _, err = inst.Client.InstanceV2Beta.AddTrustedDomain(ctxWithSysAuthZ, &instance.AddTrustedDomainRequest{InstanceId: inst.ID(), Domain: d2}) + require.Nil(t, err) + + t.Cleanup(func() { + inst.Client.InstanceV2Beta.RemoveTrustedDomain(ctxWithSysAuthZ, &instance.RemoveTrustedDomainRequest{InstanceId: inst.ID(), Domain: d1}) + inst.Client.InstanceV2Beta.RemoveTrustedDomain(ctxWithSysAuthZ, &instance.RemoveTrustedDomainRequest{InstanceId: inst.ID(), Domain: d2}) + inst.Client.InstanceV2Beta.DeleteInstance(ctxWithSysAuthZ, &instance.DeleteInstanceRequest{InstanceId: inst.ID()}) + }) + + tt := []struct { + testName string + inputRequest *instance.ListTrustedDomainsRequest + inputContext context.Context + expectedErrorMsg string + expectedErrorCode codes.Code + expectedDomains []string + }{ + { + testName: "when invalid context should return unauthN error", + inputRequest: &instance.ListTrustedDomainsRequest{ + InstanceId: inst.ID(), + Pagination: &filter.PaginationRequest{Offset: 0, Limit: 10}, + }, + inputContext: context.Background(), + expectedErrorCode: codes.Unauthenticated, + expectedErrorMsg: "auth header missing", + }, + { + testName: "when unauthZ context should return unauthZ error", + inputRequest: &instance.ListTrustedDomainsRequest{ + InstanceId: inst.ID(), + Pagination: &filter.PaginationRequest{Offset: 0, Limit: 10}, + }, + inputContext: orgOwnerCtx, + expectedErrorCode: codes.PermissionDenied, + expectedErrorMsg: "No matching permissions found (AUTH-5mWD2)", + }, + { + testName: "when valid request with filter should return paginated response", + inputRequest: &instance.ListTrustedDomainsRequest{ + InstanceId: inst.ID(), + Pagination: &filter.PaginationRequest{Offset: 0, Limit: 10}, + SortingColumn: instance.TrustedDomainFieldName_TRUSTED_DOMAIN_FIELD_NAME_CREATION_DATE, + Queries: []*instance.TrustedDomainSearchQuery{ + { + Query: &instance.TrustedDomainSearchQuery_DomainQuery{ + DomainQuery: &instance.DomainQuery{Domain: "trusted", Method: object.TextQueryMethod_TEXT_QUERY_METHOD_CONTAINS}, + }, + }, + }, + }, + inputContext: ctxWithSysAuthZ, + expectedDomains: []string{d1, d2}, + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + // Test + res, err := inst.Client.InstanceV2Beta.ListTrustedDomains(tc.inputContext, tc.inputRequest) + + // Verify + assert.Equal(t, tc.expectedErrorCode, status.Code(err)) + assert.Equal(t, tc.expectedErrorMsg, status.Convert(err).Message()) + + if tc.expectedErrorMsg == "" { + require.NotNil(t, res) + + domains := []string{} + for _, d := range res.GetTrustedDomain() { + domains = append(domains, d.GetDomain()) + } + + assert.Subset(t, domains, tc.expectedDomains) + } + }) + } +} diff --git a/internal/api/grpc/instance/v2beta/query.go b/internal/api/grpc/instance/v2beta/query.go new file mode 100644 index 0000000000..74f79313ea --- /dev/null +++ b/internal/api/grpc/instance/v2beta/query.go @@ -0,0 +1,70 @@ +package instance + +import ( + "context" + + filter "github.com/zitadel/zitadel/internal/api/grpc/filter/v2beta" + instance "github.com/zitadel/zitadel/pkg/grpc/instance/v2beta" +) + +func (s *Server) GetInstance(ctx context.Context, _ *instance.GetInstanceRequest) (*instance.GetInstanceResponse, error) { + inst, err := s.query.Instance(ctx, true) + if err != nil { + return nil, err + } + + return &instance.GetInstanceResponse{ + Instance: ToProtoObject(inst), + }, nil +} + +func (s *Server) ListInstances(ctx context.Context, req *instance.ListInstancesRequest) (*instance.ListInstancesResponse, error) { + queries, err := ListInstancesRequestToModel(req, s.systemDefaults) + if err != nil { + return nil, err + } + + instances, err := s.query.SearchInstances(ctx, queries) + if err != nil { + return nil, err + } + + return &instance.ListInstancesResponse{ + Instances: InstancesToPb(instances.Instances), + Pagination: filter.QueryToPaginationPb(queries.SearchRequest, instances.SearchResponse), + }, nil +} + +func (s *Server) ListCustomDomains(ctx context.Context, req *instance.ListCustomDomainsRequest) (*instance.ListCustomDomainsResponse, error) { + queries, err := ListCustomDomainsRequestToModel(req, s.systemDefaults) + if err != nil { + return nil, err + } + + domains, err := s.query.SearchInstanceDomains(ctx, queries) + if err != nil { + return nil, err + } + + return &instance.ListCustomDomainsResponse{ + Domains: DomainsToPb(domains.Domains), + Pagination: filter.QueryToPaginationPb(queries.SearchRequest, domains.SearchResponse), + }, nil +} + +func (s *Server) ListTrustedDomains(ctx context.Context, req *instance.ListTrustedDomainsRequest) (*instance.ListTrustedDomainsResponse, error) { + queries, err := ListTrustedDomainsRequestToModel(req, s.systemDefaults) + if err != nil { + return nil, err + } + + domains, err := s.query.SearchInstanceTrustedDomains(ctx, queries) + if err != nil { + return nil, err + } + + return &instance.ListTrustedDomainsResponse{ + TrustedDomain: trustedDomainsToPb(domains.Domains), + Pagination: filter.QueryToPaginationPb(queries.SearchRequest, domains.SearchResponse), + }, nil +} diff --git a/internal/api/grpc/instance/v2beta/server.go b/internal/api/grpc/instance/v2beta/server.go new file mode 100644 index 0000000000..aaeaa4cc8f --- /dev/null +++ b/internal/api/grpc/instance/v2beta/server.go @@ -0,0 +1,60 @@ +package instance + +import ( + "google.golang.org/grpc" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/api/grpc/server" + "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/config/systemdefaults" + "github.com/zitadel/zitadel/internal/query" + instance "github.com/zitadel/zitadel/pkg/grpc/instance/v2beta" +) + +var _ instance.InstanceServiceServer = (*Server)(nil) + +type Server struct { + instance.UnimplementedInstanceServiceServer + command *command.Commands + query *query.Queries + systemDefaults systemdefaults.SystemDefaults + defaultInstance command.InstanceSetup + externalDomain string +} + +type Config struct{} + +func CreateServer( + command *command.Commands, + query *query.Queries, + database string, + defaultInstance command.InstanceSetup, + externalDomain string, +) *Server { + return &Server{ + command: command, + query: query, + defaultInstance: defaultInstance, + externalDomain: externalDomain, + } +} + +func (s *Server) RegisterServer(grpcServer *grpc.Server) { + instance.RegisterInstanceServiceServer(grpcServer, s) +} + +func (s *Server) AppName() string { + return instance.InstanceService_ServiceDesc.ServiceName +} + +func (s *Server) MethodPrefix() string { + return instance.InstanceService_ServiceDesc.ServiceName +} + +func (s *Server) AuthMethods() authz.MethodMapping { + return instance.InstanceService_AuthMethods +} + +func (s *Server) RegisterGateway() server.RegisterGatewayFunc { + return instance.RegisterInstanceServiceHandler +} diff --git a/internal/command/instance.go b/internal/command/instance.go index 1080168842..d71be53468 100644 --- a/internal/command/instance.go +++ b/internal/command/instance.go @@ -2,6 +2,7 @@ package command import ( "context" + "strings" "time" "github.com/zitadel/logging" @@ -666,7 +667,7 @@ func setupMessageTexts(validations *[]preparation.Validation, setupMessageTexts func (c *Commands) UpdateInstance(ctx context.Context, name string) (*domain.ObjectDetails, error) { instanceAgg := instance.NewAggregate(authz.GetInstance(ctx).InstanceID()) - validation := c.prepareUpdateInstance(instanceAgg, name) + validation := c.prepareUpdateInstance(instanceAgg, strings.TrimSpace(name)) cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, validation) if err != nil { return nil, err @@ -885,7 +886,12 @@ func getSystemConfigWriteModel(ctx context.Context, filter preparation.FilterToQ } func (c *Commands) RemoveInstance(ctx context.Context, id string) (*domain.ObjectDetails, error) { - instanceAgg := instance.NewAggregate(id) + instID := strings.TrimSpace(id) + if instID == "" || len(instID) > 200 { + return nil, zerrors.ThrowInvalidArgument(nil, "COMMA-VeS2zI", "Errors.Invalid.Argument") + } + + instanceAgg := instance.NewAggregate(instID) cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, c.prepareRemoveInstance(instanceAgg)) if err != nil { return nil, err diff --git a/internal/command/instance_test.go b/internal/command/instance_test.go index 2ea248dfb1..16e51d844d 100644 --- a/internal/command/instance_test.go +++ b/internal/command/instance_test.go @@ -1257,7 +1257,7 @@ func TestCommandSide_UpdateInstance(t *testing.T) { }, args: args{ ctx: authz.WithInstanceID(context.Background(), "INSTANCE"), - name: "", + name: " ", }, res: res{ err: zerrors.IsErrorInvalidArgument, @@ -1404,6 +1404,32 @@ func TestCommandSide_RemoveInstance(t *testing.T) { args args res res }{ + { + name: "instance empty, invalid argument error", + fields: fields{ + eventstore: func(t *testing.T) *eventstore.Eventstore { return &eventstore.Eventstore{} }, + }, + args: args{ + ctx: authz.WithInstanceID(context.Background(), " "), + instanceID: " ", + }, + res: res{ + err: zerrors.IsErrorInvalidArgument, + }, + }, + { + name: "instance too long, invalid argument error", + fields: fields{ + eventstore: func(t *testing.T) *eventstore.Eventstore { return &eventstore.Eventstore{} }, + }, + args: args{ + ctx: authz.WithInstanceID(context.Background(), "averylonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonginstance"), + instanceID: "averylonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonginstance", + }, + res: res{ + err: zerrors.IsErrorInvalidArgument, + }, + }, { name: "instance not existing, not found error", fields: fields{ diff --git a/internal/command/instance_trusted_domain.go b/internal/command/instance_trusted_domain.go index f404e6665a..7d3e6abeb1 100644 --- a/internal/command/instance_trusted_domain.go +++ b/internal/command/instance_trusted_domain.go @@ -34,6 +34,14 @@ func (c *Commands) AddTrustedDomain(ctx context.Context, trustedDomain string) ( } func (c *Commands) RemoveTrustedDomain(ctx context.Context, trustedDomain string) (*domain.ObjectDetails, error) { + trustedDomain = strings.TrimSpace(trustedDomain) + if trustedDomain == "" || len(trustedDomain) > 253 { + return nil, zerrors.ThrowInvalidArgument(nil, "COMMA-ajAzwu", "Errors.Invalid.Argument") + } + if !allowDomainRunes.MatchString(trustedDomain) { + return nil, zerrors.ThrowInvalidArgument(nil, "COMMA-lfs3Te", "Errors.Instance.Domain.InvalidCharacter") + } + model := NewInstanceTrustedDomainsWriteModel(ctx) err := c.eventstore.FilterToQueryReducer(ctx, model) if err != nil { diff --git a/internal/command/instance_trusted_domain_test.go b/internal/command/instance_trusted_domain_test.go index 3caef90f01..4ba9f773ed 100644 --- a/internal/command/instance_trusted_domain_test.go +++ b/internal/command/instance_trusted_domain_test.go @@ -142,6 +142,45 @@ func TestCommands_RemoveTrustedDomain(t *testing.T) { args args want want }{ + { + name: "domain empty string, error", + fields: fields{ + eventstore: func(t *testing.T) *eventstore.Eventstore { return &eventstore.Eventstore{} }, + }, + args: args{ + ctx: authz.WithInstanceID(context.Background(), "instanceID"), + trustedDomain: " ", + }, + want: want{ + err: zerrors.ThrowInvalidArgument(nil, "COMMA-ajAzwu", "Errors.Invalid.Argument"), + }, + }, + { + name: "domain invalid character, error", + fields: fields{ + eventstore: func(t *testing.T) *eventstore.Eventstore { return &eventstore.Eventstore{} }, + }, + args: args{ + ctx: authz.WithInstanceID(context.Background(), "instanceID"), + trustedDomain: "? ", + }, + want: want{ + err: zerrors.ThrowInvalidArgument(nil, "COMMA-lfs3Te", "Errors.Instance.Domain.InvalidCharacter"), + }, + }, + { + name: "domain length exceeded, error", + fields: fields{ + eventstore: func(t *testing.T) *eventstore.Eventstore { return &eventstore.Eventstore{} }, + }, + args: args{ + ctx: authz.WithInstanceID(context.Background(), "instanceID"), + trustedDomain: "averylonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglongdomain", + }, + want: want{ + err: zerrors.ThrowInvalidArgument(nil, "COMMA-ajAzwu", "Errors.Invalid.Argument"), + }, + }, { name: "domain does not exists, error", fields: fields{ diff --git a/internal/integration/client.go b/internal/integration/client.go index f1bcfb41bd..bd9775a28a 100644 --- a/internal/integration/client.go +++ b/internal/integration/client.go @@ -26,6 +26,7 @@ import ( feature_v2beta "github.com/zitadel/zitadel/pkg/grpc/feature/v2beta" "github.com/zitadel/zitadel/pkg/grpc/idp" idp_pb "github.com/zitadel/zitadel/pkg/grpc/idp/v2" + instance "github.com/zitadel/zitadel/pkg/grpc/instance/v2beta" mgmt "github.com/zitadel/zitadel/pkg/grpc/management" "github.com/zitadel/zitadel/pkg/grpc/object/v2" object_v3alpha "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha" @@ -70,6 +71,11 @@ type Client struct { UserV3Alpha user_v3alpha.ZITADELUsersClient SAMLv2 saml_pb.SAMLServiceClient SCIM *scim.Client + InstanceV2Beta instance.InstanceServiceClient +} + +func NewDefaultClient(ctx context.Context) (*Client, error) { + return newClient(ctx, loadedConfig.Host()) } func newClient(ctx context.Context, target string) (*Client, error) { @@ -103,6 +109,7 @@ func newClient(ctx context.Context, target string) (*Client, error) { UserV3Alpha: user_v3alpha.NewZITADELUsersClient(cc), SAMLv2: saml_pb.NewSAMLServiceClient(cc), SCIM: scim.NewScimClient(target), + InstanceV2Beta: instance.NewInstanceServiceClient(cc), } return client, client.pollHealth(ctx) } diff --git a/internal/query/instance.go b/internal/query/instance.go index 1b3bb055cb..bb311cbb85 100644 --- a/internal/query/instance.go +++ b/internal/query/instance.go @@ -150,7 +150,7 @@ func (q *Queries) SearchInstances(ctx context.Context, queries *InstanceSearchQu ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - filter, query, scan := prepareInstancesQuery() + filter, query, scan := prepareInstancesQuery(queries.SortingColumn, queries.Asc) stmt, args, err := query(queries.toQuery(filter)).ToSql() if err != nil { return nil, zerrors.ThrowInvalidArgument(err, "QUERY-M9fow", "Errors.Query.SQLStatement") @@ -260,17 +260,20 @@ func (q *Queries) GetDefaultLanguage(ctx context.Context) language.Tag { return instance.DefaultLang } -func prepareInstancesQuery() (sq.SelectBuilder, func(sq.SelectBuilder) sq.SelectBuilder, func(*sql.Rows) (*Instances, error)) { +func prepareInstancesQuery(sortBy Column, isAscedingSort bool) (sq.SelectBuilder, func(sq.SelectBuilder) sq.SelectBuilder, func(*sql.Rows) (*Instances, error)) { instanceFilterTable := instanceTable.setAlias(InstancesFilterTableAlias) instanceFilterIDColumn := InstanceColumnID.setTable(instanceFilterTable) instanceFilterCountColumn := InstancesFilterTableAlias + ".count" - return sq.Select( - InstanceColumnID.identifier(), - countColumn.identifier(), - ).Distinct().From(instanceTable.identifier()). + + selector := sq.Select(InstanceColumnID.identifier(), countColumn.identifier()) + if !sortBy.isZero() { + selector = sq.Select(InstanceColumnID.identifier(), countColumn.identifier(), sortBy.identifier()) + } + + return selector.Distinct().From(instanceTable.identifier()). LeftJoin(join(InstanceDomainInstanceIDCol, InstanceColumnID)), func(builder sq.SelectBuilder) sq.SelectBuilder { - return sq.Select( + outerQuery := sq.Select( instanceFilterCountColumn, instanceFilterIDColumn.identifier(), InstanceColumnCreationDate.identifier(), @@ -292,6 +295,16 @@ func prepareInstancesQuery() (sq.SelectBuilder, func(sq.SelectBuilder) sq.Select LeftJoin(join(InstanceColumnID, instanceFilterIDColumn)). LeftJoin(join(InstanceDomainInstanceIDCol, instanceFilterIDColumn)). PlaceholderFormat(sq.Dollar) + + if !sortBy.isZero() { + sorting := sortBy.identifier() + if !isAscedingSort { + sorting += " DESC" + } + return outerQuery.OrderBy(sorting) + } + + return outerQuery }, func(rows *sql.Rows) (*Instances, error) { instances := make([]*Instance, 0) diff --git a/internal/query/instance_test.go b/internal/query/instance_test.go index 55b1c8314b..37adfc8605 100644 --- a/internal/query/instance_test.go +++ b/internal/query/instance_test.go @@ -70,7 +70,7 @@ func Test_InstancePrepares(t *testing.T) { { name: "prepareInstancesQuery no result", prepare: func() (sq.SelectBuilder, func(*sql.Rows) (*Instances, error)) { - filter, query, scan := prepareInstancesQuery() + filter, query, scan := prepareInstancesQuery(Column{}, true) return query(filter), scan }, want: want{ @@ -85,7 +85,7 @@ func Test_InstancePrepares(t *testing.T) { { name: "prepareInstancesQuery one result", prepare: func() (sq.SelectBuilder, func(*sql.Rows) (*Instances, error)) { - filter, query, scan := prepareInstancesQuery() + filter, query, scan := prepareInstancesQuery(Column{}, true) return query(filter), scan }, want: want{ @@ -149,7 +149,7 @@ func Test_InstancePrepares(t *testing.T) { { name: "prepareInstancesQuery multiple results", prepare: func() (sq.SelectBuilder, func(*sql.Rows) (*Instances, error)) { - filter, query, scan := prepareInstancesQuery() + filter, query, scan := prepareInstancesQuery(Column{}, true) return query(filter), scan }, want: want{ @@ -253,7 +253,8 @@ func Test_InstancePrepares(t *testing.T) { IsPrimary: true, }, }, - }, { + }, + { ID: "id2", CreationDate: testNow, ChangeDate: testNow, @@ -282,7 +283,7 @@ func Test_InstancePrepares(t *testing.T) { { name: "prepareInstancesQuery sql err", prepare: func() (sq.SelectBuilder, func(*sql.Rows) (*Instances, error)) { - filter, query, scan := prepareInstancesQuery() + filter, query, scan := prepareInstancesQuery(Column{}, true) return query(filter), scan }, want: want{ diff --git a/proto/zitadel/admin.proto b/proto/zitadel/admin.proto index d9f8bee2c7..9033fd8668 100644 --- a/proto/zitadel/admin.proto +++ b/proto/zitadel/admin.proto @@ -307,6 +307,7 @@ service AdminService { }; } + // Deprecated: Use [ListCustomDomains](apis/resources/instance_service_v2/instance-service-list-custom-domains.api.mdx) instead to list custom domains rpc ListInstanceDomains(ListInstanceDomainsRequest) returns (ListInstanceDomainsResponse) { option (google.api.http) = { post: "/domains/_search"; @@ -319,10 +320,12 @@ service AdminService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Instance"; summary: "List Instance Domains"; - description: "Returns a list of domains that are configured for this ZITADEL instance. These domains are the URLs where ZITADEL is running." + description: "Returns a list of domains that are configured for this ZITADEL instance. These domains are the URLs where ZITADEL is running."; + deprecated: true; }; } + // Deprecated: Use [ListTrustedDomains](apis/resources/instance_service_v2/instance-service-list-trusted-domains.api.mdx) instead to list trusted domains rpc ListInstanceTrustedDomains(ListInstanceTrustedDomainsRequest) returns (ListInstanceTrustedDomainsResponse) { option (google.api.http) = { post: "/trusted_domains/_search"; @@ -335,10 +338,12 @@ service AdminService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Instance"; summary: "List Instance Trusted Domains"; - description: "Returns a list of domains that are configured for this ZITADEL instance. These domains are trusted to be used as public hosts." + description: "Returns a list of domains that are configured for this ZITADEL instance. These domains are trusted to be used as public hosts."; + deprecated: true; }; } + // Deprecated: Use [AddTrustedDomain](apis/resources/instance_service_v2/instance-service-add-trusted-domain.api.mdx) instead to add a trusted domain rpc AddInstanceTrustedDomain(AddInstanceTrustedDomainRequest) returns (AddInstanceTrustedDomainResponse) { option (google.api.http) = { post: "/trusted_domains"; @@ -352,10 +357,12 @@ service AdminService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Instance"; summary: "Add an Instance Trusted Domain"; - description: "Returns a list of domains that are configured for this ZITADEL instance. These domains are trusted to be used as public hosts." + description: "Returns a list of domains that are configured for this ZITADEL instance. These domains are trusted to be used as public hosts."; + deprecated: true; }; } + // Deprecated: Use [RemoveTrustedDomain](apis/resources/instance_service_v2/instance-service-remove-trusted-domain.api.mdx) instead to remove a trusted domain rpc RemoveInstanceTrustedDomain(RemoveInstanceTrustedDomainRequest) returns (RemoveInstanceTrustedDomainResponse) { option (google.api.http) = { delete: "/trusted_domains/{domain}"; @@ -368,7 +375,8 @@ service AdminService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Instance"; summary: "Remove an Instance Trusted Domain"; - description: "Returns a list of domains that are configured for this ZITADEL instance. These domains are trusted to be used as public hosts." + description: "Returns a list of domains that are configured for this ZITADEL instance. These domains are trusted to be used as public hosts."; + deprecated: true; }; } diff --git a/proto/zitadel/instance/v2beta/instance.proto b/proto/zitadel/instance/v2beta/instance.proto new file mode 100644 index 0000000000..21f6148490 --- /dev/null +++ b/proto/zitadel/instance/v2beta/instance.proto @@ -0,0 +1,192 @@ +syntax = "proto3"; + +import "protoc-gen-openapiv2/options/annotations.proto"; +import "zitadel/object/v2/object.proto"; +import "validate/validate.proto"; +import "google/protobuf/timestamp.proto"; + +package zitadel.instance.v2beta; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/instance/v2beta;instance"; + +message Instance { + string id = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629023906488334\"" + } + ]; + + // change_date is the timestamp when the object was changed + // + // on read: the timestamp of the last event reduced by the projection + // + // on manipulation: the timestamp of the event(s) added by the manipulation + google.protobuf.Timestamp change_date = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; + + google.protobuf.Timestamp creation_date = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; + State state = 4 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "current state of the instance"; + } + ]; + string name = 5 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"ZITADEL\""; + } + ]; + string version = 6 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"1.0.0\""; + } + ]; + repeated Domain domains = 7; +} + +enum State { + STATE_UNSPECIFIED = 0; + STATE_CREATING = 1; + STATE_RUNNING = 2; + STATE_STOPPING = 3; + STATE_STOPPED = 4; +} + +message Domain { + string instance_id = 1; + + google.protobuf.Timestamp creation_date = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; + + string domain = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"zitadel.com\"" + } + ]; + bool primary = 4; + bool generated = 5; +} + +enum FieldName { + FIELD_NAME_UNSPECIFIED = 0; + FIELD_NAME_ID = 1; + FIELD_NAME_NAME = 2; + FIELD_NAME_CREATION_DATE = 3; +} + +message Query { + oneof query { + option (validate.required) = true; + + IdsQuery id_query = 1; + DomainsQuery domain_query = 2; + } +} + +message IdsQuery { + repeated string ids = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Instance ID"; + example: "[\"4820840938402429\",\"4820840938402422\"]" + } + ]; +} + +message DomainsQuery { + repeated string domains = 1 [ + (validate.rules).repeated = {max_items: 20, items: {string: {min_len: 1, max_len: 100}}}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + max_items: 20; + example: "[\"my-instace.zitadel.cloud\", \"auth.custom.com\"]"; + description: "Return the instances that have the requested domains"; + } + ]; +} +message DomainSearchQuery { + oneof query { + option (validate.required) = true; + + DomainQuery domain_query = 1; + DomainGeneratedQuery generated_query = 2; + DomainPrimaryQuery primary_query = 3; + } +} + +message DomainQuery { + string domain = 1 [ + (validate.rules).string = {max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + max_length: 200; + example: "\"zitadel.com\""; + } + ]; + zitadel.object.v2.TextQueryMethod method = 2 [ + (validate.rules).enum.defined_only = true, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Defines which text equality method is used"; + } + ]; +} + +message DomainGeneratedQuery { + bool generated = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Generated domains"; + } + ]; +} + +message DomainPrimaryQuery { + bool primary = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Primary domains"; + } + ]; +} + +enum DomainFieldName { + DOMAIN_FIELD_NAME_UNSPECIFIED = 0; + DOMAIN_FIELD_NAME_DOMAIN = 1; + DOMAIN_FIELD_NAME_PRIMARY = 2; + DOMAIN_FIELD_NAME_GENERATED = 3; + DOMAIN_FIELD_NAME_CREATION_DATE = 4; +} + +message TrustedDomain { + string instance_id = 1; + + google.protobuf.Timestamp creation_date = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; + + string domain = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"zitadel.com\"" + } + ]; +} + +message TrustedDomainSearchQuery { + oneof query { + option (validate.required) = true; + + DomainQuery domain_query = 1; + } +} + +enum TrustedDomainFieldName { + TRUSTED_DOMAIN_FIELD_NAME_UNSPECIFIED = 0; + TRUSTED_DOMAIN_FIELD_NAME_DOMAIN = 1; + TRUSTED_DOMAIN_FIELD_NAME_CREATION_DATE = 2; +} diff --git a/proto/zitadel/instance/v2beta/instance_service.proto b/proto/zitadel/instance/v2beta/instance_service.proto new file mode 100644 index 0000000000..0a5de00286 --- /dev/null +++ b/proto/zitadel/instance/v2beta/instance_service.proto @@ -0,0 +1,648 @@ +syntax = "proto3"; + +package zitadel.instance.v2beta; + +import "validate/validate.proto"; +import "zitadel/object/v2/object.proto"; +import "zitadel/instance/v2beta/instance.proto"; +import "zitadel/filter/v2beta/filter.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; +import "google/protobuf/empty.proto"; +import "google/api/annotations.proto"; +import "google/api/field_behavior.proto"; +import "google/protobuf/timestamp.proto"; +import "zitadel/protoc_gen_zitadel/v2/options.proto"; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/instance/v2beta;instance"; + +option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { + info: { + title: "Instance Service"; + version: "2.0-beta"; + description: "This API is intended to manage instances in ZITADEL."; + contact:{ + name: "ZITADEL" + url: "https://zitadel.com" + email: "hi@zitadel.com" + } + license: { + name: "AGPL-3.0-only", + url: "https://github.com/zitadel/zitadel/blob/main/LICENSING.md"; + }; + }; + schemes: HTTPS; + schemes: HTTP; + + consumes: "application/json"; + consumes: "application/grpc"; + + produces: "application/json"; + produces: "application/grpc"; + + consumes: "application/grpc-web+proto"; + produces: "application/grpc-web+proto"; + + host: "$CUSTOM-DOMAIN"; + base_path: "/"; + + external_docs: { + description: "Detailed information about ZITADEL", + url: "https://zitadel.com/docs" + } + security_definitions: { + security: { + key: "OAuth2"; + value: { + type: TYPE_OAUTH2; + flow: FLOW_ACCESS_CODE; + authorization_url: "$CUSTOM-DOMAIN/oauth/v2/authorize"; + token_url: "$CUSTOM-DOMAIN/oauth/v2/token"; + scopes: { + scope: { + key: "openid"; + value: "openid"; + } + scope: { + key: "urn:zitadel:iam:org:project:id:zitadel:aud"; + value: "urn:zitadel:iam:org:project:id:zitadel:aud"; + } + } + } + } + } + security: { + security_requirement: { + key: "OAuth2"; + value: { + scope: "openid"; + scope: "urn:zitadel:iam:org:project:id:zitadel:aud"; + } + } + } + responses: { + key: "403"; + value: { + description: "Returned when the user does not have permission to access the resource."; + schema: { + json_schema: { + ref: "#/definitions/rpcStatus"; + } + } + } + } + responses: { + key: "404"; + value: { + description: "Returned when the resource does not exist."; + schema: { + json_schema: { + ref: "#/definitions/rpcStatus"; + } + } + } + } +}; + +// Service to manage instances and their domains. +// The service provides methods to create, update, delete and list instances and their domains. +service InstanceService { + + // Delete Instance + // + // Deletes an instance with the given ID. + // + // Required permissions: + // - `system.instance.delete` + rpc DeleteInstance(DeleteInstanceRequest) returns (DeleteInstanceResponse) { + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "The deleted instance."; + } + }; + }; + + option (google.api.http) = { + delete: "/v2beta/instances/{instance_id}" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "system.instance.delete" + } + }; + } + + // Get Instance + // + // Returns the instance in the current context. + // + // The instace_id in the input message will be used in the future. + // + // Required permissions: + // - `iam.read` + rpc GetInstance(GetInstanceRequest) returns (GetInstanceResponse) { + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "The instance of the context."; + } + }; + }; + + option (google.api.http) = { + get: "/v2beta/instances/{instance_id}" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "iam.read" + } + }; + } + + // Update Instance + // + // Updates instance in context with the given name. + // + // The instance_id in the input message will be used in the future. + // + // Required permissions: + // - `iam.write` + rpc UpdateInstance(UpdateInstanceRequest) returns (UpdateInstanceResponse) { + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "The instance was successfully updated."; + } + }; + }; + + option (google.api.http) = { + put: "/v2beta/instances/{instance_id}" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "iam.write" + } + }; + } + + // List Instances + // + // Lists instances matching the given query. + // The query can be used to filter either by instance ID or domain. + // The request is paginated and returns 100 results by default. + // + // Required permissions: + // - `system.instance.read` + rpc ListInstances(ListInstancesRequest) returns (ListInstancesResponse) { + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "The list of instances."; + } + }; + }; + + option (google.api.http) = { + post: "/v2beta/instances/search" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "system.instance.read" + } + }; + } + + // Add Custom Domain + // + // Adds a custom domain to the instance in context. + // + // The instance_id in the input message will be used in the future + // + // Required permissions: + // - `system.domain.write` + rpc AddCustomDomain(AddCustomDomainRequest) returns (AddCustomDomainResponse) { + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "The added custom domain."; + } + }; + }; + + option (google.api.http) = { + post: "/v2beta/instances/{instance_id}/custom-domains" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "system.domain.write" + } + }; + } + + // Remove Custom Domain + // + // Removes a custom domain from the instance. + // + // The instance_id in the input message will be used in the future. + // + // Required permissions: + // - `system.domain.write` + rpc RemoveCustomDomain(RemoveCustomDomainRequest) returns (RemoveCustomDomainResponse) { + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "The removed custom domain."; + } + }; + }; + + option (google.api.http) = { + delete: "/v2beta/instances/{instance_id}/custom-domains/{domain}" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "system.domain.write" + } + }; + } + + // List Custom Domains + // + // Lists custom domains of the instance. + // + // The instance_id in the input message will be used in the future. + // + // Required permissions: + // - `iam.read` + rpc ListCustomDomains(ListCustomDomainsRequest) returns (ListCustomDomainsResponse) { + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "The list of custom domains."; + } + }; + }; + + option (google.api.http) = { + post: "/v2beta/instances/{instance_id}/custom-domains/search" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "iam.read" + } + }; + } + + // Add Trusted Domain + // + // Adds a trusted domain to the instance. + // + // The instance_id in the input message will be used in the future. + // + // Required permissions: + // - `iam.write` + rpc AddTrustedDomain(AddTrustedDomainRequest) returns (AddTrustedDomainResponse) { + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "The added trusted domain."; + } + }; + }; + + option (google.api.http) = { + post: "/v2beta/instances/{instance_id}/trusted-domains" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "iam.write" + } + }; + } + + // Remove Trusted Domain + // + // Removes a trusted domain from the instance. + // + // The instance_id in the input message will be used in the future. + // + // Required permissions: + // - `iam.write` + rpc RemoveTrustedDomain(RemoveTrustedDomainRequest) returns (RemoveTrustedDomainResponse) { + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "The removed trusted domain."; + } + }; + }; + + option (google.api.http) = { + delete: "/v2beta/instances/{instance_id}/trusted-domains/{domain}" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "iam.write" + } + }; + } + + + // List Trusted Domains + // + // Lists trusted domains of the instance. + // + // The instance_id in the input message will be used in the future. + // + // Required permissions: + // - `iam.read` + rpc ListTrustedDomains(ListTrustedDomainsRequest) returns (ListTrustedDomainsResponse) { + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "The list of trusted domains."; + } + }; + }; + + option (google.api.http) = { + post: "/v2beta/instances/{instance_id}/trusted-domains/search" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "iam.read" + } + }; + } +} + +message DeleteInstanceRequest { + string instance_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"222430354126975533\""; + } + ]; +} + +message DeleteInstanceResponse { + google.protobuf.Timestamp deletion_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message GetInstanceRequest { + string instance_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"222430354126975533\""; + } + ]; +} + +message GetInstanceResponse { + zitadel.instance.v2beta.Instance instance = 1; +} + +message UpdateInstanceRequest { + // used only to identify the instance to change. + string instance_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"222430354126975533\""; + } + ]; + string instance_name = 2 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + description: "\"name of the instance to update\""; + example: "\"my instance\""; + } + ]; +} + +message UpdateInstanceResponse { + google.protobuf.Timestamp change_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message ListInstancesRequest { + // Criterias the client is looking for. + repeated Query queries = 1; + + // Pagination and sorting. + zitadel.filter.v2beta.PaginationRequest pagination = 2; + + // The field the result is sorted by. + optional FieldName sorting_column = 3; +} + +message ListInstancesResponse { + // The list of instances. + repeated Instance instances = 1; + + // Contains the total number of instances matching the query and the applied limit. + zitadel.filter.v2beta.PaginationResponse pagination = 2; +} + +message AddCustomDomainRequest { + string instance_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"222430354126975533\""; + } + ]; + string domain = 2 [ + (validate.rules).string = {min_len: 1, max_len: 253}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 253; + } + ]; +} + +message AddCustomDomainResponse { + google.protobuf.Timestamp creation_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message RemoveCustomDomainRequest { + string instance_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"222430354126975533\""; + } + ]; + string domain = 2 [ + (validate.rules).string = {min_len: 1, max_len: 253}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 253; + } + ]; +} + +message RemoveCustomDomainResponse { + google.protobuf.Timestamp deletion_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message ListCustomDomainsRequest { + string instance_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"222430354126975533\""; + } + ]; + + // Pagination and sorting. + zitadel.filter.v2beta.PaginationRequest pagination = 2; + + // The field the result is sorted by. + DomainFieldName sorting_column = 3; + + // Criterias the client is looking for. + repeated DomainSearchQuery queries = 4; +} + +message ListCustomDomainsResponse { + repeated Domain domains = 1; + + // Contains the total number of domains matching the query and the applied limit. + zitadel.filter.v2beta.PaginationResponse pagination = 2; +} + +message AddTrustedDomainRequest { + string instance_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"222430354126975533\""; + } + ]; + string domain = 2 [ + (validate.rules).string = {min_len: 1, max_len: 253}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"login.example.com\""; + min_length: 1; + max_length: 253; + } + ]; +} + +message AddTrustedDomainResponse { + google.protobuf.Timestamp creation_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message RemoveTrustedDomainRequest { + string instance_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"222430354126975533\""; + } + ]; + string domain = 2 [ + (validate.rules).string = {min_len: 1, max_len: 253}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"login.example.com\""; + min_length: 1; + max_length: 253; + } + ]; +} + +message RemoveTrustedDomainResponse { + google.protobuf.Timestamp deletion_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message ListTrustedDomainsRequest { + string instance_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"222430354126975533\""; + } + ]; + + // Pagination and sorting. + zitadel.filter.v2beta.PaginationRequest pagination = 2; + + // The field the result is sorted by. + TrustedDomainFieldName sorting_column = 3; + + // Criterias the client is looking for. + repeated TrustedDomainSearchQuery queries = 4; +} + +message ListTrustedDomainsResponse { + repeated TrustedDomain trusted_domain = 1; + + // Contains the total number of domains matching the query and the applied limit. + zitadel.filter.v2beta.PaginationResponse pagination = 2; +} diff --git a/proto/zitadel/system.proto b/proto/zitadel/system.proto index f124c37a79..09b5559fb9 100644 --- a/proto/zitadel/system.proto +++ b/proto/zitadel/system.proto @@ -117,6 +117,8 @@ service SystemService { } // Returns a list of ZITADEL instances + // + // Deprecated: Use [ListInstances](apis/resources/instance_service_v2/instance-service-list-instances.api.mdx) instead to list instances rpc ListInstances(ListInstancesRequest) returns (ListInstancesResponse) { option (google.api.http) = { post: "/instances/_search" @@ -126,9 +128,15 @@ service SystemService { option (zitadel.v1.auth_option) = { permission: "system.instance.read"; }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + deprecated: true; + }; } // Returns the detail of an instance + // + // Deprecated: Use [GetInstance](apis/resources/instance_service_v2/instance-service-get-instance.api.mdx) instead to get the details of the instance in context rpc GetInstance(GetInstanceRequest) returns (GetInstanceResponse) { option (google.api.http) = { get: "/instances/{instance_id}"; @@ -137,6 +145,10 @@ service SystemService { option (zitadel.v1.auth_option) = { permission: "system.instance.read"; }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + deprecated: true; + }; } // Deprecated: Use CreateInstance instead @@ -151,9 +163,15 @@ service SystemService { option (zitadel.v1.auth_option) = { permission: "system.instance.write"; }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + deprecated: true; + }; } // Updates name of an existing instance + // + // Deprecated: Use [UpdateInstance](apis/resources/instance_service_v2/instance-service-update-instance.api.mdx) instead to update the name of the instance in context rpc UpdateInstance(UpdateInstanceRequest) returns (UpdateInstanceResponse) { option (google.api.http) = { put: "/instances/{instance_id}" @@ -163,6 +181,10 @@ service SystemService { option (zitadel.v1.auth_option) = { permission: "system.instance.write"; }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + deprecated: true; + }; } // Creates a new instance with all needed setup data @@ -180,6 +202,8 @@ service SystemService { // Removes an instance // This might take some time + // + // Deprecated: Use [DeleteInstance](apis/resources/instance_service_v2/instance-service-delete-instance.api.mdx) instead to delete an instance rpc RemoveInstance(RemoveInstanceRequest) returns (RemoveInstanceResponse) { option (google.api.http) = { delete: "/instances/{instance_id}" @@ -188,6 +212,10 @@ service SystemService { option (zitadel.v1.auth_option) = { permission: "system.instance.delete"; }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + deprecated: true; + }; } //Returns all instance members matching the request @@ -204,7 +232,9 @@ service SystemService { }; } - //Checks if a domain exists + // Checks if a domain exists + // + // Deprecated: Use [ListCustomDomains](apis/resources/instance_service_v2/instance-service-list-custom-domains.api.mdx) instead to check existence of an instance rpc ExistsDomain(ExistsDomainRequest) returns (ExistsDomainResponse) { option (google.api.http) = { post: "/domains/{domain}/_exists"; @@ -214,10 +244,14 @@ service SystemService { option (zitadel.v1.auth_option) = { permission: "system.domain.read"; }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + deprecated: true; + }; } // Returns the custom domains of an instance - //Checks if a domain exists + // Checks if a domain exists // Deprecated: Use the Admin APIs ListInstanceDomains on the admin API instead rpc ListDomains(ListDomainsRequest) returns (ListDomainsResponse) { option (google.api.http) = { @@ -228,9 +262,15 @@ service SystemService { option (zitadel.v1.auth_option) = { permission: "system.domain.read"; }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + deprecated: true; + }; } // Adds a domain to an instance + // + // Deprecated: Use [AddCustomDomain](apis/resources/instance_service_v2/instance-service-add-custom-domain.api.mdx) instead to add a custom domain to the instance in context rpc AddDomain(AddDomainRequest) returns (AddDomainResponse) { option (google.api.http) = { post: "/instances/{instance_id}/domains"; @@ -240,9 +280,15 @@ service SystemService { option (zitadel.v1.auth_option) = { permission: "system.domain.write"; }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + deprecated: true; + }; } // Removes the domain of an instance + // + // Deprecated: Use [RemoveDomain](apis/resources/instance_service_v2/instance-service-remove-custom-domain.api.mdx) instead to remove a custom domain from the instance in context rpc RemoveDomain(RemoveDomainRequest) returns (RemoveDomainResponse) { option (google.api.http) = { delete: "/instances/{instance_id}/domains/{domain}"; @@ -251,6 +297,10 @@ service SystemService { option (zitadel.v1.auth_option) = { permission: "system.domain.delete"; }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + deprecated: true; + }; } // Sets the primary domain of an instance From 6889d6a1daba1ce6e9696f43a6d4c0fe1b3cb400 Mon Sep 17 00:00:00 2001 From: alfa-alex <76205862+alfa-alex@users.noreply.github.com> Date: Wed, 21 May 2025 12:55:40 +0200 Subject: [PATCH 41/76] feat: add custom org ID to AddOrganizationRequest (#9720) # Which Problems Are Solved - It is not possible to specify a custom organization ID when creating an organization. According to https://github.com/zitadel/zitadel/discussions/9202#discussioncomment-11929464 this is "an inconsistency in the V2 API". # How the Problems Are Solved - Adds the `org_id` as an optional parameter to the `AddOrganizationRequest` in the `v2beta` API. # Additional Changes None. # Additional Context - Discussion [#9202](https://github.com/zitadel/zitadel/discussions/9202) - I was mostly interested in how much work it'd be to add this field. Then after completing this, I thought I'd submit this PR. I won't be angry if you just close this PR with the reasoning "we didn't ask for it". :smile: - Even though I don't think this is a breaking change, I didn't add this to the `v2` API yet (don't know what the process for this is TBH). The changes should be analogous, so if you want me to, just request it. --------- Co-authored-by: Stefan Benz <46600784+stebenz@users.noreply.github.com> --- .../grpc/org/v2/integration_test/org_test.go | 12 ++++ .../org/v2/integration_test/query_test.go | 39 +++++++++++++ internal/api/grpc/org/v2/org.go | 1 + internal/api/grpc/org/v2/org_test.go | 15 +++++ .../org/v2beta/integration_test/org_test.go | 12 ++++ internal/api/grpc/org/v2beta/org.go | 1 + internal/api/grpc/org/v2beta/org_test.go | 15 +++++ internal/command/org.go | 21 ++++++- internal/command/org_test.go | 55 +++++++++++++++++++ internal/integration/client.go | 9 +++ proto/zitadel/org/v2/org_service.proto | 8 +++ proto/zitadel/org/v2beta/org_service.proto | 8 +++ 12 files changed, 193 insertions(+), 3 deletions(-) diff --git a/internal/api/grpc/org/v2/integration_test/org_test.go b/internal/api/grpc/org/v2/integration_test/org_test.go index aa8a718e68..b28bbf5ef2 100644 --- a/internal/api/grpc/org/v2/integration_test/org_test.go +++ b/internal/api/grpc/org/v2/integration_test/org_test.go @@ -81,6 +81,18 @@ func TestServer_AddOrganization(t *testing.T) { }, wantErr: true, }, + { + name: "no admin, custom org ID", + ctx: CTX, + req: &org.AddOrganizationRequest{ + Name: gofakeit.AppName(), + OrgId: gu.Ptr("custom-org-ID"), + }, + want: &org.AddOrganizationResponse{ + OrganizationId: "custom-org-ID", + CreatedAdmins: []*org.AddOrganizationResponse_CreatedAdmin{}, + }, + }, { name: "admin with init with userID passed for Human admin", ctx: CTX, diff --git a/internal/api/grpc/org/v2/integration_test/query_test.go b/internal/api/grpc/org/v2/integration_test/query_test.go index cb7576455c..b2f27dbe62 100644 --- a/internal/api/grpc/org/v2/integration_test/query_test.go +++ b/internal/api/grpc/org/v2/integration_test/query_test.go @@ -38,6 +38,16 @@ func createOrganization(ctx context.Context, name string) orgAttr { } } +func createOrganizationWithCustomOrgID(ctx context.Context, name string, orgID string) orgAttr { + orgResp := Instance.CreateOrganizationWithCustomOrgID(ctx, name, orgID) + orgResp.Details.CreationDate = orgResp.Details.ChangeDate + return orgAttr{ + ID: orgResp.GetOrganizationId(), + Name: name, + Details: orgResp.GetDetails(), + } +} + func TestServer_ListOrganizations(t *testing.T) { type args struct { ctx context.Context @@ -163,6 +173,35 @@ func TestServer_ListOrganizations(t *testing.T) { }, }, }, + { + name: "list org by custom id, ok", + args: args{ + CTX, + &org.ListOrganizationsRequest{}, + func(ctx context.Context, request *org.ListOrganizationsRequest) ([]orgAttr, error) { + orgs := make([]orgAttr, 1) + name := fmt.Sprintf("ListOrgs-%s", gofakeit.AppName()) + orgID := gofakeit.Company() + orgs[0] = createOrganizationWithCustomOrgID(ctx, name, orgID) + request.Queries = []*org.SearchQuery{ + OrganizationIdQuery(orgID), + } + return orgs, nil + }, + }, + want: &org.ListOrganizationsResponse{ + Details: &object.ListDetails{ + TotalResult: 1, + Timestamp: timestamppb.Now(), + }, + SortingColumn: 0, + Result: []*org.Organization{ + { + State: org.OrganizationState_ORGANIZATION_STATE_ACTIVE, + }, + }, + }, + }, { name: "list org by name, ok", args: args{ diff --git a/internal/api/grpc/org/v2/org.go b/internal/api/grpc/org/v2/org.go index 5f21f7403e..bbc3caca85 100644 --- a/internal/api/grpc/org/v2/org.go +++ b/internal/api/grpc/org/v2/org.go @@ -31,6 +31,7 @@ func addOrganizationRequestToCommand(request *org.AddOrganizationRequest) (*comm Name: request.GetName(), CustomDomain: "", Admins: admins, + OrgID: request.GetOrgId(), }, nil } diff --git a/internal/api/grpc/org/v2/org_test.go b/internal/api/grpc/org/v2/org_test.go index b384f858de..7ae252a209 100644 --- a/internal/api/grpc/org/v2/org_test.go +++ b/internal/api/grpc/org/v2/org_test.go @@ -38,6 +38,21 @@ func Test_addOrganizationRequestToCommand(t *testing.T) { }, wantErr: zerrors.ThrowUnimplementedf(nil, "ORGv2-SD2r1", "userType oneOf %T in method AddOrganization not implemented", nil), }, + { + name: "custom org ID", + args: args{ + request: &org.AddOrganizationRequest{ + Name: "custom org ID", + OrgId: gu.Ptr("org-ID"), + }, + }, + want: &command.OrgSetup{ + Name: "custom org ID", + CustomDomain: "", + Admins: []*command.OrgSetupAdmin{}, + OrgID: "org-ID", + }, + }, { name: "user ID", args: args{ diff --git a/internal/api/grpc/org/v2beta/integration_test/org_test.go b/internal/api/grpc/org/v2beta/integration_test/org_test.go index 5998b17a71..a2b2bf6047 100644 --- a/internal/api/grpc/org/v2beta/integration_test/org_test.go +++ b/internal/api/grpc/org/v2beta/integration_test/org_test.go @@ -79,6 +79,18 @@ func TestServer_AddOrganization(t *testing.T) { }, wantErr: true, }, + { + name: "no admin, custom org ID", + ctx: CTX, + req: &org.AddOrganizationRequest{ + Name: gofakeit.AppName(), + OrgId: gu.Ptr("custom-org-ID"), + }, + want: &org.AddOrganizationResponse{ + OrganizationId: "custom-org-ID", + CreatedAdmins: []*org.AddOrganizationResponse_CreatedAdmin{}, + }, + }, { name: "admin with init", ctx: CTX, diff --git a/internal/api/grpc/org/v2beta/org.go b/internal/api/grpc/org/v2beta/org.go index ab2da2b766..39730f827e 100644 --- a/internal/api/grpc/org/v2beta/org.go +++ b/internal/api/grpc/org/v2beta/org.go @@ -31,6 +31,7 @@ func addOrganizationRequestToCommand(request *org.AddOrganizationRequest) (*comm Name: request.GetName(), CustomDomain: "", Admins: admins, + OrgID: request.GetOrgId(), }, nil } diff --git a/internal/api/grpc/org/v2beta/org_test.go b/internal/api/grpc/org/v2beta/org_test.go index 5024b59c1d..57ed05dfb2 100644 --- a/internal/api/grpc/org/v2beta/org_test.go +++ b/internal/api/grpc/org/v2beta/org_test.go @@ -39,6 +39,21 @@ func Test_addOrganizationRequestToCommand(t *testing.T) { }, wantErr: zerrors.ThrowUnimplementedf(nil, "ORGv2-SD2r1", "userType oneOf %T in method AddOrganization not implemented", nil), }, + { + name: "custom org ID", + args: args{ + request: &org.AddOrganizationRequest{ + Name: "custom org ID", + OrgId: gu.Ptr("org-ID"), + }, + }, + want: &command.OrgSetup{ + Name: "custom org ID", + CustomDomain: "", + Admins: []*command.OrgSetupAdmin{}, + OrgID: "org-ID", + }, + }, { name: "user ID", args: args{ diff --git a/internal/command/org.go b/internal/command/org.go index a018a90c82..9874410a5f 100644 --- a/internal/command/org.go +++ b/internal/command/org.go @@ -31,6 +31,7 @@ type OrgSetup struct { Name string CustomDomain string Admins []*OrgSetupAdmin + OrgID string } // OrgSetupAdmin describes a user to be created (Human / Machine) or an existing (ID) to be used for an org setup. @@ -64,6 +65,13 @@ type CreatedOrgAdmin struct { MachineKey *MachineKey } +func (o *OrgSetup) Validate() (err error) { + if o.OrgID != "" && strings.TrimSpace(o.OrgID) == "" { + return zerrors.ThrowInvalidArgument(nil, "ORG-4ABd3", "Errors.Invalid.Argument") + } + return nil +} + func (c *Commands) setUpOrgWithIDs(ctx context.Context, o *OrgSetup, orgID string, allowInitialMail bool, userIDs ...string) (_ *CreatedOrg, err error) { cmds := c.newOrgSetupCommands(ctx, orgID, o) for _, admin := range o.Admins { @@ -233,12 +241,19 @@ func (c *orgSetupCommands) createdMachineAdmin(admin *OrgSetupAdmin) *CreatedOrg } func (c *Commands) SetUpOrg(ctx context.Context, o *OrgSetup, allowInitialMail bool, userIDs ...string) (*CreatedOrg, error) { - orgID, err := c.idGenerator.Next() - if err != nil { + if err := o.Validate(); err != nil { return nil, err } - return c.setUpOrgWithIDs(ctx, o, orgID, allowInitialMail, userIDs...) + if o.OrgID == "" { + var err error + o.OrgID, err = c.idGenerator.Next() + if err != nil { + return nil, err + } + } + + return c.setUpOrgWithIDs(ctx, o, o.OrgID, allowInitialMail, userIDs...) } // AddOrgCommand defines the commands to create a new org, diff --git a/internal/command/org_test.go b/internal/command/org_test.go index 4ec85d61e1..4b6fd7afe5 100644 --- a/internal/command/org_test.go +++ b/internal/command/org_test.go @@ -1344,6 +1344,22 @@ func TestCommandSide_SetUpOrg(t *testing.T) { err: zerrors.ThrowInvalidArgument(nil, "ORG-mruNY", "Errors.Invalid.Argument"), }, }, + { + name: "org id empty, error", + fields: fields{ + eventstore: expectEventstore(), + }, + args: args{ + ctx: http_util.WithRequestedHost(context.Background(), "iam-domain"), + setupOrg: &OrgSetup{ + Name: "Org", + OrgID: " ", + }, + }, + res: res{ + err: zerrors.ThrowInvalidArgument(nil, "ORG-4ABd3", "Errors.Invalid.Argument"), + }, + }, { name: "userID not existing, error", fields: fields{ @@ -1523,6 +1539,45 @@ func TestCommandSide_SetUpOrg(t *testing.T) { }, }, }, + { + name: "no human added, custom org ID", + fields: fields{ + eventstore: expectEventstore( + expectPush( + eventFromEventPusher(org.NewOrgAddedEvent(context.Background(), + &org.NewAggregate("custom-org-ID").Aggregate, + "Org", + )), + eventFromEventPusher(org.NewDomainAddedEvent(context.Background(), + &org.NewAggregate("custom-org-ID").Aggregate, "org.iam-domain", + )), + eventFromEventPusher(org.NewDomainVerifiedEvent(context.Background(), + &org.NewAggregate("custom-org-ID").Aggregate, + "org.iam-domain", + )), + eventFromEventPusher(org.NewDomainPrimarySetEvent(context.Background(), + &org.NewAggregate("custom-org-ID").Aggregate, + "org.iam-domain", + )), + ), + ), + }, + args: args{ + ctx: http_util.WithRequestedHost(context.Background(), "iam-domain"), + setupOrg: &OrgSetup{ + Name: "Org", + OrgID: "custom-org-ID", + }, + }, + res: res{ + createdOrg: &CreatedOrg{ + ObjectDetails: &domain.ObjectDetails{ + ResourceOwner: "custom-org-ID", + }, + CreatedAdmins: []*CreatedOrgAdmin{}, + }, + }, + }, { name: "existing human added", fields: fields{ diff --git a/internal/integration/client.go b/internal/integration/client.go index bd9775a28a..61645cc067 100644 --- a/internal/integration/client.go +++ b/internal/integration/client.go @@ -293,6 +293,15 @@ func SetOrgID(ctx context.Context, orgID string) context.Context { return metadata.NewOutgoingContext(ctx, md) } +func (i *Instance) CreateOrganizationWithCustomOrgID(ctx context.Context, name, orgID string) *org.AddOrganizationResponse { + resp, err := i.Client.OrgV2.AddOrganization(ctx, &org.AddOrganizationRequest{ + Name: name, + OrgId: gu.Ptr(orgID), + }) + logging.OnError(err).Fatal("create org") + return resp +} + func (i *Instance) CreateOrganizationWithUserID(ctx context.Context, name, userID string) *org.AddOrganizationResponse { resp, err := i.Client.OrgV2.AddOrganization(ctx, &org.AddOrganizationRequest{ Name: name, diff --git a/proto/zitadel/org/v2/org_service.proto b/proto/zitadel/org/v2/org_service.proto index 94ced55146..729350e1f9 100644 --- a/proto/zitadel/org/v2/org_service.proto +++ b/proto/zitadel/org/v2/org_service.proto @@ -197,6 +197,14 @@ message AddOrganizationRequest{ } ]; repeated Admin admins = 2; + // optionally set your own id unique for the organization. + optional string org_id = 3 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + max_length: 200; + example: "\"d654e6ba-70a3-48ef-a95d-37c8d8a7901a\""; + } + ]; } message AddOrganizationResponse{ diff --git a/proto/zitadel/org/v2beta/org_service.proto b/proto/zitadel/org/v2beta/org_service.proto index 90c29ca354..e303b676d7 100644 --- a/proto/zitadel/org/v2beta/org_service.proto +++ b/proto/zitadel/org/v2beta/org_service.proto @@ -160,6 +160,14 @@ message AddOrganizationRequest{ } ]; repeated Admin admins = 2; + // optionally set your own id unique for the organization. + optional string org_id = 3 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + max_length: 200; + example: "\"d654e6ba-70a3-48ef-a95d-37c8d8a7901a\""; + } + ]; } message AddOrganizationResponse{ From 7eb45c6cfd8092d99bd90a318e50a1fbcb017257 Mon Sep 17 00:00:00 2001 From: Stefan Benz <46600784+stebenz@users.noreply.github.com> Date: Wed, 21 May 2025 14:40:47 +0200 Subject: [PATCH 42/76] feat: project v2beta resource API (#9742) # Which Problems Are Solved Resource management of projects and sub-resources was before limited by the context provided by the management API, which would mean you could only manage resources belonging to a specific organization. # How the Problems Are Solved With the addition of a resource-based API, it is now possible to manage projects and sub-resources on the basis of the resources themselves, which means that as long as you have the permission for the resource, you can create, read, update and delete it. - CreateProject to create a project under an organization - UpdateProject to update an existing project - DeleteProject to delete an existing project - DeactivateProject and ActivateProject to change the status of a project - GetProject to query for a specific project with an identifier - ListProject to query for projects and granted projects - CreateProjectGrant to create a project grant with project and granted organization - UpdateProjectGrant to update the roles of a project grant - DeactivateProjectGrant and ActivateProjectGrant to change the status of a project grant - DeleteProjectGrant to delete an existing project grant - ListProjectGrants to query for project grants - AddProjectRole to add a role to an existing project - UpdateProjectRole to change texts of an existing role - RemoveProjectRole to remove an existing role - ListProjectRoles to query for project roles # Additional Changes - Changes to ListProjects, which now contains granted projects as well - Changes to messages as defined in the [API_DESIGN](https://github.com/zitadel/zitadel/blob/main/API_DESIGN.md) - Permission checks for project functionality on query and command side - Added testing to unit tests on command side - Change update endpoints to no error returns if nothing changes in the resource - Changed all integration test utility to the new service - ListProjects now also correctly lists `granted projects` - Permission checks for project grant and project role functionality on query and command side - Change existing pre checks so that they also work resource specific without resourceowner - Added the resourceowner to the grant and role if no resourceowner is provided - Corrected import tests with project grants and roles - Added testing to unit tests on command side - Change update endpoints to no error returns if nothing changes in the resource - Changed all integration test utility to the new service - Corrected some naming in the proto files to adhere to the API_DESIGN # Additional Context Closes #9177 --------- Co-authored-by: Livio Spring --- cmd/start/start.go | 4 + docs/docusaurus.config.js | 8 + docs/sidebars.js | 12 + .../integration_test/execution_target_test.go | 9 +- internal/api/grpc/admin/export.go | 6 +- internal/api/grpc/admin/import.go | 6 +- .../admin/integration_test/import_test.go | 24 + internal/api/grpc/management/project.go | 27 +- .../api/grpc/management/project_converter.go | 51 +- internal/api/grpc/management/project_grant.go | 17 +- .../management/project_grant_converter.go | 16 +- .../oidc/v2/integration_test/oidc_test.go | 35 +- .../oidc/v2beta/integration_test/oidc_test.go | 29 +- .../v2beta/integration/project_grant_test.go | 1292 ++++++++++++ .../v2beta/integration/project_role_test.go | 698 +++++++ .../v2beta/integration/project_test.go | 1013 ++++++++++ .../project/v2beta/integration/query_test.go | 1738 +++++++++++++++++ .../project/v2beta/integration/server_test.go | 63 + internal/api/grpc/project/v2beta/project.go | 161 ++ .../api/grpc/project/v2beta/project_grant.go | 126 ++ .../api/grpc/project/v2beta/project_role.go | 132 ++ internal/api/grpc/project/v2beta/query.go | 427 ++++ internal/api/grpc/project/v2beta/server.go | 60 + .../api/grpc/saml/v2/integration/saml_test.go | 21 +- .../user/v2/integration_test/user_test.go | 4 +- .../user/v2beta/integration_test/user_test.go | 21 +- internal/api/oidc/access_token.go | 2 +- internal/api/oidc/auth_request.go | 6 +- .../api/oidc/integration_test/client_test.go | 10 +- .../api/oidc/integration_test/oidc_test.go | 7 +- .../integration_test/token_device_test.go | 4 +- .../integration_test/token_exchange_test.go | 6 +- .../oidc/integration_test/userinfo_test.go | 4 +- .../integration_test/users_delete_test.go | 6 +- .../eventsourcing/eventstore/auth_request.go | 4 +- .../eventstore/auth_request_test.go | 2 +- internal/command/org.go | 2 +- internal/command/permission_checks.go | 13 + internal/command/project.go | 238 ++- internal/command/project_application_api.go | 4 +- internal/command/project_application_oidc.go | 4 +- internal/command/project_application_saml.go | 2 +- internal/command/project_grant.go | 276 ++- internal/command/project_grant_model.go | 8 +- internal/command/project_grant_test.go | 505 +++-- internal/command/project_model.go | 41 +- internal/command/project_old.go | 30 +- internal/command/project_role.go | 138 +- internal/command/project_role_model.go | 4 + internal/command/project_role_test.go | 247 ++- internal/command/project_test.go | 845 ++++++-- internal/domain/permission.go | 9 + internal/domain/project_grant.go | 13 +- internal/domain/project_role.go | 13 +- internal/integration/client.go | 105 +- internal/integration/oidc.go | 34 +- internal/query/project.go | 372 +++- internal/query/project_grant.go | 88 +- internal/query/project_role.go | 43 +- internal/repository/project/aggregate.go | 6 + internal/repository/project/project.go | 7 +- proto/zitadel/management.proto | 176 +- .../project/v2beta/project_service.proto | 1237 ++++++++++++ proto/zitadel/project/v2beta/query.proto | 347 ++++ 64 files changed, 9821 insertions(+), 1037 deletions(-) create mode 100644 internal/api/grpc/project/v2beta/integration/project_grant_test.go create mode 100644 internal/api/grpc/project/v2beta/integration/project_role_test.go create mode 100644 internal/api/grpc/project/v2beta/integration/project_test.go create mode 100644 internal/api/grpc/project/v2beta/integration/query_test.go create mode 100644 internal/api/grpc/project/v2beta/integration/server_test.go create mode 100644 internal/api/grpc/project/v2beta/project.go create mode 100644 internal/api/grpc/project/v2beta/project_grant.go create mode 100644 internal/api/grpc/project/v2beta/project_role.go create mode 100644 internal/api/grpc/project/v2beta/query.go create mode 100644 internal/api/grpc/project/v2beta/server.go create mode 100644 proto/zitadel/project/v2beta/project_service.proto create mode 100644 proto/zitadel/project/v2beta/query.proto diff --git a/cmd/start/start.go b/cmd/start/start.go index 6f04e8ee82..1d83197062 100644 --- a/cmd/start/start.go +++ b/cmd/start/start.go @@ -46,6 +46,7 @@ import ( oidc_v2beta "github.com/zitadel/zitadel/internal/api/grpc/oidc/v2beta" org_v2 "github.com/zitadel/zitadel/internal/api/grpc/org/v2" org_v2beta "github.com/zitadel/zitadel/internal/api/grpc/org/v2beta" + project_v2beta "github.com/zitadel/zitadel/internal/api/grpc/project/v2beta" "github.com/zitadel/zitadel/internal/api/grpc/resources/debug_events/debug_events" user_v3_alpha "github.com/zitadel/zitadel/internal/api/grpc/resources/user/v3alpha" userschema_v3_alpha "github.com/zitadel/zitadel/internal/api/grpc/resources/userschema/v3alpha" @@ -491,6 +492,9 @@ func startAPIs( if err := apis.RegisterService(ctx, action_v2_beta.CreateServer(config.SystemDefaults, commands, queries, domain.AllActionFunctions, apis.ListGrpcMethods, apis.ListGrpcServices)); err != nil { return nil, err } + if err := apis.RegisterService(ctx, project_v2beta.CreateServer(config.SystemDefaults, commands, queries, permissionCheck)); err != nil { + return nil, err + } if err := apis.RegisterService(ctx, userschema_v3_alpha.CreateServer(config.SystemDefaults, commands, queries)); err != nil { return nil, err } diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js index 6a4429cffe..22df468475 100644 --- a/docs/docusaurus.config.js +++ b/docs/docusaurus.config.js @@ -364,6 +364,14 @@ module.exports = { categoryLinkSource: "auto", }, }, + project_v2beta: { + specPath: ".artifacts/openapi/zitadel/project/v2beta/project_service.swagger.json", + outputDir: "docs/apis/resources/project_service_v2", + sidebarOptions: { + groupPathsBy: "tag", + categoryLinkSource: "auto", + }, + }, instance_v2: { specPath: ".artifacts/openapi/zitadel/instance/v2beta/instance_service.swagger.json", outputDir: "docs/apis/resources/instance_service_v2", diff --git a/docs/sidebars.js b/docs/sidebars.js index b7dc3fd8b8..b7a399ecf1 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -12,6 +12,7 @@ const sidebar_api_feature_service_v2 = require("./docs/apis/resources/feature_se const sidebar_api_org_service_v2 = require("./docs/apis/resources/org_service_v2/sidebar.ts").default const sidebar_api_idp_service_v2 = require("./docs/apis/resources/idp_service_v2/sidebar.ts").default const sidebar_api_actions_v2 = require("./docs/apis/resources/action_service_v2/sidebar.ts").default +const sidebar_api_project_service_v2 = require("./docs/apis/resources/project_service_v2/sidebar.ts").default const sidebar_api_webkey_service_v2 = require("./docs/apis/resources/webkey_service_v2/sidebar.ts").default const sidebar_api_instance_service_v2 = require("./docs/apis/resources/instance_service_v2/sidebar.ts").default @@ -843,6 +844,17 @@ module.exports = { }, { type: "category", + label: "Project (Beta)", + link: { + type: "generated-index", + title: "Project Service API (Beta)", + slug: "/apis/resources/project_service_v2", + description: + "This API is intended to manage projects and subresources for ZITADEL. \n"+ + "\n" + + "This service is in beta state. It can AND will continue breaking until a stable version is released.", + }, + items: sidebar_api_project_service_v2, label: "Instance (Beta)", link: { type: "generated-index", diff --git a/internal/api/grpc/action/v2beta/integration_test/execution_target_test.go b/internal/api/grpc/action/v2beta/integration_test/execution_target_test.go index 0c5018dbb6..9fa568fb8b 100644 --- a/internal/api/grpc/action/v2beta/integration_test/execution_target_test.go +++ b/internal/api/grpc/action/v2beta/integration_test/execution_target_test.go @@ -580,7 +580,7 @@ func TestServer_ExecutionTargetPreUserinfo(t *testing.T) { isolatedIAMCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) ctxLoginClient := instance.WithAuthorization(CTX, integration.UserTypeLogin) - client, err := instance.CreateOIDCImplicitFlowClient(isolatedIAMCtx, redirectURIImplicit, loginV2) + client, err := instance.CreateOIDCImplicitFlowClient(isolatedIAMCtx, t, redirectURIImplicit, loginV2) require.NoError(t, err) type want struct { @@ -893,7 +893,7 @@ func TestServer_ExecutionTargetPreAccessToken(t *testing.T) { isolatedIAMCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) ctxLoginClient := instance.WithAuthorization(CTX, integration.UserTypeLogin) - client, err := instance.CreateOIDCImplicitFlowClient(isolatedIAMCtx, redirectURIImplicit, loginV2) + client, err := instance.CreateOIDCImplicitFlowClient(isolatedIAMCtx, t, redirectURIImplicit, loginV2) require.NoError(t, err) type want struct { @@ -1255,10 +1255,9 @@ func createSAMLSP(t *testing.T, idpMetadata *saml.EntityDescriptor, binding stri } func createSAMLApplication(ctx context.Context, t *testing.T, instance *integration.Instance, idpMetadata *saml.EntityDescriptor, binding string, projectRoleCheck, hasProjectCheck bool) (string, string, *samlsp.Middleware) { - project, err := instance.CreateProjectWithPermissionCheck(ctx, projectRoleCheck, hasProjectCheck) - require.NoError(t, err) + project := instance.CreateProject(ctx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), projectRoleCheck, hasProjectCheck) rootURL, sp := createSAMLSP(t, idpMetadata, binding) - _, err = instance.CreateSAMLClient(ctx, project.GetId(), sp) + _, err := instance.CreateSAMLClient(ctx, project.GetId(), sp) require.NoError(t, err) return project.GetId(), rootURL, sp } diff --git a/internal/api/grpc/admin/export.go b/internal/api/grpc/admin/export.go index 68b6053c2c..2558e5b5fc 100644 --- a/internal/api/grpc/admin/export.go +++ b/internal/api/grpc/admin/export.go @@ -736,7 +736,7 @@ func (s *Server) getProjectsAndApps(ctx context.Context, org string) ([]*v1_pb.D if err != nil { return nil, nil, nil, nil, nil, err } - queriedProjects, err := s.query.SearchProjects(ctx, &query.ProjectSearchQueries{Queries: []query.SearchQuery{projectSearch}}) + queriedProjects, err := s.query.SearchProjects(ctx, &query.ProjectSearchQueries{Queries: []query.SearchQuery{projectSearch}}, nil) if err != nil { return nil, nil, nil, nil, nil, err } @@ -763,7 +763,7 @@ func (s *Server) getProjectsAndApps(ctx context.Context, org string) ([]*v1_pb.D return nil, nil, nil, nil, nil, err } - queriedProjectRoles, err := s.query.SearchProjectRoles(ctx, false, &query.ProjectRoleSearchQueries{Queries: []query.SearchQuery{projectRoleSearch}}) + queriedProjectRoles, err := s.query.SearchProjectRoles(ctx, false, &query.ProjectRoleSearchQueries{Queries: []query.SearchQuery{projectRoleSearch}}, nil) if err != nil { return nil, nil, nil, nil, nil, err } @@ -945,7 +945,7 @@ func (s *Server) getNecessaryProjectGrantsForOrg(ctx context.Context, org string if err != nil { return nil, err } - queriedProjectGrants, err := s.query.SearchProjectGrants(ctx, &query.ProjectGrantSearchQueries{Queries: []query.SearchQuery{projectGrantSearchOrg}}) + queriedProjectGrants, err := s.query.SearchProjectGrants(ctx, &query.ProjectGrantSearchQueries{Queries: []query.SearchQuery{projectGrantSearchOrg}}, nil) if err != nil { return nil, err } diff --git a/internal/api/grpc/admin/import.go b/internal/api/grpc/admin/import.go index 5bbcab27cf..119afe9fc0 100644 --- a/internal/api/grpc/admin/import.go +++ b/internal/api/grpc/admin/import.go @@ -621,7 +621,7 @@ func importProjects(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDa } for _, project := range org.GetProjects() { logging.Debugf("import project: %s", project.GetProjectId()) - _, err := s.command.AddProjectWithID(ctx, management.ProjectCreateToDomain(project.GetProject()), org.GetOrgId(), project.GetProjectId()) + _, err := s.command.AddProject(ctx, management.ProjectCreateToCommand(project.GetProject(), project.GetProjectId(), org.GetOrgId())) if err != nil { *errors = append(*errors, &admin_pb.ImportDataError{Type: "project", Id: project.GetProjectId(), Message: err.Error()}) if isCtxTimeout(ctx) { @@ -761,7 +761,7 @@ func importProjectRoles(ctx context.Context, s *Server, errors *[]*admin_pb.Impo logging.Debugf("import projectroles: %s", role.ProjectId+"_"+role.RoleKey) // TBD: why not command.BulkAddProjectRole? - _, err := s.command.AddProjectRole(ctx, management.AddProjectRoleRequestToDomain(role), org.GetOrgId()) + _, err := s.command.AddProjectRole(ctx, management.AddProjectRoleRequestToCommand(role, org.GetOrgId())) if err != nil { *errors = append(*errors, &admin_pb.ImportDataError{Type: "project_role", Id: role.ProjectId + "_" + role.RoleKey, Message: err.Error()}) if isCtxTimeout(ctx) { @@ -1023,7 +1023,7 @@ func importOrg2(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataEr if org.ProjectGrants != nil { for _, grant := range org.GetProjectGrants() { logging.Debugf("import projectgrant: %s", grant.GetGrantId()+"_"+grant.GetProjectGrant().GetProjectId()+"_"+grant.GetProjectGrant().GetGrantedOrgId()) - _, err := s.command.AddProjectGrantWithID(ctx, management.AddProjectGrantRequestToDomain(grant.GetProjectGrant()), grant.GetGrantId(), org.GetOrgId()) + _, err := s.command.AddProjectGrant(ctx, management.AddProjectGrantRequestToCommand(grant.GetProjectGrant(), grant.GetGrantId(), org.GetOrgId())) if err != nil { *errors = append(*errors, &admin_pb.ImportDataError{Type: "project_grant", Id: org.GetOrgId() + "_" + grant.GetProjectGrant().GetProjectId() + "_" + grant.GetProjectGrant().GetGrantedOrgId(), Message: err.Error()}) if isCtxTimeout(ctx) { diff --git a/internal/api/grpc/admin/integration_test/import_test.go b/internal/api/grpc/admin/integration_test/import_test.go index 7d323e5ab8..4a546bbcf1 100644 --- a/internal/api/grpc/admin/integration_test/import_test.go +++ b/internal/api/grpc/admin/integration_test/import_test.go @@ -259,6 +259,12 @@ func TestServer_ImportData(t *testing.T) { Data: &admin.ImportDataRequest_DataOrgs{ DataOrgs: &admin.ImportDataOrg{ Orgs: []*admin.DataOrg{ + { + OrgId: orgIDs[4], + Org: &management.AddOrgRequest{ + Name: gofakeit.ProductName(), + }, + }, { OrgId: orgIDs[3], Org: &management.AddOrgRequest{ @@ -336,6 +342,9 @@ func TestServer_ImportData(t *testing.T) { }, Success: &admin.ImportDataSuccess{ Orgs: []*admin.ImportDataSuccessOrg{ + { + OrgId: orgIDs[4], + }, { OrgId: orgIDs[3], ProjectIds: projectIDs[2:4], @@ -363,6 +372,12 @@ func TestServer_ImportData(t *testing.T) { Data: &admin.ImportDataRequest_DataOrgs{ DataOrgs: &admin.ImportDataOrg{ Orgs: []*admin.DataOrg{ + { + OrgId: orgIDs[6], + Org: &management.AddOrgRequest{ + Name: gofakeit.ProductName(), + }, + }, { OrgId: orgIDs[5], Org: &management.AddOrgRequest{ @@ -383,6 +398,11 @@ func TestServer_ImportData(t *testing.T) { RoleKey: "role1", DisplayName: "role1", }, + { + ProjectId: projectIDs[4], + RoleKey: "role2", + DisplayName: "role2", + }, }, HumanUsers: []*v1.DataHumanUser{ { @@ -442,11 +462,15 @@ func TestServer_ImportData(t *testing.T) { }, Success: &admin.ImportDataSuccess{ Orgs: []*admin.ImportDataSuccessOrg{ + { + OrgId: orgIDs[6], + }, { OrgId: orgIDs[5], ProjectIds: projectIDs[4:5], ProjectRoles: []string{ projectIDs[4] + "_role1", + projectIDs[4] + "_role2", }, HumanUserIds: userIDs[2:3], ProjectGrants: []*admin.ImportDataSuccessProjectGrant{ diff --git a/internal/api/grpc/management/project.go b/internal/api/grpc/management/project.go index 52b6b10e9a..f3af8dbf86 100644 --- a/internal/api/grpc/management/project.go +++ b/internal/api/grpc/management/project.go @@ -47,7 +47,7 @@ func (s *Server) ListProjects(ctx context.Context, req *mgmt_pb.ListProjectsRequ if err != nil { return nil, err } - projects, err := s.query.SearchProjects(ctx, queries) + projects, err := s.query.SearchProjects(ctx, queries, nil) if err != nil { return nil, err } @@ -109,7 +109,7 @@ func (s *Server) ListGrantedProjects(ctx context.Context, req *mgmt_pb.ListGrant if err != nil { return nil, err } - projects, err := s.query.SearchProjectGrants(ctx, queries) + projects, err := s.query.SearchProjectGrants(ctx, queries, nil) if err != nil { return nil, err } @@ -175,25 +175,26 @@ func (s *Server) ListProjectChanges(ctx context.Context, req *mgmt_pb.ListProjec } func (s *Server) AddProject(ctx context.Context, req *mgmt_pb.AddProjectRequest) (*mgmt_pb.AddProjectResponse, error) { - project, err := s.command.AddProject(ctx, ProjectCreateToDomain(req), authz.GetCtxData(ctx).OrgID) + add := ProjectCreateToCommand(req, "", authz.GetCtxData(ctx).OrgID) + project, err := s.command.AddProject(ctx, add) if err != nil { return nil, err } return &mgmt_pb.AddProjectResponse{ - Id: project.AggregateID, - Details: object_grpc.AddToDetailsPb(project.Sequence, project.ChangeDate, project.ResourceOwner), + Id: add.AggregateID, + Details: object_grpc.AddToDetailsPb(project.Sequence, project.EventDate, project.ResourceOwner), }, nil } func (s *Server) UpdateProject(ctx context.Context, req *mgmt_pb.UpdateProjectRequest) (*mgmt_pb.UpdateProjectResponse, error) { - project, err := s.command.ChangeProject(ctx, ProjectUpdateToDomain(req), authz.GetCtxData(ctx).OrgID) + project, err := s.command.ChangeProject(ctx, ProjectUpdateToCommand(req, authz.GetCtxData(ctx).OrgID)) if err != nil { return nil, err } return &mgmt_pb.UpdateProjectResponse{ Details: object_grpc.ChangeToDetailsPb( project.Sequence, - project.ChangeDate, + project.EventDate, project.ResourceOwner, ), }, nil @@ -252,7 +253,7 @@ func (s *Server) ListProjectRoles(ctx context.Context, req *mgmt_pb.ListProjectR if err != nil { return nil, err } - roles, err := s.query.SearchProjectRoles(ctx, true, queries) + roles, err := s.query.SearchProjectRoles(ctx, true, queries, nil) if err != nil { return nil, err } @@ -263,21 +264,21 @@ func (s *Server) ListProjectRoles(ctx context.Context, req *mgmt_pb.ListProjectR } func (s *Server) AddProjectRole(ctx context.Context, req *mgmt_pb.AddProjectRoleRequest) (*mgmt_pb.AddProjectRoleResponse, error) { - role, err := s.command.AddProjectRole(ctx, AddProjectRoleRequestToDomain(req), authz.GetCtxData(ctx).OrgID) + role, err := s.command.AddProjectRole(ctx, AddProjectRoleRequestToCommand(req, authz.GetCtxData(ctx).OrgID)) if err != nil { return nil, err } return &mgmt_pb.AddProjectRoleResponse{ Details: object_grpc.AddToDetailsPb( role.Sequence, - role.ChangeDate, + role.EventDate, role.ResourceOwner, ), }, nil } func (s *Server) BulkAddProjectRoles(ctx context.Context, req *mgmt_pb.BulkAddProjectRolesRequest) (*mgmt_pb.BulkAddProjectRolesResponse, error) { - details, err := s.command.BulkAddProjectRole(ctx, req.ProjectId, authz.GetCtxData(ctx).OrgID, BulkAddProjectRolesRequestToDomain(req)) + details, err := s.command.BulkAddProjectRole(ctx, req.ProjectId, authz.GetCtxData(ctx).OrgID, BulkAddProjectRolesRequestToCommand(req, authz.GetCtxData(ctx).OrgID)) if err != nil { return nil, err } @@ -287,14 +288,14 @@ func (s *Server) BulkAddProjectRoles(ctx context.Context, req *mgmt_pb.BulkAddPr } func (s *Server) UpdateProjectRole(ctx context.Context, req *mgmt_pb.UpdateProjectRoleRequest) (*mgmt_pb.UpdateProjectRoleResponse, error) { - role, err := s.command.ChangeProjectRole(ctx, UpdateProjectRoleRequestToDomain(req), authz.GetCtxData(ctx).OrgID) + role, err := s.command.ChangeProjectRole(ctx, UpdateProjectRoleRequestToCommand(req, authz.GetCtxData(ctx).OrgID)) if err != nil { return nil, err } return &mgmt_pb.UpdateProjectRoleResponse{ Details: object_grpc.ChangeToDetailsPb( role.Sequence, - role.ChangeDate, + role.EventDate, role.ResourceOwner, ), }, nil diff --git a/internal/api/grpc/management/project_converter.go b/internal/api/grpc/management/project_converter.go index 64243ba258..83a8246feb 100644 --- a/internal/api/grpc/management/project_converter.go +++ b/internal/api/grpc/management/project_converter.go @@ -3,10 +3,13 @@ package management import ( "context" + "github.com/muhlemmer/gu" + "github.com/zitadel/zitadel/internal/api/authz" member_grpc "github.com/zitadel/zitadel/internal/api/grpc/member" "github.com/zitadel/zitadel/internal/api/grpc/object" proj_grpc "github.com/zitadel/zitadel/internal/api/grpc/project" + "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore/v1/models" "github.com/zitadel/zitadel/internal/query" @@ -14,8 +17,12 @@ import ( proj_pb "github.com/zitadel/zitadel/pkg/grpc/project" ) -func ProjectCreateToDomain(req *mgmt_pb.AddProjectRequest) *domain.Project { - return &domain.Project{ +func ProjectCreateToCommand(req *mgmt_pb.AddProjectRequest, projectID string, resourceOwner string) *command.AddProject { + return &command.AddProject{ + ObjectRoot: models.ObjectRoot{ + AggregateID: projectID, + ResourceOwner: resourceOwner, + }, Name: req.Name, ProjectRoleAssertion: req.ProjectRoleAssertion, ProjectRoleCheck: req.ProjectRoleCheck, @@ -24,16 +31,17 @@ func ProjectCreateToDomain(req *mgmt_pb.AddProjectRequest) *domain.Project { } } -func ProjectUpdateToDomain(req *mgmt_pb.UpdateProjectRequest) *domain.Project { - return &domain.Project{ +func ProjectUpdateToCommand(req *mgmt_pb.UpdateProjectRequest, resourceOwner string) *command.ChangeProject { + return &command.ChangeProject{ ObjectRoot: models.ObjectRoot{ - AggregateID: req.Id, + AggregateID: req.Id, + ResourceOwner: resourceOwner, }, - Name: req.Name, - ProjectRoleAssertion: req.ProjectRoleAssertion, - ProjectRoleCheck: req.ProjectRoleCheck, - HasProjectCheck: req.HasProjectCheck, - PrivateLabelingSetting: privateLabelingSettingToDomain(req.PrivateLabelingSetting), + Name: gu.Ptr(req.Name), + ProjectRoleAssertion: gu.Ptr(req.ProjectRoleAssertion), + ProjectRoleCheck: gu.Ptr(req.ProjectRoleCheck), + HasProjectCheck: gu.Ptr(req.HasProjectCheck), + PrivateLabelingSetting: gu.Ptr(privateLabelingSettingToDomain(req.PrivateLabelingSetting)), } } @@ -48,10 +56,11 @@ func privateLabelingSettingToDomain(setting proj_pb.PrivateLabelingSetting) doma } } -func AddProjectRoleRequestToDomain(req *mgmt_pb.AddProjectRoleRequest) *domain.ProjectRole { - return &domain.ProjectRole{ +func AddProjectRoleRequestToCommand(req *mgmt_pb.AddProjectRoleRequest, resourceOwner string) *command.AddProjectRole { + return &command.AddProjectRole{ ObjectRoot: models.ObjectRoot{ - AggregateID: req.ProjectId, + AggregateID: req.ProjectId, + ResourceOwner: resourceOwner, }, Key: req.RoleKey, DisplayName: req.DisplayName, @@ -59,12 +68,13 @@ func AddProjectRoleRequestToDomain(req *mgmt_pb.AddProjectRoleRequest) *domain.P } } -func BulkAddProjectRolesRequestToDomain(req *mgmt_pb.BulkAddProjectRolesRequest) []*domain.ProjectRole { - roles := make([]*domain.ProjectRole, len(req.Roles)) +func BulkAddProjectRolesRequestToCommand(req *mgmt_pb.BulkAddProjectRolesRequest, resourceOwner string) []*command.AddProjectRole { + roles := make([]*command.AddProjectRole, len(req.Roles)) for i, role := range req.Roles { - roles[i] = &domain.ProjectRole{ + roles[i] = &command.AddProjectRole{ ObjectRoot: models.ObjectRoot{ - AggregateID: req.ProjectId, + AggregateID: req.ProjectId, + ResourceOwner: resourceOwner, }, Key: role.Key, DisplayName: role.DisplayName, @@ -74,10 +84,11 @@ func BulkAddProjectRolesRequestToDomain(req *mgmt_pb.BulkAddProjectRolesRequest) return roles } -func UpdateProjectRoleRequestToDomain(req *mgmt_pb.UpdateProjectRoleRequest) *domain.ProjectRole { - return &domain.ProjectRole{ +func UpdateProjectRoleRequestToCommand(req *mgmt_pb.UpdateProjectRoleRequest, resourceOwner string) *command.ChangeProjectRole { + return &command.ChangeProjectRole{ ObjectRoot: models.ObjectRoot{ - AggregateID: req.ProjectId, + AggregateID: req.ProjectId, + ResourceOwner: resourceOwner, }, Key: req.RoleKey, DisplayName: req.DisplayName, diff --git a/internal/api/grpc/management/project_grant.go b/internal/api/grpc/management/project_grant.go index cea8e929a4..e9313c1327 100644 --- a/internal/api/grpc/management/project_grant.go +++ b/internal/api/grpc/management/project_grant.go @@ -31,7 +31,7 @@ func (s *Server) ListProjectGrants(ctx context.Context, req *mgmt_pb.ListProject if err != nil { return nil, err } - grants, err := s.query.SearchProjectGrants(ctx, queries) + grants, err := s.query.SearchProjectGrants(ctx, queries, nil) if err != nil { return nil, err } @@ -54,7 +54,7 @@ func (s *Server) ListAllProjectGrants(ctx context.Context, req *mgmt_pb.ListAllP if err != nil { return nil, err } - grants, err := s.query.SearchProjectGrants(ctx, queries) + grants, err := s.query.SearchProjectGrants(ctx, queries, nil) if err != nil { return nil, err } @@ -65,16 +65,17 @@ func (s *Server) ListAllProjectGrants(ctx context.Context, req *mgmt_pb.ListAllP } func (s *Server) AddProjectGrant(ctx context.Context, req *mgmt_pb.AddProjectGrantRequest) (*mgmt_pb.AddProjectGrantResponse, error) { - grant, err := s.command.AddProjectGrant(ctx, AddProjectGrantRequestToDomain(req), authz.GetCtxData(ctx).OrgID) + grant := AddProjectGrantRequestToCommand(req, "", authz.GetCtxData(ctx).OrgID) + details, err := s.command.AddProjectGrant(ctx, grant) if err != nil { return nil, err } return &mgmt_pb.AddProjectGrantResponse{ GrantId: grant.GrantID, Details: object_grpc.AddToDetailsPb( - grant.Sequence, - grant.ChangeDate, - grant.ResourceOwner, + details.Sequence, + details.EventDate, + details.ResourceOwner, ), }, nil } @@ -94,14 +95,14 @@ func (s *Server) UpdateProjectGrant(ctx context.Context, req *mgmt_pb.UpdateProj if err != nil { return nil, err } - grant, err := s.command.ChangeProjectGrant(ctx, UpdateProjectGrantRequestToDomain(req), authz.GetCtxData(ctx).OrgID, userGrantsToIDs(grants.UserGrants)...) + grant, err := s.command.ChangeProjectGrant(ctx, UpdateProjectGrantRequestToCommand(req, authz.GetCtxData(ctx).OrgID), userGrantsToIDs(grants.UserGrants)...) if err != nil { return nil, err } return &mgmt_pb.UpdateProjectGrantResponse{ Details: object_grpc.ChangeToDetailsPb( grant.Sequence, - grant.ChangeDate, + grant.EventDate, grant.ResourceOwner, ), }, nil diff --git a/internal/api/grpc/management/project_grant_converter.go b/internal/api/grpc/management/project_grant_converter.go index de7d1de041..04bc35301f 100644 --- a/internal/api/grpc/management/project_grant_converter.go +++ b/internal/api/grpc/management/project_grant_converter.go @@ -6,6 +6,7 @@ import ( "github.com/zitadel/zitadel/internal/api/authz" member_grpc "github.com/zitadel/zitadel/internal/api/grpc/member" "github.com/zitadel/zitadel/internal/api/grpc/object" + "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore/v1/models" "github.com/zitadel/zitadel/internal/query" @@ -100,20 +101,23 @@ func AllProjectGrantQueryToModel(apiQuery *proj_pb.AllProjectGrantQuery) (query. return nil, zerrors.ThrowInvalidArgument(nil, "PROJECT-M099f", "List.Query.Invalid") } } -func AddProjectGrantRequestToDomain(req *mgmt_pb.AddProjectGrantRequest) *domain.ProjectGrant { - return &domain.ProjectGrant{ +func AddProjectGrantRequestToCommand(req *mgmt_pb.AddProjectGrantRequest, grantID string, resourceOwner string) *command.AddProjectGrant { + return &command.AddProjectGrant{ ObjectRoot: models.ObjectRoot{ - AggregateID: req.ProjectId, + AggregateID: req.ProjectId, + ResourceOwner: resourceOwner, }, + GrantID: grantID, GrantedOrgID: req.GrantedOrgId, RoleKeys: req.RoleKeys, } } -func UpdateProjectGrantRequestToDomain(req *mgmt_pb.UpdateProjectGrantRequest) *domain.ProjectGrant { - return &domain.ProjectGrant{ +func UpdateProjectGrantRequestToCommand(req *mgmt_pb.UpdateProjectGrantRequest, resourceOwner string) *command.ChangeProjectGrant { + return &command.ChangeProjectGrant{ ObjectRoot: models.ObjectRoot{ - AggregateID: req.ProjectId, + AggregateID: req.ProjectId, + ResourceOwner: resourceOwner, }, GrantID: req.GrantId, RoleKeys: req.RoleKeys, diff --git a/internal/api/grpc/oidc/v2/integration_test/oidc_test.go b/internal/api/grpc/oidc/v2/integration_test/oidc_test.go index 64334bd8b1..187dc922fc 100644 --- a/internal/api/grpc/oidc/v2/integration_test/oidc_test.go +++ b/internal/api/grpc/oidc/v2/integration_test/oidc_test.go @@ -24,8 +24,7 @@ import ( ) func TestServer_GetAuthRequest(t *testing.T) { - project, err := Instance.CreateProject(CTX) - require.NoError(t, err) + project := Instance.CreateProject(CTX, t, "", gofakeit.AppName(), false, false) client, err := Instance.CreateOIDCNativeClient(CTX, redirectURI, logoutRedirectURI, project.GetId(), false) require.NoError(t, err) @@ -98,8 +97,7 @@ func TestServer_GetAuthRequest(t *testing.T) { } func TestServer_CreateCallback(t *testing.T) { - project, err := Instance.CreateProject(CTX) - require.NoError(t, err) + project := Instance.CreateProject(CTX, t, "", gofakeit.AppName(), false, false) client, err := Instance.CreateOIDCNativeClient(CTX, redirectURI, logoutRedirectURI, project.GetId(), false) require.NoError(t, err) clientV2, err := Instance.CreateOIDCClientLoginVersion(CTX, redirectURI, logoutRedirectURI, project.GetId(), app.OIDCAppType_OIDC_APP_TYPE_NATIVE, app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_NONE, false, loginV2) @@ -288,7 +286,7 @@ func TestServer_CreateCallback(t *testing.T) { ctx: CTX, req: &oidc_pb.CreateCallbackRequest{ AuthRequestId: func() string { - client, err := Instance.CreateOIDCImplicitFlowClient(CTX, redirectURIImplicit, nil) + client, err := Instance.CreateOIDCImplicitFlowClient(CTX, t, redirectURIImplicit, nil) require.NoError(t, err) authRequestID, err := Instance.CreateOIDCAuthRequestImplicit(CTX, client.GetClientId(), Instance.Users.Get(integration.UserTypeOrgOwner).ID, redirectURIImplicit) require.NoError(t, err) @@ -315,7 +313,7 @@ func TestServer_CreateCallback(t *testing.T) { ctx: CTXLoginClient, req: &oidc_pb.CreateCallbackRequest{ AuthRequestId: func() string { - clientV2, err := Instance.CreateOIDCImplicitFlowClient(CTX, redirectURIImplicit, loginV2) + clientV2, err := Instance.CreateOIDCImplicitFlowClient(CTX, t, redirectURIImplicit, loginV2) require.NoError(t, err) authRequestID, err := Instance.CreateOIDCAuthRequestImplicitWithoutLoginClientHeader(CTX, clientV2.GetClientId(), redirectURIImplicit) require.NoError(t, err) @@ -371,7 +369,7 @@ func TestServer_CreateCallback_Permission(t *testing.T) { projectID2, _ := createOIDCApplication(ctx, t, true, true) orgResp := Instance.CreateOrganization(ctx, "oidc-permission-"+gofakeit.AppName(), gofakeit.Email()) - Instance.CreateProjectGrant(ctx, projectID2, orgResp.GetOrganizationId()) + Instance.CreateProjectGrant(ctx, t, projectID2, orgResp.GetOrganizationId()) user := Instance.CreateHumanUserVerified(ctx, orgResp.GetOrganizationId(), gofakeit.Email(), gofakeit.Phone()) Instance.CreateProjectUserGrant(t, ctx, projectID, user.GetUserId()) @@ -386,7 +384,7 @@ func TestServer_CreateCallback_Permission(t *testing.T) { projectID, clientID := createOIDCApplication(ctx, t, true, true) orgResp := Instance.CreateOrganization(ctx, "oidc-permission-"+gofakeit.AppName(), gofakeit.Email()) - Instance.CreateProjectGrant(ctx, projectID, orgResp.GetOrganizationId()) + Instance.CreateProjectGrant(ctx, t, projectID, orgResp.GetOrganizationId()) user := Instance.CreateHumanUserVerified(ctx, orgResp.GetOrganizationId(), gofakeit.Email(), gofakeit.Phone()) Instance.CreateProjectUserGrant(t, ctx, projectID, user.GetUserId()) @@ -407,9 +405,9 @@ func TestServer_CreateCallback_Permission(t *testing.T) { projectID, clientID := createOIDCApplication(ctx, t, true, true) orgResp := Instance.CreateOrganization(ctx, "oidc-permission-"+gofakeit.AppName(), gofakeit.Email()) - projectGrantResp := Instance.CreateProjectGrant(ctx, projectID, orgResp.GetOrganizationId()) + Instance.CreateProjectGrant(ctx, t, projectID, orgResp.GetOrganizationId()) user := Instance.CreateHumanUserVerified(ctx, orgResp.GetOrganizationId(), gofakeit.Email(), gofakeit.Phone()) - Instance.CreateProjectGrantUserGrant(ctx, orgResp.GetOrganizationId(), projectID, projectGrantResp.GetGrantId(), user.GetUserId()) + Instance.CreateProjectGrantUserGrant(ctx, orgResp.GetOrganizationId(), projectID, orgResp.GetOrganizationId(), user.GetUserId()) return createSessionAndAuthRequestForCallback(ctx, t, clientID, Instance.Users.Get(integration.UserTypeOrgOwner).ID, user.GetUserId()) }, @@ -544,9 +542,9 @@ func TestServer_CreateCallback_Permission(t *testing.T) { projectID, clientID := createOIDCApplication(ctx, t, true, false) orgResp := Instance.CreateOrganization(ctx, "oidc-permission-"+gofakeit.AppName(), gofakeit.Email()) - projectGrantResp := Instance.CreateProjectGrant(ctx, projectID, orgResp.GetOrganizationId()) + Instance.CreateProjectGrant(ctx, t, projectID, orgResp.GetOrganizationId()) user := Instance.CreateHumanUserVerified(ctx, orgResp.GetOrganizationId(), gofakeit.Email(), gofakeit.Phone()) - Instance.CreateProjectGrantUserGrant(ctx, orgResp.GetOrganizationId(), projectID, projectGrantResp.GetGrantId(), user.GetUserId()) + Instance.CreateProjectGrantUserGrant(ctx, orgResp.GetOrganizationId(), projectID, orgResp.GetOrganizationId(), user.GetUserId()) return createSessionAndAuthRequestForCallback(ctx, t, clientID, Instance.Users.Get(integration.UserTypeOrgOwner).ID, user.GetUserId()) }, want: &oidc_pb.CreateCallbackResponse{ @@ -564,7 +562,7 @@ func TestServer_CreateCallback_Permission(t *testing.T) { projectID, clientID := createOIDCApplication(ctx, t, true, false) orgResp := Instance.CreateOrganization(ctx, "oidc-permission-"+gofakeit.AppName(), gofakeit.Email()) - Instance.CreateProjectGrant(ctx, projectID, orgResp.GetOrganizationId()) + Instance.CreateProjectGrant(ctx, t, projectID, orgResp.GetOrganizationId()) user := Instance.CreateHumanUserVerified(ctx, orgResp.GetOrganizationId(), gofakeit.Email(), gofakeit.Phone()) return createSessionAndAuthRequestForCallback(ctx, t, clientID, Instance.Users.Get(integration.UserTypeOrgOwner).ID, user.GetUserId()) }, @@ -606,7 +604,7 @@ func TestServer_CreateCallback_Permission(t *testing.T) { projectID, clientID := createOIDCApplication(ctx, t, false, true) orgResp := Instance.CreateOrganization(ctx, "oidc-permission-"+gofakeit.AppName(), gofakeit.Email()) - Instance.CreateProjectGrant(ctx, projectID, orgResp.GetOrganizationId()) + Instance.CreateProjectGrant(ctx, t, projectID, orgResp.GetOrganizationId()) user := Instance.CreateHumanUserVerified(ctx, orgResp.GetOrganizationId(), gofakeit.Email(), gofakeit.Phone()) return createSessionAndAuthRequestForCallback(ctx, t, clientID, Instance.Users.Get(integration.UserTypeOrgOwner).ID, user.GetUserId()) @@ -639,8 +637,7 @@ func TestServer_CreateCallback_Permission(t *testing.T) { } func TestServer_GetDeviceAuthorizationRequest(t *testing.T) { - project, err := Instance.CreateProject(CTX) - require.NoError(t, err) + project := Instance.CreateProject(CTX, t, "", gofakeit.AppName(), false, false) client, err := Instance.CreateOIDCClient(CTX, redirectURI, logoutRedirectURI, project.GetId(), app.OIDCAppType_OIDC_APP_TYPE_NATIVE, app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_NONE, false, app.OIDCGrantType_OIDC_GRANT_TYPE_DEVICE_CODE) require.NoError(t, err) @@ -697,8 +694,7 @@ func TestServer_GetDeviceAuthorizationRequest(t *testing.T) { } func TestServer_AuthorizeOrDenyDeviceAuthorization(t *testing.T) { - project, err := Instance.CreateProject(CTX) - require.NoError(t, err) + project := Instance.CreateProject(CTX, t, "", gofakeit.AppName(), false, false) client, err := Instance.CreateOIDCClient(CTX, redirectURI, logoutRedirectURI, project.GetId(), app.OIDCAppType_OIDC_APP_TYPE_NATIVE, app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_NONE, false, app.OIDCGrantType_OIDC_GRANT_TYPE_DEVICE_CODE) require.NoError(t, err) sessionResp := createSession(t, CTX, Instance.Users[integration.UserTypeOrgOwner].ID) @@ -895,8 +891,7 @@ func createSessionAndAuthRequestForCallback(ctx context.Context, t *testing.T, c } func createOIDCApplication(ctx context.Context, t *testing.T, projectRoleCheck, hasProjectCheck bool) (string, string) { - project, err := Instance.CreateProjectWithPermissionCheck(ctx, projectRoleCheck, hasProjectCheck) - require.NoError(t, err) + project := Instance.CreateProject(ctx, t, "", gofakeit.AppName(), projectRoleCheck, hasProjectCheck) clientV2, err := Instance.CreateOIDCClientLoginVersion(ctx, redirectURI, logoutRedirectURI, project.GetId(), app.OIDCAppType_OIDC_APP_TYPE_NATIVE, app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_NONE, false, loginV2) require.NoError(t, err) return project.GetId(), clientV2.GetClientId() diff --git a/internal/api/grpc/oidc/v2beta/integration_test/oidc_test.go b/internal/api/grpc/oidc/v2beta/integration_test/oidc_test.go index d7d746e2d0..bd02f9e068 100644 --- a/internal/api/grpc/oidc/v2beta/integration_test/oidc_test.go +++ b/internal/api/grpc/oidc/v2beta/integration_test/oidc_test.go @@ -23,8 +23,7 @@ import ( ) func TestServer_GetAuthRequest(t *testing.T) { - project, err := Instance.CreateProject(CTX) - require.NoError(t, err) + project := Instance.CreateProject(CTX, t, "", gofakeit.AppName(), false, false) client, err := Instance.CreateOIDCNativeClient(CTX, redirectURI, logoutRedirectURI, project.GetId(), false) require.NoError(t, err) @@ -97,8 +96,7 @@ func TestServer_GetAuthRequest(t *testing.T) { } func TestServer_CreateCallback(t *testing.T) { - project, err := Instance.CreateProject(CTX) - require.NoError(t, err) + project := Instance.CreateProject(CTX, t, "", gofakeit.AppName(), false, false) client, err := Instance.CreateOIDCNativeClient(CTX, redirectURI, logoutRedirectURI, project.GetId(), false) require.NoError(t, err) clientV2, err := Instance.CreateOIDCClientLoginVersion(CTX, redirectURI, logoutRedirectURI, project.GetId(), app.OIDCAppType_OIDC_APP_TYPE_NATIVE, app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_NONE, false, loginV2) @@ -289,7 +287,7 @@ func TestServer_CreateCallback(t *testing.T) { ctx: CTX, req: &oidc_pb.CreateCallbackRequest{ AuthRequestId: func() string { - client, err := Instance.CreateOIDCImplicitFlowClient(CTX, redirectURIImplicit, nil) + client, err := Instance.CreateOIDCImplicitFlowClient(CTX, t, redirectURIImplicit, nil) require.NoError(t, err) authRequestID, err := Instance.CreateOIDCAuthRequestImplicit(CTX, client.GetClientId(), Instance.Users.Get(integration.UserTypeOrgOwner).ID, redirectURIImplicit) require.NoError(t, err) @@ -316,7 +314,7 @@ func TestServer_CreateCallback(t *testing.T) { ctx: CTXLoginClient, req: &oidc_pb.CreateCallbackRequest{ AuthRequestId: func() string { - clientV2, err := Instance.CreateOIDCImplicitFlowClient(CTX, redirectURIImplicit, loginV2) + clientV2, err := Instance.CreateOIDCImplicitFlowClient(CTX, t, redirectURIImplicit, loginV2) require.NoError(t, err) authRequestID, err := Instance.CreateOIDCAuthRequestImplicitWithoutLoginClientHeader(CTX, clientV2.GetClientId(), redirectURIImplicit) require.NoError(t, err) @@ -372,7 +370,7 @@ func TestServer_CreateCallback_Permission(t *testing.T) { projectID2, _ := createOIDCApplication(ctx, t, true, true) orgResp := Instance.CreateOrganization(ctx, "oidc-permission-"+gofakeit.AppName(), gofakeit.Email()) - Instance.CreateProjectGrant(ctx, projectID2, orgResp.GetOrganizationId()) + Instance.CreateProjectGrant(ctx, t, projectID2, orgResp.GetOrganizationId()) user := Instance.CreateHumanUserVerified(ctx, orgResp.GetOrganizationId(), gofakeit.Email(), gofakeit.Phone()) Instance.CreateProjectUserGrant(t, ctx, projectID, user.GetUserId()) @@ -387,7 +385,7 @@ func TestServer_CreateCallback_Permission(t *testing.T) { projectID, clientID := createOIDCApplication(ctx, t, true, true) orgResp := Instance.CreateOrganization(ctx, "oidc-permission-"+gofakeit.AppName(), gofakeit.Email()) - Instance.CreateProjectGrant(ctx, projectID, orgResp.GetOrganizationId()) + Instance.CreateProjectGrant(ctx, t, projectID, orgResp.GetOrganizationId()) user := Instance.CreateHumanUserVerified(ctx, orgResp.GetOrganizationId(), gofakeit.Email(), gofakeit.Phone()) Instance.CreateProjectUserGrant(t, ctx, projectID, user.GetUserId()) @@ -408,9 +406,9 @@ func TestServer_CreateCallback_Permission(t *testing.T) { projectID, clientID := createOIDCApplication(ctx, t, true, true) orgResp := Instance.CreateOrganization(ctx, "oidc-permission-"+gofakeit.AppName(), gofakeit.Email()) - projectGrantResp := Instance.CreateProjectGrant(ctx, projectID, orgResp.GetOrganizationId()) + Instance.CreateProjectGrant(ctx, t, projectID, orgResp.GetOrganizationId()) user := Instance.CreateHumanUserVerified(ctx, orgResp.GetOrganizationId(), gofakeit.Email(), gofakeit.Phone()) - Instance.CreateProjectGrantUserGrant(ctx, orgResp.GetOrganizationId(), projectID, projectGrantResp.GetGrantId(), user.GetUserId()) + Instance.CreateProjectGrantUserGrant(ctx, orgResp.GetOrganizationId(), projectID, orgResp.GetOrganizationId(), user.GetUserId()) return createSessionAndAuthRequestForCallback(ctx, t, clientID, Instance.Users.Get(integration.UserTypeOrgOwner).ID, user.GetUserId()) }, @@ -545,9 +543,9 @@ func TestServer_CreateCallback_Permission(t *testing.T) { projectID, clientID := createOIDCApplication(ctx, t, true, false) orgResp := Instance.CreateOrganization(ctx, "oidc-permission-"+gofakeit.AppName(), gofakeit.Email()) - projectGrantResp := Instance.CreateProjectGrant(ctx, projectID, orgResp.GetOrganizationId()) + Instance.CreateProjectGrant(ctx, t, projectID, orgResp.GetOrganizationId()) user := Instance.CreateHumanUserVerified(ctx, orgResp.GetOrganizationId(), gofakeit.Email(), gofakeit.Phone()) - Instance.CreateProjectGrantUserGrant(ctx, orgResp.GetOrganizationId(), projectID, projectGrantResp.GetGrantId(), user.GetUserId()) + Instance.CreateProjectGrantUserGrant(ctx, orgResp.GetOrganizationId(), projectID, orgResp.GetOrganizationId(), user.GetUserId()) return createSessionAndAuthRequestForCallback(ctx, t, clientID, Instance.Users.Get(integration.UserTypeOrgOwner).ID, user.GetUserId()) }, want: &oidc_pb.CreateCallbackResponse{ @@ -565,7 +563,7 @@ func TestServer_CreateCallback_Permission(t *testing.T) { projectID, clientID := createOIDCApplication(ctx, t, true, false) orgResp := Instance.CreateOrganization(ctx, "oidc-permission-"+gofakeit.AppName(), gofakeit.Email()) - Instance.CreateProjectGrant(ctx, projectID, orgResp.GetOrganizationId()) + Instance.CreateProjectGrant(ctx, t, projectID, orgResp.GetOrganizationId()) user := Instance.CreateHumanUserVerified(ctx, orgResp.GetOrganizationId(), gofakeit.Email(), gofakeit.Phone()) return createSessionAndAuthRequestForCallback(ctx, t, clientID, Instance.Users.Get(integration.UserTypeOrgOwner).ID, user.GetUserId()) }, @@ -607,7 +605,7 @@ func TestServer_CreateCallback_Permission(t *testing.T) { projectID, clientID := createOIDCApplication(ctx, t, false, true) orgResp := Instance.CreateOrganization(ctx, "oidc-permission-"+gofakeit.AppName(), gofakeit.Email()) - Instance.CreateProjectGrant(ctx, projectID, orgResp.GetOrganizationId()) + Instance.CreateProjectGrant(ctx, t, projectID, orgResp.GetOrganizationId()) user := Instance.CreateHumanUserVerified(ctx, orgResp.GetOrganizationId(), gofakeit.Email(), gofakeit.Phone()) return createSessionAndAuthRequestForCallback(ctx, t, clientID, Instance.Users.Get(integration.UserTypeOrgOwner).ID, user.GetUserId()) @@ -669,8 +667,7 @@ func createSessionAndAuthRequestForCallback(ctx context.Context, t *testing.T, c } func createOIDCApplication(ctx context.Context, t *testing.T, projectRoleCheck, hasProjectCheck bool) (string, string) { - project, err := Instance.CreateProjectWithPermissionCheck(ctx, projectRoleCheck, hasProjectCheck) - require.NoError(t, err) + project := Instance.CreateProject(ctx, t, "", gofakeit.AppName(), projectRoleCheck, hasProjectCheck) clientV2, err := Instance.CreateOIDCClientLoginVersion(ctx, redirectURI, logoutRedirectURI, project.GetId(), app.OIDCAppType_OIDC_APP_TYPE_NATIVE, app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_NONE, false, loginV2) require.NoError(t, err) return project.GetId(), clientV2.GetClientId() diff --git a/internal/api/grpc/project/v2beta/integration/project_grant_test.go b/internal/api/grpc/project/v2beta/integration/project_grant_test.go new file mode 100644 index 0000000000..8500f24d56 --- /dev/null +++ b/internal/api/grpc/project/v2beta/integration/project_grant_test.go @@ -0,0 +1,1292 @@ +//go:build integration + +package project_test + +import ( + "context" + "testing" + "time" + + "github.com/brianvoe/gofakeit/v6" + "github.com/stretchr/testify/assert" + + "github.com/zitadel/zitadel/internal/integration" + project "github.com/zitadel/zitadel/pkg/grpc/project/v2beta" +) + +func TestServer_CreateProjectGrant(t *testing.T) { + iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + + type want struct { + creationDate bool + } + tests := []struct { + name string + ctx context.Context + prepare func(request *project.CreateProjectGrantRequest) + req *project.CreateProjectGrantRequest + want + wantErr bool + }{ + { + name: "empty projectID", + ctx: iamOwnerCtx, + req: &project.CreateProjectGrantRequest{}, + wantErr: true, + }, + { + name: "empty granted organization", + ctx: iamOwnerCtx, + req: &project.CreateProjectGrantRequest{ + ProjectId: "something", + GrantedOrganizationId: "", + }, + wantErr: true, + }, + { + name: "project not existing", + ctx: iamOwnerCtx, + prepare: func(request *project.CreateProjectGrantRequest) { + request.ProjectId = "something" + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + }, + req: &project.CreateProjectGrantRequest{}, + wantErr: true, + }, + { + name: "org not existing", + ctx: iamOwnerCtx, + prepare: func(request *project.CreateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + request.GrantedOrganizationId = "something" + }, + req: &project.CreateProjectGrantRequest{}, + wantErr: true, + }, + { + name: "already existing, error", + ctx: iamOwnerCtx, + prepare: func(request *project.CreateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + }, + req: &project.CreateProjectGrantRequest{}, + wantErr: true, + }, + { + name: "same organization, error", + ctx: iamOwnerCtx, + prepare: func(request *project.CreateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + request.GrantedOrganizationId = orgResp.GetOrganizationId() + }, + req: &project.CreateProjectGrantRequest{}, + wantErr: true, + }, + { + name: "empty, ok", + ctx: iamOwnerCtx, + prepare: func(request *project.CreateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + }, + req: &project.CreateProjectGrantRequest{}, + want: want{ + creationDate: true, + }, + }, + { + name: "with roles, not existing", + ctx: iamOwnerCtx, + prepare: func(request *project.CreateProjectGrantRequest) { + roles := []string{gofakeit.Name(), gofakeit.Name(), gofakeit.Name()} + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + request.RoleKeys = roles + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + }, + req: &project.CreateProjectGrantRequest{}, + wantErr: true, + }, + { + name: "with roles, ok", + ctx: iamOwnerCtx, + prepare: func(request *project.CreateProjectGrantRequest) { + roles := []string{gofakeit.Name(), gofakeit.Name(), gofakeit.Name()} + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + for _, role := range roles { + instance.AddProjectRole(iamOwnerCtx, t, projectResp.GetId(), role, role, "") + } + + request.RoleKeys = roles + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + }, + req: &project.CreateProjectGrantRequest{}, + want: want{ + creationDate: true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.prepare != nil { + tt.prepare(tt.req) + } + + creationDate := time.Now().UTC() + got, err := instance.Client.Projectv2Beta.CreateProjectGrant(tt.ctx, tt.req) + changeDate := time.Now().UTC() + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assertCreateProjectGrantResponse(t, creationDate, changeDate, tt.want.creationDate, got) + }) + } +} + +func TestServer_CreateProjectGrant_Permission(t *testing.T) { + iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + + type want struct { + creationDate bool + } + tests := []struct { + name string + ctx context.Context + prepare func(request *project.CreateProjectGrantRequest) + req *project.CreateProjectGrantRequest + want + wantErr bool + }{ + { + name: "unauthenticated", + ctx: CTX, + prepare: func(request *project.CreateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + grantedOrgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + + request.ProjectId = projectResp.GetId() + request.GrantedOrganizationId = grantedOrgResp.GetOrganizationId() + }, + req: &project.CreateProjectGrantRequest{}, + wantErr: true, + }, + { + name: "no permission", + ctx: instance.WithAuthorization(CTX, integration.UserTypeNoPermission), + prepare: func(request *project.CreateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + grantedOrgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + + request.ProjectId = projectResp.GetId() + request.GrantedOrganizationId = grantedOrgResp.GetOrganizationId() + }, + req: &project.CreateProjectGrantRequest{}, + wantErr: true, + }, + { + name: "organization owner, other org", + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + prepare: func(request *project.CreateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + grantedOrgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + + request.ProjectId = projectResp.GetId() + request.GrantedOrganizationId = grantedOrgResp.GetOrganizationId() + }, + req: &project.CreateProjectGrantRequest{}, + wantErr: true, + }, + { + name: "organization owner, ok", + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + prepare: func(request *project.CreateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + }, + req: &project.CreateProjectGrantRequest{}, + want: want{ + creationDate: true, + }, + }, + { + name: "instance owner", + ctx: iamOwnerCtx, + prepare: func(request *project.CreateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + grantedOrgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + + request.ProjectId = projectResp.GetId() + request.GrantedOrganizationId = grantedOrgResp.GetOrganizationId() + }, + req: &project.CreateProjectGrantRequest{}, + want: want{ + creationDate: true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.prepare != nil { + tt.prepare(tt.req) + } + + creationDate := time.Now().UTC() + got, err := instance.Client.Projectv2Beta.CreateProjectGrant(tt.ctx, tt.req) + changeDate := time.Now().UTC() + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assertCreateProjectGrantResponse(t, creationDate, changeDate, tt.want.creationDate, got) + }) + } +} + +func assertCreateProjectGrantResponse(t *testing.T, creationDate, changeDate time.Time, expectedCreationDate bool, actualResp *project.CreateProjectGrantResponse) { + if expectedCreationDate { + if !changeDate.IsZero() { + assert.WithinRange(t, actualResp.GetCreationDate().AsTime(), creationDate, changeDate) + } else { + assert.WithinRange(t, actualResp.GetCreationDate().AsTime(), creationDate, time.Now().UTC()) + } + } else { + assert.Nil(t, actualResp.CreationDate) + } +} + +func TestServer_UpdateProjectGrant(t *testing.T) { + iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + + type args struct { + ctx context.Context + req *project.UpdateProjectGrantRequest + } + type want struct { + change bool + changeDate bool + } + tests := []struct { + name string + prepare func(request *project.UpdateProjectGrantRequest) + args args + want want + wantErr bool + }{ + { + name: "not existing", + prepare: func(request *project.UpdateProjectGrantRequest) { + request.ProjectId = "notexisting" + request.GrantedOrganizationId = "notexisting" + return + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.UpdateProjectGrantRequest{ + RoleKeys: []string{"notexisting"}, + }, + }, + wantErr: true, + }, + { + name: "no change, ok", + prepare: func(request *project.UpdateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.UpdateProjectGrantRequest{}, + }, + want: want{ + change: false, + changeDate: true, + }, + }, + { + name: "change roles, ok", + prepare: func(request *project.UpdateProjectGrantRequest) { + roles := []string{gofakeit.Animal(), gofakeit.Animal(), gofakeit.Animal()} + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + for _, role := range roles { + instance.AddProjectRole(iamOwnerCtx, t, projectResp.GetId(), role, role, "") + } + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId(), roles...) + request.RoleKeys = roles + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.UpdateProjectGrantRequest{}, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + { + name: "change roles, not existing", + prepare: func(request *project.UpdateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + request.RoleKeys = []string{gofakeit.Animal(), gofakeit.Animal(), gofakeit.Animal()} + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.UpdateProjectGrantRequest{}, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + creationDate := time.Now().UTC() + if tt.prepare != nil { + tt.prepare(tt.args.req) + } + + got, err := instance.Client.Projectv2Beta.UpdateProjectGrant(tt.args.ctx, tt.args.req) + if tt.wantErr { + assert.Error(t, err) + return + } + changeDate := time.Time{} + if tt.want.change { + changeDate = time.Now().UTC() + } + assert.NoError(t, err) + assertUpdateProjectGrantResponse(t, creationDate, changeDate, tt.want.changeDate, got) + }) + } +} + +func TestServer_UpdateProjectGrant_Permission(t *testing.T) { + iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + + type args struct { + ctx context.Context + req *project.UpdateProjectGrantRequest + } + type want struct { + change bool + changeDate bool + } + tests := []struct { + name string + prepare func(request *project.UpdateProjectGrantRequest) + args args + want want + wantErr bool + }{ + { + name: "unauthenticated", + prepare: func(request *project.UpdateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + }, + args: args{ + ctx: CTX, + req: &project.UpdateProjectGrantRequest{ + RoleKeys: []string{"nopermission"}, + }, + }, + wantErr: true, + }, + { + name: "no permission", + prepare: func(request *project.UpdateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + }, + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeNoPermission), + req: &project.UpdateProjectGrantRequest{ + RoleKeys: []string{"nopermission"}, + }, + }, + wantErr: true, + }, + { + name: "organization owner, other org", + prepare: func(request *project.UpdateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + }, + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + req: &project.UpdateProjectGrantRequest{ + RoleKeys: []string{"nopermission"}, + }, + }, + wantErr: true, + }, + { + name: "organization owner, ok", + prepare: func(request *project.UpdateProjectGrantRequest) { + roles := []string{gofakeit.Animal(), gofakeit.Animal(), gofakeit.Animal()} + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + for _, role := range roles { + instance.AddProjectRole(iamOwnerCtx, t, projectResp.GetId(), role, role, "") + } + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId(), roles...) + request.RoleKeys = roles + }, + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + req: &project.UpdateProjectGrantRequest{}, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + { + name: "instance owner, ok", + prepare: func(request *project.UpdateProjectGrantRequest) { + roles := []string{gofakeit.Animal(), gofakeit.Animal(), gofakeit.Animal()} + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + for _, role := range roles { + instance.AddProjectRole(iamOwnerCtx, t, projectResp.GetId(), role, role, "") + } + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId(), roles...) + request.RoleKeys = roles + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.UpdateProjectGrantRequest{}, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + creationDate := time.Now().UTC() + if tt.prepare != nil { + tt.prepare(tt.args.req) + } + + got, err := instance.Client.Projectv2Beta.UpdateProjectGrant(tt.args.ctx, tt.args.req) + if tt.wantErr { + assert.Error(t, err) + return + } + changeDate := time.Time{} + if tt.want.change { + changeDate = time.Now().UTC() + } + assert.NoError(t, err) + assertUpdateProjectGrantResponse(t, creationDate, changeDate, tt.want.changeDate, got) + }) + } +} + +func assertUpdateProjectGrantResponse(t *testing.T, creationDate, changeDate time.Time, expectedChangeDate bool, actualResp *project.UpdateProjectGrantResponse) { + if expectedChangeDate { + if !changeDate.IsZero() { + assert.WithinRange(t, actualResp.GetChangeDate().AsTime(), creationDate, changeDate) + } else { + assert.WithinRange(t, actualResp.GetChangeDate().AsTime(), creationDate, time.Now().UTC()) + } + } else { + assert.Nil(t, actualResp.ChangeDate) + } +} + +func TestServer_DeleteProjectGrant(t *testing.T) { + iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + + tests := []struct { + name string + ctx context.Context + prepare func(request *project.DeleteProjectGrantRequest) (time.Time, time.Time) + req *project.DeleteProjectGrantRequest + wantDeletionDate bool + wantErr bool + }{ + { + name: "empty project id", + ctx: iamOwnerCtx, + req: &project.DeleteProjectGrantRequest{ + ProjectId: "", + }, + wantErr: true, + }, + { + name: "empty grantedorg id", + ctx: iamOwnerCtx, + req: &project.DeleteProjectGrantRequest{ + ProjectId: "notempty", + GrantedOrganizationId: "", + }, + wantErr: true, + }, + { + name: "delete, not existing", + ctx: iamOwnerCtx, + req: &project.DeleteProjectGrantRequest{ + ProjectId: "notexisting", + GrantedOrganizationId: "notexisting", + }, + wantErr: true, + }, + { + name: "delete", + ctx: iamOwnerCtx, + prepare: func(request *project.DeleteProjectGrantRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + return creationDate, time.Time{} + }, + req: &project.DeleteProjectGrantRequest{}, + wantDeletionDate: true, + }, + { + name: "delete deactivated", + ctx: iamOwnerCtx, + prepare: func(request *project.DeleteProjectGrantRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + instance.DeactivateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + return creationDate, time.Time{} + }, + req: &project.DeleteProjectGrantRequest{}, + wantDeletionDate: true, + }, + { + name: "delete, already removed", + ctx: iamOwnerCtx, + prepare: func(request *project.DeleteProjectGrantRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + instance.DeleteProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + return creationDate, time.Now().UTC() + }, + req: &project.DeleteProjectGrantRequest{}, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var creationDate, deletionDate time.Time + if tt.prepare != nil { + creationDate, deletionDate = tt.prepare(tt.req) + } + got, err := instance.Client.Projectv2Beta.DeleteProjectGrant(tt.ctx, tt.req) + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assertDeleteProjectGrantResponse(t, creationDate, deletionDate, tt.wantDeletionDate, got) + }) + } +} + +func TestServer_DeleteProjectGrant_Permission(t *testing.T) { + iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + + tests := []struct { + name string + ctx context.Context + prepare func(request *project.DeleteProjectGrantRequest) (time.Time, time.Time) + req *project.DeleteProjectGrantRequest + wantDeletionDate bool + wantErr bool + }{ + { + name: "unauthenticated", + ctx: CTX, + prepare: func(request *project.DeleteProjectGrantRequest) (time.Time, time.Time) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + return time.Time{}, time.Time{} + }, + req: &project.DeleteProjectGrantRequest{}, + wantErr: true, + }, + { + name: "no permission", + ctx: instance.WithAuthorization(CTX, integration.UserTypeNoPermission), + prepare: func(request *project.DeleteProjectGrantRequest) (time.Time, time.Time) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + return time.Time{}, time.Time{} + }, + req: &project.DeleteProjectGrantRequest{}, + wantErr: true, + }, + { + name: "organization owner, other org", + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + prepare: func(request *project.DeleteProjectGrantRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + return creationDate, time.Time{} + }, + req: &project.DeleteProjectGrantRequest{}, + wantErr: true, + }, + { + name: "organization owner, ok", + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + prepare: func(request *project.DeleteProjectGrantRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + return creationDate, time.Time{} + }, + req: &project.DeleteProjectGrantRequest{}, + wantDeletionDate: true, + }, + { + name: "organization owner, ok", + ctx: iamOwnerCtx, + prepare: func(request *project.DeleteProjectGrantRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + return creationDate, time.Time{} + }, + req: &project.DeleteProjectGrantRequest{}, + wantDeletionDate: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var creationDate, deletionDate time.Time + if tt.prepare != nil { + creationDate, deletionDate = tt.prepare(tt.req) + } + got, err := instance.Client.Projectv2Beta.DeleteProjectGrant(tt.ctx, tt.req) + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assertDeleteProjectGrantResponse(t, creationDate, deletionDate, tt.wantDeletionDate, got) + }) + } +} + +func assertDeleteProjectGrantResponse(t *testing.T, creationDate, deletionDate time.Time, expectedDeletionDate bool, actualResp *project.DeleteProjectGrantResponse) { + if expectedDeletionDate { + if !deletionDate.IsZero() { + assert.WithinRange(t, actualResp.GetDeletionDate().AsTime(), creationDate, deletionDate) + } else { + assert.WithinRange(t, actualResp.GetDeletionDate().AsTime(), creationDate, time.Now().UTC()) + } + } else { + assert.Nil(t, actualResp.DeletionDate) + } +} + +func TestServer_DeactivateProjectGrant(t *testing.T) { + iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + + type args struct { + ctx context.Context + req *project.DeactivateProjectGrantRequest + } + type want struct { + change bool + changeDate bool + } + tests := []struct { + name string + prepare func(request *project.DeactivateProjectGrantRequest) + args args + want want + wantErr bool + }{ + { + name: "missing permission", + prepare: func(request *project.DeactivateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + }, + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeNoPermission), + req: &project.DeactivateProjectGrantRequest{}, + }, + wantErr: true, + }, + { + name: "not existing", + prepare: func(request *project.DeactivateProjectGrantRequest) { + request.ProjectId = "notexisting" + request.GrantedOrganizationId = "notexisting" + return + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.DeactivateProjectGrantRequest{}, + }, + wantErr: true, + }, + { + name: "no change, ok", + prepare: func(request *project.DeactivateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + instance.DeactivateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.DeactivateProjectGrantRequest{}, + }, + want: want{ + change: false, + changeDate: true, + }, + }, + { + name: "change, ok", + prepare: func(request *project.DeactivateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.DeactivateProjectGrantRequest{}, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + creationDate := time.Now().UTC() + tt.prepare(tt.args.req) + + got, err := instance.Client.Projectv2Beta.DeactivateProjectGrant(tt.args.ctx, tt.args.req) + if tt.wantErr { + assert.Error(t, err) + return + } + changeDate := time.Time{} + if tt.want.change { + changeDate = time.Now().UTC() + } + assert.NoError(t, err) + assertDeactivateProjectGrantResponse(t, creationDate, changeDate, tt.want.changeDate, got) + }) + } +} + +func TestServer_DeactivateProjectGrant_Permission(t *testing.T) { + iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + + type args struct { + ctx context.Context + req *project.DeactivateProjectGrantRequest + } + type want struct { + change bool + changeDate bool + } + tests := []struct { + name string + prepare func(request *project.DeactivateProjectGrantRequest) + args args + want want + wantErr bool + }{ + { + name: "unauthenticated", + prepare: func(request *project.DeactivateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + }, + args: args{ + ctx: CTX, + req: &project.DeactivateProjectGrantRequest{}, + }, + wantErr: true, + }, + { + name: "no permission", + prepare: func(request *project.DeactivateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + }, + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeNoPermission), + req: &project.DeactivateProjectGrantRequest{}, + }, + wantErr: true, + }, + { + name: "organization owner, other org", + prepare: func(request *project.DeactivateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + }, + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + req: &project.DeactivateProjectGrantRequest{}, + }, + wantErr: true, + }, + { + name: "organization owner, ok", + prepare: func(request *project.DeactivateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + }, + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + req: &project.DeactivateProjectGrantRequest{}, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + { + name: "instance owner, ok", + prepare: func(request *project.DeactivateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.DeactivateProjectGrantRequest{}, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + creationDate := time.Now().UTC() + tt.prepare(tt.args.req) + + got, err := instance.Client.Projectv2Beta.DeactivateProjectGrant(tt.args.ctx, tt.args.req) + if tt.wantErr { + assert.Error(t, err) + return + } + changeDate := time.Time{} + if tt.want.change { + changeDate = time.Now().UTC() + } + assert.NoError(t, err) + assertDeactivateProjectGrantResponse(t, creationDate, changeDate, tt.want.changeDate, got) + }) + } +} + +func assertDeactivateProjectGrantResponse(t *testing.T, creationDate, changeDate time.Time, expectedChangeDate bool, actualResp *project.DeactivateProjectGrantResponse) { + if expectedChangeDate { + if !changeDate.IsZero() { + assert.WithinRange(t, actualResp.GetChangeDate().AsTime(), creationDate, changeDate) + } else { + assert.WithinRange(t, actualResp.GetChangeDate().AsTime(), creationDate, time.Now().UTC()) + } + } else { + assert.Nil(t, actualResp.ChangeDate) + } +} + +func TestServer_ActivateProjectGrant(t *testing.T) { + iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + + type args struct { + ctx context.Context + req *project.ActivateProjectGrantRequest + } + type want struct { + change bool + changeDate bool + } + tests := []struct { + name string + prepare func(request *project.ActivateProjectGrantRequest) + args args + want want + wantErr bool + }{ + { + name: "not existing", + prepare: func(request *project.ActivateProjectGrantRequest) { + request.ProjectId = "notexisting" + request.GrantedOrganizationId = "notexisting" + return + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.ActivateProjectGrantRequest{}, + }, + wantErr: true, + }, + { + name: "no change, ok", + prepare: func(request *project.ActivateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.ActivateProjectGrantRequest{}, + }, + want: want{ + change: false, + changeDate: true, + }, + }, + { + name: "change, ok", + prepare: func(request *project.ActivateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + instance.DeactivateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.ActivateProjectGrantRequest{}, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + creationDate := time.Now().UTC() + tt.prepare(tt.args.req) + + got, err := instance.Client.Projectv2Beta.ActivateProjectGrant(tt.args.ctx, tt.args.req) + if tt.wantErr { + assert.Error(t, err) + return + } + changeDate := time.Time{} + if tt.want.change { + changeDate = time.Now().UTC() + } + assert.NoError(t, err) + assertActivateProjectGrantResponse(t, creationDate, changeDate, tt.want.changeDate, got) + }) + } +} + +func TestServer_ActivateProjectGrant_Permission(t *testing.T) { + iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + + type args struct { + ctx context.Context + req *project.ActivateProjectGrantRequest + } + type want struct { + change bool + changeDate bool + } + tests := []struct { + name string + prepare func(request *project.ActivateProjectGrantRequest) + args args + want want + wantErr bool + }{ + { + name: "unauthenticated", + prepare: func(request *project.ActivateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + }, + args: args{ + ctx: CTX, + req: &project.ActivateProjectGrantRequest{}, + }, + wantErr: true, + }, + { + name: "no permission", + prepare: func(request *project.ActivateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + instance.DeactivateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + }, + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeNoPermission), + req: &project.ActivateProjectGrantRequest{}, + }, + wantErr: true, + }, + { + name: "organization owner, other org", + prepare: func(request *project.ActivateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + instance.DeactivateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + }, + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + req: &project.ActivateProjectGrantRequest{}, + }, + wantErr: true, + }, + { + name: "organization owner, ok", + prepare: func(request *project.ActivateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + instance.DeactivateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + }, + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + req: &project.ActivateProjectGrantRequest{}, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + { + name: "instance owner, ok", + prepare: func(request *project.ActivateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + instance.DeactivateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.ActivateProjectGrantRequest{}, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + creationDate := time.Now().UTC() + tt.prepare(tt.args.req) + + got, err := instance.Client.Projectv2Beta.ActivateProjectGrant(tt.args.ctx, tt.args.req) + if tt.wantErr { + assert.Error(t, err) + return + } + changeDate := time.Time{} + if tt.want.change { + changeDate = time.Now().UTC() + } + assert.NoError(t, err) + assertActivateProjectGrantResponse(t, creationDate, changeDate, tt.want.changeDate, got) + }) + } +} + +func assertActivateProjectGrantResponse(t *testing.T, creationDate, changeDate time.Time, expectedChangeDate bool, actualResp *project.ActivateProjectGrantResponse) { + if expectedChangeDate { + if !changeDate.IsZero() { + assert.WithinRange(t, actualResp.GetChangeDate().AsTime(), creationDate, changeDate) + } else { + assert.WithinRange(t, actualResp.GetChangeDate().AsTime(), creationDate, time.Now().UTC()) + } + } else { + assert.Nil(t, actualResp.ChangeDate) + } +} diff --git a/internal/api/grpc/project/v2beta/integration/project_role_test.go b/internal/api/grpc/project/v2beta/integration/project_role_test.go new file mode 100644 index 0000000000..5e2f0e447e --- /dev/null +++ b/internal/api/grpc/project/v2beta/integration/project_role_test.go @@ -0,0 +1,698 @@ +//go:build integration + +package project_test + +import ( + "context" + "testing" + "time" + + "github.com/brianvoe/gofakeit/v6" + "github.com/muhlemmer/gu" + "github.com/stretchr/testify/assert" + + "github.com/zitadel/zitadel/internal/integration" + project "github.com/zitadel/zitadel/pkg/grpc/project/v2beta" +) + +func TestServer_AddProjectRole(t *testing.T) { + iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + alreadyExistingProject := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + alreadyExistingProjectRoleName := gofakeit.AppName() + instance.AddProjectRole(iamOwnerCtx, t, alreadyExistingProject.GetId(), alreadyExistingProjectRoleName, alreadyExistingProjectRoleName, "") + + type want struct { + creationDate bool + } + tests := []struct { + name string + ctx context.Context + prepare func(request *project.AddProjectRoleRequest) + req *project.AddProjectRoleRequest + want + wantErr bool + }{ + { + name: "empty key", + ctx: iamOwnerCtx, + prepare: func(request *project.AddProjectRoleRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + }, + req: &project.AddProjectRoleRequest{ + RoleKey: "", + DisplayName: gofakeit.AppName(), + }, + wantErr: true, + }, + { + name: "empty displayname", + ctx: iamOwnerCtx, + prepare: func(request *project.AddProjectRoleRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + }, + req: &project.AddProjectRoleRequest{ + RoleKey: gofakeit.AppName(), + DisplayName: "", + }, + wantErr: true, + }, + { + name: "already existing, error", + ctx: iamOwnerCtx, + prepare: func(request *project.AddProjectRoleRequest) { + request.ProjectId = alreadyExistingProject.GetId() + }, + req: &project.AddProjectRoleRequest{ + RoleKey: alreadyExistingProjectRoleName, + DisplayName: alreadyExistingProjectRoleName, + }, + wantErr: true, + }, + { + name: "empty, ok", + ctx: iamOwnerCtx, + prepare: func(request *project.AddProjectRoleRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + }, + req: &project.AddProjectRoleRequest{ + RoleKey: gofakeit.Name(), + DisplayName: gofakeit.Name(), + }, + want: want{ + creationDate: true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.prepare != nil { + tt.prepare(tt.req) + } + + creationDate := time.Now().UTC() + got, err := instance.Client.Projectv2Beta.AddProjectRole(tt.ctx, tt.req) + changeDate := time.Now().UTC() + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assertAddProjectRoleResponse(t, creationDate, changeDate, tt.want.creationDate, got) + }) + } +} + +func TestServer_AddProjectRole_Permission(t *testing.T) { + iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + alreadyExistingProject := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + alreadyExistingProjectRoleName := gofakeit.AppName() + instance.AddProjectRole(iamOwnerCtx, t, alreadyExistingProject.GetId(), alreadyExistingProjectRoleName, alreadyExistingProjectRoleName, "") + + type want struct { + creationDate bool + } + tests := []struct { + name string + ctx context.Context + prepare func(request *project.AddProjectRoleRequest) + req *project.AddProjectRoleRequest + want + wantErr bool + }{ + { + name: "unauthenticated", + ctx: CTX, + prepare: func(request *project.AddProjectRoleRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + }, + req: &project.AddProjectRoleRequest{ + RoleKey: gofakeit.AppName(), + DisplayName: gofakeit.AppName(), + }, + wantErr: true, + }, + { + name: "no permission", + ctx: instance.WithAuthorization(CTX, integration.UserTypeNoPermission), + prepare: func(request *project.AddProjectRoleRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + }, + req: &project.AddProjectRoleRequest{ + RoleKey: gofakeit.AppName(), + DisplayName: gofakeit.AppName(), + }, + wantErr: true, + }, + { + name: "organization owner, other org", + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + prepare: func(request *project.AddProjectRoleRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + }, + req: &project.AddProjectRoleRequest{ + RoleKey: gofakeit.AppName(), + DisplayName: gofakeit.AppName(), + }, + wantErr: true, + }, + { + name: "organization owner, ok", + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + prepare: func(request *project.AddProjectRoleRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + }, + req: &project.AddProjectRoleRequest{ + RoleKey: gofakeit.AppName(), + DisplayName: gofakeit.AppName(), + }, + want: want{ + creationDate: true, + }, + }, + { + name: "instance owner, ok", + ctx: iamOwnerCtx, + prepare: func(request *project.AddProjectRoleRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + }, + req: &project.AddProjectRoleRequest{ + RoleKey: gofakeit.AppName(), + DisplayName: gofakeit.AppName(), + }, + want: want{ + creationDate: true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.prepare != nil { + tt.prepare(tt.req) + } + + creationDate := time.Now().UTC() + got, err := instance.Client.Projectv2Beta.AddProjectRole(tt.ctx, tt.req) + changeDate := time.Now().UTC() + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assertAddProjectRoleResponse(t, creationDate, changeDate, tt.want.creationDate, got) + }) + } +} + +func assertAddProjectRoleResponse(t *testing.T, creationDate, changeDate time.Time, expectedCreationDate bool, actualResp *project.AddProjectRoleResponse) { + if expectedCreationDate { + if !changeDate.IsZero() { + assert.WithinRange(t, actualResp.GetCreationDate().AsTime(), creationDate, changeDate) + } else { + assert.WithinRange(t, actualResp.GetCreationDate().AsTime(), creationDate, time.Now().UTC()) + } + } else { + assert.Nil(t, actualResp.CreationDate) + } +} + +func TestServer_UpdateProjectRole(t *testing.T) { + iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + + type args struct { + ctx context.Context + req *project.UpdateProjectRoleRequest + } + type want struct { + change bool + changeDate bool + } + tests := []struct { + name string + prepare func(request *project.UpdateProjectRoleRequest) + args args + want want + wantErr bool + }{ + { + name: "missing permission", + prepare: func(request *project.UpdateProjectRoleRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + roleName := gofakeit.AppName() + instance.AddProjectRole(iamOwnerCtx, t, projectResp.GetId(), roleName, roleName, "") + request.ProjectId = projectResp.GetId() + request.RoleKey = roleName + }, + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeNoPermission), + req: &project.UpdateProjectRoleRequest{ + DisplayName: gu.Ptr("changed"), + }, + }, + wantErr: true, + }, + { + name: "not existing", + prepare: func(request *project.UpdateProjectRoleRequest) { + request.RoleKey = "notexisting" + return + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.UpdateProjectRoleRequest{ + DisplayName: gu.Ptr("changed"), + }, + }, + wantErr: true, + }, + { + name: "no change, ok", + prepare: func(request *project.UpdateProjectRoleRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + roleName := gofakeit.AppName() + instance.AddProjectRole(iamOwnerCtx, t, projectResp.GetId(), roleName, roleName, "") + request.ProjectId = projectResp.GetId() + request.RoleKey = roleName + request.DisplayName = gu.Ptr(roleName) + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.UpdateProjectRoleRequest{}, + }, + want: want{ + change: false, + changeDate: true, + }, + }, + { + name: "change display name, ok", + prepare: func(request *project.UpdateProjectRoleRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + roleName := gofakeit.AppName() + instance.AddProjectRole(iamOwnerCtx, t, projectResp.GetId(), roleName, roleName, "") + request.ProjectId = projectResp.GetId() + request.RoleKey = roleName + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.UpdateProjectRoleRequest{ + DisplayName: gu.Ptr(gofakeit.AppName()), + }, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + { + name: "change full, ok", + prepare: func(request *project.UpdateProjectRoleRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + roleName := gofakeit.AppName() + instance.AddProjectRole(iamOwnerCtx, t, projectResp.GetId(), roleName, roleName, "") + request.ProjectId = projectResp.GetId() + request.RoleKey = roleName + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.UpdateProjectRoleRequest{ + DisplayName: gu.Ptr(gofakeit.AppName()), + Group: gu.Ptr(gofakeit.AppName()), + }, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + creationDate := time.Now().UTC() + tt.prepare(tt.args.req) + + got, err := instance.Client.Projectv2Beta.UpdateProjectRole(tt.args.ctx, tt.args.req) + if tt.wantErr { + assert.Error(t, err) + return + } + changeDate := time.Time{} + if tt.want.change { + changeDate = time.Now().UTC() + } + assert.NoError(t, err) + assertUpdateProjectRoleResponse(t, creationDate, changeDate, tt.want.changeDate, got) + }) + } +} + +func TestServer_UpdateProjectRole_Permission(t *testing.T) { + iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + + type args struct { + ctx context.Context + req *project.UpdateProjectRoleRequest + } + type want struct { + change bool + changeDate bool + } + tests := []struct { + name string + prepare func(request *project.UpdateProjectRoleRequest) + args args + want want + wantErr bool + }{ + { + name: "unauthenicated", + prepare: func(request *project.UpdateProjectRoleRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + roleName := gofakeit.AppName() + instance.AddProjectRole(iamOwnerCtx, t, projectResp.GetId(), roleName, roleName, "") + request.ProjectId = projectResp.GetId() + request.RoleKey = roleName + }, + args: args{ + ctx: CTX, + req: &project.UpdateProjectRoleRequest{ + DisplayName: gu.Ptr("changed"), + }, + }, + wantErr: true, + }, + { + name: "no permission", + prepare: func(request *project.UpdateProjectRoleRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + roleName := gofakeit.AppName() + instance.AddProjectRole(iamOwnerCtx, t, projectResp.GetId(), roleName, roleName, "") + request.ProjectId = projectResp.GetId() + request.RoleKey = roleName + }, + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeNoPermission), + req: &project.UpdateProjectRoleRequest{ + DisplayName: gu.Ptr("changed"), + }, + }, + wantErr: true, + }, + { + name: "organization owner, other org", + prepare: func(request *project.UpdateProjectRoleRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + roleName := gofakeit.AppName() + instance.AddProjectRole(iamOwnerCtx, t, projectResp.GetId(), roleName, roleName, "") + request.ProjectId = projectResp.GetId() + request.RoleKey = roleName + }, + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + req: &project.UpdateProjectRoleRequest{ + DisplayName: gu.Ptr("changed"), + }, + }, + wantErr: true, + }, + { + name: "organization owner, ok", + prepare: func(request *project.UpdateProjectRoleRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), false, false) + roleName := gofakeit.AppName() + instance.AddProjectRole(iamOwnerCtx, t, projectResp.GetId(), roleName, roleName, "") + request.ProjectId = projectResp.GetId() + request.RoleKey = roleName + }, + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + req: &project.UpdateProjectRoleRequest{ + DisplayName: gu.Ptr(gofakeit.AppName()), + }, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + { + name: "instance owner, ok", + prepare: func(request *project.UpdateProjectRoleRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + roleName := gofakeit.AppName() + instance.AddProjectRole(iamOwnerCtx, t, projectResp.GetId(), roleName, roleName, "") + request.ProjectId = projectResp.GetId() + request.RoleKey = roleName + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.UpdateProjectRoleRequest{ + DisplayName: gu.Ptr(gofakeit.AppName()), + }, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + creationDate := time.Now().UTC() + tt.prepare(tt.args.req) + + got, err := instance.Client.Projectv2Beta.UpdateProjectRole(tt.args.ctx, tt.args.req) + if tt.wantErr { + assert.Error(t, err) + return + } + changeDate := time.Time{} + if tt.want.change { + changeDate = time.Now().UTC() + } + assert.NoError(t, err) + assertUpdateProjectRoleResponse(t, creationDate, changeDate, tt.want.changeDate, got) + }) + } +} + +func assertUpdateProjectRoleResponse(t *testing.T, creationDate, changeDate time.Time, expectedChangeDate bool, actualResp *project.UpdateProjectRoleResponse) { + if expectedChangeDate { + if !changeDate.IsZero() { + assert.WithinRange(t, actualResp.GetChangeDate().AsTime(), creationDate, changeDate) + } else { + assert.WithinRange(t, actualResp.GetChangeDate().AsTime(), creationDate, time.Now().UTC()) + } + } else { + assert.Nil(t, actualResp.ChangeDate) + } +} + +func TestServer_DeleteProjectRole(t *testing.T) { + iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + + tests := []struct { + name string + ctx context.Context + prepare func(request *project.RemoveProjectRoleRequest) (time.Time, time.Time) + req *project.RemoveProjectRoleRequest + wantDeletionDate bool + wantErr bool + }{ + { + name: "empty id", + ctx: iamOwnerCtx, + req: &project.RemoveProjectRoleRequest{ + ProjectId: "", + RoleKey: "notexisting", + }, + wantErr: true, + }, + { + name: "delete, not existing", + ctx: iamOwnerCtx, + req: &project.RemoveProjectRoleRequest{ + ProjectId: "notexisting", + RoleKey: "notexisting", + }, + wantDeletionDate: false, + }, + { + name: "delete", + ctx: iamOwnerCtx, + prepare: func(request *project.RemoveProjectRoleRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + roleName := gofakeit.AppName() + instance.AddProjectRole(iamOwnerCtx, t, projectResp.GetId(), roleName, roleName, "") + request.ProjectId = projectResp.GetId() + request.RoleKey = roleName + return creationDate, time.Time{} + }, + req: &project.RemoveProjectRoleRequest{}, + wantDeletionDate: true, + }, + { + name: "delete, already removed", + ctx: iamOwnerCtx, + prepare: func(request *project.RemoveProjectRoleRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + roleName := gofakeit.AppName() + instance.AddProjectRole(iamOwnerCtx, t, projectResp.GetId(), roleName, roleName, "") + request.ProjectId = projectResp.GetId() + request.RoleKey = roleName + instance.RemoveProjectRole(iamOwnerCtx, t, projectResp.GetId(), roleName) + return creationDate, time.Now().UTC() + }, + req: &project.RemoveProjectRoleRequest{}, + wantDeletionDate: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var creationDate, deletionDate time.Time + if tt.prepare != nil { + creationDate, deletionDate = tt.prepare(tt.req) + } + got, err := instance.Client.Projectv2Beta.RemoveProjectRole(tt.ctx, tt.req) + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assertRemoveProjectRoleResponse(t, creationDate, deletionDate, tt.wantDeletionDate, got) + }) + } +} + +func TestServer_DeleteProjectRole_Permission(t *testing.T) { + iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + + tests := []struct { + name string + ctx context.Context + prepare func(request *project.RemoveProjectRoleRequest) (time.Time, time.Time) + req *project.RemoveProjectRoleRequest + wantDeletionDate bool + wantErr bool + }{ + { + name: "unauthenticated", + ctx: CTX, + prepare: func(request *project.RemoveProjectRoleRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + roleName := gofakeit.AppName() + instance.AddProjectRole(iamOwnerCtx, t, projectResp.GetId(), roleName, roleName, "") + request.ProjectId = projectResp.GetId() + request.RoleKey = roleName + return creationDate, time.Time{} + }, + req: &project.RemoveProjectRoleRequest{}, + wantErr: true, + }, + { + name: "no permission", + ctx: instance.WithAuthorization(CTX, integration.UserTypeNoPermission), + prepare: func(request *project.RemoveProjectRoleRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + roleName := gofakeit.AppName() + instance.AddProjectRole(iamOwnerCtx, t, projectResp.GetId(), roleName, roleName, "") + request.ProjectId = projectResp.GetId() + request.RoleKey = roleName + return creationDate, time.Time{} + }, + req: &project.RemoveProjectRoleRequest{}, + wantErr: true, + }, + { + name: "organization owner, other org", + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + prepare: func(request *project.RemoveProjectRoleRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + roleName := gofakeit.AppName() + instance.AddProjectRole(iamOwnerCtx, t, projectResp.GetId(), roleName, roleName, "") + request.ProjectId = projectResp.GetId() + request.RoleKey = roleName + return creationDate, time.Time{} + }, + req: &project.RemoveProjectRoleRequest{}, + wantErr: true, + }, + { + name: "organization owner, ok", + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + prepare: func(request *project.RemoveProjectRoleRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), false, false) + roleName := gofakeit.AppName() + instance.AddProjectRole(iamOwnerCtx, t, projectResp.GetId(), roleName, roleName, "") + request.ProjectId = projectResp.GetId() + request.RoleKey = roleName + return creationDate, time.Time{} + }, + req: &project.RemoveProjectRoleRequest{}, + wantDeletionDate: true, + }, + { + name: "instance owner, ok", + ctx: iamOwnerCtx, + prepare: func(request *project.RemoveProjectRoleRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + roleName := gofakeit.AppName() + instance.AddProjectRole(iamOwnerCtx, t, projectResp.GetId(), roleName, roleName, "") + request.ProjectId = projectResp.GetId() + request.RoleKey = roleName + return creationDate, time.Time{} + }, + req: &project.RemoveProjectRoleRequest{}, + wantDeletionDate: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var creationDate, deletionDate time.Time + if tt.prepare != nil { + creationDate, deletionDate = tt.prepare(tt.req) + } + got, err := instance.Client.Projectv2Beta.RemoveProjectRole(tt.ctx, tt.req) + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assertRemoveProjectRoleResponse(t, creationDate, deletionDate, tt.wantDeletionDate, got) + }) + } +} + +func assertRemoveProjectRoleResponse(t *testing.T, creationDate, deletionDate time.Time, expectedDeletionDate bool, actualResp *project.RemoveProjectRoleResponse) { + if expectedDeletionDate { + if !deletionDate.IsZero() { + assert.WithinRange(t, actualResp.GetRemovalDate().AsTime(), creationDate, deletionDate) + } else { + assert.WithinRange(t, actualResp.GetRemovalDate().AsTime(), creationDate, time.Now().UTC()) + } + } else { + assert.Nil(t, actualResp.RemovalDate) + } +} diff --git a/internal/api/grpc/project/v2beta/integration/project_test.go b/internal/api/grpc/project/v2beta/integration/project_test.go new file mode 100644 index 0000000000..6c0a5c96f6 --- /dev/null +++ b/internal/api/grpc/project/v2beta/integration/project_test.go @@ -0,0 +1,1013 @@ +//go:build integration + +package project_test + +import ( + "context" + "testing" + "time" + + "github.com/brianvoe/gofakeit/v6" + "github.com/muhlemmer/gu" + "github.com/stretchr/testify/assert" + + "github.com/zitadel/zitadel/internal/integration" + project "github.com/zitadel/zitadel/pkg/grpc/project/v2beta" +) + +func TestServer_CreateProject(t *testing.T) { + iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + alreadyExistingProjectName := gofakeit.AppName() + instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), alreadyExistingProjectName, false, false) + + type want struct { + id bool + creationDate bool + } + tests := []struct { + name string + ctx context.Context + req *project.CreateProjectRequest + want + wantErr bool + }{ + { + name: "empty name", + ctx: iamOwnerCtx, + req: &project.CreateProjectRequest{ + Name: "", + }, + wantErr: true, + }, + { + name: "empty organization", + ctx: iamOwnerCtx, + req: &project.CreateProjectRequest{ + Name: gofakeit.Name(), + OrganizationId: "", + }, + wantErr: true, + }, + { + name: "already existing, error", + ctx: iamOwnerCtx, + req: &project.CreateProjectRequest{ + Name: alreadyExistingProjectName, + OrganizationId: orgResp.GetOrganizationId(), + }, + wantErr: true, + }, + { + name: "empty, ok", + ctx: iamOwnerCtx, + req: &project.CreateProjectRequest{ + Name: gofakeit.Name(), + OrganizationId: orgResp.GetOrganizationId(), + }, + want: want{ + id: true, + creationDate: true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + creationDate := time.Now().UTC() + got, err := instance.Client.Projectv2Beta.CreateProject(tt.ctx, tt.req) + changeDate := time.Now().UTC() + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assertCreateProjectResponse(t, creationDate, changeDate, tt.want.creationDate, tt.want.id, got) + }) + } +} + +func TestServer_CreateProject_Permission(t *testing.T) { + iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + + type want struct { + id bool + creationDate bool + } + tests := []struct { + name string + ctx context.Context + req *project.CreateProjectRequest + want + wantErr bool + }{ + { + name: "unauthenticated", + ctx: CTX, + req: &project.CreateProjectRequest{ + Name: gofakeit.Name(), + OrganizationId: orgResp.GetOrganizationId(), + }, + wantErr: true, + }, + { + name: "missing permission", + ctx: instance.WithAuthorization(CTX, integration.UserTypeNoPermission), + req: &project.CreateProjectRequest{ + Name: gofakeit.Name(), + OrganizationId: orgResp.GetOrganizationId(), + }, + wantErr: true, + }, + { + name: "missing permission, other organization", + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + req: &project.CreateProjectRequest{ + Name: gofakeit.Name(), + OrganizationId: orgResp.GetOrganizationId(), + }, + wantErr: true, + }, + { + name: "organization owner, ok", + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + req: &project.CreateProjectRequest{ + Name: gofakeit.Name(), + OrganizationId: instance.DefaultOrg.GetId(), + }, + want: want{ + id: true, + creationDate: true, + }, + }, + { + name: "instance owner, ok", + ctx: iamOwnerCtx, + req: &project.CreateProjectRequest{ + Name: gofakeit.Name(), + OrganizationId: orgResp.GetOrganizationId(), + }, + want: want{ + id: true, + creationDate: true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + creationDate := time.Now().UTC() + got, err := instance.Client.Projectv2Beta.CreateProject(tt.ctx, tt.req) + changeDate := time.Now().UTC() + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assertCreateProjectResponse(t, creationDate, changeDate, tt.want.creationDate, tt.want.id, got) + }) + } +} + +func assertCreateProjectResponse(t *testing.T, creationDate, changeDate time.Time, expectedCreationDate, expectedID bool, actualResp *project.CreateProjectResponse) { + if expectedCreationDate { + if !changeDate.IsZero() { + assert.WithinRange(t, actualResp.GetCreationDate().AsTime(), creationDate, changeDate) + } else { + assert.WithinRange(t, actualResp.GetCreationDate().AsTime(), creationDate, time.Now().UTC()) + } + } else { + assert.Nil(t, actualResp.CreationDate) + } + + if expectedID { + assert.NotEmpty(t, actualResp.GetId()) + } else { + assert.Nil(t, actualResp.Id) + } +} + +func TestServer_UpdateProject(t *testing.T) { + iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + + type args struct { + ctx context.Context + req *project.UpdateProjectRequest + } + type want struct { + change bool + changeDate bool + } + tests := []struct { + name string + prepare func(request *project.UpdateProjectRequest) + args args + want want + wantErr bool + }{ + { + name: "not existing", + prepare: func(request *project.UpdateProjectRequest) { + request.Id = "notexisting" + return + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.UpdateProjectRequest{ + Name: gu.Ptr(gofakeit.Name()), + }, + }, + wantErr: true, + }, + { + name: "no change, ok", + prepare: func(request *project.UpdateProjectRequest) { + name := gofakeit.AppName() + projectID := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), name, false, false).GetId() + request.Id = projectID + request.Name = gu.Ptr(name) + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.UpdateProjectRequest{}, + }, + want: want{ + change: false, + changeDate: true, + }, + }, + { + name: "change name, ok", + prepare: func(request *project.UpdateProjectRequest) { + projectID := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false).GetId() + request.Id = projectID + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.UpdateProjectRequest{ + Name: gu.Ptr(gofakeit.AppName()), + }, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + { + name: "change full, ok", + prepare: func(request *project.UpdateProjectRequest) { + projectID := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false).GetId() + request.Id = projectID + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.UpdateProjectRequest{ + Name: gu.Ptr(gofakeit.AppName()), + ProjectRoleAssertion: gu.Ptr(true), + ProjectRoleCheck: gu.Ptr(true), + HasProjectCheck: gu.Ptr(true), + PrivateLabelingSetting: gu.Ptr(project.PrivateLabelingSetting_PRIVATE_LABELING_SETTING_ALLOW_LOGIN_USER_RESOURCE_OWNER_POLICY), + }, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + creationDate := time.Now().UTC() + tt.prepare(tt.args.req) + + got, err := instance.Client.Projectv2Beta.UpdateProject(tt.args.ctx, tt.args.req) + if tt.wantErr { + assert.Error(t, err) + return + } + changeDate := time.Time{} + if tt.want.change { + changeDate = time.Now().UTC() + } + assert.NoError(t, err) + assertUpdateProjectResponse(t, creationDate, changeDate, tt.want.changeDate, got) + }) + } +} + +func TestServer_UpdateProject_Permission(t *testing.T) { + iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + + type args struct { + ctx context.Context + req *project.UpdateProjectRequest + } + type want struct { + change bool + changeDate bool + } + tests := []struct { + name string + prepare func(request *project.UpdateProjectRequest) + args args + want want + wantErr bool + }{ + { + name: "unauthenticated", + prepare: func(request *project.UpdateProjectRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.Id = projectResp.GetId() + }, + args: args{ + ctx: CTX, + req: &project.UpdateProjectRequest{ + Name: gu.Ptr(gofakeit.Name()), + }, + }, + wantErr: true, + }, + { + name: "missing permission", + prepare: func(request *project.UpdateProjectRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.Id = projectResp.GetId() + }, + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeNoPermission), + req: &project.UpdateProjectRequest{ + Name: gu.Ptr(gofakeit.Name()), + }, + }, + wantErr: true, + }, + { + name: "missing permission, other organization", + prepare: func(request *project.UpdateProjectRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.Id = projectResp.GetId() + }, + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + req: &project.UpdateProjectRequest{ + Name: gu.Ptr(gofakeit.Name()), + }, + }, + wantErr: true, + }, + { + name: "organization owner, ok", + prepare: func(request *project.UpdateProjectRequest) { + projectID := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), false, false).GetId() + request.Id = projectID + }, + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + req: &project.UpdateProjectRequest{ + Name: gu.Ptr(gofakeit.AppName()), + }, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + { + name: "instance owner, ok", + prepare: func(request *project.UpdateProjectRequest) { + projectID := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), false, false).GetId() + request.Id = projectID + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.UpdateProjectRequest{ + Name: gu.Ptr(gofakeit.AppName()), + }, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + creationDate := time.Now().UTC() + tt.prepare(tt.args.req) + + got, err := instance.Client.Projectv2Beta.UpdateProject(tt.args.ctx, tt.args.req) + if tt.wantErr { + assert.Error(t, err) + return + } + changeDate := time.Time{} + if tt.want.change { + changeDate = time.Now().UTC() + } + assert.NoError(t, err) + assertUpdateProjectResponse(t, creationDate, changeDate, tt.want.changeDate, got) + }) + } +} + +func assertUpdateProjectResponse(t *testing.T, creationDate, changeDate time.Time, expectedChangeDate bool, actualResp *project.UpdateProjectResponse) { + if expectedChangeDate { + if !changeDate.IsZero() { + assert.WithinRange(t, actualResp.GetChangeDate().AsTime(), creationDate, changeDate) + } else { + assert.WithinRange(t, actualResp.GetChangeDate().AsTime(), creationDate, time.Now().UTC()) + } + } else { + assert.Nil(t, actualResp.ChangeDate) + } +} + +func TestServer_DeleteProject(t *testing.T) { + iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + + tests := []struct { + name string + ctx context.Context + prepare func(request *project.DeleteProjectRequest) (time.Time, time.Time) + req *project.DeleteProjectRequest + wantDeletionDate bool + wantErr bool + }{ + { + name: "empty id", + ctx: iamOwnerCtx, + req: &project.DeleteProjectRequest{ + Id: "", + }, + wantErr: true, + }, + { + name: "delete, not existing", + ctx: iamOwnerCtx, + req: &project.DeleteProjectRequest{ + Id: "notexisting", + }, + wantDeletionDate: false, + }, + { + name: "delete", + ctx: iamOwnerCtx, + prepare: func(request *project.DeleteProjectRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + projectID := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false).GetId() + request.Id = projectID + return creationDate, time.Time{} + }, + req: &project.DeleteProjectRequest{}, + wantDeletionDate: true, + }, + { + name: "delete, already removed", + ctx: iamOwnerCtx, + prepare: func(request *project.DeleteProjectRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + projectID := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false).GetId() + request.Id = projectID + instance.DeleteProject(iamOwnerCtx, t, projectID) + return creationDate, time.Now().UTC() + }, + req: &project.DeleteProjectRequest{}, + wantDeletionDate: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var creationDate, deletionDate time.Time + if tt.prepare != nil { + creationDate, deletionDate = tt.prepare(tt.req) + } + got, err := instance.Client.Projectv2Beta.DeleteProject(tt.ctx, tt.req) + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assertDeleteProjectResponse(t, creationDate, deletionDate, tt.wantDeletionDate, got) + }) + } +} + +func TestServer_DeleteProject_Permission(t *testing.T) { + iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + + tests := []struct { + name string + ctx context.Context + prepare func(request *project.DeleteProjectRequest) (time.Time, time.Time) + req *project.DeleteProjectRequest + wantDeletionDate bool + wantErr bool + }{ + { + name: "unauthenticated", + ctx: CTX, + prepare: func(request *project.DeleteProjectRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + projectID := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false).GetId() + request.Id = projectID + return creationDate, time.Time{} + }, + req: &project.DeleteProjectRequest{}, + wantErr: true, + }, + { + name: "missing permission", + ctx: instance.WithAuthorization(CTX, integration.UserTypeNoPermission), + prepare: func(request *project.DeleteProjectRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + projectID := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false).GetId() + request.Id = projectID + return creationDate, time.Time{} + }, + req: &project.DeleteProjectRequest{}, + wantErr: true, + }, + { + name: "organization owner, other org", + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + prepare: func(request *project.DeleteProjectRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + projectID := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false).GetId() + request.Id = projectID + return creationDate, time.Time{} + }, + req: &project.DeleteProjectRequest{}, + wantErr: true, + }, + { + name: "organization owner", + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + prepare: func(request *project.DeleteProjectRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + projectID := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), false, false).GetId() + request.Id = projectID + return creationDate, time.Time{} + }, + req: &project.DeleteProjectRequest{}, + wantDeletionDate: true, + }, + { + name: "instance owner", + ctx: iamOwnerCtx, + prepare: func(request *project.DeleteProjectRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + projectID := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), false, false).GetId() + request.Id = projectID + return creationDate, time.Time{} + }, + req: &project.DeleteProjectRequest{}, + wantDeletionDate: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var creationDate, deletionDate time.Time + if tt.prepare != nil { + creationDate, deletionDate = tt.prepare(tt.req) + } + got, err := instance.Client.Projectv2Beta.DeleteProject(tt.ctx, tt.req) + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assertDeleteProjectResponse(t, creationDate, deletionDate, tt.wantDeletionDate, got) + }) + } +} + +func assertDeleteProjectResponse(t *testing.T, creationDate, deletionDate time.Time, expectedDeletionDate bool, actualResp *project.DeleteProjectResponse) { + if expectedDeletionDate { + if !deletionDate.IsZero() { + assert.WithinRange(t, actualResp.GetDeletionDate().AsTime(), creationDate, deletionDate) + } else { + assert.WithinRange(t, actualResp.GetDeletionDate().AsTime(), creationDate, time.Now().UTC()) + } + } else { + assert.Nil(t, actualResp.DeletionDate) + } +} + +func TestServer_DeactivateProject(t *testing.T) { + iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + + type args struct { + ctx context.Context + req *project.DeactivateProjectRequest + } + type want struct { + change bool + changeDate bool + } + tests := []struct { + name string + prepare func(request *project.DeactivateProjectRequest) + args args + want want + wantErr bool + }{ + { + name: "not existing", + prepare: func(request *project.DeactivateProjectRequest) { + request.Id = "notexisting" + return + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.DeactivateProjectRequest{}, + }, + wantErr: true, + }, + { + name: "no change, ok", + prepare: func(request *project.DeactivateProjectRequest) { + name := gofakeit.AppName() + projectID := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), name, false, false).GetId() + request.Id = projectID + instance.DeactivateProject(iamOwnerCtx, t, projectID) + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.DeactivateProjectRequest{}, + }, + wantErr: true, + }, + { + name: "change, ok", + prepare: func(request *project.DeactivateProjectRequest) { + projectID := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false).GetId() + request.Id = projectID + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.DeactivateProjectRequest{}, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + creationDate := time.Now().UTC() + tt.prepare(tt.args.req) + + got, err := instance.Client.Projectv2Beta.DeactivateProject(tt.args.ctx, tt.args.req) + if tt.wantErr { + assert.Error(t, err) + return + } + changeDate := time.Time{} + if tt.want.change { + changeDate = time.Now().UTC() + } + assert.NoError(t, err) + assertDeactivateProjectResponse(t, creationDate, changeDate, tt.want.changeDate, got) + }) + } +} + +func TestServer_DeactivateProject_Permission(t *testing.T) { + iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + + type args struct { + ctx context.Context + req *project.DeactivateProjectRequest + } + type want struct { + change bool + changeDate bool + } + tests := []struct { + name string + prepare func(request *project.DeactivateProjectRequest) + args args + want want + wantErr bool + }{ + { + name: "unauthenticated", + prepare: func(request *project.DeactivateProjectRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.Id = projectResp.GetId() + }, + args: args{ + ctx: CTX, + req: &project.DeactivateProjectRequest{}, + }, + wantErr: true, + }, + { + name: "missing permission", + prepare: func(request *project.DeactivateProjectRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.Id = projectResp.GetId() + }, + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeNoPermission), + req: &project.DeactivateProjectRequest{}, + }, + wantErr: true, + }, + { + name: "organization owner, other org", + prepare: func(request *project.DeactivateProjectRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.Id = projectResp.GetId() + }, + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + req: &project.DeactivateProjectRequest{}, + }, + wantErr: true, + }, + { + name: "organization owner", + prepare: func(request *project.DeactivateProjectRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), false, false) + request.Id = projectResp.GetId() + }, + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + req: &project.DeactivateProjectRequest{}, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + { + name: "instance owner", + prepare: func(request *project.DeactivateProjectRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.Id = projectResp.GetId() + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.DeactivateProjectRequest{}, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + creationDate := time.Now().UTC() + tt.prepare(tt.args.req) + + got, err := instance.Client.Projectv2Beta.DeactivateProject(tt.args.ctx, tt.args.req) + if tt.wantErr { + assert.Error(t, err) + return + } + changeDate := time.Time{} + if tt.want.change { + changeDate = time.Now().UTC() + } + assert.NoError(t, err) + assertDeactivateProjectResponse(t, creationDate, changeDate, tt.want.changeDate, got) + }) + } +} + +func assertDeactivateProjectResponse(t *testing.T, creationDate, changeDate time.Time, expectedChangeDate bool, actualResp *project.DeactivateProjectResponse) { + if expectedChangeDate { + if !changeDate.IsZero() { + assert.WithinRange(t, actualResp.GetChangeDate().AsTime(), creationDate, changeDate) + } else { + assert.WithinRange(t, actualResp.GetChangeDate().AsTime(), creationDate, time.Now().UTC()) + } + } else { + assert.Nil(t, actualResp.ChangeDate) + } +} + +func TestServer_ActivateProject(t *testing.T) { + iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + + type args struct { + ctx context.Context + req *project.ActivateProjectRequest + } + type want struct { + change bool + changeDate bool + } + tests := []struct { + name string + prepare func(request *project.ActivateProjectRequest) + args args + want want + wantErr bool + }{ + { + name: "missing permission", + prepare: func(request *project.ActivateProjectRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.Id = projectResp.GetId() + }, + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeNoPermission), + req: &project.ActivateProjectRequest{}, + }, + wantErr: true, + }, + { + name: "not existing", + prepare: func(request *project.ActivateProjectRequest) { + request.Id = "notexisting" + return + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.ActivateProjectRequest{}, + }, + wantErr: true, + }, + { + name: "no change, ok", + prepare: func(request *project.ActivateProjectRequest) { + name := gofakeit.AppName() + projectID := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), name, false, false).GetId() + request.Id = projectID + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.ActivateProjectRequest{}, + }, + wantErr: true, + }, + { + name: "change, ok", + prepare: func(request *project.ActivateProjectRequest) { + projectID := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false).GetId() + request.Id = projectID + instance.DeactivateProject(iamOwnerCtx, t, projectID) + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.ActivateProjectRequest{}, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + creationDate := time.Now().UTC() + tt.prepare(tt.args.req) + + got, err := instance.Client.Projectv2Beta.ActivateProject(tt.args.ctx, tt.args.req) + if tt.wantErr { + assert.Error(t, err) + return + } + changeDate := time.Time{} + if tt.want.change { + changeDate = time.Now().UTC() + } + assert.NoError(t, err) + assertActivateProjectResponse(t, creationDate, changeDate, tt.want.changeDate, got) + }) + } +} + +func TestServer_ActivateProject_Permission(t *testing.T) { + iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + + type args struct { + ctx context.Context + req *project.ActivateProjectRequest + } + type want struct { + change bool + changeDate bool + } + tests := []struct { + name string + prepare func(request *project.ActivateProjectRequest) + args args + want want + wantErr bool + }{ + { + name: "unauthenticated", + prepare: func(request *project.ActivateProjectRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.Id = projectResp.GetId() + instance.DeactivateProject(iamOwnerCtx, t, projectResp.GetId()) + }, + args: args{ + ctx: CTX, + req: &project.ActivateProjectRequest{}, + }, + wantErr: true, + }, + { + name: "missing permission", + prepare: func(request *project.ActivateProjectRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.Id = projectResp.GetId() + instance.DeactivateProject(iamOwnerCtx, t, projectResp.GetId()) + }, + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeNoPermission), + req: &project.ActivateProjectRequest{}, + }, + wantErr: true, + }, + { + name: "organization owner, other org", + prepare: func(request *project.ActivateProjectRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.Id = projectResp.GetId() + instance.DeactivateProject(iamOwnerCtx, t, projectResp.GetId()) + }, + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + req: &project.ActivateProjectRequest{}, + }, + wantErr: true, + }, + { + name: "organization owner", + prepare: func(request *project.ActivateProjectRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), false, false) + request.Id = projectResp.GetId() + instance.DeactivateProject(iamOwnerCtx, t, projectResp.GetId()) + }, + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + req: &project.ActivateProjectRequest{}, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + { + name: "instance owner", + prepare: func(request *project.ActivateProjectRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.Id = projectResp.GetId() + instance.DeactivateProject(iamOwnerCtx, t, projectResp.GetId()) + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.ActivateProjectRequest{}, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + creationDate := time.Now().UTC() + tt.prepare(tt.args.req) + + got, err := instance.Client.Projectv2Beta.ActivateProject(tt.args.ctx, tt.args.req) + if tt.wantErr { + assert.Error(t, err) + return + } + changeDate := time.Time{} + if tt.want.change { + changeDate = time.Now().UTC() + } + assert.NoError(t, err) + assertActivateProjectResponse(t, creationDate, changeDate, tt.want.changeDate, got) + }) + } +} + +func assertActivateProjectResponse(t *testing.T, creationDate, changeDate time.Time, expectedChangeDate bool, actualResp *project.ActivateProjectResponse) { + if expectedChangeDate { + if !changeDate.IsZero() { + assert.WithinRange(t, actualResp.GetChangeDate().AsTime(), creationDate, changeDate) + } else { + assert.WithinRange(t, actualResp.GetChangeDate().AsTime(), creationDate, time.Now().UTC()) + } + } else { + assert.Nil(t, actualResp.ChangeDate) + } +} diff --git a/internal/api/grpc/project/v2beta/integration/query_test.go b/internal/api/grpc/project/v2beta/integration/query_test.go new file mode 100644 index 0000000000..f959bfe2f8 --- /dev/null +++ b/internal/api/grpc/project/v2beta/integration/query_test.go @@ -0,0 +1,1738 @@ +//go:build integration + +package project_test + +import ( + "context" + "testing" + "time" + + "github.com/brianvoe/gofakeit/v6" + "github.com/muhlemmer/gu" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/zitadel/zitadel/internal/integration" + filter "github.com/zitadel/zitadel/pkg/grpc/filter/v2beta" + project "github.com/zitadel/zitadel/pkg/grpc/project/v2beta" +) + +func TestServer_GetProject(t *testing.T) { + iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + + type args struct { + ctx context.Context + dep func(*project.GetProjectRequest, *project.GetProjectResponse) + req *project.GetProjectRequest + } + tests := []struct { + name string + args args + want *project.GetProjectResponse + wantErr bool + }{ + { + name: "missing permission", + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeNoPermission), + dep: func(request *project.GetProjectRequest, response *project.GetProjectResponse) { + orgID := instance.DefaultOrg.GetId() + resp := createProject(iamOwnerCtx, instance, t, orgID, false, false) + + request.Id = resp.GetId() + }, + req: &project.GetProjectRequest{}, + }, + wantErr: true, + }, + { + name: "missing permission, other org owner", + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + dep: func(request *project.GetProjectRequest, response *project.GetProjectResponse) { + name := gofakeit.AppName() + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + resp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), name, false, false) + + request.Id = resp.GetId() + }, + req: &project.GetProjectRequest{}, + }, + wantErr: true, + }, + { + name: "not found", + args: args{ + ctx: iamOwnerCtx, + req: &project.GetProjectRequest{Id: "notexisting"}, + }, + wantErr: true, + }, + { + name: "get, ok", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *project.GetProjectRequest, response *project.GetProjectResponse) { + orgID := instance.DefaultOrg.GetId() + resp := createProject(iamOwnerCtx, instance, t, orgID, false, false) + + request.Id = resp.GetId() + response.Project = resp + }, + req: &project.GetProjectRequest{}, + }, + want: &project.GetProjectResponse{ + Project: &project.Project{ + State: 1, + ProjectRoleAssertion: false, + ProjectAccessRequired: false, + AuthorizationRequired: false, + PrivateLabelingSetting: project.PrivateLabelingSetting_PRIVATE_LABELING_SETTING_UNSPECIFIED, + }, + }, + }, + { + name: "get, ok, org owner", + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + dep: func(request *project.GetProjectRequest, response *project.GetProjectResponse) { + orgID := instance.DefaultOrg.GetId() + resp := createProject(iamOwnerCtx, instance, t, orgID, false, false) + + request.Id = resp.GetId() + response.Project = resp + }, + req: &project.GetProjectRequest{}, + }, + want: &project.GetProjectResponse{ + Project: &project.Project{}, + }, + }, + { + name: "get, full, ok", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *project.GetProjectRequest, response *project.GetProjectResponse) { + orgID := instance.DefaultOrg.GetId() + resp := createProject(iamOwnerCtx, instance, t, orgID, true, true) + + request.Id = resp.GetId() + response.Project = resp + }, + req: &project.GetProjectRequest{}, + }, + want: &project.GetProjectResponse{ + Project: &project.Project{}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.args.dep != nil { + tt.args.dep(tt.args.req, tt.want) + } + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(iamOwnerCtx, 2*time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + got, err := instance.Client.Projectv2Beta.GetProject(tt.args.ctx, tt.args.req) + if tt.wantErr { + assert.Error(ttt, err) + return + } + assert.NoError(ttt, err) + assert.EqualExportedValues(ttt, tt.want, got) + }, retryDuration, tick, "timeout waiting for expected project result") + }) + } +} + +func TestServer_ListProjects(t *testing.T) { + iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + type args struct { + ctx context.Context + dep func(*project.ListProjectsRequest, *project.ListProjectsResponse) + req *project.ListProjectsRequest + } + tests := []struct { + name string + args args + want *project.ListProjectsResponse + wantErr bool + }{ + { + name: "list by id, unauthenticated", + args: args{ + ctx: CTX, + dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) { + name := gofakeit.AppName() + orgID := instance.DefaultOrg.GetId() + resp := instance.CreateProject(iamOwnerCtx, t, orgID, name, false, false) + request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &project.InProjectIDsFilter{ + ProjectIds: []string{resp.GetId()}, + }, + } + }, + req: &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{{}}, + }, + }, + wantErr: true, + }, + { + name: "list by id, no permission", + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeNoPermission), + dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) { + name := gofakeit.AppName() + orgID := instance.DefaultOrg.GetId() + resp := instance.CreateProject(iamOwnerCtx, t, orgID, name, false, false) + request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &project.InProjectIDsFilter{ + ProjectIds: []string{resp.GetId()}, + }, + } + }, + req: &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{{}}, + }, + }, + wantErr: true, + }, + { + name: "list by id, missing permission", + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) { + name := gofakeit.AppName() + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + resp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), name, false, false) + request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &project.InProjectIDsFilter{ + ProjectIds: []string{resp.GetId()}, + }, + } + }, + req: &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{{}}, + }, + }, + want: &project.ListProjectsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Projects: []*project.Project{}, + }, + }, + { + name: "list, not found", + args: args{ + ctx: iamOwnerCtx, + req: &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{ + {Filter: &project.ProjectSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &project.InProjectIDsFilter{ + ProjectIds: []string{"notfound"}, + }, + }, + }, + }, + }, + }, + want: &project.ListProjectsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 0, + AppliedLimit: 100, + }, + }, + }, + { + name: "list single id", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) { + orgID := instance.DefaultOrg.GetId() + response.Projects[0] = createProject(iamOwnerCtx, instance, t, orgID, false, false) + request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &project.InProjectIDsFilter{ + ProjectIds: []string{response.Projects[0].GetId()}, + }, + } + }, + req: &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{{}}, + }, + }, + want: &project.ListProjectsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Projects: []*project.Project{ + {}, + }, + }, + }, + { + name: "list single name", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) { + orgID := instance.DefaultOrg.GetId() + response.Projects[0] = createProject(iamOwnerCtx, instance, t, orgID, false, false) + request.Filters[0].Filter = &project.ProjectSearchFilter_ProjectNameFilter{ + ProjectNameFilter: &project.ProjectNameFilter{ + ProjectName: response.Projects[0].Name, + }, + } + }, + req: &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{{}}, + }, + }, + want: &project.ListProjectsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Projects: []*project.Project{ + { + State: 1, + ProjectRoleAssertion: false, + ProjectAccessRequired: false, + AuthorizationRequired: false, + PrivateLabelingSetting: project.PrivateLabelingSetting_PRIVATE_LABELING_SETTING_UNSPECIFIED, + }, + }, + }, + }, + { + name: "list multiple id", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) { + orgID := instance.DefaultOrg.GetId() + response.Projects[2] = createProject(iamOwnerCtx, instance, t, orgID, false, false) + response.Projects[1] = createProject(iamOwnerCtx, instance, t, orgID, true, false) + response.Projects[0] = createProject(iamOwnerCtx, instance, t, orgID, false, true) + request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &project.InProjectIDsFilter{ + ProjectIds: []string{response.Projects[0].GetId(), response.Projects[1].GetId(), response.Projects[2].GetId()}, + }, + } + }, + req: &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{{}}, + }, + }, + want: &project.ListProjectsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 3, + AppliedLimit: 100, + }, + Projects: []*project.Project{ + {}, + {}, + {}, + }, + }, + }, + { + name: "list multiple id, limited permissions", + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) { + orgID := instance.DefaultOrg.GetId() + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + resp1 := createProject(iamOwnerCtx, instance, t, orgResp.GetOrganizationId(), false, false) + resp2 := createProject(iamOwnerCtx, instance, t, orgID, true, false) + resp3 := createProject(iamOwnerCtx, instance, t, orgResp.GetOrganizationId(), false, true) + request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &project.InProjectIDsFilter{ + ProjectIds: []string{resp1.GetId(), resp2.GetId(), resp3.GetId()}, + }, + } + + response.Projects[0] = resp2 + }, + req: &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{{}}, + }, + }, + want: &project.ListProjectsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 3, + AppliedLimit: 100, + }, + Projects: []*project.Project{ + {}, + }, + }, + }, + { + name: "list project and granted projects", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) { + orgID := instance.DefaultOrg.GetId() + projectResp := createProject(iamOwnerCtx, instance, t, orgID, true, true) + response.Projects[3] = projectResp + request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &project.InProjectIDsFilter{ + ProjectIds: []string{projectResp.GetId()}, + }, + } + response.Projects[2] = createGrantedProject(iamOwnerCtx, instance, t, projectResp) + response.Projects[1] = createGrantedProject(iamOwnerCtx, instance, t, projectResp) + response.Projects[0] = createGrantedProject(iamOwnerCtx, instance, t, projectResp) + }, + req: &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{{}}, + }, + }, + want: &project.ListProjectsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 4, + AppliedLimit: 100, + }, + Projects: []*project.Project{ + {}, + {}, + {}, + {}, + }, + }, + }, + { + name: "list project and granted projects, organization", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) { + orgID := instance.DefaultOrg.GetId() + projectResp := createProject(iamOwnerCtx, instance, t, orgID, true, true) + + grantedProjectResp := createGrantedProject(iamOwnerCtx, instance, t, projectResp) + response.Projects[1] = grantedProjectResp + response.Projects[0] = createProject(iamOwnerCtx, instance, t, *grantedProjectResp.GrantedOrganizationId, true, true) + request.Filters[0].Filter = &project.ProjectSearchFilter_ProjectOrganizationIdFilter{ + ProjectOrganizationIdFilter: &project.ProjectOrganizationIDFilter{ProjectOrganizationId: *grantedProjectResp.GrantedOrganizationId}, + } + }, + req: &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{{}}, + }, + }, + want: &project.ListProjectsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 2, + AppliedLimit: 100, + }, + Projects: []*project.Project{ + {}, + {}, + }, + }, + }, + { + name: "list project and granted projects, project resourceowner", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) { + orgID := instance.DefaultOrg.GetId() + projectResp := createProject(iamOwnerCtx, instance, t, orgID, true, true) + + grantedProjectResp := createGrantedProject(iamOwnerCtx, instance, t, projectResp) + response.Projects[0] = createProject(iamOwnerCtx, instance, t, *grantedProjectResp.GrantedOrganizationId, true, true) + request.Filters[0].Filter = &project.ProjectSearchFilter_ProjectResourceOwnerFilter{ + ProjectResourceOwnerFilter: &project.ProjectResourceOwnerFilter{ProjectResourceOwner: *grantedProjectResp.GrantedOrganizationId}, + } + }, + req: &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{{}}, + }, + }, + want: &project.ListProjectsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Projects: []*project.Project{ + {}, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.args.dep != nil { + tt.args.dep(tt.args.req, tt.want) + } + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(iamOwnerCtx, time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + got, listErr := instance.Client.Projectv2Beta.ListProjects(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(ttt, listErr) + return + } + require.NoError(ttt, listErr) + + // always first check length, otherwise its failed anyway + if assert.Len(ttt, got.Projects, len(tt.want.Projects)) { + for i := range tt.want.Projects { + assert.EqualExportedValues(ttt, tt.want.Projects[i], got.Projects[i]) + } + } + assertPaginationResponse(ttt, tt.want.Pagination, got.Pagination) + }, retryDuration, tick, "timeout waiting for expected execution result") + }) + } +} + +func TestServer_ListProjects_PermissionV2(t *testing.T) { + ensureFeaturePermissionV2Enabled(t, instancePermissionV2) + iamOwnerCtx := instancePermissionV2.WithAuthorization(CTX, integration.UserTypeIAMOwner) + orgID := instancePermissionV2.DefaultOrg.GetId() + + type args struct { + ctx context.Context + dep func(*project.ListProjectsRequest, *project.ListProjectsResponse) + req *project.ListProjectsRequest + } + tests := []struct { + name string + args args + want *project.ListProjectsResponse + wantErr bool + }{ + { + name: "list by id, unauthenticated", + args: args{ + ctx: CTX, + dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) { + resp := createProject(iamOwnerCtx, instancePermissionV2, t, orgID, false, false) + request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &project.InProjectIDsFilter{ + ProjectIds: []string{resp.GetId()}, + }, + } + }, + req: &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{{}}, + }, + }, + wantErr: true, + }, + { + name: "list by id, no permission", + args: args{ + ctx: instancePermissionV2.WithAuthorization(CTX, integration.UserTypeNoPermission), + dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) { + resp := createProject(iamOwnerCtx, instancePermissionV2, t, orgID, false, false) + request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &project.InProjectIDsFilter{ + ProjectIds: []string{resp.GetId()}, + }, + } + }, + req: &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{{}}, + }, + }, + wantErr: true, + }, + { + name: "list by id, missing permission", + args: args{ + ctx: instancePermissionV2.WithAuthorization(CTX, integration.UserTypeOrgOwner), + dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) { + orgResp := instancePermissionV2.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + resp := createProject(iamOwnerCtx, instancePermissionV2, t, orgResp.GetOrganizationId(), false, false) + request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &project.InProjectIDsFilter{ + ProjectIds: []string{resp.GetId()}, + }, + } + }, + req: &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{{}}, + }, + }, + want: &project.ListProjectsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 0, + AppliedLimit: 100, + }, + Projects: []*project.Project{}, + }, + }, + { + name: "list, not found", + args: args{ + ctx: iamOwnerCtx, + req: &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{ + {Filter: &project.ProjectSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &project.InProjectIDsFilter{ + ProjectIds: []string{"notfound"}, + }, + }, + }, + }, + }, + }, + want: &project.ListProjectsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 0, + AppliedLimit: 100, + }, + }, + }, + { + name: "list single id", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) { + response.Projects[0] = createProject(iamOwnerCtx, instancePermissionV2, t, orgID, false, false) + request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &project.InProjectIDsFilter{ + ProjectIds: []string{response.Projects[0].GetId()}, + }, + } + }, + req: &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{{}}, + }, + }, + want: &project.ListProjectsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Projects: []*project.Project{ + {}, + }, + }, + }, + { + name: "list single name", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) { + response.Projects[0] = createProject(iamOwnerCtx, instancePermissionV2, t, orgID, false, false) + request.Filters[0].Filter = &project.ProjectSearchFilter_ProjectNameFilter{ + ProjectNameFilter: &project.ProjectNameFilter{ + ProjectName: response.Projects[0].Name, + }, + } + }, + req: &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{{}}, + }, + }, + want: &project.ListProjectsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Projects: []*project.Project{ + {}, + }, + }, + }, + { + name: "list multiple id", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) { + response.Projects[2] = createProject(iamOwnerCtx, instancePermissionV2, t, orgID, false, false) + response.Projects[1] = createProject(iamOwnerCtx, instancePermissionV2, t, orgID, true, false) + response.Projects[0] = createProject(iamOwnerCtx, instancePermissionV2, t, orgID, false, true) + request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &project.InProjectIDsFilter{ + ProjectIds: []string{response.Projects[0].GetId(), response.Projects[1].GetId(), response.Projects[2].GetId()}, + }, + } + }, + req: &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{{}}, + }, + }, + want: &project.ListProjectsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 3, + AppliedLimit: 100, + }, + Projects: []*project.Project{ + {}, + {}, + {}, + }, + }, + }, + { + name: "list project and granted projects", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) { + projectResp := createProject(iamOwnerCtx, instancePermissionV2, t, orgID, true, true) + response.Projects[3] = projectResp + request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &project.InProjectIDsFilter{ + ProjectIds: []string{projectResp.GetId()}, + }, + } + response.Projects[2] = createGrantedProject(iamOwnerCtx, instancePermissionV2, t, projectResp) + response.Projects[1] = createGrantedProject(iamOwnerCtx, instancePermissionV2, t, projectResp) + response.Projects[0] = createGrantedProject(iamOwnerCtx, instancePermissionV2, t, projectResp) + }, + req: &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{{}}, + }, + }, + want: &project.ListProjectsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 4, + AppliedLimit: 100, + }, + Projects: []*project.Project{ + {}, + {}, + {}, + {}, + }, + }, + }, + { + name: "list project and granted projects, organization", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) { + projectResp := createProject(iamOwnerCtx, instancePermissionV2, t, orgID, true, true) + + grantedProjectResp := createGrantedProject(iamOwnerCtx, instancePermissionV2, t, projectResp) + response.Projects[1] = grantedProjectResp + response.Projects[0] = createProject(iamOwnerCtx, instancePermissionV2, t, *grantedProjectResp.GrantedOrganizationId, true, true) + request.Filters[0].Filter = &project.ProjectSearchFilter_ProjectOrganizationIdFilter{ + ProjectOrganizationIdFilter: &project.ProjectOrganizationIDFilter{ProjectOrganizationId: *grantedProjectResp.GrantedOrganizationId}, + } + }, + req: &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{{}}, + }, + }, + want: &project.ListProjectsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 2, + AppliedLimit: 100, + }, + Projects: []*project.Project{ + {}, + {}, + }, + }, + }, + { + name: "list project and granted projects, project resourceowner", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) { + projectResp := createProject(iamOwnerCtx, instancePermissionV2, t, orgID, true, true) + + grantedProjectResp := createGrantedProject(iamOwnerCtx, instancePermissionV2, t, projectResp) + response.Projects[0] = createProject(iamOwnerCtx, instancePermissionV2, t, *grantedProjectResp.GrantedOrganizationId, true, true) + request.Filters[0].Filter = &project.ProjectSearchFilter_ProjectResourceOwnerFilter{ + ProjectResourceOwnerFilter: &project.ProjectResourceOwnerFilter{ProjectResourceOwner: *grantedProjectResp.GrantedOrganizationId}, + } + }, + req: &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{{}}, + }, + }, + want: &project.ListProjectsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Projects: []*project.Project{ + {}, + }, + }, + }, + { + name: "list multiple id, limited permissions", + args: args{ + ctx: instancePermissionV2.WithAuthorization(CTX, integration.UserTypeOrgOwner), + dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) { + orgResp := instancePermissionV2.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + resp1 := createProject(iamOwnerCtx, instancePermissionV2, t, orgResp.GetOrganizationId(), false, false) + resp2 := createProject(iamOwnerCtx, instancePermissionV2, t, orgID, true, false) + resp3 := createProject(iamOwnerCtx, instancePermissionV2, t, orgResp.GetOrganizationId(), false, true) + request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &project.InProjectIDsFilter{ + ProjectIds: []string{resp1.GetId(), resp2.GetId(), resp3.GetId()}, + }, + } + + response.Projects[0] = resp2 + }, + req: &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{{}}, + }, + }, + want: &project.ListProjectsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Projects: []*project.Project{ + {}, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.args.dep != nil { + tt.args.dep(tt.args.req, tt.want) + } + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(iamOwnerCtx, time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + got, listErr := instancePermissionV2.Client.Projectv2Beta.ListProjects(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(ttt, listErr) + return + } + require.NoError(ttt, listErr) + + // always first check length, otherwise its failed anyway + if assert.Len(ttt, got.Projects, len(tt.want.Projects)) { + for i := range tt.want.Projects { + assert.EqualExportedValues(ttt, tt.want.Projects[i], got.Projects[i]) + } + } + assertPaginationResponse(ttt, tt.want.Pagination, got.Pagination) + }, retryDuration, tick, "timeout waiting for expected execution result") + }) + } +} + +func createProject(ctx context.Context, instance *integration.Instance, t *testing.T, orgID string, projectRoleCheck, hasProjectCheck bool) *project.Project { + name := gofakeit.AppName() + resp := instance.CreateProject(ctx, t, orgID, name, projectRoleCheck, hasProjectCheck) + return &project.Project{ + Id: resp.GetId(), + Name: name, + OrganizationId: orgID, + CreationDate: resp.GetCreationDate(), + ChangeDate: resp.GetCreationDate(), + State: 1, + ProjectRoleAssertion: false, + ProjectAccessRequired: hasProjectCheck, + AuthorizationRequired: projectRoleCheck, + PrivateLabelingSetting: project.PrivateLabelingSetting_PRIVATE_LABELING_SETTING_UNSPECIFIED, + } +} + +func createGrantedProject(ctx context.Context, instance *integration.Instance, t *testing.T, projectToGrant *project.Project) *project.Project { + grantedOrgName := gofakeit.AppName() + grantedOrg := instance.CreateOrganization(ctx, grantedOrgName, gofakeit.Email()) + projectGrantResp := instance.CreateProjectGrant(ctx, t, projectToGrant.GetId(), grantedOrg.GetOrganizationId()) + + return &project.Project{ + Id: projectToGrant.GetId(), + Name: projectToGrant.GetName(), + OrganizationId: projectToGrant.GetOrganizationId(), + CreationDate: projectGrantResp.GetCreationDate(), + ChangeDate: projectGrantResp.GetCreationDate(), + State: 1, + ProjectRoleAssertion: false, + ProjectAccessRequired: projectToGrant.GetProjectAccessRequired(), + AuthorizationRequired: projectToGrant.GetAuthorizationRequired(), + PrivateLabelingSetting: project.PrivateLabelingSetting_PRIVATE_LABELING_SETTING_UNSPECIFIED, + GrantedOrganizationId: gu.Ptr(grantedOrg.GetOrganizationId()), + GrantedOrganizationName: gu.Ptr(grantedOrgName), + GrantedState: 1, + } +} + +func assertPaginationResponse(t *assert.CollectT, expected *filter.PaginationResponse, actual *filter.PaginationResponse) { + assert.Equal(t, expected.AppliedLimit, actual.AppliedLimit) + assert.Equal(t, expected.TotalResult, actual.TotalResult) +} + +func TestServer_ListProjectGrants(t *testing.T) { + iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + type args struct { + ctx context.Context + dep func(*project.ListProjectGrantsRequest, *project.ListProjectGrantsResponse) + req *project.ListProjectGrantsRequest + } + tests := []struct { + name string + args args + want *project.ListProjectGrantsResponse + wantErr bool + }{{ + name: "list by id, unauthenticated", + args: args{ + ctx: CTX, + dep: func(request *project.ListProjectGrantsRequest, response *project.ListProjectGrantsResponse) { + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), false, false) + request.Filters[0].Filter = &project.ProjectGrantSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &project.InProjectIDsFilter{ + ProjectIds: []string{projectResp.GetId()}, + }, + } + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.Filters[1].Filter = &project.ProjectGrantSearchFilter_ProjectGrantResourceOwnerFilter{ + ProjectGrantResourceOwnerFilter: &project.ProjectGrantResourceOwnerFilter{ + ProjectGrantResourceOwner: grantedOrg.GetOrganizationId(), + }, + } + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + }, + req: &project.ListProjectGrantsRequest{ + Filters: []*project.ProjectGrantSearchFilter{{}, {}}, + }, + }, + wantErr: true, + }, + { + name: "list by id, no permission", + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeNoPermission), + dep: func(request *project.ListProjectGrantsRequest, response *project.ListProjectGrantsResponse) { + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), false, false) + request.Filters[0].Filter = &project.ProjectGrantSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &project.InProjectIDsFilter{ + ProjectIds: []string{projectResp.GetId()}, + }, + } + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.Filters[1].Filter = &project.ProjectGrantSearchFilter_ProjectGrantResourceOwnerFilter{ + ProjectGrantResourceOwnerFilter: &project.ProjectGrantResourceOwnerFilter{ + ProjectGrantResourceOwner: grantedOrg.GetOrganizationId(), + }, + } + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + }, + req: &project.ListProjectGrantsRequest{ + Filters: []*project.ProjectGrantSearchFilter{{}, {}}, + }, + }, + wantErr: true, + }, + { + name: "list, not found", + args: args{ + ctx: iamOwnerCtx, + req: &project.ListProjectGrantsRequest{ + Filters: []*project.ProjectGrantSearchFilter{ + {Filter: &project.ProjectGrantSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &project.InProjectIDsFilter{ + ProjectIds: []string{"notfound"}, + }, + }, + }, + }, + }, + }, + want: &project.ListProjectGrantsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 0, + AppliedLimit: 100, + }, + }, + }, + { + name: "list by id", + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + dep: func(request *project.ListProjectGrantsRequest, response *project.ListProjectGrantsResponse) { + name := gofakeit.AppName() + orgID := instance.DefaultOrg.GetId() + projectResp := instance.CreateProject(iamOwnerCtx, t, orgID, name, false, false) + request.Filters[0].Filter = &project.ProjectGrantSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &project.InProjectIDsFilter{ + ProjectIds: []string{projectResp.GetId()}, + }, + } + + response.ProjectGrants[0] = createProjectGrant(iamOwnerCtx, instance, t, orgID, projectResp.GetId(), name) + }, + req: &project.ListProjectGrantsRequest{ + Filters: []*project.ProjectGrantSearchFilter{{}}, + }, + }, + want: &project.ListProjectGrantsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + ProjectGrants: []*project.ProjectGrant{ + {}, + }, + }, + }, + { + name: "list by id, missing permission", + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + dep: func(request *project.ListProjectGrantsRequest, response *project.ListProjectGrantsResponse) { + name := gofakeit.AppName() + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), name, false, false) + request.Filters[0].Filter = &project.ProjectGrantSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &project.InProjectIDsFilter{ + ProjectIds: []string{projectResp.GetId()}, + }, + } + + createProjectGrant(iamOwnerCtx, instance, t, orgResp.GetOrganizationId(), projectResp.GetId(), name) + }, + req: &project.ListProjectGrantsRequest{ + Filters: []*project.ProjectGrantSearchFilter{{}}, + }, + }, + want: &project.ListProjectGrantsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + ProjectGrants: []*project.ProjectGrant{}, + }, + }, + { + name: "list multiple id", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *project.ListProjectGrantsRequest, response *project.ListProjectGrantsResponse) { + name := gofakeit.AppName() + orgID := instance.DefaultOrg.GetId() + projectResp := instance.CreateProject(iamOwnerCtx, t, orgID, name, false, false) + request.Filters[0].Filter = &project.ProjectGrantSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &project.InProjectIDsFilter{ + ProjectIds: []string{projectResp.GetId()}, + }, + } + + response.ProjectGrants[2] = createProjectGrant(iamOwnerCtx, instance, t, orgID, projectResp.GetId(), name) + response.ProjectGrants[1] = createProjectGrant(iamOwnerCtx, instance, t, orgID, projectResp.GetId(), name) + response.ProjectGrants[0] = createProjectGrant(iamOwnerCtx, instance, t, orgID, projectResp.GetId(), name) + }, + req: &project.ListProjectGrantsRequest{ + Filters: []*project.ProjectGrantSearchFilter{{}}, + }, + }, + want: &project.ListProjectGrantsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 3, + AppliedLimit: 100, + }, + ProjectGrants: []*project.ProjectGrant{ + {}, {}, {}, + }, + }, + }, + { + name: "list multiple id, limited permissions", + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + dep: func(request *project.ListProjectGrantsRequest, response *project.ListProjectGrantsResponse) { + name1 := gofakeit.AppName() + name2 := gofakeit.AppName() + name3 := gofakeit.AppName() + orgID := instance.DefaultOrg.GetId() + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + project1Resp := instance.CreateProject(iamOwnerCtx, t, orgID, name1, false, false) + project2Resp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), name2, false, false) + project3Resp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), name3, false, false) + request.Filters[0].Filter = &project.ProjectGrantSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &project.InProjectIDsFilter{ + ProjectIds: []string{project1Resp.GetId(), project2Resp.GetId(), project3Resp.GetId()}, + }, + } + + response.ProjectGrants[0] = createProjectGrant(iamOwnerCtx, instance, t, orgID, project1Resp.GetId(), name1) + createProjectGrant(iamOwnerCtx, instance, t, orgResp.GetOrganizationId(), project2Resp.GetId(), name2) + createProjectGrant(iamOwnerCtx, instance, t, orgResp.GetOrganizationId(), project3Resp.GetId(), name3) + }, + req: &project.ListProjectGrantsRequest{ + Filters: []*project.ProjectGrantSearchFilter{{}}, + }, + }, + want: &project.ListProjectGrantsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 3, + AppliedLimit: 100, + }, + ProjectGrants: []*project.ProjectGrant{ + {}, + }, + }, + }, { + name: "list single id with role", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *project.ListProjectGrantsRequest, response *project.ListProjectGrantsResponse) { + name := gofakeit.AppName() + orgID := instance.DefaultOrg.GetId() + projectResp := instance.CreateProject(iamOwnerCtx, t, orgID, name, false, false) + request.Filters[0].Filter = &project.ProjectGrantSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &project.InProjectIDsFilter{ + ProjectIds: []string{projectResp.GetId()}, + }, + } + projectRoleResp := addProjectRole(iamOwnerCtx, instance, t, projectResp.GetId()) + response.ProjectGrants[0] = createProjectGrant(iamOwnerCtx, instance, t, orgID, projectResp.GetId(), name, projectRoleResp.Key) + }, + req: &project.ListProjectGrantsRequest{ + Filters: []*project.ProjectGrantSearchFilter{{}}, + }, + }, + want: &project.ListProjectGrantsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + ProjectGrants: []*project.ProjectGrant{ + {}, + }, + }, + }, + { + name: "list single id with removed role", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *project.ListProjectGrantsRequest, response *project.ListProjectGrantsResponse) { + name := gofakeit.AppName() + orgID := instance.DefaultOrg.GetId() + projectResp := instance.CreateProject(iamOwnerCtx, t, orgID, name, false, false) + request.Filters[0].Filter = &project.ProjectGrantSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &project.InProjectIDsFilter{ + ProjectIds: []string{projectResp.GetId()}, + }, + } + projectRoleResp := addProjectRole(iamOwnerCtx, instance, t, projectResp.GetId()) + response.ProjectGrants[0] = createProjectGrant(iamOwnerCtx, instance, t, orgID, projectResp.GetId(), name, projectRoleResp.Key) + + removeRoleResp := instance.RemoveProjectRole(iamOwnerCtx, t, projectResp.GetId(), projectRoleResp.Key) + response.ProjectGrants[0].GrantedRoleKeys = nil + response.ProjectGrants[0].ChangeDate = removeRoleResp.GetRemovalDate() + }, + req: &project.ListProjectGrantsRequest{ + Filters: []*project.ProjectGrantSearchFilter{{}}, + }, + }, + want: &project.ListProjectGrantsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + ProjectGrants: []*project.ProjectGrant{ + {}, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.args.dep != nil { + tt.args.dep(tt.args.req, tt.want) + } + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(iamOwnerCtx, time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + got, listErr := instance.Client.Projectv2Beta.ListProjectGrants(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(ttt, listErr, "Error: "+listErr.Error()) + return + } + require.NoError(ttt, listErr) + + // always first check length, otherwise its failed anyway + if assert.Len(ttt, got.ProjectGrants, len(tt.want.ProjectGrants)) { + for i := range tt.want.ProjectGrants { + assert.EqualExportedValues(ttt, tt.want.ProjectGrants[i], got.ProjectGrants[i]) + } + } + assertPaginationResponse(ttt, tt.want.Pagination, got.Pagination) + }, retryDuration, tick, "timeout waiting for expected execution result") + }) + } +} + +func TestServer_ListProjectGrants_PermissionV2(t *testing.T) { + ensureFeaturePermissionV2Enabled(t, instancePermissionV2) + iamOwnerCtx := instancePermissionV2.WithAuthorization(CTX, integration.UserTypeIAMOwner) + + type args struct { + ctx context.Context + dep func(*project.ListProjectGrantsRequest, *project.ListProjectGrantsResponse) + req *project.ListProjectGrantsRequest + } + tests := []struct { + name string + args args + want *project.ListProjectGrantsResponse + wantErr bool + }{ + { + name: "list by id, unauthenticated", + args: args{ + ctx: CTX, + dep: func(request *project.ListProjectGrantsRequest, response *project.ListProjectGrantsResponse) { + projectResp := instancePermissionV2.CreateProject(iamOwnerCtx, t, instancePermissionV2.DefaultOrg.GetId(), gofakeit.AppName(), false, false) + request.Filters[0].Filter = &project.ProjectGrantSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &project.InProjectIDsFilter{ + ProjectIds: []string{projectResp.GetId()}, + }, + } + grantedOrg := instancePermissionV2.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.Filters[1].Filter = &project.ProjectGrantSearchFilter_ProjectGrantResourceOwnerFilter{ + ProjectGrantResourceOwnerFilter: &project.ProjectGrantResourceOwnerFilter{ + ProjectGrantResourceOwner: grantedOrg.GetOrganizationId(), + }, + } + + instancePermissionV2.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + }, + req: &project.ListProjectGrantsRequest{ + Filters: []*project.ProjectGrantSearchFilter{{}, {}}, + }, + }, + wantErr: true, + }, + { + name: "list by id, no permission", + args: args{ + ctx: instancePermissionV2.WithAuthorization(CTX, integration.UserTypeNoPermission), + dep: func(request *project.ListProjectGrantsRequest, response *project.ListProjectGrantsResponse) { + projectResp := instancePermissionV2.CreateProject(iamOwnerCtx, t, instancePermissionV2.DefaultOrg.GetId(), gofakeit.AppName(), false, false) + request.Filters[0].Filter = &project.ProjectGrantSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &project.InProjectIDsFilter{ + ProjectIds: []string{projectResp.GetId()}, + }, + } + grantedOrg := instancePermissionV2.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.Filters[1].Filter = &project.ProjectGrantSearchFilter_ProjectGrantResourceOwnerFilter{ + ProjectGrantResourceOwnerFilter: &project.ProjectGrantResourceOwnerFilter{ + ProjectGrantResourceOwner: grantedOrg.GetOrganizationId(), + }, + } + + instancePermissionV2.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + }, + req: &project.ListProjectGrantsRequest{ + Filters: []*project.ProjectGrantSearchFilter{{}, {}}, + }, + }, + wantErr: true, + }, + { + name: "list by id", + args: args{ + ctx: instancePermissionV2.WithAuthorization(CTX, integration.UserTypeOrgOwner), + dep: func(request *project.ListProjectGrantsRequest, response *project.ListProjectGrantsResponse) { + name := gofakeit.AppName() + orgID := instancePermissionV2.DefaultOrg.GetId() + projectResp := instancePermissionV2.CreateProject(iamOwnerCtx, t, orgID, name, false, false) + request.Filters[0].Filter = &project.ProjectGrantSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &project.InProjectIDsFilter{ + ProjectIds: []string{projectResp.GetId()}, + }, + } + + response.ProjectGrants[0] = createProjectGrant(iamOwnerCtx, instancePermissionV2, t, orgID, projectResp.GetId(), name) + }, + req: &project.ListProjectGrantsRequest{ + Filters: []*project.ProjectGrantSearchFilter{{}}, + }, + }, + want: &project.ListProjectGrantsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + ProjectGrants: []*project.ProjectGrant{ + {}, + }, + }, + }, + { + name: "list by id, missing permission", + args: args{ + ctx: instancePermissionV2.WithAuthorization(CTX, integration.UserTypeOrgOwner), + dep: func(request *project.ListProjectGrantsRequest, response *project.ListProjectGrantsResponse) { + name := gofakeit.AppName() + orgResp := instancePermissionV2.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + projectResp := instancePermissionV2.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), name, false, false) + request.Filters[0].Filter = &project.ProjectGrantSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &project.InProjectIDsFilter{ + ProjectIds: []string{projectResp.GetId()}, + }, + } + + createProjectGrant(iamOwnerCtx, instancePermissionV2, t, orgResp.GetOrganizationId(), projectResp.GetId(), name) + }, + req: &project.ListProjectGrantsRequest{ + Filters: []*project.ProjectGrantSearchFilter{{}}, + }, + }, + want: &project.ListProjectGrantsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 0, + AppliedLimit: 100, + }, + ProjectGrants: []*project.ProjectGrant{}, + }, + }, + { + name: "list multiple id", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *project.ListProjectGrantsRequest, response *project.ListProjectGrantsResponse) { + name := gofakeit.AppName() + orgID := instancePermissionV2.DefaultOrg.GetId() + projectResp := instancePermissionV2.CreateProject(iamOwnerCtx, t, orgID, name, false, false) + request.Filters[0].Filter = &project.ProjectGrantSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &project.InProjectIDsFilter{ + ProjectIds: []string{projectResp.GetId()}, + }, + } + + response.ProjectGrants[2] = createProjectGrant(iamOwnerCtx, instancePermissionV2, t, orgID, projectResp.GetId(), name) + response.ProjectGrants[1] = createProjectGrant(iamOwnerCtx, instancePermissionV2, t, orgID, projectResp.GetId(), name) + response.ProjectGrants[0] = createProjectGrant(iamOwnerCtx, instancePermissionV2, t, orgID, projectResp.GetId(), name) + }, + req: &project.ListProjectGrantsRequest{ + Filters: []*project.ProjectGrantSearchFilter{{}}, + }, + }, + want: &project.ListProjectGrantsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 3, + AppliedLimit: 100, + }, + ProjectGrants: []*project.ProjectGrant{ + {}, {}, {}, + }, + }, + }, + { + name: "list multiple id, limited permissions", + args: args{ + ctx: instancePermissionV2.WithAuthorization(CTX, integration.UserTypeOrgOwner), + dep: func(request *project.ListProjectGrantsRequest, response *project.ListProjectGrantsResponse) { + name1 := gofakeit.AppName() + name2 := gofakeit.AppName() + name3 := gofakeit.AppName() + orgID := instancePermissionV2.DefaultOrg.GetId() + orgResp := instancePermissionV2.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + project1Resp := instancePermissionV2.CreateProject(iamOwnerCtx, t, orgID, name1, false, false) + project2Resp := instancePermissionV2.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), name2, false, false) + project3Resp := instancePermissionV2.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), name3, false, false) + request.Filters[0].Filter = &project.ProjectGrantSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &project.InProjectIDsFilter{ + ProjectIds: []string{project1Resp.GetId(), project2Resp.GetId(), project3Resp.GetId()}, + }, + } + + response.ProjectGrants[0] = createProjectGrant(iamOwnerCtx, instancePermissionV2, t, orgID, project1Resp.GetId(), name1) + createProjectGrant(iamOwnerCtx, instancePermissionV2, t, orgResp.GetOrganizationId(), project2Resp.GetId(), name2) + createProjectGrant(iamOwnerCtx, instancePermissionV2, t, orgResp.GetOrganizationId(), project3Resp.GetId(), name3) + }, + req: &project.ListProjectGrantsRequest{ + Filters: []*project.ProjectGrantSearchFilter{{}}, + }, + }, + want: &project.ListProjectGrantsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + ProjectGrants: []*project.ProjectGrant{ + {}, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.args.dep != nil { + tt.args.dep(tt.args.req, tt.want) + } + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(iamOwnerCtx, time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + got, listErr := instancePermissionV2.Client.Projectv2Beta.ListProjectGrants(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(ttt, listErr, "Error: "+listErr.Error()) + return + } + require.NoError(ttt, listErr) + + // always first check length, otherwise its failed anyway + if assert.Len(ttt, got.ProjectGrants, len(tt.want.ProjectGrants)) { + for i := range tt.want.ProjectGrants { + assert.EqualExportedValues(ttt, tt.want.ProjectGrants[i], got.ProjectGrants[i]) + } + } + assertPaginationResponse(ttt, tt.want.Pagination, got.Pagination) + }, retryDuration, tick, "timeout waiting for expected execution result") + }) + } +} + +func createProjectGrant(ctx context.Context, instance *integration.Instance, t *testing.T, orgID, projectID, projectName string, roles ...string) *project.ProjectGrant { + grantedOrgName := gofakeit.AppName() + grantedOrg := instance.CreateOrganization(ctx, grantedOrgName, gofakeit.Email()) + projectGrantResp := instance.CreateProjectGrant(ctx, t, projectID, grantedOrg.GetOrganizationId(), roles...) + + return &project.ProjectGrant{ + OrganizationId: orgID, + CreationDate: projectGrantResp.GetCreationDate(), + ChangeDate: projectGrantResp.GetCreationDate(), + GrantedOrganizationId: grantedOrg.GetOrganizationId(), + GrantedOrganizationName: grantedOrgName, + ProjectId: projectID, + ProjectName: projectName, + State: 1, + GrantedRoleKeys: roles, + } +} + +func TestServer_ListProjectRoles(t *testing.T) { + iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + type args struct { + ctx context.Context + dep func(*project.ListProjectRolesRequest, *project.ListProjectRolesResponse) + req *project.ListProjectRolesRequest + } + tests := []struct { + name string + args args + want *project.ListProjectRolesResponse + wantErr bool + }{ + { + name: "list by id, unauthenticated", + args: args{ + ctx: CTX, + dep: func(request *project.ListProjectRolesRequest, response *project.ListProjectRolesResponse) { + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), false, false) + + request.ProjectId = projectResp.GetId() + addProjectRole(iamOwnerCtx, instance, t, projectResp.GetId()) + }, + req: &project.ListProjectRolesRequest{ + Filters: []*project.ProjectRoleSearchFilter{{}}, + }, + }, + wantErr: true, + }, + { + name: "list by id, no permission", + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeNoPermission), + dep: func(request *project.ListProjectRolesRequest, response *project.ListProjectRolesResponse) { + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), false, false) + + request.ProjectId = projectResp.GetId() + addProjectRole(iamOwnerCtx, instance, t, projectResp.GetId()) + }, + req: &project.ListProjectRolesRequest{ + Filters: []*project.ProjectRoleSearchFilter{{}}, + }, + }, + wantErr: true, + }, + { + name: "list, not found", + args: args{ + ctx: iamOwnerCtx, + req: &project.ListProjectRolesRequest{ + ProjectId: "notfound", + }, + }, + want: &project.ListProjectRolesResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 0, + AppliedLimit: 100, + }, + }, + }, + { + name: "list single id, missing permission", + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + dep: func(request *project.ListProjectRolesRequest, response *project.ListProjectRolesResponse) { + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + + request.ProjectId = projectResp.GetId() + addProjectRole(iamOwnerCtx, instance, t, projectResp.GetId()) + }, + req: &project.ListProjectRolesRequest{}, + }, + want: &project.ListProjectRolesResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + ProjectRoles: []*project.ProjectRole{}, + }, + }, + { + name: "list single id", + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + dep: func(request *project.ListProjectRolesRequest, response *project.ListProjectRolesResponse) { + orgID := instance.DefaultOrg.GetId() + projectResp := instance.CreateProject(iamOwnerCtx, t, orgID, gofakeit.AppName(), false, false) + + request.ProjectId = projectResp.GetId() + response.ProjectRoles[0] = addProjectRole(iamOwnerCtx, instance, t, projectResp.GetId()) + }, + req: &project.ListProjectRolesRequest{}, + }, + want: &project.ListProjectRolesResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + ProjectRoles: []*project.ProjectRole{ + {}, + }, + }, + }, + { + name: "list multiple id", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *project.ListProjectRolesRequest, response *project.ListProjectRolesResponse) { + orgID := instance.DefaultOrg.GetId() + projectResp := instance.CreateProject(iamOwnerCtx, t, orgID, gofakeit.AppName(), false, false) + + request.ProjectId = projectResp.GetId() + response.ProjectRoles[2] = addProjectRole(iamOwnerCtx, instance, t, projectResp.GetId()) + response.ProjectRoles[1] = addProjectRole(iamOwnerCtx, instance, t, projectResp.GetId()) + response.ProjectRoles[0] = addProjectRole(iamOwnerCtx, instance, t, projectResp.GetId()) + }, + req: &project.ListProjectRolesRequest{}, + }, + want: &project.ListProjectRolesResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 3, + AppliedLimit: 100, + }, + ProjectRoles: []*project.ProjectRole{ + {}, {}, {}, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.args.dep != nil { + tt.args.dep(tt.args.req, tt.want) + } + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(iamOwnerCtx, time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + got, listErr := instance.Client.Projectv2Beta.ListProjectRoles(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(ttt, listErr, "Error: "+listErr.Error()) + return + } + require.NoError(ttt, listErr) + + // always first check length, otherwise its failed anyway + if assert.Len(ttt, got.ProjectRoles, len(tt.want.ProjectRoles)) { + for i := range tt.want.ProjectRoles { + assert.EqualExportedValues(ttt, tt.want.ProjectRoles[i], got.ProjectRoles[i]) + } + } + assertPaginationResponse(ttt, tt.want.Pagination, got.Pagination) + }, retryDuration, tick, "timeout waiting for expected execution result") + }) + } +} + +func TestServer_ListProjectRoles_PermissionV2(t *testing.T) { + ensureFeaturePermissionV2Enabled(t, instancePermissionV2) + iamOwnerCtx := instancePermissionV2.WithAuthorization(CTX, integration.UserTypeIAMOwner) + + type args struct { + ctx context.Context + dep func(*project.ListProjectRolesRequest, *project.ListProjectRolesResponse) + req *project.ListProjectRolesRequest + } + tests := []struct { + name string + args args + want *project.ListProjectRolesResponse + wantErr bool + }{ + { + name: "list by id, unauthenticated", + args: args{ + ctx: CTX, + dep: func(request *project.ListProjectRolesRequest, response *project.ListProjectRolesResponse) { + projectResp := instancePermissionV2.CreateProject(iamOwnerCtx, t, instancePermissionV2.DefaultOrg.GetId(), gofakeit.AppName(), false, false) + + request.ProjectId = projectResp.GetId() + addProjectRole(iamOwnerCtx, instancePermissionV2, t, projectResp.GetId()) + }, + req: &project.ListProjectRolesRequest{ + Filters: []*project.ProjectRoleSearchFilter{{}}, + }, + }, + wantErr: true, + }, + { + name: "list by id, no permission", + args: args{ + ctx: instancePermissionV2.WithAuthorization(CTX, integration.UserTypeNoPermission), + dep: func(request *project.ListProjectRolesRequest, response *project.ListProjectRolesResponse) { + projectResp := instancePermissionV2.CreateProject(iamOwnerCtx, t, instancePermissionV2.DefaultOrg.GetId(), gofakeit.AppName(), false, false) + + request.ProjectId = projectResp.GetId() + addProjectRole(iamOwnerCtx, instancePermissionV2, t, projectResp.GetId()) + }, + req: &project.ListProjectRolesRequest{ + Filters: []*project.ProjectRoleSearchFilter{{}}, + }, + }, + wantErr: true, + }, + { + name: "list, not found", + args: args{ + ctx: iamOwnerCtx, + req: &project.ListProjectRolesRequest{ + ProjectId: "notfound", + }, + }, + want: &project.ListProjectRolesResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 0, + AppliedLimit: 100, + }, + }, + }, + { + name: "list single id, missing permission", + args: args{ + ctx: instancePermissionV2.WithAuthorization(CTX, integration.UserTypeOrgOwner), + dep: func(request *project.ListProjectRolesRequest, response *project.ListProjectRolesResponse) { + orgResp := instancePermissionV2.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + projectResp := instancePermissionV2.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + + request.ProjectId = projectResp.GetId() + addProjectRole(iamOwnerCtx, instancePermissionV2, t, projectResp.GetId()) + }, + req: &project.ListProjectRolesRequest{}, + }, + want: &project.ListProjectRolesResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 0, + AppliedLimit: 100, + }, + ProjectRoles: []*project.ProjectRole{}, + }, + }, + { + name: "list single id", + args: args{ + ctx: instancePermissionV2.WithAuthorization(CTX, integration.UserTypeOrgOwner), + dep: func(request *project.ListProjectRolesRequest, response *project.ListProjectRolesResponse) { + orgID := instancePermissionV2.DefaultOrg.GetId() + projectResp := instancePermissionV2.CreateProject(iamOwnerCtx, t, orgID, gofakeit.AppName(), false, false) + + request.ProjectId = projectResp.GetId() + response.ProjectRoles[0] = addProjectRole(iamOwnerCtx, instancePermissionV2, t, projectResp.GetId()) + }, + req: &project.ListProjectRolesRequest{}, + }, + want: &project.ListProjectRolesResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + ProjectRoles: []*project.ProjectRole{ + {}, + }, + }, + }, + { + name: "list multiple id", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *project.ListProjectRolesRequest, response *project.ListProjectRolesResponse) { + orgID := instancePermissionV2.DefaultOrg.GetId() + projectResp := instancePermissionV2.CreateProject(iamOwnerCtx, t, orgID, gofakeit.AppName(), false, false) + + request.ProjectId = projectResp.GetId() + response.ProjectRoles[2] = addProjectRole(iamOwnerCtx, instancePermissionV2, t, projectResp.GetId()) + response.ProjectRoles[1] = addProjectRole(iamOwnerCtx, instancePermissionV2, t, projectResp.GetId()) + response.ProjectRoles[0] = addProjectRole(iamOwnerCtx, instancePermissionV2, t, projectResp.GetId()) + }, + req: &project.ListProjectRolesRequest{}, + }, + want: &project.ListProjectRolesResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 3, + AppliedLimit: 100, + }, + ProjectRoles: []*project.ProjectRole{ + {}, {}, {}, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.args.dep != nil { + tt.args.dep(tt.args.req, tt.want) + } + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(iamOwnerCtx, time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + got, listErr := instancePermissionV2.Client.Projectv2Beta.ListProjectRoles(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(ttt, listErr, "Error: "+listErr.Error()) + return + } + require.NoError(ttt, listErr) + + // always first check length, otherwise its failed anyway + if assert.Len(ttt, got.ProjectRoles, len(tt.want.ProjectRoles)) { + for i := range tt.want.ProjectRoles { + assert.EqualExportedValues(ttt, tt.want.ProjectRoles[i], got.ProjectRoles[i]) + } + } + assertPaginationResponse(ttt, tt.want.Pagination, got.Pagination) + }, retryDuration, tick, "timeout waiting for expected execution result") + }) + } +} + +func addProjectRole(ctx context.Context, instance *integration.Instance, t *testing.T, projectID string) *project.ProjectRole { + name := gofakeit.Animal() + projectRoleResp := instance.AddProjectRole(ctx, t, projectID, name, name, name) + + return &project.ProjectRole{ + ProjectId: projectID, + CreationDate: projectRoleResp.GetCreationDate(), + ChangeDate: projectRoleResp.GetCreationDate(), + Key: name, + DisplayName: name, + Group: name, + } +} diff --git a/internal/api/grpc/project/v2beta/integration/server_test.go b/internal/api/grpc/project/v2beta/integration/server_test.go new file mode 100644 index 0000000000..59d9745222 --- /dev/null +++ b/internal/api/grpc/project/v2beta/integration/server_test.go @@ -0,0 +1,63 @@ +//go:build integration + +package project_test + +import ( + "context" + "os" + "testing" + "time" + + "github.com/muhlemmer/gu" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/zitadel/zitadel/internal/integration" + "github.com/zitadel/zitadel/pkg/grpc/feature/v2" +) + +var ( + CTX context.Context + instance *integration.Instance + instancePermissionV2 *integration.Instance +) + +func TestMain(m *testing.M) { + os.Exit(func() int { + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute) + defer cancel() + CTX = ctx + instance = integration.NewInstance(ctx) + instancePermissionV2 = integration.NewInstance(CTX) + return m.Run() + }()) +} + +func ensureFeaturePermissionV2Enabled(t *testing.T, instance *integration.Instance) { + ctx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + f, err := instance.Client.FeatureV2.GetInstanceFeatures(ctx, &feature.GetInstanceFeaturesRequest{ + Inheritance: true, + }) + require.NoError(t, err) + if f.PermissionCheckV2.GetEnabled() { + return + } + _, err = instance.Client.FeatureV2.SetInstanceFeatures(ctx, &feature.SetInstanceFeaturesRequest{ + PermissionCheckV2: gu.Ptr(true), + }) + require.NoError(t, err) + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(ctx, 5*time.Minute) + require.EventuallyWithT(t, + func(ttt *assert.CollectT) { + f, err := instance.Client.FeatureV2.GetInstanceFeatures(ctx, &feature.GetInstanceFeaturesRequest{ + Inheritance: true, + }) + assert.NoError(ttt, err) + if f.PermissionCheckV2.GetEnabled() { + return + } + }, + retryDuration, + tick, + "timed out waiting for ensuring instance feature") +} diff --git a/internal/api/grpc/project/v2beta/project.go b/internal/api/grpc/project/v2beta/project.go new file mode 100644 index 0000000000..01b478f5be --- /dev/null +++ b/internal/api/grpc/project/v2beta/project.go @@ -0,0 +1,161 @@ +package project + +import ( + "context" + + "github.com/muhlemmer/gu" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/eventstore/v1/models" + "github.com/zitadel/zitadel/internal/query" + project_pb "github.com/zitadel/zitadel/pkg/grpc/project/v2beta" +) + +func (s *Server) CreateProject(ctx context.Context, req *project_pb.CreateProjectRequest) (*project_pb.CreateProjectResponse, error) { + add := projectCreateToCommand(req) + project, err := s.command.AddProject(ctx, add) + if err != nil { + return nil, err + } + var creationDate *timestamppb.Timestamp + if !project.EventDate.IsZero() { + creationDate = timestamppb.New(project.EventDate) + } + return &project_pb.CreateProjectResponse{ + Id: add.AggregateID, + CreationDate: creationDate, + }, nil +} + +func projectCreateToCommand(req *project_pb.CreateProjectRequest) *command.AddProject { + var aggregateID string + if req.Id != nil { + aggregateID = *req.Id + } + return &command.AddProject{ + ObjectRoot: models.ObjectRoot{ + ResourceOwner: req.OrganizationId, + AggregateID: aggregateID, + }, + Name: req.Name, + ProjectRoleAssertion: req.ProjectRoleAssertion, + ProjectRoleCheck: req.AuthorizationRequired, + HasProjectCheck: req.ProjectAccessRequired, + PrivateLabelingSetting: privateLabelingSettingToDomain(req.PrivateLabelingSetting), + } +} + +func privateLabelingSettingToDomain(setting project_pb.PrivateLabelingSetting) domain.PrivateLabelingSetting { + switch setting { + case project_pb.PrivateLabelingSetting_PRIVATE_LABELING_SETTING_ALLOW_LOGIN_USER_RESOURCE_OWNER_POLICY: + return domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy + case project_pb.PrivateLabelingSetting_PRIVATE_LABELING_SETTING_ENFORCE_PROJECT_RESOURCE_OWNER_POLICY: + return domain.PrivateLabelingSettingEnforceProjectResourceOwnerPolicy + case project_pb.PrivateLabelingSetting_PRIVATE_LABELING_SETTING_UNSPECIFIED: + return domain.PrivateLabelingSettingUnspecified + default: + return domain.PrivateLabelingSettingUnspecified + } +} + +func (s *Server) UpdateProject(ctx context.Context, req *project_pb.UpdateProjectRequest) (*project_pb.UpdateProjectResponse, error) { + project, err := s.command.ChangeProject(ctx, projectUpdateToCommand(req)) + if err != nil { + return nil, err + } + var changeDate *timestamppb.Timestamp + if !project.EventDate.IsZero() { + changeDate = timestamppb.New(project.EventDate) + } + return &project_pb.UpdateProjectResponse{ + ChangeDate: changeDate, + }, nil +} + +func projectUpdateToCommand(req *project_pb.UpdateProjectRequest) *command.ChangeProject { + var labeling *domain.PrivateLabelingSetting + if req.PrivateLabelingSetting != nil { + labeling = gu.Ptr(privateLabelingSettingToDomain(*req.PrivateLabelingSetting)) + } + return &command.ChangeProject{ + ObjectRoot: models.ObjectRoot{ + AggregateID: req.Id, + }, + Name: req.Name, + ProjectRoleAssertion: req.ProjectRoleAssertion, + ProjectRoleCheck: req.ProjectRoleCheck, + HasProjectCheck: req.HasProjectCheck, + PrivateLabelingSetting: labeling, + } +} + +func (s *Server) DeleteProject(ctx context.Context, req *project_pb.DeleteProjectRequest) (*project_pb.DeleteProjectResponse, error) { + userGrantIDs, err := s.userGrantsFromProject(ctx, req.Id) + if err != nil { + return nil, err + } + + deletedAt, err := s.command.DeleteProject(ctx, req.Id, "", userGrantIDs...) + if err != nil { + return nil, err + } + var deletionDate *timestamppb.Timestamp + if !deletedAt.IsZero() { + deletionDate = timestamppb.New(deletedAt) + } + return &project_pb.DeleteProjectResponse{ + DeletionDate: deletionDate, + }, nil +} + +func (s *Server) userGrantsFromProject(ctx context.Context, projectID string) ([]string, error) { + projectQuery, err := query.NewUserGrantProjectIDSearchQuery(projectID) + if err != nil { + return nil, err + } + userGrants, err := s.query.UserGrants(ctx, &query.UserGrantsQueries{ + Queries: []query.SearchQuery{projectQuery}, + }, false) + if err != nil { + return nil, err + } + return userGrantsToIDs(userGrants.UserGrants), nil +} + +func (s *Server) DeactivateProject(ctx context.Context, req *project_pb.DeactivateProjectRequest) (*project_pb.DeactivateProjectResponse, error) { + details, err := s.command.DeactivateProject(ctx, req.Id, "") + if err != nil { + return nil, err + } + var changeDate *timestamppb.Timestamp + if !details.EventDate.IsZero() { + changeDate = timestamppb.New(details.EventDate) + } + return &project_pb.DeactivateProjectResponse{ + ChangeDate: changeDate, + }, nil +} + +func (s *Server) ActivateProject(ctx context.Context, req *project_pb.ActivateProjectRequest) (*project_pb.ActivateProjectResponse, error) { + details, err := s.command.ReactivateProject(ctx, req.Id, "") + if err != nil { + return nil, err + } + var changeDate *timestamppb.Timestamp + if !details.EventDate.IsZero() { + changeDate = timestamppb.New(details.EventDate) + } + return &project_pb.ActivateProjectResponse{ + ChangeDate: changeDate, + }, nil +} + +func userGrantsToIDs(userGrants []*query.UserGrant) []string { + converted := make([]string, len(userGrants)) + for i, grant := range userGrants { + converted[i] = grant.ID + } + return converted +} diff --git a/internal/api/grpc/project/v2beta/project_grant.go b/internal/api/grpc/project/v2beta/project_grant.go new file mode 100644 index 0000000000..c1c20d9cbc --- /dev/null +++ b/internal/api/grpc/project/v2beta/project_grant.go @@ -0,0 +1,126 @@ +package project + +import ( + "context" + + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/eventstore/v1/models" + "github.com/zitadel/zitadel/internal/query" + project_pb "github.com/zitadel/zitadel/pkg/grpc/project/v2beta" +) + +func (s *Server) CreateProjectGrant(ctx context.Context, req *project_pb.CreateProjectGrantRequest) (*project_pb.CreateProjectGrantResponse, error) { + add := projectGrantCreateToCommand(req) + project, err := s.command.AddProjectGrant(ctx, add) + if err != nil { + return nil, err + } + var creationDate *timestamppb.Timestamp + if !project.EventDate.IsZero() { + creationDate = timestamppb.New(project.EventDate) + } + return &project_pb.CreateProjectGrantResponse{ + CreationDate: creationDate, + }, nil +} + +func projectGrantCreateToCommand(req *project_pb.CreateProjectGrantRequest) *command.AddProjectGrant { + return &command.AddProjectGrant{ + ObjectRoot: models.ObjectRoot{ + AggregateID: req.ProjectId, + }, + GrantID: req.GrantedOrganizationId, + GrantedOrgID: req.GrantedOrganizationId, + RoleKeys: req.RoleKeys, + } +} + +func (s *Server) UpdateProjectGrant(ctx context.Context, req *project_pb.UpdateProjectGrantRequest) (*project_pb.UpdateProjectGrantResponse, error) { + project, err := s.command.ChangeProjectGrant(ctx, projectGrantUpdateToCommand(req)) + if err != nil { + return nil, err + } + var changeDate *timestamppb.Timestamp + if !project.EventDate.IsZero() { + changeDate = timestamppb.New(project.EventDate) + } + return &project_pb.UpdateProjectGrantResponse{ + ChangeDate: changeDate, + }, nil +} + +func projectGrantUpdateToCommand(req *project_pb.UpdateProjectGrantRequest) *command.ChangeProjectGrant { + return &command.ChangeProjectGrant{ + ObjectRoot: models.ObjectRoot{ + AggregateID: req.ProjectId, + }, + GrantID: req.GrantedOrganizationId, + RoleKeys: req.RoleKeys, + } +} + +func (s *Server) DeactivateProjectGrant(ctx context.Context, req *project_pb.DeactivateProjectGrantRequest) (*project_pb.DeactivateProjectGrantResponse, error) { + details, err := s.command.DeactivateProjectGrant(ctx, req.ProjectId, req.GrantedOrganizationId, "") + if err != nil { + return nil, err + } + var changeDate *timestamppb.Timestamp + if !details.EventDate.IsZero() { + changeDate = timestamppb.New(details.EventDate) + } + return &project_pb.DeactivateProjectGrantResponse{ + ChangeDate: changeDate, + }, nil +} + +func (s *Server) ActivateProjectGrant(ctx context.Context, req *project_pb.ActivateProjectGrantRequest) (*project_pb.ActivateProjectGrantResponse, error) { + details, err := s.command.ReactivateProjectGrant(ctx, req.ProjectId, req.GrantedOrganizationId, "") + if err != nil { + return nil, err + } + var changeDate *timestamppb.Timestamp + if !details.EventDate.IsZero() { + changeDate = timestamppb.New(details.EventDate) + } + return &project_pb.ActivateProjectGrantResponse{ + ChangeDate: changeDate, + }, nil +} + +func (s *Server) DeleteProjectGrant(ctx context.Context, req *project_pb.DeleteProjectGrantRequest) (*project_pb.DeleteProjectGrantResponse, error) { + userGrantIDs, err := s.userGrantsFromProjectGrant(ctx, req.ProjectId, req.GrantedOrganizationId) + if err != nil { + return nil, err + } + details, err := s.command.RemoveProjectGrant(ctx, req.ProjectId, req.GrantedOrganizationId, "", userGrantIDs...) + if err != nil { + return nil, err + } + var deletionDate *timestamppb.Timestamp + if !details.EventDate.IsZero() { + deletionDate = timestamppb.New(details.EventDate) + } + return &project_pb.DeleteProjectGrantResponse{ + DeletionDate: deletionDate, + }, nil +} + +func (s *Server) userGrantsFromProjectGrant(ctx context.Context, projectID, grantedOrganizationID string) ([]string, error) { + projectQuery, err := query.NewUserGrantProjectIDSearchQuery(projectID) + if err != nil { + return nil, err + } + grantQuery, err := query.NewUserGrantGrantIDSearchQuery(grantedOrganizationID) + if err != nil { + return nil, err + } + userGrants, err := s.query.UserGrants(ctx, &query.UserGrantsQueries{ + Queries: []query.SearchQuery{projectQuery, grantQuery}, + }, false) + if err != nil { + return nil, err + } + return userGrantsToIDs(userGrants.UserGrants), nil +} diff --git a/internal/api/grpc/project/v2beta/project_role.go b/internal/api/grpc/project/v2beta/project_role.go new file mode 100644 index 0000000000..07fc4e9eac --- /dev/null +++ b/internal/api/grpc/project/v2beta/project_role.go @@ -0,0 +1,132 @@ +package project + +import ( + "context" + + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/eventstore/v1/models" + "github.com/zitadel/zitadel/internal/query" + project_pb "github.com/zitadel/zitadel/pkg/grpc/project/v2beta" +) + +func (s *Server) AddProjectRole(ctx context.Context, req *project_pb.AddProjectRoleRequest) (*project_pb.AddProjectRoleResponse, error) { + role, err := s.command.AddProjectRole(ctx, addProjectRoleRequestToCommand(req)) + if err != nil { + return nil, err + } + var creationDate *timestamppb.Timestamp + if !role.EventDate.IsZero() { + creationDate = timestamppb.New(role.EventDate) + } + return &project_pb.AddProjectRoleResponse{ + CreationDate: creationDate, + }, nil +} + +func addProjectRoleRequestToCommand(req *project_pb.AddProjectRoleRequest) *command.AddProjectRole { + group := "" + if req.Group != nil { + group = *req.Group + } + + return &command.AddProjectRole{ + ObjectRoot: models.ObjectRoot{ + AggregateID: req.ProjectId, + }, + Key: req.RoleKey, + DisplayName: req.DisplayName, + Group: group, + } +} + +func (s *Server) UpdateProjectRole(ctx context.Context, req *project_pb.UpdateProjectRoleRequest) (*project_pb.UpdateProjectRoleResponse, error) { + role, err := s.command.ChangeProjectRole(ctx, updateProjectRoleRequestToCommand(req)) + if err != nil { + return nil, err + } + var changeDate *timestamppb.Timestamp + if !role.EventDate.IsZero() { + changeDate = timestamppb.New(role.EventDate) + } + return &project_pb.UpdateProjectRoleResponse{ + ChangeDate: changeDate, + }, nil +} + +func updateProjectRoleRequestToCommand(req *project_pb.UpdateProjectRoleRequest) *command.ChangeProjectRole { + displayName := "" + if req.DisplayName != nil { + displayName = *req.DisplayName + } + group := "" + if req.Group != nil { + group = *req.Group + } + + return &command.ChangeProjectRole{ + ObjectRoot: models.ObjectRoot{ + AggregateID: req.ProjectId, + }, + Key: req.RoleKey, + DisplayName: displayName, + Group: group, + } +} + +func (s *Server) RemoveProjectRole(ctx context.Context, req *project_pb.RemoveProjectRoleRequest) (*project_pb.RemoveProjectRoleResponse, error) { + userGrantIDs, err := s.userGrantsFromProjectAndRole(ctx, req.ProjectId, req.RoleKey) + if err != nil { + return nil, err + } + projectGrantIDs, err := s.projectGrantsFromProjectAndRole(ctx, req.ProjectId, req.RoleKey) + if err != nil { + return nil, err + } + details, err := s.command.RemoveProjectRole(ctx, req.ProjectId, req.RoleKey, "", projectGrantIDs, userGrantIDs...) + if err != nil { + return nil, err + } + var deletionDate *timestamppb.Timestamp + if !details.EventDate.IsZero() { + deletionDate = timestamppb.New(details.EventDate) + } + return &project_pb.RemoveProjectRoleResponse{ + RemovalDate: deletionDate, + }, nil +} + +func (s *Server) userGrantsFromProjectAndRole(ctx context.Context, projectID, roleKey string) ([]string, error) { + projectQuery, err := query.NewUserGrantProjectIDSearchQuery(projectID) + if err != nil { + return nil, err + } + rolesQuery, err := query.NewUserGrantRoleQuery(roleKey) + if err != nil { + return nil, err + } + userGrants, err := s.query.UserGrants(ctx, &query.UserGrantsQueries{ + Queries: []query.SearchQuery{projectQuery, rolesQuery}, + }, false) + if err != nil { + return nil, err + } + return userGrantsToIDs(userGrants.UserGrants), nil +} + +func (s *Server) projectGrantsFromProjectAndRole(ctx context.Context, projectID, roleKey string) ([]string, error) { + projectGrants, err := s.query.SearchProjectGrantsByProjectIDAndRoleKey(ctx, projectID, roleKey) + if err != nil { + return nil, err + } + return projectGrantsToIDs(projectGrants), nil +} + +func projectGrantsToIDs(projectGrants *query.ProjectGrants) []string { + converted := make([]string, len(projectGrants.ProjectGrants)) + for i, grant := range projectGrants.ProjectGrants { + converted[i] = grant.GrantID + } + return converted +} diff --git a/internal/api/grpc/project/v2beta/query.go b/internal/api/grpc/project/v2beta/query.go new file mode 100644 index 0000000000..1cdf9eefbd --- /dev/null +++ b/internal/api/grpc/project/v2beta/query.go @@ -0,0 +1,427 @@ +package project + +import ( + "context" + + "google.golang.org/protobuf/types/known/timestamppb" + + filter "github.com/zitadel/zitadel/internal/api/grpc/filter/v2beta" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/query" + "github.com/zitadel/zitadel/internal/zerrors" + project_pb "github.com/zitadel/zitadel/pkg/grpc/project/v2beta" +) + +func (s *Server) GetProject(ctx context.Context, req *project_pb.GetProjectRequest) (*project_pb.GetProjectResponse, error) { + project, err := s.query.GetProjectByIDWithPermission(ctx, true, req.Id, s.checkPermission) + if err != nil { + return nil, err + } + return &project_pb.GetProjectResponse{ + Project: projectToPb(project), + }, nil +} + +func (s *Server) ListProjects(ctx context.Context, req *project_pb.ListProjectsRequest) (*project_pb.ListProjectsResponse, error) { + queries, err := s.listProjectRequestToModel(req) + if err != nil { + return nil, err + } + resp, err := s.query.SearchGrantedProjects(ctx, queries, s.checkPermission) + if err != nil { + return nil, err + } + return &project_pb.ListProjectsResponse{ + Projects: grantedProjectsToPb(resp.GrantedProjects), + Pagination: filter.QueryToPaginationPb(queries.SearchRequest, resp.SearchResponse), + }, nil +} + +func (s *Server) listProjectRequestToModel(req *project_pb.ListProjectsRequest) (*query.ProjectAndGrantedProjectSearchQueries, error) { + offset, limit, asc, err := filter.PaginationPbToQuery(s.systemDefaults, req.Pagination) + if err != nil { + return nil, err + } + queries, err := projectFiltersToQuery(req.Filters) + if err != nil { + return nil, err + } + return &query.ProjectAndGrantedProjectSearchQueries{ + SearchRequest: query.SearchRequest{ + Offset: offset, + Limit: limit, + Asc: asc, + SortingColumn: grantedProjectFieldNameToSortingColumn(req.SortingColumn), + }, + Queries: queries, + }, nil +} + +func grantedProjectFieldNameToSortingColumn(field *project_pb.ProjectFieldName) query.Column { + if field == nil { + return query.GrantedProjectColumnCreationDate + } + switch *field { + case project_pb.ProjectFieldName_PROJECT_FIELD_NAME_CREATION_DATE: + return query.GrantedProjectColumnCreationDate + case project_pb.ProjectFieldName_PROJECT_FIELD_NAME_ID: + return query.GrantedProjectColumnID + case project_pb.ProjectFieldName_PROJECT_FIELD_NAME_NAME: + return query.GrantedProjectColumnName + case project_pb.ProjectFieldName_PROJECT_FIELD_NAME_CHANGE_DATE: + return query.GrantedProjectColumnChangeDate + case project_pb.ProjectFieldName_PROJECT_FIELD_NAME_UNSPECIFIED: + return query.GrantedProjectColumnCreationDate + default: + return query.GrantedProjectColumnCreationDate + } +} + +func projectFiltersToQuery(queries []*project_pb.ProjectSearchFilter) (_ []query.SearchQuery, err error) { + q := make([]query.SearchQuery, len(queries)) + for i, qry := range queries { + q[i], err = projectFilterToModel(qry) + if err != nil { + return nil, err + } + } + return q, nil +} + +func projectFilterToModel(filter *project_pb.ProjectSearchFilter) (query.SearchQuery, error) { + switch q := filter.Filter.(type) { + case *project_pb.ProjectSearchFilter_ProjectNameFilter: + return projectNameFilterToQuery(q.ProjectNameFilter) + case *project_pb.ProjectSearchFilter_InProjectIdsFilter: + return projectInIDsFilterToQuery(q.InProjectIdsFilter) + case *project_pb.ProjectSearchFilter_ProjectResourceOwnerFilter: + return projectResourceOwnerFilterToQuery(q.ProjectResourceOwnerFilter) + case *project_pb.ProjectSearchFilter_ProjectOrganizationIdFilter: + return projectOrganizationIDFilterToQuery(q.ProjectOrganizationIdFilter) + case *project_pb.ProjectSearchFilter_ProjectGrantResourceOwnerFilter: + return projectGrantResourceOwnerFilterToQuery(q.ProjectGrantResourceOwnerFilter) + default: + return nil, zerrors.ThrowInvalidArgument(nil, "ORG-vR9nC", "List.Query.Invalid") + } +} + +func projectNameFilterToQuery(q *project_pb.ProjectNameFilter) (query.SearchQuery, error) { + return query.NewGrantedProjectNameSearchQuery(filter.TextMethodPbToQuery(q.Method), q.GetProjectName()) +} + +func projectInIDsFilterToQuery(q *project_pb.InProjectIDsFilter) (query.SearchQuery, error) { + return query.NewGrantedProjectIDSearchQuery(q.ProjectIds) +} + +func projectResourceOwnerFilterToQuery(q *project_pb.ProjectResourceOwnerFilter) (query.SearchQuery, error) { + return query.NewGrantedProjectResourceOwnerSearchQuery(q.ProjectResourceOwner) +} + +func projectOrganizationIDFilterToQuery(q *project_pb.ProjectOrganizationIDFilter) (query.SearchQuery, error) { + return query.NewGrantedProjectOrganizationIDSearchQuery(q.ProjectOrganizationId) +} + +func projectGrantResourceOwnerFilterToQuery(q *project_pb.ProjectGrantResourceOwnerFilter) (query.SearchQuery, error) { + return query.NewGrantedProjectGrantResourceOwnerSearchQuery(q.ProjectGrantResourceOwner) +} + +func grantedProjectsToPb(projects []*query.GrantedProject) []*project_pb.Project { + o := make([]*project_pb.Project, len(projects)) + for i, org := range projects { + o[i] = grantedProjectToPb(org) + } + return o +} + +func projectToPb(project *query.Project) *project_pb.Project { + return &project_pb.Project{ + Id: project.ID, + OrganizationId: project.ResourceOwner, + CreationDate: timestamppb.New(project.CreationDate), + ChangeDate: timestamppb.New(project.ChangeDate), + State: projectStateToPb(project.State), + Name: project.Name, + PrivateLabelingSetting: privateLabelingSettingToPb(project.PrivateLabelingSetting), + ProjectAccessRequired: project.HasProjectCheck, + ProjectRoleAssertion: project.ProjectRoleAssertion, + AuthorizationRequired: project.ProjectRoleCheck, + } +} + +func grantedProjectToPb(project *query.GrantedProject) *project_pb.Project { + var grantedOrganizationID, grantedOrganizationName *string + if project.GrantedOrgID != "" { + grantedOrganizationID = &project.GrantedOrgID + } + if project.OrgName != "" { + grantedOrganizationName = &project.OrgName + } + + return &project_pb.Project{ + Id: project.ProjectID, + OrganizationId: project.ResourceOwner, + CreationDate: timestamppb.New(project.CreationDate), + ChangeDate: timestamppb.New(project.ChangeDate), + State: projectStateToPb(project.ProjectState), + Name: project.ProjectName, + PrivateLabelingSetting: privateLabelingSettingToPb(project.PrivateLabelingSetting), + ProjectAccessRequired: project.HasProjectCheck, + ProjectRoleAssertion: project.ProjectRoleAssertion, + AuthorizationRequired: project.ProjectRoleCheck, + GrantedOrganizationId: grantedOrganizationID, + GrantedOrganizationName: grantedOrganizationName, + GrantedState: grantedProjectStateToPb(project.ProjectGrantState), + } +} + +func projectStateToPb(state domain.ProjectState) project_pb.ProjectState { + switch state { + case domain.ProjectStateActive: + return project_pb.ProjectState_PROJECT_STATE_ACTIVE + case domain.ProjectStateInactive: + return project_pb.ProjectState_PROJECT_STATE_INACTIVE + case domain.ProjectStateUnspecified, domain.ProjectStateRemoved: + return project_pb.ProjectState_PROJECT_STATE_UNSPECIFIED + default: + return project_pb.ProjectState_PROJECT_STATE_UNSPECIFIED + } +} +func grantedProjectStateToPb(state domain.ProjectGrantState) project_pb.GrantedProjectState { + switch state { + case domain.ProjectGrantStateActive: + return project_pb.GrantedProjectState_GRANTED_PROJECT_STATE_ACTIVE + case domain.ProjectGrantStateInactive: + return project_pb.GrantedProjectState_GRANTED_PROJECT_STATE_INACTIVE + case domain.ProjectGrantStateUnspecified, domain.ProjectGrantStateRemoved: + return project_pb.GrantedProjectState_GRANTED_PROJECT_STATE_UNSPECIFIED + default: + return project_pb.GrantedProjectState_GRANTED_PROJECT_STATE_UNSPECIFIED + } +} + +func privateLabelingSettingToPb(setting domain.PrivateLabelingSetting) project_pb.PrivateLabelingSetting { + switch setting { + case domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy: + return project_pb.PrivateLabelingSetting_PRIVATE_LABELING_SETTING_ALLOW_LOGIN_USER_RESOURCE_OWNER_POLICY + case domain.PrivateLabelingSettingEnforceProjectResourceOwnerPolicy: + return project_pb.PrivateLabelingSetting_PRIVATE_LABELING_SETTING_ENFORCE_PROJECT_RESOURCE_OWNER_POLICY + case domain.PrivateLabelingSettingUnspecified: + return project_pb.PrivateLabelingSetting_PRIVATE_LABELING_SETTING_UNSPECIFIED + default: + return project_pb.PrivateLabelingSetting_PRIVATE_LABELING_SETTING_UNSPECIFIED + } +} + +func (s *Server) ListProjectGrants(ctx context.Context, req *project_pb.ListProjectGrantsRequest) (*project_pb.ListProjectGrantsResponse, error) { + queries, err := s.listProjectGrantsRequestToModel(req) + if err != nil { + return nil, err + } + resp, err := s.query.SearchProjectGrants(ctx, queries, s.checkPermission) + if err != nil { + return nil, err + } + return &project_pb.ListProjectGrantsResponse{ + ProjectGrants: projectGrantsToPb(resp.ProjectGrants), + Pagination: filter.QueryToPaginationPb(queries.SearchRequest, resp.SearchResponse), + }, nil +} + +func (s *Server) listProjectGrantsRequestToModel(req *project_pb.ListProjectGrantsRequest) (*query.ProjectGrantSearchQueries, error) { + offset, limit, asc, err := filter.PaginationPbToQuery(s.systemDefaults, req.Pagination) + if err != nil { + return nil, err + } + queries, err := projectGrantFiltersToModel(req.Filters) + if err != nil { + return nil, err + } + return &query.ProjectGrantSearchQueries{ + SearchRequest: query.SearchRequest{ + Offset: offset, + Limit: limit, + Asc: asc, + SortingColumn: projectGrantFieldNameToSortingColumn(req.SortingColumn), + }, + Queries: queries, + }, nil +} + +func projectGrantFieldNameToSortingColumn(field *project_pb.ProjectGrantFieldName) query.Column { + if field == nil { + return query.ProjectGrantColumnCreationDate + } + switch *field { + case project_pb.ProjectGrantFieldName_PROJECT_GRANT_FIELD_NAME_PROJECT_ID: + return query.ProjectGrantColumnProjectID + case project_pb.ProjectGrantFieldName_PROJECT_GRANT_FIELD_NAME_CREATION_DATE: + return query.ProjectGrantColumnCreationDate + case project_pb.ProjectGrantFieldName_PROJECT_GRANT_FIELD_NAME_CHANGE_DATE: + return query.ProjectGrantColumnChangeDate + case project_pb.ProjectGrantFieldName_PROJECT_GRANT_FIELD_NAME_UNSPECIFIED: + return query.ProjectGrantColumnCreationDate + default: + return query.ProjectGrantColumnCreationDate + } +} + +func projectGrantFiltersToModel(queries []*project_pb.ProjectGrantSearchFilter) (_ []query.SearchQuery, err error) { + q := make([]query.SearchQuery, len(queries)) + for i, qry := range queries { + q[i], err = projectGrantFilterToModel(qry) + if err != nil { + return nil, err + } + } + return q, nil +} + +func projectGrantFilterToModel(filter *project_pb.ProjectGrantSearchFilter) (query.SearchQuery, error) { + switch q := filter.Filter.(type) { + case *project_pb.ProjectGrantSearchFilter_ProjectNameFilter: + return projectNameFilterToQuery(q.ProjectNameFilter) + case *project_pb.ProjectGrantSearchFilter_RoleKeyFilter: + return query.NewProjectGrantRoleKeySearchQuery(q.RoleKeyFilter.Key) + case *project_pb.ProjectGrantSearchFilter_InProjectIdsFilter: + return query.NewProjectGrantProjectIDsSearchQuery(q.InProjectIdsFilter.ProjectIds) + case *project_pb.ProjectGrantSearchFilter_ProjectResourceOwnerFilter: + return query.NewProjectGrantResourceOwnerSearchQuery(q.ProjectResourceOwnerFilter.ProjectResourceOwner) + case *project_pb.ProjectGrantSearchFilter_ProjectGrantResourceOwnerFilter: + return query.NewProjectGrantGrantedOrgIDSearchQuery(q.ProjectGrantResourceOwnerFilter.ProjectGrantResourceOwner) + default: + return nil, zerrors.ThrowInvalidArgument(nil, "PROJECT-M099f", "List.Query.Invalid") + } +} + +func projectGrantsToPb(projects []*query.ProjectGrant) []*project_pb.ProjectGrant { + p := make([]*project_pb.ProjectGrant, len(projects)) + for i, project := range projects { + p[i] = projectGrantToPb(project) + } + return p +} + +func projectGrantToPb(project *query.ProjectGrant) *project_pb.ProjectGrant { + return &project_pb.ProjectGrant{ + OrganizationId: project.ResourceOwner, + CreationDate: timestamppb.New(project.CreationDate), + ChangeDate: timestamppb.New(project.ChangeDate), + GrantedOrganizationId: project.GrantedOrgID, + GrantedOrganizationName: project.OrgName, + GrantedRoleKeys: project.GrantedRoleKeys, + ProjectId: project.ProjectID, + ProjectName: project.ProjectName, + State: projectGrantStateToPb(project.State), + } +} + +func projectGrantStateToPb(state domain.ProjectGrantState) project_pb.ProjectGrantState { + switch state { + case domain.ProjectGrantStateActive: + return project_pb.ProjectGrantState_PROJECT_GRANT_STATE_ACTIVE + case domain.ProjectGrantStateInactive: + return project_pb.ProjectGrantState_PROJECT_GRANT_STATE_INACTIVE + case domain.ProjectGrantStateUnspecified, domain.ProjectGrantStateRemoved: + return project_pb.ProjectGrantState_PROJECT_GRANT_STATE_UNSPECIFIED + default: + return project_pb.ProjectGrantState_PROJECT_GRANT_STATE_UNSPECIFIED + } +} + +func (s *Server) ListProjectRoles(ctx context.Context, req *project_pb.ListProjectRolesRequest) (*project_pb.ListProjectRolesResponse, error) { + queries, err := s.listProjectRolesRequestToModel(req) + if err != nil { + return nil, err + } + err = queries.AppendProjectIDQuery(req.ProjectId) + if err != nil { + return nil, err + } + roles, err := s.query.SearchProjectRoles(ctx, true, queries, s.checkPermission) + if err != nil { + return nil, err + } + return &project_pb.ListProjectRolesResponse{ + ProjectRoles: roleViewsToPb(roles.ProjectRoles), + Pagination: filter.QueryToPaginationPb(queries.SearchRequest, roles.SearchResponse), + }, nil +} + +func (s *Server) listProjectRolesRequestToModel(req *project_pb.ListProjectRolesRequest) (*query.ProjectRoleSearchQueries, error) { + offset, limit, asc, err := filter.PaginationPbToQuery(s.systemDefaults, req.Pagination) + if err != nil { + return nil, err + } + queries, err := roleQueriesToModel(req.Filters) + if err != nil { + return nil, err + } + return &query.ProjectRoleSearchQueries{ + SearchRequest: query.SearchRequest{ + Offset: offset, + Limit: limit, + Asc: asc, + SortingColumn: projectRoleFieldNameToSortingColumn(req.SortingColumn), + }, + Queries: queries, + }, nil +} + +func projectRoleFieldNameToSortingColumn(field *project_pb.ProjectRoleFieldName) query.Column { + if field == nil { + return query.ProjectRoleColumnCreationDate + } + switch *field { + case project_pb.ProjectRoleFieldName_PROJECT_ROLE_FIELD_NAME_KEY: + return query.ProjectRoleColumnKey + case project_pb.ProjectRoleFieldName_PROJECT_ROLE_FIELD_NAME_CREATION_DATE: + return query.ProjectRoleColumnCreationDate + case project_pb.ProjectRoleFieldName_PROJECT_ROLE_FIELD_NAME_CHANGE_DATE: + return query.ProjectRoleColumnChangeDate + case project_pb.ProjectRoleFieldName_PROJECT_ROLE_FIELD_NAME_UNSPECIFIED: + return query.ProjectRoleColumnCreationDate + default: + return query.ProjectRoleColumnCreationDate + } +} + +func roleQueriesToModel(queries []*project_pb.ProjectRoleSearchFilter) (_ []query.SearchQuery, err error) { + q := make([]query.SearchQuery, len(queries)) + for i, query := range queries { + q[i], err = roleQueryToModel(query) + if err != nil { + return nil, err + } + } + return q, nil +} + +func roleQueryToModel(apiQuery *project_pb.ProjectRoleSearchFilter) (query.SearchQuery, error) { + switch q := apiQuery.Filter.(type) { + case *project_pb.ProjectRoleSearchFilter_RoleKeyFilter: + return query.NewProjectRoleKeySearchQuery(filter.TextMethodPbToQuery(q.RoleKeyFilter.Method), q.RoleKeyFilter.Key) + case *project_pb.ProjectRoleSearchFilter_DisplayNameFilter: + return query.NewProjectRoleDisplayNameSearchQuery(filter.TextMethodPbToQuery(q.DisplayNameFilter.Method), q.DisplayNameFilter.DisplayName) + default: + return nil, zerrors.ThrowInvalidArgument(nil, "PROJECT-fms0e", "List.Query.Invalid") + } +} + +func roleViewsToPb(roles []*query.ProjectRole) []*project_pb.ProjectRole { + o := make([]*project_pb.ProjectRole, len(roles)) + for i, org := range roles { + o[i] = roleViewToPb(org) + } + return o +} + +func roleViewToPb(role *query.ProjectRole) *project_pb.ProjectRole { + return &project_pb.ProjectRole{ + ProjectId: role.ProjectID, + Key: role.Key, + CreationDate: timestamppb.New(role.CreationDate), + ChangeDate: timestamppb.New(role.ChangeDate), + DisplayName: role.DisplayName, + Group: role.Group, + } +} diff --git a/internal/api/grpc/project/v2beta/server.go b/internal/api/grpc/project/v2beta/server.go new file mode 100644 index 0000000000..fe197f9688 --- /dev/null +++ b/internal/api/grpc/project/v2beta/server.go @@ -0,0 +1,60 @@ +package project + +import ( + "google.golang.org/grpc" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/api/grpc/server" + "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/config/systemdefaults" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/query" + project "github.com/zitadel/zitadel/pkg/grpc/project/v2beta" +) + +var _ project.ProjectServiceServer = (*Server)(nil) + +type Server struct { + project.UnimplementedProjectServiceServer + systemDefaults systemdefaults.SystemDefaults + command *command.Commands + query *query.Queries + + checkPermission domain.PermissionCheck +} + +type Config struct{} + +func CreateServer( + systemDefaults systemdefaults.SystemDefaults, + command *command.Commands, + query *query.Queries, + checkPermission domain.PermissionCheck, +) *Server { + return &Server{ + systemDefaults: systemDefaults, + command: command, + query: query, + checkPermission: checkPermission, + } +} + +func (s *Server) RegisterServer(grpcServer *grpc.Server) { + project.RegisterProjectServiceServer(grpcServer, s) +} + +func (s *Server) AppName() string { + return project.ProjectService_ServiceDesc.ServiceName +} + +func (s *Server) MethodPrefix() string { + return project.ProjectService_ServiceDesc.ServiceName +} + +func (s *Server) AuthMethods() authz.MethodMapping { + return project.ProjectService_AuthMethods +} + +func (s *Server) RegisterGateway() server.RegisterGatewayFunc { + return project.RegisterProjectServiceHandler +} diff --git a/internal/api/grpc/saml/v2/integration/saml_test.go b/internal/api/grpc/saml/v2/integration/saml_test.go index 1f227ab149..1974c5236a 100644 --- a/internal/api/grpc/saml/v2/integration/saml_test.go +++ b/internal/api/grpc/saml/v2/integration/saml_test.go @@ -332,7 +332,7 @@ func TestServer_CreateResponse_Permission(t *testing.T) { projectID2, _, _ := createSAMLApplication(ctx, t, idpMetadata, saml.HTTPRedirectBinding, true, true) orgResp := Instance.CreateOrganization(ctx, "saml-permission-"+gofakeit.AppName(), gofakeit.Email()) - Instance.CreateProjectGrant(ctx, projectID2, orgResp.GetOrganizationId()) + Instance.CreateProjectGrant(ctx, t, projectID2, orgResp.GetOrganizationId()) user := Instance.CreateHumanUserVerified(ctx, orgResp.GetOrganizationId(), gofakeit.Email(), gofakeit.Phone()) Instance.CreateProjectUserGrant(t, ctx, projectID, user.GetUserId()) @@ -346,7 +346,7 @@ func TestServer_CreateResponse_Permission(t *testing.T) { projectID, _, sp := createSAMLApplication(ctx, t, idpMetadata, saml.HTTPRedirectBinding, true, true) orgResp := Instance.CreateOrganization(ctx, "saml-permission-"+gofakeit.AppName(), gofakeit.Email()) - Instance.CreateProjectGrant(ctx, projectID, orgResp.GetOrganizationId()) + Instance.CreateProjectGrant(ctx, t, projectID, orgResp.GetOrganizationId()) user := Instance.CreateHumanUserVerified(ctx, orgResp.GetOrganizationId(), gofakeit.Email(), gofakeit.Phone()) Instance.CreateProjectUserGrant(t, ctx, projectID, user.GetUserId()) @@ -368,9 +368,9 @@ func TestServer_CreateResponse_Permission(t *testing.T) { projectID, _, sp := createSAMLApplication(ctx, t, idpMetadata, saml.HTTPRedirectBinding, true, true) orgResp := Instance.CreateOrganization(ctx, "saml-permission-"+gofakeit.AppName(), gofakeit.Email()) - projectGrantResp := Instance.CreateProjectGrant(ctx, projectID, orgResp.GetOrganizationId()) + Instance.CreateProjectGrant(ctx, t, projectID, orgResp.GetOrganizationId()) user := Instance.CreateHumanUserVerified(ctx, orgResp.GetOrganizationId(), gofakeit.Email(), gofakeit.Phone()) - Instance.CreateProjectGrantUserGrant(ctx, orgResp.GetOrganizationId(), projectID, projectGrantResp.GetGrantId(), user.GetUserId()) + Instance.CreateProjectGrantUserGrant(ctx, orgResp.GetOrganizationId(), projectID, orgResp.GetOrganizationId(), user.GetUserId()) return createSessionAndSmlRequestForCallback(ctx, t, sp, Instance.Users[integration.UserTypeOrgOwner].ID, acsRedirect, user.GetUserId(), saml.HTTPRedirectBinding) }, @@ -502,9 +502,9 @@ func TestServer_CreateResponse_Permission(t *testing.T) { projectID, _, sp := createSAMLApplication(ctx, t, idpMetadata, saml.HTTPRedirectBinding, true, false) orgResp := Instance.CreateOrganization(ctx, "saml-permissison-"+gofakeit.AppName(), gofakeit.Email()) - projectGrantResp := Instance.CreateProjectGrant(ctx, projectID, orgResp.GetOrganizationId()) + Instance.CreateProjectGrant(ctx, t, projectID, orgResp.GetOrganizationId()) user := Instance.CreateHumanUserVerified(ctx, orgResp.GetOrganizationId(), gofakeit.Email(), gofakeit.Phone()) - Instance.CreateProjectGrantUserGrant(ctx, orgResp.GetOrganizationId(), projectID, projectGrantResp.GetGrantId(), user.GetUserId()) + Instance.CreateProjectGrantUserGrant(ctx, orgResp.GetOrganizationId(), projectID, orgResp.GetOrganizationId(), user.GetUserId()) return createSessionAndSmlRequestForCallback(ctx, t, sp, Instance.Users[integration.UserTypeOrgOwner].ID, acsRedirect, user.GetUserId(), saml.HTTPRedirectBinding) }, @@ -523,7 +523,7 @@ func TestServer_CreateResponse_Permission(t *testing.T) { projectID, _, sp := createSAMLApplication(ctx, t, idpMetadata, saml.HTTPRedirectBinding, true, false) orgResp := Instance.CreateOrganization(ctx, "saml-permissison-"+gofakeit.AppName(), gofakeit.Email()) - Instance.CreateProjectGrant(ctx, projectID, orgResp.GetOrganizationId()) + Instance.CreateProjectGrant(ctx, t, projectID, orgResp.GetOrganizationId()) user := Instance.CreateHumanUserVerified(ctx, orgResp.GetOrganizationId(), gofakeit.Email(), gofakeit.Phone()) return createSessionAndSmlRequestForCallback(ctx, t, sp, Instance.Users[integration.UserTypeOrgOwner].ID, acsRedirect, user.GetUserId(), saml.HTTPRedirectBinding) @@ -563,7 +563,7 @@ func TestServer_CreateResponse_Permission(t *testing.T) { dep: func(ctx context.Context, t *testing.T) *saml_pb.CreateResponseRequest { projectID, _, sp := createSAMLApplication(ctx, t, idpMetadata, saml.HTTPRedirectBinding, false, true) orgResp := Instance.CreateOrganization(ctx, "saml-permissison-"+gofakeit.AppName(), gofakeit.Email()) - Instance.CreateProjectGrant(ctx, projectID, orgResp.GetOrganizationId()) + Instance.CreateProjectGrant(ctx, t, projectID, orgResp.GetOrganizationId()) user := Instance.CreateHumanUserVerified(ctx, orgResp.GetOrganizationId(), gofakeit.Email(), gofakeit.Phone()) return createSessionAndSmlRequestForCallback(ctx, t, sp, Instance.Users[integration.UserTypeOrgOwner].ID, acsRedirect, user.GetUserId(), saml.HTTPRedirectBinding) @@ -640,10 +640,9 @@ func createSAMLSP(t *testing.T, idpMetadata *saml.EntityDescriptor, binding stri } func createSAMLApplication(ctx context.Context, t *testing.T, idpMetadata *saml.EntityDescriptor, binding string, projectRoleCheck, hasProjectCheck bool) (string, string, *samlsp.Middleware) { - project, err := Instance.CreateProjectWithPermissionCheck(ctx, projectRoleCheck, hasProjectCheck) - require.NoError(t, err) + project := Instance.CreateProject(ctx, t, "", gofakeit.AppName(), projectRoleCheck, hasProjectCheck) rootURL, sp := createSAMLSP(t, idpMetadata, binding) - _, err = Instance.CreateSAMLClient(ctx, project.GetId(), sp) + _, err := Instance.CreateSAMLClient(ctx, project.GetId(), sp) require.NoError(t, err) return project.GetId(), rootURL, sp } 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 eaf352c094..832268dc8c 100644 --- a/internal/api/grpc/user/v2/integration_test/user_test.go +++ b/internal/api/grpc/user/v2/integration_test/user_test.go @@ -1753,8 +1753,8 @@ func TestServer_ReactivateUser(t *testing.T) { } func TestServer_DeleteUser(t *testing.T) { - projectResp, err := Instance.CreateProject(CTX) - require.NoError(t, err) + projectResp := Instance.CreateProject(CTX, t, "", gofakeit.AppName(), false, false) + type args struct { req *user.DeleteUserRequest prepare func(*testing.T, *user.DeleteUserRequest) context.Context 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 a5a1309d1a..250322d66f 100644 --- a/internal/api/grpc/user/v2beta/integration_test/user_test.go +++ b/internal/api/grpc/user/v2beta/integration_test/user_test.go @@ -1706,12 +1706,11 @@ func TestServer_ReactivateUser(t *testing.T) { } func TestServer_DeleteUser(t *testing.T) { - projectResp, err := Instance.CreateProject(CTX) - require.NoError(t, err) + projectResp := Instance.CreateProject(CTX, t, "", gofakeit.AppName(), false, false) type args struct { ctx context.Context req *user.DeleteUserRequest - prepare func(request *user.DeleteUserRequest) error + prepare func(request *user.DeleteUserRequest) } tests := []struct { name string @@ -1726,7 +1725,7 @@ func TestServer_DeleteUser(t *testing.T) { &user.DeleteUserRequest{ UserId: "notexisting", }, - func(request *user.DeleteUserRequest) error { return nil }, + nil, }, wantErr: true, }, @@ -1735,10 +1734,9 @@ func TestServer_DeleteUser(t *testing.T) { args: args{ ctx: CTX, req: &user.DeleteUserRequest{}, - prepare: func(request *user.DeleteUserRequest) error { + prepare: func(request *user.DeleteUserRequest) { resp := Instance.CreateHumanUser(CTX) request.UserId = resp.GetUserId() - return err }, }, want: &user.DeleteUserResponse{ @@ -1753,10 +1751,9 @@ func TestServer_DeleteUser(t *testing.T) { args: args{ ctx: CTX, req: &user.DeleteUserRequest{}, - prepare: func(request *user.DeleteUserRequest) error { + prepare: func(request *user.DeleteUserRequest) { resp := Instance.CreateMachineUser(CTX) request.UserId = resp.GetUserId() - return err }, }, want: &user.DeleteUserResponse{ @@ -1771,13 +1768,12 @@ func TestServer_DeleteUser(t *testing.T) { args: args{ ctx: CTX, req: &user.DeleteUserRequest{}, - prepare: func(request *user.DeleteUserRequest) error { + prepare: func(request *user.DeleteUserRequest) { 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 }, }, want: &user.DeleteUserResponse{ @@ -1790,8 +1786,9 @@ 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) + if tt.args.prepare != nil { + tt.args.prepare(tt.args.req) + } got, err := Client.DeleteUser(tt.args.ctx, tt.args.req) if tt.wantErr { diff --git a/internal/api/oidc/access_token.go b/internal/api/oidc/access_token.go index 66da6e3ccf..2f2880efc2 100644 --- a/internal/api/oidc/access_token.go +++ b/internal/api/oidc/access_token.go @@ -122,7 +122,7 @@ func (s *Server) assertClientScopesForPAT(ctx context.Context, token *accessToke if err != nil { return zerrors.ThrowInternal(err, "OIDC-Cyc78", "Errors.Internal") } - roles, err := s.query.SearchProjectRoles(ctx, authz.GetFeatures(ctx).TriggerIntrospectionProjections, &query.ProjectRoleSearchQueries{Queries: []query.SearchQuery{projectIDQuery}}) + roles, err := s.query.SearchProjectRoles(ctx, authz.GetFeatures(ctx).TriggerIntrospectionProjections, &query.ProjectRoleSearchQueries{Queries: []query.SearchQuery{projectIDQuery}}, nil) if err != nil { return err } diff --git a/internal/api/oidc/auth_request.go b/internal/api/oidc/auth_request.go index f750b2a3ea..953ebe799c 100644 --- a/internal/api/oidc/auth_request.go +++ b/internal/api/oidc/auth_request.go @@ -459,7 +459,7 @@ func (o *OPStorage) assertProjectRoleScopes(ctx context.Context, clientID string if err != nil { return nil, zerrors.ThrowInternal(err, "OIDC-Cyc78", "Errors.Internal") } - roles, err := o.query.SearchProjectRoles(ctx, true, &query.ProjectRoleSearchQueries{Queries: []query.SearchQuery{projectIDQuery}}) + roles, err := o.query.SearchProjectRoles(ctx, true, &query.ProjectRoleSearchQueries{Queries: []query.SearchQuery{projectIDQuery}}, nil) if err != nil { return nil, err } @@ -482,7 +482,7 @@ func (o *OPStorage) assertProjectRoleScopesByProject(ctx context.Context, projec if err != nil { return nil, zerrors.ThrowInternal(err, "OIDC-Cyc78", "Errors.Internal") } - roles, err := o.query.SearchProjectRoles(ctx, true, &query.ProjectRoleSearchQueries{Queries: []query.SearchQuery{projectIDQuery}}) + roles, err := o.query.SearchProjectRoles(ctx, true, &query.ProjectRoleSearchQueries{Queries: []query.SearchQuery{projectIDQuery}}, nil) if err != nil { return nil, err } @@ -498,7 +498,7 @@ func (o *OPStorage) assertClientScopesForPAT(ctx context.Context, token *model.T if err != nil { return zerrors.ThrowInternal(err, "OIDC-Cyc78", "Errors.Internal") } - roles, err := o.query.SearchProjectRoles(ctx, true, &query.ProjectRoleSearchQueries{Queries: []query.SearchQuery{projectIDQuery}}) + roles, err := o.query.SearchProjectRoles(ctx, true, &query.ProjectRoleSearchQueries{Queries: []query.SearchQuery{projectIDQuery}}, nil) if err != nil { return err } diff --git a/internal/api/oidc/integration_test/client_test.go b/internal/api/oidc/integration_test/client_test.go index 43b000108c..08c17f69cb 100644 --- a/internal/api/oidc/integration_test/client_test.go +++ b/internal/api/oidc/integration_test/client_test.go @@ -7,6 +7,7 @@ import ( "testing" "time" + "github.com/brianvoe/gofakeit/v6" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/zitadel/oidc/v3/pkg/client" @@ -24,8 +25,7 @@ import ( ) func TestServer_Introspect(t *testing.T) { - project, err := Instance.CreateProject(CTX) - require.NoError(t, err) + project := Instance.CreateProject(CTX, t, "", gofakeit.AppName(), false, false) app, err := Instance.CreateOIDCNativeClient(CTX, redirectURI, logoutRedirectURI, project.GetId(), false) require.NoError(t, err) @@ -188,10 +188,8 @@ func assertIntrospection( // with clients that have different authentication methods. func TestServer_VerifyClient(t *testing.T) { sessionID, sessionToken, startTime, changeTime := Instance.CreateVerifiedWebAuthNSession(t, CTXLOGIN, User.GetUserId()) - project, err := Instance.CreateProject(CTX) - require.NoError(t, err) - projectInactive, err := Instance.CreateProject(CTX) - require.NoError(t, err) + project := Instance.CreateProject(CTX, t, "", gofakeit.AppName(), false, false) + projectInactive := Instance.CreateProject(CTX, t, "", gofakeit.AppName(), false, false) inactiveClient, err := Instance.CreateOIDCInactivateClient(CTX, redirectURI, logoutRedirectURI, project.GetId()) require.NoError(t, err) diff --git a/internal/api/oidc/integration_test/oidc_test.go b/internal/api/oidc/integration_test/oidc_test.go index 2ab78b972e..8bb103d0eb 100644 --- a/internal/api/oidc/integration_test/oidc_test.go +++ b/internal/api/oidc/integration_test/oidc_test.go @@ -421,21 +421,20 @@ type clientOpts struct { func createClientWithOpts(t testing.TB, instance *integration.Instance, opts clientOpts) (clientID, projectID string) { ctx := instance.WithAuthorization(CTX, integration.UserTypeOrgOwner) - project, err := instance.CreateProject(ctx) - require.NoError(t, err) + project := instance.CreateProject(ctx, t.(*testing.T), "", gofakeit.AppName(), false, false) app, err := instance.CreateOIDCClientLoginVersion(ctx, opts.redirectURI, opts.logoutURI, project.GetId(), app.OIDCAppType_OIDC_APP_TYPE_NATIVE, app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_NONE, opts.devMode, opts.LoginVersion) require.NoError(t, err) return app.GetClientId(), project.GetId() } func createImplicitClient(t testing.TB) string { - app, err := Instance.CreateOIDCImplicitFlowClient(CTX, redirectURIImplicit, nil) + app, err := Instance.CreateOIDCImplicitFlowClient(CTX, t.(*testing.T), redirectURIImplicit, nil) require.NoError(t, err) return app.GetClientId() } func createImplicitClientNoLoginClientHeader(t testing.TB) string { - app, err := Instance.CreateOIDCImplicitFlowClient(CTX, redirectURIImplicit, &app.LoginVersion{Version: &app.LoginVersion_LoginV2{LoginV2: &app.LoginV2{BaseUri: nil}}}) + app, err := Instance.CreateOIDCImplicitFlowClient(CTX, t.(*testing.T), redirectURIImplicit, &app.LoginVersion{Version: &app.LoginVersion_LoginV2{LoginV2: &app.LoginV2{BaseUri: nil}}}) require.NoError(t, err) return app.GetClientId() } diff --git a/internal/api/oidc/integration_test/token_device_test.go b/internal/api/oidc/integration_test/token_device_test.go index 0c6a65e8a2..9692909205 100644 --- a/internal/api/oidc/integration_test/token_device_test.go +++ b/internal/api/oidc/integration_test/token_device_test.go @@ -8,6 +8,7 @@ import ( "testing" "time" + "github.com/brianvoe/gofakeit/v6" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/zitadel/oidc/v3/pkg/client/rp" @@ -21,8 +22,7 @@ import ( ) func TestServer_DeviceAuth(t *testing.T) { - project, err := Instance.CreateProject(CTX) - require.NoError(t, err) + project := Instance.CreateProject(CTX, t, "", gofakeit.AppName(), false, false) client, err := Instance.CreateOIDCClient(CTX, redirectURI, logoutRedirectURI, project.GetId(), app.OIDCAppType_OIDC_APP_TYPE_NATIVE, app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_NONE, false, app.OIDCGrantType_OIDC_GRANT_TYPE_DEVICE_CODE) require.NoError(t, err) diff --git a/internal/api/oidc/integration_test/token_exchange_test.go b/internal/api/oidc/integration_test/token_exchange_test.go index 0844898a2f..dcd2d61669 100644 --- a/internal/api/oidc/integration_test/token_exchange_test.go +++ b/internal/api/oidc/integration_test/token_exchange_test.go @@ -147,7 +147,7 @@ func TestServer_TokenExchange(t *testing.T) { ctx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) userResp := instance.CreateHumanUser(ctx) - client, keyData, err := instance.CreateOIDCTokenExchangeClient(ctx) + client, keyData, err := instance.CreateOIDCTokenExchangeClient(ctx, t) require.NoError(t, err) signer, err := rp.SignerFromKeyFile(keyData)() require.NoError(t, err) @@ -371,7 +371,7 @@ func TestServer_TokenExchangeImpersonation(t *testing.T) { setTokenExchangeFeature(t, instance, true) setImpersonationPolicy(t, instance, true) - client, keyData, err := instance.CreateOIDCTokenExchangeClient(ctx) + client, keyData, err := instance.CreateOIDCTokenExchangeClient(ctx, t) require.NoError(t, err) signer, err := rp.SignerFromKeyFile(keyData)() require.NoError(t, err) @@ -591,7 +591,7 @@ func TestImpersonation_API_Call(t *testing.T) { instance := integration.NewInstance(CTX) ctx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) - client, keyData, err := instance.CreateOIDCTokenExchangeClient(ctx) + client, keyData, err := instance.CreateOIDCTokenExchangeClient(ctx, t) require.NoError(t, err) signer, err := rp.SignerFromKeyFile(keyData)() require.NoError(t, err) diff --git a/internal/api/oidc/integration_test/userinfo_test.go b/internal/api/oidc/integration_test/userinfo_test.go index da1dc6b1e3..d4aded0b48 100644 --- a/internal/api/oidc/integration_test/userinfo_test.go +++ b/internal/api/oidc/integration_test/userinfo_test.go @@ -309,9 +309,7 @@ func TestServer_UserInfo_Issue6662(t *testing.T) { roleBar = "bar" ) - project, err := Instance.CreateProject(CTX) - projectID := project.GetId() - require.NoError(t, err) + projectID := Instance.CreateProject(CTX, t, "", gofakeit.AppName(), false, false).GetId() user, _, clientID, clientSecret, err := Instance.CreateOIDCCredentialsClient(CTX) require.NoError(t, err) addProjectRolesGrants(t, user.GetUserId(), projectID, roleFoo, roleBar) diff --git a/internal/api/scim/integration_test/users_delete_test.go b/internal/api/scim/integration_test/users_delete_test.go index 86e88f46c4..23c335f93c 100644 --- a/internal/api/scim/integration_test/users_delete_test.go +++ b/internal/api/scim/integration_test/users_delete_test.go @@ -8,6 +8,7 @@ import ( "testing" "time" + "github.com/brianvoe/gofakeit/v6" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "google.golang.org/grpc/codes" @@ -76,13 +77,12 @@ func TestDeleteUser_errors(t *testing.T) { func TestDeleteUser_ensureReallyDeleted(t *testing.T) { // create user and dependencies createUserResp := Instance.CreateHumanUser(CTX) - proj, err := Instance.CreateProject(CTX) - require.NoError(t, err) + proj := Instance.CreateProject(CTX, t, "", gofakeit.AppName(), false, false) Instance.CreateProjectUserGrant(t, CTX, proj.Id, createUserResp.UserId) // delete user via scim - err = Instance.Client.SCIM.Users.Delete(CTX, Instance.DefaultOrg.Id, createUserResp.UserId) + err := Instance.Client.SCIM.Users.Delete(CTX, Instance.DefaultOrg.Id, createUserResp.UserId) assert.NoError(t, err) // ensure it is really deleted => try to delete again => should 404 diff --git a/internal/auth/repository/eventsourcing/eventstore/auth_request.go b/internal/auth/repository/eventsourcing/eventstore/auth_request.go index 0ede13ae68..7c335a752f 100644 --- a/internal/auth/repository/eventsourcing/eventstore/auth_request.go +++ b/internal/auth/repository/eventsourcing/eventstore/auth_request.go @@ -125,7 +125,7 @@ type userGrantProvider interface { type projectProvider interface { ProjectByClientID(context.Context, string) (*query.Project, error) - SearchProjectGrants(ctx context.Context, queries *query.ProjectGrantSearchQueries) (projects *query.ProjectGrants, err error) + SearchProjectGrants(ctx context.Context, queries *query.ProjectGrantSearchQueries, permissionCheck domain.PermissionCheck) (projects *query.ProjectGrants, err error) } type applicationProvider interface { @@ -1841,7 +1841,7 @@ func projectRequired(ctx context.Context, request *domain.AuthRequest, projectPr if err != nil { return false, err } - grants, err := projectProvider.SearchProjectGrants(ctx, &query.ProjectGrantSearchQueries{Queries: []query.SearchQuery{projectID, grantedOrg}}) + grants, err := projectProvider.SearchProjectGrants(ctx, &query.ProjectGrantSearchQueries{Queries: []query.SearchQuery{projectID, grantedOrg}}, nil) if err != nil { return false, err } diff --git a/internal/auth/repository/eventsourcing/eventstore/auth_request_test.go b/internal/auth/repository/eventsourcing/eventstore/auth_request_test.go index 7d71ddecd9..78edd2a7e4 100644 --- a/internal/auth/repository/eventsourcing/eventstore/auth_request_test.go +++ b/internal/auth/repository/eventsourcing/eventstore/auth_request_test.go @@ -286,7 +286,7 @@ func (m *mockProject) ProjectByClientID(ctx context.Context, s string) (*query.P return &query.Project{ResourceOwner: m.resourceOwner, HasProjectCheck: m.projectCheck}, nil } -func (m *mockProject) SearchProjectGrants(ctx context.Context, queries *query.ProjectGrantSearchQueries) (*query.ProjectGrants, error) { +func (m *mockProject) SearchProjectGrants(ctx context.Context, queries *query.ProjectGrantSearchQueries, permissionCheck domain.PermissionCheck) (*query.ProjectGrants, error) { if m.hasProject { mockProjectGrant := new(query.ProjectGrant) return &query.ProjectGrants{ProjectGrants: []*query.ProjectGrant{mockProjectGrant}}, nil diff --git a/internal/command/org.go b/internal/command/org.go index 9874410a5f..b6650ef7f2 100644 --- a/internal/command/org.go +++ b/internal/command/org.go @@ -468,7 +468,7 @@ func (c *Commands) prepareRemoveOrg(a *org.Aggregate) preparation.Validation { return nil, zerrors.ThrowPreconditionFailed(nil, "COMMA-wG9p1", "Errors.Org.DefaultOrgNotDeletable") } - err := c.checkProjectExists(ctx, instance.ProjectID(), a.ID) + _, err := c.checkProjectExists(ctx, instance.ProjectID(), a.ID) // if there is no error, the ZITADEL project was found on the org to be deleted if err == nil { return nil, zerrors.ThrowPreconditionFailed(err, "COMMA-AF3JW", "Errors.Org.ZitadelOrgNotDeletable") diff --git a/internal/command/permission_checks.go b/internal/command/permission_checks.go index bec2e9b7d4..253b6ee72a 100644 --- a/internal/command/permission_checks.go +++ b/internal/command/permission_checks.go @@ -6,6 +6,7 @@ import ( "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/project" "github.com/zitadel/zitadel/internal/v2/user" "github.com/zitadel/zitadel/internal/zerrors" ) @@ -58,3 +59,15 @@ func (c *Commands) checkPermissionUpdateUser(ctx context.Context, resourceOwner, func (c *Commands) checkPermissionUpdateUserCredentials(ctx context.Context, resourceOwner, userID string) error { return c.checkPermissionOnUser(ctx, domain.PermissionUserCredentialWrite)(resourceOwner, userID) } + +func (c *Commands) checkPermissionDeleteProject(ctx context.Context, resourceOwner, projectID string) error { + return c.newPermissionCheck(ctx, domain.PermissionProjectDelete, project.AggregateType)(resourceOwner, projectID) +} + +func (c *Commands) checkPermissionUpdateProject(ctx context.Context, resourceOwner, projectID string) error { + return c.newPermissionCheck(ctx, domain.PermissionProjectWrite, project.AggregateType)(resourceOwner, projectID) +} + +func (c *Commands) checkPermissionWriteProjectGrant(ctx context.Context, resourceOwner, projectGrantID string) error { + return c.newPermissionCheck(ctx, domain.PermissionProjectGrantWrite, project.AggregateType)(resourceOwner, projectGrantID) +} diff --git a/internal/command/project.go b/internal/command/project.go index df4f8ab545..bf72306417 100644 --- a/internal/command/project.go +++ b/internal/command/project.go @@ -3,6 +3,7 @@ package command import ( "context" "strings" + "time" "github.com/zitadel/logging" @@ -10,6 +11,7 @@ import ( "github.com/zitadel/zitadel/internal/command/preparation" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/eventstore/v1/models" "github.com/zitadel/zitadel/internal/feature" "github.com/zitadel/zitadel/internal/query/projection" "github.com/zitadel/zitadel/internal/repository/project" @@ -17,67 +19,60 @@ import ( "github.com/zitadel/zitadel/internal/zerrors" ) -func (c *Commands) AddProjectWithID(ctx context.Context, project *domain.Project, resourceOwner, projectID string) (_ *domain.Project, err error) { - ctx, span := tracing.NewSpan(ctx) - defer func() { span.EndWithError(err) }() - if resourceOwner == "" { - return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-w8tnSoJxtn", "Errors.ResourceOwnerMissing") - } - if projectID == "" { - return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-nDXf5vXoUj", "Errors.IDMissing") - } - if !project.IsValid() { - return nil, zerrors.ThrowInvalidArgument(nil, "PROJECT-IOVCC", "Errors.Project.Invalid") - } - project, err = c.addProjectWithID(ctx, project, resourceOwner, projectID) - if err != nil { - return nil, err - } - return project, nil +type AddProject struct { + models.ObjectRoot + + Name string + ProjectRoleAssertion bool + ProjectRoleCheck bool + HasProjectCheck bool + PrivateLabelingSetting domain.PrivateLabelingSetting } -func (c *Commands) AddProject(ctx context.Context, project *domain.Project, resourceOwner string) (_ *domain.Project, err error) { - ctx, span := tracing.NewSpan(ctx) - defer func() { span.EndWithError(err) }() - if !project.IsValid() { - return nil, zerrors.ThrowInvalidArgument(nil, "PROJECT-IOVCC", "Errors.Project.Invalid") +func (p *AddProject) IsValid() error { + if p.ResourceOwner == "" { + return zerrors.ThrowInvalidArgument(nil, "COMMAND-fmq7bqQX1s", "Errors.ResourceOwnerMissing") } - if resourceOwner == "" { - return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-fmq7bqQX1s", "Errors.ResourceOwnerMissing") + if p.Name == "" { + return zerrors.ThrowInvalidArgument(nil, "PROJECT-IOVCC", "Errors.Project.Invalid") } - - projectID, err := c.idGenerator.Next() - if err != nil { - return nil, err - } - - project, err = c.addProjectWithID(ctx, project, resourceOwner, projectID) - if err != nil { - return nil, err - } - return project, nil + return nil } -func (c *Commands) addProjectWithID(ctx context.Context, projectAdd *domain.Project, resourceOwner, projectID string) (_ *domain.Project, err error) { - projectAdd.AggregateID = projectID - projectWriteModel, err := c.getProjectWriteModelByID(ctx, projectAdd.AggregateID, resourceOwner) +func (c *Commands) AddProject(ctx context.Context, add *AddProject) (_ *domain.ObjectDetails, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + if err := add.IsValid(); err != nil { + return nil, err + } + + if add.AggregateID == "" { + add.AggregateID, err = c.idGenerator.Next() + if err != nil { + return nil, err + } + } + wm, err := c.getProjectWriteModelByID(ctx, add.AggregateID, add.ResourceOwner) if err != nil { return nil, err } - if isProjectStateExists(projectWriteModel.State) { + if isProjectStateExists(wm.State) { return nil, zerrors.ThrowAlreadyExists(nil, "COMMAND-opamwu", "Errors.Project.AlreadyExisting") } + if err := c.checkPermissionUpdateProject(ctx, wm.ResourceOwner, wm.AggregateID); err != nil { + return nil, err + } events := []eventstore.Command{ project.NewProjectAddedEvent( ctx, - //nolint: contextcheck - ProjectAggregateFromWriteModel(&projectWriteModel.WriteModel), - projectAdd.Name, - projectAdd.ProjectRoleAssertion, - projectAdd.ProjectRoleCheck, - projectAdd.HasProjectCheck, - projectAdd.PrivateLabelingSetting), + ProjectAggregateFromWriteModelWithCTX(ctx, &wm.WriteModel), + add.Name, + add.ProjectRoleAssertion, + add.ProjectRoleCheck, + add.HasProjectCheck, + add.PrivateLabelingSetting), } postCommit, err := c.projectCreatedMilestone(ctx, &events) if err != nil { @@ -88,11 +83,11 @@ func (c *Commands) addProjectWithID(ctx context.Context, projectAdd *domain.Proj return nil, err } postCommit(ctx) - err = AppendAndReduce(projectWriteModel, pushedEvents...) + err = AppendAndReduce(wm, pushedEvents...) if err != nil { return nil, err } - return projectWriteModelToProject(projectWriteModel), nil + return writeModelToObjectDetails(&wm.WriteModel), nil } func AddProjectCommand( @@ -143,14 +138,14 @@ func projectWriteModel(ctx context.Context, filter preparation.FilterToQueryRedu return project, nil } -func (c *Commands) projectAggregateByID(ctx context.Context, projectID, resourceOwner string) (*eventstore.Aggregate, domain.ProjectState, error) { - result, err := c.projectState(ctx, projectID, resourceOwner) +func (c *Commands) projectAggregateByID(ctx context.Context, projectID string) (*eventstore.Aggregate, domain.ProjectState, error) { + result, err := c.projectState(ctx, projectID) if err != nil { return nil, domain.ProjectStateUnspecified, zerrors.ThrowNotFound(err, "COMMA-NDQoF", "Errors.Project.NotFound") } if len(result) == 0 { _ = projection.ProjectGrantFields.Trigger(ctx) - result, err = c.projectState(ctx, projectID, resourceOwner) + result, err = c.projectState(ctx, projectID) if err != nil || len(result) == 0 { return nil, domain.ProjectStateUnspecified, zerrors.ThrowNotFound(err, "COMMA-U1nza", "Errors.Project.NotFound") } @@ -164,7 +159,7 @@ func (c *Commands) projectAggregateByID(ctx context.Context, projectID, resource return &result[0].Aggregate, state, nil } -func (c *Commands) projectState(ctx context.Context, projectID, resourceOwner string) ([]*eventstore.SearchResult, error) { +func (c *Commands) projectState(ctx context.Context, projectID string) ([]*eventstore.SearchResult, error) { return c.eventstore.Search( ctx, map[eventstore.FieldType]any{ @@ -172,12 +167,11 @@ func (c *Commands) projectState(ctx context.Context, projectID, resourceOwner st eventstore.FieldTypeObjectID: projectID, eventstore.FieldTypeObjectRevision: project.ProjectObjectRevision, eventstore.FieldTypeFieldName: project.ProjectStateSearchField, - eventstore.FieldTypeResourceOwner: resourceOwner, }, ) } -func (c *Commands) checkProjectExists(ctx context.Context, projectID, resourceOwner string) (err error) { +func (c *Commands) checkProjectExists(ctx context.Context, projectID, resourceOwner string) (_ string, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() @@ -185,50 +179,69 @@ func (c *Commands) checkProjectExists(ctx context.Context, projectID, resourceOw return c.checkProjectExistsOld(ctx, projectID, resourceOwner) } - _, state, err := c.projectAggregateByID(ctx, projectID, resourceOwner) + agg, state, err := c.projectAggregateByID(ctx, projectID) if err != nil || !state.Valid() { - return zerrors.ThrowPreconditionFailed(err, "COMMA-VCnwD", "Errors.Project.NotFound") + return "", zerrors.ThrowPreconditionFailed(err, "COMMA-VCnwD", "Errors.Project.NotFound") + } + return agg.ResourceOwner, nil +} + +type ChangeProject struct { + models.ObjectRoot + + Name *string + ProjectRoleAssertion *bool + ProjectRoleCheck *bool + HasProjectCheck *bool + PrivateLabelingSetting *domain.PrivateLabelingSetting +} + +func (p *ChangeProject) IsValid() error { + if p.AggregateID == "" { + return zerrors.ThrowInvalidArgument(nil, "COMMAND-4m9vS", "Errors.Project.Invalid") + } + if p.Name != nil && *p.Name == "" { + return zerrors.ThrowInvalidArgument(nil, "COMMAND-4m9vS", "Errors.Project.Invalid") } return nil } -func (c *Commands) ChangeProject(ctx context.Context, projectChange *domain.Project, resourceOwner string) (*domain.Project, error) { - if !projectChange.IsValid() || projectChange.AggregateID == "" { - return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-4m9vS", "Errors.Project.Invalid") +func (c *Commands) ChangeProject(ctx context.Context, change *ChangeProject) (_ *domain.ObjectDetails, err error) { + if err := change.IsValid(); err != nil { + return nil, err } - existingProject, err := c.getProjectWriteModelByID(ctx, projectChange.AggregateID, resourceOwner) + existing, err := c.getProjectWriteModelByID(ctx, change.AggregateID, change.ResourceOwner) if err != nil { return nil, err } - if !isProjectStateExists(existingProject.State) { + if !isProjectStateExists(existing.State) { return nil, zerrors.ThrowNotFound(nil, "COMMAND-3M9sd", "Errors.Project.NotFound") } + if err := c.checkPermissionUpdateProject(ctx, existing.ResourceOwner, existing.AggregateID); err != nil { + return nil, err + } - projectAgg := ProjectAggregateFromWriteModel(&existingProject.WriteModel) - changedEvent, hasChanged, err := existingProject.NewChangedEvent( + changedEvent := existing.NewChangedEvent( ctx, - projectAgg, - projectChange.Name, - projectChange.ProjectRoleAssertion, - projectChange.ProjectRoleCheck, - projectChange.HasProjectCheck, - projectChange.PrivateLabelingSetting) + ProjectAggregateFromWriteModelWithCTX(ctx, &existing.WriteModel), + change.Name, + change.ProjectRoleAssertion, + change.ProjectRoleCheck, + change.HasProjectCheck, + change.PrivateLabelingSetting) + if changedEvent == nil { + return writeModelToObjectDetails(&existing.WriteModel), nil + } + err = c.pushAppendAndReduce(ctx, existing, changedEvent) if err != nil { return nil, err } - if !hasChanged { - return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-2M0fs", "Errors.NoChangesFound") - } - err = c.pushAppendAndReduce(ctx, existingProject, changedEvent) - if err != nil { - return nil, err - } - return projectWriteModelToProject(existingProject), nil + return writeModelToObjectDetails(&existing.WriteModel), nil } func (c *Commands) DeactivateProject(ctx context.Context, projectID string, resourceOwner string) (*domain.ObjectDetails, error) { - if projectID == "" || resourceOwner == "" { + if projectID == "" { return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-88iF0", "Errors.Project.ProjectIDMissing") } @@ -236,7 +249,7 @@ func (c *Commands) DeactivateProject(ctx context.Context, projectID string, reso return c.deactivateProjectOld(ctx, projectID, resourceOwner) } - projectAgg, state, err := c.projectAggregateByID(ctx, projectID, resourceOwner) + projectAgg, state, err := c.projectAggregateByID(ctx, projectID) if err != nil { return nil, err } @@ -247,6 +260,9 @@ func (c *Commands) DeactivateProject(ctx context.Context, projectID string, reso if state != domain.ProjectStateActive { return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-mki55", "Errors.Project.NotActive") } + if err := c.checkPermissionUpdateProject(ctx, projectAgg.ResourceOwner, projectAgg.ID); err != nil { + return nil, err + } pushedEvents, err := c.eventstore.Push(ctx, project.NewProjectDeactivatedEvent(ctx, projectAgg)) if err != nil { @@ -261,7 +277,7 @@ func (c *Commands) DeactivateProject(ctx context.Context, projectID string, reso } func (c *Commands) ReactivateProject(ctx context.Context, projectID string, resourceOwner string) (*domain.ObjectDetails, error) { - if projectID == "" || resourceOwner == "" { + if projectID == "" { return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-3ihsF", "Errors.Project.ProjectIDMissing") } @@ -269,7 +285,7 @@ func (c *Commands) ReactivateProject(ctx context.Context, projectID string, reso return c.reactivateProjectOld(ctx, projectID, resourceOwner) } - projectAgg, state, err := c.projectAggregateByID(ctx, projectID, resourceOwner) + projectAgg, state, err := c.projectAggregateByID(ctx, projectID) if err != nil { return nil, err } @@ -280,6 +296,9 @@ func (c *Commands) ReactivateProject(ctx context.Context, projectID string, reso if state != domain.ProjectStateInactive { return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-5M9bs", "Errors.Project.NotInactive") } + if err := c.checkPermissionUpdateProject(ctx, projectAgg.ResourceOwner, projectAgg.ID); err != nil { + return nil, err + } pushedEvents, err := c.eventstore.Push(ctx, project.NewProjectReactivatedEvent(ctx, projectAgg)) if err != nil { @@ -293,6 +312,7 @@ func (c *Commands) ReactivateProject(ctx context.Context, projectID string, reso }, nil } +// Deprecated: use commands.DeleteProject func (c *Commands) RemoveProject(ctx context.Context, projectID, resourceOwner string, cascadingUserGrantIDs ...string) (*domain.ObjectDetails, error) { if projectID == "" || resourceOwner == "" { return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-66hM9", "Errors.Project.ProjectIDMissing") @@ -316,15 +336,18 @@ func (c *Commands) RemoveProject(ctx context.Context, projectID, resourceOwner s uniqueConstraints[i] = project.NewRemoveSAMLConfigEntityIDUniqueConstraint(entityID.EntityID) } - projectAgg := ProjectAggregateFromWriteModel(&existingProject.WriteModel) events := []eventstore.Command{ - project.NewProjectRemovedEvent(ctx, projectAgg, existingProject.Name, uniqueConstraints), + project.NewProjectRemovedEvent(ctx, + ProjectAggregateFromWriteModelWithCTX(ctx, &existingProject.WriteModel), + existingProject.Name, + uniqueConstraints, + ), } for _, grantID := range cascadingUserGrantIDs { event, _, err := c.removeUserGrant(ctx, grantID, "", true) if err != nil { - logging.LogWithFields("COMMAND-b8Djf", "usergrantid", grantID).WithError(err).Warn("could not cascade remove user grant") + logging.WithFields("id", "COMMAND-b8Djf", "usergrantid", grantID).WithError(err).Warn("could not cascade remove user grant") continue } events = append(events, event) @@ -341,6 +364,53 @@ func (c *Commands) RemoveProject(ctx context.Context, projectID, resourceOwner s return writeModelToObjectDetails(&existingProject.WriteModel), nil } +func (c *Commands) DeleteProject(ctx context.Context, id, resourceOwner string, cascadingUserGrantIDs ...string) (time.Time, error) { + if id == "" { + return time.Time{}, zerrors.ThrowInvalidArgument(nil, "COMMAND-obqos2l3no", "Errors.IDMissing") + } + + existing, err := c.getProjectWriteModelByID(ctx, id, resourceOwner) + if err != nil { + return time.Time{}, err + } + if !isProjectStateExists(existing.State) { + return existing.WriteModel.ChangeDate, nil + } + if err := c.checkPermissionDeleteProject(ctx, existing.ResourceOwner, existing.AggregateID); err != nil { + return time.Time{}, err + } + + samlEntityIDsAgg, err := c.getSAMLEntityIdsWriteModelByProjectID(ctx, id, resourceOwner) + if err != nil { + return time.Time{}, err + } + + uniqueConstraints := make([]*eventstore.UniqueConstraint, len(samlEntityIDsAgg.EntityIDs)) + for i, entityID := range samlEntityIDsAgg.EntityIDs { + uniqueConstraints[i] = project.NewRemoveSAMLConfigEntityIDUniqueConstraint(entityID.EntityID) + } + events := []eventstore.Command{ + project.NewProjectRemovedEvent(ctx, + ProjectAggregateFromWriteModelWithCTX(ctx, &existing.WriteModel), + existing.Name, + uniqueConstraints, + ), + } + for _, grantID := range cascadingUserGrantIDs { + event, _, err := c.removeUserGrant(ctx, grantID, "", true) + if err != nil { + logging.WithFields("id", "COMMAND-b8Djf", "usergrantid", grantID).WithError(err).Warn("could not cascade remove user grant") + continue + } + events = append(events, event) + } + + if err := c.pushAppendAndReduce(ctx, existing, events...); err != nil { + return time.Time{}, err + } + return existing.WriteModel.ChangeDate, nil +} + func (c *Commands) getProjectWriteModelByID(ctx context.Context, projectID, resourceOwner string) (_ *ProjectWriteModel, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() diff --git a/internal/command/project_application_api.go b/internal/command/project_application_api.go index 21c3bc5ee7..5b8bbafcdf 100644 --- a/internal/command/project_application_api.go +++ b/internal/command/project_application_api.go @@ -79,7 +79,7 @@ func (c *Commands) AddAPIApplicationWithID(ctx context.Context, apiApp *domain.A return nil, zerrors.ThrowPreconditionFailed(nil, "PROJECT-mabu12", "Errors.Project.App.AlreadyExisting") } - if err := c.checkProjectExists(ctx, apiApp.AggregateID, resourceOwner); err != nil { + if _, err := c.checkProjectExists(ctx, apiApp.AggregateID, resourceOwner); err != nil { return nil, err } return c.addAPIApplicationWithID(ctx, apiApp, resourceOwner, appID) @@ -90,7 +90,7 @@ func (c *Commands) AddAPIApplication(ctx context.Context, apiApp *domain.APIApp, return nil, zerrors.ThrowInvalidArgument(nil, "PROJECT-5m9E", "Errors.Project.App.Invalid") } - if err := c.checkProjectExists(ctx, apiApp.AggregateID, resourceOwner); err != nil { + if _, err := c.checkProjectExists(ctx, apiApp.AggregateID, resourceOwner); err != nil { return nil, err } if !apiApp.IsValid() { diff --git a/internal/command/project_application_oidc.go b/internal/command/project_application_oidc.go index fccb0efe06..491bd38fca 100644 --- a/internal/command/project_application_oidc.go +++ b/internal/command/project_application_oidc.go @@ -132,7 +132,7 @@ func (c *Commands) AddOIDCApplicationWithID(ctx context.Context, oidcApp *domain return nil, zerrors.ThrowPreconditionFailed(nil, "PROJECT-lxowmp", "Errors.Project.App.AlreadyExisting") } - if err := c.checkProjectExists(ctx, oidcApp.AggregateID, resourceOwner); err != nil { + if _, err := c.checkProjectExists(ctx, oidcApp.AggregateID, resourceOwner); err != nil { return nil, err } return c.addOIDCApplicationWithID(ctx, oidcApp, resourceOwner, appID) @@ -142,7 +142,7 @@ func (c *Commands) AddOIDCApplication(ctx context.Context, oidcApp *domain.OIDCA if oidcApp == nil || oidcApp.AggregateID == "" { return nil, zerrors.ThrowInvalidArgument(nil, "PROJECT-34Fm0", "Errors.Project.App.Invalid") } - if err := c.checkProjectExists(ctx, oidcApp.AggregateID, resourceOwner); err != nil { + if _, err := c.checkProjectExists(ctx, oidcApp.AggregateID, resourceOwner); err != nil { return nil, err } if oidcApp.AppName == "" || !oidcApp.IsValid() { diff --git a/internal/command/project_application_saml.go b/internal/command/project_application_saml.go index b14bed0758..1a5cefa221 100644 --- a/internal/command/project_application_saml.go +++ b/internal/command/project_application_saml.go @@ -16,7 +16,7 @@ func (c *Commands) AddSAMLApplication(ctx context.Context, application *domain.S return nil, zerrors.ThrowInvalidArgument(nil, "PROJECT-35Fn0", "Errors.Project.App.Invalid") } - if err := c.checkProjectExists(ctx, application.AggregateID, resourceOwner); err != nil { + if _, err := c.checkProjectExists(ctx, application.AggregateID, resourceOwner); err != nil { return nil, err } addedApplication := NewSAMLApplicationWriteModel(application.AggregateID, resourceOwner) diff --git a/internal/command/project_grant.go b/internal/command/project_grant.go index 82d5dcab38..763ea7ab67 100644 --- a/internal/command/project_grant.go +++ b/internal/command/project_grant.go @@ -9,6 +9,7 @@ import ( "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" + es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models" "github.com/zitadel/zitadel/internal/feature" "github.com/zitadel/zitadel/internal/repository/org" "github.com/zitadel/zitadel/internal/repository/project" @@ -16,69 +17,107 @@ import ( "github.com/zitadel/zitadel/internal/zerrors" ) -func (c *Commands) AddProjectGrantWithID(ctx context.Context, grant *domain.ProjectGrant, grantID string, resourceOwner string) (_ *domain.ProjectGrant, err error) { +type AddProjectGrant struct { + es_models.ObjectRoot + + GrantID string + GrantedOrgID string + RoleKeys []string +} + +func (p *AddProjectGrant) IsValid() error { + if p.AggregateID == "" { + return zerrors.ThrowInvalidArgument(nil, "COMMAND-FYRnWEzBzV", "Errors.Project.Grant.Invalid") + } + if p.GrantedOrgID == "" { + return zerrors.ThrowInvalidArgument(nil, "COMMAND-PPhHpWGRAE", "Errors.Project.Grant.Invalid") + } + return nil +} + +func (c *Commands) AddProjectGrant(ctx context.Context, grant *AddProjectGrant) (_ *domain.ObjectDetails, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - return c.addProjectGrantWithID(ctx, grant, grantID, resourceOwner) + if err := grant.IsValid(); err != nil { + return nil, err + } + + if grant.GrantID == "" { + grant.GrantID, err = c.idGenerator.Next() + if err != nil { + return nil, err + } + } + + projectResourceOwner, err := c.checkProjectGrantPreCondition(ctx, grant.AggregateID, grant.GrantedOrgID, grant.ResourceOwner, grant.RoleKeys) + if err != nil { + return nil, err + } + // if there is no resourceowner provided then use the resourceowner of the project + if grant.ResourceOwner == "" { + grant.ResourceOwner = projectResourceOwner + } + if err := c.checkPermissionWriteProjectGrant(ctx, grant.ResourceOwner, grant.GrantID); err != nil { + return nil, err + } + + wm := NewProjectGrantWriteModel(grant.GrantID, grant.AggregateID, grant.ResourceOwner) + // error if provided resourceowner is not equal to the resourceowner of the project or the project grant is for the same organization + if projectResourceOwner != wm.ResourceOwner || wm.ResourceOwner == grant.GrantedOrgID { + return nil, zerrors.ThrowPreconditionFailed(nil, "PROJECT-ckUpbvboAH", "Errors.Project.Grant.Invalid") + } + if err := c.pushAppendAndReduce(ctx, + wm, + project.NewGrantAddedEvent(ctx, + ProjectAggregateFromWriteModelWithCTX(ctx, &wm.WriteModel), + grant.GrantID, + grant.GrantedOrgID, + grant.RoleKeys), + ); err != nil { + return nil, err + } + return writeModelToObjectDetails(&wm.WriteModel), nil } -func (c *Commands) AddProjectGrant(ctx context.Context, grant *domain.ProjectGrant, resourceOwner string) (_ *domain.ProjectGrant, err error) { - if !grant.IsValid() { - return nil, zerrors.ThrowInvalidArgument(nil, "PROJECT-3b8fs", "Errors.Project.Grant.Invalid") - } - err = c.checkProjectGrantPreCondition(ctx, grant, resourceOwner) - if err != nil { - return nil, err - } +type ChangeProjectGrant struct { + es_models.ObjectRoot - grantID, err := c.idGenerator.Next() - if err != nil { - return nil, err - } - - return c.addProjectGrantWithID(ctx, grant, grantID, resourceOwner) + GrantID string + RoleKeys []string } -func (c *Commands) addProjectGrantWithID(ctx context.Context, grant *domain.ProjectGrant, grantID string, resourceOwner string) (_ *domain.ProjectGrant, err error) { - grant.GrantID = grantID - - addedGrant := NewProjectGrantWriteModel(grant.GrantID, grant.AggregateID, resourceOwner) - projectAgg := ProjectAggregateFromWriteModel(&addedGrant.WriteModel) - pushedEvents, err := c.eventstore.Push( - ctx, - project.NewGrantAddedEvent(ctx, projectAgg, grant.GrantID, grant.GrantedOrgID, grant.RoleKeys)) - if err != nil { - return nil, err - } - err = AppendAndReduce(addedGrant, pushedEvents...) - if err != nil { - return nil, err - } - return projectGrantWriteModelToProjectGrant(addedGrant), nil -} - -func (c *Commands) ChangeProjectGrant(ctx context.Context, grant *domain.ProjectGrant, resourceOwner string, cascadeUserGrantIDs ...string) (_ *domain.ProjectGrant, err error) { +func (c *Commands) ChangeProjectGrant(ctx context.Context, grant *ChangeProjectGrant, cascadeUserGrantIDs ...string) (_ *domain.ObjectDetails, err error) { if grant.GrantID == "" { return nil, zerrors.ThrowInvalidArgument(nil, "PROJECT-1j83s", "Errors.IDMissing") } - existingGrant, err := c.projectGrantWriteModelByID(ctx, grant.GrantID, grant.AggregateID, resourceOwner) + existingGrant, err := c.projectGrantWriteModelByID(ctx, grant.GrantID, grant.AggregateID, grant.ResourceOwner) if err != nil { return nil, err } - grant.GrantedOrgID = existingGrant.GrantedOrgID - err = c.checkProjectGrantPreCondition(ctx, grant, resourceOwner) + if err := c.checkPermissionWriteProjectGrant(ctx, existingGrant.ResourceOwner, existingGrant.GrantID); err != nil { + return nil, err + } + projectResourceOwner, err := c.checkProjectGrantPreCondition(ctx, existingGrant.AggregateID, existingGrant.GrantedOrgID, existingGrant.ResourceOwner, grant.RoleKeys) if err != nil { return nil, err } - projectAgg := ProjectAggregateFromWriteModel(&existingGrant.WriteModel) + // error if provided resourceowner is not equal to the resourceowner of the project + if existingGrant.ResourceOwner != projectResourceOwner { + return nil, zerrors.ThrowPreconditionFailed(nil, "PROJECT-q1BhA68RBC", "Errors.Project.Grant.Invalid") + } + // return if there are no changes to the project grant roles if reflect.DeepEqual(existingGrant.RoleKeys, grant.RoleKeys) { - return nil, zerrors.ThrowPreconditionFailed(nil, "PROJECT-0o0pL", "Errors.NoChangesFoundc") + return writeModelToObjectDetails(&existingGrant.WriteModel), nil } events := []eventstore.Command{ - project.NewGrantChangedEvent(ctx, projectAgg, grant.GrantID, grant.RoleKeys), + project.NewGrantChangedEvent(ctx, + ProjectAggregateFromWriteModelWithCTX(ctx, &existingGrant.WriteModel), + existingGrant.GrantID, + grant.RoleKeys, + ), } removedRoles := domain.GetRemovedRoles(existingGrant.RoleKeys, grant.RoleKeys) @@ -91,7 +130,7 @@ func (c *Commands) ChangeProjectGrant(ctx context.Context, grant *domain.Project if err != nil { return nil, err } - return projectGrantWriteModelToProjectGrant(existingGrant), nil + return writeModelToObjectDetails(&existingGrant.WriteModel), nil } for _, userGrantID := range cascadeUserGrantIDs { @@ -109,7 +148,7 @@ func (c *Commands) ChangeProjectGrant(ctx context.Context, grant *domain.Project if err != nil { return nil, err } - return projectGrantWriteModelToProjectGrant(existingGrant), nil + return writeModelToObjectDetails(&existingGrant.WriteModel), nil } func (c *Commands) removeRoleFromProjectGrant(ctx context.Context, projectAgg *eventstore.Aggregate, projectID, projectGrantID, roleKey string, cascade bool) (_ eventstore.Command, _ *ProjectGrantWriteModel, err error) { @@ -147,7 +186,7 @@ func (c *Commands) DeactivateProjectGrant(ctx context.Context, projectID, grantI return details, zerrors.ThrowInvalidArgument(nil, "PROJECT-p0s4V", "Errors.IDMissing") } - err = c.checkProjectExists(ctx, projectID, resourceOwner) + projectResourceOwner, err := c.checkProjectExists(ctx, projectID, resourceOwner) if err != nil { return nil, err } @@ -156,12 +195,27 @@ func (c *Commands) DeactivateProjectGrant(ctx context.Context, projectID, grantI if err != nil { return details, err } + // error if provided resourceowner is not equal to the resourceowner of the project + if projectResourceOwner != existingGrant.ResourceOwner { + return nil, zerrors.ThrowPreconditionFailed(nil, "PROJECT-0l10S9OmZV", "Errors.Project.Grant.Invalid") + } + // return if project grant is already inactive + if existingGrant.State == domain.ProjectGrantStateInactive { + return writeModelToObjectDetails(&existingGrant.WriteModel), nil + } + // error if project grant is neither active nor inactive if existingGrant.State != domain.ProjectGrantStateActive { return details, zerrors.ThrowPreconditionFailed(nil, "PROJECT-47fu8", "Errors.Project.Grant.NotActive") } - projectAgg := ProjectAggregateFromWriteModel(&existingGrant.WriteModel) - - pushedEvents, err := c.eventstore.Push(ctx, project.NewGrantDeactivateEvent(ctx, projectAgg, grantID)) + if err := c.checkPermissionWriteProjectGrant(ctx, existingGrant.ResourceOwner, existingGrant.GrantID); err != nil { + return nil, err + } + pushedEvents, err := c.eventstore.Push(ctx, + project.NewGrantDeactivateEvent(ctx, + ProjectAggregateFromWriteModelWithCTX(ctx, &existingGrant.WriteModel), + grantID, + ), + ) if err != nil { return nil, err } @@ -177,7 +231,7 @@ func (c *Commands) ReactivateProjectGrant(ctx context.Context, projectID, grantI return details, zerrors.ThrowInvalidArgument(nil, "PROJECT-p0s4V", "Errors.IDMissing") } - err = c.checkProjectExists(ctx, projectID, resourceOwner) + projectResourceOwner, err := c.checkProjectExists(ctx, projectID, resourceOwner) if err != nil { return nil, err } @@ -186,11 +240,27 @@ func (c *Commands) ReactivateProjectGrant(ctx context.Context, projectID, grantI if err != nil { return details, err } + // error if provided resourceowner is not equal to the resourceowner of the project + if projectResourceOwner != existingGrant.ResourceOwner { + return nil, zerrors.ThrowPreconditionFailed(nil, "PROJECT-byscAarAST", "Errors.Project.Grant.Invalid") + } + // return if project grant is already active + if existingGrant.State == domain.ProjectGrantStateActive { + return writeModelToObjectDetails(&existingGrant.WriteModel), nil + } + // error if project grant is neither active nor inactive if existingGrant.State != domain.ProjectGrantStateInactive { return details, zerrors.ThrowPreconditionFailed(nil, "PROJECT-47fu8", "Errors.Project.Grant.NotInactive") } - projectAgg := ProjectAggregateFromWriteModel(&existingGrant.WriteModel) - pushedEvents, err := c.eventstore.Push(ctx, project.NewGrantReactivatedEvent(ctx, projectAgg, grantID)) + if err := c.checkPermissionWriteProjectGrant(ctx, existingGrant.ResourceOwner, existingGrant.GrantID); err != nil { + return nil, err + } + pushedEvents, err := c.eventstore.Push(ctx, + project.NewGrantReactivatedEvent(ctx, + ProjectAggregateFromWriteModelWithCTX(ctx, &existingGrant.WriteModel), + grantID, + ), + ) if err != nil { return nil, err } @@ -209,9 +279,20 @@ func (c *Commands) RemoveProjectGrant(ctx context.Context, projectID, grantID, r if err != nil { return details, err } + // return if project grant does not exist, or was removed already + if !existingGrant.State.Exists() { + return writeModelToObjectDetails(&existingGrant.WriteModel), nil + } + if err := c.checkPermissionDeleteProjectGrant(ctx, existingGrant.ResourceOwner, existingGrant.GrantID); err != nil { + return nil, err + } events := make([]eventstore.Command, 0) - projectAgg := ProjectAggregateFromWriteModel(&existingGrant.WriteModel) - events = append(events, project.NewGrantRemovedEvent(ctx, projectAgg, grantID, existingGrant.GrantedOrgID)) + events = append(events, project.NewGrantRemovedEvent(ctx, + ProjectAggregateFromWriteModelWithCTX(ctx, &existingGrant.WriteModel), + grantID, + existingGrant.GrantedOrgID, + ), + ) for _, userGrantID := range cascadeUserGrantIDs { event, _, err := c.removeUserGrant(ctx, userGrantID, "", true) @@ -232,6 +313,10 @@ func (c *Commands) RemoveProjectGrant(ctx context.Context, projectID, grantID, r return writeModelToObjectDetails(&existingGrant.WriteModel), nil } +func (c *Commands) checkPermissionDeleteProjectGrant(ctx context.Context, resourceOwner, projectGrantID string) error { + return c.checkPermission(ctx, domain.PermissionProjectGrantDelete, resourceOwner, projectGrantID) +} + func (c *Commands) projectGrantWriteModelByID(ctx context.Context, grantID, projectID, resourceOwner string) (member *ProjectGrantWriteModel, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() @@ -249,55 +334,61 @@ func (c *Commands) projectGrantWriteModelByID(ctx context.Context, grantID, proj return writeModel, nil } -func (c *Commands) checkProjectGrantPreCondition(ctx context.Context, projectGrant *domain.ProjectGrant, resourceOwner string) error { +func (c *Commands) checkProjectGrantPreCondition(ctx context.Context, projectID, grantedOrgID, resourceOwner string, roles []string) (string, error) { if !authz.GetFeatures(ctx).ShouldUseImprovedPerformance(feature.ImprovedPerformanceTypeProjectGrant) { - return c.checkProjectGrantPreConditionOld(ctx, projectGrant, resourceOwner) + return c.checkProjectGrantPreConditionOld(ctx, projectID, grantedOrgID, resourceOwner, roles) } - existingRoleKeys, err := c.searchProjectGrantState(ctx, projectGrant.AggregateID, projectGrant.GrantedOrgID, resourceOwner) + projectResourceOwner, existingRoleKeys, err := c.searchProjectGrantState(ctx, projectID, grantedOrgID, resourceOwner) if err != nil { - return err + return "", err } - if projectGrant.HasInvalidRoles(existingRoleKeys) { - return zerrors.ThrowPreconditionFailed(err, "COMMAND-6m9gd", "Errors.Project.Role.NotFound") + if domain.HasInvalidRoles(existingRoleKeys, roles) { + return "", zerrors.ThrowPreconditionFailed(err, "COMMAND-6m9gd", "Errors.Project.Role.NotFound") } - return nil + return projectResourceOwner, nil } -func (c *Commands) searchProjectGrantState(ctx context.Context, projectID, grantedOrgID, resourceOwner string) (existingRoleKeys []string, err error) { +func (c *Commands) searchProjectGrantState(ctx context.Context, projectID, grantedOrgID, resourceOwner string) (_ string, existingRoleKeys []string, err error) { + projectStateQuery := map[eventstore.FieldType]any{ + eventstore.FieldTypeAggregateType: project.AggregateType, + eventstore.FieldTypeAggregateID: projectID, + eventstore.FieldTypeFieldName: project.ProjectStateSearchField, + eventstore.FieldTypeObjectType: project.ProjectSearchType, + } + grantedOrgQuery := map[eventstore.FieldType]any{ + eventstore.FieldTypeAggregateType: org.AggregateType, + eventstore.FieldTypeAggregateID: grantedOrgID, + eventstore.FieldTypeFieldName: org.OrgStateSearchField, + eventstore.FieldTypeObjectType: org.OrgSearchType, + } + roleQuery := map[eventstore.FieldType]any{ + eventstore.FieldTypeAggregateType: project.AggregateType, + eventstore.FieldTypeAggregateID: projectID, + eventstore.FieldTypeFieldName: project.ProjectRoleKeySearchField, + eventstore.FieldTypeObjectType: project.ProjectRoleSearchType, + } + + // as resourceowner is not always provided, it has to be separately + if resourceOwner != "" { + projectStateQuery[eventstore.FieldTypeResourceOwner] = resourceOwner + roleQuery[eventstore.FieldTypeResourceOwner] = resourceOwner + } + results, err := c.eventstore.Search( ctx, - // project state query - map[eventstore.FieldType]any{ - eventstore.FieldTypeResourceOwner: resourceOwner, - eventstore.FieldTypeAggregateType: project.AggregateType, - eventstore.FieldTypeAggregateID: projectID, - eventstore.FieldTypeFieldName: project.ProjectStateSearchField, - eventstore.FieldTypeObjectType: project.ProjectSearchType, - }, - // granted org query - map[eventstore.FieldType]any{ - eventstore.FieldTypeAggregateType: org.AggregateType, - eventstore.FieldTypeAggregateID: grantedOrgID, - eventstore.FieldTypeFieldName: org.OrgStateSearchField, - eventstore.FieldTypeObjectType: org.OrgSearchType, - }, - // role query - map[eventstore.FieldType]any{ - eventstore.FieldTypeResourceOwner: resourceOwner, - eventstore.FieldTypeAggregateType: project.AggregateType, - eventstore.FieldTypeAggregateID: projectID, - eventstore.FieldTypeFieldName: project.ProjectRoleKeySearchField, - eventstore.FieldTypeObjectType: project.ProjectRoleSearchType, - }, + projectStateQuery, + grantedOrgQuery, + roleQuery, ) if err != nil { - return nil, err + return "", nil, err } var ( - existsProject bool - existsGrantedOrg bool + existsProject bool + existingProjectResourceOwner string + existsGrantedOrg bool ) for _, result := range results { @@ -306,31 +397,32 @@ func (c *Commands) searchProjectGrantState(ctx context.Context, projectID, grant var role string err := result.Value.Unmarshal(&role) if err != nil { - return nil, err + return "", nil, err } existingRoleKeys = append(existingRoleKeys, role) case org.OrgSearchType: var state domain.OrgState err := result.Value.Unmarshal(&state) if err != nil { - return nil, err + return "", nil, err } existsGrantedOrg = state.Valid() && state != domain.OrgStateRemoved case project.ProjectSearchType: var state domain.ProjectState err := result.Value.Unmarshal(&state) if err != nil { - return nil, err + return "", nil, err } existsProject = state.Valid() && state != domain.ProjectStateRemoved + existingProjectResourceOwner = result.Aggregate.ResourceOwner } } if !existsProject { - return nil, zerrors.ThrowPreconditionFailed(err, "COMMAND-m9gsd", "Errors.Project.NotFound") + return "", nil, zerrors.ThrowPreconditionFailed(err, "COMMAND-m9gsd", "Errors.Project.NotFound") } if !existsGrantedOrg { - return nil, zerrors.ThrowPreconditionFailed(err, "COMMAND-3m9gg", "Errors.Org.NotFound") + return "", nil, zerrors.ThrowPreconditionFailed(err, "COMMAND-3m9gg", "Errors.Org.NotFound") } - return existingRoleKeys, nil + return existingProjectResourceOwner, existingRoleKeys, nil } diff --git a/internal/command/project_grant_model.go b/internal/command/project_grant_model.go index c658b00b69..a8c1fe2850 100644 --- a/internal/command/project_grant_model.go +++ b/internal/command/project_grant_model.go @@ -133,14 +133,18 @@ func (wm *ProjectGrantPreConditionReadModel) Reduce() error { for _, event := range wm.Events { switch e := event.(type) { case *project.ProjectAddedEvent: - if e.Aggregate().ResourceOwner != wm.ResourceOwner { + if wm.ResourceOwner == "" { + wm.ResourceOwner = e.Aggregate().ResourceOwner + } + if wm.ResourceOwner != e.Aggregate().ResourceOwner { continue } wm.ProjectExists = true case *project.ProjectRemovedEvent: - if e.Aggregate().ResourceOwner != wm.ResourceOwner { + if wm.ResourceOwner != e.Aggregate().ResourceOwner { continue } + wm.ResourceOwner = "" wm.ProjectExists = false case *project.RoleAddedEvent: if e.Aggregate().ResourceOwner != wm.ResourceOwner { diff --git a/internal/command/project_grant_test.go b/internal/command/project_grant_test.go index e39835e2f4..f1befa0de2 100644 --- a/internal/command/project_grant_test.go +++ b/internal/command/project_grant_test.go @@ -19,16 +19,16 @@ import ( func TestCommandSide_AddProjectGrant(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore - idGenerator id.Generator + eventstore func(t *testing.T) *eventstore.Eventstore + idGenerator id.Generator + checkPermission domain.PermissionCheck } type args struct { - ctx context.Context - projectGrant *domain.ProjectGrant - resourceOwner string + ctx context.Context + projectGrant *AddProjectGrant } type res struct { - want *domain.ProjectGrant + want *domain.ObjectDetails err func(error) bool } tests := []struct { @@ -40,18 +40,18 @@ func TestCommandSide_AddProjectGrant(t *testing.T) { { name: "invalid usergrant, error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), - projectGrant: &domain.ProjectGrant{ + projectGrant: &AddProjectGrant{ ObjectRoot: models.ObjectRoot{ - AggregateID: "project1", + AggregateID: "project1", + ResourceOwner: "org1", }, + GrantID: "grant1", }, - resourceOwner: "org1", }, res: res{ err: zerrors.IsErrorInvalidArgument, @@ -60,20 +60,21 @@ func TestCommandSide_AddProjectGrant(t *testing.T) { { name: "project not existing, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), - projectGrant: &domain.ProjectGrant{ + projectGrant: &AddProjectGrant{ ObjectRoot: models.ObjectRoot{ - AggregateID: "project1", + AggregateID: "project1", + ResourceOwner: "org1", }, + GrantID: "grant1", GrantedOrgID: "grantedorg1", }, - resourceOwner: "org1", }, res: res{ err: zerrors.IsPreconditionFailed, @@ -82,8 +83,7 @@ func TestCommandSide_AddProjectGrant(t *testing.T) { { name: "project not existing in org, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -108,16 +108,18 @@ func TestCommandSide_AddProjectGrant(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), - projectGrant: &domain.ProjectGrant{ + projectGrant: &AddProjectGrant{ ObjectRoot: models.ObjectRoot{ - AggregateID: "project1", + AggregateID: "project1", + ResourceOwner: "org1", }, + GrantID: "grant1", GrantedOrgID: "grantedorg1", }, - resourceOwner: "org1", }, res: res{ err: zerrors.IsPreconditionFailed, @@ -126,8 +128,7 @@ func TestCommandSide_AddProjectGrant(t *testing.T) { { name: "granted org not existing, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -138,16 +139,18 @@ func TestCommandSide_AddProjectGrant(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), - projectGrant: &domain.ProjectGrant{ + projectGrant: &AddProjectGrant{ ObjectRoot: models.ObjectRoot{ - AggregateID: "project1", + AggregateID: "project1", + ResourceOwner: "org1", }, + GrantID: "grant1", GrantedOrgID: "grantedorg1", }, - resourceOwner: "org1", }, res: res{ err: zerrors.IsPreconditionFailed, @@ -156,8 +159,7 @@ func TestCommandSide_AddProjectGrant(t *testing.T) { { name: "project roles not existing, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -174,27 +176,74 @@ func TestCommandSide_AddProjectGrant(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), - projectGrant: &domain.ProjectGrant{ + projectGrant: &AddProjectGrant{ ObjectRoot: models.ObjectRoot{ - AggregateID: "project1", + AggregateID: "project1", + ResourceOwner: "org1", }, + GrantID: "grant1", GrantedOrgID: "grantedorg1", RoleKeys: []string{"key1"}, }, - resourceOwner: "org1", }, res: res{ err: zerrors.IsPreconditionFailed, }, }, { - name: "usergrant for project, ok", + name: "grant for project, same resourceowner", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectname1", true, true, true, + domain.PrivateLabelingSettingUnspecified, + ), + ), + eventFromEventPusher( + org.NewOrgAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "granted org", + ), + ), + eventFromEventPusher( + project.NewRoleAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "key1", + "key", + "", + ), + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "projectgrant1"), + }, + args: args{ + ctx: context.Background(), + projectGrant: &AddProjectGrant{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + ResourceOwner: "org1", + }, + GrantedOrgID: "org1", + RoleKeys: []string{"key1"}, + }, + }, + res: res{ + err: zerrors.IsPreconditionFailed, + }, + }, + { + name: "grant for project, ok", + fields: fields{ + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -227,21 +276,67 @@ func TestCommandSide_AddProjectGrant(t *testing.T) { ), ), ), - idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "projectgrant1"), + checkPermission: newMockPermissionCheckAllowed(), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "projectgrant1"), }, args: args{ ctx: context.Background(), - projectGrant: &domain.ProjectGrant{ + projectGrant: &AddProjectGrant{ ObjectRoot: models.ObjectRoot{ - AggregateID: "project1", + AggregateID: "project1", + ResourceOwner: "org1", }, GrantedOrgID: "grantedorg1", RoleKeys: []string{"key1"}, }, - resourceOwner: "org1", }, res: res{ - want: &domain.ProjectGrant{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "grant for project, id, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectname1", true, true, true, + domain.PrivateLabelingSettingUnspecified, + ), + ), + eventFromEventPusher( + org.NewOrgAddedEvent(context.Background(), + &org.NewAggregate("grantedorg1").Aggregate, + "granted org", + ), + ), + eventFromEventPusher( + project.NewRoleAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "key1", + "key", + "", + ), + ), + ), + expectPush( + project.NewGrantAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "grantedorg1", + []string{"key1"}, + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + projectGrant: &AddProjectGrant{ ObjectRoot: models.ObjectRoot{ AggregateID: "project1", ResourceOwner: "org1", @@ -249,7 +344,11 @@ func TestCommandSide_AddProjectGrant(t *testing.T) { GrantID: "projectgrant1", GrantedOrgID: "grantedorg1", RoleKeys: []string{"key1"}, - State: domain.ProjectGrantStateActive, + }, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", }, }, }, @@ -257,10 +356,11 @@ func TestCommandSide_AddProjectGrant(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, - idGenerator: tt.fields.idGenerator, + eventstore: tt.fields.eventstore(t), + idGenerator: tt.fields.idGenerator, + checkPermission: tt.fields.checkPermission, } - got, err := r.AddProjectGrant(tt.args.ctx, tt.args.projectGrant, tt.args.resourceOwner) + got, err := r.AddProjectGrant(tt.args.ctx, tt.args.projectGrant) if tt.res.err == nil { assert.NoError(t, err) } @@ -268,7 +368,8 @@ func TestCommandSide_AddProjectGrant(t *testing.T) { t.Errorf("got wrong err: %v ", err) } if tt.res.err == nil { - assert.Equal(t, tt.res.want, got) + assert.NotEmpty(t, got.ID) + assertObjectDetails(t, tt.res.want, got) } }) } @@ -276,16 +377,16 @@ func TestCommandSide_AddProjectGrant(t *testing.T) { func TestCommandSide_ChangeProjectGrant(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore + checkPermission domain.PermissionCheck } type args struct { ctx context.Context - projectGrant *domain.ProjectGrant - resourceOwner string + projectGrant *ChangeProjectGrant cascadeUserGrantIDs []string } type res struct { - want *domain.ProjectGrant + want *domain.ObjectDetails err func(error) bool } tests := []struct { @@ -297,18 +398,17 @@ func TestCommandSide_ChangeProjectGrant(t *testing.T) { { name: "invalid projectgrant, error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), - projectGrant: &domain.ProjectGrant{ + projectGrant: &ChangeProjectGrant{ ObjectRoot: models.ObjectRoot{ - AggregateID: "project1", + AggregateID: "project1", + ResourceOwner: "org1", }, }, - resourceOwner: "org1", }, res: res{ err: zerrors.IsErrorInvalidArgument, @@ -317,22 +417,21 @@ func TestCommandSide_ChangeProjectGrant(t *testing.T) { { name: "projectgrant not existing, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), - projectGrant: &domain.ProjectGrant{ + projectGrant: &ChangeProjectGrant{ ObjectRoot: models.ObjectRoot{ - AggregateID: "project1", + AggregateID: "project1", + ResourceOwner: "org1", }, - GrantID: "projectgrant1", - GrantedOrgID: "grantedorg1", - RoleKeys: []string{"key1"}, + GrantID: "projectgrant1", + RoleKeys: []string{"key1"}, }, - resourceOwner: "org1", }, res: res{ err: zerrors.IsNotFound, @@ -341,8 +440,7 @@ func TestCommandSide_ChangeProjectGrant(t *testing.T) { { name: "project not existing, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher(project.NewGrantAddedEvent(context.Background(), &project.NewAggregate("project1", "org1").Aggregate, @@ -353,17 +451,17 @@ func TestCommandSide_ChangeProjectGrant(t *testing.T) { ), expectFilter(), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), - projectGrant: &domain.ProjectGrant{ + projectGrant: &ChangeProjectGrant{ ObjectRoot: models.ObjectRoot{ - AggregateID: "project1", + AggregateID: "project1", + ResourceOwner: "org1", }, - GrantID: "projectgrant1", - GrantedOrgID: "grantedorg1", + GrantID: "projectgrant1", }, - resourceOwner: "org1", }, res: res{ err: zerrors.IsPreconditionFailed, @@ -372,8 +470,7 @@ func TestCommandSide_ChangeProjectGrant(t *testing.T) { { name: "project not existing in org, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher(project.NewGrantAddedEvent(context.Background(), &project.NewAggregate("project1", "org1").Aggregate, @@ -398,18 +495,18 @@ func TestCommandSide_ChangeProjectGrant(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), - projectGrant: &domain.ProjectGrant{ + projectGrant: &ChangeProjectGrant{ ObjectRoot: models.ObjectRoot{ - AggregateID: "project1", + AggregateID: "project1", + ResourceOwner: "org1", }, - GrantID: "projectgrant1", - GrantedOrgID: "grantedorg1", - RoleKeys: []string{"key1"}, + GrantID: "projectgrant1", + RoleKeys: []string{"key1"}, }, - resourceOwner: "org1", }, res: res{ err: zerrors.IsPreconditionFailed, @@ -418,8 +515,7 @@ func TestCommandSide_ChangeProjectGrant(t *testing.T) { { name: "granted org not existing, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher(project.NewGrantAddedEvent(context.Background(), &project.NewAggregate("project1", "org1").Aggregate, @@ -438,17 +534,17 @@ func TestCommandSide_ChangeProjectGrant(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), - projectGrant: &domain.ProjectGrant{ + projectGrant: &ChangeProjectGrant{ ObjectRoot: models.ObjectRoot{ - AggregateID: "project1", + AggregateID: "project1", + ResourceOwner: "org1", }, - GrantID: "projectgrant1", - GrantedOrgID: "grantedorg1", + GrantID: "projectgrant1", }, - resourceOwner: "org1", }, res: res{ err: zerrors.IsPreconditionFailed, @@ -457,8 +553,7 @@ func TestCommandSide_ChangeProjectGrant(t *testing.T) { { name: "project roles not existing, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher(project.NewGrantAddedEvent(context.Background(), &project.NewAggregate("project1", "org1").Aggregate, @@ -483,18 +578,18 @@ func TestCommandSide_ChangeProjectGrant(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), - projectGrant: &domain.ProjectGrant{ + projectGrant: &ChangeProjectGrant{ ObjectRoot: models.ObjectRoot{ - AggregateID: "project1", + AggregateID: "project1", + ResourceOwner: "org1", }, - GrantID: "projectgrant1", - GrantedOrgID: "grantedorg1", - RoleKeys: []string{"key1"}, + GrantID: "projectgrant1", + RoleKeys: []string{"key1"}, }, - resourceOwner: "org1", }, res: res{ err: zerrors.IsPreconditionFailed, @@ -503,8 +598,7 @@ func TestCommandSide_ChangeProjectGrant(t *testing.T) { { name: "projectgrant not changed, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher(project.NewGrantAddedEvent(context.Background(), &project.NewAggregate("project1", "org1").Aggregate, @@ -537,28 +631,29 @@ func TestCommandSide_ChangeProjectGrant(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), - projectGrant: &domain.ProjectGrant{ + projectGrant: &ChangeProjectGrant{ ObjectRoot: models.ObjectRoot{ - AggregateID: "project1", + AggregateID: "project1", + ResourceOwner: "org1", }, - GrantID: "projectgrant1", - GrantedOrgID: "grantedorg1", - RoleKeys: []string{"key1"}, + GrantID: "projectgrant1", + RoleKeys: []string{"key1"}, }, - resourceOwner: "org1", }, res: res{ - err: zerrors.IsPreconditionFailed, + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, }, }, { name: "projectgrant only added roles, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher(project.NewGrantAddedEvent(context.Background(), &project.NewAggregate("project1", "org1").Aggregate, @@ -606,37 +701,29 @@ func TestCommandSide_ChangeProjectGrant(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), - projectGrant: &domain.ProjectGrant{ - ObjectRoot: models.ObjectRoot{ - AggregateID: "project1", - }, - GrantID: "projectgrant1", - GrantedOrgID: "grantedorg1", - RoleKeys: []string{"key1", "key2"}, - }, - resourceOwner: "org1", - }, - res: res{ - want: &domain.ProjectGrant{ + projectGrant: &ChangeProjectGrant{ ObjectRoot: models.ObjectRoot{ AggregateID: "project1", ResourceOwner: "org1", }, - GrantID: "projectgrant1", - GrantedOrgID: "grantedorg1", - RoleKeys: []string{"key1", "key2"}, - State: domain.ProjectGrantStateActive, + GrantID: "projectgrant1", + RoleKeys: []string{"key1", "key2"}, + }, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", }, }, }, { name: "projectgrant remove roles, usergrant not found, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher(project.NewGrantAddedEvent(context.Background(), &project.NewAggregate("project1", "org1").Aggregate, @@ -685,38 +772,30 @@ func TestCommandSide_ChangeProjectGrant(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), - projectGrant: &domain.ProjectGrant{ - ObjectRoot: models.ObjectRoot{ - AggregateID: "project1", - }, - GrantID: "projectgrant1", - GrantedOrgID: "grantedorg1", - RoleKeys: []string{"key1"}, - }, - resourceOwner: "org1", - cascadeUserGrantIDs: []string{"usergrant1"}, - }, - res: res{ - want: &domain.ProjectGrant{ + projectGrant: &ChangeProjectGrant{ ObjectRoot: models.ObjectRoot{ AggregateID: "project1", ResourceOwner: "org1", }, - GrantID: "projectgrant1", - GrantedOrgID: "grantedorg1", - RoleKeys: []string{"key1"}, - State: domain.ProjectGrantStateActive, + GrantID: "projectgrant1", + RoleKeys: []string{"key1"}, + }, + cascadeUserGrantIDs: []string{"usergrant1"}, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", }, }, }, { name: "projectgrant remove roles, usergrant not found, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher(project.NewGrantAddedEvent(context.Background(), &project.NewAggregate("project1", "org1").Aggregate, @@ -778,30 +857,23 @@ func TestCommandSide_ChangeProjectGrant(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), - projectGrant: &domain.ProjectGrant{ - ObjectRoot: models.ObjectRoot{ - AggregateID: "project1", - }, - GrantID: "projectgrant1", - GrantedOrgID: "grantedorg1", - RoleKeys: []string{"key1"}, - }, - resourceOwner: "org1", - cascadeUserGrantIDs: []string{"usergrant1"}, - }, - res: res{ - want: &domain.ProjectGrant{ + projectGrant: &ChangeProjectGrant{ ObjectRoot: models.ObjectRoot{ AggregateID: "project1", ResourceOwner: "org1", }, - GrantID: "projectgrant1", - GrantedOrgID: "grantedorg1", - RoleKeys: []string{"key1"}, - State: domain.ProjectGrantStateActive, + GrantID: "projectgrant1", + RoleKeys: []string{"key1"}, + }, + cascadeUserGrantIDs: []string{"usergrant1"}, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", }, }, }, @@ -809,9 +881,10 @@ func TestCommandSide_ChangeProjectGrant(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), + checkPermission: tt.fields.checkPermission, } - got, err := r.ChangeProjectGrant(tt.args.ctx, tt.args.projectGrant, tt.args.resourceOwner, tt.args.cascadeUserGrantIDs...) + got, err := r.ChangeProjectGrant(tt.args.ctx, tt.args.projectGrant, tt.args.cascadeUserGrantIDs...) if tt.res.err == nil { assert.NoError(t, err) } @@ -819,7 +892,7 @@ func TestCommandSide_ChangeProjectGrant(t *testing.T) { t.Errorf("got wrong err: %v ", err) } if tt.res.err == nil { - assert.Equal(t, tt.res.want, got) + assertObjectDetails(t, tt.res.want, got) } }) } @@ -827,7 +900,8 @@ func TestCommandSide_ChangeProjectGrant(t *testing.T) { func TestCommandSide_DeactivateProjectGrant(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore + checkPermission domain.PermissionCheck } type args struct { ctx context.Context @@ -848,9 +922,8 @@ func TestCommandSide_DeactivateProjectGrant(t *testing.T) { { name: "missing projectid, invalid error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -864,9 +937,8 @@ func TestCommandSide_DeactivateProjectGrant(t *testing.T) { { name: "missing grantid, invalid error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -880,10 +952,10 @@ func TestCommandSide_DeactivateProjectGrant(t *testing.T) { { name: "project not existing, precondition failed error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -898,8 +970,7 @@ func TestCommandSide_DeactivateProjectGrant(t *testing.T) { { name: "projectgrant not existing, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -911,6 +982,7 @@ func TestCommandSide_DeactivateProjectGrant(t *testing.T) { ), expectFilter(), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -923,10 +995,9 @@ func TestCommandSide_DeactivateProjectGrant(t *testing.T) { }, }, { - name: "projectgrant already deactivated, precondition error", + name: "projectgrant already deactivated, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -949,6 +1020,7 @@ func TestCommandSide_DeactivateProjectGrant(t *testing.T) { )), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -957,14 +1029,15 @@ func TestCommandSide_DeactivateProjectGrant(t *testing.T) { resourceOwner: "org1", }, res: res{ - err: zerrors.IsPreconditionFailed, + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, }, }, { name: "projectgrant deactivate, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -989,6 +1062,7 @@ func TestCommandSide_DeactivateProjectGrant(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -1006,7 +1080,8 @@ func TestCommandSide_DeactivateProjectGrant(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), + checkPermission: tt.fields.checkPermission, } got, err := r.DeactivateProjectGrant(tt.args.ctx, tt.args.projectID, tt.args.grantID, tt.args.resourceOwner) if tt.res.err == nil { @@ -1024,7 +1099,8 @@ func TestCommandSide_DeactivateProjectGrant(t *testing.T) { func TestCommandSide_ReactivateProjectGrant(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore + checkPermission domain.PermissionCheck } type args struct { ctx context.Context @@ -1045,9 +1121,8 @@ func TestCommandSide_ReactivateProjectGrant(t *testing.T) { { name: "missing projectid, invalid error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -1061,9 +1136,8 @@ func TestCommandSide_ReactivateProjectGrant(t *testing.T) { { name: "missing grantid, invalid error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -1077,10 +1151,10 @@ func TestCommandSide_ReactivateProjectGrant(t *testing.T) { { name: "project not existing, precondition failed error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -1095,8 +1169,7 @@ func TestCommandSide_ReactivateProjectGrant(t *testing.T) { { name: "projectgrant not existing, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -1108,6 +1181,7 @@ func TestCommandSide_ReactivateProjectGrant(t *testing.T) { ), expectFilter(), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -1122,8 +1196,7 @@ func TestCommandSide_ReactivateProjectGrant(t *testing.T) { { name: "projectgrant not inactive, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -1142,6 +1215,7 @@ func TestCommandSide_ReactivateProjectGrant(t *testing.T) { )), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -1150,14 +1224,15 @@ func TestCommandSide_ReactivateProjectGrant(t *testing.T) { resourceOwner: "org1", }, res: res{ - err: zerrors.IsPreconditionFailed, + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, }, }, { name: "projectgrant reactivate, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -1186,6 +1261,7 @@ func TestCommandSide_ReactivateProjectGrant(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -1203,7 +1279,8 @@ func TestCommandSide_ReactivateProjectGrant(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), + checkPermission: tt.fields.checkPermission, } got, err := r.ReactivateProjectGrant(tt.args.ctx, tt.args.projectID, tt.args.grantID, tt.args.resourceOwner) if tt.res.err == nil { @@ -1221,7 +1298,8 @@ func TestCommandSide_ReactivateProjectGrant(t *testing.T) { func TestCommandSide_RemoveProjectGrant(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore + checkPermission domain.PermissionCheck } type args struct { ctx context.Context @@ -1243,9 +1321,8 @@ func TestCommandSide_RemoveProjectGrant(t *testing.T) { { name: "missing projectid, invalid error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -1259,9 +1336,8 @@ func TestCommandSide_RemoveProjectGrant(t *testing.T) { { name: "missing grantid, invalid error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -1275,8 +1351,7 @@ func TestCommandSide_RemoveProjectGrant(t *testing.T) { { name: "project already removed, precondition failed error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher(project.NewGrantAddedEvent(context.Background(), &project.NewAggregate("project1", "org1").Aggregate, @@ -1293,6 +1368,7 @@ func TestCommandSide_RemoveProjectGrant(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -1307,10 +1383,10 @@ func TestCommandSide_RemoveProjectGrant(t *testing.T) { { name: "projectgrant not existing, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -1325,8 +1401,7 @@ func TestCommandSide_RemoveProjectGrant(t *testing.T) { { name: "projectgrant remove, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher(project.NewGrantAddedEvent(context.Background(), &project.NewAggregate("project1", "org1").Aggregate, @@ -1343,6 +1418,7 @@ func TestCommandSide_RemoveProjectGrant(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -1359,8 +1435,7 @@ func TestCommandSide_RemoveProjectGrant(t *testing.T) { { name: "projectgrant remove, cascading usergrant not found, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher(project.NewGrantAddedEvent(context.Background(), &project.NewAggregate("project1", "org1").Aggregate, @@ -1378,6 +1453,7 @@ func TestCommandSide_RemoveProjectGrant(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -1395,8 +1471,7 @@ func TestCommandSide_RemoveProjectGrant(t *testing.T) { { name: "projectgrant remove with cascading usergrants, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher(project.NewGrantAddedEvent(context.Background(), &project.NewAggregate("project1", "org1").Aggregate, @@ -1426,6 +1501,7 @@ func TestCommandSide_RemoveProjectGrant(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -1444,7 +1520,8 @@ func TestCommandSide_RemoveProjectGrant(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), + checkPermission: tt.fields.checkPermission, } got, err := r.RemoveProjectGrant(tt.args.ctx, tt.args.projectID, tt.args.grantID, tt.args.resourceOwner, tt.args.cascadeUserGrantIDs...) if tt.res.err == nil { diff --git a/internal/command/project_model.go b/internal/command/project_model.go index a46b07a8fe..cabceb8500 100644 --- a/internal/command/project_model.go +++ b/internal/command/project_model.go @@ -88,40 +88,35 @@ func (wm *ProjectWriteModel) Query() *eventstore.SearchQueryBuilder { func (wm *ProjectWriteModel) NewChangedEvent( ctx context.Context, aggregate *eventstore.Aggregate, - name string, + name *string, projectRoleAssertion, projectRoleCheck, - hasProjectCheck bool, - privateLabelingSetting domain.PrivateLabelingSetting, -) (*project.ProjectChangeEvent, bool, error) { + hasProjectCheck *bool, + privateLabelingSetting *domain.PrivateLabelingSetting, +) *project.ProjectChangeEvent { changes := make([]project.ProjectChanges, 0) - var err error oldName := "" - if wm.Name != name { + if name != nil && wm.Name != *name { oldName = wm.Name - changes = append(changes, project.ChangeName(name)) + changes = append(changes, project.ChangeName(*name)) } - if wm.ProjectRoleAssertion != projectRoleAssertion { - changes = append(changes, project.ChangeProjectRoleAssertion(projectRoleAssertion)) + if projectRoleAssertion != nil && wm.ProjectRoleAssertion != *projectRoleAssertion { + changes = append(changes, project.ChangeProjectRoleAssertion(*projectRoleAssertion)) } - if wm.ProjectRoleCheck != projectRoleCheck { - changes = append(changes, project.ChangeProjectRoleCheck(projectRoleCheck)) + if projectRoleCheck != nil && wm.ProjectRoleCheck != *projectRoleCheck { + changes = append(changes, project.ChangeProjectRoleCheck(*projectRoleCheck)) } - if wm.HasProjectCheck != hasProjectCheck { - changes = append(changes, project.ChangeHasProjectCheck(hasProjectCheck)) + if hasProjectCheck != nil && wm.HasProjectCheck != *hasProjectCheck { + changes = append(changes, project.ChangeHasProjectCheck(*hasProjectCheck)) } - if wm.PrivateLabelingSetting != privateLabelingSetting { - changes = append(changes, project.ChangePrivateLabelingSetting(privateLabelingSetting)) + if privateLabelingSetting != nil && wm.PrivateLabelingSetting != *privateLabelingSetting { + changes = append(changes, project.ChangePrivateLabelingSetting(*privateLabelingSetting)) } if len(changes) == 0 { - return nil, false, nil + return nil } - changeEvent, err := project.NewProjectChangeEvent(ctx, aggregate, oldName, changes) - if err != nil { - return nil, false, err - } - return changeEvent, true, nil + return project.NewProjectChangeEvent(ctx, aggregate, oldName, changes) } func isProjectStateExists(state domain.ProjectState) bool { @@ -132,6 +127,10 @@ func ProjectAggregateFromWriteModel(wm *eventstore.WriteModel) *eventstore.Aggre return eventstore.AggregateFromWriteModel(wm, project.AggregateType, project.AggregateVersion) } +func ProjectAggregateFromWriteModelWithCTX(ctx context.Context, wm *eventstore.WriteModel) *eventstore.Aggregate { + return project.AggregateFromWriteModel(ctx, wm) +} + func hasProjectState(check domain.ProjectState, states ...domain.ProjectState) bool { for _, state := range states { if check == state { diff --git a/internal/command/project_old.go b/internal/command/project_old.go index 35ea9b3ebb..e1b6f02721 100644 --- a/internal/command/project_old.go +++ b/internal/command/project_old.go @@ -9,18 +9,18 @@ import ( "github.com/zitadel/zitadel/internal/zerrors" ) -func (c *Commands) checkProjectExistsOld(ctx context.Context, projectID, resourceOwner string) (err error) { +func (c *Commands) checkProjectExistsOld(ctx context.Context, projectID, resourceOwner string) (_ string, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() projectWriteModel, err := c.getProjectWriteModelByID(ctx, projectID, resourceOwner) if err != nil { - return err + return "", err } if !isProjectStateExists(projectWriteModel.State) { - return zerrors.ThrowPreconditionFailed(nil, "COMMAND-EbFMN", "Errors.Project.NotFound") + return "", zerrors.ThrowPreconditionFailed(nil, "COMMAND-EbFMN", "Errors.Project.NotFound") } - return nil + return projectWriteModel.ResourceOwner, nil } func (c *Commands) deactivateProjectOld(ctx context.Context, projectID string, resourceOwner string) (*domain.ObjectDetails, error) { @@ -34,6 +34,9 @@ func (c *Commands) deactivateProjectOld(ctx context.Context, projectID string, r if existingProject.State != domain.ProjectStateActive { return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-mki55", "Errors.Project.NotActive") } + if err := c.checkPermissionUpdateProject(ctx, existingProject.ResourceOwner, existingProject.AggregateID); err != nil { + return nil, err + } //nolint: contextcheck projectAgg := ProjectAggregateFromWriteModel(&existingProject.WriteModel) @@ -59,6 +62,9 @@ func (c *Commands) reactivateProjectOld(ctx context.Context, projectID string, r if existingProject.State != domain.ProjectStateInactive { return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-5M9bs", "Errors.Project.NotInactive") } + if err := c.checkPermissionUpdateProject(ctx, existingProject.ResourceOwner, existingProject.AggregateID); err != nil { + return nil, err + } //nolint: contextcheck projectAgg := ProjectAggregateFromWriteModel(&existingProject.WriteModel) @@ -73,20 +79,20 @@ func (c *Commands) reactivateProjectOld(ctx context.Context, projectID string, r return writeModelToObjectDetails(&existingProject.WriteModel), nil } -func (c *Commands) checkProjectGrantPreConditionOld(ctx context.Context, projectGrant *domain.ProjectGrant, resourceOwner string) error { - preConditions := NewProjectGrantPreConditionReadModel(projectGrant.AggregateID, projectGrant.GrantedOrgID, resourceOwner) +func (c *Commands) checkProjectGrantPreConditionOld(ctx context.Context, projectID, grantedOrgID, resourceOwner string, roles []string) (string, error) { + preConditions := NewProjectGrantPreConditionReadModel(projectID, grantedOrgID, resourceOwner) err := c.eventstore.FilterToQueryReducer(ctx, preConditions) if err != nil { - return err + return "", err } if !preConditions.ProjectExists { - return zerrors.ThrowPreconditionFailed(err, "COMMAND-m9gsd", "Errors.Project.NotFound") + return "", zerrors.ThrowPreconditionFailed(err, "COMMAND-m9gsd", "Errors.Project.NotFound") } if !preConditions.GrantedOrgExists { - return zerrors.ThrowPreconditionFailed(err, "COMMAND-3m9gg", "Errors.Org.NotFound") + return "", zerrors.ThrowPreconditionFailed(err, "COMMAND-3m9gg", "Errors.Org.NotFound") } - if projectGrant.HasInvalidRoles(preConditions.ExistingRoleKeys) { - return zerrors.ThrowPreconditionFailed(err, "COMMAND-6m9gd", "Errors.Project.Role.NotFound") + if domain.HasInvalidRoles(preConditions.ExistingRoleKeys, roles) { + return "", zerrors.ThrowPreconditionFailed(err, "COMMAND-6m9gd", "Errors.Project.Role.NotFound") } - return nil + return preConditions.ResourceOwner, nil } diff --git a/internal/command/project_role.go b/internal/command/project_role.go index 3c8f9c725c..60d002b967 100644 --- a/internal/command/project_role.go +++ b/internal/command/project_role.go @@ -7,22 +7,45 @@ import ( "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/eventstore/v1/models" "github.com/zitadel/zitadel/internal/repository/project" "github.com/zitadel/zitadel/internal/telemetry/tracing" "github.com/zitadel/zitadel/internal/zerrors" ) -func (c *Commands) AddProjectRole(ctx context.Context, projectRole *domain.ProjectRole, resourceOwner string) (_ *domain.ProjectRole, err error) { +type AddProjectRole struct { + models.ObjectRoot + + Key string + DisplayName string + Group string +} + +func (p *AddProjectRole) IsValid() bool { + return p.AggregateID != "" && p.Key != "" +} + +func (c *Commands) AddProjectRole(ctx context.Context, projectRole *AddProjectRole) (_ *domain.ObjectDetails, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - err = c.checkProjectExists(ctx, projectRole.AggregateID, resourceOwner) + projectResourceOwner, err := c.checkProjectExists(ctx, projectRole.AggregateID, projectRole.ResourceOwner) if err != nil { return nil, err } + if projectRole.ResourceOwner == "" { + projectRole.ResourceOwner = projectResourceOwner + } + if err := c.checkPermissionWriteProjectRole(ctx, projectRole.ResourceOwner, projectRole.Key); err != nil { + return nil, err + } - roleWriteModel := NewProjectRoleWriteModelWithKey(projectRole.Key, projectRole.AggregateID, resourceOwner) - projectAgg := ProjectAggregateFromWriteModel(&roleWriteModel.WriteModel) + roleWriteModel := NewProjectRoleWriteModelWithKey(projectRole.Key, projectRole.AggregateID, projectRole.ResourceOwner) + if roleWriteModel.ResourceOwner != projectResourceOwner { + return nil, zerrors.ThrowPreconditionFailed(nil, "PROJECT-RLB4UpqQSd", "Errors.Project.Role.Invalid") + } + + projectAgg := ProjectAggregateFromWriteModelWithCTX(ctx, &roleWriteModel.WriteModel) events, err := c.addProjectRoles(ctx, projectAgg, projectRole) if err != nil { return nil, err @@ -35,37 +58,45 @@ func (c *Commands) AddProjectRole(ctx context.Context, projectRole *domain.Proje if err != nil { return nil, err } - return roleWriteModelToRole(roleWriteModel), nil + return writeModelToObjectDetails(&roleWriteModel.WriteModel), nil } -func (c *Commands) BulkAddProjectRole(ctx context.Context, projectID, resourceOwner string, projectRoles []*domain.ProjectRole) (details *domain.ObjectDetails, err error) { - err = c.checkProjectExists(ctx, projectID, resourceOwner) +func (c *Commands) checkPermissionWriteProjectRole(ctx context.Context, resourceOwner, roleKey string) error { + return c.checkPermission(ctx, domain.PermissionProjectRoleWrite, resourceOwner, roleKey) +} + +func (c *Commands) BulkAddProjectRole(ctx context.Context, projectID, resourceOwner string, projectRoles []*AddProjectRole) (details *domain.ObjectDetails, err error) { + projectResourceOwner, err := c.checkProjectExists(ctx, projectID, resourceOwner) if err != nil { return nil, err } + for _, projectRole := range projectRoles { + if projectRole.ResourceOwner == "" { + projectRole.ResourceOwner = projectResourceOwner + } + if err := c.checkPermissionWriteProjectRole(ctx, projectRole.ResourceOwner, projectRole.Key); err != nil { + return nil, err + } + if projectRole.ResourceOwner != projectResourceOwner { + return nil, zerrors.ThrowPreconditionFailed(nil, "PROJECT-9ZXtdaJKJJ", "Errors.Project.Role.Invalid") + } + } roleWriteModel := NewProjectRoleWriteModel(projectID, resourceOwner) - projectAgg := ProjectAggregateFromWriteModel(&roleWriteModel.WriteModel) + projectAgg := ProjectAggregateFromWriteModelWithCTX(ctx, &roleWriteModel.WriteModel) events, err := c.addProjectRoles(ctx, projectAgg, projectRoles...) if err != nil { return details, err } - - pushedEvents, err := c.eventstore.Push(ctx, events...) - if err != nil { - return nil, err - } - err = AppendAndReduce(roleWriteModel, pushedEvents...) - if err != nil { - return nil, err - } - return writeModelToObjectDetails(&roleWriteModel.WriteModel), nil + return c.pushAppendAndReduceDetails(ctx, roleWriteModel, events...) } -func (c *Commands) addProjectRoles(ctx context.Context, projectAgg *eventstore.Aggregate, projectRoles ...*domain.ProjectRole) ([]eventstore.Command, error) { +func (c *Commands) addProjectRoles(ctx context.Context, projectAgg *eventstore.Aggregate, projectRoles ...*AddProjectRole) ([]eventstore.Command, error) { var events []eventstore.Command for _, projectRole := range projectRoles { - projectRole.AggregateID = projectAgg.ID + if projectRole.ResourceOwner != projectAgg.ResourceOwner { + return nil, zerrors.ThrowPreconditionFailed(nil, "PROJECT-4Q2WjlbHvc", "Errors.Project.Role.Invalid") + } if !projectRole.IsValid() { return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-4m9vS", "Errors.Project.Role.Invalid") } @@ -81,42 +112,55 @@ func (c *Commands) addProjectRoles(ctx context.Context, projectAgg *eventstore.A return events, nil } -func (c *Commands) ChangeProjectRole(ctx context.Context, projectRole *domain.ProjectRole, resourceOwner string) (_ *domain.ProjectRole, err error) { +type ChangeProjectRole struct { + models.ObjectRoot + + Key string + DisplayName string + Group string +} + +func (p *ChangeProjectRole) IsValid() bool { + return p.AggregateID != "" && p.Key != "" +} + +func (c *Commands) ChangeProjectRole(ctx context.Context, projectRole *ChangeProjectRole) (_ *domain.ObjectDetails, err error) { if !projectRole.IsValid() { return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-2ilfW", "Errors.Project.Invalid") } - err = c.checkProjectExists(ctx, projectRole.AggregateID, resourceOwner) + projectResourceOwner, err := c.checkProjectExists(ctx, projectRole.AggregateID, projectRole.ResourceOwner) if err != nil { return nil, err } + if projectRole.ResourceOwner == "" { + projectRole.ResourceOwner = projectResourceOwner + } + if err := c.checkPermissionWriteProjectRole(ctx, projectRole.ResourceOwner, projectRole.Key); err != nil { + return nil, err + } - existingRole, err := c.getProjectRoleWriteModelByID(ctx, projectRole.Key, projectRole.AggregateID, resourceOwner) + existingRole, err := c.getProjectRoleWriteModelByID(ctx, projectRole.Key, projectRole.AggregateID, projectRole.ResourceOwner) if err != nil { return nil, err } - if existingRole.State == domain.ProjectRoleStateUnspecified || existingRole.State == domain.ProjectRoleStateRemoved { + if !existingRole.State.Exists() { return nil, zerrors.ThrowNotFound(nil, "COMMAND-vv8M9", "Errors.Project.Role.NotExisting") } + if existingRole.ResourceOwner != projectResourceOwner { + return nil, zerrors.ThrowPreconditionFailed(nil, "PROJECT-3MizLWveMf", "Errors.Project.Role.Invalid") + } - projectAgg := ProjectAggregateFromWriteModel(&existingRole.WriteModel) + projectAgg := ProjectAggregateFromWriteModelWithCTX(ctx, &existingRole.WriteModel) changeEvent, changed, err := existingRole.NewProjectRoleChangedEvent(ctx, projectAgg, projectRole.Key, projectRole.DisplayName, projectRole.Group) if err != nil { return nil, err } if !changed { - return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-5M0cs", "Errors.NoChangesFound") + return writeModelToObjectDetails(&existingRole.WriteModel), nil } - pushedEvents, err := c.eventstore.Push(ctx, changeEvent) - if err != nil { - return nil, err - } - err = AppendAndReduce(existingRole, pushedEvents...) - if err != nil { - return nil, err - } - return roleWriteModelToRole(existingRole), nil + return c.pushAppendAndReduceDetails(ctx, existingRole, changeEvent) } func (c *Commands) RemoveProjectRole(ctx context.Context, projectID, key, resourceOwner string, cascadingProjectGrantIds []string, cascadeUserGrantIDs ...string) (details *domain.ObjectDetails, err error) { @@ -127,10 +171,14 @@ func (c *Commands) RemoveProjectRole(ctx context.Context, projectID, key, resour if err != nil { return details, err } - if existingRole.State == domain.ProjectRoleStateUnspecified || existingRole.State == domain.ProjectRoleStateRemoved { - return details, zerrors.ThrowNotFound(nil, "COMMAND-m9vMf", "Errors.Project.Role.NotExisting") + // return if project role is not existing + if !existingRole.State.Exists() { + return writeModelToObjectDetails(&existingRole.WriteModel), nil } - projectAgg := ProjectAggregateFromWriteModel(&existingRole.WriteModel) + if err := c.checkPermissionDeleteProjectRole(ctx, existingRole.ResourceOwner, existingRole.Key); err != nil { + return nil, err + } + projectAgg := ProjectAggregateFromWriteModelWithCTX(ctx, &existingRole.WriteModel) events := []eventstore.Command{ project.NewRoleRemovedEvent(ctx, projectAgg, key), } @@ -153,15 +201,11 @@ func (c *Commands) RemoveProjectRole(ctx context.Context, projectID, key, resour events = append(events, event) } - pushedEvents, err := c.eventstore.Push(ctx, events...) - if err != nil { - return nil, err - } - err = AppendAndReduce(existingRole, pushedEvents...) - if err != nil { - return nil, err - } - return writeModelToObjectDetails(&existingRole.WriteModel), nil + return c.pushAppendAndReduceDetails(ctx, existingRole, events...) +} + +func (c *Commands) checkPermissionDeleteProjectRole(ctx context.Context, resourceOwner, roleKey string) error { + return c.checkPermission(ctx, domain.PermissionProjectRoleDelete, resourceOwner, roleKey) } func (c *Commands) getProjectRoleWriteModelByID(ctx context.Context, key, projectID, resourceOwner string) (*ProjectRoleWriteModel, error) { diff --git a/internal/command/project_role_model.go b/internal/command/project_role_model.go index 641879f238..3fc9a6c814 100644 --- a/internal/command/project_role_model.go +++ b/internal/command/project_role_model.go @@ -17,6 +17,10 @@ type ProjectRoleWriteModel struct { State domain.ProjectRoleState } +func (wm *ProjectRoleWriteModel) GetWriteModel() *eventstore.WriteModel { + return &wm.WriteModel +} + func NewProjectRoleWriteModelWithKey(key, projectID, resourceOwner string) *ProjectRoleWriteModel { return &ProjectRoleWriteModel{ WriteModel: eventstore.WriteModel{ diff --git a/internal/command/project_role_test.go b/internal/command/project_role_test.go index 2dc7ee35a6..14db198270 100644 --- a/internal/command/project_role_test.go +++ b/internal/command/project_role_test.go @@ -16,15 +16,15 @@ import ( func TestCommandSide_AddProjectRole(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore + checkPermission domain.PermissionCheck } type args struct { - ctx context.Context - role *domain.ProjectRole - resourceOwner string + ctx context.Context + role *AddProjectRole } type res struct { - want *domain.ProjectRole + want *domain.ObjectDetails err func(error) bool } tests := []struct { @@ -36,8 +36,7 @@ func TestCommandSide_AddProjectRole(t *testing.T) { { name: "project not existing, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -55,16 +54,16 @@ func TestCommandSide_AddProjectRole(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), - role: &domain.ProjectRole{ + role: &AddProjectRole{ ObjectRoot: models.ObjectRoot{ AggregateID: "project1", }, Key: "key1", }, - resourceOwner: "org1", }, res: res{ err: zerrors.IsPreconditionFailed, @@ -73,8 +72,7 @@ func TestCommandSide_AddProjectRole(t *testing.T) { { name: "invalid role, error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -85,15 +83,15 @@ func TestCommandSide_AddProjectRole(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), - role: &domain.ProjectRole{ + role: &AddProjectRole{ ObjectRoot: models.ObjectRoot{ AggregateID: "project1", }, }, - resourceOwner: "org1", }, res: res{ err: zerrors.IsErrorInvalidArgument, @@ -102,8 +100,7 @@ func TestCommandSide_AddProjectRole(t *testing.T) { { name: "role key already exists, already exists error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -123,10 +120,11 @@ func TestCommandSide_AddProjectRole(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), - role: &domain.ProjectRole{ + role: &AddProjectRole{ ObjectRoot: models.ObjectRoot{ AggregateID: "project1", }, @@ -134,7 +132,6 @@ func TestCommandSide_AddProjectRole(t *testing.T) { DisplayName: "key", Group: "group", }, - resourceOwner: "org1", }, res: res{ err: zerrors.IsErrorAlreadyExists, @@ -143,8 +140,7 @@ func TestCommandSide_AddProjectRole(t *testing.T) { { name: "add role,ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -164,10 +160,11 @@ func TestCommandSide_AddProjectRole(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), - role: &domain.ProjectRole{ + role: &AddProjectRole{ ObjectRoot: models.ObjectRoot{ AggregateID: "project1", }, @@ -175,10 +172,41 @@ func TestCommandSide_AddProjectRole(t *testing.T) { DisplayName: "key", Group: "group", }, - resourceOwner: "org1", }, res: res{ - want: &domain.ProjectRole{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "add role, resourceowner, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectname1", true, true, true, + domain.PrivateLabelingSettingUnspecified, + ), + ), + ), + expectPush( + project.NewRoleAddedEvent( + context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "key1", + "key", + "group", + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + role: &AddProjectRole{ ObjectRoot: models.ObjectRoot{ AggregateID: "project1", ResourceOwner: "org1", @@ -188,14 +216,20 @@ func TestCommandSide_AddProjectRole(t *testing.T) { Group: "group", }, }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), + checkPermission: tt.fields.checkPermission, } - got, err := r.AddProjectRole(tt.args.ctx, tt.args.role, tt.args.resourceOwner) + got, err := r.AddProjectRole(tt.args.ctx, tt.args.role) if tt.res.err == nil { assert.NoError(t, err) } @@ -203,7 +237,7 @@ func TestCommandSide_AddProjectRole(t *testing.T) { t.Errorf("got wrong err: %v ", err) } if tt.res.err == nil { - assert.Equal(t, tt.res.want, got) + assertObjectDetails(t, tt.res.want, got) } }) } @@ -211,11 +245,12 @@ func TestCommandSide_AddProjectRole(t *testing.T) { func TestCommandSide_BulkAddProjectRole(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore + checkPermission domain.PermissionCheck } type args struct { ctx context.Context - roles []*domain.ProjectRole + roles []*AddProjectRole projectID string resourceOwner string } @@ -232,8 +267,7 @@ func TestCommandSide_BulkAddProjectRole(t *testing.T) { { name: "project not existing, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -251,10 +285,11 @@ func TestCommandSide_BulkAddProjectRole(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), - roles: []*domain.ProjectRole{ + roles: []*AddProjectRole{ { ObjectRoot: models.ObjectRoot{ AggregateID: "project1", @@ -271,8 +306,7 @@ func TestCommandSide_BulkAddProjectRole(t *testing.T) { { name: "invalid role, error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -283,10 +317,11 @@ func TestCommandSide_BulkAddProjectRole(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), - roles: []*domain.ProjectRole{ + roles: []*AddProjectRole{ { ObjectRoot: models.ObjectRoot{}, }, @@ -304,8 +339,7 @@ func TestCommandSide_BulkAddProjectRole(t *testing.T) { { name: "role key already exists, already exists error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -332,16 +366,23 @@ func TestCommandSide_BulkAddProjectRole(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), - roles: []*domain.ProjectRole{ + roles: []*AddProjectRole{ { + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + }, Key: "key1", DisplayName: "key", Group: "group", }, { + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + }, Key: "key2", DisplayName: "key2", Group: "group", @@ -357,8 +398,7 @@ func TestCommandSide_BulkAddProjectRole(t *testing.T) { { name: "add roles,ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -385,16 +425,23 @@ func TestCommandSide_BulkAddProjectRole(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), - roles: []*domain.ProjectRole{ + roles: []*AddProjectRole{ { + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + }, Key: "key1", DisplayName: "key", Group: "group", }, { + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + }, Key: "key2", DisplayName: "key2", Group: "group", @@ -413,7 +460,8 @@ func TestCommandSide_BulkAddProjectRole(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), + checkPermission: tt.fields.checkPermission, } got, err := r.BulkAddProjectRole(tt.args.ctx, tt.args.projectID, tt.args.resourceOwner, tt.args.roles) if tt.res.err == nil { @@ -431,15 +479,15 @@ func TestCommandSide_BulkAddProjectRole(t *testing.T) { func TestCommandSide_ChangeProjectRole(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore + checkPermission domain.PermissionCheck } type args struct { - ctx context.Context - role *domain.ProjectRole - resourceOwner string + ctx context.Context + role *ChangeProjectRole } type res struct { - want *domain.ProjectRole + want *domain.ObjectDetails err func(error) bool } tests := []struct { @@ -451,18 +499,16 @@ func TestCommandSide_ChangeProjectRole(t *testing.T) { { name: "invalid role, error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), - role: &domain.ProjectRole{ + role: &ChangeProjectRole{ ObjectRoot: models.ObjectRoot{ AggregateID: "project1", }, }, - resourceOwner: "org1", }, res: res{ err: zerrors.IsErrorInvalidArgument, @@ -471,8 +517,7 @@ func TestCommandSide_ChangeProjectRole(t *testing.T) { { name: "project not existing, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -490,16 +535,16 @@ func TestCommandSide_ChangeProjectRole(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), - role: &domain.ProjectRole{ + role: &ChangeProjectRole{ ObjectRoot: models.ObjectRoot{ AggregateID: "project1", }, Key: "key1", }, - resourceOwner: "org1", }, res: res{ err: zerrors.IsPreconditionFailed, @@ -508,8 +553,7 @@ func TestCommandSide_ChangeProjectRole(t *testing.T) { { name: "role removed, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -536,10 +580,11 @@ func TestCommandSide_ChangeProjectRole(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), - role: &domain.ProjectRole{ + role: &ChangeProjectRole{ ObjectRoot: models.ObjectRoot{ AggregateID: "project1", }, @@ -547,7 +592,6 @@ func TestCommandSide_ChangeProjectRole(t *testing.T) { DisplayName: "key", Group: "group", }, - resourceOwner: "org1", }, res: res{ err: zerrors.IsNotFound, @@ -556,8 +600,7 @@ func TestCommandSide_ChangeProjectRole(t *testing.T) { { name: "role not changed, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -578,10 +621,11 @@ func TestCommandSide_ChangeProjectRole(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), - role: &domain.ProjectRole{ + role: &ChangeProjectRole{ ObjectRoot: models.ObjectRoot{ AggregateID: "project1", }, @@ -589,17 +633,17 @@ func TestCommandSide_ChangeProjectRole(t *testing.T) { DisplayName: "key", Group: "group", }, - resourceOwner: "org1", }, res: res{ - err: zerrors.IsPreconditionFailed, + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, }, }, { name: "role changed, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -623,10 +667,11 @@ func TestCommandSide_ChangeProjectRole(t *testing.T) { newRoleChangedEvent(context.Background(), "project1", "org1", "key1", "keychanged", "groupchanged"), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), - role: &domain.ProjectRole{ + role: &ChangeProjectRole{ ObjectRoot: models.ObjectRoot{ AggregateID: "project1", }, @@ -634,17 +679,10 @@ func TestCommandSide_ChangeProjectRole(t *testing.T) { DisplayName: "keychanged", Group: "groupchanged", }, - resourceOwner: "org1", }, res: res{ - want: &domain.ProjectRole{ - ObjectRoot: models.ObjectRoot{ - AggregateID: "project1", - ResourceOwner: "org1", - }, - Key: "key1", - DisplayName: "keychanged", - Group: "groupchanged", + want: &domain.ObjectDetails{ + ResourceOwner: "org1", }, }, }, @@ -652,9 +690,10 @@ func TestCommandSide_ChangeProjectRole(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), + checkPermission: tt.fields.checkPermission, } - got, err := r.ChangeProjectRole(tt.args.ctx, tt.args.role, tt.args.resourceOwner) + got, err := r.ChangeProjectRole(tt.args.ctx, tt.args.role) if tt.res.err == nil { assert.NoError(t, err) } @@ -662,7 +701,7 @@ func TestCommandSide_ChangeProjectRole(t *testing.T) { t.Errorf("got wrong err: %v ", err) } if tt.res.err == nil { - assert.Equal(t, tt.res.want, got) + assertObjectDetails(t, tt.res.want, got) } }) } @@ -670,7 +709,8 @@ func TestCommandSide_ChangeProjectRole(t *testing.T) { func TestCommandSide_RemoveProjectRole(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore + checkPermission domain.PermissionCheck } type args struct { ctx context.Context @@ -693,9 +733,8 @@ func TestCommandSide_RemoveProjectRole(t *testing.T) { { name: "invalid projectid, error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -709,9 +748,8 @@ func TestCommandSide_RemoveProjectRole(t *testing.T) { { name: "invalid key, error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -726,10 +764,10 @@ func TestCommandSide_RemoveProjectRole(t *testing.T) { { name: "role not existing, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -738,14 +776,15 @@ func TestCommandSide_RemoveProjectRole(t *testing.T) { resourceOwner: "org1", }, res: res{ - err: zerrors.IsNotFound, + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, }, }, { name: "role removed, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewRoleAddedEvent(context.Background(), @@ -763,6 +802,7 @@ func TestCommandSide_RemoveProjectRole(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -771,14 +811,15 @@ func TestCommandSide_RemoveProjectRole(t *testing.T) { resourceOwner: "org1", }, res: res{ - err: zerrors.IsNotFound, + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, }, }, { name: "role removed, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewRoleAddedEvent(context.Background(), @@ -796,6 +837,7 @@ func TestCommandSide_RemoveProjectRole(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -812,8 +854,7 @@ func TestCommandSide_RemoveProjectRole(t *testing.T) { { name: "role removed with cascadingProjectGrantids, grant not found, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewRoleAddedEvent(context.Background(), @@ -832,6 +873,7 @@ func TestCommandSide_RemoveProjectRole(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -849,8 +891,7 @@ func TestCommandSide_RemoveProjectRole(t *testing.T) { { name: "role removed with cascadingProjectGrantids, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewRoleAddedEvent(context.Background(), @@ -883,6 +924,7 @@ func TestCommandSide_RemoveProjectRole(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -900,8 +942,7 @@ func TestCommandSide_RemoveProjectRole(t *testing.T) { { name: "role removed with cascadingUserGrantIDs, grant not found, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewRoleAddedEvent(context.Background(), @@ -920,6 +961,7 @@ func TestCommandSide_RemoveProjectRole(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -937,8 +979,7 @@ func TestCommandSide_RemoveProjectRole(t *testing.T) { { name: "role removed with cascadingUserGrantIDs, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewRoleAddedEvent(context.Background(), @@ -969,6 +1010,7 @@ func TestCommandSide_RemoveProjectRole(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -987,7 +1029,8 @@ func TestCommandSide_RemoveProjectRole(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), + checkPermission: tt.fields.checkPermission, } got, err := r.RemoveProjectRole(tt.args.ctx, tt.args.projectID, tt.args.key, tt.args.resourceOwner, tt.args.cascadingProjectGrantIDs, tt.args.cascadingUserGrantIDs...) if tt.res.err == nil { diff --git a/internal/command/project_test.go b/internal/command/project_test.go index 842e1aa640..6c03420f6b 100644 --- a/internal/command/project_test.go +++ b/internal/command/project_test.go @@ -4,6 +4,7 @@ import ( "context" "testing" + "github.com/muhlemmer/gu" "github.com/stretchr/testify/assert" "github.com/zitadel/zitadel/internal/api/authz" @@ -18,16 +19,16 @@ import ( func TestCommandSide_AddProject(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore - idGenerator id.Generator + eventstore func(t *testing.T) *eventstore.Eventstore + idGenerator id.Generator + checkPermission domain.PermissionCheck } type args struct { - ctx context.Context - project *domain.Project - resourceOwner string + ctx context.Context + project *AddProject } type res struct { - want *domain.Project + want *domain.ObjectDetails err func(error) bool } tests := []struct { @@ -39,14 +40,14 @@ func TestCommandSide_AddProject(t *testing.T) { { name: "invalid project, error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ - ctx: authz.WithInstanceID(context.Background(), "instanceID"), - project: &domain.Project{}, - resourceOwner: "org1", + ctx: authz.WithInstanceID(context.Background(), "instanceID"), + project: &AddProject{ + ObjectRoot: models.ObjectRoot{ResourceOwner: "org1"}, + }, }, res: res{ err: zerrors.IsErrorInvalidArgument, @@ -55,62 +56,51 @@ func TestCommandSide_AddProject(t *testing.T) { { name: "project, resourceowner empty", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: authz.WithInstanceID(context.Background(), "instanceID"), - project: &domain.Project{ + project: &AddProject{ + ObjectRoot: models.ObjectRoot{ResourceOwner: ""}, Name: "project", ProjectRoleAssertion: true, ProjectRoleCheck: true, HasProjectCheck: true, PrivateLabelingSetting: domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy, }, - resourceOwner: "", }, res: res{ err: zerrors.IsErrorInvalidArgument, }, }, { - name: "project, error already exists", + name: "project, no permission", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), - expectPushFailed(zerrors.ThrowAlreadyExists(nil, "ERROR", "internl"), - project.NewProjectAddedEvent( - context.Background(), - &project.NewAggregate("project1", "org1").Aggregate, - "project", true, true, true, - domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy, - ), - ), ), - idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "project1"), + checkPermission: newMockPermissionCheckNotAllowed(), }, args: args{ ctx: authz.WithInstanceID(context.Background(), "instanceID"), - project: &domain.Project{ + project: &AddProject{ + ObjectRoot: models.ObjectRoot{AggregateID: "project1", ResourceOwner: "org1"}, Name: "project", ProjectRoleAssertion: true, ProjectRoleCheck: true, HasProjectCheck: true, PrivateLabelingSetting: domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy, }, - resourceOwner: "org1", }, res: res{ - err: zerrors.IsErrorAlreadyExists, + err: zerrors.IsPermissionDenied, }, }, { name: "project, already exists", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -120,18 +110,19 @@ func TestCommandSide_AddProject(t *testing.T) { ), ), ), - idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "project1"), + checkPermission: newMockPermissionCheckAllowed(), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "project1"), }, args: args{ ctx: authz.WithInstanceID(context.Background(), "instanceID"), - project: &domain.Project{ + project: &AddProject{ + ObjectRoot: models.ObjectRoot{ResourceOwner: "org1"}, Name: "project", ProjectRoleAssertion: true, ProjectRoleCheck: true, HasProjectCheck: true, PrivateLabelingSetting: domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy, }, - resourceOwner: "org1", }, res: res{ err: zerrors.IsErrorAlreadyExists, @@ -140,8 +131,7 @@ func TestCommandSide_AddProject(t *testing.T) { { name: "project, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), expectPush( project.NewProjectAddedEvent( @@ -152,25 +142,46 @@ func TestCommandSide_AddProject(t *testing.T) { ), ), ), - idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "project1"), + checkPermission: newMockPermissionCheckAllowed(), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "project1"), }, args: args{ ctx: authz.WithInstanceID(context.Background(), "instanceID"), - project: &domain.Project{ + project: &AddProject{ + ObjectRoot: models.ObjectRoot{ResourceOwner: "org1"}, Name: "project", ProjectRoleAssertion: true, ProjectRoleCheck: true, HasProjectCheck: true, PrivateLabelingSetting: domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy, }, - resourceOwner: "org1", }, res: res{ - want: &domain.Project{ - ObjectRoot: models.ObjectRoot{ - ResourceOwner: "org1", - AggregateID: "project1", - }, + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "project, with id, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter(), + expectPush( + project.NewProjectAddedEvent( + context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "project", true, true, true, + domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy, + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: authz.WithInstanceID(context.Background(), "instanceID"), + project: &AddProject{ + ObjectRoot: models.ObjectRoot{AggregateID: "project1", ResourceOwner: "org1"}, Name: "project", ProjectRoleAssertion: true, ProjectRoleCheck: true, @@ -178,16 +189,22 @@ func TestCommandSide_AddProject(t *testing.T) { PrivateLabelingSetting: domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy, }, }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { c := &Commands{ - eventstore: tt.fields.eventstore, - idGenerator: tt.fields.idGenerator, + eventstore: tt.fields.eventstore(t), + idGenerator: tt.fields.idGenerator, + checkPermission: tt.fields.checkPermission, } c.setMilestonesCompletedForTest("instanceID") - got, err := c.AddProject(tt.args.ctx, tt.args.project, tt.args.resourceOwner) + got, err := c.AddProject(tt.args.ctx, tt.args.project) if tt.res.err == nil { assert.NoError(t, err) } @@ -195,7 +212,8 @@ func TestCommandSide_AddProject(t *testing.T) { t.Errorf("got wrong err: %v ", err) } if tt.res.err == nil { - assert.Equal(t, tt.res.want, got) + assert.NotEmpty(t, got.ID) + assertObjectDetails(t, tt.res.want, got) } }) } @@ -203,15 +221,16 @@ func TestCommandSide_AddProject(t *testing.T) { func TestCommandSide_ChangeProject(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore + checkPermission domain.PermissionCheck } type args struct { ctx context.Context - project *domain.Project + project *ChangeProject resourceOwner string } type res struct { - want *domain.Project + want *domain.ObjectDetails err func(error) bool } tests := []struct { @@ -223,16 +242,16 @@ func TestCommandSide_ChangeProject(t *testing.T) { { name: "invalid project, invalid error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), - project: &domain.Project{ + project: &ChangeProject{ ObjectRoot: models.ObjectRoot{ AggregateID: "project1", }, + Name: gu.Ptr(""), }, resourceOwner: "org1", }, @@ -243,14 +262,13 @@ func TestCommandSide_ChangeProject(t *testing.T) { { name: "invalid project empty aggregateid, invalid error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), - project: &domain.Project{ - Name: "project", + project: &ChangeProject{ + Name: gu.Ptr("project"), }, resourceOwner: "org1", }, @@ -261,18 +279,18 @@ func TestCommandSide_ChangeProject(t *testing.T) { { name: "project not existing, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), - project: &domain.Project{ + project: &ChangeProject{ ObjectRoot: models.ObjectRoot{ AggregateID: "project1", }, - Name: "project change", + Name: gu.Ptr("project change"), }, resourceOwner: "org1", }, @@ -283,8 +301,7 @@ func TestCommandSide_ChangeProject(t *testing.T) { { name: "project removed, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -300,14 +317,15 @@ func TestCommandSide_ChangeProject(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), - project: &domain.Project{ + project: &ChangeProject{ ObjectRoot: models.ObjectRoot{ AggregateID: "project1", }, - Name: "project change", + Name: gu.Ptr("project change"), }, resourceOwner: "org1", }, @@ -316,10 +334,9 @@ func TestCommandSide_ChangeProject(t *testing.T) { }, }, { - name: "no changes, precondition error", + name: "no changes, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -329,30 +346,65 @@ func TestCommandSide_ChangeProject(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), - project: &domain.Project{ + project: &ChangeProject{ ObjectRoot: models.ObjectRoot{ AggregateID: "project1", }, - Name: "project", - ProjectRoleAssertion: true, - ProjectRoleCheck: true, - HasProjectCheck: true, - PrivateLabelingSetting: domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy, + Name: gu.Ptr("project"), + ProjectRoleAssertion: gu.Ptr(true), + ProjectRoleCheck: gu.Ptr(true), + HasProjectCheck: gu.Ptr(true), + PrivateLabelingSetting: gu.Ptr(domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy), }, resourceOwner: "org1", }, res: res{ - err: zerrors.IsPreconditionFailed, + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "no changes, no permission", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "project", true, true, true, + domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy), + ), + ), + ), + checkPermission: newMockPermissionCheckNotAllowed(), + }, + args: args{ + ctx: context.Background(), + project: &ChangeProject{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + }, + Name: gu.Ptr("project"), + ProjectRoleAssertion: gu.Ptr(true), + ProjectRoleCheck: gu.Ptr(true), + HasProjectCheck: gu.Ptr(true), + PrivateLabelingSetting: gu.Ptr(domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy), + }, + resourceOwner: "org1", + }, + res: res{ + err: zerrors.IsPermissionDenied, }, }, { name: "project change with name and unique constraints, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -374,40 +426,32 @@ func TestCommandSide_ChangeProject(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), - project: &domain.Project{ + project: &ChangeProject{ ObjectRoot: models.ObjectRoot{ AggregateID: "project1", }, - Name: "project-new", - ProjectRoleAssertion: false, - ProjectRoleCheck: false, - HasProjectCheck: false, - PrivateLabelingSetting: domain.PrivateLabelingSettingEnforceProjectResourceOwnerPolicy, + Name: gu.Ptr("project-new"), + ProjectRoleAssertion: gu.Ptr(false), + ProjectRoleCheck: gu.Ptr(false), + HasProjectCheck: gu.Ptr(false), + PrivateLabelingSetting: gu.Ptr(domain.PrivateLabelingSettingEnforceProjectResourceOwnerPolicy), }, resourceOwner: "org1", }, res: res{ - want: &domain.Project{ - ObjectRoot: models.ObjectRoot{ - AggregateID: "project1", - ResourceOwner: "org1", - }, - Name: "project-new", - ProjectRoleAssertion: false, - ProjectRoleCheck: false, - HasProjectCheck: false, - PrivateLabelingSetting: domain.PrivateLabelingSettingEnforceProjectResourceOwnerPolicy, + want: &domain.ObjectDetails{ + ResourceOwner: "org1", }, }, }, { name: "project change without name and unique constraints, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -429,32 +473,25 @@ func TestCommandSide_ChangeProject(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), - project: &domain.Project{ + project: &ChangeProject{ ObjectRoot: models.ObjectRoot{ AggregateID: "project1", }, - Name: "project", - ProjectRoleAssertion: false, - ProjectRoleCheck: false, - HasProjectCheck: false, - PrivateLabelingSetting: domain.PrivateLabelingSettingEnforceProjectResourceOwnerPolicy, + Name: gu.Ptr("project"), + ProjectRoleAssertion: gu.Ptr(false), + ProjectRoleCheck: gu.Ptr(false), + HasProjectCheck: gu.Ptr(false), + PrivateLabelingSetting: gu.Ptr(domain.PrivateLabelingSettingEnforceProjectResourceOwnerPolicy), }, resourceOwner: "org1", }, res: res{ - want: &domain.Project{ - ObjectRoot: models.ObjectRoot{ - AggregateID: "project1", - ResourceOwner: "org1", - }, - Name: "project", - ProjectRoleAssertion: false, - ProjectRoleCheck: false, - HasProjectCheck: false, - PrivateLabelingSetting: domain.PrivateLabelingSettingEnforceProjectResourceOwnerPolicy, + want: &domain.ObjectDetails{ + ResourceOwner: "org1", }, }, }, @@ -462,9 +499,10 @@ func TestCommandSide_ChangeProject(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), + checkPermission: tt.fields.checkPermission, } - got, err := r.ChangeProject(tt.args.ctx, tt.args.project, tt.args.resourceOwner) + got, err := r.ChangeProject(tt.args.ctx, tt.args.project) if tt.res.err == nil { assert.NoError(t, err) } @@ -472,7 +510,7 @@ func TestCommandSide_ChangeProject(t *testing.T) { t.Errorf("got wrong err: %v ", err) } if tt.res.err == nil { - assert.Equal(t, tt.res.want, got) + assertObjectDetails(t, tt.res.want, got) } }) } @@ -480,7 +518,8 @@ func TestCommandSide_ChangeProject(t *testing.T) { func TestCommandSide_DeactivateProject(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore + checkPermission domain.PermissionCheck } type args struct { ctx context.Context @@ -500,9 +539,8 @@ func TestCommandSide_DeactivateProject(t *testing.T) { { name: "invalid project id, invalid error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -513,29 +551,13 @@ func TestCommandSide_DeactivateProject(t *testing.T) { err: zerrors.IsErrorInvalidArgument, }, }, - { - name: "invalid resourceowner, invalid error", - fields: fields{ - eventstore: eventstoreExpect( - t, - ), - }, - args: args{ - ctx: context.Background(), - projectID: "project1", - resourceOwner: "", - }, - res: res{ - err: zerrors.IsErrorInvalidArgument, - }, - }, { name: "project not existing, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -549,8 +571,7 @@ func TestCommandSide_DeactivateProject(t *testing.T) { { name: "project removed, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -566,6 +587,7 @@ func TestCommandSide_DeactivateProject(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -579,8 +601,7 @@ func TestCommandSide_DeactivateProject(t *testing.T) { { name: "project already inactive, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -594,6 +615,7 @@ func TestCommandSide_DeactivateProject(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -605,10 +627,9 @@ func TestCommandSide_DeactivateProject(t *testing.T) { }, }, { - name: "project deactivate, ok", + name: "project deactivate,no resourceOwner, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -622,6 +643,61 @@ func TestCommandSide_DeactivateProject(t *testing.T) { &project.NewAggregate("project1", "org1").Aggregate), ), ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + resourceOwner: "", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "project deactivate, no permission", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "project", true, true, true, + domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy), + ), + ), + ), + checkPermission: newMockPermissionCheckNotAllowed(), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + resourceOwner: "org1", + }, + res: res{ + err: zerrors.IsPermissionDenied, + }, + }, + { + name: "project deactivate, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "project", true, true, true, + domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy), + ), + ), + expectPush( + project.NewProjectDeactivatedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -638,7 +714,8 @@ func TestCommandSide_DeactivateProject(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), + checkPermission: tt.fields.checkPermission, } got, err := r.DeactivateProject(tt.args.ctx, tt.args.projectID, tt.args.resourceOwner) if tt.res.err == nil { @@ -656,7 +733,8 @@ func TestCommandSide_DeactivateProject(t *testing.T) { func TestCommandSide_ReactivateProject(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore + checkPermission domain.PermissionCheck } type args struct { ctx context.Context @@ -676,9 +754,8 @@ func TestCommandSide_ReactivateProject(t *testing.T) { { name: "invalid project id, invalid error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -689,29 +766,13 @@ func TestCommandSide_ReactivateProject(t *testing.T) { err: zerrors.IsErrorInvalidArgument, }, }, - { - name: "invalid resourceowner, invalid error", - fields: fields{ - eventstore: eventstoreExpect( - t, - ), - }, - args: args{ - ctx: context.Background(), - projectID: "project1", - resourceOwner: "", - }, - res: res{ - err: zerrors.IsErrorInvalidArgument, - }, - }, { name: "project not existing, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -725,8 +786,7 @@ func TestCommandSide_ReactivateProject(t *testing.T) { { name: "project removed, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -742,6 +802,7 @@ func TestCommandSide_ReactivateProject(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -755,8 +816,7 @@ func TestCommandSide_ReactivateProject(t *testing.T) { { name: "project not inactive, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -766,6 +826,7 @@ func TestCommandSide_ReactivateProject(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -777,10 +838,9 @@ func TestCommandSide_ReactivateProject(t *testing.T) { }, }, { - name: "project reactivate, ok", + name: "project reactivate, no resourceOwner, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -798,6 +858,69 @@ func TestCommandSide_ReactivateProject(t *testing.T) { &project.NewAggregate("project1", "org1").Aggregate), ), ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + resourceOwner: "", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "project reactivate, no permission", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "project", true, true, true, + domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy), + ), + eventFromEventPusher( + project.NewProjectDeactivatedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate), + ), + ), + ), + checkPermission: newMockPermissionCheckNotAllowed(), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + resourceOwner: "org1", + }, + res: res{ + err: zerrors.IsPermissionDenied, + }, + }, + { + name: "project reactivate, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "project", true, true, true, + domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy), + ), + eventFromEventPusher( + project.NewProjectDeactivatedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate), + ), + ), + expectPush( + project.NewProjectReactivatedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -814,7 +937,8 @@ func TestCommandSide_ReactivateProject(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), + checkPermission: tt.fields.checkPermission, } got, err := r.ReactivateProject(tt.args.ctx, tt.args.projectID, tt.args.resourceOwner) if tt.res.err == nil { @@ -832,7 +956,8 @@ func TestCommandSide_ReactivateProject(t *testing.T) { func TestCommandSide_RemoveProject(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore + checkPermission domain.PermissionCheck } type args struct { ctx context.Context @@ -852,9 +977,8 @@ func TestCommandSide_RemoveProject(t *testing.T) { { name: "invalid project id, invalid error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -868,9 +992,8 @@ func TestCommandSide_RemoveProject(t *testing.T) { { name: "invalid resourceowner, invalid error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -884,10 +1007,10 @@ func TestCommandSide_RemoveProject(t *testing.T) { { name: "project not existing, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -901,8 +1024,7 @@ func TestCommandSide_RemoveProject(t *testing.T) { { name: "project removed, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -918,6 +1040,7 @@ func TestCommandSide_RemoveProject(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -931,8 +1054,7 @@ func TestCommandSide_RemoveProject(t *testing.T) { { name: "project remove, without entityConstraints, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -950,6 +1072,7 @@ func TestCommandSide_RemoveProject(t *testing.T) { nil), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -965,8 +1088,7 @@ func TestCommandSide_RemoveProject(t *testing.T) { { name: "project remove, with entityConstraints, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -1003,6 +1125,7 @@ func TestCommandSide_RemoveProject(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -1018,8 +1141,7 @@ func TestCommandSide_RemoveProject(t *testing.T) { { name: "project remove, with multiple entityConstraints, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -1090,6 +1212,7 @@ func TestCommandSide_RemoveProject(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -1106,7 +1229,8 @@ func TestCommandSide_RemoveProject(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), + checkPermission: tt.fields.checkPermission, } got, err := r.RemoveProject(tt.args.ctx, tt.args.projectID, tt.args.resourceOwner) if tt.res.err == nil { @@ -1122,6 +1246,328 @@ func TestCommandSide_RemoveProject(t *testing.T) { } } +func TestCommandSide_DeleteProject(t *testing.T) { + type fields struct { + eventstore func(t *testing.T) *eventstore.Eventstore + checkPermission domain.PermissionCheck + } + type args struct { + ctx context.Context + projectID string + resourceOwner string + } + type res struct { + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "invalid project id, invalid error", + fields: fields{ + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + projectID: "", + resourceOwner: "org1", + }, + res: res{ + err: zerrors.IsErrorInvalidArgument, + }, + }, + { + name: "project not existing, not found error", + fields: fields{ + eventstore: expectEventstore( + expectFilter(), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + resourceOwner: "org1", + }, + res: res{ + err: nil, + }, + }, + { + name: "project removed, not found error", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "project", true, true, true, + domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy), + ), + eventFromEventPusher( + project.NewProjectRemovedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "project", + nil), + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + resourceOwner: "org1", + }, + res: res{ + err: nil, + }, + }, { + name: "project remove, no resourceOwner, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "project", true, true, true, + domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy), + ), + ), + // no saml application events + expectFilter(), + expectPush( + project.NewProjectRemovedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "project", + nil), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + resourceOwner: "", + }, + res: res{ + err: nil, + }, + }, + { + name: "project remove, no permission", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "project", true, true, true, + domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy), + ), + ), + ), + checkPermission: newMockPermissionCheckNotAllowed(), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + resourceOwner: "", + }, + res: res{ + err: zerrors.IsPermissionDenied, + }, + }, + { + name: "project remove, without entityConstraints, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "project", true, true, true, + domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy), + ), + ), + // no saml application events + expectFilter(), + expectPush( + project.NewProjectRemovedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "project", + nil), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + resourceOwner: "org1", + }, + res: res{ + err: nil, + }, + }, + { + name: "project remove, with entityConstraints, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "project", true, true, true, + domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy), + ), + ), + expectFilter( + eventFromEventPusher(project.NewApplicationAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "app1", + "app", + )), + eventFromEventPusher( + project.NewSAMLConfigAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "app1", + "https://test.com/saml/metadata", + []byte("\n\n \n urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified\n \n \n \n"), + "http://localhost:8080/saml/metadata", + domain.LoginVersionUnspecified, + "", + ), + ), + ), + expectPush( + project.NewProjectRemovedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "project", + []*eventstore.UniqueConstraint{ + project.NewRemoveSAMLConfigEntityIDUniqueConstraint("https://test.com/saml/metadata"), + }, + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + resourceOwner: "org1", + }, + res: res{ + err: nil, + }, + }, + { + name: "project remove, with multiple entityConstraints, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "project", true, true, true, + domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy), + ), + ), + expectFilter( + eventFromEventPusher(project.NewApplicationAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "app1", + "app", + )), + eventFromEventPusher( + project.NewSAMLConfigAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "app1", + "https://test1.com/saml/metadata", + []byte("\n\n \n urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified\n \n \n \n"), + "", + domain.LoginVersionUnspecified, + "", + ), + ), + eventFromEventPusher(project.NewApplicationAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "app2", + "app", + )), + eventFromEventPusher( + project.NewSAMLConfigAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "app2", + "https://test2.com/saml/metadata", + []byte("\n\n \n urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified\n \n \n \n"), + "", + domain.LoginVersionUnspecified, + "", + ), + ), + eventFromEventPusher(project.NewApplicationAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "app3", + "app", + )), + eventFromEventPusher( + project.NewSAMLConfigAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "app3", + "https://test3.com/saml/metadata", + []byte("\n\n \n urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified\n \n \n \n"), + "", + domain.LoginVersionUnspecified, + "", + ), + ), + ), + expectPush( + project.NewProjectRemovedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "project", + []*eventstore.UniqueConstraint{ + project.NewRemoveSAMLConfigEntityIDUniqueConstraint("https://test1.com/saml/metadata"), + project.NewRemoveSAMLConfigEntityIDUniqueConstraint("https://test2.com/saml/metadata"), + project.NewRemoveSAMLConfigEntityIDUniqueConstraint("https://test3.com/saml/metadata"), + }, + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + resourceOwner: "org1", + }, + res: res{ + err: nil, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore(t), + checkPermission: tt.fields.checkPermission, + } + _, err := r.DeleteProject(tt.args.ctx, tt.args.projectID, tt.args.resourceOwner) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + }) + } +} + func newProjectChangedEvent(ctx context.Context, projectID, resourceOwner, oldName, newName string, roleAssertion, roleCheck, hasProjectCheck bool, privateLabelingSetting domain.PrivateLabelingSetting) *project.ProjectChangeEvent { changes := []project.ProjectChanges{ project.ChangeProjectRoleAssertion(roleAssertion), @@ -1132,12 +1578,11 @@ func newProjectChangedEvent(ctx context.Context, projectID, resourceOwner, oldNa if newName != "" { changes = append(changes, project.ChangeName(newName)) } - event, _ := project.NewProjectChangeEvent(ctx, + return project.NewProjectChangeEvent(ctx, &project.NewAggregate(projectID, resourceOwner).Aggregate, oldName, changes, ) - return event } func TestAddProject(t *testing.T) { diff --git a/internal/domain/permission.go b/internal/domain/permission.go index 0ddf08a664..fd300f63b9 100644 --- a/internal/domain/permission.go +++ b/internal/domain/permission.go @@ -38,6 +38,15 @@ const ( PermissionOrgRead = "org.read" PermissionIDPRead = "iam.idp.read" PermissionOrgIDPRead = "org.idp.read" + PermissionProjectWrite = "project.write" + PermissionProjectRead = "project.read" + PermissionProjectDelete = "project.delete" + PermissionProjectGrantWrite = "project.grant.write" + PermissionProjectGrantRead = "project.grant.read" + PermissionProjectGrantDelete = "project.grant.delete" + PermissionProjectRoleWrite = "project.role.write" + PermissionProjectRoleRead = "project.role.read" + PermissionProjectRoleDelete = "project.role.delete" ) // ProjectPermissionCheck is used as a check for preconditions dependent on application, project, user resourceowner and usergrants. diff --git a/internal/domain/project_grant.go b/internal/domain/project_grant.go index 31c6d9edbc..6376c9ed74 100644 --- a/internal/domain/project_grant.go +++ b/internal/domain/project_grant.go @@ -26,17 +26,12 @@ func (s ProjectGrantState) Valid() bool { return s > ProjectGrantStateUnspecified && s < projectGrantStateMax } -func (p *ProjectGrant) IsValid() bool { - return p.GrantedOrgID != "" +func (s ProjectGrantState) Exists() bool { + return s != ProjectGrantStateUnspecified && s != ProjectGrantStateRemoved } -func (g *ProjectGrant) HasInvalidRoles(validRoles []string) bool { - for _, roleKey := range g.RoleKeys { - if !containsRoleKey(roleKey, validRoles) { - return true - } - } - return false +func (p *ProjectGrant) IsValid() bool { + return p.GrantedOrgID != "" } func GetRemovedRoles(existingRoles, newRoles []string) []string { diff --git a/internal/domain/project_role.go b/internal/domain/project_role.go index e6c782bad1..c3593a1517 100644 --- a/internal/domain/project_role.go +++ b/internal/domain/project_role.go @@ -20,14 +20,23 @@ const ( ProjectRoleStateRemoved ) -func NewProjectRole(projectID, key string) *ProjectRole { - return &ProjectRole{ObjectRoot: models.ObjectRoot{AggregateID: projectID}, Key: key} +func (s ProjectRoleState) Exists() bool { + return s != ProjectRoleStateUnspecified && s != ProjectRoleStateRemoved } func (p *ProjectRole) IsValid() bool { return p.AggregateID != "" && p.Key != "" } +func HasInvalidRoles(validRoles, roles []string) bool { + for _, roleKey := range roles { + if !containsRoleKey(roleKey, validRoles) { + return true + } + } + return false +} + func containsRoleKey(roleKey string, validRoles []string) bool { for _, validRole := range validRoles { if roleKey == validRole { diff --git a/internal/integration/client.go b/internal/integration/client.go index 61645cc067..3efd682ee1 100644 --- a/internal/integration/client.go +++ b/internal/integration/client.go @@ -34,6 +34,7 @@ import ( oidc_pb_v2beta "github.com/zitadel/zitadel/pkg/grpc/oidc/v2beta" "github.com/zitadel/zitadel/pkg/grpc/org/v2" org_v2beta "github.com/zitadel/zitadel/pkg/grpc/org/v2beta" + project_v2beta "github.com/zitadel/zitadel/pkg/grpc/project/v2beta" user_v3alpha "github.com/zitadel/zitadel/pkg/grpc/resources/user/v3alpha" userschema_v3alpha "github.com/zitadel/zitadel/pkg/grpc/resources/userschema/v3alpha" saml_pb "github.com/zitadel/zitadel/pkg/grpc/saml/v2" @@ -71,6 +72,7 @@ type Client struct { UserV3Alpha user_v3alpha.ZITADELUsersClient SAMLv2 saml_pb.SAMLServiceClient SCIM *scim.Client + Projectv2Beta project_v2beta.ProjectServiceClient InstanceV2Beta instance.InstanceServiceClient } @@ -109,6 +111,7 @@ func newClient(ctx context.Context, target string) (*Client, error) { UserV3Alpha: user_v3alpha.NewZITADELUsersClient(cc), SAMLv2: saml_pb.NewSAMLServiceClient(cc), SCIM: scim.NewScimClient(target), + Projectv2Beta: project_v2beta.NewProjectServiceClient(cc), InstanceV2Beta: instance.NewInstanceServiceClient(cc), } return client, client.pollHealth(ctx) @@ -446,6 +449,70 @@ func (i *Instance) SetUserPassword(ctx context.Context, userID, password string, return resp.GetDetails() } +func (i *Instance) CreateProject(ctx context.Context, t *testing.T, orgID, name string, projectRoleCheck, hasProjectCheck bool) *project_v2beta.CreateProjectResponse { + if orgID == "" { + orgID = i.DefaultOrg.GetId() + } + + resp, err := i.Client.Projectv2Beta.CreateProject(ctx, &project_v2beta.CreateProjectRequest{ + OrganizationId: orgID, + Name: name, + AuthorizationRequired: projectRoleCheck, + ProjectAccessRequired: hasProjectCheck, + }) + require.NoError(t, err) + return resp +} + +func (i *Instance) DeleteProject(ctx context.Context, t *testing.T, projectID string) *project_v2beta.DeleteProjectResponse { + resp, err := i.Client.Projectv2Beta.DeleteProject(ctx, &project_v2beta.DeleteProjectRequest{ + Id: projectID, + }) + require.NoError(t, err) + return resp +} + +func (i *Instance) DeactivateProject(ctx context.Context, t *testing.T, projectID string) *project_v2beta.DeactivateProjectResponse { + resp, err := i.Client.Projectv2Beta.DeactivateProject(ctx, &project_v2beta.DeactivateProjectRequest{ + Id: projectID, + }) + require.NoError(t, err) + return resp +} + +func (i *Instance) ActivateProject(ctx context.Context, t *testing.T, projectID string) *project_v2beta.ActivateProjectResponse { + resp, err := i.Client.Projectv2Beta.ActivateProject(ctx, &project_v2beta.ActivateProjectRequest{ + Id: projectID, + }) + require.NoError(t, err) + return resp +} + +func (i *Instance) AddProjectRole(ctx context.Context, t *testing.T, projectID, roleKey, displayName, group string) *project_v2beta.AddProjectRoleResponse { + var groupP *string + if group != "" { + groupP = &group + } + + resp, err := i.Client.Projectv2Beta.AddProjectRole(ctx, &project_v2beta.AddProjectRoleRequest{ + ProjectId: projectID, + RoleKey: roleKey, + DisplayName: displayName, + Group: groupP, + }) + require.NoError(t, err) + return resp +} + +func (i *Instance) RemoveProjectRole(ctx context.Context, t *testing.T, projectID, roleKey string) *project_v2beta.RemoveProjectRoleResponse { + resp, err := i.Client.Projectv2Beta.RemoveProjectRole(ctx, &project_v2beta.RemoveProjectRoleRequest{ + ProjectId: projectID, + RoleKey: roleKey, + }) + require.NoError(t, err) + return resp +} + func (i *Instance) AddProviderToDefaultLoginPolicy(ctx context.Context, id string) { _, err := i.Client.Admin.AddIDPToLoginPolicy(ctx, &admin.AddIDPToLoginPolicyRequest{ IdpId: id, @@ -705,12 +772,40 @@ func (i *Instance) CreateIntentSession(t *testing.T, ctx context.Context, userID 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, - ProjectId: projectID, +func (i *Instance) CreateProjectGrant(ctx context.Context, t *testing.T, projectID, grantedOrgID string, roles ...string) *project_v2beta.CreateProjectGrantResponse { + resp, err := i.Client.Projectv2Beta.CreateProjectGrant(ctx, &project_v2beta.CreateProjectGrantRequest{ + GrantedOrganizationId: grantedOrgID, + ProjectId: projectID, + RoleKeys: roles, }) - logging.OnError(err).Panic("create project grant") + require.NoError(t, err) + return resp +} + +func (i *Instance) DeleteProjectGrant(ctx context.Context, t *testing.T, projectID, grantedOrgID string) *project_v2beta.DeleteProjectGrantResponse { + resp, err := i.Client.Projectv2Beta.DeleteProjectGrant(ctx, &project_v2beta.DeleteProjectGrantRequest{ + GrantedOrganizationId: grantedOrgID, + ProjectId: projectID, + }) + require.NoError(t, err) + return resp +} + +func (i *Instance) DeactivateProjectGrant(ctx context.Context, t *testing.T, projectID, grantedOrgID string) *project_v2beta.DeactivateProjectGrantResponse { + resp, err := i.Client.Projectv2Beta.DeactivateProjectGrant(ctx, &project_v2beta.DeactivateProjectGrantRequest{ + ProjectId: projectID, + GrantedOrganizationId: grantedOrgID, + }) + require.NoError(t, err) + return resp +} + +func (i *Instance) ActivateProjectGrant(ctx context.Context, t *testing.T, projectID, grantedOrgID string) *project_v2beta.ActivateProjectGrantResponse { + resp, err := i.Client.Projectv2Beta.ActivateProjectGrant(ctx, &project_v2beta.ActivateProjectGrantRequest{ + ProjectId: projectID, + GrantedOrganizationId: grantedOrgID, + }) + require.NoError(t, err) return resp } diff --git a/internal/integration/oidc.go b/internal/integration/oidc.go index 159fcb0119..3323be3f97 100644 --- a/internal/integration/oidc.go +++ b/internal/integration/oidc.go @@ -7,6 +7,7 @@ import ( "net/http" "net/url" "strings" + "testing" "time" "github.com/brianvoe/gofakeit/v6" @@ -133,13 +134,9 @@ func (i *Instance) CreateOIDCInactivateProjectClient(ctx context.Context, redire return client, err } -func (i *Instance) CreateOIDCImplicitFlowClient(ctx context.Context, redirectURI string, loginVersion *app.LoginVersion) (*management.AddOIDCAppResponse, error) { - project, err := i.Client.Mgmt.AddProject(ctx, &management.AddProjectRequest{ - Name: fmt.Sprintf("project-%d", time.Now().UnixNano()), - }) - if err != nil { - return nil, err - } +func (i *Instance) CreateOIDCImplicitFlowClient(ctx context.Context, t *testing.T, redirectURI string, loginVersion *app.LoginVersion) (*management.AddOIDCAppResponse, error) { + project := i.CreateProject(ctx, t, "", gofakeit.AppName(), false, false) + resp, err := i.Client.Mgmt.AddOIDCApp(ctx, &management.AddOIDCAppRequest{ ProjectId: project.GetId(), Name: fmt.Sprintf("app-%d", time.Now().UnixNano()), @@ -178,30 +175,11 @@ func (i *Instance) CreateOIDCImplicitFlowClient(ctx context.Context, redirectURI }) } -func (i *Instance) CreateOIDCTokenExchangeClient(ctx context.Context) (client *management.AddOIDCAppResponse, keyData []byte, err error) { - project, err := i.Client.Mgmt.AddProject(ctx, &management.AddProjectRequest{ - Name: fmt.Sprintf("project-%d", time.Now().UnixNano()), - }) - if err != nil { - return nil, nil, err - } +func (i *Instance) CreateOIDCTokenExchangeClient(ctx context.Context, t *testing.T) (client *management.AddOIDCAppResponse, keyData []byte, err error) { + project := i.CreateProject(ctx, t, "", gofakeit.AppName(), false, false) return i.CreateOIDCWebClientJWT(ctx, "", "", project.GetId(), app.OIDCGrantType_OIDC_GRANT_TYPE_TOKEN_EXCHANGE, app.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE, app.OIDCGrantType_OIDC_GRANT_TYPE_REFRESH_TOKEN) } -func (i *Instance) CreateProject(ctx context.Context) (*management.AddProjectResponse, error) { - return i.Client.Mgmt.AddProject(ctx, &management.AddProjectRequest{ - Name: fmt.Sprintf("project-%d", time.Now().UnixNano()), - }) -} - -func (i *Instance) CreateProjectWithPermissionCheck(ctx context.Context, projectRoleCheck, hasProjectCheck bool) (*management.AddProjectResponse, error) { - return i.Client.Mgmt.AddProject(ctx, &management.AddProjectRequest{ - Name: fmt.Sprintf("project-%d", time.Now().UnixNano()), - HasProjectCheck: hasProjectCheck, - ProjectRoleCheck: projectRoleCheck, - }) -} - func (i *Instance) CreateAPIClientJWT(ctx context.Context, projectID string) (*management.AddAPIAppResponse, error) { return i.Client.Mgmt.AddAPIApp(ctx, &management.AddAPIAppRequest{ ProjectId: projectID, diff --git a/internal/query/project.go b/internal/query/project.go index 7501047182..ab58bd11a8 100644 --- a/internal/query/project.go +++ b/internal/query/project.go @@ -4,6 +4,7 @@ import ( "context" "database/sql" "errors" + "slices" "time" sq "github.com/Masterminds/squirrel" @@ -72,11 +73,104 @@ var ( } ) +var ( + grantedProjectsAlias = table{ + name: "granted_projects", + instanceIDCol: projection.ProjectColumnInstanceID, + } + GrantedProjectColumnID = Column{ + name: projection.ProjectColumnID, + table: grantedProjectsAlias, + } + GrantedProjectColumnCreationDate = Column{ + name: projection.ProjectColumnCreationDate, + table: grantedProjectsAlias, + } + GrantedProjectColumnChangeDate = Column{ + name: projection.ProjectColumnChangeDate, + table: grantedProjectsAlias, + } + grantedProjectColumnResourceOwner = Column{ + name: projection.ProjectColumnResourceOwner, + table: grantedProjectsAlias, + } + grantedProjectColumnInstanceID = Column{ + name: projection.ProjectGrantColumnInstanceID, + table: grantedProjectsAlias, + } + grantedProjectColumnState = Column{ + name: "project_state", + table: grantedProjectsAlias, + } + GrantedProjectColumnName = Column{ + name: "project_name", + table: grantedProjectsAlias, + } + grantedProjectColumnProjectRoleAssertion = Column{ + name: projection.ProjectColumnProjectRoleAssertion, + table: grantedProjectsAlias, + } + grantedProjectColumnProjectRoleCheck = Column{ + name: projection.ProjectColumnProjectRoleCheck, + table: grantedProjectsAlias, + } + grantedProjectColumnHasProjectCheck = Column{ + name: projection.ProjectColumnHasProjectCheck, + table: grantedProjectsAlias, + } + grantedProjectColumnPrivateLabelingSetting = Column{ + name: projection.ProjectColumnPrivateLabelingSetting, + table: grantedProjectsAlias, + } + grantedProjectColumnGrantResourceOwner = Column{ + name: "project_grant_resource_owner", + table: grantedProjectsAlias, + } + grantedProjectColumnGrantedOrganization = Column{ + name: projection.ProjectGrantColumnGrantedOrgID, + table: grantedProjectsAlias, + } + grantedProjectColumnGrantedOrganizationName = Column{ + name: "granted_org_name", + table: grantedProjectsAlias, + } + grantedProjectColumnGrantState = Column{ + name: "project_grant_state", + table: grantedProjectsAlias, + } +) + type Projects struct { SearchResponse Projects []*Project } +func projectsCheckPermission(ctx context.Context, projects *Projects, permissionCheck domain.PermissionCheck) { + projects.Projects = slices.DeleteFunc(projects.Projects, + func(project *Project) bool { + return projectCheckPermission(ctx, project.ResourceOwner, project.ID, permissionCheck) != nil + }, + ) +} + +func projectCheckPermission(ctx context.Context, resourceOwner string, projectID string, permissionCheck domain.PermissionCheck) error { + return permissionCheck(ctx, domain.PermissionProjectRead, resourceOwner, projectID) +} + +func projectPermissionCheckV2(ctx context.Context, query sq.SelectBuilder, enabled bool, queries *ProjectAndGrantedProjectSearchQueries) sq.SelectBuilder { + if !enabled { + return query + } + join, args := PermissionClause( + ctx, + grantedProjectColumnResourceOwner, + domain.PermissionProjectRead, + SingleOrgPermissionOption(queries.Queries), + OwnedRowsPermissionOption(GrantedProjectColumnID), + ) + return query.JoinClause(join, args...) +} + type Project struct { ID string CreationDate time.Time @@ -94,7 +188,19 @@ type Project struct { type ProjectSearchQueries struct { SearchRequest - Queries []SearchQuery + Queries []SearchQuery + GrantQueries []SearchQuery +} + +func (q *Queries) GetProjectByIDWithPermission(ctx context.Context, shouldTriggerBulk bool, id string, permissionCheck domain.PermissionCheck) (*Project, error) { + project, err := q.ProjectByID(ctx, shouldTriggerBulk, id) + if err != nil { + return nil, err + } + if err := projectCheckPermission(ctx, project.ResourceOwner, project.ID, permissionCheck); err != nil { + return nil, err + } + return project, nil } func (q *Queries) ProjectByID(ctx context.Context, shouldTriggerBulk bool, id string) (project *Project, err error) { @@ -125,7 +231,18 @@ func (q *Queries) ProjectByID(ctx context.Context, shouldTriggerBulk bool, id st return project, err } -func (q *Queries) SearchProjects(ctx context.Context, queries *ProjectSearchQueries) (projects *Projects, err error) { +func (q *Queries) SearchProjects(ctx context.Context, queries *ProjectSearchQueries, permissionCheck domain.PermissionCheck) (*Projects, error) { + projects, err := q.searchProjects(ctx, queries) + if err != nil { + return nil, err + } + if permissionCheck != nil { + projectsCheckPermission(ctx, projects, permissionCheck) + } + return projects, nil +} + +func (q *Queries) searchProjects(ctx context.Context, queries *ProjectSearchQueries) (projects *Projects, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() @@ -147,6 +264,89 @@ func (q *Queries) SearchProjects(ctx context.Context, queries *ProjectSearchQuer return projects, err } +type ProjectAndGrantedProjectSearchQueries struct { + SearchRequest + Queries []SearchQuery +} + +func (q *ProjectAndGrantedProjectSearchQueries) toQuery(query sq.SelectBuilder) sq.SelectBuilder { + query = q.SearchRequest.toQuery(query) + for _, q := range q.Queries { + query = q.toQuery(query) + } + return query +} + +func (q *Queries) SearchGrantedProjects(ctx context.Context, queries *ProjectAndGrantedProjectSearchQueries, permissionCheck domain.PermissionCheck) (*GrantedProjects, error) { + permissionCheckV2 := PermissionV2(ctx, permissionCheck) + projects, err := q.searchGrantedProjects(ctx, queries, permissionCheckV2) + if err != nil { + return nil, err + } + if permissionCheck != nil && !authz.GetFeatures(ctx).PermissionCheckV2 { + grantedProjectsCheckPermission(ctx, projects, permissionCheck) + } + return projects, nil +} + +func (q *Queries) searchGrantedProjects(ctx context.Context, queries *ProjectAndGrantedProjectSearchQueries, permissionCheckV2 bool) (grantedProjects *GrantedProjects, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + query, scan := prepareGrantedProjectsQuery() + query = projectPermissionCheckV2(ctx, query, permissionCheckV2, queries) + eq := sq.Eq{grantedProjectColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID()} + stmt, args, err := queries.toQuery(query).Where(eq).ToSql() + if err != nil { + return nil, zerrors.ThrowInvalidArgument(err, "QUERY-T84X9", "Errors.Query.InvalidRequest") + } + + err = q.client.QueryContext(ctx, func(rows *sql.Rows) error { + grantedProjects, err = scan(rows) + return err + }, stmt, args...) + if err != nil { + return nil, err + } + return grantedProjects, nil +} + +func NewGrantedProjectNameSearchQuery(method TextComparison, value string) (SearchQuery, error) { + return NewTextQuery(GrantedProjectColumnName, value, method) +} + +func NewGrantedProjectResourceOwnerSearchQuery(value string) (SearchQuery, error) { + return NewTextQuery(grantedProjectColumnResourceOwner, value, TextEquals) +} + +func NewGrantedProjectIDSearchQuery(ids []string) (SearchQuery, error) { + list := make([]interface{}, len(ids)) + for i, value := range ids { + list[i] = value + } + return NewListQuery(GrantedProjectColumnID, list, ListIn) +} + +func NewGrantedProjectOrganizationIDSearchQuery(value string) (SearchQuery, error) { + project, err := NewTextQuery(grantedProjectColumnResourceOwner, value, TextEquals) + if err != nil { + return nil, err + } + grant, err := NewTextQuery(grantedProjectColumnGrantedOrganization, value, TextEquals) + if err != nil { + return nil, err + } + return NewOrQuery(project, grant) +} + +func NewGrantedProjectGrantResourceOwnerSearchQuery(value string) (SearchQuery, error) { + return NewTextQuery(grantedProjectColumnGrantResourceOwner, value, TextEquals) +} + +func NewGrantedProjectGrantedOrganizationIDSearchQuery(value string) (SearchQuery, error) { + return NewTextQuery(grantedProjectColumnGrantedOrganization, value, TextEquals) +} + func NewProjectNameSearchQuery(method TextComparison, value string) (SearchQuery, error) { return NewTextQuery(ProjectColumnName, value, method) } @@ -285,3 +485,171 @@ func prepareProjectsQuery() (sq.SelectBuilder, func(*sql.Rows) (*Projects, error }, nil } } + +type GrantedProjects struct { + SearchResponse + GrantedProjects []*GrantedProject +} + +func grantedProjectsCheckPermission(ctx context.Context, grantedProjects *GrantedProjects, permissionCheck domain.PermissionCheck) { + grantedProjects.GrantedProjects = slices.DeleteFunc(grantedProjects.GrantedProjects, + func(grantedProject *GrantedProject) bool { + return projectCheckPermission(ctx, grantedProject.ResourceOwner, grantedProject.ProjectID, permissionCheck) != nil + }, + ) +} + +type GrantedProject struct { + ProjectID string + CreationDate time.Time + ChangeDate time.Time + ResourceOwner string + InstanceID string + ProjectState domain.ProjectState + ProjectName string + + ProjectRoleAssertion bool + ProjectRoleCheck bool + HasProjectCheck bool + PrivateLabelingSetting domain.PrivateLabelingSetting + + GrantedOrgID string + OrgName string + ProjectGrantState domain.ProjectGrantState +} + +func prepareGrantedProjectsQuery() (sq.SelectBuilder, func(*sql.Rows) (*GrantedProjects, error)) { + return sq.Select( + GrantedProjectColumnID.identifier(), + GrantedProjectColumnCreationDate.identifier(), + GrantedProjectColumnChangeDate.identifier(), + grantedProjectColumnResourceOwner.identifier(), + grantedProjectColumnInstanceID.identifier(), + grantedProjectColumnState.identifier(), + GrantedProjectColumnName.identifier(), + grantedProjectColumnProjectRoleAssertion.identifier(), + grantedProjectColumnProjectRoleCheck.identifier(), + grantedProjectColumnHasProjectCheck.identifier(), + grantedProjectColumnPrivateLabelingSetting.identifier(), + grantedProjectColumnGrantedOrganization.identifier(), + grantedProjectColumnGrantedOrganizationName.identifier(), + grantedProjectColumnGrantState.identifier(), + countColumn.identifier(), + ).From(getProjectsAndGrantedProjectsFromQuery()). + PlaceholderFormat(sq.Dollar), + func(rows *sql.Rows) (*GrantedProjects, error) { + projects := make([]*GrantedProject, 0) + var ( + count uint64 + orgID = sql.NullString{} + orgName = sql.NullString{} + projectGrantState = sql.NullInt16{} + ) + for rows.Next() { + grantedProject := new(GrantedProject) + err := rows.Scan( + &grantedProject.ProjectID, + &grantedProject.CreationDate, + &grantedProject.ChangeDate, + &grantedProject.ResourceOwner, + &grantedProject.InstanceID, + &grantedProject.ProjectState, + &grantedProject.ProjectName, + &grantedProject.ProjectRoleAssertion, + &grantedProject.ProjectRoleCheck, + &grantedProject.HasProjectCheck, + &grantedProject.PrivateLabelingSetting, + &orgID, + &orgName, + &projectGrantState, + &count, + ) + if err != nil { + return nil, err + } + if orgID.Valid { + grantedProject.GrantedOrgID = orgID.String + } + if orgName.Valid { + grantedProject.OrgName = orgName.String + } + if projectGrantState.Valid { + grantedProject.ProjectGrantState = domain.ProjectGrantState(projectGrantState.Int16) + } + projects = append(projects, grantedProject) + } + + if err := rows.Close(); err != nil { + return nil, zerrors.ThrowInternal(err, "QUERY-K9gEE", "Errors.Query.CloseRows") + } + + return &GrantedProjects{ + GrantedProjects: projects, + SearchResponse: SearchResponse{ + Count: count, + }, + }, nil + } +} + +func getProjectsAndGrantedProjectsFromQuery() string { + return "(" + + prepareProjects() + + " UNION ALL " + + prepareGrantedProjects() + + ") AS " + grantedProjectsAlias.identifier() +} + +func prepareProjects() string { + builder := sq.Select( + ProjectColumnID.identifier()+" AS "+GrantedProjectColumnID.name, + ProjectColumnCreationDate.identifier()+" AS "+GrantedProjectColumnCreationDate.name, + ProjectColumnChangeDate.identifier()+" AS "+GrantedProjectColumnChangeDate.name, + ProjectColumnResourceOwner.identifier()+" AS "+grantedProjectColumnResourceOwner.name, + ProjectColumnInstanceID.identifier()+" AS "+grantedProjectColumnInstanceID.name, + ProjectColumnState.identifier()+" AS "+grantedProjectColumnState.name, + ProjectColumnName.identifier()+" AS "+GrantedProjectColumnName.name, + ProjectColumnProjectRoleAssertion.identifier()+" AS "+grantedProjectColumnProjectRoleAssertion.name, + ProjectColumnProjectRoleCheck.identifier()+" AS "+grantedProjectColumnProjectRoleCheck.name, + ProjectColumnHasProjectCheck.identifier()+" AS "+grantedProjectColumnHasProjectCheck.name, + ProjectColumnPrivateLabelingSetting.identifier()+" AS "+grantedProjectColumnPrivateLabelingSetting.name, + "NULL::TEXT AS "+grantedProjectColumnGrantResourceOwner.name, + "NULL::TEXT AS "+grantedProjectColumnGrantedOrganization.name, + "NULL::TEXT AS "+grantedProjectColumnGrantedOrganizationName.name, + "NULL::SMALLINT AS "+grantedProjectColumnGrantState.name, + countColumn.identifier()). + From(projectsTable.identifier()). + PlaceholderFormat(sq.Dollar) + + stmt, _ := builder.MustSql() + return stmt +} + +func prepareGrantedProjects() string { + grantedOrgTable := orgsTable.setAlias(ProjectGrantGrantedOrgTableAlias) + grantedOrgIDColumn := OrgColumnID.setTable(grantedOrgTable) + builder := sq.Select( + ProjectGrantColumnProjectID.identifier()+" AS "+GrantedProjectColumnID.name, + ProjectGrantColumnCreationDate.identifier()+" AS "+GrantedProjectColumnCreationDate.name, + ProjectGrantColumnChangeDate.identifier()+" AS "+GrantedProjectColumnChangeDate.name, + ProjectColumnResourceOwner.identifier()+" AS "+grantedProjectColumnResourceOwner.name, + ProjectGrantColumnInstanceID.identifier()+" AS "+grantedProjectColumnInstanceID.name, + ProjectColumnState.identifier()+" AS "+grantedProjectColumnState.name, + ProjectColumnName.identifier()+" AS "+GrantedProjectColumnName.name, + ProjectColumnProjectRoleAssertion.identifier()+" AS "+grantedProjectColumnProjectRoleAssertion.name, + ProjectColumnProjectRoleCheck.identifier()+" AS "+grantedProjectColumnProjectRoleCheck.name, + ProjectColumnHasProjectCheck.identifier()+" AS "+grantedProjectColumnHasProjectCheck.name, + ProjectColumnPrivateLabelingSetting.identifier()+" AS "+grantedProjectColumnPrivateLabelingSetting.name, + ProjectGrantColumnResourceOwner.identifier()+" AS "+grantedProjectColumnGrantResourceOwner.name, + ProjectGrantColumnGrantedOrgID.identifier()+" AS "+grantedProjectColumnGrantedOrganization.name, + ProjectGrantColumnGrantedOrgName.identifier()+" AS "+grantedProjectColumnGrantedOrganizationName.name, + ProjectGrantColumnState.identifier()+" AS "+grantedProjectColumnGrantState.name, + countColumn.identifier()). + From(projectGrantsTable.identifier()). + PlaceholderFormat(sq.Dollar). + LeftJoin(join(ProjectColumnID, ProjectGrantColumnProjectID)). + LeftJoin(join(grantedOrgIDColumn, ProjectGrantColumnGrantedOrgID)) + + stmt, _ := builder.MustSql() + return stmt +} diff --git a/internal/query/project_grant.go b/internal/query/project_grant.go index b971593c77..a0dbd7c121 100644 --- a/internal/query/project_grant.go +++ b/internal/query/project_grant.go @@ -4,6 +4,7 @@ import ( "context" "database/sql" "errors" + "slices" "time" sq "github.com/Masterminds/squirrel" @@ -104,6 +105,43 @@ type ProjectGrantSearchQueries struct { Queries []SearchQuery } +func projectGrantsCheckPermission(ctx context.Context, projectGrants *ProjectGrants, permissionCheck domain.PermissionCheck) { + projectGrants.ProjectGrants = slices.DeleteFunc(projectGrants.ProjectGrants, + func(projectGrant *ProjectGrant) bool { + return projectGrantCheckPermission(ctx, projectGrant.ResourceOwner, projectGrant.GrantID, permissionCheck) != nil + }, + ) +} + +func projectGrantCheckPermission(ctx context.Context, resourceOwner string, grantID string, permissionCheck domain.PermissionCheck) error { + return permissionCheck(ctx, domain.PermissionProjectGrantRead, resourceOwner, grantID) +} + +func projectGrantPermissionCheckV2(ctx context.Context, query sq.SelectBuilder, enabled bool, queries *ProjectGrantSearchQueries) sq.SelectBuilder { + if !enabled { + return query + } + join, args := PermissionClause( + ctx, + ProjectGrantColumnResourceOwner, + domain.PermissionProjectGrantRead, + SingleOrgPermissionOption(queries.Queries), + OwnedRowsPermissionOption(ProjectGrantColumnGrantID), + ) + return query.JoinClause(join, args...) +} + +func (q *Queries) GetProjectGrantByIDWithPermission(ctx context.Context, shouldTriggerBulk bool, id string, permissionCheck domain.PermissionCheck) (*ProjectGrant, error) { + projectGrant, err := q.ProjectGrantByID(ctx, shouldTriggerBulk, id) + if err != nil { + return nil, err + } + if err := projectCheckPermission(ctx, projectGrant.ResourceOwner, projectGrant.GrantID, permissionCheck); err != nil { + return nil, err + } + return projectGrant, nil +} + func (q *Queries) ProjectGrantByID(ctx context.Context, shouldTriggerBulk bool, id string) (grant *ProjectGrant, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() @@ -154,11 +192,24 @@ func (q *Queries) ProjectGrantByIDAndGrantedOrg(ctx context.Context, id, granted return grant, err } -func (q *Queries) SearchProjectGrants(ctx context.Context, queries *ProjectGrantSearchQueries) (grants *ProjectGrants, err error) { +func (q *Queries) SearchProjectGrants(ctx context.Context, queries *ProjectGrantSearchQueries, permissionCheck domain.PermissionCheck) (grants *ProjectGrants, err error) { + permissionCheckV2 := PermissionV2(ctx, permissionCheck) + projectsGrants, err := q.searchProjectGrants(ctx, queries, permissionCheckV2) + if err != nil { + return nil, err + } + if permissionCheck != nil && !authz.GetFeatures(ctx).PermissionCheckV2 { + projectGrantsCheckPermission(ctx, projectsGrants, permissionCheck) + } + return projectsGrants, nil +} + +func (q *Queries) searchProjectGrants(ctx context.Context, queries *ProjectGrantSearchQueries, permissionCheckV2 bool) (grants *ProjectGrants, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() query, scan := prepareProjectGrantsQuery() + query = projectGrantPermissionCheckV2(ctx, query, permissionCheckV2, queries) eq := sq.Eq{ ProjectGrantColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), } @@ -179,6 +230,7 @@ func (q *Queries) SearchProjectGrants(ctx context.Context, queries *ProjectGrant return grants, err } +// SearchProjectGrantsByProjectIDAndRoleKey is used internally to remove the roles of a project grant, so no permission check necessary func (q *Queries) SearchProjectGrantsByProjectIDAndRoleKey(ctx context.Context, projectID, roleKey string) (projects *ProjectGrants, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() @@ -195,13 +247,33 @@ func (q *Queries) SearchProjectGrantsByProjectIDAndRoleKey(ctx context.Context, if err != nil { return nil, err } - return q.SearchProjectGrants(ctx, searchQuery) + return q.SearchProjectGrants(ctx, searchQuery, nil) } func NewProjectGrantProjectIDSearchQuery(value string) (SearchQuery, error) { return NewTextQuery(ProjectGrantColumnProjectID, value, TextEquals) } +func (q *ProjectGrantSearchQueries) AppendPermissionQueries(permissions []string) error { + if !authz.HasGlobalPermission(permissions) { + ids := authz.GetAllPermissionCtxIDs(permissions) + query, err := NewProjectGrantIDsSearchQuery(ids) + if err != nil { + return err + } + q.Queries = append(q.Queries, query) + } + return nil +} + +func NewProjectGrantProjectIDsSearchQuery(ids []string) (SearchQuery, error) { + list := make([]interface{}, len(ids)) + for i, value := range ids { + list[i] = value + } + return NewListQuery(ProjectGrantColumnProjectID, list, ListIn) +} + func NewProjectGrantIDsSearchQuery(values []string) (SearchQuery, error) { list := make([]interface{}, len(values)) for i, value := range values { @@ -243,18 +315,6 @@ func (q *ProjectGrantSearchQueries) AppendGrantedOrgQuery(orgID string) error { return nil } -func (q *ProjectGrantSearchQueries) AppendPermissionQueries(permissions []string) error { - if !authz.HasGlobalPermission(permissions) { - ids := authz.GetAllPermissionCtxIDs(permissions) - query, err := NewProjectGrantIDsSearchQuery(ids) - if err != nil { - return err - } - q.Queries = append(q.Queries, query) - } - return nil -} - func (q *ProjectGrantSearchQueries) toQuery(query sq.SelectBuilder) sq.SelectBuilder { query = q.SearchRequest.toQuery(query) for _, q := range q.Queries { diff --git a/internal/query/project_role.go b/internal/query/project_role.go index ab4f40ca38..15ae806cd4 100644 --- a/internal/query/project_role.go +++ b/internal/query/project_role.go @@ -3,12 +3,14 @@ package query import ( "context" "database/sql" + "slices" "time" sq "github.com/Masterminds/squirrel" "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore/handler/v2" "github.com/zitadel/zitadel/internal/query/projection" "github.com/zitadel/zitadel/internal/telemetry/tracing" @@ -80,7 +82,45 @@ type ProjectRoleSearchQueries struct { Queries []SearchQuery } -func (q *Queries) SearchProjectRoles(ctx context.Context, shouldTriggerBulk bool, queries *ProjectRoleSearchQueries) (roles *ProjectRoles, err error) { +func projectRolesCheckPermission(ctx context.Context, projectRoles *ProjectRoles, permissionCheck domain.PermissionCheck) { + projectRoles.ProjectRoles = slices.DeleteFunc(projectRoles.ProjectRoles, + func(projectRole *ProjectRole) bool { + return projectRoleCheckPermission(ctx, projectRole.ResourceOwner, projectRole.Key, permissionCheck) != nil + }, + ) +} + +func projectRoleCheckPermission(ctx context.Context, resourceOwner string, grantID string, permissionCheck domain.PermissionCheck) error { + return permissionCheck(ctx, domain.PermissionProjectGrantRead, resourceOwner, grantID) +} + +func projectRolePermissionCheckV2(ctx context.Context, query sq.SelectBuilder, enabled bool, queries *ProjectRoleSearchQueries) sq.SelectBuilder { + if !enabled { + return query + } + join, args := PermissionClause( + ctx, + ProjectRoleColumnResourceOwner, + domain.PermissionProjectRoleRead, + SingleOrgPermissionOption(queries.Queries), + OwnedRowsPermissionOption(ProjectRoleColumnKey), + ) + return query.JoinClause(join, args...) +} + +func (q *Queries) SearchProjectRoles(ctx context.Context, shouldTriggerBulk bool, queries *ProjectRoleSearchQueries, permissionCheck domain.PermissionCheck) (roles *ProjectRoles, err error) { + permissionCheckV2 := PermissionV2(ctx, permissionCheck) + projectRoles, err := q.searchProjectRoles(ctx, shouldTriggerBulk, queries, permissionCheckV2) + if err != nil { + return nil, err + } + if permissionCheck != nil && !authz.GetFeatures(ctx).PermissionCheckV2 { + projectRolesCheckPermission(ctx, projectRoles, permissionCheck) + } + return projectRoles, nil +} + +func (q *Queries) searchProjectRoles(ctx context.Context, shouldTriggerBulk bool, queries *ProjectRoleSearchQueries, permissionCheckV2 bool) (roles *ProjectRoles, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() @@ -94,6 +134,7 @@ func (q *Queries) SearchProjectRoles(ctx context.Context, shouldTriggerBulk bool eq := sq.Eq{ProjectRoleColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID()} query, scan := prepareProjectRolesQuery() + query = projectRolePermissionCheckV2(ctx, query, permissionCheckV2, queries) stmt, args, err := queries.toQuery(query).Where(eq).ToSql() if err != nil { return nil, zerrors.ThrowInvalidArgument(err, "QUERY-3N9ff", "Errors.Query.InvalidRequest") diff --git a/internal/repository/project/aggregate.go b/internal/repository/project/aggregate.go index 5391718812..15cb24278d 100644 --- a/internal/repository/project/aggregate.go +++ b/internal/repository/project/aggregate.go @@ -1,6 +1,8 @@ package project import ( + "context" + "github.com/zitadel/zitadel/internal/eventstore" ) @@ -23,3 +25,7 @@ func NewAggregate(id, resourceOwner string) *Aggregate { }, } } + +func AggregateFromWriteModel(ctx context.Context, wm *eventstore.WriteModel) *eventstore.Aggregate { + return eventstore.AggregateFromWriteModelCtx(ctx, wm, AggregateType, AggregateVersion) +} diff --git a/internal/repository/project/project.go b/internal/repository/project/project.go index 44f882b3e1..c86fed5272 100644 --- a/internal/repository/project/project.go +++ b/internal/repository/project/project.go @@ -184,10 +184,7 @@ func NewProjectChangeEvent( aggregate *eventstore.Aggregate, oldName string, changes []ProjectChanges, -) (*ProjectChangeEvent, error) { - if len(changes) == 0 { - return nil, zerrors.ThrowPreconditionFailed(nil, "PROJECT-mV9xc", "Errors.NoChangesFound") - } +) *ProjectChangeEvent { changeEvent := &ProjectChangeEvent{ BaseEvent: *eventstore.NewBaseEventForPush( ctx, @@ -199,7 +196,7 @@ func NewProjectChangeEvent( for _, change := range changes { change(changeEvent) } - return changeEvent, nil + return changeEvent } type ProjectChanges func(event *ProjectChangeEvent) diff --git a/proto/zitadel/management.proto b/proto/zitadel/management.proto index 84c7823009..c90c44667c 100644 --- a/proto/zitadel/management.proto +++ b/proto/zitadel/management.proto @@ -2678,6 +2678,11 @@ service ManagementService { }; } + // Get Project By ID + // + // Deprecated: [Get Project](apis/resources/project_service_v2/project-service-get-project.api.mdx) to get project by ID. + // + // Returns a project owned by the organization (no granted projects). A Project is a vessel for different applications sharing the same role context. rpc GetProjectByID(GetProjectByIDRequest) returns (GetProjectByIDResponse) { option (google.api.http) = { get: "/projects/{id}" @@ -2690,8 +2695,7 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Projects"; - summary: "Get Project By ID"; - description: "Returns a project owned by the organization (no granted projects). A Project is a vessel for different applications sharing the same role context." + deprecated: true; parameters: { headers: { name: "x-zitadel-orgid"; @@ -2703,6 +2707,11 @@ service ManagementService { }; } + // Get Granted Project By ID + // + // Deprecated: [List Projects](apis/resources/project_service_v2/project-service-list-projects.api.mdx) to get granted projects. + // + // Returns a project owned by another organization and granted to my organization. A Project is a vessel for different applications sharing the same role context. rpc GetGrantedProjectByID(GetGrantedProjectByIDRequest) returns (GetGrantedProjectByIDResponse) { option (google.api.http) = { get: "/granted_projects/{project_id}/grants/{grant_id}" @@ -2715,8 +2724,7 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Projects"; - summary: "Get Granted Project By ID"; - description: "Returns a project owned by another organization and granted to my organization. A Project is a vessel for different applications sharing the same role context." + deprecated: true; parameters: { headers: { name: "x-zitadel-orgid"; @@ -2728,6 +2736,11 @@ service ManagementService { }; } + // List Projects + // + // Deprecated: [List Projects](apis/resources/project_service_v2/project-service-list-projects.api.mdx) to list all projects and granted projects. + // + // Lists projects my organization is the owner of (no granted projects). A Project is a vessel for different applications sharing the same role context. rpc ListProjects(ListProjectsRequest) returns (ListProjectsResponse) { option (google.api.http) = { post: "/projects/_search" @@ -2740,8 +2753,7 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Projects"; - summary: "Search Project"; - description: "Lists projects my organization is the owner of (no granted projects). A Project is a vessel for different applications sharing the same role context." + deprecated: true; parameters: { headers: { name: "x-zitadel-orgid"; @@ -2753,6 +2765,11 @@ service ManagementService { }; } + // List Granted Projects + // + // Deprecated: [List Projects](apis/resources/project_service_v2/project-service-list-projects.api.mdx) to list all projects and granted projects. + // + // Lists projects my organization got granted from another organization. A Project is a vessel for different applications sharing the same role context. rpc ListGrantedProjects(ListGrantedProjectsRequest) returns (ListGrantedProjectsResponse) { option (google.api.http) = { post: "/granted_projects/_search" @@ -2765,8 +2782,7 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Projects"; - summary: "Search Granted Project"; - description: "Lists projects my organization got granted from another organization. A Project is a vessel for different applications sharing the same role context." + deprecated: true; parameters: { headers: { name: "x-zitadel-orgid"; @@ -2828,6 +2844,11 @@ service ManagementService { }; } + // Create Project + // + // Deprecated: [Create Project](apis/resources/project_service_v2/project-service-create-project.api.mdx) to create a project. + // + // Create a new project. A Project is a vessel for different applications sharing the same role context. rpc AddProject(AddProjectRequest) returns (AddProjectResponse) { option (google.api.http) = { post: "/projects" @@ -2840,8 +2861,7 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Projects"; - summary: "Create Project"; - description: "Create a new project. A Project is a vessel for different applications sharing the same role context." + deprecated: true; parameters: { headers: { name: "x-zitadel-orgid"; @@ -2853,6 +2873,11 @@ service ManagementService { }; } + // Update Project + // + // Deprecated: [Update Project](apis/resources/project_service_v2/project-service-update-project.api.mdx) to update a project. + // + // Update a project and its settings. A Project is a vessel for different applications sharing the same role context. rpc UpdateProject(UpdateProjectRequest) returns (UpdateProjectResponse) { option (google.api.http) = { put: "/projects/{id}" @@ -2866,8 +2891,7 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Projects"; - summary: "Update Project"; - description: "Update a project and its settings. A Project is a vessel for different applications sharing the same role context." + deprecated: true; parameters: { headers: { name: "x-zitadel-orgid"; @@ -2879,6 +2903,11 @@ service ManagementService { }; } + // Deactivate Project + // + // Deprecated: [Deactivate Project](apis/resources/project_service_v2/project-service-deactivate-project.api.mdx) to deactivate a project. + // + // Set the state of a project to deactivated. Request returns an error if the project is already deactivated. rpc DeactivateProject(DeactivateProjectRequest) returns (DeactivateProjectResponse) { option (google.api.http) = { post: "/projects/{id}/_deactivate" @@ -2892,8 +2921,7 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Projects"; - summary: "Deactivate Project"; - description: "Set the state of a project to deactivated. Request returns an error if the project is already deactivated." + deprecated: true; parameters: { headers: { name: "x-zitadel-orgid"; @@ -2905,6 +2933,11 @@ service ManagementService { }; } + // Activate Project + // + // Deprecated: [Activate Project](apis/resources/project_service_v2/project-service-activate-project.api.mdx) to activate a project. + // + // Set the state of a project to active. Request returns an error if the project is not deactivated. rpc ReactivateProject(ReactivateProjectRequest) returns (ReactivateProjectResponse) { option (google.api.http) = { post: "/projects/{id}/_reactivate" @@ -2918,8 +2951,7 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Projects"; - summary: "Reactivate Project"; - description: "Set the state of a project to active. Request returns an error if the project is not deactivated." + deprecated: true; parameters: { headers: { name: "x-zitadel-orgid"; @@ -2931,6 +2963,11 @@ service ManagementService { }; } + // Remove Project + // + // Deprecated: [Delete Project](apis/resources/project_service_v2/project-service-delete-project.api.mdx) to remove a project. + // + // Project and all its sub-resources like project grants, applications, roles and user grants will be removed. rpc RemoveProject(RemoveProjectRequest) returns (RemoveProjectResponse) { option (google.api.http) = { delete: "/projects/{id}" @@ -2943,8 +2980,7 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Projects"; - summary: "Remove Project"; - description: "Project and all its sub-resources like project grants, applications, roles and user grants will be removed." + deprecated: true; parameters: { headers: { name: "x-zitadel-orgid"; @@ -2956,6 +2992,11 @@ service ManagementService { }; } + // Search Project Roles + // + // Deprecated: [List Project Roles](apis/resources/project_service_v2/project-service-list-project-roles.api.mdx) to get project roles. + // + // Returns all roles of a project matching the search query. rpc ListProjectRoles(ListProjectRolesRequest) returns (ListProjectRolesResponse) { option (google.api.http) = { post: "/projects/{project_id}/roles/_search" @@ -2969,8 +3010,7 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Project Roles"; - summary: "Search Project Roles"; - description: "Returns all roles of a project matching the search query." + deprecated: true; parameters: { headers: { name: "x-zitadel-orgid"; @@ -2982,6 +3022,11 @@ service ManagementService { }; } + // Add Project Role + // + // Deprecated: [Add Project Role](apis/resources/project_service_v2/project-service-add-project-role.api.mdx) to add a project role. + // + // Add a new project role to a project. The key must be unique within the project.\n\nDeprecated: please use user service v2 AddProjectRole. rpc AddProjectRole(AddProjectRoleRequest) returns (AddProjectRoleResponse) { option (google.api.http) = { post: "/projects/{project_id}/roles" @@ -2995,8 +3040,7 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Project Roles"; - summary: "Add Project Role"; - description: "Add a new project role to a project. The key must be unique within the project." + deprecated: true; parameters: { headers: { name: "x-zitadel-orgid"; @@ -3008,6 +3052,11 @@ service ManagementService { }; } + // Bulk add Project Role + // + // Deprecated: [Add Project Role](apis/resources/project_service_v2/project-service-add-project-role.api.mdx) to add a project role. + // + // Add a list of roles to a project. The keys must be unique within the project. rpc BulkAddProjectRoles(BulkAddProjectRolesRequest) returns (BulkAddProjectRolesResponse) { option (google.api.http) = { post: "/projects/{project_id}/roles/_bulk" @@ -3021,8 +3070,7 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Project Roles"; - summary: "Bulk Add Project Role"; - description: "Add a list of roles to a project. The keys must be unique within the project." + deprecated: true; parameters: { headers: { name: "x-zitadel-orgid"; @@ -3034,6 +3082,11 @@ service ManagementService { }; } + // Update Project Role + // + // Deprecated: [Update Project Role](apis/resources/project_service_v2/project-service-update-project-role.api.mdx) to update a project role. + // + // Change a project role. The key is not editable. If a key should change, remove the role and create a new one. rpc UpdateProjectRole(UpdateProjectRoleRequest) returns (UpdateProjectRoleResponse) { option (google.api.http) = { put: "/projects/{project_id}/roles/{role_key}" @@ -3047,8 +3100,7 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Project Roles"; - summary: "Change Project Role"; - description: "Change a project role. The key is not editable. If a key should change, remove the role and create a new one." + deprecated: true; parameters: { headers: { name: "x-zitadel-orgid"; @@ -3060,6 +3112,11 @@ service ManagementService { }; } + // Remove Project Role + // + // Deprecated: [Delete Project Role](apis/resources/project_service_v2/project-service-update-project-role.api.mdx) to remove a project role. + // + // Removes the role from the project and on every resource it has a dependency. This includes project grants and user grants. rpc RemoveProjectRole(RemoveProjectRoleRequest) returns (RemoveProjectRoleResponse) { option (google.api.http) = { delete: "/projects/{project_id}/roles/{role_key}" @@ -3072,8 +3129,7 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Project Roles"; - summary: "Remove Project Role"; - description: "Removes the role from the project and on every resource it has a dependency. This includes project grants and user grants." + deprecated: true; parameters: { headers: { name: "x-zitadel-orgid"; @@ -3737,6 +3793,11 @@ service ManagementService { }; } + // Get Project Grant By ID + // + // Deprecated: [List Project Grants](apis/resources/project_service_v2/project-service-list-project-grants.api.mdx) to get a project grant. + // + // Returns a project grant. A project grant is when the organization grants its project to another organization. rpc GetProjectGrantByID(GetProjectGrantByIDRequest) returns (GetProjectGrantByIDResponse) { option (google.api.http) = { get: "/projects/{project_id}/grants/{grant_id}" @@ -3748,8 +3809,7 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Projects"; - summary: "Project Grant By ID"; - description: "Returns a project grant. A project grant is when the organization grants its project to another organization." + deprecated: true; parameters: { headers: { name: "x-zitadel-orgid"; @@ -3761,6 +3821,11 @@ service ManagementService { }; } + // List Project Grants + // + // Deprecated: [List Project Grants](apis/resources/project_service_v2/project-service-list-project-grants.api.mdx) to list project grants. + // + // Returns a list of project grants for a specific project. A project grant is when the organization grants its project to another organization. rpc ListProjectGrants(ListProjectGrantsRequest) returns (ListProjectGrantsResponse) { option (google.api.http) = { post: "/projects/{project_id}/grants/_search" @@ -3774,8 +3839,7 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Project Grants"; - summary: "Search Project Grants from Project"; - description: "Returns a list of project grants for a specific project. A project grant is when the organization grants its project to another organization." + deprecated: true; parameters: { headers: { name: "x-zitadel-orgid"; @@ -3787,6 +3851,11 @@ service ManagementService { }; } + // Search Project Grants + // + // Deprecated: [List Project Grants](apis/resources/project_service_v2/project-service-list-project-grants.api.mdx) to list project grants. + // + // Returns a list of project grants. A project grant is when the organization grants its project to another organization. rpc ListAllProjectGrants(ListAllProjectGrantsRequest) returns (ListAllProjectGrantsResponse) { option (google.api.http) = { post: "/projectgrants/_search" @@ -3799,8 +3868,7 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Project Grants"; - summary: "Search Project Grants"; - description: "Returns a list of project grants. A project grant is when the organization grants its project to another organization." + deprecated: true; parameters: { headers: { name: "x-zitadel-orgid"; @@ -3812,6 +3880,11 @@ service ManagementService { }; } + // Add Project Grant + // + // Deprecated: [Create Project Grant](apis/resources/project_service_v2/project-service-create-project-grant.api.mdx) to add a project grant. + // + // Grant a project to another organization. The project grant will allow the granted organization to access the project and manage the authorizations for its users. Project Grant will be listed in the granted project of the granted organization. rpc AddProjectGrant(AddProjectGrantRequest) returns (AddProjectGrantResponse) { option (google.api.http) = { post: "/projects/{project_id}/grants" @@ -3824,8 +3897,7 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Project Grants"; - summary: "Add Project Grant"; - description: "Grant a project to another organization. The project grant will allow the granted organization to access the project and manage the authorizations for its users. Project Grant will be listed in the granted project of the granted organization" + deprecated: true; parameters: { headers: { name: "x-zitadel-orgid"; @@ -3837,6 +3909,11 @@ service ManagementService { }; } + // Update Project Grant + // + // Deprecated: [Update Project Grant](apis/resources/project_service_v2/project-service-update-project-grant.api.mdx) to update a project grant. + // + // Change the roles of the project that is granted to another organization. The project grant will allow the granted organization to access the project and manage the authorizations for its users. Project Grant will be listed in the granted project of the granted organization. rpc UpdateProjectGrant(UpdateProjectGrantRequest) returns (UpdateProjectGrantResponse) { option (google.api.http) = { put: "/projects/{project_id}/grants/{grant_id}" @@ -3849,8 +3926,7 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Project Grants"; - summary: "Change Project Grant"; - description: "Change the roles of the project that is granted to another organization. The project grant will allow the granted organization to access the project and manage the authorizations for its users. Project Grant will be listed in the granted project of the granted organization" + deprecated: true; parameters: { headers: { name: "x-zitadel-orgid"; @@ -3862,6 +3938,11 @@ service ManagementService { }; } + // Deactivate Project Grant + // + // Deprecated: [Deactivate Project Grant](apis/resources/project_service_v2/project-service-deactivate-project-grant.api.mdx) to deactivate a project grant. + // + // Set the state of the project grant to deactivated. The grant has to be active to be able to deactivate. rpc DeactivateProjectGrant(DeactivateProjectGrantRequest) returns (DeactivateProjectGrantResponse) { option (google.api.http) = { post: "/projects/{project_id}/grants/{grant_id}/_deactivate" @@ -3874,8 +3955,7 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Project Grants"; - summary: "Deactivate Project Grant"; - description: "Set the state of the project grant to deactivated. The grant has to be active to be able to deactivate." + deprecated: true; parameters: { headers: { name: "x-zitadel-orgid"; @@ -3887,6 +3967,11 @@ service ManagementService { }; } + // Reactivate Project Grant + // + // Deprecated: [Activate Project Grant](apis/resources/project_service_v2/project-service-activate-project-grant.api.mdx) to activate a project grant. + // + // Set the state of the project grant to active. The grant has to be deactivated to be able to reactivate. rpc ReactivateProjectGrant(ReactivateProjectGrantRequest) returns (ReactivateProjectGrantResponse) { option (google.api.http) = { post: "/projects/{project_id}/grants/{grant_id}/_reactivate" @@ -3899,8 +3984,7 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Project Grants"; - summary: "Reactivate Project Grant"; - description: "Set the state of the project grant to active. The grant has to be deactivated to be able to reactivate." + deprecated: true; parameters: { headers: { name: "x-zitadel-orgid"; @@ -3912,6 +3996,11 @@ service ManagementService { }; } + // Remove Project Grant + // + // Deprecated: [Delete Project Grant](apis/resources/project_service_v2/project-service-delete-project-grant.api.mdx) to remove a project grant. + // + // Remove a project grant. All user grants for this project grant will also be removed. A user will not have access to the project afterward (if permissions are checked). rpc RemoveProjectGrant(RemoveProjectGrantRequest) returns (RemoveProjectGrantResponse) { option (google.api.http) = { delete: "/projects/{project_id}/grants/{grant_id}" @@ -3923,8 +4012,7 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Project Grants"; - summary: "Remove Project Grant"; - description: "Remove a project grant. All user grants for this project grant will also be removed. A user will not have access to the project afterward (if permissions are checked)." + deprecated: true; parameters: { headers: { name: "x-zitadel-orgid"; diff --git a/proto/zitadel/project/v2beta/project_service.proto b/proto/zitadel/project/v2beta/project_service.proto new file mode 100644 index 0000000000..cb7110bc91 --- /dev/null +++ b/proto/zitadel/project/v2beta/project_service.proto @@ -0,0 +1,1237 @@ +syntax = "proto3"; + +package zitadel.project.v2beta; + +import "google/api/annotations.proto"; +import "google/api/field_behavior.proto"; +import "google/protobuf/duration.proto"; +import "google/protobuf/struct.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; +import "validate/validate.proto"; + +import "zitadel/protoc_gen_zitadel/v2/options.proto"; + +import "zitadel/project/v2beta/query.proto"; +import "google/protobuf/timestamp.proto"; +import "zitadel/filter/v2beta/filter.proto"; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/project/v2beta;project"; + +option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { + info: { + title: "Project Service"; + version: "2.0-beta"; + description: "This API is intended to manage Projects in a ZITADEL Organization. This service is in beta state. It can AND will continue breaking until a stable version is released."; + contact:{ + name: "ZITADEL" + url: "https://zitadel.com" + email: "hi@zitadel.com" + } + license: { + name: "Apache 2.0", + url: "https://github.com/zitadel/zitadel/blob/main/LICENSING.md"; + }; + }; + schemes: HTTPS; + schemes: HTTP; + + consumes: "application/json"; + consumes: "application/grpc"; + + produces: "application/json"; + produces: "application/grpc"; + + consumes: "application/grpc-web+proto"; + produces: "application/grpc-web+proto"; + + host: "$CUSTOM-DOMAIN"; + base_path: "/"; + + external_docs: { + description: "Detailed information about ZITADEL", + url: "https://zitadel.com/docs" + } + security_definitions: { + security: { + key: "OAuth2"; + value: { + type: TYPE_OAUTH2; + flow: FLOW_ACCESS_CODE; + authorization_url: "$CUSTOM-DOMAIN/oauth/v2/authorize"; + token_url: "$CUSTOM-DOMAIN/oauth/v2/token"; + scopes: { + scope: { + key: "openid"; + value: "openid"; + } + scope: { + key: "urn:zitadel:iam:org:project:id:zitadel:aud"; + value: "urn:zitadel:iam:org:project:id:zitadel:aud"; + } + } + } + } + } + security: { + security_requirement: { + key: "OAuth2"; + value: { + scope: "openid"; + scope: "urn:zitadel:iam:org:project:id:zitadel:aud"; + } + } + } + responses: { + key: "403"; + value: { + description: "Returned when the user does not have permission to access the resource."; + schema: { + json_schema: { + ref: "#/definitions/rpcStatus"; + } + } + } + } + responses: { + key: "404"; + value: { + description: "Returned when the resource does not exist."; + schema: { + json_schema: { + ref: "#/definitions/rpcStatus"; + } + } + } + } +}; + +// Service to manage projects. +service ProjectService { + + // Create Project + // + // Create a new Project. + // + // Required permission: + // - `project.create` + rpc CreateProject (CreateProjectRequest) returns (CreateProjectResponse) { + option (google.api.http) = { + post: "/v2beta/projects" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "Project created successfully"; + }; + }; + responses: { + key: "409" + value: { + description: "The project to create already exists."; + } + }; + }; + } + + // Update Project + // + // Update an existing project. + // + // Required permission: + // - `project.write` + rpc UpdateProject (UpdateProjectRequest) returns (UpdateProjectResponse) { + option (google.api.http) = { + post: "/v2beta/projects/{id}" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "Project successfully updated or left unchanged"; + }; + }; + responses: { + key: "404" + value: { + description: "The project to update does not exist."; + } + }; + }; + } + + // Delete Project + // + // Delete an existing project. + // In case the project is not found, the request will return a successful response as + // the desired state is already achieved. + // + // Required permission: + // - `project.delete` + rpc DeleteProject (DeleteProjectRequest) returns (DeleteProjectResponse) { + option (google.api.http) = { + delete: "/v2beta/projects/{id}" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "Project deleted successfully"; + }; + }; + }; + } + + // Get Project + // + // Returns the project identified by the requested ID. + // + // Required permission: + // - `project.read` + rpc GetProject (GetProjectRequest) returns (GetProjectResponse) { + option (google.api.http) = { + get: "/v2beta/projects/{id}" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "project.read" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200" + value: { + description: "Project retrieved successfully"; + } + }; + responses: { + key: "404" + value: { + description: "The project to get does not exist."; + } + }; + }; + } + + // List Projects + // + // List all matching projects. By default all projects of the instance that the caller has permission to read are returned. + // Make sure to include a limit and sorting for pagination. + // + // Required permission: + // - `project.read` + rpc ListProjects (ListProjectsRequest) returns (ListProjectsResponse) { + option (google.api.http) = { + post: "/v2beta/projects/search", + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "project.read" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "A list of all projects matching the query"; + }; + }; + responses: { + key: "400"; + value: { + description: "invalid list query"; + }; + }; + }; + } + + // Deactivate Project + // + // Set the state of a project to deactivated. Request returns no error if the project is already deactivated. + // Applications under deactivated projects are not able to login anymore. + // + // Required permission: + // - `project.write` + rpc DeactivateProject (DeactivateProjectRequest) returns (DeactivateProjectResponse) { + option (google.api.http) = { + post: "/v2beta/projects/{id}/deactivate" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "Project deactivated successfully"; + }; + }; + responses: { + key: "404" + value: { + description: "The project to deactivate does not exist."; + } + }; + }; + } + + // Activate Project + // + // Set the state of a project to active. Request returns no error if the project is already activated. + // + // Required permission: + // - `project.write` + rpc ActivateProject (ActivateProjectRequest) returns (ActivateProjectResponse) { + option (google.api.http) = { + post: "/v2beta/projects/{id}/activate" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "Project activated successfully"; + }; + }; + responses: { + key: "404" + value: { + description: "The project to activate does not exist."; + } + }; + }; + } + + // Add Project Role + // + // Add a new project role to a project. The key must be unique within the project. + // + // Required permission: + // - `project.role.write` + rpc AddProjectRole (AddProjectRoleRequest) returns (AddProjectRoleResponse) { + option (google.api.http) = { + post: "/v2beta/projects/{project_id}/roles" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "Project role added successfully"; + }; + }; + responses: { + key: "404" + value: { + description: "The project to add the project role does not exist."; + } + }; + }; + } + + // Update Project Role + // + // Change a project role. The key is not editable. If a key should change, remove the role and create a new one. + // + // Required permission: + // - `project.role.write` + rpc UpdateProjectRole (UpdateProjectRoleRequest) returns (UpdateProjectRoleResponse) { + option (google.api.http) = { + post: "/v2beta/projects/{project_id}/roles/{role_key}" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "Project role updated successfully"; + }; + }; + responses: { + key: "404" + value: { + description: "The project role to update does not exist."; + } + }; + }; + } + + // Remove Project Role + // + // Removes the role from the project and on every resource it has a dependency. This includes project grants and user grants. + // + // Required permission: + // - `project.role.write` + rpc RemoveProjectRole (RemoveProjectRoleRequest) returns (RemoveProjectRoleResponse) { + option (google.api.http) = { + delete: "/v2beta/projects/{project_id}/roles/{role_key}" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "Project role removed successfully"; + }; + }; + responses: { + key: "404" + value: { + description: "The project role to remove does not exist."; + } + }; + }; + } + + // List Project Roles + // + // Returns all roles of a project matching the search query. + // + // Required permission: + // - `project.role.read` + rpc ListProjectRoles (ListProjectRolesRequest) returns (ListProjectRolesResponse) { + option (google.api.http) = { + delete: "/v2beta/projects/{project_id}/roles/search" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "project.role.read" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "A list of all project roles matching the query"; + }; + }; + responses: { + key: "400"; + value: { + description: "invalid list query"; + }; + }; + }; + } + + // Create Project Grant + // + // Grant a project to another organization. + // The project grant will allow the granted organization to access the project and manage the authorizations for its users. + // + // Required permission: + // - `project.grant.create` + rpc CreateProjectGrant (CreateProjectGrantRequest) returns (CreateProjectGrantResponse) { + option (google.api.http) = { + post: "/v2beta/projects/{project_id}/grants" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "Project grant created successfully"; + }; + }; + responses: { + key: "409" + value: { + description: "The project grant to create already exists."; + } + }; + }; + } + + // Update Project Grant + // + // Change the roles of the project that is granted to another organization. + // The project grant will allow the granted organization to access the project and manage the authorizations for its users. + // + // Required permission: + // - `project.grant.write` + rpc UpdateProjectGrant (UpdateProjectGrantRequest) returns (UpdateProjectGrantResponse) { + option (google.api.http) = { + post: "/v2beta/projects/{project_id}/grants/{granted_organization_id}" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "Project grant successfully updated or left unchanged"; + }; + }; + responses: { + key: "404" + value: { + description: "The project grant to update does not exist."; + } + }; + }; + } + + // Delete Project Grant + // + // Delete a project grant. All user grants for this project grant will also be removed. + // A user will not have access to the project afterward (if permissions are checked). + // In case the project grant is not found, the request will return a successful response as + // the desired state is already achieved. + // + // Required permission: + // - `project.grant.delete` + rpc DeleteProjectGrant (DeleteProjectGrantRequest) returns (DeleteProjectGrantResponse) { + option (google.api.http) = { + delete: "/v2beta/projects/{project_id}/grants/{granted_organization_id}" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "Project grant deleted successfully"; + }; + }; + }; + } + + // Deactivate Project Grant + // + // Set the state of the project grant to deactivated. + // Applications under deactivated projects grants are not able to login anymore. + // + // Required permission: + // - `project.grant.write` + rpc DeactivateProjectGrant(DeactivateProjectGrantRequest) returns (DeactivateProjectGrantResponse) { + option (google.api.http) = { + post: "/v2beta/projects/{project_id}/grants/{granted_organization_id}/deactivate" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "Project grant deactivated successfully"; + }; + }; + }; + } + + // Activate Project Grant + // + // Set the state of the project grant to activated. + // + // Required permission: + // - `project.grant.write` + rpc ActivateProjectGrant(ActivateProjectGrantRequest) returns (ActivateProjectGrantResponse) { + option (google.api.http) = { + post: "/v2beta/projects/{project_id}/grants/{granted_organization_id}/activate" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "Project grant activated successfully"; + }; + }; + }; + } + + // List Project Grants + // + // Returns a list of project grants. A project grant is when the organization grants its project to another organization. + // + // Required permission: + // - `project.grant.write` + rpc ListProjectGrants(ListProjectGrantsRequest) returns (ListProjectGrantsResponse) { + option (google.api.http) = { + post: "/v2beta/projects/grants/search" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "project.grant.read" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "A list of all project grants matching the query"; + }; + }; + responses: { + key: "400"; + value: { + description: "invalid list query"; + }; + }; + }; + } +} + +message CreateProjectRequest { + // The unique identifier of the organization the project belongs to. + string organization_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1, + max_length: 200, + example: "\"69629026806489455\""; + } + ]; + // The unique identifier of the project. + optional string id = 2 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1, + max_length: 200, + example: "\"69629026806489455\""; + } + ]; + // Name of the project. + string name = 3 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"MyProject\""; + } + ]; + // Enable this setting to have role information included in the user info endpoint. It is also dependent on your application settings to include it in tokens and other types. + bool project_role_assertion = 4; + // When enabled ZITADEL will check if a user has an authorization to use this project assigned when login into an application of this project. + bool authorization_required = 5; + // When enabled ZITADEL will check if the organization of the user, that is trying to log in, has access to this project (either owns the project or is granted). + bool project_access_required = 6; + // Define which private labeling/branding should trigger when getting to a login of this project. + PrivateLabelingSetting private_labeling_setting = 7 [ + (validate.rules).enum = {defined_only: true} + ]; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { + example: "{\"organizationId\":\"69629026806489455\",\"name\":\"MyProject\",\"projectRoleAssertion\":true,\"projectRoleCheck\":true,\"hasProjectCheck\":true,\"privateLabelingSetting\":\"PRIVATE_LABELING_SETTING_UNSPECIFIED\"}"; + }; +} + +message CreateProjectResponse { + // The unique identifier of the newly created project. + string id = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629012906488334\""; + } + ]; + // The timestamp of the project creation. + google.protobuf.Timestamp creation_date = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; +} + +message UpdateProjectRequest { + string id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1, + max_length: 200, + example: "\"69629026806489455\""; + } + ]; + // Name of the project. + optional string name = 2 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"MyProject-Updated\""; + } + ]; + // Enable this setting to have role information included in the user info endpoint. It is also dependent on your application settings to include it in tokens and other types. + optional bool project_role_assertion = 3; + // When enabled ZITADEL will check if a user has a role of this project assigned when login into an application of this project. + optional bool project_role_check = 4; + // When enabled ZITADEL will check if the organization of the user, that is trying to log in, has a grant to this project. + optional bool has_project_check = 5; + // Define which private labeling/branding should trigger when getting to a login of this project. + optional PrivateLabelingSetting private_labeling_setting = 6 [ + (validate.rules).enum = {defined_only: true} + ]; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { + example: "{\"name\":\"MyProject-Updated\",\"projectRoleAssertion\":true,\"projectRoleCheck\":true,\"hasProjectCheck\":true,\"privateLabelingSetting\":\"PRIVATE_LABELING_SETTING_UNSPECIFIED\"}"; + }; +} + +message UpdateProjectResponse { + // The timestamp of the change of the project. + google.protobuf.Timestamp change_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message DeleteProjectRequest { + // The unique identifier of the project. + string id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1, + max_length: 200, + example: "\"69629026806489455\""; + } + ]; +} + +message DeleteProjectResponse { + // The timestamp of the deletion of the project. + // 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 = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message GetProjectRequest { + // The unique identifier of the project. + string id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1, + max_length: 200, + example: "\"69629026806489455\""; + } + ]; +} + +message GetProjectResponse { + Project project = 1; +} + +message ListProjectsRequest { + // 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 ProjectFieldName sorting_column = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"PROJECT_FIELD_NAME_CREATION_DATE\"" + } + ]; + // Define the criteria to query for. + repeated ProjectSearchFilter filters = 3; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { + example: "{\"pagination\":{\"offset\":0,\"limit\":0,\"asc\":true},\"sortingColumn\":\"PROJECT_FIELD_NAME_CREATION_DATE\",\"filters\":[{\"projectNameFilter\":{\"projectName\":\"MyProject\",\"method\":\"TEXT_FILTER_METHOD_EQUALS\"}},{\"inProjectIdsFilter\":{\"projectIds\":[\"69629023906488334\",\"69622366012355662\"]}}]}"; + }; +} + +message ListProjectsResponse { + zitadel.filter.v2beta.PaginationResponse pagination = 1; + repeated Project projects = 2; +} + +message DeactivateProjectRequest { + // The unique identifier of the project. + string id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1, + max_length: 200, + example: "\"69629026806489455\""; + } + ]; +} + +message DeactivateProjectResponse { + // The timestamp of the change of the project. + google.protobuf.Timestamp change_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message ActivateProjectRequest { + // The unique identifier of the project. + string id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1, + max_length: 200, + example: "\"69629026806489455\""; + } + ]; +} + +message ActivateProjectResponse { + // The timestamp of the change of the project. + google.protobuf.Timestamp change_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message AddProjectRoleRequest { + // ID of the project. + string project_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"69629026806489455\""; + } + ]; + // The key is the only relevant attribute for ZITADEL regarding the authorization checks. + string role_key = 2 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"ADMIN\""; + } + ]; + // Name displayed for the role. + string display_name = 3 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"Administrator\""; + } + ]; + // The group is only used for display purposes. That you have better handling, like giving all the roles from a group to a user. + optional string group = 4 [ + (validate.rules).string = {max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + max_length: 200; + example: "\"Admins\""; + } + ]; +} + +message AddProjectRoleResponse { + // The timestamp of the project role creation. + google.protobuf.Timestamp creation_date = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; +} + +message UpdateProjectRoleRequest { + // ID of the project. + string project_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"69629026806489455\""; + } + ]; + // The key is the only relevant attribute for ZITADEL regarding the authorization checks. + string role_key = 2 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"ADMIN\""; + } + ]; + // Name displayed for the role. + optional string display_name = 3 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"Administrator\""; + } + ]; + // The group is only used for display purposes. That you have better handling, like giving all the roles from a group to a user. + optional string group = 4 [ + (validate.rules).string = {max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + max_length: 200; + example: "\"Admins\""; + } + ]; +} + +message UpdateProjectRoleResponse { + // The timestamp of the change of the project role. + google.protobuf.Timestamp change_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message RemoveProjectRoleRequest { + // ID of the project. + string project_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"69629026806489455\""; + } + ]; + // The key is the only relevant attribute for ZITADEL regarding the authorization checks. + string role_key = 2 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"ADMIN\""; + } + ]; +} + +message RemoveProjectRoleResponse { + // The timestamp of the removal of the project role. + // Note that the removal date is only guaranteed to be set if the removal was successful during the request. + // In case the removal occurred in a previous request, the removal date might be empty. + google.protobuf.Timestamp removal_date = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message ListProjectRolesRequest { + // ID of the project. + string project_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"69629026806489455\""; + } + ]; + // List limitations and ordering. + optional zitadel.filter.v2beta.PaginationRequest pagination = 2; + // 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 ProjectRoleFieldName sorting_column = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"PROJECT_ROLE_FIELD_NAME_CREATION_DATE\"" + } + ]; + // Define the criteria to query for. + repeated ProjectRoleSearchFilter filters = 4; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { + example: "{\"pagination\":{\"offset\":0,\"limit\":0,\"asc\":true},\"sortingColumn\":\"PROJECT_ROLE_FIELD_NAME_CREATION_DATE\",\"filters\":[{\"keyFilter\":{\"key\":\"role.super.man\",\"method\":\"TEXT_FILTER_METHOD_EQUALS\"}},{\"displayNameFilter\":{\"displayName\":\"SUPER\"}}]}"; + }; +} + +message ListProjectRolesResponse { + zitadel.filter.v2beta.PaginationResponse pagination = 1; + repeated ProjectRole project_roles = 2; +} + + +message CreateProjectGrantRequest { + // ID of the project. + string project_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"69629026806489455\""; + } + ]; + // Organization the project is granted to. + string granted_organization_id = 2 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"28746028909593987\"" + } + ]; + // Keys of the role available for the project grant. + repeated string role_keys = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "[\"RoleKey1\", \"RoleKey2\"]"; + } + ]; +} + +message CreateProjectGrantResponse { + // The timestamp of the project grant creation. + google.protobuf.Timestamp creation_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; +} + +message UpdateProjectGrantRequest { + // ID of the project. + string project_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"69629026806489455\""; + } + ]; + // Organization the project is granted to. + string granted_organization_id = 2 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"28746028909593987\"" + } + ]; + // Keys of the role available for the project grant. + repeated string role_keys = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "[\"RoleKey1\", \"RoleKey2\"]"; + } + ]; +} + +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 DeleteProjectGrantRequest { + // ID of the project. + string project_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"69629026806489455\""; + } + ]; + // Organization the project is granted to. + string granted_organization_id = 2 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"28746028909593987\"" + } + ]; +} + +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 = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message DeactivateProjectGrantRequest { + // ID of the project. + string project_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"69629026806489455\""; + } + ]; + // Organization the project is granted to. + string granted_organization_id = 2 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"28746028909593987\"" + } + ]; +} + +message DeactivateProjectGrantResponse { + // 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 ActivateProjectGrantRequest { + // ID of the project. + string project_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"69629026806489455\""; + } + ]; + // Organization the project is granted to. + string granted_organization_id = 2 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"28746028909593987\"" + } + ]; +} + +message ActivateProjectGrantResponse { + // 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 ListProjectGrantsRequest { + // 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 ProjectGrantFieldName sorting_column = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"PROJECT_GRANT_FIELD_NAME_CREATION_DATE\"" + } + ]; + // Define the criteria to query for. + repeated ProjectGrantSearchFilter filters = 3; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { + example: "{\"pagination\":{\"offset\":0,\"limit\":0,\"asc\":true},\"filters\":[{\"projectNameFilter\":{\"projectName\":\"MyProject\",\"method\":\"TEXT_FILTER_METHOD_EQUALS\"}},{\"inProjectIdsFilter\":{\"projectIds\":[\"69629023906488334\",\"69622366012355662\"]}}]}"; + }; +} + +message ListProjectGrantsResponse { + zitadel.filter.v2beta.PaginationResponse pagination = 1; + repeated ProjectGrant project_grants = 2; +} \ No newline at end of file diff --git a/proto/zitadel/project/v2beta/query.proto b/proto/zitadel/project/v2beta/query.proto new file mode 100644 index 0000000000..f328b65189 --- /dev/null +++ b/proto/zitadel/project/v2beta/query.proto @@ -0,0 +1,347 @@ +syntax = "proto3"; + +package zitadel.project.v2beta; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/project/v2beta;project"; + +import "google/api/field_behavior.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; +import "validate/validate.proto"; +import "google/protobuf/timestamp.proto"; + +import "zitadel/filter/v2beta/filter.proto"; + +message ProjectGrant { + // The unique identifier of the organization which granted the project to the granted_organization_id. + string organization_id = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629012906488334\""; + } + ]; + // The timestamp of the granted project creation. + google.protobuf.Timestamp creation_date = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; + // The timestamp of the last change to the granted project (e.g. creation, activation, deactivation). + google.protobuf.Timestamp change_date = 4 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; + // The ID of the organization the project is granted to. + string granted_organization_id = 5 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629023906488334\"" + } + ]; + // The name of the organization the project is granted to. + string granted_organization_name = 6 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"Some Organization\"" + } + ]; + // The roles of the granted project. + repeated string granted_role_keys = 7 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "[\"role.super.man\"]" + } + ]; + // The ID of the granted project. + string project_id = 8 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629023906488334\"" + } + ]; + // The name of the granted project. + string project_name = 9 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"ZITADEL\"" + } + ]; + // Describes the current state of the granted project. + ProjectGrantState state = 10 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "current state of the project"; + } + ]; +} + +enum ProjectGrantState { + PROJECT_GRANT_STATE_UNSPECIFIED = 0; + PROJECT_GRANT_STATE_ACTIVE = 1; + PROJECT_GRANT_STATE_INACTIVE = 2; +} + +message Project { + // The unique identifier of the project. + string id = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629012906488334\""; + } + ]; + // The unique identifier of the organization the project belongs to. + string organization_id = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629012906488334\""; + } + ]; + // The timestamp of the project creation. + google.protobuf.Timestamp creation_date = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; + // The timestamp of the last change to the project (e.g. creation, activation, deactivation). + google.protobuf.Timestamp change_date = 4 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; + // The name of the project. + string name = 5 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"ip_allow_list\""; + } + ]; + // Describes the current state of the project. + ProjectState state = 6 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "current state of the project"; + } + ]; + // Describes if the roles of the user should be added to the token. + bool project_role_assertion = 7; + // When enabled ZITADEL will check if a user has an authorization to use this project assigned when login into an application of this project. + bool authorization_required = 8; + // When enabled ZITADEL will check if the organization of the user, that is trying to log in, has access to this project (either owns the project or is granted). + bool project_access_required = 9; + // Defines from where the private labeling should be triggered. + PrivateLabelingSetting private_labeling_setting = 10; + + // The ID of the organization the project is granted to. + optional string granted_organization_id = 12 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629023906488334\"" + } + ]; + // The name of the organization the project is granted to. + optional string granted_organization_name = 13 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"Some Organization\"" + } + ]; + // Describes the current state of the granted project. + GrantedProjectState granted_state = 14 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "current state of the granted project"; + } + ]; +} + +enum ProjectState { + PROJECT_STATE_UNSPECIFIED = 0; + PROJECT_STATE_ACTIVE = 1; + PROJECT_STATE_INACTIVE = 2; +} + +enum GrantedProjectState { + GRANTED_PROJECT_STATE_UNSPECIFIED = 0; + GRANTED_PROJECT_STATE_ACTIVE = 1; + GRANTED_PROJECT_STATE_INACTIVE = 2; +} + +enum PrivateLabelingSetting { + PRIVATE_LABELING_SETTING_UNSPECIFIED = 0; + PRIVATE_LABELING_SETTING_ENFORCE_PROJECT_RESOURCE_OWNER_POLICY = 1; + PRIVATE_LABELING_SETTING_ALLOW_LOGIN_USER_RESOURCE_OWNER_POLICY = 2; +} + +enum ProjectFieldName { + PROJECT_FIELD_NAME_UNSPECIFIED = 0; + PROJECT_FIELD_NAME_ID = 1; + PROJECT_FIELD_NAME_CREATION_DATE = 2; + PROJECT_FIELD_NAME_CHANGE_DATE = 3; + PROJECT_FIELD_NAME_NAME = 4; +} + +enum ProjectGrantFieldName { + PROJECT_GRANT_FIELD_NAME_UNSPECIFIED = 0; + PROJECT_GRANT_FIELD_NAME_PROJECT_ID = 1; + PROJECT_GRANT_FIELD_NAME_CREATION_DATE = 2; + PROJECT_GRANT_FIELD_NAME_CHANGE_DATE = 3; +} + +enum ProjectRoleFieldName { + PROJECT_ROLE_FIELD_NAME_UNSPECIFIED = 0; + PROJECT_ROLE_FIELD_NAME_KEY = 1; + PROJECT_ROLE_FIELD_NAME_CREATION_DATE = 2; + PROJECT_ROLE_FIELD_NAME_CHANGE_DATE = 3; +} + +message ProjectSearchFilter { + oneof filter { + option (validate.required) = true; + + ProjectNameFilter project_name_filter = 1; + InProjectIDsFilter in_project_ids_filter = 2; + ProjectResourceOwnerFilter project_resource_owner_filter = 3; + ProjectGrantResourceOwnerFilter project_grant_resource_owner_filter = 4; + ProjectOrganizationIDFilter project_organization_id_filter = 5; + } +} + +message ProjectNameFilter { + // Defines the name of the project to query for. + string project_name = 1 [ + (validate.rules).string = {max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + max_length: 200; + example: "\"ip_allow_list\""; + } + ]; + // Defines which text comparison method used for the name query. + zitadel.filter.v2beta.TextFilterMethod method = 2 [ + (validate.rules).enum.defined_only = true, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "defines which text equality method is used"; + } + ]; +} + +message InProjectIDsFilter { + // Defines the ids to query for. + repeated string project_ids = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "the ids of the projects to include" + example: "[\"69629023906488334\",\"69622366012355662\"]"; + } + ]; +} + +message ProjectResourceOwnerFilter { + // Defines the ID of organization the project belongs to query for. + string project_resource_owner = 1 [ + (validate.rules).string = {max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629023906488334\"" + } + ]; +} + +message ProjectGrantResourceOwnerFilter { + // Defines the ID of organization the project grant belongs to query for. + string project_grant_resource_owner = 1 [ + (validate.rules).string = {max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629023906488334\"" + } + ]; +} + +message ProjectOrganizationIDFilter { + // Defines the ID of organization the project and granted project belong to query for. + string project_organization_id = 1 [ + (validate.rules).string = {max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629023906488334\"" + } + ]; +} + +message ProjectGrantSearchFilter { + oneof filter { + option (validate.required) = true; + + ProjectNameFilter project_name_filter = 1; + ProjectRoleKeyFilter role_key_filter = 2; + InProjectIDsFilter in_project_ids_filter = 3; + ProjectResourceOwnerFilter project_resource_owner_filter = 4; + ProjectGrantResourceOwnerFilter project_grant_resource_owner_filter = 5; + } +} + +message GrantedOrganizationIDFilter { + // Defines the ID of organization the project is granted to query for. + string granted_organization_id = 1 [ + (validate.rules).string = {max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629023906488334\"" + } + ]; +} + +message ProjectRole { + // ID of the project. + string project_id = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629026806489455\""; + } + ]; + // Key of the project role. + string key = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"role.super.man\"" + } + ]; + // The timestamp of the project role creation. + google.protobuf.Timestamp creation_date = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; + // The timestamp of the last change to the project role. + google.protobuf.Timestamp change_date = 4 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; + // Display name of the project role. + string display_name = 5 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"Super man\"" + } + ]; + // Group of the project role. + string group = 6 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"people\"" + } + ]; +} + +message ProjectRoleSearchFilter { + oneof filter { + option (validate.required) = true; + + ProjectRoleKeyFilter role_key_filter = 1; + ProjectRoleDisplayNameFilter display_name_filter = 2; + } +} + +message ProjectRoleKeyFilter { + string key = 1 [ + (validate.rules).string = {max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"role.super.man\"" + } + ]; + // Defines which text comparison method used for the name query. + zitadel.filter.v2beta.TextFilterMethod method = 2 [ + (validate.rules).enum.defined_only = true + ]; +} + +message ProjectRoleDisplayNameFilter { + string display_name = 1 [ + (validate.rules).string = {max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"SUPER\"" + } + ]; + // Defines which text comparison method used for the name query. + zitadel.filter.v2beta.TextFilterMethod method = 2 [ + (validate.rules).enum.defined_only = true + ]; +} \ No newline at end of file From 2cf3ef4de4ef993367daec6ff3974bdbdf70d2f3 Mon Sep 17 00:00:00 2001 From: Livio Spring Date: Fri, 23 May 2025 13:52:25 +0200 Subject: [PATCH 43/76] feat: federated logout for SAML IdPs (#9931) # Which Problems Are Solved Currently if a user signs in using an IdP, once they sign out of Zitadel, the corresponding IdP session is not terminated. This can be the desired behavior. In some cases, e.g. when using a shared computer it results in a potential security risk, since a follower user might be able to sign in as the previous using the still open IdP session. # How the Problems Are Solved - Admins can enabled a federated logout option on SAML IdPs through the Admin and Management APIs. - During the termination of a login V1 session using OIDC end_session endpoint, Zitadel will check if an IdP was used to authenticate that session. - In case there was a SAML IdP used with Federated Logout enabled, it will intercept the logout process, store the information into the shared cache and redirect to the federated logout endpoint in the V1 login. - The V1 login federated logout endpoint checks every request on an existing cache entry. On success it will create a SAML logout request for the used IdP and either redirect or POST to the configured SLO endpoint. The cache entry is updated with a `redirected` state. - A SLO endpoint is added to the `/idp` handlers, which will handle the SAML logout responses. At the moment it will check again for an existing federated logout entry (with state `redirected`) in the cache. On success, the user is redirected to the initially provided `post_logout_redirect_uri` from the end_session request. # Additional Changes None # Additional Context - This PR merges the https://github.com/zitadel/zitadel/pull/9841 and https://github.com/zitadel/zitadel/pull/9854 to main, additionally updating the docs on Entra ID SAML. - closes #9228 - backport to 3.x --------- Co-authored-by: Silvan <27845747+adlerhurst@users.noreply.github.com> Co-authored-by: Zach Hirschtritt --- cmd/defaults.yaml | 10 ++ cmd/setup/56.go | 27 ++++ cmd/setup/56.sql | 1 + cmd/setup/config.go | 1 + cmd/setup/setup.go | 2 + cmd/start/start.go | 30 ++++- .../provider-oidc.component.html | 13 +- .../provider-saml-sp.component.html | 11 +- .../provider-saml-sp.component.scss | 10 +- .../provider-saml-sp.component.ts | 7 + console/src/assets/i18n/bg.json | 3 +- console/src/assets/i18n/cs.json | 2 + console/src/assets/i18n/de.json | 2 + console/src/assets/i18n/en.json | 2 + 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 + .../identity-providers/azure-ad-saml.mdx | 5 +- internal/api/grpc/admin/idp_converter.go | 2 + internal/api/grpc/idp/converter.go | 1 + internal/api/grpc/idp/v2/query.go | 1 + internal/api/grpc/management/idp_converter.go | 2 + internal/api/idp/idp.go | 44 +++++++ internal/api/oidc/auth_request.go | 63 ++++++++- internal/api/oidc/op.go | 19 ++- .../api/ui/login/external_provider_handler.go | 121 ++++++++++++++++++ internal/api/ui/login/login.go | 30 +++-- internal/api/ui/login/router.go | 2 + .../eventsourcing/eventstore/user.go | 5 + .../eventsourcing/view/user_session.go | 4 + internal/auth/repository/user.go | 2 + internal/cache/cache.go | 1 + internal/cache/connector/connector.go | 1 + internal/cache/purpose_enumer.go | 12 +- internal/command/idp.go | 1 + internal/command/idp_intent_test.go | 2 + internal/command/idp_model.go | 10 ++ internal/command/instance_idp.go | 3 + internal/command/instance_idp_model.go | 2 + internal/command/instance_idp_test.go | 8 ++ internal/command/org_idp.go | 3 + internal/command/org_idp_model.go | 2 + internal/command/org_idp_test.go | 8 ++ internal/domain/federatedlogout/logout.go | 37 ++++++ internal/query/idp_template.go | 13 ++ internal/query/idp_template_test.go | 25 ++++ internal/query/projection/idp_template.go | 6 + .../query/projection/idp_template_test.go | 18 ++- internal/repository/idp/saml.go | 10 ++ internal/repository/instance/idp.go | 2 + internal/repository/org/idp.go | 2 + .../user/repository/view/user_session.sql | 29 +++++ .../user/repository/view/user_session_view.go | 19 ++- proto/zitadel/admin.proto | 6 + proto/zitadel/idp.proto | 3 + proto/zitadel/idp/v2/idp.proto | 3 + proto/zitadel/management.proto | 6 + 69 files changed, 633 insertions(+), 51 deletions(-) create mode 100644 cmd/setup/56.go create mode 100644 cmd/setup/56.sql create mode 100644 internal/domain/federatedlogout/logout.go create mode 100644 internal/user/repository/view/user_session.sql diff --git a/cmd/defaults.yaml b/cmd/defaults.yaml index 397e9af376..7bb44b743f 100644 --- a/cmd/defaults.yaml +++ b/cmd/defaults.yaml @@ -312,6 +312,16 @@ Caches: AddSource: true Formatter: Format: text + # Federated logouts store the information needed to handle federated logout and their state transfer + FederatedLogouts: + Connector: "postgres" + MaxAge: 1h + LastUseAge: 10m + Log: + Level: error + AddSource: true + Formatter: + Format: text Machine: # Cloud-hosted VMs need to specify their metadata endpoint so that the machine can be uniquely identified. diff --git a/cmd/setup/56.go b/cmd/setup/56.go new file mode 100644 index 0000000000..72ccb5e6ff --- /dev/null +++ b/cmd/setup/56.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 56.sql + addSAMLFederatedLogout string +) + +type IDPTemplate6SAMLFederatedLogout struct { + dbClient *database.DB +} + +func (mig *IDPTemplate6SAMLFederatedLogout) Execute(ctx context.Context, _ eventstore.Event) error { + _, err := mig.dbClient.ExecContext(ctx, addSAMLFederatedLogout) + return err +} + +func (mig *IDPTemplate6SAMLFederatedLogout) String() string { + return "56_idp_templates6_add_saml_federated_logout" +} diff --git a/cmd/setup/56.sql b/cmd/setup/56.sql new file mode 100644 index 0000000000..f5544eddf5 --- /dev/null +++ b/cmd/setup/56.sql @@ -0,0 +1 @@ +ALTER TABLE IF EXISTS projections.idp_templates6_saml ADD COLUMN IF NOT EXISTS federated_logout_enabled BOOLEAN DEFAULT FALSE; diff --git a/cmd/setup/config.go b/cmd/setup/config.go index 5e5c842b14..bd2abde9ea 100644 --- a/cmd/setup/config.go +++ b/cmd/setup/config.go @@ -152,6 +152,7 @@ type Steps struct { s53InitPermittedOrgsFunction *InitPermittedOrgsFunction53 s54InstancePositionIndex *InstancePositionIndex s55ExecutionHandlerStart *ExecutionHandlerStart + s56IDPTemplate6SAMLFederatedLogout *IDPTemplate6SAMLFederatedLogout } func MustNewSteps(v *viper.Viper) *Steps { diff --git a/cmd/setup/setup.go b/cmd/setup/setup.go index 58bc89d2e4..c84976f282 100644 --- a/cmd/setup/setup.go +++ b/cmd/setup/setup.go @@ -214,6 +214,7 @@ func Setup(ctx context.Context, config *Config, steps *Steps, masterKey string) steps.s53InitPermittedOrgsFunction = &InitPermittedOrgsFunction53{dbClient: dbClient} steps.s54InstancePositionIndex = &InstancePositionIndex{dbClient: dbClient} steps.s55ExecutionHandlerStart = &ExecutionHandlerStart{dbClient: dbClient} + steps.s56IDPTemplate6SAMLFederatedLogout = &IDPTemplate6SAMLFederatedLogout{dbClient: dbClient} err = projection.Create(ctx, dbClient, eventstoreClient, config.Projections, nil, nil, nil) logging.OnError(err).Fatal("unable to start projections") @@ -258,6 +259,7 @@ func Setup(ctx context.Context, config *Config, steps *Steps, masterKey string) steps.s53InitPermittedOrgsFunction, steps.s54InstancePositionIndex, steps.s55ExecutionHandlerStart, + steps.s56IDPTemplate6SAMLFederatedLogout, } { setupErr = executeMigration(ctx, eventstoreClient, step, "migration failed") if setupErr != nil { diff --git a/cmd/start/start.go b/cmd/start/start.go index 1d83197062..af76b29e99 100644 --- a/cmd/start/start.go +++ b/cmd/start/start.go @@ -74,12 +74,14 @@ import ( "github.com/zitadel/zitadel/internal/authz" authz_repo "github.com/zitadel/zitadel/internal/authz/repository" authz_es "github.com/zitadel/zitadel/internal/authz/repository/eventsourcing/eventstore" + "github.com/zitadel/zitadel/internal/cache" "github.com/zitadel/zitadel/internal/cache/connector" "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/crypto" cryptoDB "github.com/zitadel/zitadel/internal/crypto/database" "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/domain/federatedlogout" "github.com/zitadel/zitadel/internal/eventstore" old_es "github.com/zitadel/zitadel/internal/eventstore/repository/sql" new_es "github.com/zitadel/zitadel/internal/eventstore/v3" @@ -511,7 +513,12 @@ func startAPIs( assetsCache := middleware.AssetsCacheInterceptor(config.AssetStorage.Cache.MaxAge, config.AssetStorage.Cache.SharedMaxAge) apis.RegisterHandlerOnPrefix(assets.HandlerPrefix, assets.NewHandler(commands, verifier, config.SystemAuthZ, config.InternalAuthZ, id.SonyFlakeGenerator(), store, queries, middleware.CallDurationHandler, instanceInterceptor.Handler, assetsCache.Handler, limitingAccessInterceptor.Handle)) - apis.RegisterHandlerOnPrefix(idp.HandlerPrefix, idp.NewHandler(commands, queries, keys.IDPConfig, instanceInterceptor.Handler)) + federatedLogoutsCache, err := connector.StartCache[federatedlogout.Index, string, *federatedlogout.FederatedLogout](ctx, []federatedlogout.Index{federatedlogout.IndexRequestID}, cache.PurposeFederatedLogout, cacheConnectors.Config.FederatedLogouts, cacheConnectors) + if err != nil { + return nil, err + } + + apis.RegisterHandlerOnPrefix(idp.HandlerPrefix, idp.NewHandler(commands, queries, keys.IDPConfig, instanceInterceptor.Handler, federatedLogoutsCache)) userAgentInterceptor, err := middleware.NewUserAgentHandler(config.UserAgentCookie, keys.UserAgentCookieKey, id.SonyFlakeGenerator(), config.ExternalSecure, login.EndpointResources, login.EndpointExternalLoginCallbackFormPost, login.EndpointSAMLACS) if err != nil { @@ -532,7 +539,25 @@ func startAPIs( } apis.RegisterHandlerOnPrefix(openapi.HandlerPrefix, openAPIHandler) - oidcServer, err := oidc.NewServer(ctx, config.OIDC, login.DefaultLoggedOutPath, config.ExternalSecure, commands, queries, authRepo, keys.OIDC, keys.OIDCKey, eventstore, dbClient, userAgentInterceptor, instanceInterceptor.Handler, limitingAccessInterceptor, config.Log.Slog(), config.SystemDefaults.SecretHasher) + oidcServer, err := oidc.NewServer( + ctx, + config.OIDC, + login.DefaultLoggedOutPath, + config.ExternalSecure, + commands, + queries, + authRepo, + keys.OIDC, + keys.OIDCKey, + eventstore, + dbClient, + userAgentInterceptor, + instanceInterceptor.Handler, + limitingAccessInterceptor, + config.Log.Slog(), + config.SystemDefaults.SecretHasher, + federatedLogoutsCache, + ) if err != nil { return nil, fmt.Errorf("unable to start oidc provider: %w", err) } @@ -581,6 +606,7 @@ func startAPIs( keys.IDPConfig, keys.CSRFCookieKey, cacheConnectors, + federatedLogoutsCache, ) if err != nil { return nil, fmt.Errorf("unable to start login: %w", err) diff --git a/console/src/app/modules/providers/provider-oidc/provider-oidc.component.html b/console/src/app/modules/providers/provider-oidc/provider-oidc.component.html index e326d3be26..c7bb48ea71 100644 --- a/console/src/app/modules/providers/provider-oidc/provider-oidc.component.html +++ b/console/src/app/modules/providers/provider-oidc/provider-oidc.component.html @@ -98,13 +98,12 @@

{{ 'IDP.ISIDTOKENMAPPING_DESC' | translate }}

{{ 'IDP.ISIDTOKENMAPPING' | translate }} - - - -
-

{{ 'IDP.USEPKCE_DESC' | translate }}

- {{ 'IDP.USEPKCE' | translate }} -
+ +
+

{{ 'IDP.USEPKCE_DESC' | translate }}

+ {{ 'IDP.USEPKCE' | translate }} +
+
diff --git a/console/src/app/modules/providers/provider-saml-sp/provider-saml-sp.component.html b/console/src/app/modules/providers/provider-saml-sp/provider-saml-sp.component.html index d738b3fec9..245a963fa8 100644 --- a/console/src/app/modules/providers/provider-saml-sp/provider-saml-sp.component.html +++ b/console/src/app/modules/providers/provider-saml-sp/provider-saml-sp.component.html @@ -82,7 +82,7 @@
-

{{ 'IDP.SAML.TRANSIENTMAPPINGATTRIBUTENAME_DESC' | translate }}

+

{{ 'IDP.SAML.TRANSIENTMAPPINGATTRIBUTENAME_DESC' | translate }}

@@ -90,6 +90,15 @@
+ + +
+

{{ 'IDP.FEDERATEDLOGOUTENABLED_DESC' | translate }}

+ {{ + 'IDP.FEDERATEDLOGOUTENABLED' | translate + }} +
+
{{.Form}}`)) +) + +type samlSLOPostData struct { + Form template.HTML +} + +func samlPostLogoutRequest(w http.ResponseWriter, sp crewjam_saml.ServiceProvider, slo, nameID, sessionID string) error { + lr, err := sp.MakeLogoutRequest(slo, nameID) + if err != nil { + return err + } + + return samlSLOPostTemplate.Execute(w, &samlSLOPostData{Form: template.HTML(lr.Post(sessionID))}) +} + +func (l *Login) externalUserID(ctx context.Context, userID, idpID string) (string, error) { + userIDQuery, err := query.NewIDPUserLinksUserIDSearchQuery(userID) + if err != nil { + return "", err + } + idpIDQuery, err := query.NewIDPUserLinkIDPIDSearchQuery(idpID) + if err != nil { + return "", err + } + links, err := l.query.IDPUserLinks(ctx, &query.IDPUserLinksSearchQuery{Queries: []query.SearchQuery{userIDQuery, idpIDQuery}}, nil) + if err != nil || len(links.Links) != 1 { + return "", zerrors.ThrowPreconditionFailed(err, "LOGIN-ADK21", "Errors.User.ExternalIDP.NotFound") + } + return links.Links[0].ProvidedUserID, nil +} + // IdPError wraps an error from an external IDP to be able to distinguish it from other errors and to display it // more prominent (popup style) . // It's used if an error occurs during the login process with an external IDP and local authentication is allowed, diff --git a/internal/api/ui/login/login.go b/internal/api/ui/login/login.go index 444c5aaa85..f1ce9bfa2a 100644 --- a/internal/api/ui/login/login.go +++ b/internal/api/ui/login/login.go @@ -21,6 +21,7 @@ import ( "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/domain/federatedlogout" "github.com/zitadel/zitadel/internal/form" "github.com/zitadel/zitadel/internal/query" "github.com/zitadel/zitadel/internal/static" @@ -60,25 +61,20 @@ const ( DefaultLoggedOutPath = HandlerPrefix + EndpointLogoutDone ) -func CreateLogin(config Config, +func CreateLogin( + config Config, command *command.Commands, query *query.Queries, authRepo *eventsourcing.EsRepository, staticStorage static.Storage, consolePath string, - oidcAuthCallbackURL func(context.Context, string) string, - samlAuthCallbackURL func(context.Context, string) string, + oidcAuthCallbackURL, samlAuthCallbackURL func(context.Context, string) string, externalSecure bool, - userAgentCookie, - issuerInterceptor, - oidcInstanceHandler, - samlInstanceHandler, - assetCache, - accessHandler mux.MiddlewareFunc, - userCodeAlg crypto.EncryptionAlgorithm, - idpConfigAlg crypto.EncryptionAlgorithm, + userAgentCookie, issuerInterceptor, oidcInstanceHandler, samlInstanceHandler, assetCache, accessHandler mux.MiddlewareFunc, + userCodeAlg, idpConfigAlg crypto.EncryptionAlgorithm, csrfCookieKey []byte, cacheConnectors connector.Connectors, + federateLogoutCache cache.Cache[federatedlogout.Index, string, *federatedlogout.FederatedLogout], ) (*Login, error) { login := &Login{ oidcAuthCallbackURL: oidcAuthCallbackURL, @@ -101,7 +97,7 @@ func CreateLogin(config Config, login.parser = form.NewParser() var err error - login.caches, err = startCaches(context.Background(), cacheConnectors) + login.caches, err = startCaches(context.Background(), cacheConnectors, federateLogoutCache) if err != nil { return nil, err } @@ -112,7 +108,11 @@ func csp() *middleware.CSP { csp := middleware.DefaultSCP csp.ObjectSrc = middleware.CSPSourceOptsSelf() csp.StyleSrc = csp.StyleSrc.AddNonce() - csp.ScriptSrc = csp.ScriptSrc.AddNonce().AddHash("sha256", "AjPdJSbZmeWHnEc5ykvJFay8FTWeTeRbs9dutfZ0HqE=") + csp.ScriptSrc = csp.ScriptSrc.AddNonce(). + // SAML POST ACS + AddHash("sha256", "AjPdJSbZmeWHnEc5ykvJFay8FTWeTeRbs9dutfZ0HqE="). + // SAML POST SLO + AddHash("sha256", "4Su6mBWzEIFnH4pAGMOuaeBrstwJN4Z3pq/s1Kn4/KQ=") return &csp } @@ -215,14 +215,16 @@ func (l *Login) baseURL(ctx context.Context) string { type Caches struct { idpFormCallbacks cache.Cache[idpFormCallbackIndex, string, *idpFormCallback] + federatedLogouts cache.Cache[federatedlogout.Index, string, *federatedlogout.FederatedLogout] } -func startCaches(background context.Context, connectors connector.Connectors) (_ *Caches, err error) { +func startCaches(background context.Context, connectors connector.Connectors, federateLogoutCache cache.Cache[federatedlogout.Index, string, *federatedlogout.FederatedLogout]) (_ *Caches, err error) { caches := new(Caches) caches.idpFormCallbacks, err = connector.StartCache[idpFormCallbackIndex, string, *idpFormCallback](background, []idpFormCallbackIndex{idpFormCallbackIndexRequestID}, cache.PurposeIdPFormCallback, connectors.Config.IdPFormCallbacks, connectors) if err != nil { return nil, err } + caches.federatedLogouts = federateLogoutCache return caches, nil } diff --git a/internal/api/ui/login/router.go b/internal/api/ui/login/router.go index 6e346c9da0..459a0aab5d 100644 --- a/internal/api/ui/login/router.go +++ b/internal/api/ui/login/router.go @@ -14,6 +14,7 @@ const ( EndpointExternalLogin = "/login/externalidp" EndpointExternalLoginCallback = "/login/externalidp/callback" EndpointExternalLoginCallbackFormPost = "/login/externalidp/callback/form" + EndpointExternalLogout = "/logout/externalidp" EndpointSAMLACS = "/login/externalidp/saml/acs" EndpointJWTAuthorize = "/login/jwt/authorize" EndpointJWTCallback = "/login/jwt/callback" @@ -77,6 +78,7 @@ func CreateRouter(login *Login, interceptors ...mux.MiddlewareFunc) *mux.Router router.HandleFunc(EndpointExternalLogin, login.handleExternalLogin).Methods(http.MethodGet) router.HandleFunc(EndpointExternalLoginCallback, login.handleExternalLoginCallback).Methods(http.MethodGet) router.HandleFunc(EndpointExternalLoginCallbackFormPost, login.handleExternalLoginCallbackForm).Methods(http.MethodPost) + router.HandleFunc(EndpointExternalLogout, login.handleExternalLogout).Methods(http.MethodGet) router.HandleFunc(EndpointSAMLACS, login.handleExternalLoginCallback).Methods(http.MethodGet) router.HandleFunc(EndpointSAMLACS, login.handleExternalLoginCallbackForm).Methods(http.MethodPost) router.HandleFunc(EndpointJWTAuthorize, login.handleJWTRequest).Methods(http.MethodGet) diff --git a/internal/auth/repository/eventsourcing/eventstore/user.go b/internal/auth/repository/eventsourcing/eventstore/user.go index 61895c263d..6c5375a6b9 100644 --- a/internal/auth/repository/eventsourcing/eventstore/user.go +++ b/internal/auth/repository/eventsourcing/eventstore/user.go @@ -13,6 +13,7 @@ import ( "github.com/zitadel/zitadel/internal/query" "github.com/zitadel/zitadel/internal/repository/user" usr_view "github.com/zitadel/zitadel/internal/user/repository/view" + "github.com/zitadel/zitadel/internal/user/repository/view/model" "github.com/zitadel/zitadel/internal/zerrors" ) @@ -49,6 +50,10 @@ func (repo *UserRepo) UserAgentIDBySessionID(ctx context.Context, sessionID stri return repo.View.UserAgentIDBySessionID(ctx, sessionID, authz.GetInstance(ctx).InstanceID()) } +func (repo *UserRepo) UserSessionByID(ctx context.Context, sessionID string) (*model.UserSessionView, error) { + return repo.View.UserSessionByID(ctx, sessionID, authz.GetInstance(ctx).InstanceID()) +} + func (repo *UserRepo) ActiveUserSessionsBySessionID(ctx context.Context, sessionID string) (userAgentID string, signoutSessions []command.HumanSignOutSession, err error) { userAgentID, sessions, err := repo.View.ActiveUserSessionsBySessionID(ctx, sessionID, authz.GetInstance(ctx).InstanceID()) if err != nil { diff --git a/internal/auth/repository/eventsourcing/view/user_session.go b/internal/auth/repository/eventsourcing/view/user_session.go index a4618e11fb..777bc3213f 100644 --- a/internal/auth/repository/eventsourcing/view/user_session.go +++ b/internal/auth/repository/eventsourcing/view/user_session.go @@ -16,6 +16,10 @@ func (v *View) UserSessionByIDs(ctx context.Context, agentID, userID, instanceID return view.UserSessionByIDs(ctx, v.client, agentID, userID, instanceID) } +func (v *View) UserSessionByID(ctx context.Context, userSessionID, instanceID string) (*model.UserSessionView, error) { + return view.UserSessionByID(ctx, v.client, userSessionID, instanceID) +} + func (v *View) UserSessionsByAgentID(ctx context.Context, agentID, instanceID string) ([]*model.UserSessionView, error) { return view.UserSessionsByAgentID(ctx, v.client, agentID, instanceID) } diff --git a/internal/auth/repository/user.go b/internal/auth/repository/user.go index f09581b32e..b51ca27f24 100644 --- a/internal/auth/repository/user.go +++ b/internal/auth/repository/user.go @@ -4,10 +4,12 @@ import ( "context" "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/user/repository/view/model" ) type UserRepository interface { UserSessionsByAgentID(ctx context.Context, agentID string) (sessions []command.HumanSignOutSession, err error) UserAgentIDBySessionID(ctx context.Context, sessionID string) (string, error) + UserSessionByID(ctx context.Context, sessionID string) (*model.UserSessionView, error) ActiveUserSessionsBySessionID(ctx context.Context, sessionID string) (userAgentID string, sessions []command.HumanSignOutSession, err error) } diff --git a/internal/cache/cache.go b/internal/cache/cache.go index dc05208caa..233308a3cb 100644 --- a/internal/cache/cache.go +++ b/internal/cache/cache.go @@ -18,6 +18,7 @@ const ( PurposeMilestones PurposeOrganization PurposeIdPFormCallback + PurposeFederatedLogout ) // Cache stores objects with a value of type `V`. diff --git a/internal/cache/connector/connector.go b/internal/cache/connector/connector.go index 1a0534759a..532e795a81 100644 --- a/internal/cache/connector/connector.go +++ b/internal/cache/connector/connector.go @@ -23,6 +23,7 @@ type CachesConfig struct { Milestones *cache.Config Organization *cache.Config IdPFormCallbacks *cache.Config + FederatedLogouts *cache.Config } type Connectors struct { diff --git a/internal/cache/purpose_enumer.go b/internal/cache/purpose_enumer.go index a93a978efb..f721435593 100644 --- a/internal/cache/purpose_enumer.go +++ b/internal/cache/purpose_enumer.go @@ -7,11 +7,11 @@ import ( "strings" ) -const _PurposeName = "unspecifiedauthz_instancemilestonesorganizationid_p_form_callback" +const _PurposeName = "unspecifiedauthz_instancemilestonesorganizationid_p_form_callbackfederated_logout" -var _PurposeIndex = [...]uint8{0, 11, 25, 35, 47, 65} +var _PurposeIndex = [...]uint8{0, 11, 25, 35, 47, 65, 81} -const _PurposeLowerName = "unspecifiedauthz_instancemilestonesorganizationid_p_form_callback" +const _PurposeLowerName = "unspecifiedauthz_instancemilestonesorganizationid_p_form_callbackfederated_logout" func (i Purpose) String() string { if i < 0 || i >= Purpose(len(_PurposeIndex)-1) { @@ -29,9 +29,10 @@ func _PurposeNoOp() { _ = x[PurposeMilestones-(2)] _ = x[PurposeOrganization-(3)] _ = x[PurposeIdPFormCallback-(4)] + _ = x[PurposeFederatedLogout-(5)] } -var _PurposeValues = []Purpose{PurposeUnspecified, PurposeAuthzInstance, PurposeMilestones, PurposeOrganization, PurposeIdPFormCallback} +var _PurposeValues = []Purpose{PurposeUnspecified, PurposeAuthzInstance, PurposeMilestones, PurposeOrganization, PurposeIdPFormCallback, PurposeFederatedLogout} var _PurposeNameToValueMap = map[string]Purpose{ _PurposeName[0:11]: PurposeUnspecified, @@ -44,6 +45,8 @@ var _PurposeNameToValueMap = map[string]Purpose{ _PurposeLowerName[35:47]: PurposeOrganization, _PurposeName[47:65]: PurposeIdPFormCallback, _PurposeLowerName[47:65]: PurposeIdPFormCallback, + _PurposeName[65:81]: PurposeFederatedLogout, + _PurposeLowerName[65:81]: PurposeFederatedLogout, } var _PurposeNames = []string{ @@ -52,6 +55,7 @@ var _PurposeNames = []string{ _PurposeName[25:35], _PurposeName[35:47], _PurposeName[47:65], + _PurposeName[65:81], } // PurposeString retrieves an enum value from the enum constants string name. diff --git a/internal/command/idp.go b/internal/command/idp.go index 821a577900..06d8f473c0 100644 --- a/internal/command/idp.go +++ b/internal/command/idp.go @@ -122,6 +122,7 @@ type SAMLProvider struct { WithSignedRequest bool NameIDFormat *domain.SAMLNameIDFormat TransientMappingAttributeName string + FederatedLogoutEnabled bool IDPOptions idp.Options } diff --git a/internal/command/idp_intent_test.go b/internal/command/idp_intent_test.go index 1be3971e87..6cf835f521 100644 --- a/internal/command/idp_intent_test.go +++ b/internal/command/idp_intent_test.go @@ -743,6 +743,7 @@ func TestCommands_AuthFromProvider_SAML(t *testing.T) { false, gu.Ptr(domain.SAMLNameIDFormatUnspecified), "", + false, rep_idp.Options{}, )), ), @@ -763,6 +764,7 @@ func TestCommands_AuthFromProvider_SAML(t *testing.T) { false, gu.Ptr(domain.SAMLNameIDFormatUnspecified), "", + false, rep_idp.Options{}, )), ), diff --git a/internal/command/idp_model.go b/internal/command/idp_model.go index 5257d38bf4..188d45e6ec 100644 --- a/internal/command/idp_model.go +++ b/internal/command/idp_model.go @@ -1760,6 +1760,7 @@ type SAMLIDPWriteModel struct { WithSignedRequest bool NameIDFormat *domain.SAMLNameIDFormat TransientMappingAttributeName string + FederatedLogoutEnabled bool idp.Options State domain.IDPState @@ -1788,6 +1789,7 @@ func (wm *SAMLIDPWriteModel) reduceAddedEvent(e *idp.SAMLIDPAddedEvent) { wm.WithSignedRequest = e.WithSignedRequest wm.NameIDFormat = e.NameIDFormat wm.TransientMappingAttributeName = e.TransientMappingAttributeName + wm.FederatedLogoutEnabled = e.FederatedLogoutEnabled wm.Options = e.Options wm.State = domain.IDPStateActive } @@ -1817,6 +1819,9 @@ func (wm *SAMLIDPWriteModel) reduceChangedEvent(e *idp.SAMLIDPChangedEvent) { if e.TransientMappingAttributeName != nil { wm.TransientMappingAttributeName = *e.TransientMappingAttributeName } + if e.FederatedLogoutEnabled != nil { + wm.FederatedLogoutEnabled = *e.FederatedLogoutEnabled + } wm.Options.ReduceChanges(e.OptionChanges) } @@ -1830,6 +1835,7 @@ func (wm *SAMLIDPWriteModel) NewChanges( withSignedRequest bool, nameIDFormat *domain.SAMLNameIDFormat, transientMappingAttributeName string, + federatedLogoutEnabled bool, options idp.Options, ) ([]idp.SAMLIDPChanges, error) { changes := make([]idp.SAMLIDPChanges, 0) @@ -1861,6 +1867,9 @@ func (wm *SAMLIDPWriteModel) NewChanges( if wm.TransientMappingAttributeName != transientMappingAttributeName { changes = append(changes, idp.ChangeSAMLTransientMappingAttributeName(transientMappingAttributeName)) } + if wm.FederatedLogoutEnabled != federatedLogoutEnabled { + changes = append(changes, idp.ChangeSAMLFederatedLogoutEnabled(federatedLogoutEnabled)) + } opts := wm.Options.Changes(options) if !opts.IsZero() { changes = append(changes, idp.ChangeSAMLOptions(opts)) @@ -1899,6 +1908,7 @@ func (wm *SAMLIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.Encryp if wm.TransientMappingAttributeName != "" { opts = append(opts, saml2.WithTransientMappingAttributeName(wm.TransientMappingAttributeName)) } + // TODO: ? if wm.FederatedLogoutEnabled opts = append(opts, saml2.WithCustomRequestTracker( requesttracker.New( addRequest, diff --git a/internal/command/instance_idp.go b/internal/command/instance_idp.go index 348f55cd9c..90efb3edd4 100644 --- a/internal/command/instance_idp.go +++ b/internal/command/instance_idp.go @@ -1795,6 +1795,7 @@ func (c *Commands) prepareAddInstanceSAMLProvider(a *instance.Aggregate, writeMo provider.WithSignedRequest, provider.NameIDFormat, provider.TransientMappingAttributeName, + provider.FederatedLogoutEnabled, provider.IDPOptions, ), }, nil @@ -1848,6 +1849,7 @@ func (c *Commands) prepareUpdateInstanceSAMLProvider(a *instance.Aggregate, writ provider.WithSignedRequest, provider.NameIDFormat, provider.TransientMappingAttributeName, + provider.FederatedLogoutEnabled, provider.IDPOptions, ) if err != nil || event == nil { @@ -1893,6 +1895,7 @@ func (c *Commands) prepareRegenerateInstanceSAMLProviderCertificate(a *instance. writeModel.WithSignedRequest, writeModel.NameIDFormat, writeModel.TransientMappingAttributeName, + writeModel.FederatedLogoutEnabled, writeModel.Options, ) if err != nil || event == nil { diff --git a/internal/command/instance_idp_model.go b/internal/command/instance_idp_model.go index d94c19d318..03d9cd9c36 100644 --- a/internal/command/instance_idp_model.go +++ b/internal/command/instance_idp_model.go @@ -923,6 +923,7 @@ func (wm *InstanceSAMLIDPWriteModel) NewChangedEvent( withSignedRequest bool, nameIDFormat *domain.SAMLNameIDFormat, transientMappingAttributeName string, + federatedLogoutEnabled bool, options idp.Options, ) (*instance.SAMLIDPChangedEvent, error) { changes, err := wm.SAMLIDPWriteModel.NewChanges( @@ -935,6 +936,7 @@ func (wm *InstanceSAMLIDPWriteModel) NewChangedEvent( withSignedRequest, nameIDFormat, transientMappingAttributeName, + federatedLogoutEnabled, options, ) if err != nil || len(changes) == 0 { diff --git a/internal/command/instance_idp_test.go b/internal/command/instance_idp_test.go index 7002598af5..4805d075dd 100644 --- a/internal/command/instance_idp_test.go +++ b/internal/command/instance_idp_test.go @@ -5437,6 +5437,7 @@ func TestCommandSide_AddInstanceSAMLIDP(t *testing.T) { false, nil, "", + false, idp.Options{}, ), ), @@ -5478,6 +5479,7 @@ func TestCommandSide_AddInstanceSAMLIDP(t *testing.T) { true, gu.Ptr(domain.SAMLNameIDFormatTransient), "customAttribute", + true, idp.Options{ IsCreationAllowed: true, IsLinkingAllowed: true, @@ -5500,6 +5502,7 @@ func TestCommandSide_AddInstanceSAMLIDP(t *testing.T) { WithSignedRequest: true, NameIDFormat: gu.Ptr(domain.SAMLNameIDFormatTransient), TransientMappingAttributeName: "customAttribute", + FederatedLogoutEnabled: true, IDPOptions: idp.Options{ IsCreationAllowed: true, IsLinkingAllowed: true, @@ -5665,6 +5668,7 @@ func TestCommandSide_UpdateInstanceGenericSAMLIDP(t *testing.T) { false, nil, "", + false, idp.Options{}, )), ), @@ -5703,6 +5707,7 @@ func TestCommandSide_UpdateInstanceGenericSAMLIDP(t *testing.T) { false, gu.Ptr(domain.SAMLNameIDFormatUnspecified), "", + false, idp.Options{}, )), ), @@ -5718,6 +5723,7 @@ func TestCommandSide_UpdateInstanceGenericSAMLIDP(t *testing.T) { idp.ChangeSAMLWithSignedRequest(true), idp.ChangeSAMLNameIDFormat(gu.Ptr(domain.SAMLNameIDFormatTransient)), idp.ChangeSAMLTransientMappingAttributeName("customAttribute"), + idp.ChangeSAMLFederatedLogoutEnabled(true), idp.ChangeSAMLOptions(idp.OptionChanges{ IsCreationAllowed: &t, IsLinkingAllowed: &t, @@ -5742,6 +5748,7 @@ func TestCommandSide_UpdateInstanceGenericSAMLIDP(t *testing.T) { WithSignedRequest: true, NameIDFormat: gu.Ptr(domain.SAMLNameIDFormatTransient), TransientMappingAttributeName: "customAttribute", + FederatedLogoutEnabled: true, IDPOptions: idp.Options{ IsCreationAllowed: true, IsLinkingAllowed: true, @@ -5845,6 +5852,7 @@ func TestCommandSide_RegenerateInstanceSAMLProviderCertificate(t *testing.T) { false, gu.Ptr(domain.SAMLNameIDFormatUnspecified), "", + false, idp.Options{}, )), ), diff --git a/internal/command/org_idp.go b/internal/command/org_idp.go index b72fc1fd77..6cae78acc7 100644 --- a/internal/command/org_idp.go +++ b/internal/command/org_idp.go @@ -1768,6 +1768,7 @@ func (c *Commands) prepareAddOrgSAMLProvider(a *org.Aggregate, writeModel *OrgSA provider.WithSignedRequest, provider.NameIDFormat, provider.TransientMappingAttributeName, + provider.FederatedLogoutEnabled, provider.IDPOptions, ), }, nil @@ -1821,6 +1822,7 @@ func (c *Commands) prepareUpdateOrgSAMLProvider(a *org.Aggregate, writeModel *Or provider.WithSignedRequest, provider.NameIDFormat, provider.TransientMappingAttributeName, + provider.FederatedLogoutEnabled, provider.IDPOptions, ) if err != nil || event == nil { @@ -1866,6 +1868,7 @@ func (c *Commands) prepareRegenerateOrgSAMLProviderCertificate(a *org.Aggregate, writeModel.WithSignedRequest, writeModel.NameIDFormat, writeModel.TransientMappingAttributeName, + writeModel.FederatedLogoutEnabled, writeModel.Options, ) if err != nil || event == nil { diff --git a/internal/command/org_idp_model.go b/internal/command/org_idp_model.go index 3baea11495..fdafb7f087 100644 --- a/internal/command/org_idp_model.go +++ b/internal/command/org_idp_model.go @@ -935,6 +935,7 @@ func (wm *OrgSAMLIDPWriteModel) NewChangedEvent( withSignedRequest bool, nameIDFormat *domain.SAMLNameIDFormat, transientMappingAttributeName string, + federatedLogoutEnabled bool, options idp.Options, ) (*org.SAMLIDPChangedEvent, error) { changes, err := wm.SAMLIDPWriteModel.NewChanges( @@ -947,6 +948,7 @@ func (wm *OrgSAMLIDPWriteModel) NewChangedEvent( withSignedRequest, nameIDFormat, transientMappingAttributeName, + federatedLogoutEnabled, options, ) if err != nil || len(changes) == 0 { diff --git a/internal/command/org_idp_test.go b/internal/command/org_idp_test.go index 9959ced97d..54f508da30 100644 --- a/internal/command/org_idp_test.go +++ b/internal/command/org_idp_test.go @@ -5519,6 +5519,7 @@ func TestCommandSide_AddOrgSAMLIDP(t *testing.T) { false, nil, "", + false, idp.Options{}, ), ), @@ -5560,6 +5561,7 @@ func TestCommandSide_AddOrgSAMLIDP(t *testing.T) { true, gu.Ptr(domain.SAMLNameIDFormatTransient), "customAttribute", + true, idp.Options{ IsCreationAllowed: true, IsLinkingAllowed: true, @@ -5583,6 +5585,7 @@ func TestCommandSide_AddOrgSAMLIDP(t *testing.T) { WithSignedRequest: true, NameIDFormat: gu.Ptr(domain.SAMLNameIDFormatTransient), TransientMappingAttributeName: "customAttribute", + FederatedLogoutEnabled: true, IDPOptions: idp.Options{ IsCreationAllowed: true, IsLinkingAllowed: true, @@ -5756,6 +5759,7 @@ func TestCommandSide_UpdateOrgSAMLIDP(t *testing.T) { false, nil, "", + false, idp.Options{}, )), ), @@ -5795,6 +5799,7 @@ func TestCommandSide_UpdateOrgSAMLIDP(t *testing.T) { false, gu.Ptr(domain.SAMLNameIDFormatUnspecified), "", + false, idp.Options{}, )), ), @@ -5810,6 +5815,7 @@ func TestCommandSide_UpdateOrgSAMLIDP(t *testing.T) { idp.ChangeSAMLWithSignedRequest(true), idp.ChangeSAMLNameIDFormat(gu.Ptr(domain.SAMLNameIDFormatTransient)), idp.ChangeSAMLTransientMappingAttributeName("customAttribute"), + idp.ChangeSAMLFederatedLogoutEnabled(true), idp.ChangeSAMLOptions(idp.OptionChanges{ IsCreationAllowed: &t, IsLinkingAllowed: &t, @@ -5835,6 +5841,7 @@ func TestCommandSide_UpdateOrgSAMLIDP(t *testing.T) { WithSignedRequest: true, NameIDFormat: gu.Ptr(domain.SAMLNameIDFormatTransient), TransientMappingAttributeName: "customAttribute", + FederatedLogoutEnabled: true, IDPOptions: idp.Options{ IsCreationAllowed: true, IsLinkingAllowed: true, @@ -5943,6 +5950,7 @@ func TestCommandSide_RegenerateOrgSAMLProviderCertificate(t *testing.T) { false, gu.Ptr(domain.SAMLNameIDFormatUnspecified), "", + false, idp.Options{}, )), ), diff --git a/internal/domain/federatedlogout/logout.go b/internal/domain/federatedlogout/logout.go new file mode 100644 index 0000000000..ca208a129a --- /dev/null +++ b/internal/domain/federatedlogout/logout.go @@ -0,0 +1,37 @@ +package federatedlogout + +type Index int + +const ( + IndexUnspecified Index = iota + IndexRequestID +) + +type FederatedLogout struct { + InstanceID string + FingerPrintID string + SessionID string + IDPID string + UserID string + PostLogoutRedirectURI string + State State +} + +// Keys implements cache.Entry +func (c *FederatedLogout) Keys(i Index) []string { + if i == IndexRequestID { + return []string{Key(c.InstanceID, c.SessionID)} + } + return nil +} + +func Key(instanceID, sessionID string) string { + return instanceID + "-" + sessionID +} + +type State int + +const ( + StateCreated State = iota + StateRedirected +) diff --git a/internal/query/idp_template.go b/internal/query/idp_template.go index f51e9a11a7..c1c47e73bc 100644 --- a/internal/query/idp_template.go +++ b/internal/query/idp_template.go @@ -165,6 +165,7 @@ type SAMLIDPTemplate struct { WithSignedRequest bool NameIDFormat sql.Null[domain.SAMLNameIDFormat] TransientMappingAttributeName string + FederatedLogoutEnabled bool } var ( @@ -724,6 +725,10 @@ var ( name: projection.SAMLTransientMappingAttributeName, table: samlIdpTemplateTable, } + SAMLFederatedLogoutEnabledCol = Column{ + name: projection.SAMLFederatedLogoutEnabled, + table: samlIdpTemplateTable, + } ) // IDPTemplateByID searches for the requested id with permission check if necessary @@ -948,6 +953,7 @@ func prepareIDPTemplateByIDQuery() (sq.SelectBuilder, func(*sql.Row) (*IDPTempla SAMLWithSignedRequestCol.identifier(), SAMLNameIDFormatCol.identifier(), SAMLTransientMappingAttributeNameCol.identifier(), + SAMLFederatedLogoutEnabledCol.identifier(), // ldap LDAPIDCol.identifier(), LDAPServersCol.identifier(), @@ -1067,6 +1073,7 @@ func prepareIDPTemplateByIDQuery() (sq.SelectBuilder, func(*sql.Row) (*IDPTempla samlWithSignedRequest := sql.NullBool{} samlNameIDFormat := sql.Null[domain.SAMLNameIDFormat]{} samlTransientMappingAttributeName := sql.NullString{} + samlFederatedLogoutEnabled := sql.NullBool{} ldapID := sql.NullString{} ldapServers := database.TextArray[string]{} @@ -1184,6 +1191,7 @@ func prepareIDPTemplateByIDQuery() (sq.SelectBuilder, func(*sql.Row) (*IDPTempla &samlWithSignedRequest, &samlNameIDFormat, &samlTransientMappingAttributeName, + &samlFederatedLogoutEnabled, // ldap &ldapID, &ldapServers, @@ -1323,6 +1331,7 @@ func prepareIDPTemplateByIDQuery() (sq.SelectBuilder, func(*sql.Row) (*IDPTempla WithSignedRequest: samlWithSignedRequest.Bool, NameIDFormat: samlNameIDFormat, TransientMappingAttributeName: samlTransientMappingAttributeName.String, + FederatedLogoutEnabled: samlFederatedLogoutEnabled.Bool, } } if ldapID.Valid { @@ -1456,6 +1465,7 @@ func prepareIDPTemplatesQuery() (sq.SelectBuilder, func(*sql.Rows) (*IDPTemplate SAMLWithSignedRequestCol.identifier(), SAMLNameIDFormatCol.identifier(), SAMLTransientMappingAttributeNameCol.identifier(), + SAMLFederatedLogoutEnabledCol.identifier(), // ldap LDAPIDCol.identifier(), LDAPServersCol.identifier(), @@ -1580,6 +1590,7 @@ func prepareIDPTemplatesQuery() (sq.SelectBuilder, func(*sql.Rows) (*IDPTemplate samlWithSignedRequest := sql.NullBool{} samlNameIDFormat := sql.Null[domain.SAMLNameIDFormat]{} samlTransientMappingAttributeName := sql.NullString{} + samlFederatedLogoutEnabled := sql.NullBool{} ldapID := sql.NullString{} ldapServers := database.TextArray[string]{} @@ -1697,6 +1708,7 @@ func prepareIDPTemplatesQuery() (sq.SelectBuilder, func(*sql.Rows) (*IDPTemplate &samlWithSignedRequest, &samlNameIDFormat, &samlTransientMappingAttributeName, + &samlFederatedLogoutEnabled, // ldap &ldapID, &ldapServers, @@ -1835,6 +1847,7 @@ func prepareIDPTemplatesQuery() (sq.SelectBuilder, func(*sql.Rows) (*IDPTemplate WithSignedRequest: samlWithSignedRequest.Bool, NameIDFormat: samlNameIDFormat, TransientMappingAttributeName: samlTransientMappingAttributeName.String, + FederatedLogoutEnabled: samlFederatedLogoutEnabled.Bool, } } if ldapID.Valid { diff --git a/internal/query/idp_template_test.go b/internal/query/idp_template_test.go index 702e5d0ced..aed243c61e 100644 --- a/internal/query/idp_template_test.go +++ b/internal/query/idp_template_test.go @@ -99,6 +99,7 @@ var ( ` projections.idp_templates6_saml.with_signed_request,` + ` projections.idp_templates6_saml.name_id_format,` + ` projections.idp_templates6_saml.transient_mapping_attribute_name,` + + ` projections.idp_templates6_saml.federated_logout_enabled,` + // ldap ` projections.idp_templates6_ldap2.idp_id,` + ` projections.idp_templates6_ldap2.servers,` + @@ -228,6 +229,7 @@ var ( "with_signed_request", "name_id_format", "transient_mapping_attribute_name", + "federated_logout_enabled", // ldap config "idp_id", "servers", @@ -344,6 +346,7 @@ var ( ` projections.idp_templates6_saml.with_signed_request,` + ` projections.idp_templates6_saml.name_id_format,` + ` projections.idp_templates6_saml.transient_mapping_attribute_name,` + + ` projections.idp_templates6_saml.federated_logout_enabled,` + // ldap ` projections.idp_templates6_ldap2.idp_id,` + ` projections.idp_templates6_ldap2.servers,` + @@ -474,6 +477,7 @@ var ( "with_signed_request", "name_id_format", "transient_mapping_attribute_name", + "federated_logout_enabled", // ldap config "idp_id", "servers", @@ -630,6 +634,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + nil, // ldap config nil, nil, @@ -784,6 +789,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + nil, // ldap config nil, nil, @@ -936,6 +942,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + nil, // ldap config nil, nil, @@ -1086,6 +1093,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + nil, // ldap config nil, nil, @@ -1235,6 +1243,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + nil, // ldap config nil, nil, @@ -1384,6 +1393,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + nil, // ldap config nil, nil, @@ -1534,6 +1544,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + nil, // ldap config nil, nil, @@ -1683,6 +1694,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { false, domain.SAMLNameIDFormatTransient, "customAttribute", + true, // ldap config nil, nil, @@ -1742,6 +1754,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { WithSignedRequest: false, NameIDFormat: sql.Null[domain.SAMLNameIDFormat]{V: domain.SAMLNameIDFormatTransient, Valid: true}, TransientMappingAttributeName: "customAttribute", + FederatedLogoutEnabled: true, }, }, }, @@ -1836,6 +1849,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + nil, // ldap config "idp-id", database.TextArray[string]{"server"}, @@ -2006,6 +2020,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + nil, // ldap config nil, nil, @@ -2157,6 +2172,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + nil, // ldap config nil, nil, @@ -2336,6 +2352,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + nil, // ldap config "idp-id", database.TextArray[string]{"server"}, @@ -2515,6 +2532,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + nil, // ldap config nil, nil, @@ -2667,6 +2685,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + nil, // ldap config "idp-id-ldap", database.TextArray[string]{"server"}, @@ -2784,6 +2803,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { false, domain.SAMLNameIDFormatTransient, "customAttribute", + true, // ldap config nil, nil, @@ -2901,6 +2921,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + nil, // ldap config nil, nil, @@ -3018,6 +3039,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + nil, // ldap config nil, nil, @@ -3135,6 +3157,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + nil, // ldap config nil, nil, @@ -3252,6 +3275,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + nil, // ldap config nil, nil, @@ -3360,6 +3384,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { WithSignedRequest: false, NameIDFormat: sql.Null[domain.SAMLNameIDFormat]{V: domain.SAMLNameIDFormatTransient, Valid: true}, TransientMappingAttributeName: "customAttribute", + FederatedLogoutEnabled: true, }, }, { diff --git a/internal/query/projection/idp_template.go b/internal/query/projection/idp_template.go index 55c74b851c..11eeb8c613 100644 --- a/internal/query/projection/idp_template.go +++ b/internal/query/projection/idp_template.go @@ -173,6 +173,7 @@ const ( SAMLWithSignedRequestCol = "with_signed_request" SAMLNameIDFormatCol = "name_id_format" SAMLTransientMappingAttributeName = "transient_mapping_attribute_name" + SAMLFederatedLogoutEnabled = "federated_logout_enabled" ) type idpTemplateProjection struct{} @@ -377,6 +378,7 @@ func (*idpTemplateProjection) Init() *old_handler.Check { handler.NewColumn(SAMLWithSignedRequestCol, handler.ColumnTypeBool, handler.Nullable()), handler.NewColumn(SAMLNameIDFormatCol, handler.ColumnTypeEnum, handler.Nullable()), handler.NewColumn(SAMLTransientMappingAttributeName, handler.ColumnTypeText, handler.Nullable()), + handler.NewColumn(SAMLFederatedLogoutEnabled, handler.ColumnTypeBool, handler.Default(false)), }, handler.NewPrimaryKey(SAMLInstanceIDCol, SAMLIDCol), IDPTemplateSAMLSuffix, @@ -1990,6 +1992,7 @@ func (p *idpTemplateProjection) reduceSAMLIDPAdded(event eventstore.Event) (*han handler.NewCol(SAMLBindingCol, idpEvent.Binding), handler.NewCol(SAMLWithSignedRequestCol, idpEvent.WithSignedRequest), handler.NewCol(SAMLTransientMappingAttributeName, idpEvent.TransientMappingAttributeName), + handler.NewCol(SAMLFederatedLogoutEnabled, idpEvent.FederatedLogoutEnabled), } if idpEvent.NameIDFormat != nil { columns = append(columns, handler.NewCol(SAMLNameIDFormatCol, *idpEvent.NameIDFormat)) @@ -2525,5 +2528,8 @@ func reduceSAMLIDPChangedColumns(idpEvent idp.SAMLIDPChangedEvent) []handler.Col if idpEvent.TransientMappingAttributeName != nil { SAMLCols = append(SAMLCols, handler.NewCol(SAMLTransientMappingAttributeName, *idpEvent.TransientMappingAttributeName)) } + if idpEvent.FederatedLogoutEnabled != nil { + SAMLCols = append(SAMLCols, handler.NewCol(SAMLFederatedLogoutEnabled, *idpEvent.FederatedLogoutEnabled)) + } return SAMLCols } diff --git a/internal/query/projection/idp_template_test.go b/internal/query/projection/idp_template_test.go index cebf3f8791..6ba6dabd47 100644 --- a/internal/query/projection/idp_template_test.go +++ b/internal/query/projection/idp_template_test.go @@ -2793,7 +2793,8 @@ func TestIDPTemplateProjection_reducesSAML(t *testing.T) { "isLinkingAllowed": true, "isAutoCreation": true, "isAutoUpdate": true, - "autoLinkingOption": 1 + "autoLinkingOption": 1, + "federatedLogoutEnabled": true }`), ), instance.SAMLIDPAddedEventMapper), }, @@ -2824,7 +2825,7 @@ func TestIDPTemplateProjection_reducesSAML(t *testing.T) { }, }, { - expectedStmt: "INSERT INTO projections.idp_templates6_saml (idp_id, instance_id, metadata, key, certificate, binding, with_signed_request, transient_mapping_attribute_name, name_id_format) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", + expectedStmt: "INSERT INTO projections.idp_templates6_saml (idp_id, instance_id, metadata, key, certificate, binding, with_signed_request, transient_mapping_attribute_name, federated_logout_enabled, name_id_format) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)", expectedArgs: []interface{}{ "idp-id", "instance-id", @@ -2834,6 +2835,7 @@ func TestIDPTemplateProjection_reducesSAML(t *testing.T) { "binding", true, "customAttribute", + true, domain.SAMLNameIDFormatTransient, }, }, @@ -2865,7 +2867,8 @@ func TestIDPTemplateProjection_reducesSAML(t *testing.T) { "isLinkingAllowed": true, "isAutoCreation": true, "isAutoUpdate": true, - "autoLinkingOption": 1 + "autoLinkingOption": 1, + "federatedLogoutEnabled": true }`), ), org.SAMLIDPAddedEventMapper), }, @@ -2896,7 +2899,7 @@ func TestIDPTemplateProjection_reducesSAML(t *testing.T) { }, }, { - expectedStmt: "INSERT INTO projections.idp_templates6_saml (idp_id, instance_id, metadata, key, certificate, binding, with_signed_request, transient_mapping_attribute_name, name_id_format) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", + expectedStmt: "INSERT INTO projections.idp_templates6_saml (idp_id, instance_id, metadata, key, certificate, binding, with_signed_request, transient_mapping_attribute_name, federated_logout_enabled, name_id_format) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)", expectedArgs: []interface{}{ "idp-id", "instance-id", @@ -2906,6 +2909,7 @@ func TestIDPTemplateProjection_reducesSAML(t *testing.T) { "binding", true, "customAttribute", + true, domain.SAMLNameIDFormatTransient, }, }, @@ -2976,7 +2980,8 @@ func TestIDPTemplateProjection_reducesSAML(t *testing.T) { "isLinkingAllowed": true, "isAutoCreation": true, "isAutoUpdate": true, - "autoLinkingOption": 1 + "autoLinkingOption": 1, + "federatedLogoutEnabled": true }`), ), instance.SAMLIDPChangedEventMapper), }, @@ -3002,13 +3007,14 @@ func TestIDPTemplateProjection_reducesSAML(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.idp_templates6_saml SET (metadata, key, certificate, binding, with_signed_request) = ($1, $2, $3, $4, $5) WHERE (idp_id = $6) AND (instance_id = $7)", + expectedStmt: "UPDATE projections.idp_templates6_saml SET (metadata, key, certificate, binding, with_signed_request, federated_logout_enabled) = ($1, $2, $3, $4, $5, $6) WHERE (idp_id = $7) AND (instance_id = $8)", expectedArgs: []interface{}{ []byte("metadata"), anyArg{}, anyArg{}, "binding", true, + true, "idp-id", "instance-id", }, diff --git a/internal/repository/idp/saml.go b/internal/repository/idp/saml.go index da6a52b1be..209d8371b3 100644 --- a/internal/repository/idp/saml.go +++ b/internal/repository/idp/saml.go @@ -19,6 +19,7 @@ type SAMLIDPAddedEvent struct { WithSignedRequest bool `json:"withSignedRequest,omitempty"` NameIDFormat *domain.SAMLNameIDFormat `json:"nameIDFormat,omitempty"` TransientMappingAttributeName string `json:"transientMappingAttributeName,omitempty"` + FederatedLogoutEnabled bool `json:"federatedLogoutEnabled,omitempty"` Options } @@ -33,6 +34,7 @@ func NewSAMLIDPAddedEvent( withSignedRequest bool, nameIDFormat *domain.SAMLNameIDFormat, transientMappingAttributeName string, + federatedLogoutEnabled bool, options Options, ) *SAMLIDPAddedEvent { return &SAMLIDPAddedEvent{ @@ -46,6 +48,7 @@ func NewSAMLIDPAddedEvent( WithSignedRequest: withSignedRequest, NameIDFormat: nameIDFormat, TransientMappingAttributeName: transientMappingAttributeName, + FederatedLogoutEnabled: federatedLogoutEnabled, Options: options, } } @@ -83,6 +86,7 @@ type SAMLIDPChangedEvent struct { WithSignedRequest *bool `json:"withSignedRequest,omitempty"` NameIDFormat *domain.SAMLNameIDFormat `json:"nameIDFormat,omitempty"` TransientMappingAttributeName *string `json:"transientMappingAttributeName,omitempty"` + FederatedLogoutEnabled *bool `json:"federatedLogoutEnabled,omitempty"` OptionChanges } @@ -154,6 +158,12 @@ func ChangeSAMLTransientMappingAttributeName(name string) func(*SAMLIDPChangedEv } } +func ChangeSAMLFederatedLogoutEnabled(federatedLogoutEnabled bool) func(*SAMLIDPChangedEvent) { + return func(e *SAMLIDPChangedEvent) { + e.FederatedLogoutEnabled = &federatedLogoutEnabled + } +} + func ChangeSAMLOptions(options OptionChanges) func(*SAMLIDPChangedEvent) { return func(e *SAMLIDPChangedEvent) { e.OptionChanges = options diff --git a/internal/repository/instance/idp.go b/internal/repository/instance/idp.go index 6ab60c0dd5..f0f324cb7d 100644 --- a/internal/repository/instance/idp.go +++ b/internal/repository/instance/idp.go @@ -1024,6 +1024,7 @@ func NewSAMLIDPAddedEvent( withSignedRequest bool, nameIDFormat *domain.SAMLNameIDFormat, transientMappingAttributeName string, + federatedLogoutEnabled bool, options idp.Options, ) *SAMLIDPAddedEvent { return &SAMLIDPAddedEvent{ @@ -1042,6 +1043,7 @@ func NewSAMLIDPAddedEvent( withSignedRequest, nameIDFormat, transientMappingAttributeName, + federatedLogoutEnabled, options, ), } diff --git a/internal/repository/org/idp.go b/internal/repository/org/idp.go index 0070f71a95..5f061370f1 100644 --- a/internal/repository/org/idp.go +++ b/internal/repository/org/idp.go @@ -1025,6 +1025,7 @@ func NewSAMLIDPAddedEvent( withSignedRequest bool, nameIDFormat *domain.SAMLNameIDFormat, transientMappingAttributeName string, + federatedLogoutEnabled bool, options idp.Options, ) *SAMLIDPAddedEvent { @@ -1044,6 +1045,7 @@ func NewSAMLIDPAddedEvent( withSignedRequest, nameIDFormat, transientMappingAttributeName, + federatedLogoutEnabled, options, ), } diff --git a/internal/user/repository/view/user_session.sql b/internal/user/repository/view/user_session.sql new file mode 100644 index 0000000000..3e2a97206b --- /dev/null +++ b/internal/user/repository/view/user_session.sql @@ -0,0 +1,29 @@ +SELECT s.creation_date, + s.change_date, + s.resource_owner, + s.state, + s.user_agent_id, + s.user_id, + u.username, + l.login_name, + h.display_name, + h.avatar_key, + s.selected_idp_config_id, + s.password_verification, + s.passwordless_verification, + s.external_login_verification, + s.second_factor_verification, + s.second_factor_verification_type, + s.multi_factor_verification, + s.multi_factor_verification_type, + s.sequence, + s.instance_id, + s.id +FROM auth.user_sessions s + LEFT JOIN projections.users14 u ON s.user_id = u.id AND s.instance_id = u.instance_id + LEFT JOIN projections.users14_humans h ON s.user_id = h.user_id AND s.instance_id = h.instance_id + LEFT JOIN projections.login_names3 l ON s.user_id = l.user_id AND s.instance_id = l.instance_id AND l.is_primary = true +WHERE (s.id = $1) + AND (s.instance_id = $2) +LIMIT 1 +; \ No newline at end of file diff --git a/internal/user/repository/view/user_session_view.go b/internal/user/repository/view/user_session_view.go index b3d155f1ec..dec22a181d 100644 --- a/internal/user/repository/view/user_session_view.go +++ b/internal/user/repository/view/user_session_view.go @@ -12,6 +12,9 @@ import ( ) //go:embed user_session_by_id.sql +var userSessionByIDsQuery string + +//go:embed user_session.sql var userSessionByIDQuery string //go:embed user_sessions_by_user_agent.sql @@ -30,7 +33,7 @@ func UserSessionByIDs(ctx context.Context, db *database.DB, agentID, userID, ins userSession, err = scanUserSession(row) return err }, - userSessionByIDQuery, + userSessionByIDsQuery, agentID, userID, instanceID, @@ -38,6 +41,20 @@ func UserSessionByIDs(ctx context.Context, db *database.DB, agentID, userID, ins return userSession, err } +func UserSessionByID(ctx context.Context, db *database.DB, userSessionID, instanceID string) (userSession *model.UserSessionView, err error) { + err = db.QueryRowContext( + ctx, + func(row *sql.Row) error { + userSession, err = scanUserSession(row) + return err + }, + userSessionByIDQuery, + userSessionID, + instanceID, + ) + return userSession, err +} + func UserSessionsByAgentID(ctx context.Context, db *database.DB, agentID, instanceID string) (userSessions []*model.UserSessionView, err error) { err = db.QueryContext( ctx, diff --git a/proto/zitadel/admin.proto b/proto/zitadel/admin.proto index 9033fd8668..1e7f3b7407 100644 --- a/proto/zitadel/admin.proto +++ b/proto/zitadel/admin.proto @@ -7035,6 +7035,9 @@ message AddSAMLProviderRequest { // Optionally specify the name of the attribute, which will be used to map the user // in case the nameid-format returned is `urn:oasis:names:tc:SAML:2.0:nameid-format:transient`. optional string transient_mapping_attribute_name = 8; + // Optionally enable federated logout. If enabled, ZITADEL will send a logout request to the identity provider, + // if the user terminates the session in ZITADEL. Be sure to provide a SLO endpoint as part of the metadata. + optional bool federated_logout_enabled = 9; } message AddSAMLProviderResponse { @@ -7069,6 +7072,9 @@ message UpdateSAMLProviderRequest { // Optionally specify the name of the attribute, which will be used to map the user // in case the nameid-format returned is `urn:oasis:names:tc:SAML:2.0:nameid-format:transient`. optional string transient_mapping_attribute_name = 9; + // Optionally enable federated logout. If enabled, ZITADEL will send a logout request to the identity provider, + // if the user terminates the session in ZITADEL. Be sure to provide a SLO endpoint as part of the metadata. + optional bool federated_logout_enabled = 10; } message UpdateSAMLProviderResponse { diff --git a/proto/zitadel/idp.proto b/proto/zitadel/idp.proto index 82e32aa873..de497d8c93 100644 --- a/proto/zitadel/idp.proto +++ b/proto/zitadel/idp.proto @@ -479,6 +479,9 @@ message SAMLConfig { // Optional name of the attribute, which will be used to map the user // in case the nameid-format returned is `urn:oasis:names:tc:SAML:2.0:nameid-format:transient`. optional string transient_mapping_attribute_name = 5; + // Boolean weather federated logout is enabled. If enabled, ZITADEL will send a logout request to the identity provider, + // if the user terminates the session in ZITADEL. Be sure to provide a SLO endpoint as part of the metadata. + optional bool federated_logout_enabled = 6; } message AzureADConfig { diff --git a/proto/zitadel/idp/v2/idp.proto b/proto/zitadel/idp/v2/idp.proto index 0c95b742f1..663581a659 100644 --- a/proto/zitadel/idp/v2/idp.proto +++ b/proto/zitadel/idp/v2/idp.proto @@ -303,6 +303,9 @@ message SAMLConfig { // in case the nameid-format returned is // `urn:oasis:names:tc:SAML:2.0:nameid-format:transient`. optional string transient_mapping_attribute_name = 5; + // Boolean weather federated logout is enabled. If enabled, ZITADEL will send a logout request to the identity provider, + // if the user terminates the session in ZITADEL. Be sure to provide a SLO endpoint as part of the metadata. + optional bool federated_logout_enabled = 6; } message AzureADConfig { diff --git a/proto/zitadel/management.proto b/proto/zitadel/management.proto index c90c44667c..3018ebe600 100644 --- a/proto/zitadel/management.proto +++ b/proto/zitadel/management.proto @@ -13408,6 +13408,9 @@ message AddSAMLProviderRequest { // Optionally specify the name of the attribute, which will be used to map the user // in case the nameid-format returned is `urn:oasis:names:tc:SAML:2.0:nameid-format:transient`. optional string transient_mapping_attribute_name = 8; + // Optionally enable federated logout. If enabled, ZITADEL will send a logout request to the identity provider, + // if the user terminates the session in ZITADEL. Be sure to provide a SLO endpoint as part of the metadata. + optional bool federated_logout_enabled = 9; } message AddSAMLProviderResponse { @@ -13442,6 +13445,9 @@ message UpdateSAMLProviderRequest { // Optionally specify the name of the attribute, which will be used to map the user // in case the nameid-format returned is `urn:oasis:names:tc:SAML:2.0:nameid-format:transient`. optional string transient_mapping_attribute_name = 9; + // Optionally enable federated logout. If enabled, ZITADEL will send a logout request to the identity provider, + // if the user terminates the session in ZITADEL. Be sure to provide a SLO endpoint as part of the metadata. + optional bool federated_logout_enabled = 10; } message UpdateSAMLProviderResponse { From eb0eed21fa682774e476d0c720c501b37f0f7793 Mon Sep 17 00:00:00 2001 From: Silvan <27845747+adlerhurst@users.noreply.github.com> Date: Mon, 26 May 2025 13:23:38 +0200 Subject: [PATCH 44/76] fix(api): correct mapping of user state queries (#9956) # Which Problems Are Solved the mapping of `ListUsers` was wrong for user states. # How the Problems Are Solved mapping of user state introduced to correctly map it # Additional Changes mapping of user type introduced to prevent same issue # Additional Context Requires backport to 2.x and 3.x Co-authored-by: Livio Spring --- internal/api/grpc/user/query.go | 4 +-- internal/api/grpc/user/v2/query.go | 4 +-- internal/api/grpc/user/v2beta/query.go | 4 +-- .../api/scim/resources/user_query_builder.go | 2 +- internal/query/user.go | 4 +-- pkg/grpc/user/user.go | 36 +++++++++++++++++++ pkg/grpc/user/v2/user.go | 34 ++++++++++++++++++ pkg/grpc/user/v2beta/user.go | 34 ++++++++++++++++++ 8 files changed, 113 insertions(+), 9 deletions(-) diff --git a/internal/api/grpc/user/query.go b/internal/api/grpc/user/query.go index 41cce01a8c..66edbac90e 100644 --- a/internal/api/grpc/user/query.go +++ b/internal/api/grpc/user/query.go @@ -84,11 +84,11 @@ func EmailQueryToQuery(q *user_pb.EmailQuery) (query.SearchQuery, error) { } func StateQueryToQuery(q *user_pb.StateQuery) (query.SearchQuery, error) { - return query.NewUserStateSearchQuery(int32(q.State)) + return query.NewUserStateSearchQuery(q.State.ToDomain()) } func TypeQueryToQuery(q *user_pb.TypeQuery) (query.SearchQuery, error) { - return query.NewUserTypeSearchQuery(int32(q.Type)) + return query.NewUserTypeSearchQuery(q.Type.ToDomain()) } func LoginNameQueryToQuery(q *user_pb.LoginNameQuery) (query.SearchQuery, error) { diff --git a/internal/api/grpc/user/v2/query.go b/internal/api/grpc/user/v2/query.go index 136a4a0932..dc886462be 100644 --- a/internal/api/grpc/user/v2/query.go +++ b/internal/api/grpc/user/v2/query.go @@ -298,11 +298,11 @@ func phoneQueryToQuery(q *user.PhoneQuery) (query.SearchQuery, error) { } func stateQueryToQuery(q *user.StateQuery) (query.SearchQuery, error) { - return query.NewUserStateSearchQuery(int32(q.State)) + return query.NewUserStateSearchQuery(q.State.ToDomain()) } func typeQueryToQuery(q *user.TypeQuery) (query.SearchQuery, error) { - return query.NewUserTypeSearchQuery(int32(q.Type)) + return query.NewUserTypeSearchQuery(q.Type.ToDomain()) } func loginNameQueryToQuery(q *user.LoginNameQuery) (query.SearchQuery, error) { diff --git a/internal/api/grpc/user/v2beta/query.go b/internal/api/grpc/user/v2beta/query.go index e3602abc33..46b009a72e 100644 --- a/internal/api/grpc/user/v2beta/query.go +++ b/internal/api/grpc/user/v2beta/query.go @@ -292,11 +292,11 @@ func phoneQueryToQuery(q *user.PhoneQuery) (query.SearchQuery, error) { } func stateQueryToQuery(q *user.StateQuery) (query.SearchQuery, error) { - return query.NewUserStateSearchQuery(int32(q.State)) + return query.NewUserStateSearchQuery(q.State.ToDomain()) } func typeQueryToQuery(q *user.TypeQuery) (query.SearchQuery, error) { - return query.NewUserTypeSearchQuery(int32(q.Type)) + return query.NewUserTypeSearchQuery(q.Type.ToDomain()) } func loginNameQueryToQuery(q *user.LoginNameQuery) (query.SearchQuery, error) { diff --git a/internal/api/scim/resources/user_query_builder.go b/internal/api/scim/resources/user_query_builder.go index b86b171fb5..7a06e4b3fd 100644 --- a/internal/api/scim/resources/user_query_builder.go +++ b/internal/api/scim/resources/user_query_builder.go @@ -70,7 +70,7 @@ func (h *UsersHandler) buildListQuery(ctx context.Context, request *ListRequest) } // the zitadel scim implementation only supports humans for now - userTypeQuery, err := query.NewUserTypeSearchQuery(int32(domain.UserTypeHuman)) + userTypeQuery, err := query.NewUserTypeSearchQuery(domain.UserTypeHuman) if err != nil { return nil, err } diff --git a/internal/query/user.go b/internal/query/user.go index a97e3bbd14..3d47847cac 100644 --- a/internal/query/user.go +++ b/internal/query/user.go @@ -776,11 +776,11 @@ func NewUserVerifiedPhoneSearchQuery(value string, comparison TextComparison) (S return NewTextQuery(NotifyVerifiedPhoneCol, value, comparison) } -func NewUserStateSearchQuery(value int32) (SearchQuery, error) { +func NewUserStateSearchQuery(value domain.UserState) (SearchQuery, error) { return NewNumberQuery(UserStateCol, value, NumberEquals) } -func NewUserTypeSearchQuery(value int32) (SearchQuery, error) { +func NewUserTypeSearchQuery(value domain.UserType) (SearchQuery, error) { return NewNumberQuery(UserTypeCol, value, NumberEquals) } diff --git a/pkg/grpc/user/user.go b/pkg/grpc/user/user.go index 450370e704..a86c957fd8 100644 --- a/pkg/grpc/user/user.go +++ b/pkg/grpc/user/user.go @@ -1,5 +1,7 @@ package user +import "github.com/zitadel/zitadel/internal/domain" + type SearchQuery_ResourceOwner struct { ResourceOwner *ResourceOwnerQuery } @@ -13,3 +15,37 @@ type ResourceOwnerQuery struct { type UserType = isUser_Type type MembershipType = isMembership_Type + +func (s UserState) ToDomain() domain.UserState { + switch s { + case UserState_USER_STATE_UNSPECIFIED: + return domain.UserStateUnspecified + case UserState_USER_STATE_ACTIVE: + return domain.UserStateActive + case UserState_USER_STATE_INACTIVE: + return domain.UserStateInactive + case UserState_USER_STATE_DELETED: + return domain.UserStateDeleted + case UserState_USER_STATE_LOCKED: + return domain.UserStateLocked + case UserState_USER_STATE_SUSPEND: + return domain.UserStateSuspend + case UserState_USER_STATE_INITIAL: + return domain.UserStateInitial + default: + return domain.UserStateUnspecified + } +} + +func (t Type) ToDomain() domain.UserType { + switch t { + case Type_TYPE_UNSPECIFIED: + return domain.UserTypeUnspecified + case Type_TYPE_HUMAN: + return domain.UserTypeHuman + case Type_TYPE_MACHINE: + return domain.UserTypeMachine + default: + return domain.UserTypeUnspecified + } +} diff --git a/pkg/grpc/user/v2/user.go b/pkg/grpc/user/v2/user.go index ec9245c8eb..20c3c6fe9b 100644 --- a/pkg/grpc/user/v2/user.go +++ b/pkg/grpc/user/v2/user.go @@ -1,3 +1,37 @@ package user +import "github.com/zitadel/zitadel/internal/domain" + type UserType = isUser_Type + +func (s UserState) ToDomain() domain.UserState { + switch s { + case UserState_USER_STATE_UNSPECIFIED: + return domain.UserStateUnspecified + case UserState_USER_STATE_ACTIVE: + return domain.UserStateActive + case UserState_USER_STATE_INACTIVE: + return domain.UserStateInactive + case UserState_USER_STATE_DELETED: + return domain.UserStateDeleted + case UserState_USER_STATE_LOCKED: + return domain.UserStateLocked + case UserState_USER_STATE_INITIAL: + return domain.UserStateInitial + default: + return domain.UserStateUnspecified + } +} + +func (t Type) ToDomain() domain.UserType { + switch t { + case Type_TYPE_UNSPECIFIED: + return domain.UserTypeUnspecified + case Type_TYPE_HUMAN: + return domain.UserTypeHuman + case Type_TYPE_MACHINE: + return domain.UserTypeMachine + default: + return domain.UserTypeUnspecified + } +} diff --git a/pkg/grpc/user/v2beta/user.go b/pkg/grpc/user/v2beta/user.go index ec9245c8eb..20c3c6fe9b 100644 --- a/pkg/grpc/user/v2beta/user.go +++ b/pkg/grpc/user/v2beta/user.go @@ -1,3 +1,37 @@ package user +import "github.com/zitadel/zitadel/internal/domain" + type UserType = isUser_Type + +func (s UserState) ToDomain() domain.UserState { + switch s { + case UserState_USER_STATE_UNSPECIFIED: + return domain.UserStateUnspecified + case UserState_USER_STATE_ACTIVE: + return domain.UserStateActive + case UserState_USER_STATE_INACTIVE: + return domain.UserStateInactive + case UserState_USER_STATE_DELETED: + return domain.UserStateDeleted + case UserState_USER_STATE_LOCKED: + return domain.UserStateLocked + case UserState_USER_STATE_INITIAL: + return domain.UserStateInitial + default: + return domain.UserStateUnspecified + } +} + +func (t Type) ToDomain() domain.UserType { + switch t { + case Type_TYPE_UNSPECIFIED: + return domain.UserTypeUnspecified + case Type_TYPE_HUMAN: + return domain.UserTypeHuman + case Type_TYPE_MACHINE: + return domain.UserTypeMachine + default: + return domain.UserTypeUnspecified + } +} From 833f6279e11c43652a579f2211311e2fc6c0e2a7 Mon Sep 17 00:00:00 2001 From: Livio Spring Date: Mon, 26 May 2025 13:59:20 +0200 Subject: [PATCH 45/76] fix: allow invite codes for users with verified mails (#9962) # Which Problems Are Solved Users who started the invitation code verification, but haven't set up any authentication method, need to be able to do so. This might require a new invitation code, which was currently not possible since creation was prevented for users with verified emails. # How the Problems Are Solved - Allow creation of invitation emails for users with verified emails. - Merged the creation and resend into a single method, defaulting the urlTemplate, applicatioName and authRequestID from the previous code (if one exists). On the user service API, the `ResendInviteCode` endpoint has been deprecated in favor of the `CreateInviteCode` # Additional Changes None # Additional Context - Noticed while investigating something internally. - requires backport to 2.x and 3.x --- .../user/v2/integration_test/user_test.go | 27 ++++++ internal/command/user_v2_invite.go | 83 ++++++++----------- internal/command/user_v2_invite_model.go | 2 +- internal/command/user_v2_invite_test.go | 75 +---------------- proto/zitadel/user/v2/user.proto | 4 +- proto/zitadel/user/v2/user_service.proto | 5 ++ 6 files changed, 71 insertions(+), 125 deletions(-) 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 832268dc8c..7f211afd6f 100644 --- a/internal/api/grpc/user/v2/integration_test/user_test.go +++ b/internal/api/grpc/user/v2/integration_test/user_test.go @@ -3190,6 +3190,33 @@ func TestServer_CreateInviteCode(t *testing.T) { }, }, }, + { + name: "recreate", + args: args{ + ctx: CTX, + req: &user.CreateInviteCodeRequest{}, + prepare: func(request *user.CreateInviteCodeRequest) error { + resp := Instance.CreateHumanUser(CTX) + request.UserId = resp.GetUserId() + _, err := Instance.Client.UserV2.CreateInviteCode(CTX, &user.CreateInviteCodeRequest{ + UserId: resp.GetUserId(), + Verification: &user.CreateInviteCodeRequest_SendCode{ + SendCode: &user.SendInviteCode{ + UrlTemplate: gu.Ptr("https://example.com/email/verify?userID={{.UserID}}&code={{.Code}}&orgID={{.OrgID}}"), + ApplicationName: gu.Ptr("TestApp"), + }, + }, + }) + return err + }, + }, + want: &user.CreateInviteCodeResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Instance.DefaultOrg.Id, + }, + }, + }, { name: "create, return code, ok", args: args{ diff --git a/internal/command/user_v2_invite.go b/internal/command/user_v2_invite.go index 7760107146..430ba8c7d1 100644 --- a/internal/command/user_v2_invite.go +++ b/internal/command/user_v2_invite.go @@ -19,14 +19,34 @@ type CreateUserInvite struct { URLTemplate string ReturnCode bool ApplicationName string + AuthRequestID string } func (c *Commands) CreateInviteCode(ctx context.Context, invite *CreateUserInvite) (details *domain.ObjectDetails, returnCode *string, err error) { + return c.sendInviteCode(ctx, invite, "", false) +} + +// ResendInviteCode resends the invite mail with a new code and an optional authRequestID. +// It will reuse the applicationName from the previous code. +func (c *Commands) ResendInviteCode(ctx context.Context, userID, resourceOwner, authRequestID string) (objectDetails *domain.ObjectDetails, err error) { + details, _, err := c.sendInviteCode( + ctx, + &CreateUserInvite{ + UserID: userID, + AuthRequestID: authRequestID, + }, + resourceOwner, + true, + ) + return details, err +} + +func (c *Commands) sendInviteCode(ctx context.Context, invite *CreateUserInvite, resourceOwner string, requireExisting bool) (details *domain.ObjectDetails, returnCode *string, err error) { invite.UserID = strings.TrimSpace(invite.UserID) if invite.UserID == "" { return nil, nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-4jio3", "Errors.User.UserIDMissing") } - wm, err := c.userInviteCodeWriteModel(ctx, invite.UserID, "") + wm, err := c.userInviteCodeWriteModel(ctx, invite.UserID, resourceOwner) if err != nil { return nil, nil, err } @@ -39,10 +59,22 @@ func (c *Commands) CreateInviteCode(ctx context.Context, invite *CreateUserInvit if !wm.CreationAllowed() { return nil, nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-EF34g", "Errors.User.AlreadyInitialised") } + if requireExisting && wm.InviteCode == nil || wm.CodeReturned { + return nil, nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-Wr3gq", "Errors.User.Code.NotFound") + } code, err := c.newUserInviteCode(ctx, c.eventstore.Filter, c.userEncryption) //nolint if err != nil { return nil, nil, err } + if invite.URLTemplate == "" { + invite.URLTemplate = wm.URLTemplate + } + if invite.ApplicationName == "" { + invite.ApplicationName = wm.ApplicationName + } + if invite.AuthRequestID == "" { + invite.AuthRequestID = wm.AuthRequestID + } err = c.pushAppendAndReduce(ctx, wm, user.NewHumanInviteCodeAddedEvent( ctx, UserAggregateFromWriteModelCtx(ctx, &wm.WriteModel), @@ -51,7 +83,7 @@ func (c *Commands) CreateInviteCode(ctx context.Context, invite *CreateUserInvit invite.URLTemplate, invite.ReturnCode, invite.ApplicationName, - "", + invite.AuthRequestID, )) if err != nil { return nil, nil, err @@ -62,53 +94,6 @@ func (c *Commands) CreateInviteCode(ctx context.Context, invite *CreateUserInvit return writeModelToObjectDetails(&wm.WriteModel), returnCode, nil } -// ResendInviteCode resends the invite mail with a new code and an optional authRequestID. -// It will reuse the applicationName from the previous code. -func (c *Commands) ResendInviteCode(ctx context.Context, userID, resourceOwner, authRequestID string) (objectDetails *domain.ObjectDetails, err error) { - if userID == "" { - return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-2n8vs", "Errors.User.UserIDMissing") - } - - existingCode, err := c.userInviteCodeWriteModel(ctx, userID, resourceOwner) - if 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") - } - if existingCode.InviteCode == nil || existingCode.CodeReturned { - return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-Wr3gq", "Errors.User.Code.NotFound") - } - code, err := c.newUserInviteCode(ctx, c.eventstore.Filter, c.userEncryption) //nolint - if err != nil { - return nil, err - } - if authRequestID == "" { - authRequestID = existingCode.AuthRequestID - } - err = c.pushAppendAndReduce(ctx, existingCode, - user.NewHumanInviteCodeAddedEvent( - ctx, - UserAggregateFromWriteModelCtx(ctx, &existingCode.WriteModel), - code.Crypted, - code.Expiry, - existingCode.URLTemplate, - false, - existingCode.ApplicationName, - authRequestID, - )) - if err != nil { - return nil, err - } - return writeModelToObjectDetails(&existingCode.WriteModel), nil -} - func (c *Commands) InviteCodeSent(ctx context.Context, userID, orgID string) (err error) { if userID == "" { return zerrors.ThrowInvalidArgument(nil, "COMMAND-Sgf31", "Errors.User.UserIDMissing") diff --git a/internal/command/user_v2_invite_model.go b/internal/command/user_v2_invite_model.go index 23f6322a19..6b2ab62e0d 100644 --- a/internal/command/user_v2_invite_model.go +++ b/internal/command/user_v2_invite_model.go @@ -28,7 +28,7 @@ type UserV2InviteWriteModel struct { } func (wm *UserV2InviteWriteModel) CreationAllowed() bool { - return !wm.EmailVerified && !wm.AuthMethodSet + return !wm.AuthMethodSet } func newUserV2InviteWriteModel(userID, orgID string) *UserV2InviteWriteModel { diff --git a/internal/command/user_v2_invite_test.go b/internal/command/user_v2_invite_test.go index 817987e7e4..04c00d876e 100644 --- a/internal/command/user_v2_invite_test.go +++ b/internal/command/user_v2_invite_test.go @@ -11,7 +11,6 @@ import ( "go.uber.org/mock/gomock" "golang.org/x/text/language" - "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" @@ -316,7 +315,7 @@ func TestCommands_ResendInviteCode(t *testing.T) { userID: "", }, want{ - err: zerrors.ThrowInvalidArgument(nil, "COMMAND-2n8vs", "Errors.User.UserIDMissing"), + err: zerrors.ThrowInvalidArgument(nil, "COMMAND-4jio3", "Errors.User.UserIDMissing"), }, }, { @@ -362,7 +361,7 @@ func TestCommands_ResendInviteCode(t *testing.T) { userID: "unknown", }, want{ - err: zerrors.ThrowPreconditionFailed(nil, "COMMAND-H3b2a", "Errors.User.NotFound"), + err: zerrors.ThrowPreconditionFailed(nil, "COMMAND-Wgvn4", "Errors.User.NotFound"), }, }, { @@ -580,76 +579,6 @@ func TestCommands_ResendInviteCode(t *testing.T) { }, }, }, - { - "resend with own user ok", - fields{ - eventstore: expectEventstore( - expectFilter( - eventFromEventPusher( - user.NewHumanAddedEvent(context.Background(), - &user.NewAggregate("userID", "org1").Aggregate, - "username", "firstName", - "lastName", - "nickName", - "displayName", - language.Afrikaans, - domain.GenderUnspecified, - "email", - false, - ), - ), - eventFromEventPusher( - user.NewHumanInviteCodeAddedEvent(context.Background(), - &user.NewAggregate("userID", "org1").Aggregate, - &crypto.CryptoValue{ - CryptoType: crypto.TypeEncryption, - Algorithm: "enc", - KeyID: "id", - Crypted: []byte("code"), - }, - time.Hour, - "", - false, - "", - "authRequestID", - ), - ), - ), - expectPush( - eventFromEventPusher( - user.NewHumanInviteCodeAddedEvent(authz.NewMockContext("instanceID", "org1", "userID"), - &user.NewAggregate("userID", "org1").Aggregate, - &crypto.CryptoValue{ - CryptoType: crypto.TypeEncryption, - Algorithm: "enc", - KeyID: "id", - Crypted: []byte("code"), - }, - time.Hour, - "", - false, - "", - "authRequestID2", - ), - ), - ), - ), - checkPermission: newMockPermissionCheckNotAllowed(), // user does not have permission, is allowed in the own context - newEncryptedCodeWithDefault: mockEncryptedCodeWithDefault("code", time.Hour), - defaultSecretGenerators: &SecretGenerators{}, - }, - args{ - ctx: authz.NewMockContext("instanceID", "org1", "userID"), - userID: "userID", - authRequestID: "authRequestID2", - }, - want{ - details: &domain.ObjectDetails{ - ResourceOwner: "org1", - ID: "userID", - }, - }, - }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/proto/zitadel/user/v2/user.proto b/proto/zitadel/user/v2/user.proto index e2a140ea27..9ea2b8906e 100644 --- a/proto/zitadel/user/v2/user.proto +++ b/proto/zitadel/user/v2/user.proto @@ -334,7 +334,7 @@ message AuthFactorU2F { message SendInviteCode { // Optionally set a url_template, which will be used in the invite mail sent by ZITADEL to guide the user to your invitation page. - // If no template is set, the default ZITADEL url will be used. + // If no template is set and no previous code was created, the default ZITADEL url will be used. // // The following placeholders can be used: UserID, OrgID, Code optional string url_template = 1 [ @@ -346,7 +346,7 @@ message SendInviteCode { } ]; // Optionally set an application name, which will be used in the invite mail sent by ZITADEL. - // If no application name is set, ZITADEL will be used as default. + // If no application name is set and no previous code was created, ZITADEL will be used as default. optional string application_name = 2 [ (validate.rules).string = {min_len: 1, max_len: 200}, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { diff --git a/proto/zitadel/user/v2/user_service.proto b/proto/zitadel/user/v2/user_service.proto index 15bc2d7775..44d25c07b3 100644 --- a/proto/zitadel/user/v2/user_service.proto +++ b/proto/zitadel/user/v2/user_service.proto @@ -1135,6 +1135,8 @@ service UserService { // Create an invite code for a user // // Create an invite code for a user to initialize their first authentication method (password, passkeys, IdP) depending on the organization's available methods. + // If an invite code has been created previously, it's url template and application name will be used as defaults for the new code. + // The new code will overwrite the previous one and make it invalid. rpc CreateInviteCode (CreateInviteCodeRequest) returns (CreateInviteCodeResponse) { option (google.api.http) = { post: "/v2/users/{user_id}/invite_code" @@ -1158,6 +1160,8 @@ service UserService { // Resend an invite code for a user // + // Deprecated: Use [CreateInviteCode](apis/resources/user_service_v2/user-service-create-invite-code.api.mdx) instead. + // // Resend an invite code for a user to initialize their first authentication method (password, passkeys, IdP) depending on the organization's available methods. // A resend is only possible if a code has been created previously and sent to the user. If there is no code or it was directly returned, an error will be returned. rpc ResendInviteCode (ResendInviteCodeRequest) returns (ResendInviteCodeResponse) { @@ -1172,6 +1176,7 @@ service UserService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + deprecated: true; responses: { key: "200" value: { From 4d66a786c88ba624bb8ca7bd67a128b920e0cb7f Mon Sep 17 00:00:00 2001 From: Livio Spring Date: Tue, 27 May 2025 16:26:46 +0200 Subject: [PATCH 46/76] feat: JWT IdP intent (#9966) # Which Problems Are Solved The login v1 allowed to use JWTs as IdP using the JWT IDP. The login V2 uses idp intents for such cases, which were not yet able to handle JWT IdPs. # How the Problems Are Solved - Added handling of JWT IdPs in `StartIdPIntent` and `RetrieveIdPIntent` - The redirect returned by the start, uses the existing `authRequestID` and `userAgentID` parameter names for compatibility reasons. - Added `/idps/jwt` endpoint to handle the proxied (callback) endpoint , which extracts and validates the JWT against the configured endpoint. # Additional Changes None # Additional Context - closes #9758 --- .../integrate/identity-providers/jwt_idp.md | 2 +- docs/static/img/guides/jwt_idp.png | Bin 275050 -> 38738 bytes .../user/v2/integration_test/user_test.go | 112 ++++++++++++++++++ internal/api/grpc/user/v2/intent.go | 2 +- internal/api/idp/idp.go | 86 ++++++++++++++ internal/idp/providers/jwt/jwt.go | 22 ++-- internal/idp/providers/jwt/jwt_test.go | 28 ++++- internal/idp/providers/jwt/session.go | 13 ++ internal/integration/client.go | 18 +++ internal/integration/sink/server.go | 52 ++++++++ 10 files changed, 319 insertions(+), 16 deletions(-) diff --git a/docs/docs/guides/integrate/identity-providers/jwt_idp.md b/docs/docs/guides/integrate/identity-providers/jwt_idp.md index 2a0e8b8e7a..52324ad5c2 100644 --- a/docs/docs/guides/integrate/identity-providers/jwt_idp.md +++ b/docs/docs/guides/integrate/identity-providers/jwt_idp.md @@ -49,7 +49,7 @@ The **JWT IdP Configuration** might then be: Therefore, if the user is redirected from ZITADEL to the JWT Endpoint on the WAF (`https://apps.test.com/existing/auth-new`), the session cookies previously issued by the WAF, will be sent along by the browser due to the path being on the same domain as the exiting application. -The WAF will reuse the session and send the JWT in the HTTP header `x-custom-tkn` to its upstream, the ZITADEL JWT Endpoint (`https://accounts.test.com/ui/login/login/jwt/authorize`). +The WAF will reuse the session and send the JWT in the HTTP header `x-custom-tkn` to its upstream, the ZITADEL JWT Endpoint (`https://accounts.test.com/ipds/jwt`). For the signature validation, ZITADEL must be able to connect to Keys Endpoint (`https://issuer.test.internal/keys`) and it will check if the token was signed (claim `iss`) by the defined Issuer (`https://issuer.test.internal`). diff --git a/docs/static/img/guides/jwt_idp.png b/docs/static/img/guides/jwt_idp.png index 73d5353521a9eb844c801ad9fe3bfc6291181dc9..218996aef756b0033575c54ff604034108a7f46a 100644 GIT binary patch literal 38738 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 literal 275050 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

xuQ1w*QK&q!^g}p!>(K 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

<<~ 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 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 7f211afd6f..4cf4ab21f8 100644 --- a/internal/api/grpc/user/v2/integration_test/user_test.go +++ b/internal/api/grpc/user/v2/integration_test/user_test.go @@ -1875,6 +1875,7 @@ func TestServer_StartIdentityProviderIntent(t *testing.T) { samlIdpID := Instance.AddSAMLProvider(IamCTX) samlRedirectIdpID := Instance.AddSAMLRedirectProvider(IamCTX, "") samlPostIdpID := Instance.AddSAMLPostProvider(IamCTX) + jwtIdPID := Instance.AddJWTProvider(IamCTX) type args struct { ctx context.Context req *user.StartIdentityProviderIntentRequest @@ -2097,6 +2098,30 @@ func TestServer_StartIdentityProviderIntent(t *testing.T) { }, wantErr: false, }, + { + name: "next step jwt idp", + args: args{ + CTX, + &user.StartIdentityProviderIntentRequest{ + IdpId: jwtIdPID, + Content: &user.StartIdentityProviderIntentRequest_Urls{ + Urls: &user.RedirectURLs{ + SuccessUrl: "https://example.com/success", + FailureUrl: "https://example.com/failure", + }, + }, + }, + }, + want: want{ + details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Instance.ID(), + }, + url: "https://example.com/jwt", + parametersExisting: []string{"authRequestID", "userAgentID"}, + }, + wantErr: false, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -2134,6 +2159,7 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) { oidcIdpID := Instance.AddGenericOIDCProvider(IamCTX, gofakeit.AppName()).GetId() samlIdpID := Instance.AddSAMLPostProvider(IamCTX) ldapIdpID := Instance.AddLDAPProvider(IamCTX) + jwtIdPID := Instance.AddJWTProvider(IamCTX) authURL, err := url.Parse(Instance.CreateIntent(CTX, oauthIdpID).GetAuthUrl()) require.NoError(t, err) intentID := authURL.Query().Get("state") @@ -2168,6 +2194,10 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) { require.NoError(t, err) samlSuccessfulWithUserID, samlWithUserToken, samlWithUserChangeDate, samlWithUserSequence, err := sink.SuccessfulSAMLIntent(Instance.ID(), samlIdpID, "id", "user", expiry) require.NoError(t, err) + jwtSuccessfulID, jwtToken, jwtChangeDate, jwtSequence, err := sink.SuccessfulJWTIntent(Instance.ID(), jwtIdPID, "id", "", expiry) + require.NoError(t, err) + jwtSuccessfulWithUserID, jwtWithUserToken, jwtWithUserChangeDate, jwtWithUserSequence, err := sink.SuccessfulJWTIntent(Instance.ID(), jwtIdPID, "id", "user", expiry) + require.NoError(t, err) type args struct { ctx context.Context req *user.RetrieveIdentityProviderIntentRequest @@ -2591,6 +2621,88 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) { }, wantErr: false, }, + { + name: "retrieve successful jwt intent", + args: args{ + CTX, + &user.RetrieveIdentityProviderIntentRequest{ + IdpIntentId: jwtSuccessfulID, + IdpIntentToken: jwtToken, + }, + }, + want: &user.RetrieveIdentityProviderIntentResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.New(jwtChangeDate), + ResourceOwner: Instance.ID(), + Sequence: jwtSequence, + }, + IdpInformation: &user.IDPInformation{ + Access: &user.IDPInformation_Oauth{ + Oauth: &user.IDPOAuthAccessInformation{ + IdToken: gu.Ptr("idToken"), + }, + }, + IdpId: jwtIdPID, + UserId: "id", + UserName: "", + RawInformation: func() *structpb.Struct { + s, err := structpb.NewStruct(map[string]interface{}{ + "sub": "id", + }) + require.NoError(t, err) + return s + }(), + }, + AddHumanUser: &user.AddHumanUserRequest{ + Profile: &user.SetHumanProfile{ + PreferredLanguage: gu.Ptr("und"), + }, + IdpLinks: []*user.IDPLink{ + {IdpId: jwtIdPID, UserId: "id"}, + }, + Email: &user.SetHumanEmail{ + Verification: &user.SetHumanEmail_SendCode{SendCode: &user.SendEmailVerificationCode{}}, + }, + }, + }, + wantErr: false, + }, + { + name: "retrieve successful jwt intent with linked user", + args: args{ + CTX, + &user.RetrieveIdentityProviderIntentRequest{ + IdpIntentId: jwtSuccessfulWithUserID, + IdpIntentToken: jwtWithUserToken, + }, + }, + want: &user.RetrieveIdentityProviderIntentResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.New(jwtWithUserChangeDate), + ResourceOwner: Instance.ID(), + Sequence: jwtWithUserSequence, + }, + IdpInformation: &user.IDPInformation{ + Access: &user.IDPInformation_Oauth{ + Oauth: &user.IDPOAuthAccessInformation{ + IdToken: gu.Ptr("idToken"), + }, + }, + IdpId: jwtIdPID, + UserId: "id", + UserName: "", + RawInformation: func() *structpb.Struct { + s, err := structpb.NewStruct(map[string]interface{}{ + "sub": "id", + }) + require.NoError(t, err) + return s + }(), + }, + UserId: "user", + }, + wantErr: false, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/internal/api/grpc/user/v2/intent.go b/internal/api/grpc/user/v2/intent.go index afb34deb83..5514b6ef03 100644 --- a/internal/api/grpc/user/v2/intent.go +++ b/internal/api/grpc/user/v2/intent.go @@ -173,7 +173,7 @@ func (s *Server) RetrieveIdentityProviderIntent(ctx context.Context, req *user.R case *oidc.Provider: idpUser, err = unmarshalIdpUser(intent.IDPUser, oidc.InitUser()) case *jwt.Provider: - idpUser, err = unmarshalIdpUser(intent.IDPUser, &jwt.User{}) + idpUser, err = unmarshalIdpUser(intent.IDPUser, jwt.InitUser()) case *azuread.Provider: idpUser, err = unmarshalRawIdpUser(intent.IDPUser, p.User()) case *github.Provider: diff --git a/internal/api/idp/idp.go b/internal/api/idp/idp.go index f688ba2352..8b1c24134a 100644 --- a/internal/api/idp/idp.go +++ b/internal/api/idp/idp.go @@ -3,6 +3,7 @@ package idp import ( "bytes" "context" + "encoding/base64" "encoding/xml" "errors" "fmt" @@ -48,6 +49,7 @@ const ( acsPath = idpPrefix + "/saml/acs" certificatePath = idpPrefix + "/saml/certificate" sloPath = idpPrefix + "/saml/slo" + jwtPath = "/jwt" paramIntentID = "id" paramToken = "token" @@ -129,6 +131,7 @@ func NewHandler( router.HandleFunc(certificatePath, h.handleCertificate) router.HandleFunc(acsPath, h.handleACS) router.HandleFunc(sloPath, h.handleSLO) + router.HandleFunc(jwtPath, h.handleJWT) return router } @@ -307,6 +310,89 @@ func (h *Handler) handleACS(w http.ResponseWriter, r *http.Request) { redirectToSuccessURL(w, r, intent, token, userID) } +func (h *Handler) handleJWT(w http.ResponseWriter, r *http.Request) { + intentID, err := h.intentIDFromJWTRequest(r) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + intent, err := h.commands.GetActiveIntent(r.Context(), intentID) + if err != nil { + if zerrors.IsNotFound(err) { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + redirectToFailureURLErr(w, r, intent, err) + return + } + idpConfig, err := h.getProvider(r.Context(), intent.IDPID) + if err != nil { + cmdErr := h.commands.FailIDPIntent(r.Context(), intent, err.Error()) + logging.WithFields("intent", intent.AggregateID).OnError(cmdErr).Error("failed to push failed event on idp intent") + redirectToFailureURLErr(w, r, intent, err) + return + } + jwtIDP, ok := idpConfig.(*jwt.Provider) + if !ok { + err := zerrors.ThrowInvalidArgument(nil, "IDP-JK23ed", "Errors.ExternalIDP.IDPTypeNotImplemented") + cmdErr := h.commands.FailIDPIntent(r.Context(), intent, err.Error()) + logging.WithFields("intent", intent.AggregateID).OnError(cmdErr).Error("failed to push failed event on idp intent") + redirectToFailureURLErr(w, r, intent, err) + return + } + h.handleJWTExtraction(w, r, intent, jwtIDP) +} + +func (h *Handler) intentIDFromJWTRequest(r *http.Request) (string, error) { + // for compatibility of the old JWT provider we use the auth request id parameter to pass the intent id + intentID := r.FormValue(jwt.QueryAuthRequestID) + // for compatibility of the old JWT provider we use the user agent id parameter to pass the encrypted intent id + encryptedIntentID := r.FormValue(jwt.QueryUserAgentID) + if err := h.checkIntentID(intentID, encryptedIntentID); err != nil { + return "", err + } + return intentID, nil +} + +func (h *Handler) checkIntentID(intentID, encryptedIntentID string) error { + if intentID == "" || encryptedIntentID == "" { + return zerrors.ThrowInvalidArgument(nil, "LOGIN-adfzz", "Errors.AuthRequest.MissingParameters") + } + id, err := base64.RawURLEncoding.DecodeString(encryptedIntentID) + if err != nil { + return err + } + decryptedIntentID, err := h.encryptionAlgorithm.DecryptString(id, h.encryptionAlgorithm.EncryptionKeyID()) + if err != nil { + return err + } + if intentID != decryptedIntentID { + return zerrors.ThrowInvalidArgument(nil, "LOGIN-adfzz", "Errors.AuthRequest.MissingParameters") + } + return nil +} + +func (h *Handler) handleJWTExtraction(w http.ResponseWriter, r *http.Request, intent *command.IDPIntentWriteModel, identityProvider *jwt.Provider) { + session := jwt.NewSessionFromRequest(identityProvider, r) + user, err := session.FetchUser(r.Context()) + if err != nil { + cmdErr := h.commands.FailIDPIntent(r.Context(), intent, err.Error()) + logging.WithFields("intent", intent.AggregateID).OnError(cmdErr).Error("failed to push failed event on idp intent") + redirectToFailureURLErr(w, r, intent, err) + return + } + + userID, err := h.checkExternalUser(r.Context(), intent.IDPID, user.GetID()) + logging.WithFields("intent", intent.AggregateID).OnError(err).Error("could not check if idp user already exists") + + token, err := h.commands.SucceedIDPIntent(r.Context(), intent, user, session, userID) + if err != nil { + redirectToFailureURLErr(w, r, intent, err) + return + } + redirectToSuccessURL(w, r, intent, token, userID) +} + func (h *Handler) handleCallback(w http.ResponseWriter, r *http.Request) { ctx := r.Context() data, err := h.parseCallbackRequest(r) diff --git a/internal/idp/providers/jwt/jwt.go b/internal/idp/providers/jwt/jwt.go index 99347f31a3..d972102b01 100644 --- a/internal/idp/providers/jwt/jwt.go +++ b/internal/idp/providers/jwt/jwt.go @@ -11,14 +11,14 @@ import ( ) const ( - queryAuthRequestID = "authRequestID" - queryUserAgentID = "userAgentID" + QueryAuthRequestID = "authRequestID" + QueryUserAgentID = "userAgentID" ) var _ idp.Provider = (*Provider)(nil) var ( - ErrMissingUserAgentID = errors.New("userAgentID missing") + ErrMissingState = errors.New("state missing") ) // Provider is the [idp.Provider] implementation for a JWT provider @@ -92,32 +92,32 @@ func (p *Provider) Name() string { // It will create a [Session] with an AuthURL, pointing to the jwtEndpoint // with the authRequest and encrypted userAgent ids. func (p *Provider) BeginAuth(ctx context.Context, state string, params ...idp.Parameter) (idp.Session, error) { - userAgentID, err := userAgentIDFromParams(params...) - if err != nil { - return nil, err + if state == "" { + return nil, ErrMissingState } + userAgentID := userAgentIDFromParams(state, params...) redirect, err := url.Parse(p.jwtEndpoint) if err != nil { return nil, err } q := redirect.Query() - q.Set(queryAuthRequestID, state) + q.Set(QueryAuthRequestID, state) nonce, err := p.encryptionAlg.Encrypt([]byte(userAgentID)) if err != nil { return nil, err } - q.Set(queryUserAgentID, base64.RawURLEncoding.EncodeToString(nonce)) + q.Set(QueryUserAgentID, base64.RawURLEncoding.EncodeToString(nonce)) redirect.RawQuery = q.Encode() return &Session{AuthURL: redirect.String()}, nil } -func userAgentIDFromParams(params ...idp.Parameter) (string, error) { +func userAgentIDFromParams(state string, params ...idp.Parameter) string { for _, param := range params { if id, ok := param.(idp.UserAgentID); ok { - return string(id), nil + return string(id) } } - return "", ErrMissingUserAgentID + return state } // IsLinkingAllowed implements the [idp.Provider] interface. diff --git a/internal/idp/providers/jwt/jwt_test.go b/internal/idp/providers/jwt/jwt_test.go index 59e32b4690..5756c58e07 100644 --- a/internal/idp/providers/jwt/jwt_test.go +++ b/internal/idp/providers/jwt/jwt_test.go @@ -23,6 +23,7 @@ func TestProvider_BeginAuth(t *testing.T) { encryptionAlg func(t *testing.T) crypto.EncryptionAlgorithm } type args struct { + state string params []idp.Parameter } type want struct { @@ -36,7 +37,7 @@ func TestProvider_BeginAuth(t *testing.T) { want want }{ { - name: "missing userAgentID error", + name: "missing state, error", fields: fields{ issuer: "https://jwt.com", jwtEndpoint: "https://auth.com/jwt", @@ -47,14 +48,34 @@ func TestProvider_BeginAuth(t *testing.T) { }, }, args: args{ + state: "", params: nil, }, want: want{ err: func(err error) bool { - return errors.Is(err, ErrMissingUserAgentID) + return errors.Is(err, ErrMissingState) }, }, }, + { + name: "missing userAgentID, fallback to state", + fields: fields{ + issuer: "https://jwt.com", + jwtEndpoint: "https://auth.com/jwt", + keysEndpoint: "https://jwt.com/keys", + headerName: "jwt-header", + encryptionAlg: func(t *testing.T) crypto.EncryptionAlgorithm { + return crypto.CreateMockEncryptionAlg(gomock.NewController(t)) + }, + }, + args: args{ + state: "testState", + params: nil, + }, + want: want{ + session: &Session{AuthURL: "https://auth.com/jwt?authRequestID=testState&userAgentID=dGVzdFN0YXRl"}, + }, + }, { name: "successful auth", fields: fields{ @@ -67,6 +88,7 @@ func TestProvider_BeginAuth(t *testing.T) { }, }, args: args{ + state: "testState", params: []idp.Parameter{ idp.UserAgentID("agent"), }, @@ -91,7 +113,7 @@ func TestProvider_BeginAuth(t *testing.T) { require.NoError(t, err) ctx := context.Background() - session, err := provider.BeginAuth(ctx, "testState", tt.args.params...) + session, err := provider.BeginAuth(ctx, tt.args.state, tt.args.params...) if tt.want.err != nil && !tt.want.err(err) { a.Fail("invalid error", err) } diff --git a/internal/idp/providers/jwt/session.go b/internal/idp/providers/jwt/session.go index 5138812f3c..85b164a9c5 100644 --- a/internal/idp/providers/jwt/session.go +++ b/internal/idp/providers/jwt/session.go @@ -5,11 +5,13 @@ import ( "errors" "fmt" "net/http" + "strings" "time" "github.com/zitadel/logging" "github.com/zitadel/oidc/v3/pkg/client/rp" "github.com/zitadel/oidc/v3/pkg/oidc" + "golang.org/x/oauth2" "golang.org/x/text/language" "github.com/zitadel/zitadel/internal/domain" @@ -34,6 +36,11 @@ func NewSession(provider *Provider, tokens *oidc.Tokens[*oidc.IDTokenClaims]) *S return &Session{Provider: provider, Tokens: tokens} } +func NewSessionFromRequest(provider *Provider, r *http.Request) *Session { + token := strings.TrimPrefix(r.Header.Get(provider.headerName), oidc.PrefixBearer) + return NewSession(provider, &oidc.Tokens[*oidc.IDTokenClaims]{IDToken: token, Token: &oauth2.Token{}}) +} + // GetAuth implements the [idp.Session] interface. func (s *Session) GetAuth(ctx context.Context) (string, bool) { return idp.Redirect(s.AuthURL) @@ -99,6 +106,12 @@ func (s *Session) validateToken(ctx context.Context, token string) (*oidc.IDToke return claims, nil } +func InitUser() *User { + return &User{ + IDTokenClaims: &oidc.IDTokenClaims{}, + } +} + type User struct { *oidc.IDTokenClaims } diff --git a/internal/integration/client.go b/internal/integration/client.go index 3efd682ee1..320809a7e8 100644 --- a/internal/integration/client.go +++ b/internal/integration/client.go @@ -684,6 +684,24 @@ func (i *Instance) AddLDAPProvider(ctx context.Context) string { return resp.GetId() } +func (i *Instance) AddJWTProvider(ctx context.Context) string { + resp, err := i.Client.Admin.AddJWTProvider(ctx, &admin.AddJWTProviderRequest{ + Name: "jwt-idp", + Issuer: "https://example.com", + JwtEndpoint: "https://example.com/jwt", + KeysEndpoint: "https://example.com/keys", + HeaderName: "Authorization", + ProviderOptions: &idp.Options{ + IsLinkingAllowed: true, + IsCreationAllowed: true, + IsAutoCreation: true, + IsAutoUpdate: true, + }, + }) + logging.OnError(err).Panic("create jwt idp") + return resp.GetId() +} + func (i *Instance) CreateIntent(ctx context.Context, idpID string) *user_v2.StartIdentityProviderIntentResponse { resp, err := i.Client.UserV2.StartIdentityProviderIntent(ctx, &user_v2.StartIdentityProviderIntentRequest{ IdpId: idpID, diff --git a/internal/integration/sink/server.go b/internal/integration/sink/server.go index 8abb31a63e..653c5236d6 100644 --- a/internal/integration/sink/server.go +++ b/internal/integration/sink/server.go @@ -27,6 +27,7 @@ import ( "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/command" + "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" @@ -124,6 +125,25 @@ func SuccessfulLDAPIntent(instanceID, idpID, idpUserID, userID string) (string, return resp.IntentID, resp.Token, resp.ChangeDate, resp.Sequence, nil } +func SuccessfulJWTIntent(instanceID, idpID, idpUserID, userID string, expiry time.Time) (string, string, time.Time, uint64, error) { + u := url.URL{ + Scheme: "http", + Host: host, + Path: successfulIntentJWTPath(), + } + resp, err := callIntent(u.String(), &SuccessfulIntentRequest{ + InstanceID: instanceID, + IDPID: idpID, + IDPUserID: idpUserID, + UserID: userID, + Expiry: expiry, + }) + if err != nil { + return "", "", time.Time{}, uint64(0), err + } + return resp.IntentID, resp.Token, resp.ChangeDate, resp.Sequence, nil +} + // StartServer starts a simple HTTP server on localhost:8081 // ZITADEL can use the server to send HTTP requests which can be // used to validate tests through [Subscribe]rs. @@ -145,6 +165,7 @@ func StartServer(commands *command.Commands) (close func()) { router.HandleFunc(successfulIntentOIDCPath(), successfulIntentHandler(commands, createSuccessfulOIDCIntent)) router.HandleFunc(successfulIntentSAMLPath(), successfulIntentHandler(commands, createSuccessfulSAMLIntent)) router.HandleFunc(successfulIntentLDAPPath(), successfulIntentHandler(commands, createSuccessfulLDAPIntent)) + router.HandleFunc(successfulIntentJWTPath(), successfulIntentHandler(commands, createSuccessfulJWTIntent)) } s := &http.Server{ Addr: listenAddr, @@ -195,6 +216,10 @@ func successfulIntentLDAPPath() string { return path.Join(successfulIntentPath(), "/", "ldap") } +func successfulIntentJWTPath() string { + return path.Join(successfulIntentPath(), "/", "jwt") +} + // forwarder handles incoming HTTP requests from ZITADEL and // forwards them to all subscribed web sockets. type forwarder struct { @@ -497,3 +522,30 @@ func createSuccessfulLDAPIntent(ctx context.Context, cmd *command.Commands, req writeModel.ProcessedSequence, }, nil } + +func createSuccessfulJWTIntent(ctx context.Context, cmd *command.Commands, req *SuccessfulIntentRequest) (*SuccessfulIntentResponse, error) { + intentID, err := createIntent(ctx, cmd, req.InstanceID, req.IDPID) + writeModel, err := cmd.GetIntentWriteModel(ctx, intentID, req.InstanceID) + idpUser := &jwt.User{ + IDTokenClaims: &oidc.IDTokenClaims{ + TokenClaims: oidc.TokenClaims{ + Subject: req.IDPUserID, + }, + }, + } + session := &jwt.Session{ + Tokens: &oidc.Tokens[*oidc.IDTokenClaims]{ + IDToken: "idToken", + }, + } + token, err := cmd.SucceedIDPIntent(ctx, writeModel, idpUser, session, req.UserID) + if err != nil { + return nil, err + } + return &SuccessfulIntentResponse{ + intentID, + token, + writeModel.ChangeDate, + writeModel.ProcessedSequence, + }, nil +} From 77b433367ef8ac643e5988d54d9a9ce30e127ddc Mon Sep 17 00:00:00 2001 From: Connor <80653133+connorHashDash@users.noreply.github.com> Date: Wed, 28 May 2025 07:06:27 +0100 Subject: [PATCH 47/76] fix(login): Copy to clipboard button in MFA login step now compatible in non-chrome browser (#9880) related to issue [#9379](https://github.com/zitadel/zitadel/issues/9379) # Which Problems Are Solved Copy to clipboard button was not compatible with Webkit/ Firefox browsers. # How the Problems Are Solved The previous function used addEventListener without a callback function as a second argument. I simply added the callback function and left existing code intact to fix the bug. # Additional Changes Added `type=button` to prevent submitting the form when clicking the button. # Additional Context none --------- Co-authored-by: Livio Spring --- .../api/ui/login/static/resources/scripts/copy_to_clipboard.js | 2 +- internal/api/ui/login/static/templates/mfa_init_otp.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/api/ui/login/static/resources/scripts/copy_to_clipboard.js b/internal/api/ui/login/static/resources/scripts/copy_to_clipboard.js index 97359cdde5..848aa0742b 100644 --- a/internal/api/ui/login/static/resources/scripts/copy_to_clipboard.js +++ b/internal/api/ui/login/static/resources/scripts/copy_to_clipboard.js @@ -3,4 +3,4 @@ const copyToClipboard = str => { }; let copyButton = document.getElementById("copy"); -copyButton.addEventListener("click", copyToClipboard(copyButton.getAttribute("data-copy"))); +copyButton.addEventListener("click", () => copyToClipboard(copyButton.getAttribute("data-copy"))); diff --git a/internal/api/ui/login/static/templates/mfa_init_otp.html b/internal/api/ui/login/static/templates/mfa_init_otp.html index b84a88a5b1..a9ae31dcdd 100644 --- a/internal/api/ui/login/static/templates/mfa_init_otp.html +++ b/internal/api/ui/login/static/templates/mfa_init_otp.html @@ -28,7 +28,7 @@

From c097887bc5f680e12c998580fb56d98a15758f53 Mon Sep 17 00:00:00 2001 From: Livio Spring Date: Wed, 28 May 2025 10:12:27 +0200 Subject: [PATCH 48/76] fix: validate proto header and provide https enforcement (#9975) # Which Problems Are Solved ZITADEL uses the notification triggering requests Forwarded or X-Forwarded-Proto header to build the button link sent in emails for confirming a password reset with the emailed code. If this header is overwritten and a user clicks the link to a malicious site in the email, the secret code can be retrieved and used to reset the users password and take over his account. Accounts with MFA or Passwordless enabled can not be taken over by this attack. # How the Problems Are Solved - The `X-Forwarded-Proto` and `proto` of the Forwarded headers are validated (http / https). - Additionally, when exposing ZITADEL through https. An overwrite to http is no longer possible. # Additional Changes None # Additional Context None --- .../api/http/middleware/origin_interceptor.go | 32 +++++++------- .../middleware/origin_interceptor_test.go | 42 +++++++++---------- 2 files changed, 39 insertions(+), 35 deletions(-) diff --git a/internal/api/http/middleware/origin_interceptor.go b/internal/api/http/middleware/origin_interceptor.go index 35af8770b7..607855b80f 100644 --- a/internal/api/http/middleware/origin_interceptor.go +++ b/internal/api/http/middleware/origin_interceptor.go @@ -10,12 +10,12 @@ import ( http_util "github.com/zitadel/zitadel/internal/api/http" ) -func WithOrigin(fallBackToHttps bool, http1Header, http2Header string, instanceHostHeaders, publicDomainHeaders []string) mux.MiddlewareFunc { +func WithOrigin(enforceHttps bool, http1Header, http2Header string, instanceHostHeaders, publicDomainHeaders []string) mux.MiddlewareFunc { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { origin := composeDomainContext( r, - fallBackToHttps, + enforceHttps, // to make sure we don't break existing configurations we append the existing checked headers as well slices.Compact(append(instanceHostHeaders, http1Header, http2Header, http_util.Forwarded, http_util.ZitadelForwarded, http_util.ForwardedFor, http_util.ForwardedHost, http_util.ForwardedProto)), publicDomainHeaders, @@ -25,28 +25,32 @@ func WithOrigin(fallBackToHttps bool, http1Header, http2Header string, instanceH } } -func composeDomainContext(r *http.Request, fallBackToHttps bool, instanceDomainHeaders, publicDomainHeaders []string) *http_util.DomainCtx { +func composeDomainContext(r *http.Request, enforceHttps bool, instanceDomainHeaders, publicDomainHeaders []string) *http_util.DomainCtx { instanceHost, instanceProto := hostFromRequest(r, instanceDomainHeaders) publicHost, publicProto := hostFromRequest(r, publicDomainHeaders) - if publicProto == "" { - publicProto = instanceProto - } - if publicProto == "" { - publicProto = "http" - if fallBackToHttps { - publicProto = "https" - } - } if instanceHost == "" { instanceHost = r.Host } return &http_util.DomainCtx{ InstanceHost: instanceHost, - Protocol: publicProto, + Protocol: protocolFromRequest(instanceProto, publicProto, enforceHttps), PublicHost: publicHost, } } +func protocolFromRequest(instanceProto, publicProto string, enforceHttps bool) string { + if enforceHttps { + return "https" + } + if publicProto != "" { + return publicProto + } + if instanceProto != "" { + return instanceProto + } + return "http" +} + func hostFromRequest(r *http.Request, headers []string) (host, proto string) { var hostFromHeader, protoFromHeader string for _, header := range headers { @@ -65,7 +69,7 @@ func hostFromRequest(r *http.Request, headers []string) (host, proto string) { if host == "" { host = hostFromHeader } - if proto == "" { + if proto == "" && (protoFromHeader == "http" || protoFromHeader == "https") { proto = protoFromHeader } } diff --git a/internal/api/http/middleware/origin_interceptor_test.go b/internal/api/http/middleware/origin_interceptor_test.go index 989e4d48b3..7419c91aba 100644 --- a/internal/api/http/middleware/origin_interceptor_test.go +++ b/internal/api/http/middleware/origin_interceptor_test.go @@ -11,8 +11,8 @@ import ( func Test_composeOrigin(t *testing.T) { type args struct { - h http.Header - fallBackToHttps bool + h http.Header + enforceHttps bool } tests := []struct { name string @@ -30,7 +30,7 @@ func Test_composeOrigin(t *testing.T) { h: http.Header{ "Forwarded": []string{"proto=https"}, }, - fallBackToHttps: false, + enforceHttps: false, }, want: &http_util.DomainCtx{ InstanceHost: "host.header", @@ -42,7 +42,7 @@ func Test_composeOrigin(t *testing.T) { h: http.Header{ "Forwarded": []string{"host=forwarded.host"}, }, - fallBackToHttps: false, + enforceHttps: false, }, want: &http_util.DomainCtx{ InstanceHost: "forwarded.host", @@ -54,7 +54,7 @@ func Test_composeOrigin(t *testing.T) { h: http.Header{ "Forwarded": []string{"proto=https;host=forwarded.host"}, }, - fallBackToHttps: false, + enforceHttps: false, }, want: &http_util.DomainCtx{ InstanceHost: "forwarded.host", @@ -66,7 +66,7 @@ func Test_composeOrigin(t *testing.T) { h: http.Header{ "Forwarded": []string{"proto=https;host=forwarded.host, proto=http;host=forwarded.host2"}, }, - fallBackToHttps: false, + enforceHttps: false, }, want: &http_util.DomainCtx{ InstanceHost: "forwarded.host", @@ -78,7 +78,7 @@ func Test_composeOrigin(t *testing.T) { h: http.Header{ "Forwarded": []string{"proto=https;host=forwarded.host, proto=http"}, }, - fallBackToHttps: false, + enforceHttps: false, }, want: &http_util.DomainCtx{ InstanceHost: "forwarded.host", @@ -90,11 +90,11 @@ func Test_composeOrigin(t *testing.T) { h: http.Header{ "Forwarded": []string{"proto=http", "proto=https;host=forwarded.host", "proto=http"}, }, - fallBackToHttps: true, + enforceHttps: true, }, want: &http_util.DomainCtx{ InstanceHost: "forwarded.host", - Protocol: "http", + Protocol: "https", }, }, { name: "x-forwarded-proto https", @@ -102,7 +102,7 @@ func Test_composeOrigin(t *testing.T) { h: http.Header{ "X-Forwarded-Proto": []string{"https"}, }, - fallBackToHttps: false, + enforceHttps: false, }, want: &http_util.DomainCtx{ InstanceHost: "host.header", @@ -114,25 +114,25 @@ func Test_composeOrigin(t *testing.T) { h: http.Header{ "X-Forwarded-Proto": []string{"http"}, }, - fallBackToHttps: true, + enforceHttps: true, }, want: &http_util.DomainCtx{ InstanceHost: "host.header", - Protocol: "http", + Protocol: "https", }, }, { name: "fallback to http", args: args{ - fallBackToHttps: false, + enforceHttps: false, }, want: &http_util.DomainCtx{ InstanceHost: "host.header", Protocol: "http", }, }, { - name: "fallback to https", + name: "enforce https", args: args{ - fallBackToHttps: true, + enforceHttps: true, }, want: &http_util.DomainCtx{ InstanceHost: "host.header", @@ -144,7 +144,7 @@ func Test_composeOrigin(t *testing.T) { h: http.Header{ "X-Forwarded-Host": []string{"x-forwarded.host"}, }, - fallBackToHttps: false, + enforceHttps: false, }, want: &http_util.DomainCtx{ InstanceHost: "x-forwarded.host", @@ -157,7 +157,7 @@ func Test_composeOrigin(t *testing.T) { "X-Forwarded-Proto": []string{"https"}, "X-Forwarded-Host": []string{"x-forwarded.host"}, }, - fallBackToHttps: false, + enforceHttps: false, }, want: &http_util.DomainCtx{ InstanceHost: "x-forwarded.host", @@ -170,7 +170,7 @@ func Test_composeOrigin(t *testing.T) { "Forwarded": []string{"host=forwarded.host"}, "X-Forwarded-Host": []string{"x-forwarded.host"}, }, - fallBackToHttps: false, + enforceHttps: false, }, want: &http_util.DomainCtx{ InstanceHost: "forwarded.host", @@ -183,7 +183,7 @@ func Test_composeOrigin(t *testing.T) { "Forwarded": []string{"host=forwarded.host"}, "X-Forwarded-Proto": []string{"https"}, }, - fallBackToHttps: false, + enforceHttps: false, }, want: &http_util.DomainCtx{ InstanceHost: "forwarded.host", @@ -198,10 +198,10 @@ func Test_composeOrigin(t *testing.T) { Host: "host.header", Header: tt.args.h, }, - tt.args.fallBackToHttps, + tt.args.enforceHttps, []string{http_util.Forwarded, http_util.ForwardedFor, http_util.ForwardedHost, http_util.ForwardedProto}, []string{"x-zitadel-public-host"}, - ), "headers: %+v, fallBackToHttps: %t", tt.args.h, tt.args.fallBackToHttps) + ), "headers: %+v, enforceHttps: %t", tt.args.h, tt.args.enforceHttps) }) } } From 046b165db85f397c4a437c4112be34695cfd5f58 Mon Sep 17 00:00:00 2001 From: Silvan <27845747+adlerhurst@users.noreply.github.com> Date: Wed, 28 May 2025 10:47:42 +0200 Subject: [PATCH 49/76] docs(a10016): add versions for v2.66 - v3 (#9908) # Which Problems Are Solved versions were missing in https://github.com/zitadel/zitadel/pull/9882 # How the Problems Are Solved added versions for 2.66.x, 2.67.x, 2.68.x, 2.69.x, 2.70.x, 2.71.x, 3.x # Additional Context can be merged after: - https://github.com/zitadel/zitadel/pull/9901 - https://github.com/zitadel/zitadel/pull/9903 - https://github.com/zitadel/zitadel/pull/9904 - https://github.com/zitadel/zitadel/pull/9907 - https://github.com/zitadel/zitadel/pull/9905 - https://github.com/zitadel/zitadel/pull/9906 - https://github.com/zitadel/zitadel/pull/9916 --- docs/docs/support/advisory/a10016.md | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/docs/docs/support/advisory/a10016.md b/docs/docs/support/advisory/a10016.md index 794c354e42..84dd1cd34c 100644 --- a/docs/docs/support/advisory/a10016.md +++ b/docs/docs/support/advisory/a10016.md @@ -4,15 +4,19 @@ title: Technical Advisory 10016 ## Date -Versions:[^1] - - v2.65.x: > v2.65.9 +- v2.66.x > v2.66.17 +- v2.67.x > v2.67.14 +- v2.68.x > v2.68.10 +- v2.69.x > v2.69.10 +- v2.70.x > v2.70.11 +- v2.71.x > v2.71.10 +- v3.x > v3.2.1 + 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. +Last updated: 2025-05-19 ## Description From 131f70db3423b80da1b038a822da59c908e4ffa6 Mon Sep 17 00:00:00 2001 From: Silvan <27845747+adlerhurst@users.noreply.github.com> Date: Wed, 28 May 2025 23:54:18 +0200 Subject: [PATCH 50/76] fix(eventstore): use decimal, correct mirror (#9914) # Eventstore fixes - `event.Position` used float64 before which can lead to [precision loss](https://github.com/golang/go/issues/47300). The type got replaced by [a type without precision loss](https://github.com/jackc/pgx-shopspring-decimal) - the handler reported the wrong error if the current state was updated and therefore took longer to retry failed events. # Mirror fixes - max age of auth requests can be configured to speed up copying data from `auth.auth_requests` table. Auth requests last updated before the set age will be ignored. Default is 1 month - notification projections are skipped because notifications should be sent by the source system. The projections are set to the latest position - ensure that mirror can be executed multiple times --------- Co-authored-by: Livio Spring --- cmd/mirror/event.go | 4 +- cmd/mirror/event_store.go | 14 ++++-- cmd/mirror/projections.go | 7 +++ go.mod | 2 + go.sum | 4 ++ .../eventsourcing/handler/handler.go | 16 +++++-- internal/api/oidc/key.go | 17 +++---- internal/api/saml/certificate.go | 17 +++---- .../eventsourcing/handler/handler.go | 16 +++++-- internal/database/database.go | 4 ++ internal/database/dialect/connections.go | 8 +++- internal/eventstore/event.go | 4 +- internal/eventstore/event_base.go | 5 +- internal/eventstore/eventstore.go | 17 +++++-- .../eventstore/eventstore_querier_test.go | 16 ++++--- internal/eventstore/eventstore_test.go | 17 +++---- .../eventstore/handler/v2/field_handler.go | 18 ++++--- internal/eventstore/handler/v2/handler.go | 32 +++++++++---- internal/eventstore/handler/v2/state.go | 8 ++-- internal/eventstore/handler/v2/state_test.go | 13 ++--- internal/eventstore/handler/v2/statement.go | 5 +- internal/eventstore/local_postgres_test.go | 19 ++++++-- internal/eventstore/read_model.go | 22 +++++---- internal/eventstore/repository/event.go | 5 +- .../repository/mock/repository.mock.go | 15 +++--- .../repository/mock/repository.mock.impl.go | 5 +- .../eventstore/repository/search_query.go | 10 ++-- .../repository/sql/local_postgres_test.go | 8 +++- .../eventstore/repository/sql/postgres.go | 14 +++--- .../repository/sql/postgres_test.go | 4 +- internal/eventstore/repository/sql/query.go | 25 +++++----- .../eventstore/repository/sql/query_test.go | 44 +++++++++-------- internal/eventstore/search_query.go | 24 +++++----- internal/eventstore/search_query_test.go | 4 +- internal/eventstore/v1/models/event.go | 6 ++- internal/eventstore/v3/event.go | 7 +-- internal/notification/projections.go | 20 ++++++++ internal/query/access_token.go | 5 +- internal/query/current_state.go | 11 +++-- internal/query/current_state_test.go | 8 ++-- internal/query/projection/projection.go | 38 +++++++++++---- internal/query/user_grant.go | 4 +- internal/query/user_membership.go | 4 +- internal/v2/database/number_filter.go | 3 +- internal/v2/eventstore/event_store.go | 6 ++- internal/v2/eventstore/postgres/push_test.go | 24 +++++----- internal/v2/eventstore/postgres/query_test.go | 48 ++++++++++--------- internal/v2/eventstore/query.go | 6 ++- internal/v2/eventstore/query_test.go | 26 +++++----- .../v2/readmodel/last_successful_mirror.go | 7 ++- internal/v2/system/mirror/succeeded.go | 6 ++- 51 files changed, 436 insertions(+), 236 deletions(-) diff --git a/cmd/mirror/event.go b/cmd/mirror/event.go index d513990e10..567fb659a2 100644 --- a/cmd/mirror/event.go +++ b/cmd/mirror/event.go @@ -3,6 +3,8 @@ package mirror import ( "context" + "github.com/shopspring/decimal" + "github.com/zitadel/zitadel/internal/v2/eventstore" "github.com/zitadel/zitadel/internal/v2/readmodel" "github.com/zitadel/zitadel/internal/v2/system" @@ -29,7 +31,7 @@ func queryLastSuccessfulMigration(ctx context.Context, destinationES *eventstore return lastSuccess, nil } -func writeMigrationSucceeded(ctx context.Context, destinationES *eventstore.EventStore, id, source string, position float64) error { +func writeMigrationSucceeded(ctx context.Context, destinationES *eventstore.EventStore, id, source string, position decimal.Decimal) error { return destinationES.Push( ctx, eventstore.NewPushIntent( diff --git a/cmd/mirror/event_store.go b/cmd/mirror/event_store.go index 41c529c025..be14abe340 100644 --- a/cmd/mirror/event_store.go +++ b/cmd/mirror/event_store.go @@ -8,7 +8,9 @@ import ( "io" "time" + "github.com/jackc/pgx/v5/pgconn" "github.com/jackc/pgx/v5/stdlib" + "github.com/shopspring/decimal" "github.com/spf13/cobra" "github.com/spf13/viper" "github.com/zitadel/logging" @@ -89,7 +91,7 @@ func copyEvents(ctx context.Context, source, dest *db.DB, bulkSize uint32) { previousMigration, err := queryLastSuccessfulMigration(ctx, destinationES, source.DatabaseName()) logging.OnError(err).Fatal("unable to query latest successful migration") - var maxPosition float64 + var maxPosition decimal.Decimal err = source.QueryRowContext(ctx, func(row *sql.Row) error { return row.Scan(&maxPosition) @@ -101,7 +103,7 @@ func copyEvents(ctx context.Context, source, dest *db.DB, bulkSize uint32) { logging.WithFields("from", previousMigration.Position, "to", maxPosition).Info("start event migration") nextPos := make(chan bool, 1) - pos := make(chan float64, 1) + pos := make(chan decimal.Decimal, 1) errs := make(chan error, 3) go func() { @@ -152,7 +154,7 @@ func copyEvents(ctx context.Context, source, dest *db.DB, bulkSize uint32) { go func() { defer close(pos) for range nextPos { - var position float64 + var position decimal.Decimal err := dest.QueryRowContext( ctx, func(row *sql.Row) error { @@ -175,6 +177,10 @@ func copyEvents(ctx context.Context, source, dest *db.DB, bulkSize uint32) { tag, err := conn.PgConn().CopyFrom(ctx, reader, "COPY eventstore.events2 FROM STDIN") eventCount = tag.RowsAffected() if err != nil { + pgErr := new(pgconn.PgError) + errors.As(err, &pgErr) + + logging.WithError(err).WithField("pg_err_details", pgErr.Detail).Error("unable to copy events into destination") return zerrors.ThrowUnknown(err, "MIGRA-DTHi7", "unable to copy events into destination") } @@ -187,7 +193,7 @@ func copyEvents(ctx context.Context, source, dest *db.DB, bulkSize uint32) { logging.WithFields("took", time.Since(start), "count", eventCount).Info("events migrated") } -func writeCopyEventsDone(ctx context.Context, es *eventstore.EventStore, id, source string, position float64, errs <-chan error) { +func writeCopyEventsDone(ctx context.Context, es *eventstore.EventStore, id, source string, position decimal.Decimal, errs <-chan error) { joinedErrs := make([]error, 0, len(errs)) for err := range errs { joinedErrs = append(joinedErrs, err) diff --git a/cmd/mirror/projections.go b/cmd/mirror/projections.go index 4e12b29748..0ff4356d6f 100644 --- a/cmd/mirror/projections.go +++ b/cmd/mirror/projections.go @@ -296,6 +296,13 @@ func execProjections(ctx context.Context, instances <-chan string, failedInstanc continue } + err = projection.ProjectInstanceFields(ctx) + if err != nil { + logging.WithFields("instance", instance).WithError(err).Info("trigger fields failed") + failedInstances <- instance + continue + } + err = auth_handler.ProjectInstance(ctx) if err != nil { logging.WithFields("instance", instance).WithError(err).Info("trigger auth handler failed") diff --git a/go.mod b/go.mod index ec86708942..c1cbf2dd77 100644 --- a/go.mod +++ b/go.mod @@ -45,6 +45,7 @@ require ( github.com/h2non/gock v1.2.0 github.com/hashicorp/golang-lru/v2 v2.0.7 github.com/improbable-eng/grpc-web v0.15.0 + github.com/jackc/pgx-shopspring-decimal v0.0.0-20220624020537-1d36b5a1853e github.com/jackc/pgx/v5 v5.7.5 github.com/jarcoal/jpath v0.0.0-20140328210829-f76b8b2dbf52 github.com/jinzhu/gorm v1.9.16 @@ -65,6 +66,7 @@ require ( github.com/riverqueue/river/rivertype v0.22.0 github.com/rs/cors v1.11.1 github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 + github.com/shopspring/decimal v1.3.1 github.com/sony/gobreaker/v2 v2.1.0 github.com/sony/sonyflake v1.2.1 github.com/spf13/cobra v1.9.1 diff --git a/go.sum b/go.sum index 6d54730acd..cc3bc35841 100644 --- a/go.sum +++ b/go.sum @@ -442,6 +442,8 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx-shopspring-decimal v0.0.0-20220624020537-1d36b5a1853e h1:i3gQ/Zo7sk4LUVbsAjTNeC4gIjoPNIZVzs4EXstssV4= +github.com/jackc/pgx-shopspring-decimal v0.0.0-20220624020537-1d36b5a1853e/go.mod h1:zUHglCZ4mpDUPgIwqEKoba6+tcUQzRdb1+DPTuYe9pI= github.com/jackc/pgx/v5 v5.7.5 h1:JHGfMnQY+IEtGM63d+NGMjoRpysB2JBwDr5fsngwmJs= github.com/jackc/pgx/v5 v5.7.5/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= @@ -705,6 +707,8 @@ github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0 github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4= github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= +github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= diff --git a/internal/admin/repository/eventsourcing/handler/handler.go b/internal/admin/repository/eventsourcing/handler/handler.go index 76584b55b0..b38e890e66 100644 --- a/internal/admin/repository/eventsourcing/handler/handler.go +++ b/internal/admin/repository/eventsourcing/handler/handler.go @@ -2,9 +2,11 @@ package handler import ( "context" + "errors" "fmt" "time" + "github.com/jackc/pgx/v5/pgconn" "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/admin/repository/eventsourcing/view" @@ -63,9 +65,17 @@ func Start(ctx context.Context) { func ProjectInstance(ctx context.Context) error { 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 + for { + _, err := projection.Trigger(ctx) + if err == nil { + break + } + var pgErr *pgconn.PgError + errors.As(err, &pgErr) + if pgErr.Code != database.PgUniqueConstraintErrorCode { + return err + } + logging.WithFields("name", projection.ProjectionName(), "instance", authz.GetInstance(ctx).InstanceID()).WithError(err).Debug("admin projection failed because of unique constraint, retrying") } logging.WithFields("name", projection.ProjectionName(), "instance", authz.GetInstance(ctx).InstanceID(), "index", fmt.Sprintf("%d/%d", i, len(projections))).Info("admin projection done") } diff --git a/internal/api/oidc/key.go b/internal/api/oidc/key.go index 81f3b1c466..852bbc7db8 100644 --- a/internal/api/oidc/key.go +++ b/internal/api/oidc/key.go @@ -11,6 +11,7 @@ import ( "github.com/go-jose/go-jose/v4" "github.com/jonboulle/clockwork" "github.com/muhlemmer/gu" + "github.com/shopspring/decimal" "github.com/zitadel/logging" "github.com/zitadel/oidc/v3/pkg/op" @@ -350,14 +351,14 @@ func (o *OPStorage) getSigningKey(ctx context.Context) (op.SigningKey, error) { if len(keys.Keys) > 0 { return PrivateKeyToSigningKey(SelectSigningKey(keys.Keys), o.encAlg) } - var position float64 + var position decimal.Decimal if keys.State != nil { position = keys.State.Position } return nil, o.refreshSigningKey(ctx, position) } -func (o *OPStorage) refreshSigningKey(ctx context.Context, position float64) error { +func (o *OPStorage) refreshSigningKey(ctx context.Context, position decimal.Decimal) error { ok, err := o.ensureIsLatestKey(ctx, position) if err != nil || !ok { return zerrors.ThrowInternal(err, "OIDC-ASfh3", "cannot ensure that projection is up to date") @@ -369,12 +370,12 @@ func (o *OPStorage) refreshSigningKey(ctx context.Context, position float64) err return zerrors.ThrowInternal(nil, "OIDC-Df1bh", "") } -func (o *OPStorage) ensureIsLatestKey(ctx context.Context, position float64) (bool, error) { - maxSequence, err := o.getMaxKeySequence(ctx) +func (o *OPStorage) ensureIsLatestKey(ctx context.Context, position decimal.Decimal) (bool, error) { + maxSequence, err := o.getMaxKeyPosition(ctx) if err != nil { return false, fmt.Errorf("error retrieving new events: %w", err) } - return position >= maxSequence, nil + return position.GreaterThanOrEqual(maxSequence), nil } func PrivateKeyToSigningKey(key query.PrivateKey, algorithm crypto.EncryptionAlgorithm) (_ op.SigningKey, err error) { @@ -412,9 +413,9 @@ func (o *OPStorage) lockAndGenerateSigningKeyPair(ctx context.Context) error { return o.command.GenerateSigningKeyPair(setOIDCCtx(ctx), "RS256") } -func (o *OPStorage) getMaxKeySequence(ctx context.Context) (float64, error) { - return o.eventstore.LatestSequence(ctx, - eventstore.NewSearchQueryBuilder(eventstore.ColumnsMaxSequence). +func (o *OPStorage) getMaxKeyPosition(ctx context.Context) (decimal.Decimal, error) { + return o.eventstore.LatestPosition(ctx, + eventstore.NewSearchQueryBuilder(eventstore.ColumnsMaxPosition). ResourceOwner(authz.GetInstance(ctx).InstanceID()). AwaitOpenTransactions(). AddQuery(). diff --git a/internal/api/saml/certificate.go b/internal/api/saml/certificate.go index ff130f7709..14752cd5cd 100644 --- a/internal/api/saml/certificate.go +++ b/internal/api/saml/certificate.go @@ -6,6 +6,7 @@ import ( "time" "github.com/go-jose/go-jose/v4" + "github.com/shopspring/decimal" "github.com/zitadel/logging" "github.com/zitadel/saml/pkg/provider/key" @@ -76,7 +77,7 @@ func (p *Storage) getCertificateAndKey(ctx context.Context, usage crypto.KeyUsag return p.certificateToCertificateAndKey(selectCertificate(certs.Certificates)) } - var position float64 + var position decimal.Decimal if certs.State != nil { position = certs.State.Position } @@ -87,7 +88,7 @@ func (p *Storage) getCertificateAndKey(ctx context.Context, usage crypto.KeyUsag func (p *Storage) refreshCertificate( ctx context.Context, usage crypto.KeyUsage, - position float64, + position decimal.Decimal, ) error { ok, err := p.ensureIsLatestCertificate(ctx, position) if err != nil { @@ -103,12 +104,12 @@ func (p *Storage) refreshCertificate( return nil } -func (p *Storage) ensureIsLatestCertificate(ctx context.Context, position float64) (bool, error) { - maxSequence, err := p.getMaxKeySequence(ctx) +func (p *Storage) ensureIsLatestCertificate(ctx context.Context, position decimal.Decimal) (bool, error) { + maxSequence, err := p.getMaxKeyPosition(ctx) if err != nil { return false, fmt.Errorf("error retrieving new events: %w", err) } - return position >= maxSequence, nil + return position.GreaterThanOrEqual(maxSequence), nil } func (p *Storage) lockAndGenerateCertificateAndKey(ctx context.Context, usage crypto.KeyUsage) error { @@ -151,9 +152,9 @@ func (p *Storage) lockAndGenerateCertificateAndKey(ctx context.Context, usage cr } } -func (p *Storage) getMaxKeySequence(ctx context.Context) (float64, error) { - return p.eventstore.LatestSequence(ctx, - eventstore.NewSearchQueryBuilder(eventstore.ColumnsMaxSequence). +func (p *Storage) getMaxKeyPosition(ctx context.Context) (decimal.Decimal, error) { + return p.eventstore.LatestPosition(ctx, + eventstore.NewSearchQueryBuilder(eventstore.ColumnsMaxPosition). ResourceOwner(authz.GetInstance(ctx).InstanceID()). AwaitOpenTransactions(). AddQuery(). diff --git a/internal/auth/repository/eventsourcing/handler/handler.go b/internal/auth/repository/eventsourcing/handler/handler.go index 74a27a8312..0c151bb412 100644 --- a/internal/auth/repository/eventsourcing/handler/handler.go +++ b/internal/auth/repository/eventsourcing/handler/handler.go @@ -2,9 +2,11 @@ package handler import ( "context" + "errors" "fmt" "time" + "github.com/jackc/pgx/v5/pgconn" "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/api/authz" @@ -78,9 +80,17 @@ func Projections() []*handler2.Handler { func ProjectInstance(ctx context.Context) error { 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 + for { + _, err := projection.Trigger(ctx) + if err == nil { + break + } + var pgErr *pgconn.PgError + errors.As(err, &pgErr) + if pgErr.Code != database.PgUniqueConstraintErrorCode { + return err + } + logging.WithFields("name", projection.ProjectionName(), "instance", authz.GetInstance(ctx).InstanceID()).WithError(err).Debug("auth projection failed because of unique constraint, retrying") } logging.WithFields("name", projection.ProjectionName(), "instance", authz.GetInstance(ctx).InstanceID(), "index", fmt.Sprintf("%d/%d", i, len(projections))).Info("auth projection done") } diff --git a/internal/database/database.go b/internal/database/database.go index ddc26a7961..b40715d6b5 100644 --- a/internal/database/database.go +++ b/internal/database/database.go @@ -64,6 +64,10 @@ func CloseTransaction(tx Tx, err error) error { return commitErr } +const ( + PgUniqueConstraintErrorCode = "23505" +) + type Config struct { Dialects map[string]interface{} `mapstructure:",remain"` connector dialect.Connector diff --git a/internal/database/dialect/connections.go b/internal/database/dialect/connections.go index 11b2681fea..a5c90b4059 100644 --- a/internal/database/dialect/connections.go +++ b/internal/database/dialect/connections.go @@ -5,6 +5,7 @@ import ( "errors" "reflect" + pgxdecimal "github.com/jackc/pgx-shopspring-decimal" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgtype" ) @@ -23,7 +24,12 @@ type ConnectionConfig struct { AfterRelease []func(c *pgx.Conn) error } -var afterConnectFuncs []func(ctx context.Context, c *pgx.Conn) error +var afterConnectFuncs = []func(ctx context.Context, c *pgx.Conn) error{ + func(ctx context.Context, c *pgx.Conn) error { + pgxdecimal.Register(c.TypeMap()) + return nil + }, +} func RegisterAfterConnect(f func(ctx context.Context, c *pgx.Conn) error) { afterConnectFuncs = append(afterConnectFuncs, f) diff --git a/internal/eventstore/event.go b/internal/eventstore/event.go index 3df096f069..656a02f33d 100644 --- a/internal/eventstore/event.go +++ b/internal/eventstore/event.go @@ -5,6 +5,8 @@ import ( "reflect" "time" + "github.com/shopspring/decimal" + "github.com/zitadel/zitadel/internal/zerrors" ) @@ -44,7 +46,7 @@ type Event interface { // CreatedAt is the time the event was created at CreatedAt() time.Time // Position is the global position of the event - Position() float64 + Position() decimal.Decimal // Unmarshal parses the payload and stores the result // in the value pointed to by ptr. If ptr is nil or not a pointer, diff --git a/internal/eventstore/event_base.go b/internal/eventstore/event_base.go index ed81e95320..6a911bc0eb 100644 --- a/internal/eventstore/event_base.go +++ b/internal/eventstore/event_base.go @@ -7,6 +7,7 @@ import ( "strings" "time" + "github.com/shopspring/decimal" "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/api/authz" @@ -25,7 +26,7 @@ type BaseEvent struct { Agg *Aggregate `json:"-"` Seq uint64 - Pos float64 + Pos decimal.Decimal Creation time.Time previousAggregateSequence uint64 previousAggregateTypeSequence uint64 @@ -38,7 +39,7 @@ type BaseEvent struct { } // Position implements Event. -func (e *BaseEvent) Position() float64 { +func (e *BaseEvent) Position() decimal.Decimal { return e.Pos } diff --git a/internal/eventstore/eventstore.go b/internal/eventstore/eventstore.go index 4954df86c8..8a8d32bc43 100644 --- a/internal/eventstore/eventstore.go +++ b/internal/eventstore/eventstore.go @@ -7,6 +7,7 @@ import ( "time" "github.com/jackc/pgx/v5/pgconn" + "github.com/shopspring/decimal" "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/api/authz" @@ -14,6 +15,12 @@ import ( "github.com/zitadel/zitadel/internal/zerrors" ) +func init() { + // this is needed to ensure that position is marshaled as a number + // otherwise it will be marshaled as a string + decimal.MarshalJSONWithoutQuotes = true +} + // Eventstore abstracts all functions needed to store valid events // and filters the stored events type Eventstore struct { @@ -229,11 +236,11 @@ func (es *Eventstore) FilterToReducer(ctx context.Context, searchQuery *SearchQu }) } -// LatestSequence filters the latest sequence for the given search query -func (es *Eventstore) LatestSequence(ctx context.Context, queryFactory *SearchQueryBuilder) (float64, error) { +// LatestPosition filters the latest position for the given search query +func (es *Eventstore) LatestPosition(ctx context.Context, queryFactory *SearchQueryBuilder) (decimal.Decimal, error) { queryFactory.InstanceID(authz.GetInstance(ctx).InstanceID()) - return es.querier.LatestSequence(ctx, queryFactory) + return es.querier.LatestPosition(ctx, queryFactory) } // InstanceIDs returns the distinct instance ids found by the search query @@ -265,8 +272,8 @@ type Querier interface { Health(ctx context.Context) error // FilterToReducer calls r for every event returned from the storage FilterToReducer(ctx context.Context, searchQuery *SearchQueryBuilder, r Reducer) error - // LatestSequence returns the latest sequence found by the search query - LatestSequence(ctx context.Context, queryFactory *SearchQueryBuilder) (float64, error) + // LatestPosition returns the latest position found by the search query + LatestPosition(ctx context.Context, queryFactory *SearchQueryBuilder) (decimal.Decimal, error) // InstanceIDs returns the instance ids found by the search query InstanceIDs(ctx context.Context, queryFactory *SearchQueryBuilder) ([]string, error) // Client returns the underlying database connection diff --git a/internal/eventstore/eventstore_querier_test.go b/internal/eventstore/eventstore_querier_test.go index 3f23c5da75..88797a835e 100644 --- a/internal/eventstore/eventstore_querier_test.go +++ b/internal/eventstore/eventstore_querier_test.go @@ -4,6 +4,8 @@ import ( "context" "testing" + "github.com/shopspring/decimal" + "github.com/zitadel/zitadel/internal/eventstore" ) @@ -131,7 +133,7 @@ func TestEventstore_Filter(t *testing.T) { } } -func TestEventstore_LatestSequence(t *testing.T) { +func TestEventstore_LatestPosition(t *testing.T) { type args struct { searchQuery *eventstore.SearchQueryBuilder } @@ -139,7 +141,7 @@ func TestEventstore_LatestSequence(t *testing.T) { existingEvents []eventstore.Command } type res struct { - sequence float64 + position decimal.Decimal } tests := []struct { name string @@ -151,7 +153,7 @@ func TestEventstore_LatestSequence(t *testing.T) { { name: "aggregate type filter no sequence", args: args{ - searchQuery: eventstore.NewSearchQueryBuilder(eventstore.ColumnsMaxSequence). + searchQuery: eventstore.NewSearchQueryBuilder(eventstore.ColumnsMaxPosition). AddQuery(). AggregateTypes("not found"). Builder(), @@ -168,7 +170,7 @@ func TestEventstore_LatestSequence(t *testing.T) { { name: "aggregate type filter sequence", args: args{ - searchQuery: eventstore.NewSearchQueryBuilder(eventstore.ColumnsMaxSequence). + searchQuery: eventstore.NewSearchQueryBuilder(eventstore.ColumnsMaxPosition). AddQuery(). AggregateTypes(eventstore.AggregateType(t.Name())). Builder(), @@ -202,12 +204,12 @@ func TestEventstore_LatestSequence(t *testing.T) { return } - sequence, err := db.LatestSequence(context.Background(), tt.args.searchQuery) + position, err := db.LatestPosition(context.Background(), tt.args.searchQuery) if (err != nil) != tt.wantErr { t.Errorf("eventstore.query() error = %v, wantErr %v", err, tt.wantErr) } - if tt.res.sequence > sequence { - t.Errorf("eventstore.query() expected sequence: %v got %v", tt.res.sequence, sequence) + if tt.res.position.GreaterThan(position) { + t.Errorf("eventstore.query() expected position: %v got %v", tt.res.position, position) } }) } diff --git a/internal/eventstore/eventstore_test.go b/internal/eventstore/eventstore_test.go index 9e1aa77db1..5452572faa 100644 --- a/internal/eventstore/eventstore_test.go +++ b/internal/eventstore/eventstore_test.go @@ -9,6 +9,7 @@ import ( "time" "github.com/jackc/pgx/v5/pgconn" + "github.com/shopspring/decimal" "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/service" @@ -397,7 +398,7 @@ func (repo *testPusher) Push(_ context.Context, _ database.ContextQueryExecuter, type testQuerier struct { events []Event - sequence float64 + sequence decimal.Decimal instances []string err error t *testing.T @@ -430,9 +431,9 @@ func (repo *testQuerier) FilterToReducer(ctx context.Context, searchQuery *Searc return nil } -func (repo *testQuerier) LatestSequence(ctx context.Context, queryFactory *SearchQueryBuilder) (float64, error) { +func (repo *testQuerier) LatestPosition(ctx context.Context, queryFactory *SearchQueryBuilder) (decimal.Decimal, error) { if repo.err != nil { - return 0, repo.err + return decimal.Decimal{}, repo.err } return repo.sequence, nil } @@ -1076,7 +1077,7 @@ func TestEventstore_FilterEvents(t *testing.T) { } } -func TestEventstore_LatestSequence(t *testing.T) { +func TestEventstore_LatestPosition(t *testing.T) { type args struct { query *SearchQueryBuilder } @@ -1096,7 +1097,7 @@ func TestEventstore_LatestSequence(t *testing.T) { name: "no events", args: args{ query: &SearchQueryBuilder{ - columns: ColumnsMaxSequence, + columns: ColumnsMaxPosition, queries: []*SearchQuery{ { builder: &SearchQueryBuilder{}, @@ -1119,7 +1120,7 @@ func TestEventstore_LatestSequence(t *testing.T) { name: "repo error", args: args{ query: &SearchQueryBuilder{ - columns: ColumnsMaxSequence, + columns: ColumnsMaxPosition, queries: []*SearchQuery{ { builder: &SearchQueryBuilder{}, @@ -1142,7 +1143,7 @@ func TestEventstore_LatestSequence(t *testing.T) { name: "found events", args: args{ query: &SearchQueryBuilder{ - columns: ColumnsMaxSequence, + columns: ColumnsMaxPosition, queries: []*SearchQuery{ { builder: &SearchQueryBuilder{}, @@ -1168,7 +1169,7 @@ func TestEventstore_LatestSequence(t *testing.T) { querier: tt.fields.repo, } - _, err := es.LatestSequence(context.Background(), tt.args.query) + _, err := es.LatestPosition(context.Background(), tt.args.query) if (err != nil) != tt.res.wantErr { t.Errorf("Eventstore.aggregatesToEvents() error = %v, wantErr %v", err, tt.res.wantErr) } diff --git a/internal/eventstore/handler/v2/field_handler.go b/internal/eventstore/handler/v2/field_handler.go index ad309ac790..3c25731c83 100644 --- a/internal/eventstore/handler/v2/field_handler.go +++ b/internal/eventstore/handler/v2/field_handler.go @@ -8,6 +8,7 @@ import ( "time" "github.com/jackc/pgx/v5/pgconn" + "github.com/shopspring/decimal" "github.com/zitadel/zitadel/internal/eventstore" ) @@ -126,10 +127,15 @@ func (h *FieldHandler) processEvents(ctx context.Context, config *triggerConfig) return additionalIteration, err } // stop execution if currentState.eventTimestamp >= config.maxCreatedAt - if config.maxPosition != 0 && currentState.position >= config.maxPosition { + if !config.maxPosition.IsZero() && currentState.position.GreaterThanOrEqual(config.maxPosition) { return false, nil } + if config.minPosition.GreaterThan(decimal.NewFromInt(0)) { + currentState.position = config.minPosition + currentState.offset = 0 + } + events, additionalIteration, err := h.fetchEvents(ctx, tx, currentState) if err != nil { return additionalIteration, err @@ -159,7 +165,7 @@ func (h *FieldHandler) fetchEvents(ctx context.Context, tx *sql.Tx, currentState idx, offset := skipPreviouslyReducedEvents(events, currentState) - if currentState.position == events[len(events)-1].Position() { + if currentState.position.Equal(events[len(events)-1].Position()) { offset += currentState.offset } currentState.position = events[len(events)-1].Position() @@ -179,7 +185,7 @@ func (h *FieldHandler) fetchEvents(ctx context.Context, tx *sql.Tx, currentState fillFieldsEvents := make([]eventstore.FillFieldsEvent, len(events)) highestPosition := events[len(events)-1].Position() for i, event := range events { - if event.Position() == highestPosition { + if event.Position().Equal(highestPosition) { offset++ } fillFieldsEvents[i] = event.(eventstore.FillFieldsEvent) @@ -189,14 +195,14 @@ func (h *FieldHandler) fetchEvents(ctx context.Context, tx *sql.Tx, currentState } func skipPreviouslyReducedEvents(events []eventstore.Event, currentState *state) (index int, offset uint32) { - var position float64 + var position decimal.Decimal for i, event := range events { - if event.Position() != position { + if !event.Position().Equal(position) { offset = 0 position = event.Position() } offset++ - if event.Position() == currentState.position && + if event.Position().Equal(currentState.position) && event.Aggregate().ID == currentState.aggregateID && event.Aggregate().Type == currentState.aggregateType && event.Sequence() == currentState.sequence { diff --git a/internal/eventstore/handler/v2/handler.go b/internal/eventstore/handler/v2/handler.go index fb696ad090..fd8b206b38 100644 --- a/internal/eventstore/handler/v2/handler.go +++ b/internal/eventstore/handler/v2/handler.go @@ -4,13 +4,13 @@ import ( "context" "database/sql" "errors" - "math" "math/rand" "slices" "sync" "time" "github.com/jackc/pgx/v5/pgconn" + "github.com/shopspring/decimal" "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/api/authz" @@ -395,7 +395,8 @@ func (h *Handler) existingInstances(ctx context.Context) ([]string, error) { type triggerConfig struct { awaitRunning bool - maxPosition float64 + maxPosition decimal.Decimal + minPosition decimal.Decimal } type TriggerOpt func(conf *triggerConfig) @@ -406,12 +407,18 @@ func WithAwaitRunning() TriggerOpt { } } -func WithMaxPosition(position float64) TriggerOpt { +func WithMaxPosition(position decimal.Decimal) TriggerOpt { return func(conf *triggerConfig) { conf.maxPosition = position } } +func WithMinPosition(position decimal.Decimal) TriggerOpt { + return func(conf *triggerConfig) { + conf.minPosition = position + } +} + func (h *Handler) Trigger(ctx context.Context, opts ...TriggerOpt) (_ context.Context, err error) { config := new(triggerConfig) for _, opt := range opts { @@ -520,10 +527,15 @@ func (h *Handler) processEvents(ctx context.Context, config *triggerConfig) (add return additionalIteration, err } // stop execution if currentState.position >= config.maxPosition - if config.maxPosition != 0 && currentState.position >= config.maxPosition { + if !config.maxPosition.Equal(decimal.Decimal{}) && currentState.position.GreaterThanOrEqual(config.maxPosition) { return false, nil } + if config.minPosition.GreaterThan(decimal.NewFromInt(0)) { + currentState.position = config.minPosition + currentState.offset = 0 + } + var statements []*Statement statements, additionalIteration, err = h.generateStatements(ctx, tx, currentState) if err != nil { @@ -565,7 +577,10 @@ func (h *Handler) processEvents(ctx context.Context, config *triggerConfig) (add currentState.sequence = statements[lastProcessedIndex].Sequence currentState.eventTimestamp = statements[lastProcessedIndex].CreationDate - err = h.setState(tx, currentState) + setStateErr := h.setState(tx, currentState) + if setStateErr != nil { + err = setStateErr + } return additionalIteration, err } @@ -615,7 +630,7 @@ func (h *Handler) generateStatements(ctx context.Context, tx *sql.Tx, currentSta func skipPreviouslyReducedStatements(statements []*Statement, currentState *state) int { for i, statement := range statements { - if statement.Position == currentState.position && + if statement.Position.Equal(currentState.position) && statement.Aggregate.ID == currentState.aggregateID && statement.Aggregate.Type == currentState.aggregateType && statement.Sequence == currentState.sequence { @@ -678,9 +693,8 @@ func (h *Handler) eventQuery(currentState *state) *eventstore.SearchQueryBuilder OrderAsc(). InstanceID(currentState.instanceID) - if currentState.position > 0 { - // decrease position by 10 because builder.PositionAfter filters for position > and we need position >= - builder = builder.PositionAfter(math.Float64frombits(math.Float64bits(currentState.position) - 10)) + if currentState.position.GreaterThan(decimal.Decimal{}) { + builder = builder.PositionAtLeast(currentState.position) if currentState.offset > 0 { builder = builder.Offset(currentState.offset) } diff --git a/internal/eventstore/handler/v2/state.go b/internal/eventstore/handler/v2/state.go index d3b6953488..c4afaed204 100644 --- a/internal/eventstore/handler/v2/state.go +++ b/internal/eventstore/handler/v2/state.go @@ -7,6 +7,8 @@ import ( "errors" "time" + "github.com/shopspring/decimal" + "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/zerrors" @@ -14,7 +16,7 @@ import ( type state struct { instanceID string - position float64 + position decimal.Decimal eventTimestamp time.Time aggregateType eventstore.AggregateType aggregateID string @@ -45,7 +47,7 @@ func (h *Handler) currentState(ctx context.Context, tx *sql.Tx, config *triggerC aggregateType = new(sql.NullString) sequence = new(sql.NullInt64) timestamp = new(sql.NullTime) - position = new(sql.NullFloat64) + position = new(decimal.NullDecimal) offset = new(sql.NullInt64) ) @@ -75,7 +77,7 @@ func (h *Handler) currentState(ctx context.Context, tx *sql.Tx, config *triggerC currentState.aggregateType = eventstore.AggregateType(aggregateType.String) currentState.sequence = uint64(sequence.Int64) currentState.eventTimestamp = timestamp.Time - currentState.position = position.Float64 + currentState.position = position.Decimal // psql does not provide unsigned numbers so we work around it currentState.offset = uint32(offset.Int64) return currentState, nil diff --git a/internal/eventstore/handler/v2/state_test.go b/internal/eventstore/handler/v2/state_test.go index cc5fb1fbab..ef91d78e55 100644 --- a/internal/eventstore/handler/v2/state_test.go +++ b/internal/eventstore/handler/v2/state_test.go @@ -11,6 +11,7 @@ import ( "time" "github.com/jackc/pgx/v5/pgconn" + "github.com/shopspring/decimal" "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/database/mock" @@ -166,7 +167,7 @@ func TestHandler_updateLastUpdated(t *testing.T) { updatedState: &state{ instanceID: "instance", eventTimestamp: time.Now(), - position: 42, + position: decimal.NewFromInt(42), }, }, isErr: func(t *testing.T, err error) { @@ -192,7 +193,7 @@ func TestHandler_updateLastUpdated(t *testing.T) { updatedState: &state{ instanceID: "instance", eventTimestamp: time.Now(), - position: 42, + position: decimal.NewFromInt(42), }, }, isErr: func(t *testing.T, err error) { @@ -217,7 +218,7 @@ func TestHandler_updateLastUpdated(t *testing.T) { eventstore.AggregateType("aggregate type"), uint64(42), mock.AnyType[time.Time]{}, - float64(42), + decimal.NewFromInt(42), uint32(0), ), mock.WithExecRowsAffected(1), @@ -228,7 +229,7 @@ func TestHandler_updateLastUpdated(t *testing.T) { updatedState: &state{ instanceID: "instance", eventTimestamp: time.Now(), - position: 42, + position: decimal.NewFromInt(42), aggregateType: "aggregate type", aggregateID: "aggregate id", sequence: 42, @@ -397,7 +398,7 @@ func TestHandler_currentState(t *testing.T) { "aggregate type", int64(42), testTime, - float64(42), + decimal.NewFromInt(42).String(), uint16(10), }, }, @@ -412,7 +413,7 @@ func TestHandler_currentState(t *testing.T) { currentState: &state{ instanceID: "instance", eventTimestamp: testTime, - position: 42, + position: decimal.NewFromInt(42), aggregateType: "aggregate type", aggregateID: "aggregate id", sequence: 42, diff --git a/internal/eventstore/handler/v2/statement.go b/internal/eventstore/handler/v2/statement.go index a02e5d3580..5024c8c945 100644 --- a/internal/eventstore/handler/v2/statement.go +++ b/internal/eventstore/handler/v2/statement.go @@ -9,6 +9,7 @@ import ( "strings" "time" + "github.com/shopspring/decimal" "github.com/zitadel/logging" "golang.org/x/exp/constraints" @@ -52,7 +53,7 @@ func (h *Handler) eventsToStatements(tx *sql.Tx, events []eventstore.Event, curr return statements, err } offset++ - if previousPosition != event.Position() { + if !previousPosition.Equal(event.Position()) { // offset is 1 because we want to skip this event offset = 1 } @@ -82,7 +83,7 @@ func (h *Handler) reduce(event eventstore.Event) (*Statement, error) { type Statement struct { Aggregate *eventstore.Aggregate Sequence uint64 - Position float64 + Position decimal.Decimal CreationDate time.Time offset uint32 diff --git a/internal/eventstore/local_postgres_test.go b/internal/eventstore/local_postgres_test.go index d75292b3ff..fdb8b4f516 100644 --- a/internal/eventstore/local_postgres_test.go +++ b/internal/eventstore/local_postgres_test.go @@ -2,12 +2,13 @@ package eventstore_test import ( "context" - "database/sql" "encoding/json" "os" "testing" "time" + pgxdecimal "github.com/jackc/pgx-shopspring-decimal" + "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" "github.com/jackc/pgx/v5/stdlib" "github.com/zitadel/logging" @@ -40,7 +41,10 @@ func TestMain(m *testing.M) { connConfig, err := pgxpool.ParseConfig(config.GetConnectionURL()) logging.OnError(err).Fatal("unable to parse db url") - connConfig.AfterConnect = new_es.RegisterEventstoreTypes + connConfig.AfterConnect = func(ctx context.Context, conn *pgx.Conn) error { + pgxdecimal.Register(conn.TypeMap()) + return new_es.RegisterEventstoreTypes(ctx, conn) + } pool, err := pgxpool.NewWithConfig(context.Background(), connConfig) logging.OnError(err).Fatal("unable to create db pool") @@ -101,10 +105,19 @@ func initDB(ctx context.Context, db *database.DB) error { } func connectLocalhost() (*database.DB, error) { - client, err := sql.Open("pgx", "postgresql://postgres@localhost:5432/postgres?sslmode=disable") + config, err := pgxpool.ParseConfig("postgresql://postgres@localhost:5432/postgres?sslmode=disable") if err != nil { return nil, err } + config.AfterConnect = func(ctx context.Context, conn *pgx.Conn) error { + pgxdecimal.Register(conn.TypeMap()) + return new_es.RegisterEventstoreTypes(ctx, conn) + } + pool, err := pgxpool.NewWithConfig(context.Background(), config) + if err != nil { + return nil, err + } + client := stdlib.OpenDBFromPool(pool) if err = client.Ping(); err != nil { return nil, err } diff --git a/internal/eventstore/read_model.go b/internal/eventstore/read_model.go index d2c755cc3a..ae77275732 100644 --- a/internal/eventstore/read_model.go +++ b/internal/eventstore/read_model.go @@ -1,19 +1,23 @@ package eventstore -import "time" +import ( + "time" + + "github.com/shopspring/decimal" +) // ReadModel is the minimum representation of a read model. // It implements a basic reducer // it might be saved in a database or in memory type ReadModel struct { - AggregateID string `json:"-"` - ProcessedSequence uint64 `json:"-"` - CreationDate time.Time `json:"-"` - ChangeDate time.Time `json:"-"` - Events []Event `json:"-"` - ResourceOwner string `json:"-"` - InstanceID string `json:"-"` - Position float64 `json:"-"` + AggregateID string `json:"-"` + ProcessedSequence uint64 `json:"-"` + CreationDate time.Time `json:"-"` + ChangeDate time.Time `json:"-"` + Events []Event `json:"-"` + ResourceOwner string `json:"-"` + InstanceID string `json:"-"` + Position decimal.Decimal `json:"-"` } // AppendEvents adds all the events to the read model. diff --git a/internal/eventstore/repository/event.go b/internal/eventstore/repository/event.go index d0d2660d79..1107649934 100644 --- a/internal/eventstore/repository/event.go +++ b/internal/eventstore/repository/event.go @@ -7,6 +7,7 @@ import ( "strings" "time" + "github.com/shopspring/decimal" "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/eventstore" @@ -22,7 +23,7 @@ type Event struct { // Seq is the sequence of the event Seq uint64 // Pos is the global sequence of the event multiple events can have the same sequence - Pos float64 + Pos decimal.Decimal //CreationDate is the time the event is created // it's used for human readability. @@ -97,7 +98,7 @@ func (e *Event) Sequence() uint64 { } // Position implements [eventstore.Event] -func (e *Event) Position() float64 { +func (e *Event) Position() decimal.Decimal { return e.Pos } diff --git a/internal/eventstore/repository/mock/repository.mock.go b/internal/eventstore/repository/mock/repository.mock.go index 8d5c0430ad..12925bc975 100644 --- a/internal/eventstore/repository/mock/repository.mock.go +++ b/internal/eventstore/repository/mock/repository.mock.go @@ -13,6 +13,7 @@ import ( context "context" reflect "reflect" + decimal "github.com/shopspring/decimal" database "github.com/zitadel/zitadel/internal/database" eventstore "github.com/zitadel/zitadel/internal/eventstore" gomock "go.uber.org/mock/gomock" @@ -98,19 +99,19 @@ func (mr *MockQuerierMockRecorder) InstanceIDs(arg0, arg1 any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InstanceIDs", reflect.TypeOf((*MockQuerier)(nil).InstanceIDs), arg0, arg1) } -// LatestSequence mocks base method. -func (m *MockQuerier) LatestSequence(arg0 context.Context, arg1 *eventstore.SearchQueryBuilder) (float64, error) { +// LatestPosition mocks base method. +func (m *MockQuerier) LatestPosition(arg0 context.Context, arg1 *eventstore.SearchQueryBuilder) (decimal.Decimal, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "LatestSequence", arg0, arg1) - ret0, _ := ret[0].(float64) + ret := m.ctrl.Call(m, "LatestPosition", arg0, arg1) + ret0, _ := ret[0].(decimal.Decimal) ret1, _ := ret[1].(error) return ret0, ret1 } -// LatestSequence indicates an expected call of LatestSequence. -func (mr *MockQuerierMockRecorder) LatestSequence(arg0, arg1 any) *gomock.Call { +// LatestPosition indicates an expected call of LatestPosition. +func (mr *MockQuerierMockRecorder) LatestPosition(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LatestSequence", reflect.TypeOf((*MockQuerier)(nil).LatestSequence), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LatestPosition", reflect.TypeOf((*MockQuerier)(nil).LatestPosition), arg0, arg1) } // MockPusher is a mock of Pusher interface. diff --git a/internal/eventstore/repository/mock/repository.mock.impl.go b/internal/eventstore/repository/mock/repository.mock.impl.go index ced76953cb..313f7ee5e8 100644 --- a/internal/eventstore/repository/mock/repository.mock.impl.go +++ b/internal/eventstore/repository/mock/repository.mock.impl.go @@ -7,6 +7,7 @@ import ( "testing" "time" + "github.com/shopspring/decimal" "github.com/stretchr/testify/assert" "go.uber.org/mock/gomock" @@ -197,8 +198,8 @@ func (e *mockEvent) Sequence() uint64 { return e.sequence } -func (e *mockEvent) Position() float64 { - return 0 +func (e *mockEvent) Position() decimal.Decimal { + return decimal.Decimal{} } func (e *mockEvent) CreatedAt() time.Time { diff --git a/internal/eventstore/repository/search_query.go b/internal/eventstore/repository/search_query.go index 6ffba31ca8..760f7f616c 100644 --- a/internal/eventstore/repository/search_query.go +++ b/internal/eventstore/repository/search_query.go @@ -3,6 +3,8 @@ package repository import ( "database/sql" + "github.com/shopspring/decimal" + "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/zerrors" @@ -57,6 +59,8 @@ const ( // OperationNotIn checks if a stored value does not match one of the passed value list OperationNotIn + OperationGreaterOrEquals + operationCount ) @@ -250,10 +254,10 @@ func instanceIDsFilter(builder *eventstore.SearchQueryBuilder, query *SearchQuer } func positionAfterFilter(builder *eventstore.SearchQueryBuilder, query *SearchQuery) *Filter { - if builder.GetPositionAfter() == 0 { + if builder.GetPositionAtLeast().IsZero() { return nil } - query.Position = NewFilter(FieldPosition, builder.GetPositionAfter(), OperationGreater) + query.Position = NewFilter(FieldPosition, builder.GetPositionAtLeast(), OperationGreaterOrEquals) return query.Position } @@ -295,7 +299,7 @@ func eventDataFilter(query *eventstore.SearchQuery) *Filter { } func eventPositionAfterFilter(query *eventstore.SearchQuery) *Filter { - if pos := query.GetPositionAfter(); pos != 0 { + if pos := query.GetPositionAfter(); !pos.Equal(decimal.Decimal{}) { return NewFilter(FieldPosition, pos, OperationGreater) } return nil diff --git a/internal/eventstore/repository/sql/local_postgres_test.go b/internal/eventstore/repository/sql/local_postgres_test.go index 765da213e3..ae1f7b4831 100644 --- a/internal/eventstore/repository/sql/local_postgres_test.go +++ b/internal/eventstore/repository/sql/local_postgres_test.go @@ -7,6 +7,8 @@ import ( "testing" "time" + pgxdecimal "github.com/jackc/pgx-shopspring-decimal" + "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" "github.com/jackc/pgx/v5/stdlib" "github.com/zitadel/logging" @@ -30,7 +32,11 @@ func TestMain(m *testing.M) { connConfig, err := pgxpool.ParseConfig(config.GetConnectionURL()) logging.OnError(err).Fatal("unable to parse db url") - connConfig.AfterConnect = new_es.RegisterEventstoreTypes + connConfig.AfterConnect = func(ctx context.Context, conn *pgx.Conn) error { + pgxdecimal.Register(conn.TypeMap()) + return new_es.RegisterEventstoreTypes(ctx, conn) + } + pool, err := pgxpool.NewWithConfig(context.Background(), connConfig) logging.OnError(err).Fatal("unable to create db pool") diff --git a/internal/eventstore/repository/sql/postgres.go b/internal/eventstore/repository/sql/postgres.go index bc9ad2e029..0dc2210f7b 100644 --- a/internal/eventstore/repository/sql/postgres.go +++ b/internal/eventstore/repository/sql/postgres.go @@ -2,12 +2,12 @@ package sql import ( "context" - "database/sql" "errors" "regexp" "strconv" "github.com/jackc/pgx/v5/pgconn" + "github.com/shopspring/decimal" "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/eventstore" @@ -55,11 +55,11 @@ func (psql *Postgres) FilterToReducer(ctx context.Context, searchQuery *eventsto return err } -// LatestSequence returns the latest sequence found by the search query -func (db *Postgres) LatestSequence(ctx context.Context, searchQuery *eventstore.SearchQueryBuilder) (float64, error) { - var position sql.NullFloat64 +// LatestPosition returns the latest position found by the search query +func (db *Postgres) LatestPosition(ctx context.Context, searchQuery *eventstore.SearchQueryBuilder) (decimal.Decimal, error) { + var position decimal.Decimal err := query(ctx, db, searchQuery, &position, false) - return position.Float64, err + return position, err } // InstanceIDs returns the instance ids found by the search query @@ -126,7 +126,7 @@ func (db *Postgres) eventQuery(useV1 bool) string { " FROM eventstore.events2" } -func (db *Postgres) maxSequenceQuery(useV1 bool) string { +func (db *Postgres) maxPositionQuery(useV1 bool) string { if useV1 { return `SELECT event_sequence FROM eventstore.events` } @@ -207,6 +207,8 @@ func (db *Postgres) operation(operation repository.Operation) string { return "=" case repository.OperationGreater: return ">" + case repository.OperationGreaterOrEquals: + return ">=" case repository.OperationLess: return "<" case repository.OperationJSONContains: diff --git a/internal/eventstore/repository/sql/postgres_test.go b/internal/eventstore/repository/sql/postgres_test.go index 151fdd1b6a..8a9b7bc049 100644 --- a/internal/eventstore/repository/sql/postgres_test.go +++ b/internal/eventstore/repository/sql/postgres_test.go @@ -4,6 +4,8 @@ import ( "database/sql" "testing" + "github.com/shopspring/decimal" + "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/eventstore/repository" ) @@ -312,7 +314,7 @@ func generateEvent(t *testing.T, aggregateID string, opts ...func(*repository.Ev ResourceOwner: sql.NullString{String: "ro", Valid: true}, Typ: "test.created", Version: "v1", - Pos: 42, + Pos: decimal.NewFromInt(42), } for _, opt := range opts { diff --git a/internal/eventstore/repository/sql/query.go b/internal/eventstore/repository/sql/query.go index a545225d9e..8584a82fa0 100644 --- a/internal/eventstore/repository/sql/query.go +++ b/internal/eventstore/repository/sql/query.go @@ -9,6 +9,7 @@ import ( "strconv" "strings" + "github.com/shopspring/decimal" "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/database" @@ -24,7 +25,7 @@ type querier interface { conditionFormat(repository.Operation) string placeholder(query string) string eventQuery(useV1 bool) string - maxSequenceQuery(useV1 bool) string + maxPositionQuery(useV1 bool) string instanceIDsQuery(useV1 bool) string Client() *database.DB orderByEventSequence(desc, shouldOrderBySequence, useV1 bool) string @@ -68,7 +69,7 @@ func query(ctx context.Context, criteria querier, searchQuery *eventstore.Search // instead of using the max function of the database (which doesn't work for postgres) // we select the most recent row - if q.Columns == eventstore.ColumnsMaxSequence { + if q.Columns == eventstore.ColumnsMaxPosition { q.Limit = 1 q.Desc = true } @@ -85,7 +86,7 @@ func query(ctx context.Context, criteria querier, searchQuery *eventstore.Search switch q.Columns { case eventstore.ColumnsEvent, - eventstore.ColumnsMaxSequence: + eventstore.ColumnsMaxPosition: query += criteria.orderByEventSequence(q.Desc, shouldOrderBySequence, useV1) } @@ -141,8 +142,8 @@ func query(ctx context.Context, criteria querier, searchQuery *eventstore.Search func prepareColumns(criteria querier, columns eventstore.Columns, useV1 bool) (string, func(s scan, dest interface{}) error) { switch columns { - case eventstore.ColumnsMaxSequence: - return criteria.maxSequenceQuery(useV1), maxSequenceScanner + case eventstore.ColumnsMaxPosition: + return criteria.maxPositionQuery(useV1), maxPositionScanner case eventstore.ColumnsInstanceIDs: return criteria.instanceIDsQuery(useV1), instanceIDsScanner case eventstore.ColumnsEvent: @@ -152,13 +153,15 @@ func prepareColumns(criteria querier, columns eventstore.Columns, useV1 bool) (s } } -func maxSequenceScanner(row scan, dest any) (err error) { - position, ok := dest.(*sql.NullFloat64) +func maxPositionScanner(row scan, dest interface{}) (err error) { + position, ok := dest.(*decimal.Decimal) if !ok { - return zerrors.ThrowInvalidArgumentf(nil, "SQL-NBjA9", "type must be sql.NullInt64 got: %T", dest) + return zerrors.ThrowInvalidArgumentf(nil, "SQL-NBjA9", "type must be pointer to decimal.Decimal got: %T", dest) } - err = row(position) + var res decimal.NullDecimal + err = row(&res) if err == nil || errors.Is(err, sql.ErrNoRows) { + *position = res.Decimal return nil } return zerrors.ThrowInternal(err, "SQL-bN5xg", "something went wrong") @@ -187,7 +190,7 @@ func eventsScanner(useV1 bool) func(scanner scan, dest interface{}) (err error) return zerrors.ThrowInvalidArgumentf(nil, "SQL-4GP6F", "events scanner: invalid type %T", dest) } event := new(repository.Event) - position := new(sql.NullFloat64) + position := new(decimal.NullDecimal) if useV1 { err = scanner( @@ -224,7 +227,7 @@ func eventsScanner(useV1 bool) func(scanner scan, dest interface{}) (err error) logging.New().WithError(err).Warn("unable to scan row") return zerrors.ThrowInternal(err, "SQL-M0dsf", "unable to scan row") } - event.Pos = position.Float64 + event.Pos = position.Decimal return reduce(event) } } diff --git a/internal/eventstore/repository/sql/query_test.go b/internal/eventstore/repository/sql/query_test.go index 3df819be64..0e2425dd07 100644 --- a/internal/eventstore/repository/sql/query_test.go +++ b/internal/eventstore/repository/sql/query_test.go @@ -7,10 +7,12 @@ import ( "reflect" "regexp" "strconv" + "strings" "testing" "time" "github.com/DATA-DOG/go-sqlmock" + "github.com/shopspring/decimal" "github.com/stretchr/testify/assert" "github.com/zitadel/zitadel/internal/database" @@ -111,36 +113,36 @@ func Test_prepareColumns(t *testing.T) { { name: "max column", args: args{ - columns: eventstore.ColumnsMaxSequence, - dest: new(sql.NullFloat64), + columns: eventstore.ColumnsMaxPosition, + dest: new(decimal.Decimal), useV1: true, }, res: res{ query: `SELECT event_sequence FROM eventstore.events`, - expected: sql.NullFloat64{Float64: 43, Valid: true}, + expected: decimal.NewFromInt(42), }, fields: fields{ - dbRow: []interface{}{sql.NullFloat64{Float64: 43, Valid: true}}, + dbRow: []interface{}{decimal.NewNullDecimal(decimal.NewFromInt(42))}, }, }, { name: "max column v2", args: args{ - columns: eventstore.ColumnsMaxSequence, - dest: new(sql.NullFloat64), + columns: eventstore.ColumnsMaxPosition, + dest: new(decimal.Decimal), }, res: res{ query: `SELECT "position" FROM eventstore.events2`, - expected: sql.NullFloat64{Float64: 43, Valid: true}, + expected: decimal.NewFromInt(42), }, fields: fields{ - dbRow: []interface{}{sql.NullFloat64{Float64: 43, Valid: true}}, + dbRow: []interface{}{decimal.NewNullDecimal(decimal.NewFromInt(42))}, }, }, { name: "max sequence wrong dest type", args: args{ - columns: eventstore.ColumnsMaxSequence, + columns: eventstore.ColumnsMaxPosition, dest: new(uint64), }, res: res{ @@ -180,11 +182,11 @@ func Test_prepareColumns(t *testing.T) { res: res{ query: `SELECT created_at, event_type, "sequence", "position", payload, creator, "owner", instance_id, aggregate_type, aggregate_id, revision FROM eventstore.events2`, expected: []eventstore.Event{ - &repository.Event{AggregateID: "hodor", AggregateType: "user", Seq: 5, Pos: 42, Data: nil, Version: "v1"}, + &repository.Event{AggregateID: "hodor", AggregateType: "user", Seq: 5, Pos: decimal.NewFromInt(42), Data: nil, Version: "v1"}, }, }, fields: fields{ - dbRow: []interface{}{time.Time{}, eventstore.EventType(""), uint64(5), sql.NullFloat64{Float64: 42, Valid: true}, sql.RawBytes(nil), "", sql.NullString{}, "", eventstore.AggregateType("user"), "hodor", uint8(1)}, + dbRow: []interface{}{time.Time{}, eventstore.EventType(""), uint64(5), decimal.NewNullDecimal(decimal.NewFromInt(42)), sql.RawBytes(nil), "", sql.NullString{}, "", eventstore.AggregateType("user"), "hodor", uint8(1)}, }, }, { @@ -199,11 +201,11 @@ func Test_prepareColumns(t *testing.T) { res: res{ query: `SELECT created_at, event_type, "sequence", "position", payload, creator, "owner", instance_id, aggregate_type, aggregate_id, revision FROM eventstore.events2`, expected: []eventstore.Event{ - &repository.Event{AggregateID: "hodor", AggregateType: "user", Seq: 5, Pos: 0, Data: nil, Version: "v1"}, + &repository.Event{AggregateID: "hodor", AggregateType: "user", Seq: 5, Pos: decimal.Decimal{}, Data: nil, Version: "v1"}, }, }, fields: fields{ - dbRow: []interface{}{time.Time{}, eventstore.EventType(""), uint64(5), sql.NullFloat64{Float64: 0, Valid: false}, sql.RawBytes(nil), "", sql.NullString{}, "", eventstore.AggregateType("user"), "hodor", uint8(1)}, + dbRow: []interface{}{time.Time{}, eventstore.EventType(""), uint64(5), decimal.NullDecimal{}, sql.RawBytes(nil), "", sql.NullString{}, "", eventstore.AggregateType("user"), "hodor", uint8(1)}, }, }, { @@ -901,7 +903,7 @@ func Test_query_events_mocked(t *testing.T) { InstanceID("instanceID"). OrderDesc(). Limit(5). - PositionAfter(123.456). + PositionAtLeast(decimal.NewFromFloat(123.456)). AddQuery(). AggregateTypes("notify"). EventTypes("notify.foo.bar"). @@ -914,8 +916,8 @@ func Test_query_events_mocked(t *testing.T) { }, fields: fields{ mock: newMockClient(t).expectQuery( - regexp.QuoteMeta(`SELECT creation_date, event_type, event_sequence, event_data, editor_user, resource_owner, instance_id, aggregate_type, aggregate_id, aggregate_version FROM eventstore.events WHERE instance_id = $1 AND aggregate_type = $2 AND event_type = $3 AND "position" > $4 AND aggregate_id NOT IN (SELECT aggregate_id FROM eventstore.events WHERE aggregate_type = $5 AND event_type = ANY($6) AND instance_id = $7 AND "position" > $8) ORDER BY event_sequence DESC LIMIT $9`), - []driver.Value{"instanceID", eventstore.AggregateType("notify"), eventstore.EventType("notify.foo.bar"), 123.456, eventstore.AggregateType("notify"), []eventstore.EventType{"notification.failed", "notification.success"}, "instanceID", 123.456, uint64(5)}, + regexp.QuoteMeta(`SELECT creation_date, event_type, event_sequence, event_data, editor_user, resource_owner, instance_id, aggregate_type, aggregate_id, aggregate_version FROM eventstore.events WHERE instance_id = $1 AND aggregate_type = $2 AND event_type = $3 AND "position" >= $4 AND aggregate_id NOT IN (SELECT aggregate_id FROM eventstore.events WHERE aggregate_type = $5 AND event_type = ANY($6) AND instance_id = $7 AND "position" >= $8) ORDER BY event_sequence DESC LIMIT $9`), + []driver.Value{"instanceID", eventstore.AggregateType("notify"), eventstore.EventType("notify.foo.bar"), decimal.NewFromFloat(123.456), eventstore.AggregateType("notify"), []eventstore.EventType{"notification.failed", "notification.success"}, "instanceID", decimal.NewFromFloat(123.456), uint64(5)}, ), }, res: res{ @@ -930,7 +932,7 @@ func Test_query_events_mocked(t *testing.T) { InstanceID("instanceID"). OrderDesc(). Limit(5). - PositionAfter(123.456). + PositionAtLeast(decimal.NewFromFloat(123.456)). AddQuery(). AggregateTypes("notify"). EventTypes("notify.foo.bar"). @@ -943,8 +945,8 @@ func Test_query_events_mocked(t *testing.T) { }, fields: fields{ mock: newMockClient(t).expectQuery( - regexp.QuoteMeta(`SELECT created_at, event_type, "sequence", "position", payload, creator, "owner", instance_id, aggregate_type, aggregate_id, revision FROM eventstore.events2 WHERE instance_id = $1 AND aggregate_type = $2 AND event_type = $3 AND "position" > $4 AND aggregate_id NOT IN (SELECT aggregate_id FROM eventstore.events2 WHERE aggregate_type = $5 AND event_type = ANY($6) AND instance_id = $7 AND "position" > $8) ORDER BY "position" DESC, in_tx_order DESC LIMIT $9`), - []driver.Value{"instanceID", eventstore.AggregateType("notify"), eventstore.EventType("notify.foo.bar"), 123.456, eventstore.AggregateType("notify"), []eventstore.EventType{"notification.failed", "notification.success"}, "instanceID", 123.456, uint64(5)}, + regexp.QuoteMeta(`SELECT created_at, event_type, "sequence", "position", payload, creator, "owner", instance_id, aggregate_type, aggregate_id, revision FROM eventstore.events2 WHERE instance_id = $1 AND aggregate_type = $2 AND event_type = $3 AND "position" >= $4 AND aggregate_id NOT IN (SELECT aggregate_id FROM eventstore.events2 WHERE aggregate_type = $5 AND event_type = ANY($6) AND instance_id = $7 AND "position" >= $8) ORDER BY "position" DESC, in_tx_order DESC LIMIT $9`), + []driver.Value{"instanceID", eventstore.AggregateType("notify"), eventstore.EventType("notify.foo.bar"), decimal.NewFromFloat(123.456), eventstore.AggregateType("notify"), []eventstore.EventType{"notification.failed", "notification.success"}, "instanceID", decimal.NewFromFloat(123.456), uint64(5)}, ), }, res: res{ @@ -988,6 +990,10 @@ func Test_query_events_mocked(t *testing.T) { client.DB.DB = tt.fields.mock.client } + if strings.HasPrefix(tt.name, "aggregate / event type, position and exclusion") { + t.Log("hodor") + } + err := query(context.Background(), client, tt.args.query, tt.args.dest, tt.args.useV1) if (err != nil) != tt.res.wantErr { t.Errorf("query() error = %v, wantErr %v", err, tt.res.wantErr) diff --git a/internal/eventstore/search_query.go b/internal/eventstore/search_query.go index 1596936a36..dc92f5a4de 100644 --- a/internal/eventstore/search_query.go +++ b/internal/eventstore/search_query.go @@ -5,6 +5,8 @@ import ( "database/sql" "time" + "github.com/shopspring/decimal" + "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/zerrors" ) @@ -25,7 +27,7 @@ type SearchQueryBuilder struct { tx *sql.Tx lockRows bool lockOption LockOption - positionAfter float64 + positionAtLeast decimal.Decimal awaitOpenTransactions bool creationDateAfter time.Time creationDateBefore time.Time @@ -76,8 +78,8 @@ func (b *SearchQueryBuilder) GetTx() *sql.Tx { return b.tx } -func (b SearchQueryBuilder) GetPositionAfter() float64 { - return b.positionAfter +func (b SearchQueryBuilder) GetPositionAtLeast() decimal.Decimal { + return b.positionAtLeast } func (b SearchQueryBuilder) GetAwaitOpenTransactions() bool { @@ -113,7 +115,7 @@ type SearchQuery struct { aggregateIDs []string eventTypes []EventType eventData map[string]interface{} - positionAfter float64 + positionAfter decimal.Decimal } func (q SearchQuery) GetAggregateTypes() []AggregateType { @@ -132,7 +134,7 @@ func (q SearchQuery) GetEventData() map[string]interface{} { return q.eventData } -func (q SearchQuery) GetPositionAfter() float64 { +func (q SearchQuery) GetPositionAfter() decimal.Decimal { return q.positionAfter } @@ -156,8 +158,8 @@ type Columns int8 const ( //ColumnsEvent represents all fields of an event ColumnsEvent = iota + 1 - // ColumnsMaxSequence represents the latest sequence of the filtered events - ColumnsMaxSequence + // ColumnsMaxPosition represents the latest sequence of the filtered events + ColumnsMaxPosition // ColumnsInstanceIDs represents the instance ids of the filtered events ColumnsInstanceIDs @@ -284,9 +286,9 @@ func (builder *SearchQueryBuilder) EditorUser(id string) *SearchQueryBuilder { return builder } -// PositionAfter filters for events which happened after the specified time -func (builder *SearchQueryBuilder) PositionAfter(position float64) *SearchQueryBuilder { - builder.positionAfter = position +// PositionAtLeast filters for events which happened after the specified time +func (builder *SearchQueryBuilder) PositionAtLeast(position decimal.Decimal) *SearchQueryBuilder { + builder.positionAtLeast = position return builder } @@ -393,7 +395,7 @@ func (query *SearchQuery) EventData(data map[string]interface{}) *SearchQuery { return query } -func (query *SearchQuery) PositionAfter(position float64) *SearchQuery { +func (query *SearchQuery) PositionAfter(position decimal.Decimal) *SearchQuery { query.positionAfter = position return query } diff --git a/internal/eventstore/search_query_test.go b/internal/eventstore/search_query_test.go index b8f570dc0d..3325ee0c4b 100644 --- a/internal/eventstore/search_query_test.go +++ b/internal/eventstore/search_query_test.go @@ -106,10 +106,10 @@ func TestSearchQuerybuilderSetters(t *testing.T) { { name: "set columns", args: args{ - setters: []func(*SearchQueryBuilder) *SearchQueryBuilder{testSetColumns(ColumnsMaxSequence)}, + setters: []func(*SearchQueryBuilder) *SearchQueryBuilder{testSetColumns(ColumnsMaxPosition)}, }, res: &SearchQueryBuilder{ - columns: ColumnsMaxSequence, + columns: ColumnsMaxPosition, }, }, { diff --git a/internal/eventstore/v1/models/event.go b/internal/eventstore/v1/models/event.go index 8c50d64da0..ab2b608872 100644 --- a/internal/eventstore/v1/models/event.go +++ b/internal/eventstore/v1/models/event.go @@ -5,6 +5,8 @@ import ( "reflect" "time" + "github.com/shopspring/decimal" + "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/zerrors" ) @@ -20,7 +22,7 @@ var _ eventstore.Event = (*Event)(nil) type Event struct { ID string Seq uint64 - Pos float64 + Pos decimal.Decimal CreationDate time.Time Typ eventstore.EventType PreviousSequence uint64 @@ -80,7 +82,7 @@ func (e *Event) Sequence() uint64 { } // Position implements [eventstore.Event] -func (e *Event) Position() float64 { +func (e *Event) Position() decimal.Decimal { return e.Pos } diff --git a/internal/eventstore/v3/event.go b/internal/eventstore/v3/event.go index 1141a9eacf..c9ea4d2c62 100644 --- a/internal/eventstore/v3/event.go +++ b/internal/eventstore/v3/event.go @@ -6,6 +6,7 @@ import ( "strconv" "time" + "github.com/shopspring/decimal" "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/api/authz" @@ -42,7 +43,7 @@ type event struct { command *command createdAt time.Time sequence uint64 - position float64 + position decimal.Decimal } // TODO: remove on v3 @@ -152,8 +153,8 @@ func (e *event) Sequence() uint64 { return e.sequence } -// Sequence implements [eventstore.Event] -func (e *event) Position() float64 { +// Position implements [eventstore.Event] +func (e *event) Position() decimal.Decimal { return e.position } diff --git a/internal/notification/projections.go b/internal/notification/projections.go index 9b6b975fa1..7fda08135c 100644 --- a/internal/notification/projections.go +++ b/internal/notification/projections.go @@ -71,6 +71,26 @@ func Start(ctx context.Context) { } } +func SetCurrentState(ctx context.Context, es *eventstore.Eventstore) error { + if len(projections) == 0 { + return nil + } + position, err := es.LatestPosition(ctx, eventstore.NewSearchQueryBuilder(eventstore.ColumnsMaxPosition).InstanceID(authz.GetInstance(ctx).InstanceID()).OrderDesc().Limit(1)) + if err != nil { + return err + } + + for i, projection := range projections { + logging.WithFields("name", projection.ProjectionName(), "instance", authz.GetInstance(ctx).InstanceID(), "index", fmt.Sprintf("%d/%d", i, len(projections))).Info("set current state of notification projection") + _, err = projection.Trigger(ctx, handler.WithMinPosition(position)) + if err != nil { + return err + } + logging.WithFields("name", projection.ProjectionName(), "instance", authz.GetInstance(ctx).InstanceID(), "index", fmt.Sprintf("%d/%d", i, len(projections))).Info("current state of notification projection set") + } + return nil +} + func ProjectInstance(ctx context.Context) error { 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") diff --git a/internal/query/access_token.go b/internal/query/access_token.go index 0fc1bbb369..030ddda473 100644 --- a/internal/query/access_token.go +++ b/internal/query/access_token.go @@ -5,6 +5,7 @@ import ( "strings" "time" + "github.com/shopspring/decimal" "golang.org/x/text/language" "github.com/zitadel/zitadel/internal/domain" @@ -140,7 +141,7 @@ func (q *Queries) accessTokenByOIDCSessionAndTokenID(ctx context.Context, oidcSe // checkSessionNotTerminatedAfter checks if a [session.TerminateType] event (or user events leading to a session termination) // occurred after a certain time and will return an error if so. -func (q *Queries) checkSessionNotTerminatedAfter(ctx context.Context, sessionID, userID string, position float64, fingerprintID string) (err error) { +func (q *Queries) checkSessionNotTerminatedAfter(ctx context.Context, sessionID, userID string, position decimal.Decimal, fingerprintID string) (err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() @@ -165,7 +166,7 @@ func (q *Queries) checkSessionNotTerminatedAfter(ctx context.Context, sessionID, } type sessionTerminatedModel struct { - position float64 + position decimal.Decimal sessionID string userID string fingerPrintID string diff --git a/internal/query/current_state.go b/internal/query/current_state.go index 6fae52713f..d0a5b369bf 100644 --- a/internal/query/current_state.go +++ b/internal/query/current_state.go @@ -10,6 +10,7 @@ import ( "time" sq "github.com/Masterminds/squirrel" + "github.com/shopspring/decimal" "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/eventstore" @@ -25,7 +26,7 @@ type Stateful interface { type State struct { LastRun time.Time - Position float64 + Position decimal.Decimal EventCreatedAt time.Time AggregateID string AggregateType eventstore.AggregateType @@ -220,7 +221,7 @@ func prepareLatestState() (sq.SelectBuilder, func(*sql.Row) (*State, error)) { var ( creationDate sql.NullTime lastUpdated sql.NullTime - position sql.NullFloat64 + position decimal.NullDecimal ) err := row.Scan( &creationDate, @@ -233,7 +234,7 @@ func prepareLatestState() (sq.SelectBuilder, func(*sql.Row) (*State, error)) { return &State{ EventCreatedAt: creationDate.Time, LastRun: lastUpdated.Time, - Position: position.Float64, + Position: position.Decimal, }, nil } } @@ -258,7 +259,7 @@ func prepareCurrentStateQuery() (sq.SelectBuilder, func(*sql.Rows) (*CurrentStat var ( lastRun sql.NullTime eventDate sql.NullTime - currentPosition sql.NullFloat64 + currentPosition decimal.NullDecimal aggregateType sql.NullString aggregateID sql.NullString sequence sql.NullInt64 @@ -279,7 +280,7 @@ func prepareCurrentStateQuery() (sq.SelectBuilder, func(*sql.Rows) (*CurrentStat } currentState.State.EventCreatedAt = eventDate.Time currentState.State.LastRun = lastRun.Time - currentState.Position = currentPosition.Float64 + currentState.Position = currentPosition.Decimal currentState.AggregateType = eventstore.AggregateType(aggregateType.String) currentState.AggregateID = aggregateID.String currentState.Sequence = uint64(sequence.Int64) diff --git a/internal/query/current_state_test.go b/internal/query/current_state_test.go index c0895dc439..29761b8cb3 100644 --- a/internal/query/current_state_test.go +++ b/internal/query/current_state_test.go @@ -7,6 +7,8 @@ import ( "fmt" "regexp" "testing" + + "github.com/shopspring/decimal" ) var ( @@ -86,7 +88,7 @@ func Test_CurrentSequencesPrepares(t *testing.T) { State: State{ EventCreatedAt: testNow, LastRun: testNow, - Position: 20211108, + Position: decimal.NewFromInt(20211108), AggregateID: "agg-id", AggregateType: "agg-type", Sequence: 20211108, @@ -133,7 +135,7 @@ func Test_CurrentSequencesPrepares(t *testing.T) { ProjectionName: "projection-name", State: State{ EventCreatedAt: testNow, - Position: 20211108, + Position: decimal.NewFromInt(20211108), LastRun: testNow, AggregateID: "agg-id", AggregateType: "agg-type", @@ -144,7 +146,7 @@ func Test_CurrentSequencesPrepares(t *testing.T) { ProjectionName: "projection-name2", State: State{ EventCreatedAt: testNow, - Position: 20211108, + Position: decimal.NewFromInt(20211108), LastRun: testNow, AggregateID: "agg-id", AggregateType: "agg-type", diff --git a/internal/query/projection/projection.go b/internal/query/projection/projection.go index 07953a27e8..77a28ac79a 100644 --- a/internal/query/projection/projection.go +++ b/internal/query/projection/projection.go @@ -2,8 +2,10 @@ package projection import ( "context" + "errors" "fmt" + "github.com/jackc/pgx/v5/pgconn" "github.com/zitadel/logging" internal_authz "github.com/zitadel/zitadel/internal/api/authz" @@ -212,11 +214,19 @@ func Start(ctx context.Context) { func ProjectInstance(ctx context.Context) error { 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 + for { + _, err := projection.Trigger(ctx) + if err == nil { + break + } + var pgErr *pgconn.PgError + errors.As(err, &pgErr) + if pgErr.Code != database.PgUniqueConstraintErrorCode { + return err + } + logging.WithFields("name", projection.ProjectionName(), "instance", internal_authz.GetInstance(ctx).InstanceID()).WithError(err).Debug("projection failed because of unique constraint, retrying") } - logging.WithFields("name", projection.ProjectionName(), "instance", internal_authz.GetInstance(ctx).InstanceID()).Info("projection done") + logging.WithFields("name", projection.ProjectionName(), "instance", internal_authz.GetInstance(ctx).InstanceID(), "index", fmt.Sprintf("%d/%d", i, len(projections))).Info("projection done") } return nil } @@ -224,11 +234,19 @@ func ProjectInstance(ctx context.Context) error { func ProjectInstanceFields(ctx context.Context) error { 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 + for { + err := fieldProjection.Trigger(ctx) + if err == nil { + break + } + var pgErr *pgconn.PgError + errors.As(err, &pgErr) + if pgErr.Code != database.PgUniqueConstraintErrorCode { + return err + } + logging.WithFields("name", fieldProjection.ProjectionName(), "instance", internal_authz.GetInstance(ctx).InstanceID()).WithError(err).Debug("fields projection failed because of unique constraint, retrying") } - logging.WithFields("name", fieldProjection.ProjectionName(), "instance", internal_authz.GetInstance(ctx).InstanceID()).Info("fields projection done") + logging.WithFields("name", fieldProjection.ProjectionName(), "instance", internal_authz.GetInstance(ctx).InstanceID(), "index", fmt.Sprintf("%d/%d", i, len(fields))).Info("fields projection done") } return nil } @@ -257,6 +275,10 @@ func applyCustomConfig(config handler.Config, customConfig CustomConfig) handler return config } +// we know this is ugly, but we need to have a singleton slice of all projections +// and are only able to initialize it after all projections are created +// as setup and start currently create them individually, we make sure we get the right one +// will be refactored when changing to new id based projections func newFieldsList() { fields = []*handler.FieldHandler{ ProjectGrantFields, diff --git a/internal/query/user_grant.go b/internal/query/user_grant.go index c3f24c066e..ebd4ab7c0c 100644 --- a/internal/query/user_grant.go +++ b/internal/query/user_grant.go @@ -280,7 +280,7 @@ func (q *Queries) UserGrants(ctx context.Context, queries *UserGrantsQueries, sh return nil, zerrors.ThrowInternal(err, "QUERY-wXnQR", "Errors.Query.SQLStatement") } - latestSequence, err := q.latestState(ctx, userGrantTable) + latestState, err := q.latestState(ctx, userGrantTable) if err != nil { return nil, err } @@ -293,7 +293,7 @@ func (q *Queries) UserGrants(ctx context.Context, queries *UserGrantsQueries, sh return nil, err } - grants.State = latestSequence + grants.State = latestState return grants, nil } diff --git a/internal/query/user_membership.go b/internal/query/user_membership.go index cae2b4dae3..cb7588624f 100644 --- a/internal/query/user_membership.go +++ b/internal/query/user_membership.go @@ -143,7 +143,7 @@ func (q *Queries) Memberships(ctx context.Context, queries *MembershipSearchQuer if err != nil { return nil, zerrors.ThrowInvalidArgument(err, "QUERY-T84X9", "Errors.Query.InvalidRequest") } - latestSequence, err := q.latestState(ctx, orgMemberTable, instanceMemberTable, projectMemberTable, projectGrantMemberTable) + latestState, err := q.latestState(ctx, orgMemberTable, instanceMemberTable, projectMemberTable, projectGrantMemberTable) if err != nil { return nil, err } @@ -156,7 +156,7 @@ func (q *Queries) Memberships(ctx context.Context, queries *MembershipSearchQuer if err != nil { return nil, err } - memberships.State = latestSequence + memberships.State = latestState return memberships, nil } diff --git a/internal/v2/database/number_filter.go b/internal/v2/database/number_filter.go index ce263ceeee..4853806457 100644 --- a/internal/v2/database/number_filter.go +++ b/internal/v2/database/number_filter.go @@ -3,6 +3,7 @@ package database import ( "time" + "github.com/shopspring/decimal" "github.com/zitadel/logging" "golang.org/x/exp/constraints" ) @@ -94,7 +95,7 @@ func (c numberCompare) String() string { } type number interface { - constraints.Integer | constraints.Float | time.Time + constraints.Integer | constraints.Float | time.Time | decimal.Decimal // TODO: condition must know if it's args are named parameters or not // constraints.Integer | constraints.Float | time.Time | placeholder } diff --git a/internal/v2/eventstore/event_store.go b/internal/v2/eventstore/event_store.go index cc447c5e15..e89786c657 100644 --- a/internal/v2/eventstore/event_store.go +++ b/internal/v2/eventstore/event_store.go @@ -2,6 +2,8 @@ package eventstore import ( "context" + + "github.com/shopspring/decimal" ) func NewEventstore(querier Querier, pusher Pusher) *EventStore { @@ -30,12 +32,12 @@ type healthier interface { } type GlobalPosition struct { - Position float64 + Position decimal.Decimal InPositionOrder uint32 } func (gp GlobalPosition) IsLess(other GlobalPosition) bool { - return gp.Position < other.Position || (gp.Position == other.Position && gp.InPositionOrder < other.InPositionOrder) + return gp.Position.LessThan(other.Position) || (gp.Position.Equal(other.Position) && gp.InPositionOrder < other.InPositionOrder) } type Reducer interface { diff --git a/internal/v2/eventstore/postgres/push_test.go b/internal/v2/eventstore/postgres/push_test.go index bb3254427c..afd5fe8b8e 100644 --- a/internal/v2/eventstore/postgres/push_test.go +++ b/internal/v2/eventstore/postgres/push_test.go @@ -8,6 +8,8 @@ import ( "testing" "time" + "github.com/shopspring/decimal" + "github.com/zitadel/zitadel/internal/v2/database/mock" "github.com/zitadel/zitadel/internal/v2/eventstore" "github.com/zitadel/zitadel/internal/zerrors" @@ -818,7 +820,7 @@ func Test_push(t *testing.T) { [][]driver.Value{ { time.Now(), - float64(123), + decimal.NewFromFloat(123).String(), }, }, ), @@ -899,11 +901,11 @@ func Test_push(t *testing.T) { [][]driver.Value{ { time.Now(), - float64(123), + decimal.NewFromFloat(123).String(), }, { time.Now(), - float64(123.1), + decimal.NewFromFloat(123.1).String(), }, }, ), @@ -984,11 +986,11 @@ func Test_push(t *testing.T) { [][]driver.Value{ { time.Now(), - float64(123), + decimal.NewFromFloat(123).String(), }, { time.Now(), - float64(123.1), + decimal.NewFromFloat(123.1).String(), }, }, ), @@ -1044,7 +1046,7 @@ func Test_push(t *testing.T) { [][]driver.Value{ { time.Now(), - float64(123), + decimal.NewFromFloat(123).String(), }, }, ), @@ -1099,7 +1101,7 @@ func Test_push(t *testing.T) { [][]driver.Value{ { time.Now(), - float64(123), + decimal.NewFromFloat(123).String(), }, }, ), @@ -1181,11 +1183,11 @@ func Test_push(t *testing.T) { [][]driver.Value{ { time.Now(), - float64(123), + decimal.NewFromFloat(123).String(), }, { time.Now(), - float64(123.1), + decimal.NewFromFloat(123.1).String(), }, }, ), @@ -1272,11 +1274,11 @@ func Test_push(t *testing.T) { [][]driver.Value{ { time.Now(), - float64(123), + decimal.NewFromFloat(123).String(), }, { time.Now(), - float64(123.1), + decimal.NewFromFloat(123.1).String(), }, }, ), diff --git a/internal/v2/eventstore/postgres/query_test.go b/internal/v2/eventstore/postgres/query_test.go index 56f506ac50..34b73bd820 100644 --- a/internal/v2/eventstore/postgres/query_test.go +++ b/internal/v2/eventstore/postgres/query_test.go @@ -8,6 +8,8 @@ import ( "testing" "time" + "github.com/shopspring/decimal" + "github.com/zitadel/zitadel/internal/v2/database" "github.com/zitadel/zitadel/internal/v2/database/mock" "github.com/zitadel/zitadel/internal/v2/eventstore" @@ -541,13 +543,13 @@ func Test_writeFilter(t *testing.T) { args: args{ filter: eventstore.NewFilter( eventstore.FilterPagination( - eventstore.PositionGreater(123.4, 0), + eventstore.PositionGreater(decimal.NewFromFloat(123.4), 0), ), ), }, want: wantQuery{ query: " WHERE instance_id = $1 AND position > $2 ORDER BY position, in_tx_order", - args: []any{"i1", 123.4}, + args: []any{"i1", decimal.NewFromFloat(123.4)}, }, }, { @@ -555,18 +557,18 @@ func Test_writeFilter(t *testing.T) { args: args{ filter: eventstore.NewFilter( eventstore.FilterPagination( - // eventstore.PositionGreater(123.4, 0), + // eventstore.PositionGreater(decimal.NewFromFloat(123.4), 0), // eventstore.PositionLess(125.4, 10), eventstore.PositionBetween( - &eventstore.GlobalPosition{Position: 123.4}, - &eventstore.GlobalPosition{Position: 125.4, InPositionOrder: 10}, + &eventstore.GlobalPosition{Position: decimal.NewFromFloat(123.4)}, + &eventstore.GlobalPosition{Position: decimal.NewFromFloat(125.4), InPositionOrder: 10}, ), ), ), }, want: wantQuery{ query: " WHERE instance_id = $1 AND ((position = $2 AND in_tx_order < $3) OR position < $4) AND position > $5 ORDER BY position, in_tx_order", - args: []any{"i1", 125.4, uint32(10), 125.4, 123.4}, + args: []any{"i1", decimal.NewFromFloat(125.4), uint32(10), decimal.NewFromFloat(125.4), decimal.NewFromFloat(123.4)}, // TODO: (adlerhurst) would require some refactoring to reuse existing args // query: " WHERE instance_id = $1 AND position > $2 AND ((position = $3 AND in_tx_order < $4) OR position < $3) ORDER BY position, in_tx_order", // args: []any{"i1", 123.4, 125.4, uint32(10)}, @@ -577,13 +579,13 @@ func Test_writeFilter(t *testing.T) { args: args{ filter: eventstore.NewFilter( eventstore.FilterPagination( - eventstore.PositionGreater(123.4, 12), + eventstore.PositionGreater(decimal.NewFromFloat(123.4), 12), ), ), }, want: wantQuery{ query: " WHERE instance_id = $1 AND ((position = $2 AND in_tx_order > $3) OR position > $4) ORDER BY position, in_tx_order", - args: []any{"i1", 123.4, uint32(12), 123.4}, + args: []any{"i1", decimal.NewFromFloat(123.4), uint32(12), decimal.NewFromFloat(123.4)}, }, }, { @@ -593,13 +595,13 @@ func Test_writeFilter(t *testing.T) { eventstore.FilterPagination( eventstore.Limit(10), eventstore.Offset(3), - eventstore.PositionGreater(123.4, 12), + eventstore.PositionGreater(decimal.NewFromFloat(123.4), 12), ), ), }, want: wantQuery{ query: " WHERE instance_id = $1 AND ((position = $2 AND in_tx_order > $3) OR position > $4) ORDER BY position, in_tx_order LIMIT $5 OFFSET $6", - args: []any{"i1", 123.4, uint32(12), 123.4, uint32(10), uint32(3)}, + args: []any{"i1", decimal.NewFromFloat(123.4), uint32(12), decimal.NewFromFloat(123.4), uint32(10), uint32(3)}, }, }, { @@ -609,14 +611,14 @@ func Test_writeFilter(t *testing.T) { eventstore.FilterPagination( eventstore.Limit(10), eventstore.Offset(3), - eventstore.PositionGreater(123.4, 12), + eventstore.PositionGreater(decimal.NewFromFloat(123.4), 12), ), eventstore.AppendAggregateFilter("user"), ), }, want: wantQuery{ query: " WHERE instance_id = $1 AND aggregate_type = $2 AND ((position = $3 AND in_tx_order > $4) OR position > $5) ORDER BY position, in_tx_order LIMIT $6 OFFSET $7", - args: []any{"i1", "user", 123.4, uint32(12), 123.4, uint32(10), uint32(3)}, + args: []any{"i1", "user", decimal.NewFromFloat(123.4), uint32(12), decimal.NewFromFloat(123.4), uint32(10), uint32(3)}, }, }, { @@ -626,7 +628,7 @@ func Test_writeFilter(t *testing.T) { eventstore.FilterPagination( eventstore.Limit(10), eventstore.Offset(3), - eventstore.PositionGreater(123.4, 12), + eventstore.PositionGreater(decimal.NewFromFloat(123.4), 12), ), eventstore.AppendAggregateFilter("user"), eventstore.AppendAggregateFilter( @@ -637,7 +639,7 @@ func Test_writeFilter(t *testing.T) { }, want: wantQuery{ query: " WHERE instance_id = $1 AND (aggregate_type = $2 OR (aggregate_type = $3 AND aggregate_id = $4)) AND ((position = $5 AND in_tx_order > $6) OR position > $7) ORDER BY position, in_tx_order LIMIT $8 OFFSET $9", - args: []any{"i1", "user", "org", "o1", 123.4, uint32(12), 123.4, uint32(10), uint32(3)}, + args: []any{"i1", "user", "org", "o1", decimal.NewFromFloat(123.4), uint32(12), decimal.NewFromFloat(123.4), uint32(10), uint32(3)}, }, }, } @@ -956,7 +958,7 @@ func Test_writeQueryUse_examples(t *testing.T) { ), eventstore.FilterPagination( // used because we need to check for first login and an app which is not console - eventstore.PositionGreater(12, 4), + eventstore.PositionGreater(decimal.NewFromInt(12), 4), ), ), eventstore.NewFilter( @@ -1065,9 +1067,9 @@ func Test_writeQueryUse_examples(t *testing.T) { "instance", "user", "user.token.added", - float64(12), + decimal.NewFromInt(12), uint32(4), - float64(12), + decimal.NewFromInt(12), "instance", "instance", []string{"instance.idp.config.added", "instance.idp.oauth.added", "instance.idp.oidc.added", "instance.idp.jwt.added", "instance.idp.azure.added", "instance.idp.github.added", "instance.idp.github.enterprise.added", "instance.idp.gitlab.added", "instance.idp.gitlab.selfhosted.added", "instance.idp.google.added", "instance.idp.ldap.added", "instance.idp.config.apple.added", "instance.idp.saml.added"}, @@ -1201,7 +1203,7 @@ func Test_executeQuery(t *testing.T) { time.Now(), "event.type", uint32(23), - float64(123), + decimal.NewFromInt(123).String(), uint32(0), nil, "gigi", @@ -1235,7 +1237,7 @@ func Test_executeQuery(t *testing.T) { time.Now(), "event.type", uint32(23), - float64(123), + decimal.NewFromInt(123).String(), uint32(0), []byte(`{"name": "gigi"}`), "gigi", @@ -1269,7 +1271,7 @@ func Test_executeQuery(t *testing.T) { time.Now(), "event.type", uint32(23), - float64(123), + decimal.NewFromInt(123).String(), uint32(0), nil, "gigi", @@ -1283,7 +1285,7 @@ func Test_executeQuery(t *testing.T) { time.Now(), "event.type", uint32(24), - float64(124), + decimal.NewFromInt(124).String(), uint32(0), []byte(`{"name": "gigi"}`), "gigi", @@ -1317,7 +1319,7 @@ func Test_executeQuery(t *testing.T) { time.Now(), "event.type", uint32(23), - float64(123), + decimal.NewFromInt(123).String(), uint32(0), nil, "gigi", @@ -1331,7 +1333,7 @@ func Test_executeQuery(t *testing.T) { time.Now(), "event.type", uint32(24), - float64(124), + decimal.NewFromInt(124).String(), uint32(0), []byte(`{"name": "gigi"}`), "gigi", diff --git a/internal/v2/eventstore/query.go b/internal/v2/eventstore/query.go index c9b3cecd37..f7a30a2139 100644 --- a/internal/v2/eventstore/query.go +++ b/internal/v2/eventstore/query.go @@ -7,6 +7,8 @@ import ( "slices" "time" + "github.com/shopspring/decimal" + "github.com/zitadel/zitadel/internal/v2/database" ) @@ -723,7 +725,7 @@ func (pc *PositionCondition) Min() *GlobalPosition { // PositionGreater prepares the condition as follows // if inPositionOrder is set: position = AND in_tx_order > OR or position > // if inPositionOrder is NOT set: position > -func PositionGreater(position float64, inPositionOrder uint32) paginationOpt { +func PositionGreater(position decimal.Decimal, inPositionOrder uint32) paginationOpt { return func(p *Pagination) { p.ensurePosition() p.position.min = &GlobalPosition{ @@ -743,7 +745,7 @@ func GlobalPositionGreater(position *GlobalPosition) paginationOpt { // PositionLess prepares the condition as follows // if inPositionOrder is set: position = AND in_tx_order > OR or position > // if inPositionOrder is NOT set: position > -func PositionLess(position float64, inPositionOrder uint32) paginationOpt { +func PositionLess(position decimal.Decimal, inPositionOrder uint32) paginationOpt { return func(p *Pagination) { p.ensurePosition() p.position.max = &GlobalPosition{ diff --git a/internal/v2/eventstore/query_test.go b/internal/v2/eventstore/query_test.go index 00c08914c1..0f313e9560 100644 --- a/internal/v2/eventstore/query_test.go +++ b/internal/v2/eventstore/query_test.go @@ -6,6 +6,8 @@ import ( "testing" "time" + "github.com/shopspring/decimal" + "github.com/zitadel/zitadel/internal/v2/database" ) @@ -74,13 +76,13 @@ func TestPaginationOpt(t *testing.T) { name: "global position greater", args: args{ opts: []paginationOpt{ - GlobalPositionGreater(&GlobalPosition{Position: 10}), + GlobalPositionGreater(&GlobalPosition{Position: decimal.NewFromInt(10)}), }, }, want: &Pagination{ position: &PositionCondition{ min: &GlobalPosition{ - Position: 10, + Position: decimal.NewFromInt(10), InPositionOrder: 0, }, }, @@ -90,13 +92,13 @@ func TestPaginationOpt(t *testing.T) { name: "position greater", args: args{ opts: []paginationOpt{ - PositionGreater(10, 0), + PositionGreater(decimal.NewFromInt(10), 0), }, }, want: &Pagination{ position: &PositionCondition{ min: &GlobalPosition{ - Position: 10, + Position: decimal.NewFromInt(10), InPositionOrder: 0, }, }, @@ -107,13 +109,13 @@ func TestPaginationOpt(t *testing.T) { name: "position less", args: args{ opts: []paginationOpt{ - PositionLess(10, 12), + PositionLess(decimal.NewFromInt(10), 12), }, }, want: &Pagination{ position: &PositionCondition{ max: &GlobalPosition{ - Position: 10, + Position: decimal.NewFromInt(10), InPositionOrder: 12, }, }, @@ -123,13 +125,13 @@ func TestPaginationOpt(t *testing.T) { name: "global position less", args: args{ opts: []paginationOpt{ - GlobalPositionLess(&GlobalPosition{Position: 12, InPositionOrder: 24}), + GlobalPositionLess(&GlobalPosition{Position: decimal.NewFromInt(12), InPositionOrder: 24}), }, }, want: &Pagination{ position: &PositionCondition{ max: &GlobalPosition{ - Position: 12, + Position: decimal.NewFromInt(12), InPositionOrder: 24, }, }, @@ -140,19 +142,19 @@ func TestPaginationOpt(t *testing.T) { args: args{ opts: []paginationOpt{ PositionBetween( - &GlobalPosition{10, 12}, - &GlobalPosition{20, 0}, + &GlobalPosition{decimal.NewFromInt(10), 12}, + &GlobalPosition{decimal.NewFromInt(20), 0}, ), }, }, want: &Pagination{ position: &PositionCondition{ min: &GlobalPosition{ - Position: 10, + Position: decimal.NewFromInt(10), InPositionOrder: 12, }, max: &GlobalPosition{ - Position: 20, + Position: decimal.NewFromInt(20), InPositionOrder: 0, }, }, diff --git a/internal/v2/readmodel/last_successful_mirror.go b/internal/v2/readmodel/last_successful_mirror.go index 80b436b896..ca7815b2a8 100644 --- a/internal/v2/readmodel/last_successful_mirror.go +++ b/internal/v2/readmodel/last_successful_mirror.go @@ -1,6 +1,8 @@ package readmodel import ( + "github.com/shopspring/decimal" + "github.com/zitadel/zitadel/internal/v2/eventstore" "github.com/zitadel/zitadel/internal/v2/system" "github.com/zitadel/zitadel/internal/v2/system/mirror" @@ -8,7 +10,7 @@ import ( type LastSuccessfulMirror struct { ID string - Position float64 + Position decimal.Decimal source string } @@ -34,6 +36,7 @@ func (p *LastSuccessfulMirror) Filter() *eventstore.Filter { ), eventstore.FilterPagination( eventstore.Descending(), + eventstore.Limit(1), ), ) } @@ -53,7 +56,7 @@ func (h *LastSuccessfulMirror) Reduce(events ...*eventstore.StorageEvent) (err e func (h *LastSuccessfulMirror) reduceSucceeded(event *eventstore.StorageEvent) error { // if position is set we skip all older events - if h.Position > 0 { + if h.Position.GreaterThan(decimal.NewFromInt(0)) { return nil } diff --git a/internal/v2/system/mirror/succeeded.go b/internal/v2/system/mirror/succeeded.go index 6d0fba2c25..34d74f184f 100644 --- a/internal/v2/system/mirror/succeeded.go +++ b/internal/v2/system/mirror/succeeded.go @@ -1,6 +1,8 @@ package mirror import ( + "github.com/shopspring/decimal" + "github.com/zitadel/zitadel/internal/v2/eventstore" "github.com/zitadel/zitadel/internal/zerrors" ) @@ -9,7 +11,7 @@ type succeededPayload struct { // Source is the name of the database data are mirrored from Source string `json:"source"` // Position until data will be mirrored - Position float64 `json:"position"` + Position decimal.Decimal `json:"position"` } const SucceededType = eventTypePrefix + "succeeded" @@ -38,7 +40,7 @@ func SucceededEventFromStorage(event *eventstore.StorageEvent) (e *SucceededEven }, nil } -func NewSucceededCommand(source string, position float64) *eventstore.Command { +func NewSucceededCommand(source string, position decimal.Decimal) *eventstore.Command { return &eventstore.Command{ Action: eventstore.Action[any]{ Creator: Creator, From 5e87fafadf1ecafdefc934e45eaa94055773fbb2 Mon Sep 17 00:00:00 2001 From: Maximilian Date: Thu, 29 May 2025 20:23:43 +0200 Subject: [PATCH 51/76] docs: fix broken link (#9988) # Which Problems Are Solved Broken links on the default settings page. # How the Problems Are Solved Fixed the reference # Additional Changes # Additional Context --- docs/docs/guides/manage/console/default-settings.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/guides/manage/console/default-settings.mdx b/docs/docs/guides/manage/console/default-settings.mdx index e8e36956a1..f255d15d93 100644 --- a/docs/docs/guides/manage/console/default-settings.mdx +++ b/docs/docs/guides/manage/console/default-settings.mdx @@ -17,7 +17,7 @@ When you configure your default settings, you can set the following: - **Organizations**: A list of your organizations - [**Features**](#features): Feature Settings let you try out new features before they become generally available. You can also disable features you are not interested in. -- [**Notification settings**](#notification-providers-and-smtp): Setup Notification and Email Server settings for initialization-, verification- and other mails. Setup Twilio as SMS notification provider. +- [**Notification settings**](#notification-settings): Setup Notification and Email Server settings for initialization-, verification- and other mails. Setup Twilio as SMS notification provider. - [**Login Behavior and Access**](#login-behavior-and-access): Multifactor Authentication Options and Enforcement, Define whether Passwordless authentication methods are allowed or not, Set Login Lifetimes and advanced behavour for the login interface. - [**Identity Providers**](#identity-providers): Define IDPs which are available for all organizations - [**Password Complexity**](#password-complexity): Requirements for Passwords ex. Symbols, Numbers, min length and more. From 93a92446bfa7e7a8b38aa32125e1020ae08c64f1 Mon Sep 17 00:00:00 2001 From: Florian Forster Date: Fri, 30 May 2025 01:15:28 -0700 Subject: [PATCH 52/76] chore: update docusaurus to 3.8.0 (#9974) > [!IMPORTANT] > We need to change the ENV `VERCEL_FORCE_NO_BUILD_CACHE` to `0` which is currently `1` to enable the cache on all deployments This pull request includes several updates to the documentation and benchmarking components, focusing on improving performance, error handling, and compatibility with newer versions of Docusaurus. The key changes include the removal of outdated configurations, updates to dependencies, and enhancements to the `BenchmarkChart` component for better error handling and data validation. ### Documentation and Configuration Updates: * **Removed outdated Babel and Webpack configurations**: The `babel.config.js` file was deleted, and the Webpack configuration was removed from `docusaurus.config.js` to align with the latest Docusaurus setup. [[1]](diffhunk://#diff-2ed4f5b03d34a87ef641e9e36af4a98a1c0ddaf74d07ce93665957be69b7b09aL1-L4) [[2]](diffhunk://#diff-28742c737e523f302e6de471b7fc27284dc8cf720be639e6afe4c17a550cd654L204-L225) * **Added experimental features in Docusaurus**: Introduced a `future` section in `docusaurus.config.js` to enable experimental features like `swcJsLoader`, `rspackBundler`, and `lightningCssMinimizer`, while disabling problematic settings due to known issues. ### Dependency Updates: * **Upgraded Docusaurus and related packages**: Updated dependencies in `package.json` to use Docusaurus version `^3.8.0` and newer versions of associated plugins and themes for improved performance and compatibility. [[1]](diffhunk://#diff-adfa337ce44dc2902621da20152a048dac41878cf3716dfc4cc56d03aa212a56L25-R39) [[2]](diffhunk://#diff-adfa337ce44dc2902621da20152a048dac41878cf3716dfc4cc56d03aa212a56L66-R67) ### Component Enhancements: * **Improved `BenchmarkChart` error handling**: Refactored the `BenchmarkChart` component to validate input data, handle errors gracefully, and provide meaningful fallback messages when data is missing or invalid. [[1]](diffhunk://#diff-ce9fccf51f6b863dd58a39f361a9cf980b10357bccc7381f928788483b30cb0eL4-R21) [[2]](diffhunk://#diff-ce9fccf51f6b863dd58a39f361a9cf980b10357bccc7381f928788483b30cb0eR72-R76) * **Fixed edge cases in chart rendering**: Addressed issues like invalid timestamps, undefined `p99` values, and empty data sets to ensure robust chart generation. [[1]](diffhunk://#diff-ce9fccf51f6b863dd58a39f361a9cf980b10357bccc7381f928788483b30cb0eL19-L29) [[2]](diffhunk://#diff-ce9fccf51f6b863dd58a39f361a9cf980b10357bccc7381f928788483b30cb0eL38-R61) ### Documentation Benchmark Updates: * **Simplified imports in benchmark files**: Replaced the use of `raw-loader` with direct imports for benchmark data in multiple `.mdx` files to streamline the documentation setup. [[1]](diffhunk://#diff-a9710709396e5ff6756aedf89dfcbd62aeea15368ba33bf3932ebf33046a29e8L66-R66) [[2]](diffhunk://#diff-0a9b6103c97c58792450bfd2d337bbb8a6b72df2ae326cc56ebc96e01c0acd6bL35-R35) [[3]](diffhunk://#diff-38f45388e065c57f1282a43bb319354da3c218e96d95ca20f4d11709f48491b8L36-R36) [[4]](diffhunk://#diff-b8e792ebe42fcb16a493e35d23b58a91c2117d949953487e70f379c64e5cb7c0L36-R36) [[5]](diffhunk://#diff-3778acfa893504004008b162fa95f21f1c7c40dcf1868bbbaaa504ac5d51901aL38-R38) --- docs/babel.config.js | 4 - docs/docs/apis/benchmarks/_template.mdx | 2 +- .../machine_jwt_profile_grant/index.mdx | 2 +- .../machine_jwt_profile_grant/index.mdx | 2 +- .../machine_jwt_profile_grant/index.mdx | 2 +- .../benchmarks/v2.70.0/oidc_session/index.mdx | 2 +- docs/docusaurus.config.js | 35 +- docs/package.json | 30 +- docs/src/components/benchmark_chart.jsx | 32 +- docs/yarn.lock | 5297 ++++++++++++----- 10 files changed, 3794 insertions(+), 1614 deletions(-) delete mode 100644 docs/babel.config.js diff --git a/docs/babel.config.js b/docs/babel.config.js deleted file mode 100644 index 279a0ff91c..0000000000 --- a/docs/babel.config.js +++ /dev/null @@ -1,4 +0,0 @@ -module.exports = { - presets: [require.resolve("@docusaurus/core/lib/babel/preset")], - compact: auto -}; diff --git a/docs/docs/apis/benchmarks/_template.mdx b/docs/docs/apis/benchmarks/_template.mdx index f015d20768..578ebcd842 100644 --- a/docs/docs/apis/benchmarks/_template.mdx +++ b/docs/docs/apis/benchmarks/_template.mdx @@ -63,7 +63,7 @@ TODO: describe the outcome of the test? ## Endpoint latencies -import OutputSource from "!!raw-loader!./output.json"; +import OutputSource from "./output.json"; import { BenchmarkChart } from '/src/components/benchmark_chart'; diff --git a/docs/docs/apis/benchmarks/v2.65.0/machine_jwt_profile_grant/index.mdx b/docs/docs/apis/benchmarks/v2.65.0/machine_jwt_profile_grant/index.mdx index 4c2809feb4..a8c10780ad 100644 --- a/docs/docs/apis/benchmarks/v2.65.0/machine_jwt_profile_grant/index.mdx +++ b/docs/docs/apis/benchmarks/v2.65.0/machine_jwt_profile_grant/index.mdx @@ -32,7 +32,7 @@ Tests are halted after this test run because of too many [client read events](ht ## /token endpoint latencies -import OutputSource from "!!raw-loader!./output.json"; +import OutputSource from "./output.json"; import { BenchmarkChart } from '/src/components/benchmark_chart'; diff --git a/docs/docs/apis/benchmarks/v2.66.0/machine_jwt_profile_grant/index.mdx b/docs/docs/apis/benchmarks/v2.66.0/machine_jwt_profile_grant/index.mdx index 881e7a38ee..6ab40eb4d4 100644 --- a/docs/docs/apis/benchmarks/v2.66.0/machine_jwt_profile_grant/index.mdx +++ b/docs/docs/apis/benchmarks/v2.66.0/machine_jwt_profile_grant/index.mdx @@ -33,7 +33,7 @@ The tests showed heavy database load by time by the first two database queries. ## Endpoint latencies -import OutputSource from "!!raw-loader!./output.json"; +import OutputSource from "./output.json"; import { BenchmarkChart } from '/src/components/benchmark_chart'; diff --git a/docs/docs/apis/benchmarks/v2.70.0/machine_jwt_profile_grant/index.mdx b/docs/docs/apis/benchmarks/v2.70.0/machine_jwt_profile_grant/index.mdx index fa8c84bc7e..d4fd2708a8 100644 --- a/docs/docs/apis/benchmarks/v2.70.0/machine_jwt_profile_grant/index.mdx +++ b/docs/docs/apis/benchmarks/v2.70.0/machine_jwt_profile_grant/index.mdx @@ -33,7 +33,7 @@ The performance goals of [this issue](https://github.com/zitadel/zitadel/issues/ ## Endpoint latencies -import OutputSource from "!!raw-loader!./output.json"; +import OutputSource from "./output.json"; import { BenchmarkChart } from '/src/components/benchmark_chart'; diff --git a/docs/docs/apis/benchmarks/v2.70.0/oidc_session/index.mdx b/docs/docs/apis/benchmarks/v2.70.0/oidc_session/index.mdx index 4615413d2b..94fd83f119 100644 --- a/docs/docs/apis/benchmarks/v2.70.0/oidc_session/index.mdx +++ b/docs/docs/apis/benchmarks/v2.70.0/oidc_session/index.mdx @@ -35,7 +35,7 @@ The tests showed that querying the user takes too much time because Zitadel ensu ## Endpoint latencies -import OutputSource from "!!raw-loader!./output.json"; +import OutputSource from "./output.json"; import { BenchmarkChart } from '/src/components/benchmark_chart'; diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js index 22df468475..c161d38d9f 100644 --- a/docs/docusaurus.config.js +++ b/docs/docusaurus.config.js @@ -201,28 +201,6 @@ module.exports = { runmeLinkLabel: 'Checkout via Runme' }, }, - webpack: { - jsLoader: (isServer) => ({ - loader: require.resolve('swc-loader'), - options: { - jsc: { - parser: { - syntax: 'typescript', - tsx: true, - }, - transform: { - react: { - runtime: 'automatic', - }, - }, - target: 'es2017', - }, - module: { - type: isServer ? 'commonjs' : 'es6', - }, - }, - }), - }, presets: [ [ "classic", @@ -397,4 +375,17 @@ module.exports = { }, ], themes: [ "docusaurus-theme-github-codeblock", "docusaurus-theme-openapi-docs"], + future: { + v4: false, // Disabled because of some problems related to https://github.com/facebook/docusaurus/issues/11040 + experimental_faster: { + swcJsLoader: false, // Disabled because of memory usage > 8GB which is a problem on vercel default runners + swcJsMinimizer: true, + swcHtmlMinimizer : true, + lightningCssMinimizer: true, + mdxCrossCompilerCache: true, + ssgWorkerThreads: false, // Disabled because of some problems related to https://github.com/facebook/docusaurus/issues/11040 + rspackBundler: true, + rspackPersistentCache: true, + }, + }, }; diff --git a/docs/package.json b/docs/package.json index f9636418dd..014a8ec0ca 100644 --- a/docs/package.json +++ b/docs/package.json @@ -6,7 +6,7 @@ "docusaurus": "docusaurus", "start": "docusaurus start", "start:api": "yarn run generate && docusaurus start", - "build": "yarn run generate && NODE_OPTIONS=--max-old-space-size=8192 docusaurus build", + "build": "yarn run generate && docusaurus build", "swizzle": "docusaurus swizzle", "deploy": "docusaurus deploy", "clear": "docusaurus clear", @@ -22,33 +22,27 @@ }, "dependencies": { "@bufbuild/buf": "^1.14.0", - "@docusaurus/core": "3.4.0", - "@docusaurus/preset-classic": "3.4.0", - "@docusaurus/theme-mermaid": "3.4.0", - "@docusaurus/theme-search-algolia": "3.4.0", + "@docusaurus/core": "^3.8.0", + "@docusaurus/faster": "^3.8.0", + "@docusaurus/preset-classic": "^3.8.0", + "@docusaurus/theme-mermaid": "^3.8.0", + "@docusaurus/theme-search-algolia": "^3.8.0", "@headlessui/react": "^1.7.4", "@heroicons/react": "^2.0.13", - "@mdx-js/react": "^3.0.0", - "@swc/core": "^1.3.74", "autoprefixer": "^10.4.13", "clsx": "^1.2.1", - "docusaurus-plugin-image-zoom": "^1.0.1", - "docusaurus-plugin-openapi-docs": "3.0.1", + "docusaurus-plugin-image-zoom": "^3.0.1", + "docusaurus-plugin-openapi-docs": "4.4.0", "docusaurus-theme-github-codeblock": "^2.0.2", - "docusaurus-theme-openapi-docs": "3.0.1", + "docusaurus-theme-openapi-docs": "4.4.0", "mdx-mermaid": "^2.0.0", - "mermaid": "^10.9.1", "postcss": "^8.4.31", - "prism-react-renderer": "^2.1.0", "raw-loader": "^4.0.2", "react": "^18.2.0", "react-copy-to-clipboard": "^5.1.0", "react-dom": "^18.2.0", "react-google-charts": "^5.2.1", - "react-player": "^2.15.1", - "sitemap": "7.1.1", - "swc-loader": "^0.2.3", - "wait-on": "6.0.1" + "react-player": "^2.15.1" }, "browserslist": { "production": [ @@ -63,8 +57,8 @@ ] }, "devDependencies": { - "@docusaurus/module-type-aliases": "3.4.0", - "@docusaurus/types": "3.4.0", + "@docusaurus/module-type-aliases": "^3.8.0", + "@docusaurus/types": "^3.8.0", "tailwindcss": "^3.2.4" }, "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" diff --git a/docs/src/components/benchmark_chart.jsx b/docs/src/components/benchmark_chart.jsx index 4f0d4bc61c..cf93a842ef 100644 --- a/docs/src/components/benchmark_chart.jsx +++ b/docs/src/components/benchmark_chart.jsx @@ -1,11 +1,24 @@ import React from "react"; import Chart from "react-google-charts"; -export function BenchmarkChart(testResults=[], height='500px') { +export function BenchmarkChart({ testResults = [], height = '500px' } = {}) { + if (!Array.isArray(testResults)) { + console.error("BenchmarkChart: testResults is not an array. Received:", testResults); + return

Error: Benchmark data is not available or in the wrong format.

; + } + + if (testResults.length === 0) { + return

No benchmark data to display.

; + } + const dataPerMetric = new Map(); let maxVValue = 0; - JSON.parse(testResults.testResults).forEach((result) => { + testResults.forEach((result) => { + if (!result || typeof result.metric_name === 'undefined') { + console.warn("BenchmarkChart: Skipping invalid result item:", result); + return; + } if (!dataPerMetric.has(result.metric_name)) { dataPerMetric.set(result.metric_name, [ [ @@ -16,17 +29,16 @@ export function BenchmarkChart(testResults=[], height='500px') { ], ]); } - if (result.p99 > maxVValue) { + if (result.p99 !== undefined && result.p99 > maxVValue) { maxVValue = result.p99; } dataPerMetric.get(result.metric_name).push([ - new Date(result.timestamp), + result.timestamp ? new Date(result.timestamp) : null, result.p50, result.p95, result.p99, ]); }); - const options = { legend: { position: 'bottom' }, focusTarget: 'category', @@ -35,17 +47,18 @@ export function BenchmarkChart(testResults=[], height='500px') { }, vAxis: { title: 'latency (ms)', - maxValue: maxVValue, + maxValue: maxVValue > 0 ? maxVValue : undefined, }, title: '' }; const charts = []; dataPerMetric.forEach((data, metric) => { - const opt = Object.create(options); + const opt = { ...options }; opt.title = metric; charts.push( No chart data could be generated.

; + } - return (charts); + return <>{charts}; } \ No newline at end of file diff --git a/docs/yarn.lock b/docs/yarn.lock index ad31e03b5e..70f2de1f05 100644 --- a/docs/yarn.lock +++ b/docs/yarn.lock @@ -2,158 +2,153 @@ # yarn lockfile v1 -"@algolia/autocomplete-core@1.9.3": - version "1.9.3" - resolved "https://registry.yarnpkg.com/@algolia/autocomplete-core/-/autocomplete-core-1.9.3.tgz#1d56482a768c33aae0868c8533049e02e8961be7" - integrity sha512-009HdfugtGCdC4JdXUbVJClA0q0zh24yyePn+KUGk3rP7j8FEe/m5Yo/z65gn6nP/cM39PxpzqKrL7A6fP6PPw== +"@algolia/autocomplete-core@1.17.9": + version "1.17.9" + resolved "https://registry.yarnpkg.com/@algolia/autocomplete-core/-/autocomplete-core-1.17.9.tgz#83374c47dc72482aa45d6b953e89377047f0dcdc" + integrity sha512-O7BxrpLDPJWWHv/DLA9DRFWs+iY1uOJZkqUwjS5HSZAGcl0hIVCQ97LTLewiZmZ402JYUrun+8NqFP+hCknlbQ== dependencies: - "@algolia/autocomplete-plugin-algolia-insights" "1.9.3" - "@algolia/autocomplete-shared" "1.9.3" + "@algolia/autocomplete-plugin-algolia-insights" "1.17.9" + "@algolia/autocomplete-shared" "1.17.9" -"@algolia/autocomplete-plugin-algolia-insights@1.9.3": - version "1.9.3" - resolved "https://registry.yarnpkg.com/@algolia/autocomplete-plugin-algolia-insights/-/autocomplete-plugin-algolia-insights-1.9.3.tgz#9b7f8641052c8ead6d66c1623d444cbe19dde587" - integrity sha512-a/yTUkcO/Vyy+JffmAnTWbr4/90cLzw+CC3bRbhnULr/EM0fGNvM13oQQ14f2moLMcVDyAx/leczLlAOovhSZg== +"@algolia/autocomplete-plugin-algolia-insights@1.17.9": + version "1.17.9" + resolved "https://registry.yarnpkg.com/@algolia/autocomplete-plugin-algolia-insights/-/autocomplete-plugin-algolia-insights-1.17.9.tgz#74c86024d09d09e8bfa3dd90b844b77d9f9947b6" + integrity sha512-u1fEHkCbWF92DBeB/KHeMacsjsoI0wFhjZtlCq2ddZbAehshbZST6Hs0Avkc0s+4UyBGbMDnSuXHLuvRWK5iDQ== dependencies: - "@algolia/autocomplete-shared" "1.9.3" + "@algolia/autocomplete-shared" "1.17.9" -"@algolia/autocomplete-preset-algolia@1.9.3": - version "1.9.3" - resolved "https://registry.yarnpkg.com/@algolia/autocomplete-preset-algolia/-/autocomplete-preset-algolia-1.9.3.tgz#64cca4a4304cfcad2cf730e83067e0c1b2f485da" - integrity sha512-d4qlt6YmrLMYy95n5TB52wtNDr6EgAIPH81dvvvW8UmuWRgxEtY0NJiPwl/h95JtG2vmRM804M0DSwMCNZlzRA== +"@algolia/autocomplete-preset-algolia@1.17.9": + version "1.17.9" + resolved "https://registry.yarnpkg.com/@algolia/autocomplete-preset-algolia/-/autocomplete-preset-algolia-1.17.9.tgz#911f3250544eb8ea4096fcfb268f156b085321b5" + integrity sha512-Na1OuceSJeg8j7ZWn5ssMu/Ax3amtOwk76u4h5J4eK2Nx2KB5qt0Z4cOapCsxot9VcEN11ADV5aUSlQF4RhGjQ== dependencies: - "@algolia/autocomplete-shared" "1.9.3" + "@algolia/autocomplete-shared" "1.17.9" -"@algolia/autocomplete-shared@1.9.3": - version "1.9.3" - resolved "https://registry.yarnpkg.com/@algolia/autocomplete-shared/-/autocomplete-shared-1.9.3.tgz#2e22e830d36f0a9cf2c0ccd3c7f6d59435b77dfa" - integrity sha512-Wnm9E4Ye6Rl6sTTqjoymD+l8DjSTHsHboVRYrKgEt8Q7UHm9nYbqhN/i0fhUYA3OAEH7WA8x3jfpnmJm3rKvaQ== +"@algolia/autocomplete-shared@1.17.9": + version "1.17.9" + resolved "https://registry.yarnpkg.com/@algolia/autocomplete-shared/-/autocomplete-shared-1.17.9.tgz#5f38868f7cb1d54b014b17a10fc4f7e79d427fa8" + integrity sha512-iDf05JDQ7I0b7JEA/9IektxN/80a2MZ1ToohfmNS3rfeuQnIKI3IJlIafD0xu4StbtQTghx9T3Maa97ytkXenQ== -"@algolia/cache-browser-local-storage@4.23.3": - version "4.23.3" - resolved "https://registry.yarnpkg.com/@algolia/cache-browser-local-storage/-/cache-browser-local-storage-4.23.3.tgz#0cc26b96085e1115dac5fcb9d826651ba57faabc" - integrity sha512-vRHXYCpPlTDE7i6UOy2xE03zHF2C8MEFjPN2v7fRbqVpcOvAUQK81x3Kc21xyb5aSIpYCjWCZbYZuz8Glyzyyg== +"@algolia/client-abtesting@5.25.0": + version "5.25.0" + resolved "https://registry.yarnpkg.com/@algolia/client-abtesting/-/client-abtesting-5.25.0.tgz#012204f1614e1a71366fb1e117c8f195186ff081" + integrity sha512-1pfQulNUYNf1Tk/svbfjfkLBS36zsuph6m+B6gDkPEivFmso/XnRgwDvjAx80WNtiHnmeNjIXdF7Gos8+OLHqQ== dependencies: - "@algolia/cache-common" "4.23.3" + "@algolia/client-common" "5.25.0" + "@algolia/requester-browser-xhr" "5.25.0" + "@algolia/requester-fetch" "5.25.0" + "@algolia/requester-node-http" "5.25.0" -"@algolia/cache-common@4.23.3": - version "4.23.3" - resolved "https://registry.yarnpkg.com/@algolia/cache-common/-/cache-common-4.23.3.tgz#3bec79092d512a96c9bfbdeec7cff4ad36367166" - integrity sha512-h9XcNI6lxYStaw32pHpB1TMm0RuxphF+Ik4o7tcQiodEdpKK+wKufY6QXtba7t3k8eseirEMVB83uFFF3Nu54A== - -"@algolia/cache-in-memory@4.23.3": - version "4.23.3" - resolved "https://registry.yarnpkg.com/@algolia/cache-in-memory/-/cache-in-memory-4.23.3.tgz#3945f87cd21ffa2bec23890c85305b6b11192423" - integrity sha512-yvpbuUXg/+0rbcagxNT7un0eo3czx2Uf0y4eiR4z4SD7SiptwYTpbuS0IHxcLHG3lq22ukx1T6Kjtk/rT+mqNg== +"@algolia/client-analytics@5.25.0": + version "5.25.0" + resolved "https://registry.yarnpkg.com/@algolia/client-analytics/-/client-analytics-5.25.0.tgz#eba015bfafb3dbb82712c9160a00717a5974ff71" + integrity sha512-AFbG6VDJX/o2vDd9hqncj1B6B4Tulk61mY0pzTtzKClyTDlNP0xaUiEKhl6E7KO9I/x0FJF5tDCm0Hn6v5x18A== dependencies: - "@algolia/cache-common" "4.23.3" + "@algolia/client-common" "5.25.0" + "@algolia/requester-browser-xhr" "5.25.0" + "@algolia/requester-fetch" "5.25.0" + "@algolia/requester-node-http" "5.25.0" -"@algolia/client-account@4.23.3": - version "4.23.3" - resolved "https://registry.yarnpkg.com/@algolia/client-account/-/client-account-4.23.3.tgz#8751bbf636e6741c95e7c778488dee3ee430ac6f" - integrity sha512-hpa6S5d7iQmretHHF40QGq6hz0anWEHGlULcTIT9tbUssWUriN9AUXIFQ8Ei4w9azD0hc1rUok9/DeQQobhQMA== - dependencies: - "@algolia/client-common" "4.23.3" - "@algolia/client-search" "4.23.3" - "@algolia/transporter" "4.23.3" +"@algolia/client-common@5.25.0": + version "5.25.0" + resolved "https://registry.yarnpkg.com/@algolia/client-common/-/client-common-5.25.0.tgz#2def8947efe849266057d92f67d1b8d83de0c005" + integrity sha512-il1zS/+Rc6la6RaCdSZ2YbJnkQC6W1wiBO8+SH+DE6CPMWBU6iDVzH0sCKSAtMWl9WBxoN6MhNjGBnCv9Yy2bA== -"@algolia/client-analytics@4.23.3": - version "4.23.3" - resolved "https://registry.yarnpkg.com/@algolia/client-analytics/-/client-analytics-4.23.3.tgz#f88710885278fe6fb6964384af59004a5a6f161d" - integrity sha512-LBsEARGS9cj8VkTAVEZphjxTjMVCci+zIIiRhpFun9jGDUlS1XmhCW7CTrnaWeIuCQS/2iPyRqSy1nXPjcBLRA== +"@algolia/client-insights@5.25.0": + version "5.25.0" + resolved "https://registry.yarnpkg.com/@algolia/client-insights/-/client-insights-5.25.0.tgz#b87df8614b96c4cc9c9aa7765cce07fa70864fa8" + integrity sha512-blbjrUH1siZNfyCGeq0iLQu00w3a4fBXm0WRIM0V8alcAPo7rWjLbMJMrfBtzL9X5ic6wgxVpDADXduGtdrnkw== dependencies: - "@algolia/client-common" "4.23.3" - "@algolia/client-search" "4.23.3" - "@algolia/requester-common" "4.23.3" - "@algolia/transporter" "4.23.3" + "@algolia/client-common" "5.25.0" + "@algolia/requester-browser-xhr" "5.25.0" + "@algolia/requester-fetch" "5.25.0" + "@algolia/requester-node-http" "5.25.0" -"@algolia/client-common@4.23.3": - version "4.23.3" - resolved "https://registry.yarnpkg.com/@algolia/client-common/-/client-common-4.23.3.tgz#891116aa0db75055a7ecc107649f7f0965774704" - integrity sha512-l6EiPxdAlg8CYhroqS5ybfIczsGUIAC47slLPOMDeKSVXYG1n0qGiz4RjAHLw2aD0xzh2EXZ7aRguPfz7UKDKw== +"@algolia/client-personalization@5.25.0": + version "5.25.0" + resolved "https://registry.yarnpkg.com/@algolia/client-personalization/-/client-personalization-5.25.0.tgz#74b041f0e7d91e1009c131c8d716c34e4d45c30f" + integrity sha512-aywoEuu1NxChBcHZ1pWaat0Plw7A8jDMwjgRJ00Mcl7wGlwuPt5dJ/LTNcg3McsEUbs2MBNmw0ignXBw9Tbgow== dependencies: - "@algolia/requester-common" "4.23.3" - "@algolia/transporter" "4.23.3" + "@algolia/client-common" "5.25.0" + "@algolia/requester-browser-xhr" "5.25.0" + "@algolia/requester-fetch" "5.25.0" + "@algolia/requester-node-http" "5.25.0" -"@algolia/client-personalization@4.23.3": - version "4.23.3" - resolved "https://registry.yarnpkg.com/@algolia/client-personalization/-/client-personalization-4.23.3.tgz#35fa8e5699b0295fbc400a8eb211dc711e5909db" - integrity sha512-3E3yF3Ocr1tB/xOZiuC3doHQBQ2zu2MPTYZ0d4lpfWads2WTKG7ZzmGnsHmm63RflvDeLK/UVx7j2b3QuwKQ2g== +"@algolia/client-query-suggestions@5.25.0": + version "5.25.0" + resolved "https://registry.yarnpkg.com/@algolia/client-query-suggestions/-/client-query-suggestions-5.25.0.tgz#e92d935d9e2994f790d43c64d3518d81070a3888" + integrity sha512-a/W2z6XWKjKjIW1QQQV8PTTj1TXtaKx79uR3NGBdBdGvVdt24KzGAaN7sCr5oP8DW4D3cJt44wp2OY/fZcPAVA== dependencies: - "@algolia/client-common" "4.23.3" - "@algolia/requester-common" "4.23.3" - "@algolia/transporter" "4.23.3" + "@algolia/client-common" "5.25.0" + "@algolia/requester-browser-xhr" "5.25.0" + "@algolia/requester-fetch" "5.25.0" + "@algolia/requester-node-http" "5.25.0" -"@algolia/client-search@4.23.3": - version "4.23.3" - resolved "https://registry.yarnpkg.com/@algolia/client-search/-/client-search-4.23.3.tgz#a3486e6af13a231ec4ab43a915a1f318787b937f" - integrity sha512-P4VAKFHqU0wx9O+q29Q8YVuaowaZ5EM77rxfmGnkHUJggh28useXQdopokgwMeYw2XUht49WX5RcTQ40rZIabw== +"@algolia/client-search@5.25.0": + version "5.25.0" + resolved "https://registry.yarnpkg.com/@algolia/client-search/-/client-search-5.25.0.tgz#dc38ca1015f2f4c9f5053a4517f96fb28a2117f8" + integrity sha512-9rUYcMIBOrCtYiLX49djyzxqdK9Dya/6Z/8sebPn94BekT+KLOpaZCuc6s0Fpfq7nx5J6YY5LIVFQrtioK9u0g== dependencies: - "@algolia/client-common" "4.23.3" - "@algolia/requester-common" "4.23.3" - "@algolia/transporter" "4.23.3" + "@algolia/client-common" "5.25.0" + "@algolia/requester-browser-xhr" "5.25.0" + "@algolia/requester-fetch" "5.25.0" + "@algolia/requester-node-http" "5.25.0" "@algolia/events@^4.0.1": version "4.0.1" resolved "https://registry.yarnpkg.com/@algolia/events/-/events-4.0.1.tgz#fd39e7477e7bc703d7f893b556f676c032af3950" integrity sha512-FQzvOCgoFXAbf5Y6mYozw2aj5KCJoA3m4heImceldzPSMbdyS4atVjJzXKMsfX3wnZTFYwkkt8/z8UesLHlSBQ== -"@algolia/logger-common@4.23.3": - version "4.23.3" - resolved "https://registry.yarnpkg.com/@algolia/logger-common/-/logger-common-4.23.3.tgz#35c6d833cbf41e853a4f36ba37c6e5864920bfe9" - integrity sha512-y9kBtmJwiZ9ZZ+1Ek66P0M68mHQzKRxkW5kAAXYN/rdzgDN0d2COsViEFufxJ0pb45K4FRcfC7+33YB4BLrZ+g== - -"@algolia/logger-console@4.23.3": - version "4.23.3" - resolved "https://registry.yarnpkg.com/@algolia/logger-console/-/logger-console-4.23.3.tgz#30f916781826c4db5f51fcd9a8a264a06e136985" - integrity sha512-8xoiseoWDKuCVnWP8jHthgaeobDLolh00KJAdMe9XPrWPuf1by732jSpgy2BlsLTaT9m32pHI8CRfrOqQzHv3A== +"@algolia/ingestion@1.25.0": + version "1.25.0" + resolved "https://registry.yarnpkg.com/@algolia/ingestion/-/ingestion-1.25.0.tgz#4d13c56dda0a05c7bacb0e3ef5866292dfd86ed5" + integrity sha512-jJeH/Hk+k17Vkokf02lkfYE4A+EJX+UgnMhTLR/Mb+d1ya5WhE+po8p5a/Nxb6lo9OLCRl6w3Hmk1TX1e9gVbQ== dependencies: - "@algolia/logger-common" "4.23.3" + "@algolia/client-common" "5.25.0" + "@algolia/requester-browser-xhr" "5.25.0" + "@algolia/requester-fetch" "5.25.0" + "@algolia/requester-node-http" "5.25.0" -"@algolia/recommend@4.23.3": - version "4.23.3" - resolved "https://registry.yarnpkg.com/@algolia/recommend/-/recommend-4.23.3.tgz#53d4f194d22d9c72dc05f3f7514c5878f87c5890" - integrity sha512-9fK4nXZF0bFkdcLBRDexsnGzVmu4TSYZqxdpgBW2tEyfuSSY54D4qSRkLmNkrrz4YFvdh2GM1gA8vSsnZPR73w== +"@algolia/monitoring@1.25.0": + version "1.25.0" + resolved "https://registry.yarnpkg.com/@algolia/monitoring/-/monitoring-1.25.0.tgz#d59360cfe556338519d05a9d8107147e9dbcb020" + integrity sha512-Ls3i1AehJ0C6xaHe7kK9vPmzImOn5zBg7Kzj8tRYIcmCWVyuuFwCIsbuIIz/qzUf1FPSWmw0TZrGeTumk2fqXg== dependencies: - "@algolia/cache-browser-local-storage" "4.23.3" - "@algolia/cache-common" "4.23.3" - "@algolia/cache-in-memory" "4.23.3" - "@algolia/client-common" "4.23.3" - "@algolia/client-search" "4.23.3" - "@algolia/logger-common" "4.23.3" - "@algolia/logger-console" "4.23.3" - "@algolia/requester-browser-xhr" "4.23.3" - "@algolia/requester-common" "4.23.3" - "@algolia/requester-node-http" "4.23.3" - "@algolia/transporter" "4.23.3" + "@algolia/client-common" "5.25.0" + "@algolia/requester-browser-xhr" "5.25.0" + "@algolia/requester-fetch" "5.25.0" + "@algolia/requester-node-http" "5.25.0" -"@algolia/requester-browser-xhr@4.23.3": - version "4.23.3" - resolved "https://registry.yarnpkg.com/@algolia/requester-browser-xhr/-/requester-browser-xhr-4.23.3.tgz#9e47e76f60d540acc8b27b4ebc7a80d1b41938b9" - integrity sha512-jDWGIQ96BhXbmONAQsasIpTYWslyjkiGu0Quydjlowe+ciqySpiDUrJHERIRfELE5+wFc7hc1Q5hqjGoV7yghw== +"@algolia/recommend@5.25.0": + version "5.25.0" + resolved "https://registry.yarnpkg.com/@algolia/recommend/-/recommend-5.25.0.tgz#b96f12c85aa74a0326982c7801fcd4a610b420f4" + integrity sha512-79sMdHpiRLXVxSjgw7Pt4R1aNUHxFLHiaTDnN2MQjHwJ1+o3wSseb55T9VXU4kqy3m7TUme3pyRhLk5ip/S4Mw== dependencies: - "@algolia/requester-common" "4.23.3" + "@algolia/client-common" "5.25.0" + "@algolia/requester-browser-xhr" "5.25.0" + "@algolia/requester-fetch" "5.25.0" + "@algolia/requester-node-http" "5.25.0" -"@algolia/requester-common@4.23.3": - version "4.23.3" - resolved "https://registry.yarnpkg.com/@algolia/requester-common/-/requester-common-4.23.3.tgz#7dbae896e41adfaaf1d1fa5f317f83a99afb04b3" - integrity sha512-xloIdr/bedtYEGcXCiF2muajyvRhwop4cMZo+K2qzNht0CMzlRkm8YsDdj5IaBhshqfgmBb3rTg4sL4/PpvLYw== - -"@algolia/requester-node-http@4.23.3": - version "4.23.3" - resolved "https://registry.yarnpkg.com/@algolia/requester-node-http/-/requester-node-http-4.23.3.tgz#c9f94a5cb96a15f48cea338ab6ef16bbd0ff989f" - integrity sha512-zgu++8Uj03IWDEJM3fuNl34s746JnZOWn1Uz5taV1dFyJhVM/kTNw9Ik7YJWiUNHJQXcaD8IXD1eCb0nq/aByA== +"@algolia/requester-browser-xhr@5.25.0": + version "5.25.0" + resolved "https://registry.yarnpkg.com/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.25.0.tgz#c194fa5f49206b9343e6646c41bfbca2a3f2ac54" + integrity sha512-JLaF23p1SOPBmfEqozUAgKHQrGl3z/Z5RHbggBu6s07QqXXcazEsub5VLonCxGVqTv6a61AAPr8J1G5HgGGjEw== dependencies: - "@algolia/requester-common" "4.23.3" + "@algolia/client-common" "5.25.0" -"@algolia/transporter@4.23.3": - version "4.23.3" - resolved "https://registry.yarnpkg.com/@algolia/transporter/-/transporter-4.23.3.tgz#545b045b67db3850ddf0bbecbc6c84ff1f3398b7" - integrity sha512-Wjl5gttqnf/gQKJA+dafnD0Y6Yw97yvfY8R9h0dQltX1GXTgNs1zWgvtWW0tHl1EgMdhAyw189uWiZMnL3QebQ== +"@algolia/requester-fetch@5.25.0": + version "5.25.0" + resolved "https://registry.yarnpkg.com/@algolia/requester-fetch/-/requester-fetch-5.25.0.tgz#231a2d0da2397d141f80b8f28e2cb6e3d219d38d" + integrity sha512-rtzXwqzFi1edkOF6sXxq+HhmRKDy7tz84u0o5t1fXwz0cwx+cjpmxu/6OQKTdOJFS92JUYHsG51Iunie7xbqfQ== dependencies: - "@algolia/cache-common" "4.23.3" - "@algolia/logger-common" "4.23.3" - "@algolia/requester-common" "4.23.3" + "@algolia/client-common" "5.25.0" + +"@algolia/requester-node-http@5.25.0": + version "5.25.0" + resolved "https://registry.yarnpkg.com/@algolia/requester-node-http/-/requester-node-http-5.25.0.tgz#0ce13c550890de21c558b04381535d2d245a3725" + integrity sha512-ZO0UKvDyEFvyeJQX0gmZDQEvhLZ2X10K+ps6hViMo1HgE2V8em00SwNsQ+7E/52a+YiBkVWX61pJJJE44juDMQ== + dependencies: + "@algolia/client-common" "5.25.0" "@alloc/quick-lru@^5.2.0": version "5.2.0" @@ -168,6 +163,19 @@ "@jridgewell/gen-mapping" "^0.3.5" "@jridgewell/trace-mapping" "^0.3.24" +"@antfu/install-pkg@^1.0.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@antfu/install-pkg/-/install-pkg-1.1.0.tgz#78fa036be1a6081b5a77a5cf59f50c7752b6ba26" + integrity sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ== + dependencies: + package-manager-detector "^1.3.0" + tinyexec "^1.0.1" + +"@antfu/utils@^8.1.0": + version "8.1.1" + resolved "https://registry.yarnpkg.com/@antfu/utils/-/utils-8.1.1.tgz#95b1947d292a9a2efffba2081796dcaa05ecedfb" + integrity sha512-Mex9nXf9vR6AhcXmMrlz/HVgYYZpVGJ6YlPgwl7UnaFpnshXs6EK/oa5Gpf3CzENMjkvEx2tQtntGnb7UtSTOQ== + "@apidevtools/json-schema-ref-parser@^11.5.4": version "11.6.4" resolved "https://registry.yarnpkg.com/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-11.6.4.tgz#0f3e02302f646471d621a8850e6a346d63c8ebd4" @@ -177,7 +185,7 @@ "@types/json-schema" "^7.0.15" js-yaml "^4.1.0" -"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.16.0", "@babel/code-frame@^7.24.7", "@babel/code-frame@^7.8.3": +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.24.7.tgz#882fd9e09e8ee324e496bd040401c6f046ef4465" integrity sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA== @@ -194,12 +202,26 @@ js-tokens "^4.0.0" picocolors "^1.0.0" +"@babel/code-frame@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.27.1.tgz#200f715e66d52a23b221a9435534a91cc13ad5be" + integrity sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg== + dependencies: + "@babel/helper-validator-identifier" "^7.27.1" + js-tokens "^4.0.0" + picocolors "^1.1.1" + "@babel/compat-data@^7.22.6", "@babel/compat-data@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.24.7.tgz#d23bbea508c3883ba8251fb4164982c36ea577ed" integrity sha512-qJzAIcv03PyaWqxRgO4mSU3lihncDT296vnyuE2O8uA4w3UHWI4S3hgeZd1L8W1Bft40w9JxJ2b412iDUFFRhw== -"@babel/core@^7.21.3", "@babel/core@^7.23.3": +"@babel/compat-data@^7.27.2": + version "7.27.3" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.27.3.tgz#cc49c2ac222d69b889bf34c795f537c0c6311111" + integrity sha512-V42wFfx1ymFte+ecf6iXghnnP8kWTO+ZLXIyZq+1LAXHHvTZdVxicn4yiVYdYMGaCO3tmqub11AorKkv+iodqw== + +"@babel/core@^7.21.3": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.24.7.tgz#b676450141e0b52a3d43bc91da86aa608f950ac4" integrity sha512-nykK+LEK86ahTkX/3TgauT0ikKoNCfKHEaZYTUVupJdTLzGNvrblu4u6fa7DhZONAltdf8e662t/abY8idrd/g== @@ -220,7 +242,28 @@ json5 "^2.2.3" semver "^6.3.1" -"@babel/generator@^7.23.3", "@babel/generator@^7.24.7": +"@babel/core@^7.25.9": + version "7.27.3" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.27.3.tgz#d7d05502bccede3cab36373ed142e6a1df554c2f" + integrity sha512-hyrN8ivxfvJ4i0fIJuV4EOlV0WDMz5Ui4StRTgVaAvWeiRCilXgwVvxJKtFQ3TKtHgJscB2YiXKGNJuVwhQMtA== + dependencies: + "@ampproject/remapping" "^2.2.0" + "@babel/code-frame" "^7.27.1" + "@babel/generator" "^7.27.3" + "@babel/helper-compilation-targets" "^7.27.2" + "@babel/helper-module-transforms" "^7.27.3" + "@babel/helpers" "^7.27.3" + "@babel/parser" "^7.27.3" + "@babel/template" "^7.27.2" + "@babel/traverse" "^7.27.3" + "@babel/types" "^7.27.3" + convert-source-map "^2.0.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.2.3" + semver "^6.3.1" + +"@babel/generator@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.24.7.tgz#1654d01de20ad66b4b4d99c135471bc654c55e6d" integrity sha512-oipXieGC3i45Y1A41t4tAqpnEZWgB/lC6Ehh6+rOviR5XWpTtMmLN+fGjz9vOiNRt0p6RtO6DtD0pdU3vpqdSA== @@ -230,6 +273,17 @@ "@jridgewell/trace-mapping" "^0.3.25" jsesc "^2.5.1" +"@babel/generator@^7.25.9", "@babel/generator@^7.27.3": + version "7.27.3" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.27.3.tgz#ef1c0f7cfe3b5fc8cbb9f6cc69f93441a68edefc" + integrity sha512-xnlJYj5zepml8NXtjkG0WquFUv8RskFqyFcVgTBp5k+NaA/8uw/K+OSVf8AMGw5e9HKP2ETd5xpK5MLZQD6b4Q== + dependencies: + "@babel/parser" "^7.27.3" + "@babel/types" "^7.27.3" + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.25" + jsesc "^3.0.2" + "@babel/helper-annotate-as-pure@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.24.7.tgz#5373c7bc8366b12a033b4be1ac13a206c6656aab" @@ -237,6 +291,13 @@ dependencies: "@babel/types" "^7.24.7" +"@babel/helper-annotate-as-pure@^7.27.1": + version "7.27.3" + resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz#f31fd86b915fc4daf1f3ac6976c59be7084ed9c5" + integrity sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg== + dependencies: + "@babel/types" "^7.27.3" + "@babel/helper-builder-binary-assignment-operator-visitor@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.24.7.tgz#37d66feb012024f2422b762b9b2a7cfe27c7fba3" @@ -256,6 +317,17 @@ lru-cache "^5.1.1" semver "^6.3.1" +"@babel/helper-compilation-targets@^7.27.1", "@babel/helper-compilation-targets@^7.27.2": + version "7.27.2" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz#46a0f6efab808d51d29ce96858dd10ce8732733d" + integrity sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ== + dependencies: + "@babel/compat-data" "^7.27.2" + "@babel/helper-validator-option" "^7.27.1" + browserslist "^4.24.0" + lru-cache "^5.1.1" + semver "^6.3.1" + "@babel/helper-create-class-features-plugin@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.24.7.tgz#2eaed36b3a1c11c53bdf80d53838b293c52f5b3b" @@ -271,6 +343,19 @@ "@babel/helper-split-export-declaration" "^7.24.7" semver "^6.3.1" +"@babel/helper-create-class-features-plugin@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.27.1.tgz#5bee4262a6ea5ddc852d0806199eb17ca3de9281" + integrity sha512-QwGAmuvM17btKU5VqXfb+Giw4JcN0hjuufz3DYnpeVDvZLAObloM77bhMXiqry3Iio+Ai4phVRDwl6WU10+r5A== + dependencies: + "@babel/helper-annotate-as-pure" "^7.27.1" + "@babel/helper-member-expression-to-functions" "^7.27.1" + "@babel/helper-optimise-call-expression" "^7.27.1" + "@babel/helper-replace-supers" "^7.27.1" + "@babel/helper-skip-transparent-expression-wrappers" "^7.27.1" + "@babel/traverse" "^7.27.1" + semver "^6.3.1" + "@babel/helper-create-regexp-features-plugin@^7.18.6", "@babel/helper-create-regexp-features-plugin@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.24.7.tgz#be4f435a80dc2b053c76eeb4b7d16dd22cfc89da" @@ -280,6 +365,15 @@ regexpu-core "^5.3.1" semver "^6.3.1" +"@babel/helper-create-regexp-features-plugin@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.27.1.tgz#05b0882d97ba1d4d03519e4bce615d70afa18c53" + integrity sha512-uVDC72XVf8UbrH5qQTc18Agb8emwjTiZrQE11Nv3CuBEZmVvTwwE9CBUEvHku06gQCAyYf8Nv6ja1IN+6LMbxQ== + dependencies: + "@babel/helper-annotate-as-pure" "^7.27.1" + regexpu-core "^6.2.0" + semver "^6.3.1" + "@babel/helper-define-polyfill-provider@^0.6.1", "@babel/helper-define-polyfill-provider@^0.6.2": version "0.6.2" resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.2.tgz#18594f789c3594acb24cfdb4a7f7b7d2e8bd912d" @@ -291,6 +385,17 @@ lodash.debounce "^4.0.8" resolve "^1.14.2" +"@babel/helper-define-polyfill-provider@^0.6.3": + version "0.6.4" + resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.4.tgz#15e8746368bfa671785f5926ff74b3064c291fab" + integrity sha512-jljfR1rGnXXNWnmQg2K3+bvhkxB51Rl32QRaOTuwwjviGrHzIbSc8+x9CpraDtbT7mfyjXObULP4w/adunNwAw== + dependencies: + "@babel/helper-compilation-targets" "^7.22.6" + "@babel/helper-plugin-utils" "^7.22.5" + debug "^4.1.1" + lodash.debounce "^4.0.8" + resolve "^1.14.2" + "@babel/helper-environment-visitor@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.7.tgz#4b31ba9551d1f90781ba83491dd59cf9b269f7d9" @@ -321,6 +426,14 @@ "@babel/traverse" "^7.24.7" "@babel/types" "^7.24.7" +"@babel/helper-member-expression-to-functions@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.27.1.tgz#ea1211276be93e798ce19037da6f06fbb994fa44" + integrity sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA== + dependencies: + "@babel/traverse" "^7.27.1" + "@babel/types" "^7.27.1" + "@babel/helper-module-imports@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz#f2f980392de5b84c3328fc71d38bd81bbb83042b" @@ -329,6 +442,14 @@ "@babel/traverse" "^7.24.7" "@babel/types" "^7.24.7" +"@babel/helper-module-imports@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz#7ef769a323e2655e126673bb6d2d6913bbead204" + integrity sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w== + dependencies: + "@babel/traverse" "^7.27.1" + "@babel/types" "^7.27.1" + "@babel/helper-module-transforms@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.24.7.tgz#31b6c9a2930679498db65b685b1698bfd6c7daf8" @@ -340,6 +461,15 @@ "@babel/helper-split-export-declaration" "^7.24.7" "@babel/helper-validator-identifier" "^7.24.7" +"@babel/helper-module-transforms@^7.27.1", "@babel/helper-module-transforms@^7.27.3": + version "7.27.3" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz#db0bbcfba5802f9ef7870705a7ef8788508ede02" + integrity sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg== + dependencies: + "@babel/helper-module-imports" "^7.27.1" + "@babel/helper-validator-identifier" "^7.27.1" + "@babel/traverse" "^7.27.3" + "@babel/helper-optimise-call-expression@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.24.7.tgz#8b0a0456c92f6b323d27cfd00d1d664e76692a0f" @@ -347,11 +477,23 @@ dependencies: "@babel/types" "^7.24.7" +"@babel/helper-optimise-call-expression@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz#c65221b61a643f3e62705e5dd2b5f115e35f9200" + integrity sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw== + dependencies: + "@babel/types" "^7.27.1" + "@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.22.5", "@babel/helper-plugin-utils@^7.24.7", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.7.tgz#98c84fe6fe3d0d3ae7bfc3a5e166a46844feb2a0" integrity sha512-Rq76wjt7yz9AAc1KnlRKNAi/dMSVWgDRx43FHoJEbcYU6xOWaE2dVPwcdTukJrjxS65GITyfbvEYHvkirZ6uEg== +"@babel/helper-plugin-utils@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz#ddb2f876534ff8013e6c2b299bf4d39b3c51d44c" + integrity sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw== + "@babel/helper-remap-async-to-generator@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.24.7.tgz#b3f0f203628522713849d49403f1a414468be4c7" @@ -361,6 +503,15 @@ "@babel/helper-environment-visitor" "^7.24.7" "@babel/helper-wrap-function" "^7.24.7" +"@babel/helper-remap-async-to-generator@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.27.1.tgz#4601d5c7ce2eb2aea58328d43725523fcd362ce6" + integrity sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA== + dependencies: + "@babel/helper-annotate-as-pure" "^7.27.1" + "@babel/helper-wrap-function" "^7.27.1" + "@babel/traverse" "^7.27.1" + "@babel/helper-replace-supers@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.24.7.tgz#f933b7eed81a1c0265740edc91491ce51250f765" @@ -370,6 +521,15 @@ "@babel/helper-member-expression-to-functions" "^7.24.7" "@babel/helper-optimise-call-expression" "^7.24.7" +"@babel/helper-replace-supers@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.27.1.tgz#b1ed2d634ce3bdb730e4b52de30f8cccfd692bc0" + integrity sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA== + dependencies: + "@babel/helper-member-expression-to-functions" "^7.27.1" + "@babel/helper-optimise-call-expression" "^7.27.1" + "@babel/traverse" "^7.27.1" + "@babel/helper-simple-access@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz#bcade8da3aec8ed16b9c4953b74e506b51b5edb3" @@ -386,6 +546,14 @@ "@babel/traverse" "^7.24.7" "@babel/types" "^7.24.7" +"@babel/helper-skip-transparent-expression-wrappers@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz#62bb91b3abba8c7f1fec0252d9dbea11b3ee7a56" + integrity sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg== + dependencies: + "@babel/traverse" "^7.27.1" + "@babel/types" "^7.27.1" + "@babel/helper-split-export-declaration@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz#83949436890e07fa3d6873c61a96e3bbf692d856" @@ -403,6 +571,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz#1aabb72ee72ed35789b4bbcad3ca2862ce614e8c" integrity sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA== +"@babel/helper-string-parser@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz#54da796097ab19ce67ed9f88b47bb2ec49367687" + integrity sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA== + "@babel/helper-validator-identifier@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz#75b889cfaf9e35c2aaf42cf0d72c8e91719251db" @@ -413,11 +586,21 @@ resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz#24b64e2c3ec7cd3b3c547729b8d16871f22cbdc7" integrity sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ== +"@babel/helper-validator-identifier@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz#a7054dcc145a967dd4dc8fee845a57c1316c9df8" + integrity sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow== + "@babel/helper-validator-option@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.24.7.tgz#24c3bb77c7a425d1742eec8fb433b5a1b38e62f6" integrity sha512-yy1/KvjhV/ZCL+SM7hBrvnZJ3ZuT9OuZgIJAGpPEToANvc3iM6iDvBnRjtElWibHU6n8/LPR/EjX9EtIEYO3pw== +"@babel/helper-validator-option@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz#fa52f5b1e7db1ab049445b421c4471303897702f" + integrity sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg== + "@babel/helper-wrap-function@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.24.7.tgz#52d893af7e42edca7c6d2c6764549826336aae1f" @@ -428,6 +611,15 @@ "@babel/traverse" "^7.24.7" "@babel/types" "^7.24.7" +"@babel/helper-wrap-function@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.27.1.tgz#b88285009c31427af318d4fe37651cd62a142409" + integrity sha512-NFJK2sHUvrjo8wAU/nQTWU890/zB2jj0qBcCbZbbf+005cAsv6tMjXz31fBign6M5ov1o0Bllu+9nbqkfsjjJQ== + dependencies: + "@babel/template" "^7.27.1" + "@babel/traverse" "^7.27.1" + "@babel/types" "^7.27.1" + "@babel/helpers@^7.24.7": version "7.27.0" resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.27.0.tgz#53d156098defa8243eab0f32fa17589075a1b808" @@ -436,6 +628,14 @@ "@babel/template" "^7.27.0" "@babel/types" "^7.27.0" +"@babel/helpers@^7.27.3": + version "7.27.3" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.27.3.tgz#387d65d279290e22fe7a47a8ffcd2d0c0184edd0" + integrity sha512-h/eKy9agOya1IGuLaZ9tEUgz+uIRXcbtOhRtUyyMf8JFmn1iT13vnl/IGVWSkdOCG/pC57U4S1jnAabAavTMwg== + dependencies: + "@babel/template" "^7.27.2" + "@babel/types" "^7.27.3" + "@babel/highlight@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.24.7.tgz#a05ab1df134b286558aae0ed41e6c5f731bf409d" @@ -458,6 +658,13 @@ dependencies: "@babel/types" "^7.27.0" +"@babel/parser@^7.27.2", "@babel/parser@^7.27.3": + version "7.27.3" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.27.3.tgz#1b7533f0d908ad2ac545c4d05cbe2fb6dc8cfaaf" + integrity sha512-xyYxRj6+tLNDTWi0KCBcZ9V7yg3/lwL9DWh9Uwh/RIVlIfFidggcgxKX3GCXwCiswwcGRawBKbEg2LG/Y8eJhw== + dependencies: + "@babel/types" "^7.27.3" + "@babel/plugin-bugfix-firefox-class-in-computed-class-key@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.24.7.tgz#fd059fd27b184ea2b4c7e646868a9a381bbc3055" @@ -466,6 +673,21 @@ "@babel/helper-environment-visitor" "^7.24.7" "@babel/helper-plugin-utils" "^7.24.7" +"@babel/plugin-bugfix-firefox-class-in-computed-class-key@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.27.1.tgz#61dd8a8e61f7eb568268d1b5f129da3eee364bf9" + integrity sha512-QPG3C9cCVRQLxAVwmefEmwdTanECuUBMQZ/ym5kiw3XKCGA7qkuQLcjWWHcrD/GKbn/WmJwaezfuuAOcyKlRPA== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/traverse" "^7.27.1" + +"@babel/plugin-bugfix-safari-class-field-initializer-scope@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.27.1.tgz#43f70a6d7efd52370eefbdf55ae03d91b293856d" + integrity sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.24.7.tgz#468096ca44bbcbe8fcc570574e12eb1950e18107" @@ -473,6 +695,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.24.7" +"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.27.1.tgz#beb623bd573b8b6f3047bd04c32506adc3e58a72" + integrity sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.24.7.tgz#e4eabdd5109acc399b38d7999b2ef66fc2022f89" @@ -482,6 +711,15 @@ "@babel/helper-skip-transparent-expression-wrappers" "^7.24.7" "@babel/plugin-transform-optional-chaining" "^7.24.7" +"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.27.1.tgz#e134a5479eb2ba9c02714e8c1ebf1ec9076124fd" + integrity sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-skip-transparent-expression-wrappers" "^7.27.1" + "@babel/plugin-transform-optional-chaining" "^7.27.1" + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.24.7.tgz#71b21bb0286d5810e63a1538aa901c58e87375ec" @@ -490,6 +728,14 @@ "@babel/helper-environment-visitor" "^7.24.7" "@babel/helper-plugin-utils" "^7.24.7" +"@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.27.1.tgz#bb1c25af34d75115ce229a1de7fa44bf8f955670" + integrity sha512-6BpaYGDavZqkI6yT+KSPdpZFfpnd68UKXbcjI9pJ13pvHhPrCKWOOLp+ysvMeA+DxnhuPpgIaRpxRxo5A9t5jw== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/traverse" "^7.27.1" + "@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2": version "7.21.0-placeholder-for-preset-env.2" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz#7844f9289546efa9febac2de4cfe358a050bd703" @@ -537,6 +783,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.24.7" +"@babel/plugin-syntax-import-assertions@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.27.1.tgz#88894aefd2b03b5ee6ad1562a7c8e1587496aecd" + integrity sha512-UT/Jrhw57xg4ILHLFnzFpPDlMbcdEicaAtjPQpbj9wa8T4r5KVWCimHcL/460g8Ht0DMxDyjsLgiWSkVjnwPFg== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-syntax-import-attributes@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.24.7.tgz#b4f9ea95a79e6912480c4b626739f86a076624ca" @@ -544,6 +797,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.24.7" +"@babel/plugin-syntax-import-attributes@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz#34c017d54496f9b11b61474e7ea3dfd5563ffe07" + integrity sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-syntax-import-meta@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz#ee601348c370fa334d2207be158777496521fd51" @@ -565,6 +825,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.24.7" +"@babel/plugin-syntax-jsx@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz#2f9beb5eff30fa507c5532d107daac7b888fa34c" + integrity sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-syntax-logical-assignment-operators@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699" @@ -628,6 +895,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.24.7" +"@babel/plugin-syntax-typescript@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz#5147d29066a793450f220c63fa3a9431b7e6dd18" + integrity sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-syntax-unicode-sets-regex@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz#d49a3b3e6b52e5be6740022317580234a6a47357" @@ -643,6 +917,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.24.7" +"@babel/plugin-transform-arrow-functions@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz#6e2061067ba3ab0266d834a9f94811196f2aba9a" + integrity sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-async-generator-functions@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.24.7.tgz#7330a5c50e05181ca52351b8fd01642000c96cfd" @@ -653,6 +934,15 @@ "@babel/helper-remap-async-to-generator" "^7.24.7" "@babel/plugin-syntax-async-generators" "^7.8.4" +"@babel/plugin-transform-async-generator-functions@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.27.1.tgz#ca433df983d68e1375398e7ca71bf2a4f6fd89d7" + integrity sha512-eST9RrwlpaoJBDHShc+DS2SG4ATTi2MYNb4OxYkf3n+7eb49LWpnS+HSpVfW4x927qQwgk8A2hGNVaajAEw0EA== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-remap-async-to-generator" "^7.27.1" + "@babel/traverse" "^7.27.1" + "@babel/plugin-transform-async-to-generator@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.24.7.tgz#72a3af6c451d575842a7e9b5a02863414355bdcc" @@ -662,6 +952,15 @@ "@babel/helper-plugin-utils" "^7.24.7" "@babel/helper-remap-async-to-generator" "^7.24.7" +"@babel/plugin-transform-async-to-generator@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.27.1.tgz#9a93893b9379b39466c74474f55af03de78c66e7" + integrity sha512-NREkZsZVJS4xmTr8qzE5y8AfIPqsdQfRuUiLRTEzb7Qii8iFWCyDKaUV2c0rCuh4ljDZ98ALHP/PetiBV2nddA== + dependencies: + "@babel/helper-module-imports" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-remap-async-to-generator" "^7.27.1" + "@babel/plugin-transform-block-scoped-functions@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.24.7.tgz#a4251d98ea0c0f399dafe1a35801eaba455bbf1f" @@ -669,6 +968,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.24.7" +"@babel/plugin-transform-block-scoped-functions@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.27.1.tgz#558a9d6e24cf72802dd3b62a4b51e0d62c0f57f9" + integrity sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-block-scoping@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.24.7.tgz#42063e4deb850c7bd7c55e626bf4e7ab48e6ce02" @@ -676,6 +982,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.24.7" +"@babel/plugin-transform-block-scoping@^7.27.1": + version "7.27.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.27.3.tgz#a21f37e222dc0a7b91c3784fa3bd4edf8d7a6dc1" + integrity sha512-+F8CnfhuLhwUACIJMLWnjz6zvzYM2r0yeIHKlbgfw7ml8rOMJsXNXV/hyRcb3nb493gRs4WvYpQAndWj/qQmkQ== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-class-properties@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.24.7.tgz#256879467b57b0b68c7ddfc5b76584f398cd6834" @@ -684,6 +997,14 @@ "@babel/helper-create-class-features-plugin" "^7.24.7" "@babel/helper-plugin-utils" "^7.24.7" +"@babel/plugin-transform-class-properties@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.27.1.tgz#dd40a6a370dfd49d32362ae206ddaf2bb082a925" + integrity sha512-D0VcalChDMtuRvJIu3U/fwWjf8ZMykz5iZsg77Nuj821vCKI3zCyRLwRdWbsuJ/uRwZhZ002QtCqIkwC/ZkvbA== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-class-static-block@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.24.7.tgz#c82027ebb7010bc33c116d4b5044fbbf8c05484d" @@ -693,6 +1014,14 @@ "@babel/helper-plugin-utils" "^7.24.7" "@babel/plugin-syntax-class-static-block" "^7.14.5" +"@babel/plugin-transform-class-static-block@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.27.1.tgz#7e920d5625b25bbccd3061aefbcc05805ed56ce4" + integrity sha512-s734HmYU78MVzZ++joYM+NkJusItbdRcbm+AGRgJCt3iA+yux0QpD9cBVdz3tKyrjVYWRl7j0mHSmv4lhV0aoA== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-classes@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.24.7.tgz#4ae6ef43a12492134138c1e45913f7c46c41b4bf" @@ -707,6 +1036,18 @@ "@babel/helper-split-export-declaration" "^7.24.7" globals "^11.1.0" +"@babel/plugin-transform-classes@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.27.1.tgz#03bb04bea2c7b2f711f0db7304a8da46a85cced4" + integrity sha512-7iLhfFAubmpeJe/Wo2TVuDrykh/zlWXLzPNdL0Jqn/Xu8R3QQ8h9ff8FQoISZOsw74/HFqFI7NX63HN7QFIHKA== + dependencies: + "@babel/helper-annotate-as-pure" "^7.27.1" + "@babel/helper-compilation-targets" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-replace-supers" "^7.27.1" + "@babel/traverse" "^7.27.1" + globals "^11.1.0" + "@babel/plugin-transform-computed-properties@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.24.7.tgz#4cab3214e80bc71fae3853238d13d097b004c707" @@ -715,6 +1056,14 @@ "@babel/helper-plugin-utils" "^7.24.7" "@babel/template" "^7.24.7" +"@babel/plugin-transform-computed-properties@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.27.1.tgz#81662e78bf5e734a97982c2b7f0a793288ef3caa" + integrity sha512-lj9PGWvMTVksbWiDT2tW68zGS/cyo4AkZ/QTp0sQT0mjPopCmrSkzxeXkznjqBxzDI6TclZhOJbBmbBLjuOZUw== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/template" "^7.27.1" + "@babel/plugin-transform-destructuring@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.24.7.tgz#a097f25292defb6e6cc16d6333a4cfc1e3c72d9e" @@ -722,6 +1071,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.24.7" +"@babel/plugin-transform-destructuring@^7.27.1", "@babel/plugin-transform-destructuring@^7.27.3": + version "7.27.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.27.3.tgz#3cc8299ed798d9a909f8d66ddeb40849ec32e3b0" + integrity sha512-s4Jrok82JpiaIprtY2nHsYmrThKvvwgHwjgd7UMiYhZaN0asdXNLr0y+NjTfkA7SyQE5i2Fb7eawUOZmLvyqOA== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-dotall-regex@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.24.7.tgz#5f8bf8a680f2116a7207e16288a5f974ad47a7a0" @@ -730,6 +1086,14 @@ "@babel/helper-create-regexp-features-plugin" "^7.24.7" "@babel/helper-plugin-utils" "^7.24.7" +"@babel/plugin-transform-dotall-regex@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.27.1.tgz#aa6821de864c528b1fecf286f0a174e38e826f4d" + integrity sha512-gEbkDVGRvjj7+T1ivxrfgygpT7GUd4vmODtYpbs0gZATdkX8/iSnOtZSxiZnsgm1YjTgjI6VKBGSJJevkrclzw== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-duplicate-keys@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.24.7.tgz#dd20102897c9a2324e5adfffb67ff3610359a8ee" @@ -737,6 +1101,21 @@ dependencies: "@babel/helper-plugin-utils" "^7.24.7" +"@babel/plugin-transform-duplicate-keys@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.27.1.tgz#f1fbf628ece18e12e7b32b175940e68358f546d1" + integrity sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-duplicate-named-capturing-groups-regex@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.27.1.tgz#5043854ca620a94149372e69030ff8cb6a9eb0ec" + integrity sha512-hkGcueTEzuhB30B3eJCbCYeCaaEQOmQR0AdvzpD4LoN0GXMWzzGSuRrxR2xTnCrvNbVwK9N6/jQ92GSLfiZWoQ== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-dynamic-import@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.24.7.tgz#4d8b95e3bae2b037673091aa09cd33fecd6419f4" @@ -745,6 +1124,13 @@ "@babel/helper-plugin-utils" "^7.24.7" "@babel/plugin-syntax-dynamic-import" "^7.8.3" +"@babel/plugin-transform-dynamic-import@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.27.1.tgz#4c78f35552ac0e06aa1f6e3c573d67695e8af5a4" + integrity sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-exponentiation-operator@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.24.7.tgz#b629ee22645f412024297d5245bce425c31f9b0d" @@ -753,6 +1139,13 @@ "@babel/helper-builder-binary-assignment-operator-visitor" "^7.24.7" "@babel/helper-plugin-utils" "^7.24.7" +"@babel/plugin-transform-exponentiation-operator@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.27.1.tgz#fc497b12d8277e559747f5a3ed868dd8064f83e1" + integrity sha512-uspvXnhHvGKf2r4VVtBpeFnuDWsJLQ6MF6lGJLC89jBR1uoVeqM416AZtTuhTezOfgHicpJQmoD5YUakO/YmXQ== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-export-namespace-from@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.24.7.tgz#176d52d8d8ed516aeae7013ee9556d540c53f197" @@ -761,6 +1154,13 @@ "@babel/helper-plugin-utils" "^7.24.7" "@babel/plugin-syntax-export-namespace-from" "^7.8.3" +"@babel/plugin-transform-export-namespace-from@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.27.1.tgz#71ca69d3471edd6daa711cf4dfc3400415df9c23" + integrity sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-for-of@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.24.7.tgz#f25b33f72df1d8be76399e1b8f3f9d366eb5bc70" @@ -769,6 +1169,14 @@ "@babel/helper-plugin-utils" "^7.24.7" "@babel/helper-skip-transparent-expression-wrappers" "^7.24.7" +"@babel/plugin-transform-for-of@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.27.1.tgz#bc24f7080e9ff721b63a70ac7b2564ca15b6c40a" + integrity sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-skip-transparent-expression-wrappers" "^7.27.1" + "@babel/plugin-transform-function-name@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.24.7.tgz#6d8601fbffe665c894440ab4470bc721dd9131d6" @@ -778,6 +1186,15 @@ "@babel/helper-function-name" "^7.24.7" "@babel/helper-plugin-utils" "^7.24.7" +"@babel/plugin-transform-function-name@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.27.1.tgz#4d0bf307720e4dce6d7c30fcb1fd6ca77bdeb3a7" + integrity sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ== + dependencies: + "@babel/helper-compilation-targets" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/traverse" "^7.27.1" + "@babel/plugin-transform-json-strings@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.24.7.tgz#f3e9c37c0a373fee86e36880d45b3664cedaf73a" @@ -786,6 +1203,13 @@ "@babel/helper-plugin-utils" "^7.24.7" "@babel/plugin-syntax-json-strings" "^7.8.3" +"@babel/plugin-transform-json-strings@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.27.1.tgz#a2e0ce6ef256376bd527f290da023983527a4f4c" + integrity sha512-6WVLVJiTjqcQauBhn1LkICsR2H+zm62I3h9faTDKt1qP4jn2o72tSvqMwtGFKGTpojce0gJs+76eZ2uCHRZh0Q== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-literals@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.24.7.tgz#36b505c1e655151a9d7607799a9988fc5467d06c" @@ -793,6 +1217,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.24.7" +"@babel/plugin-transform-literals@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.27.1.tgz#baaefa4d10a1d4206f9dcdda50d7d5827bb70b24" + integrity sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-logical-assignment-operators@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.24.7.tgz#a58fb6eda16c9dc8f9ff1c7b1ba6deb7f4694cb0" @@ -801,6 +1232,13 @@ "@babel/helper-plugin-utils" "^7.24.7" "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" +"@babel/plugin-transform-logical-assignment-operators@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.27.1.tgz#890cb20e0270e0e5bebe3f025b434841c32d5baa" + integrity sha512-SJvDs5dXxiae4FbSL1aBJlG4wvl594N6YEVVn9e3JGulwioy6z3oPjx/sQBO3Y4NwUu5HNix6KJ3wBZoewcdbw== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-member-expression-literals@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.24.7.tgz#3b4454fb0e302e18ba4945ba3246acb1248315df" @@ -808,6 +1246,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.24.7" +"@babel/plugin-transform-member-expression-literals@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.27.1.tgz#37b88ba594d852418e99536f5612f795f23aeaf9" + integrity sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-modules-amd@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.24.7.tgz#65090ed493c4a834976a3ca1cde776e6ccff32d7" @@ -816,6 +1261,14 @@ "@babel/helper-module-transforms" "^7.24.7" "@babel/helper-plugin-utils" "^7.24.7" +"@babel/plugin-transform-modules-amd@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.27.1.tgz#a4145f9d87c2291fe2d05f994b65dba4e3e7196f" + integrity sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA== + dependencies: + "@babel/helper-module-transforms" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-modules-commonjs@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.24.7.tgz#9fd5f7fdadee9085886b183f1ad13d1ab260f4ab" @@ -825,6 +1278,14 @@ "@babel/helper-plugin-utils" "^7.24.7" "@babel/helper-simple-access" "^7.24.7" +"@babel/plugin-transform-modules-commonjs@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.27.1.tgz#8e44ed37c2787ecc23bdc367f49977476614e832" + integrity sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw== + dependencies: + "@babel/helper-module-transforms" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-modules-systemjs@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.24.7.tgz#f8012316c5098f6e8dee6ecd58e2bc6f003d0ce7" @@ -835,6 +1296,16 @@ "@babel/helper-plugin-utils" "^7.24.7" "@babel/helper-validator-identifier" "^7.24.7" +"@babel/plugin-transform-modules-systemjs@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.27.1.tgz#00e05b61863070d0f3292a00126c16c0e024c4ed" + integrity sha512-w5N1XzsRbc0PQStASMksmUeqECuzKuTJer7kFagK8AXgpCMkeDMO5S+aaFb7A51ZYDF7XI34qsTX+fkHiIm5yA== + dependencies: + "@babel/helper-module-transforms" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-validator-identifier" "^7.27.1" + "@babel/traverse" "^7.27.1" + "@babel/plugin-transform-modules-umd@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.24.7.tgz#edd9f43ec549099620df7df24e7ba13b5c76efc8" @@ -843,6 +1314,14 @@ "@babel/helper-module-transforms" "^7.24.7" "@babel/helper-plugin-utils" "^7.24.7" +"@babel/plugin-transform-modules-umd@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.27.1.tgz#63f2cf4f6dc15debc12f694e44714863d34cd334" + integrity sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w== + dependencies: + "@babel/helper-module-transforms" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-named-capturing-groups-regex@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.24.7.tgz#9042e9b856bc6b3688c0c2e4060e9e10b1460923" @@ -851,6 +1330,14 @@ "@babel/helper-create-regexp-features-plugin" "^7.24.7" "@babel/helper-plugin-utils" "^7.24.7" +"@babel/plugin-transform-named-capturing-groups-regex@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.27.1.tgz#f32b8f7818d8fc0cc46ee20a8ef75f071af976e1" + integrity sha512-SstR5JYy8ddZvD6MhV0tM/j16Qds4mIpJTOd1Yu9J9pJjH93bxHECF7pgtc28XvkzTD6Pxcm/0Z73Hvk7kb3Ng== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-new-target@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.24.7.tgz#31ff54c4e0555cc549d5816e4ab39241dfb6ab00" @@ -858,6 +1345,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.24.7" +"@babel/plugin-transform-new-target@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.27.1.tgz#259c43939728cad1706ac17351b7e6a7bea1abeb" + integrity sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-nullish-coalescing-operator@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.24.7.tgz#1de4534c590af9596f53d67f52a92f12db984120" @@ -866,6 +1360,13 @@ "@babel/helper-plugin-utils" "^7.24.7" "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" +"@babel/plugin-transform-nullish-coalescing-operator@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.27.1.tgz#4f9d3153bf6782d73dd42785a9d22d03197bc91d" + integrity sha512-aGZh6xMo6q9vq1JGcw58lZ1Z0+i0xB2x0XaauNIUXd6O1xXc3RwoWEBlsTQrY4KQ9Jf0s5rgD6SiNkaUdJegTA== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-numeric-separator@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.24.7.tgz#bea62b538c80605d8a0fac9b40f48e97efa7de63" @@ -874,6 +1375,13 @@ "@babel/helper-plugin-utils" "^7.24.7" "@babel/plugin-syntax-numeric-separator" "^7.10.4" +"@babel/plugin-transform-numeric-separator@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.27.1.tgz#614e0b15cc800e5997dadd9bd6ea524ed6c819c6" + integrity sha512-fdPKAcujuvEChxDBJ5c+0BTaS6revLV7CJL08e4m3de8qJfNIuCc2nc7XJYOjBoTMJeqSmwXJ0ypE14RCjLwaw== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-object-rest-spread@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.24.7.tgz#d13a2b93435aeb8a197e115221cab266ba6e55d6" @@ -884,6 +1392,16 @@ "@babel/plugin-syntax-object-rest-spread" "^7.8.3" "@babel/plugin-transform-parameters" "^7.24.7" +"@babel/plugin-transform-object-rest-spread@^7.27.2": + version "7.27.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.27.3.tgz#ce130aa73fef828bc3e3e835f9bc6144be3eb1c0" + integrity sha512-7ZZtznF9g4l2JCImCo5LNKFHB5eXnN39lLtLY5Tg+VkR0jwOt7TBciMckuiQIOIW7L5tkQOCh3bVGYeXgMx52Q== + dependencies: + "@babel/helper-compilation-targets" "^7.27.2" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-destructuring" "^7.27.3" + "@babel/plugin-transform-parameters" "^7.27.1" + "@babel/plugin-transform-object-super@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.24.7.tgz#66eeaff7830bba945dd8989b632a40c04ed625be" @@ -892,6 +1410,14 @@ "@babel/helper-plugin-utils" "^7.24.7" "@babel/helper-replace-supers" "^7.24.7" +"@babel/plugin-transform-object-super@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.27.1.tgz#1c932cd27bf3874c43a5cac4f43ebf970c9871b5" + integrity sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-replace-supers" "^7.27.1" + "@babel/plugin-transform-optional-catch-binding@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.24.7.tgz#00eabd883d0dd6a60c1c557548785919b6e717b4" @@ -900,6 +1426,13 @@ "@babel/helper-plugin-utils" "^7.24.7" "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" +"@babel/plugin-transform-optional-catch-binding@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.27.1.tgz#84c7341ebde35ccd36b137e9e45866825072a30c" + integrity sha512-txEAEKzYrHEX4xSZN4kJ+OfKXFVSWKB2ZxM9dpcE3wT7smwkNmXo5ORRlVzMVdJbD+Q8ILTgSD7959uj+3Dm3Q== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-optional-chaining@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.24.7.tgz#b8f6848a80cf2da98a8a204429bec04756c6d454" @@ -909,6 +1442,14 @@ "@babel/helper-skip-transparent-expression-wrappers" "^7.24.7" "@babel/plugin-syntax-optional-chaining" "^7.8.3" +"@babel/plugin-transform-optional-chaining@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.27.1.tgz#874ce3c4f06b7780592e946026eb76a32830454f" + integrity sha512-BQmKPPIuc8EkZgNKsv0X4bPmOoayeu4F1YCwx2/CfmDSXDbp7GnzlUH+/ul5VGfRg1AoFPsrIThlEBj2xb4CAg== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-skip-transparent-expression-wrappers" "^7.27.1" + "@babel/plugin-transform-parameters@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.24.7.tgz#5881f0ae21018400e320fc7eb817e529d1254b68" @@ -916,6 +1457,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.24.7" +"@babel/plugin-transform-parameters@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.1.tgz#80334b54b9b1ac5244155a0c8304a187a618d5a7" + integrity sha512-018KRk76HWKeZ5l4oTj2zPpSh+NbGdt0st5S6x0pga6HgrjBOJb24mMDHorFopOOd6YHkLgOZ+zaCjZGPO4aKg== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-private-methods@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.24.7.tgz#e6318746b2ae70a59d023d5cc1344a2ba7a75f5e" @@ -924,6 +1472,14 @@ "@babel/helper-create-class-features-plugin" "^7.24.7" "@babel/helper-plugin-utils" "^7.24.7" +"@babel/plugin-transform-private-methods@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.27.1.tgz#fdacbab1c5ed81ec70dfdbb8b213d65da148b6af" + integrity sha512-10FVt+X55AjRAYI9BrdISN9/AQWHqldOeZDUoLyif1Kn05a56xVBXb8ZouL8pZ9jem8QpXaOt8TS7RHUIS+GPA== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-private-property-in-object@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.24.7.tgz#4eec6bc701288c1fab5f72e6a4bbc9d67faca061" @@ -934,6 +1490,15 @@ "@babel/helper-plugin-utils" "^7.24.7" "@babel/plugin-syntax-private-property-in-object" "^7.14.5" +"@babel/plugin-transform-private-property-in-object@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.27.1.tgz#4dbbef283b5b2f01a21e81e299f76e35f900fb11" + integrity sha512-5J+IhqTi1XPa0DXF83jYOaARrX+41gOewWbkPyjMNRDqgOCqdffGh8L3f/Ek5utaEBZExjSAzcyjmV9SSAWObQ== + dependencies: + "@babel/helper-annotate-as-pure" "^7.27.1" + "@babel/helper-create-class-features-plugin" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-property-literals@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.24.7.tgz#f0d2ed8380dfbed949c42d4d790266525d63bbdc" @@ -941,6 +1506,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.24.7" +"@babel/plugin-transform-property-literals@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.27.1.tgz#07eafd618800591e88073a0af1b940d9a42c6424" + integrity sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-react-constant-elements@^7.21.3": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.24.7.tgz#b85e8f240b14400277f106c9c9b585d9acf608a1" @@ -955,6 +1527,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.24.7" +"@babel/plugin-transform-react-display-name@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.27.1.tgz#43af31362d71f7848cfac0cbc212882b1a16e80f" + integrity sha512-p9+Vl3yuHPmkirRrg021XiP+EETmPMQTLr6Ayjj85RLNEbb3Eya/4VI0vAdzQG9SEAl2Lnt7fy5lZyMzjYoZQQ== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-react-jsx-development@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.24.7.tgz#eaee12f15a93f6496d852509a850085e6361470b" @@ -962,6 +1541,13 @@ dependencies: "@babel/plugin-transform-react-jsx" "^7.24.7" +"@babel/plugin-transform-react-jsx-development@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.27.1.tgz#47ff95940e20a3a70e68ad3d4fcb657b647f6c98" + integrity sha512-ykDdF5yI4f1WrAolLqeF3hmYU12j9ntLQl/AOG1HAS21jxyg1Q0/J/tpREuYLfatGdGmXp/3yS0ZA76kOlVq9Q== + dependencies: + "@babel/plugin-transform-react-jsx" "^7.27.1" + "@babel/plugin-transform-react-jsx@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.24.7.tgz#17cd06b75a9f0e2bd076503400e7c4b99beedac4" @@ -973,6 +1559,17 @@ "@babel/plugin-syntax-jsx" "^7.24.7" "@babel/types" "^7.24.7" +"@babel/plugin-transform-react-jsx@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.27.1.tgz#1023bc94b78b0a2d68c82b5e96aed573bcfb9db0" + integrity sha512-2KH4LWGSrJIkVf5tSiBFYuXDAoWRq2MMwgivCf+93dd0GQi8RXLjKA/0EvRnVV5G0hrHczsquXuD01L8s6dmBw== + dependencies: + "@babel/helper-annotate-as-pure" "^7.27.1" + "@babel/helper-module-imports" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-syntax-jsx" "^7.27.1" + "@babel/types" "^7.27.1" + "@babel/plugin-transform-react-pure-annotations@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.24.7.tgz#bdd9d140d1c318b4f28b29a00fb94f97ecab1595" @@ -981,6 +1578,14 @@ "@babel/helper-annotate-as-pure" "^7.24.7" "@babel/helper-plugin-utils" "^7.24.7" +"@babel/plugin-transform-react-pure-annotations@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.27.1.tgz#339f1ce355eae242e0649f232b1c68907c02e879" + integrity sha512-JfuinvDOsD9FVMTHpzA/pBLisxpv1aSf+OIV8lgH3MuWrks19R27e6a6DipIg4aX1Zm9Wpb04p8wljfKrVSnPA== + dependencies: + "@babel/helper-annotate-as-pure" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-regenerator@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.24.7.tgz#021562de4534d8b4b1851759fd7af4e05d2c47f8" @@ -989,6 +1594,21 @@ "@babel/helper-plugin-utils" "^7.24.7" regenerator-transform "^0.15.2" +"@babel/plugin-transform-regenerator@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.27.1.tgz#0a471df9213416e44cd66bf67176b66f65768401" + integrity sha512-B19lbbL7PMrKr52BNPjCqg1IyNUIjTcxKj8uX9zHO+PmWN93s19NDr/f69mIkEp2x9nmDJ08a7lgHaTTzvW7mw== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-regexp-modifiers@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.27.1.tgz#df9ba5577c974e3f1449888b70b76169998a6d09" + integrity sha512-TtEciroaiODtXvLZv4rmfMhkCv8jx3wgKpL68PuiPh2M4fvz5jhsA7697N1gMvkvr/JTF13DrFYyEbY9U7cVPA== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-reserved-words@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.24.7.tgz#80037fe4fbf031fc1125022178ff3938bb3743a4" @@ -996,15 +1616,22 @@ dependencies: "@babel/helper-plugin-utils" "^7.24.7" -"@babel/plugin-transform-runtime@^7.22.9": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.24.7.tgz#00a5bfaf8c43cf5c8703a8a6e82b59d9c58f38ca" - integrity sha512-YqXjrk4C+a1kZjewqt+Mmu2UuV1s07y8kqcUf4qYLnoqemhR4gRQikhdAhSVJioMjVTu6Mo6pAbaypEA3jY6fw== +"@babel/plugin-transform-reserved-words@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.27.1.tgz#40fba4878ccbd1c56605a4479a3a891ac0274bb4" + integrity sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw== dependencies: - "@babel/helper-module-imports" "^7.24.7" - "@babel/helper-plugin-utils" "^7.24.7" + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-runtime@^7.25.9": + version "7.27.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.27.3.tgz#ad35f1eff5ba18a5e23f7270e939fb5a59d3ec0b" + integrity sha512-bA9ZL5PW90YwNgGfjg6U+7Qh/k3zCEQJ06BFgAGRp/yMjw9hP9UGbGPtx3KSOkHGljEPCCxaE+PH4fUR2h1sDw== + dependencies: + "@babel/helper-module-imports" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" babel-plugin-polyfill-corejs2 "^0.4.10" - babel-plugin-polyfill-corejs3 "^0.10.1" + babel-plugin-polyfill-corejs3 "^0.11.0" babel-plugin-polyfill-regenerator "^0.6.1" semver "^6.3.1" @@ -1015,6 +1642,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.24.7" +"@babel/plugin-transform-shorthand-properties@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.27.1.tgz#532abdacdec87bfee1e0ef8e2fcdee543fe32b90" + integrity sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-spread@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.24.7.tgz#e8a38c0fde7882e0fb8f160378f74bd885cc7bb3" @@ -1023,6 +1657,14 @@ "@babel/helper-plugin-utils" "^7.24.7" "@babel/helper-skip-transparent-expression-wrappers" "^7.24.7" +"@babel/plugin-transform-spread@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.27.1.tgz#1a264d5fc12750918f50e3fe3e24e437178abb08" + integrity sha512-kpb3HUqaILBJcRFVhFUs6Trdd4mkrzcGXss+6/mxUd273PfbWqSDHRzMT2234gIg2QYfAjvXLSquP1xECSg09Q== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-skip-transparent-expression-wrappers" "^7.27.1" + "@babel/plugin-transform-sticky-regex@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.24.7.tgz#96ae80d7a7e5251f657b5cf18f1ea6bf926f5feb" @@ -1030,6 +1672,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.24.7" +"@babel/plugin-transform-sticky-regex@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.27.1.tgz#18984935d9d2296843a491d78a014939f7dcd280" + integrity sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-template-literals@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.24.7.tgz#a05debb4a9072ae8f985bcf77f3f215434c8f8c8" @@ -1037,6 +1686,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.24.7" +"@babel/plugin-transform-template-literals@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.27.1.tgz#1a0eb35d8bb3e6efc06c9fd40eb0bcef548328b8" + integrity sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-typeof-symbol@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.24.7.tgz#f074be466580d47d6e6b27473a840c9f9ca08fb0" @@ -1044,6 +1700,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.24.7" +"@babel/plugin-transform-typeof-symbol@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.27.1.tgz#70e966bb492e03509cf37eafa6dcc3051f844369" + integrity sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-typescript@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.24.7.tgz#b006b3e0094bf0813d505e0c5485679eeaf4a881" @@ -1054,6 +1717,17 @@ "@babel/helper-plugin-utils" "^7.24.7" "@babel/plugin-syntax-typescript" "^7.24.7" +"@babel/plugin-transform-typescript@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.27.1.tgz#d3bb65598bece03f773111e88cc4e8e5070f1140" + integrity sha512-Q5sT5+O4QUebHdbwKedFBEwRLb02zJ7r4A5Gg2hUoLuU3FjdMcyqcywqUrLCaDsFCxzokf7u9kuy7qz51YUuAg== + dependencies: + "@babel/helper-annotate-as-pure" "^7.27.1" + "@babel/helper-create-class-features-plugin" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-skip-transparent-expression-wrappers" "^7.27.1" + "@babel/plugin-syntax-typescript" "^7.27.1" + "@babel/plugin-transform-unicode-escapes@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.24.7.tgz#2023a82ced1fb4971630a2e079764502c4148e0e" @@ -1061,6 +1735,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.24.7" +"@babel/plugin-transform-unicode-escapes@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.27.1.tgz#3e3143f8438aef842de28816ece58780190cf806" + integrity sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-unicode-property-regex@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.24.7.tgz#9073a4cd13b86ea71c3264659590ac086605bbcd" @@ -1069,6 +1750,14 @@ "@babel/helper-create-regexp-features-plugin" "^7.24.7" "@babel/helper-plugin-utils" "^7.24.7" +"@babel/plugin-transform-unicode-property-regex@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.27.1.tgz#bdfe2d3170c78c5691a3c3be934c8c0087525956" + integrity sha512-uW20S39PnaTImxp39O5qFlHLS9LJEmANjMG7SxIhap8rCHqu0Ik+tLEPX5DKmHn6CsWQ7j3lix2tFOa5YtL12Q== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-unicode-regex@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.24.7.tgz#dfc3d4a51127108099b19817c0963be6a2adf19f" @@ -1077,6 +1766,14 @@ "@babel/helper-create-regexp-features-plugin" "^7.24.7" "@babel/helper-plugin-utils" "^7.24.7" +"@babel/plugin-transform-unicode-regex@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.27.1.tgz#25948f5c395db15f609028e370667ed8bae9af97" + integrity sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-unicode-sets-regex@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.24.7.tgz#d40705d67523803a576e29c63cef6e516b858ed9" @@ -1085,7 +1782,15 @@ "@babel/helper-create-regexp-features-plugin" "^7.24.7" "@babel/helper-plugin-utils" "^7.24.7" -"@babel/preset-env@^7.20.2", "@babel/preset-env@^7.22.9": +"@babel/plugin-transform-unicode-sets-regex@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.27.1.tgz#6ab706d10f801b5c72da8bb2548561fa04193cd1" + integrity sha512-EtkOujbc4cgvb0mlpQefi4NTPBzhSIevblFevACNLUspmrALgmEBdL/XfnyyITfd8fKBZrZys92zOWcik7j9Tw== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/preset-env@^7.20.2": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.24.7.tgz#ff067b4e30ba4a72f225f12f123173e77b987f37" integrity sha512-1YZNsc+y6cTvWlDHidMBsQZrZfEFjRIo/BZCT906PMdzOyXtSLTgqGdrpcuTDCXyd11Am5uQULtDIcCfnTc8fQ== @@ -1172,6 +1877,81 @@ core-js-compat "^3.31.0" semver "^6.3.1" +"@babel/preset-env@^7.25.9": + version "7.27.2" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.27.2.tgz#106e6bfad92b591b1f6f76fd4cf13b7725a7bf9a" + integrity sha512-Ma4zSuYSlGNRlCLO+EAzLnCmJK2vdstgv+n7aUP+/IKZrOfWHOJVdSJtuub8RzHTj3ahD37k5OKJWvzf16TQyQ== + dependencies: + "@babel/compat-data" "^7.27.2" + "@babel/helper-compilation-targets" "^7.27.2" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-validator-option" "^7.27.1" + "@babel/plugin-bugfix-firefox-class-in-computed-class-key" "^7.27.1" + "@babel/plugin-bugfix-safari-class-field-initializer-scope" "^7.27.1" + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression" "^7.27.1" + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.27.1" + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly" "^7.27.1" + "@babel/plugin-proposal-private-property-in-object" "7.21.0-placeholder-for-preset-env.2" + "@babel/plugin-syntax-import-assertions" "^7.27.1" + "@babel/plugin-syntax-import-attributes" "^7.27.1" + "@babel/plugin-syntax-unicode-sets-regex" "^7.18.6" + "@babel/plugin-transform-arrow-functions" "^7.27.1" + "@babel/plugin-transform-async-generator-functions" "^7.27.1" + "@babel/plugin-transform-async-to-generator" "^7.27.1" + "@babel/plugin-transform-block-scoped-functions" "^7.27.1" + "@babel/plugin-transform-block-scoping" "^7.27.1" + "@babel/plugin-transform-class-properties" "^7.27.1" + "@babel/plugin-transform-class-static-block" "^7.27.1" + "@babel/plugin-transform-classes" "^7.27.1" + "@babel/plugin-transform-computed-properties" "^7.27.1" + "@babel/plugin-transform-destructuring" "^7.27.1" + "@babel/plugin-transform-dotall-regex" "^7.27.1" + "@babel/plugin-transform-duplicate-keys" "^7.27.1" + "@babel/plugin-transform-duplicate-named-capturing-groups-regex" "^7.27.1" + "@babel/plugin-transform-dynamic-import" "^7.27.1" + "@babel/plugin-transform-exponentiation-operator" "^7.27.1" + "@babel/plugin-transform-export-namespace-from" "^7.27.1" + "@babel/plugin-transform-for-of" "^7.27.1" + "@babel/plugin-transform-function-name" "^7.27.1" + "@babel/plugin-transform-json-strings" "^7.27.1" + "@babel/plugin-transform-literals" "^7.27.1" + "@babel/plugin-transform-logical-assignment-operators" "^7.27.1" + "@babel/plugin-transform-member-expression-literals" "^7.27.1" + "@babel/plugin-transform-modules-amd" "^7.27.1" + "@babel/plugin-transform-modules-commonjs" "^7.27.1" + "@babel/plugin-transform-modules-systemjs" "^7.27.1" + "@babel/plugin-transform-modules-umd" "^7.27.1" + "@babel/plugin-transform-named-capturing-groups-regex" "^7.27.1" + "@babel/plugin-transform-new-target" "^7.27.1" + "@babel/plugin-transform-nullish-coalescing-operator" "^7.27.1" + "@babel/plugin-transform-numeric-separator" "^7.27.1" + "@babel/plugin-transform-object-rest-spread" "^7.27.2" + "@babel/plugin-transform-object-super" "^7.27.1" + "@babel/plugin-transform-optional-catch-binding" "^7.27.1" + "@babel/plugin-transform-optional-chaining" "^7.27.1" + "@babel/plugin-transform-parameters" "^7.27.1" + "@babel/plugin-transform-private-methods" "^7.27.1" + "@babel/plugin-transform-private-property-in-object" "^7.27.1" + "@babel/plugin-transform-property-literals" "^7.27.1" + "@babel/plugin-transform-regenerator" "^7.27.1" + "@babel/plugin-transform-regexp-modifiers" "^7.27.1" + "@babel/plugin-transform-reserved-words" "^7.27.1" + "@babel/plugin-transform-shorthand-properties" "^7.27.1" + "@babel/plugin-transform-spread" "^7.27.1" + "@babel/plugin-transform-sticky-regex" "^7.27.1" + "@babel/plugin-transform-template-literals" "^7.27.1" + "@babel/plugin-transform-typeof-symbol" "^7.27.1" + "@babel/plugin-transform-unicode-escapes" "^7.27.1" + "@babel/plugin-transform-unicode-property-regex" "^7.27.1" + "@babel/plugin-transform-unicode-regex" "^7.27.1" + "@babel/plugin-transform-unicode-sets-regex" "^7.27.1" + "@babel/preset-modules" "0.1.6-no-external-plugins" + babel-plugin-polyfill-corejs2 "^0.4.10" + babel-plugin-polyfill-corejs3 "^0.11.0" + babel-plugin-polyfill-regenerator "^0.6.1" + core-js-compat "^3.40.0" + semver "^6.3.1" + "@babel/preset-modules@0.1.6-no-external-plugins": version "0.1.6-no-external-plugins" resolved "https://registry.yarnpkg.com/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz#ccb88a2c49c817236861fee7826080573b8a923a" @@ -1181,7 +1961,7 @@ "@babel/types" "^7.4.4" esutils "^2.0.2" -"@babel/preset-react@^7.18.6", "@babel/preset-react@^7.22.5": +"@babel/preset-react@^7.18.6": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.24.7.tgz#480aeb389b2a798880bf1f889199e3641cbb22dc" integrity sha512-AAH4lEkpmzFWrGVlHaxJB7RLH21uPQ9+He+eFLWHmF9IuFQVugz8eAsamaW0DXRrTfco5zj1wWtpdcXJUOfsag== @@ -1193,7 +1973,19 @@ "@babel/plugin-transform-react-jsx-development" "^7.24.7" "@babel/plugin-transform-react-pure-annotations" "^7.24.7" -"@babel/preset-typescript@^7.21.0", "@babel/preset-typescript@^7.22.5": +"@babel/preset-react@^7.25.9": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.27.1.tgz#86ea0a5ca3984663f744be2fd26cb6747c3fd0ec" + integrity sha512-oJHWh2gLhU9dW9HHr42q0cI0/iHHXTLGe39qvpAZZzagHy0MzYLCnCVV0symeRvzmjHyVU7mw2K06E6u/JwbhA== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-validator-option" "^7.27.1" + "@babel/plugin-transform-react-display-name" "^7.27.1" + "@babel/plugin-transform-react-jsx" "^7.27.1" + "@babel/plugin-transform-react-jsx-development" "^7.27.1" + "@babel/plugin-transform-react-pure-annotations" "^7.27.1" + +"@babel/preset-typescript@^7.21.0": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.24.7.tgz#66cd86ea8f8c014855671d5ea9a737139cbbfef1" integrity sha512-SyXRe3OdWwIwalxDg5UtJnJQO+YPcTfwiIY2B0Xlddh9o7jpWLvv8X1RthIeDOxQ+O1ML5BLPCONToObyVQVuQ== @@ -1204,26 +1996,41 @@ "@babel/plugin-transform-modules-commonjs" "^7.24.7" "@babel/plugin-transform-typescript" "^7.24.7" +"@babel/preset-typescript@^7.25.9": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.27.1.tgz#190742a6428d282306648a55b0529b561484f912" + integrity sha512-l7WfQfX0WK4M0v2RudjuQK4u99BS6yLHYEmdtVPP7lKV013zr9DygFuWNlnbvQ9LR+LS0Egz/XAvGx5U9MX0fQ== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-validator-option" "^7.27.1" + "@babel/plugin-syntax-jsx" "^7.27.1" + "@babel/plugin-transform-modules-commonjs" "^7.27.1" + "@babel/plugin-transform-typescript" "^7.27.1" + "@babel/regjsgen@^0.8.0": version "0.8.0" resolved "https://registry.yarnpkg.com/@babel/regjsgen/-/regjsgen-0.8.0.tgz#f0ba69b075e1f05fb2825b7fad991e7adbb18310" integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA== -"@babel/runtime-corejs3@^7.22.6": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.24.7.tgz#65a99097e4c28e6c3a174825591700cc5abd710e" - integrity sha512-eytSX6JLBY6PVAeQa2bFlDx/7Mmln/gaEpsit5a3WEvjGfiIytEsgAwuIXCPM0xvw0v0cJn3ilq0/TvXrW0kgA== +"@babel/runtime-corejs3@^7.25.9": + version "7.27.3" + resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.27.3.tgz#b971a4a0a376171e266629152e74ef50e9931f79" + integrity sha512-ZYcgrwb+dkWNcDlsTe4fH1CMdqMDSJ5lWFd1by8Si2pI54XcQjte/+ViIPqAk7EAWisaUxvQ89grv+bNX2x8zg== dependencies: core-js-pure "^3.30.2" - regenerator-runtime "^0.14.0" -"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.3", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.15.4", "@babel/runtime@^7.22.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2": +"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.3", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.15.4", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2": version "7.27.0" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.27.0.tgz#fbee7cf97c709518ecc1f590984481d5460d4762" integrity sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw== dependencies: regenerator-runtime "^0.14.0" +"@babel/runtime@^7.25.9": + version "7.27.3" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.27.3.tgz#10491113799fb8d77e1d9273384d5d68deeea8f6" + integrity sha512-7EYtGezsdiDMyY80+65EzwiGmcJqpmcZCojSXaRgdrBaGtWTgDZKq69cPIVped6MkIM78cTQ2GOiEYjwOlG4xw== + "@babel/template@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.24.7.tgz#02efcee317d0609d2c07117cb70ef8fb17ab7315" @@ -1242,7 +2049,16 @@ "@babel/parser" "^7.27.0" "@babel/types" "^7.27.0" -"@babel/traverse@^7.22.8", "@babel/traverse@^7.24.7": +"@babel/template@^7.27.1", "@babel/template@^7.27.2": + version "7.27.2" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.27.2.tgz#fa78ceed3c4e7b63ebf6cb39e5852fca45f6809d" + integrity sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw== + dependencies: + "@babel/code-frame" "^7.27.1" + "@babel/parser" "^7.27.2" + "@babel/types" "^7.27.1" + +"@babel/traverse@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.24.7.tgz#de2b900163fa741721ba382163fe46a936c40cf5" integrity sha512-yb65Ed5S/QAcewNPh0nZczy9JdYXkkAbIsEo+P7BE7yO3txAY30Y/oPa3QkQ5It3xVG2kpKMg9MsdxZaO31uKA== @@ -1258,6 +2074,19 @@ debug "^4.3.1" globals "^11.1.0" +"@babel/traverse@^7.25.9", "@babel/traverse@^7.27.1", "@babel/traverse@^7.27.3": + version "7.27.3" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.27.3.tgz#8b62a6c2d10f9d921ba7339c90074708509cffae" + integrity sha512-lId/IfN/Ye1CIu8xG7oKBHXd2iNb2aW1ilPszzGcJug6M8RCKfVNcYhpI5+bMvFYjK7lXIM0R+a+6r8xhHp2FQ== + dependencies: + "@babel/code-frame" "^7.27.1" + "@babel/generator" "^7.27.3" + "@babel/parser" "^7.27.3" + "@babel/template" "^7.27.2" + "@babel/types" "^7.27.3" + debug "^4.3.1" + globals "^11.1.0" + "@babel/types@^7.21.3", "@babel/types@^7.24.7", "@babel/types@^7.4.4": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.24.7.tgz#6027fe12bc1aa724cd32ab113fb7f1988f1f66f2" @@ -1275,10 +2104,18 @@ "@babel/helper-string-parser" "^7.25.9" "@babel/helper-validator-identifier" "^7.25.9" -"@braintree/sanitize-url@^6.0.1": - version "6.0.4" - resolved "https://registry.yarnpkg.com/@braintree/sanitize-url/-/sanitize-url-6.0.4.tgz#923ca57e173c6b232bbbb07347b1be982f03e783" - integrity sha512-s3jaWicZd0pkP0jf5ysyHUI/RE7MHos6qlToFcGWXVp+ykHOy77OUMrfbgJ9it2C5bow7OIQwYYaHjk9XlBQ2A== +"@babel/types@^7.27.1", "@babel/types@^7.27.3": + version "7.27.3" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.27.3.tgz#c0257bedf33aad6aad1f406d35c44758321eb3ec" + integrity sha512-Y1GkI4ktrtvmawoSq+4FCVHNryea6uR+qUQy0AGxLSsjCX0nVmkYQMBLHDkXZuo5hGx7eYdnIaslsdBFm7zbUw== + dependencies: + "@babel/helper-string-parser" "^7.27.1" + "@babel/helper-validator-identifier" "^7.27.1" + +"@braintree/sanitize-url@^7.0.4": + version "7.1.1" + resolved "https://registry.yarnpkg.com/@braintree/sanitize-url/-/sanitize-url-7.1.1.tgz#15e19737d946559289b915e5dad3b4c28407735e" + integrity sha512-i1L7noDNxtFyL5DmZafWy1wRVhGehQmzZaz1HiN5e7iylJMSZR7ekOV7NsIqa5qBldlLrsKv4HbgFUVlQrz8Mw== "@bufbuild/buf-darwin-arm64@1.33.0": version "1.33.0" @@ -1322,138 +2159,543 @@ "@bufbuild/buf-win32-arm64" "1.33.0" "@bufbuild/buf-win32-x64" "1.33.0" +"@chevrotain/cst-dts-gen@11.0.3": + version "11.0.3" + resolved "https://registry.yarnpkg.com/@chevrotain/cst-dts-gen/-/cst-dts-gen-11.0.3.tgz#5e0863cc57dc45e204ccfee6303225d15d9d4783" + integrity sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ== + dependencies: + "@chevrotain/gast" "11.0.3" + "@chevrotain/types" "11.0.3" + lodash-es "4.17.21" + +"@chevrotain/gast@11.0.3": + version "11.0.3" + resolved "https://registry.yarnpkg.com/@chevrotain/gast/-/gast-11.0.3.tgz#e84d8880323fe8cbe792ef69ce3ffd43a936e818" + integrity sha512-+qNfcoNk70PyS/uxmj3li5NiECO+2YKZZQMbmjTqRI3Qchu8Hig/Q9vgkHpI3alNjr7M+a2St5pw5w5F6NL5/Q== + dependencies: + "@chevrotain/types" "11.0.3" + lodash-es "4.17.21" + +"@chevrotain/regexp-to-ast@11.0.3": + version "11.0.3" + resolved "https://registry.yarnpkg.com/@chevrotain/regexp-to-ast/-/regexp-to-ast-11.0.3.tgz#11429a81c74a8e6a829271ce02fc66166d56dcdb" + integrity sha512-1fMHaBZxLFvWI067AVbGJav1eRY7N8DDvYCTwGBiE/ytKBgP8azTdgyrKyWZ9Mfh09eHWb5PgTSO8wi7U824RA== + +"@chevrotain/types@11.0.3": + version "11.0.3" + resolved "https://registry.yarnpkg.com/@chevrotain/types/-/types-11.0.3.tgz#f8a03914f7b937f594f56eb89312b3b8f1c91848" + integrity sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ== + +"@chevrotain/utils@11.0.3": + version "11.0.3" + resolved "https://registry.yarnpkg.com/@chevrotain/utils/-/utils-11.0.3.tgz#e39999307b102cff3645ec4f5b3665f5297a2224" + integrity sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ== + "@colors/colors@1.5.0": version "1.5.0" resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ== +"@csstools/cascade-layer-name-parser@^2.0.5": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@csstools/cascade-layer-name-parser/-/cascade-layer-name-parser-2.0.5.tgz#43f962bebead0052a9fed1a2deeb11f85efcbc72" + integrity sha512-p1ko5eHgV+MgXFVa4STPKpvPxr6ReS8oS2jzTukjR74i5zJNyWO1ZM1m8YKBXnzDKWfBN1ztLYlHxbVemDD88A== + +"@csstools/color-helpers@^5.0.2": + version "5.0.2" + resolved "https://registry.yarnpkg.com/@csstools/color-helpers/-/color-helpers-5.0.2.tgz#82592c9a7c2b83c293d9161894e2a6471feb97b8" + integrity sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA== + +"@csstools/css-calc@^2.1.4": + version "2.1.4" + resolved "https://registry.yarnpkg.com/@csstools/css-calc/-/css-calc-2.1.4.tgz#8473f63e2fcd6e459838dd412401d5948f224c65" + integrity sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ== + +"@csstools/css-color-parser@^3.0.10": + version "3.0.10" + resolved "https://registry.yarnpkg.com/@csstools/css-color-parser/-/css-color-parser-3.0.10.tgz#79fc68864dd43c3b6782d2b3828bc0fa9d085c10" + integrity sha512-TiJ5Ajr6WRd1r8HSiwJvZBiJOqtH86aHpUjq5aEKWHiII2Qfjqd/HCWKPOW8EP4vcspXbHnXrwIDlu5savQipg== + dependencies: + "@csstools/color-helpers" "^5.0.2" + "@csstools/css-calc" "^2.1.4" + +"@csstools/css-parser-algorithms@^3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz#5755370a9a29abaec5515b43c8b3f2cf9c2e3076" + integrity sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ== + +"@csstools/css-tokenizer@^3.0.4": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz#333fedabc3fd1a8e5d0100013731cf19e6a8c5d3" + integrity sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw== + +"@csstools/media-query-list-parser@^4.0.3": + version "4.0.3" + resolved "https://registry.yarnpkg.com/@csstools/media-query-list-parser/-/media-query-list-parser-4.0.3.tgz#7aec77bcb89c2da80ef207e73f474ef9e1b3cdf1" + integrity sha512-HAYH7d3TLRHDOUQK4mZKf9k9Ph/m8Akstg66ywKR4SFAigjs3yBiUeZtFxywiTm5moZMAp/5W/ZuFnNXXYLuuQ== + +"@csstools/postcss-cascade-layers@^5.0.1": + version "5.0.1" + resolved "https://registry.yarnpkg.com/@csstools/postcss-cascade-layers/-/postcss-cascade-layers-5.0.1.tgz#9640313e64b5e39133de7e38a5aa7f40dc259597" + integrity sha512-XOfhI7GShVcKiKwmPAnWSqd2tBR0uxt+runAxttbSp/LY2U16yAVPmAf7e9q4JJ0d+xMNmpwNDLBXnmRCl3HMQ== + dependencies: + "@csstools/selector-specificity" "^5.0.0" + postcss-selector-parser "^7.0.0" + +"@csstools/postcss-color-function@^4.0.10": + version "4.0.10" + resolved "https://registry.yarnpkg.com/@csstools/postcss-color-function/-/postcss-color-function-4.0.10.tgz#11ad43a66ef2cc794ab826a07df8b5fa9fb47a3a" + integrity sha512-4dY0NBu7NVIpzxZRgh/Q/0GPSz/jLSw0i/u3LTUor0BkQcz/fNhN10mSWBDsL0p9nDb0Ky1PD6/dcGbhACuFTQ== + dependencies: + "@csstools/css-color-parser" "^3.0.10" + "@csstools/css-parser-algorithms" "^3.0.5" + "@csstools/css-tokenizer" "^3.0.4" + "@csstools/postcss-progressive-custom-properties" "^4.1.0" + "@csstools/utilities" "^2.0.0" + +"@csstools/postcss-color-mix-function@^3.0.10": + version "3.0.10" + resolved "https://registry.yarnpkg.com/@csstools/postcss-color-mix-function/-/postcss-color-mix-function-3.0.10.tgz#8c9d0ccfae5c45a9870dd84807ea2995c7a3a514" + integrity sha512-P0lIbQW9I4ShE7uBgZRib/lMTf9XMjJkFl/d6w4EMNHu2qvQ6zljJGEcBkw/NsBtq/6q3WrmgxSS8kHtPMkK4Q== + dependencies: + "@csstools/css-color-parser" "^3.0.10" + "@csstools/css-parser-algorithms" "^3.0.5" + "@csstools/css-tokenizer" "^3.0.4" + "@csstools/postcss-progressive-custom-properties" "^4.1.0" + "@csstools/utilities" "^2.0.0" + +"@csstools/postcss-color-mix-variadic-function-arguments@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@csstools/postcss-color-mix-variadic-function-arguments/-/postcss-color-mix-variadic-function-arguments-1.0.0.tgz#0b29cb9b4630d7ed68549db265662d41554a17ed" + integrity sha512-Z5WhouTyD74dPFPrVE7KydgNS9VvnjB8qcdes9ARpCOItb4jTnm7cHp4FhxCRUoyhabD0WVv43wbkJ4p8hLAlQ== + dependencies: + "@csstools/css-color-parser" "^3.0.10" + "@csstools/css-parser-algorithms" "^3.0.5" + "@csstools/css-tokenizer" "^3.0.4" + "@csstools/postcss-progressive-custom-properties" "^4.1.0" + "@csstools/utilities" "^2.0.0" + +"@csstools/postcss-content-alt-text@^2.0.6": + version "2.0.6" + resolved "https://registry.yarnpkg.com/@csstools/postcss-content-alt-text/-/postcss-content-alt-text-2.0.6.tgz#548862226eac54bab0ee5f1bf3a9981393ab204b" + integrity sha512-eRjLbOjblXq+byyaedQRSrAejKGNAFued+LcbzT+LCL78fabxHkxYjBbxkroONxHHYu2qxhFK2dBStTLPG3jpQ== + dependencies: + "@csstools/css-parser-algorithms" "^3.0.5" + "@csstools/css-tokenizer" "^3.0.4" + "@csstools/postcss-progressive-custom-properties" "^4.1.0" + "@csstools/utilities" "^2.0.0" + +"@csstools/postcss-exponential-functions@^2.0.9": + version "2.0.9" + resolved "https://registry.yarnpkg.com/@csstools/postcss-exponential-functions/-/postcss-exponential-functions-2.0.9.tgz#fc03d1272888cb77e64cc1a7d8a33016e4f05c69" + integrity sha512-abg2W/PI3HXwS/CZshSa79kNWNZHdJPMBXeZNyPQFbbj8sKO3jXxOt/wF7juJVjyDTc6JrvaUZYFcSBZBhaxjw== + dependencies: + "@csstools/css-calc" "^2.1.4" + "@csstools/css-parser-algorithms" "^3.0.5" + "@csstools/css-tokenizer" "^3.0.4" + +"@csstools/postcss-font-format-keywords@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@csstools/postcss-font-format-keywords/-/postcss-font-format-keywords-4.0.0.tgz#6730836eb0153ff4f3840416cc2322f129c086e6" + integrity sha512-usBzw9aCRDvchpok6C+4TXC57btc4bJtmKQWOHQxOVKen1ZfVqBUuCZ/wuqdX5GHsD0NRSr9XTP+5ID1ZZQBXw== + dependencies: + "@csstools/utilities" "^2.0.0" + postcss-value-parser "^4.2.0" + +"@csstools/postcss-gamut-mapping@^2.0.10": + version "2.0.10" + resolved "https://registry.yarnpkg.com/@csstools/postcss-gamut-mapping/-/postcss-gamut-mapping-2.0.10.tgz#f518d941231d721dbecf5b41e3c441885ff2f28b" + integrity sha512-QDGqhJlvFnDlaPAfCYPsnwVA6ze+8hhrwevYWlnUeSjkkZfBpcCO42SaUD8jiLlq7niouyLgvup5lh+f1qessg== + dependencies: + "@csstools/css-color-parser" "^3.0.10" + "@csstools/css-parser-algorithms" "^3.0.5" + "@csstools/css-tokenizer" "^3.0.4" + +"@csstools/postcss-gradients-interpolation-method@^5.0.10": + version "5.0.10" + resolved "https://registry.yarnpkg.com/@csstools/postcss-gradients-interpolation-method/-/postcss-gradients-interpolation-method-5.0.10.tgz#3146da352c31142a721fdba062ac3a6d11dbbec3" + integrity sha512-HHPauB2k7Oits02tKFUeVFEU2ox/H3OQVrP3fSOKDxvloOikSal+3dzlyTZmYsb9FlY9p5EUpBtz0//XBmy+aw== + dependencies: + "@csstools/css-color-parser" "^3.0.10" + "@csstools/css-parser-algorithms" "^3.0.5" + "@csstools/css-tokenizer" "^3.0.4" + "@csstools/postcss-progressive-custom-properties" "^4.1.0" + "@csstools/utilities" "^2.0.0" + +"@csstools/postcss-hwb-function@^4.0.10": + version "4.0.10" + resolved "https://registry.yarnpkg.com/@csstools/postcss-hwb-function/-/postcss-hwb-function-4.0.10.tgz#f93f3c457e6440ac37ef9b908feb5d901b417d50" + integrity sha512-nOKKfp14SWcdEQ++S9/4TgRKchooLZL0TUFdun3nI4KPwCjETmhjta1QT4ICQcGVWQTvrsgMM/aLB5We+kMHhQ== + dependencies: + "@csstools/css-color-parser" "^3.0.10" + "@csstools/css-parser-algorithms" "^3.0.5" + "@csstools/css-tokenizer" "^3.0.4" + "@csstools/postcss-progressive-custom-properties" "^4.1.0" + "@csstools/utilities" "^2.0.0" + +"@csstools/postcss-ic-unit@^4.0.2": + version "4.0.2" + resolved "https://registry.yarnpkg.com/@csstools/postcss-ic-unit/-/postcss-ic-unit-4.0.2.tgz#7561e09db65fac8304ceeab9dd3e5c6e43414587" + integrity sha512-lrK2jjyZwh7DbxaNnIUjkeDmU8Y6KyzRBk91ZkI5h8nb1ykEfZrtIVArdIjX4DHMIBGpdHrgP0n4qXDr7OHaKA== + dependencies: + "@csstools/postcss-progressive-custom-properties" "^4.1.0" + "@csstools/utilities" "^2.0.0" + postcss-value-parser "^4.2.0" + +"@csstools/postcss-initial@^2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@csstools/postcss-initial/-/postcss-initial-2.0.1.tgz#c385bd9d8ad31ad159edd7992069e97ceea4d09a" + integrity sha512-L1wLVMSAZ4wovznquK0xmC7QSctzO4D0Is590bxpGqhqjboLXYA16dWZpfwImkdOgACdQ9PqXsuRroW6qPlEsg== + +"@csstools/postcss-is-pseudo-class@^5.0.1": + version "5.0.1" + resolved "https://registry.yarnpkg.com/@csstools/postcss-is-pseudo-class/-/postcss-is-pseudo-class-5.0.1.tgz#12041448fedf01090dd4626022c28b7f7623f58e" + integrity sha512-JLp3POui4S1auhDR0n8wHd/zTOWmMsmK3nQd3hhL6FhWPaox5W7j1se6zXOG/aP07wV2ww0lxbKYGwbBszOtfQ== + dependencies: + "@csstools/selector-specificity" "^5.0.0" + postcss-selector-parser "^7.0.0" + +"@csstools/postcss-light-dark-function@^2.0.9": + version "2.0.9" + resolved "https://registry.yarnpkg.com/@csstools/postcss-light-dark-function/-/postcss-light-dark-function-2.0.9.tgz#9fb080188907539734a9d5311d2a1cb82531ef38" + integrity sha512-1tCZH5bla0EAkFAI2r0H33CDnIBeLUaJh1p+hvvsylJ4svsv2wOmJjJn+OXwUZLXef37GYbRIVKX+X+g6m+3CQ== + dependencies: + "@csstools/css-parser-algorithms" "^3.0.5" + "@csstools/css-tokenizer" "^3.0.4" + "@csstools/postcss-progressive-custom-properties" "^4.1.0" + "@csstools/utilities" "^2.0.0" + +"@csstools/postcss-logical-float-and-clear@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@csstools/postcss-logical-float-and-clear/-/postcss-logical-float-and-clear-3.0.0.tgz#62617564182cf86ab5d4e7485433ad91e4c58571" + integrity sha512-SEmaHMszwakI2rqKRJgE+8rpotFfne1ZS6bZqBoQIicFyV+xT1UF42eORPxJkVJVrH9C0ctUgwMSn3BLOIZldQ== + +"@csstools/postcss-logical-overflow@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@csstools/postcss-logical-overflow/-/postcss-logical-overflow-2.0.0.tgz#c6de7c5f04e3d4233731a847f6c62819bcbcfa1d" + integrity sha512-spzR1MInxPuXKEX2csMamshR4LRaSZ3UXVaRGjeQxl70ySxOhMpP2252RAFsg8QyyBXBzuVOOdx1+bVO5bPIzA== + +"@csstools/postcss-logical-overscroll-behavior@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@csstools/postcss-logical-overscroll-behavior/-/postcss-logical-overscroll-behavior-2.0.0.tgz#43c03eaecdf34055ef53bfab691db6dc97a53d37" + integrity sha512-e/webMjoGOSYfqLunyzByZj5KKe5oyVg/YSbie99VEaSDE2kimFm0q1f6t/6Jo+VVCQ/jbe2Xy+uX+C4xzWs4w== + +"@csstools/postcss-logical-resize@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@csstools/postcss-logical-resize/-/postcss-logical-resize-3.0.0.tgz#4df0eeb1a61d7bd85395e56a5cce350b5dbfdca6" + integrity sha512-DFbHQOFW/+I+MY4Ycd/QN6Dg4Hcbb50elIJCfnwkRTCX05G11SwViI5BbBlg9iHRl4ytB7pmY5ieAFk3ws7yyg== + dependencies: + postcss-value-parser "^4.2.0" + +"@csstools/postcss-logical-viewport-units@^3.0.4": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@csstools/postcss-logical-viewport-units/-/postcss-logical-viewport-units-3.0.4.tgz#016d98a8b7b5f969e58eb8413447eb801add16fc" + integrity sha512-q+eHV1haXA4w9xBwZLKjVKAWn3W2CMqmpNpZUk5kRprvSiBEGMgrNH3/sJZ8UA3JgyHaOt3jwT9uFa4wLX4EqQ== + dependencies: + "@csstools/css-tokenizer" "^3.0.4" + "@csstools/utilities" "^2.0.0" + +"@csstools/postcss-media-minmax@^2.0.9": + version "2.0.9" + resolved "https://registry.yarnpkg.com/@csstools/postcss-media-minmax/-/postcss-media-minmax-2.0.9.tgz#184252d5b93155ae526689328af6bdf3fc113987" + integrity sha512-af9Qw3uS3JhYLnCbqtZ9crTvvkR+0Se+bBqSr7ykAnl9yKhk6895z9rf+2F4dClIDJWxgn0iZZ1PSdkhrbs2ig== + dependencies: + "@csstools/css-calc" "^2.1.4" + "@csstools/css-parser-algorithms" "^3.0.5" + "@csstools/css-tokenizer" "^3.0.4" + "@csstools/media-query-list-parser" "^4.0.3" + +"@csstools/postcss-media-queries-aspect-ratio-number-values@^3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@csstools/postcss-media-queries-aspect-ratio-number-values/-/postcss-media-queries-aspect-ratio-number-values-3.0.5.tgz#f485c31ec13d6b0fb5c528a3474334a40eff5f11" + integrity sha512-zhAe31xaaXOY2Px8IYfoVTB3wglbJUVigGphFLj6exb7cjZRH9A6adyE22XfFK3P2PzwRk0VDeTJmaxpluyrDg== + dependencies: + "@csstools/css-parser-algorithms" "^3.0.5" + "@csstools/css-tokenizer" "^3.0.4" + "@csstools/media-query-list-parser" "^4.0.3" + +"@csstools/postcss-nested-calc@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@csstools/postcss-nested-calc/-/postcss-nested-calc-4.0.0.tgz#754e10edc6958d664c11cde917f44ba144141c62" + integrity sha512-jMYDdqrQQxE7k9+KjstC3NbsmC063n1FTPLCgCRS2/qHUbHM0mNy9pIn4QIiQGs9I/Bg98vMqw7mJXBxa0N88A== + dependencies: + "@csstools/utilities" "^2.0.0" + postcss-value-parser "^4.2.0" + +"@csstools/postcss-normalize-display-values@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@csstools/postcss-normalize-display-values/-/postcss-normalize-display-values-4.0.0.tgz#ecdde2daf4e192e5da0c6fd933b6d8aff32f2a36" + integrity sha512-HlEoG0IDRoHXzXnkV4in47dzsxdsjdz6+j7MLjaACABX2NfvjFS6XVAnpaDyGesz9gK2SC7MbNwdCHusObKJ9Q== + dependencies: + postcss-value-parser "^4.2.0" + +"@csstools/postcss-oklab-function@^4.0.10": + version "4.0.10" + resolved "https://registry.yarnpkg.com/@csstools/postcss-oklab-function/-/postcss-oklab-function-4.0.10.tgz#d4c23c51dd0be45e6dedde22432d7d0003710780" + integrity sha512-ZzZUTDd0fgNdhv8UUjGCtObPD8LYxMH+MJsW9xlZaWTV8Ppr4PtxlHYNMmF4vVWGl0T6f8tyWAKjoI6vePSgAg== + dependencies: + "@csstools/css-color-parser" "^3.0.10" + "@csstools/css-parser-algorithms" "^3.0.5" + "@csstools/css-tokenizer" "^3.0.4" + "@csstools/postcss-progressive-custom-properties" "^4.1.0" + "@csstools/utilities" "^2.0.0" + +"@csstools/postcss-progressive-custom-properties@^4.1.0": + version "4.1.0" + resolved "https://registry.yarnpkg.com/@csstools/postcss-progressive-custom-properties/-/postcss-progressive-custom-properties-4.1.0.tgz#70c8d41b577f4023633b7e3791604e0b7f3775bc" + integrity sha512-YrkI9dx8U4R8Sz2EJaoeD9fI7s7kmeEBfmO+UURNeL6lQI7VxF6sBE+rSqdCBn4onwqmxFdBU3lTwyYb/lCmxA== + dependencies: + postcss-value-parser "^4.2.0" + +"@csstools/postcss-random-function@^2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@csstools/postcss-random-function/-/postcss-random-function-2.0.1.tgz#3191f32fe72936e361dadf7dbfb55a0209e2691e" + integrity sha512-q+FQaNiRBhnoSNo+GzqGOIBKoHQ43lYz0ICrV+UudfWnEF6ksS6DsBIJSISKQT2Bvu3g4k6r7t0zYrk5pDlo8w== + dependencies: + "@csstools/css-calc" "^2.1.4" + "@csstools/css-parser-algorithms" "^3.0.5" + "@csstools/css-tokenizer" "^3.0.4" + +"@csstools/postcss-relative-color-syntax@^3.0.10": + version "3.0.10" + resolved "https://registry.yarnpkg.com/@csstools/postcss-relative-color-syntax/-/postcss-relative-color-syntax-3.0.10.tgz#daa840583969461e1e06b12e9c591e52a790ec86" + integrity sha512-8+0kQbQGg9yYG8hv0dtEpOMLwB9M+P7PhacgIzVzJpixxV4Eq9AUQtQw8adMmAJU1RBBmIlpmtmm3XTRd/T00g== + dependencies: + "@csstools/css-color-parser" "^3.0.10" + "@csstools/css-parser-algorithms" "^3.0.5" + "@csstools/css-tokenizer" "^3.0.4" + "@csstools/postcss-progressive-custom-properties" "^4.1.0" + "@csstools/utilities" "^2.0.0" + +"@csstools/postcss-scope-pseudo-class@^4.0.1": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@csstools/postcss-scope-pseudo-class/-/postcss-scope-pseudo-class-4.0.1.tgz#9fe60e9d6d91d58fb5fc6c768a40f6e47e89a235" + integrity sha512-IMi9FwtH6LMNuLea1bjVMQAsUhFxJnyLSgOp/cpv5hrzWmrUYU5fm0EguNDIIOHUqzXode8F/1qkC/tEo/qN8Q== + dependencies: + postcss-selector-parser "^7.0.0" + +"@csstools/postcss-sign-functions@^1.1.4": + version "1.1.4" + resolved "https://registry.yarnpkg.com/@csstools/postcss-sign-functions/-/postcss-sign-functions-1.1.4.tgz#a9ac56954014ae4c513475b3f1b3e3424a1e0c12" + integrity sha512-P97h1XqRPcfcJndFdG95Gv/6ZzxUBBISem0IDqPZ7WMvc/wlO+yU0c5D/OCpZ5TJoTt63Ok3knGk64N+o6L2Pg== + dependencies: + "@csstools/css-calc" "^2.1.4" + "@csstools/css-parser-algorithms" "^3.0.5" + "@csstools/css-tokenizer" "^3.0.4" + +"@csstools/postcss-stepped-value-functions@^4.0.9": + version "4.0.9" + resolved "https://registry.yarnpkg.com/@csstools/postcss-stepped-value-functions/-/postcss-stepped-value-functions-4.0.9.tgz#36036f1a0e5e5ee2308e72f3c9cb433567c387b9" + integrity sha512-h9btycWrsex4dNLeQfyU3y3w40LMQooJWFMm/SK9lrKguHDcFl4VMkncKKoXi2z5rM9YGWbUQABI8BT2UydIcA== + dependencies: + "@csstools/css-calc" "^2.1.4" + "@csstools/css-parser-algorithms" "^3.0.5" + "@csstools/css-tokenizer" "^3.0.4" + +"@csstools/postcss-text-decoration-shorthand@^4.0.2": + version "4.0.2" + resolved "https://registry.yarnpkg.com/@csstools/postcss-text-decoration-shorthand/-/postcss-text-decoration-shorthand-4.0.2.tgz#a3bcf80492e6dda36477538ab8e8943908c9f80a" + integrity sha512-8XvCRrFNseBSAGxeaVTaNijAu+FzUvjwFXtcrynmazGb/9WUdsPCpBX+mHEHShVRq47Gy4peYAoxYs8ltUnmzA== + dependencies: + "@csstools/color-helpers" "^5.0.2" + postcss-value-parser "^4.2.0" + +"@csstools/postcss-trigonometric-functions@^4.0.9": + version "4.0.9" + resolved "https://registry.yarnpkg.com/@csstools/postcss-trigonometric-functions/-/postcss-trigonometric-functions-4.0.9.tgz#3f94ed2e319b57f2c59720b64e4d0a8a6fb8c3b2" + integrity sha512-Hnh5zJUdpNrJqK9v1/E3BbrQhaDTj5YiX7P61TOvUhoDHnUmsNNxcDAgkQ32RrcWx9GVUvfUNPcUkn8R3vIX6A== + dependencies: + "@csstools/css-calc" "^2.1.4" + "@csstools/css-parser-algorithms" "^3.0.5" + "@csstools/css-tokenizer" "^3.0.4" + +"@csstools/postcss-unset-value@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@csstools/postcss-unset-value/-/postcss-unset-value-4.0.0.tgz#7caa981a34196d06a737754864baf77d64de4bba" + integrity sha512-cBz3tOCI5Fw6NIFEwU3RiwK6mn3nKegjpJuzCndoGq3BZPkUjnsq7uQmIeMNeMbMk7YD2MfKcgCpZwX5jyXqCA== + +"@csstools/selector-resolve-nested@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@csstools/selector-resolve-nested/-/selector-resolve-nested-3.0.0.tgz#704a9b637975680e025e069a4c58b3beb3e2752a" + integrity sha512-ZoK24Yku6VJU1gS79a5PFmC8yn3wIapiKmPgun0hZgEI5AOqgH2kiPRsPz1qkGv4HL+wuDLH83yQyk6inMYrJQ== + +"@csstools/selector-specificity@^5.0.0": + version "5.0.0" + resolved "https://registry.yarnpkg.com/@csstools/selector-specificity/-/selector-specificity-5.0.0.tgz#037817b574262134cabd68fc4ec1a454f168407b" + integrity sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw== + +"@csstools/utilities@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@csstools/utilities/-/utilities-2.0.0.tgz#f7ff0fee38c9ffb5646d47b6906e0bc8868bde60" + integrity sha512-5VdOr0Z71u+Yp3ozOx8T11N703wIFGVRgOWbOZMKgglPJsWA54MRIoMNVMa7shUToIhx5J8vX4sOZgD2XiihiQ== + "@discoveryjs/json-ext@0.5.7": version "0.5.7" resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70" integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw== -"@docsearch/css@3.6.0": - version "3.6.0" - resolved "https://registry.yarnpkg.com/@docsearch/css/-/css-3.6.0.tgz#0e9f56f704b3a34d044d15fd9962ebc1536ba4fb" - integrity sha512-+sbxb71sWre+PwDK7X2T8+bhS6clcVMLwBPznX45Qu6opJcgRjAp7gYSDzVFp187J+feSj5dNBN1mJoi6ckkUQ== +"@docsearch/css@3.9.0": + version "3.9.0" + resolved "https://registry.yarnpkg.com/@docsearch/css/-/css-3.9.0.tgz#3bc29c96bf024350d73b0cfb7c2a7b71bf251cd5" + integrity sha512-cQbnVbq0rrBwNAKegIac/t6a8nWoUAn8frnkLFW6YARaRmAQr5/Eoe6Ln2fqkUCZ40KpdrKbpSAmgrkviOxuWA== -"@docsearch/react@^3.5.2": - version "3.6.0" - resolved "https://registry.yarnpkg.com/@docsearch/react/-/react-3.6.0.tgz#b4f25228ecb7fc473741aefac592121e86dd2958" - integrity sha512-HUFut4ztcVNmqy9gp/wxNbC7pTOHhgVVkHVGCACTuLhUKUhKAF9KYHJtMiLUJxEqiFLQiuri1fWF8zqwM/cu1w== +"@docsearch/react@^3.9.0": + version "3.9.0" + resolved "https://registry.yarnpkg.com/@docsearch/react/-/react-3.9.0.tgz#d0842b700c3ee26696786f3c8ae9f10c1a3f0db3" + integrity sha512-mb5FOZYZIkRQ6s/NWnM98k879vu5pscWqTLubLFBO87igYYT4VzVazh4h5o/zCvTIZgEt3PvsCOMOswOUo9yHQ== dependencies: - "@algolia/autocomplete-core" "1.9.3" - "@algolia/autocomplete-preset-algolia" "1.9.3" - "@docsearch/css" "3.6.0" - algoliasearch "^4.19.1" + "@algolia/autocomplete-core" "1.17.9" + "@algolia/autocomplete-preset-algolia" "1.17.9" + "@docsearch/css" "3.9.0" + algoliasearch "^5.14.2" -"@docusaurus/core@3.4.0": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/core/-/core-3.4.0.tgz#bdbf1af4b2f25d1bf4a5b62ec6137d84c821cb3c" - integrity sha512-g+0wwmN2UJsBqy2fQRQ6fhXruoEa62JDeEa5d8IdTJlMoaDaEDfHh7WjwGRn4opuTQWpjAwP/fbcgyHKlE+64w== +"@docusaurus/babel@3.8.0": + version "3.8.0" + resolved "https://registry.yarnpkg.com/@docusaurus/babel/-/babel-3.8.0.tgz#2f390cc4e588a96ec496d87921e44890899738a6" + integrity sha512-9EJwSgS6TgB8IzGk1L8XddJLhZod8fXT4ULYMx6SKqyCBqCFpVCEjR/hNXXhnmtVM2irDuzYoVLGWv7srG/VOA== dependencies: - "@babel/core" "^7.23.3" - "@babel/generator" "^7.23.3" + "@babel/core" "^7.25.9" + "@babel/generator" "^7.25.9" "@babel/plugin-syntax-dynamic-import" "^7.8.3" - "@babel/plugin-transform-runtime" "^7.22.9" - "@babel/preset-env" "^7.22.9" - "@babel/preset-react" "^7.22.5" - "@babel/preset-typescript" "^7.22.5" - "@babel/runtime" "^7.22.6" - "@babel/runtime-corejs3" "^7.22.6" - "@babel/traverse" "^7.22.8" - "@docusaurus/cssnano-preset" "3.4.0" - "@docusaurus/logger" "3.4.0" - "@docusaurus/mdx-loader" "3.4.0" - "@docusaurus/utils" "3.4.0" - "@docusaurus/utils-common" "3.4.0" - "@docusaurus/utils-validation" "3.4.0" - autoprefixer "^10.4.14" - babel-loader "^9.1.3" + "@babel/plugin-transform-runtime" "^7.25.9" + "@babel/preset-env" "^7.25.9" + "@babel/preset-react" "^7.25.9" + "@babel/preset-typescript" "^7.25.9" + "@babel/runtime" "^7.25.9" + "@babel/runtime-corejs3" "^7.25.9" + "@babel/traverse" "^7.25.9" + "@docusaurus/logger" "3.8.0" + "@docusaurus/utils" "3.8.0" babel-plugin-dynamic-import-node "^2.3.3" - boxen "^6.2.1" - chalk "^4.1.2" - chokidar "^3.5.3" + fs-extra "^11.1.1" + tslib "^2.6.0" + +"@docusaurus/bundler@3.8.0": + version "3.8.0" + resolved "https://registry.yarnpkg.com/@docusaurus/bundler/-/bundler-3.8.0.tgz#386f54dca594d81bac6b617c71822e0808d6e2f6" + integrity sha512-Rq4Z/MSeAHjVzBLirLeMcjLIAQy92pF1OI+2rmt18fSlMARfTGLWRE8Vb+ljQPTOSfJxwDYSzsK6i7XloD2rNA== + dependencies: + "@babel/core" "^7.25.9" + "@docusaurus/babel" "3.8.0" + "@docusaurus/cssnano-preset" "3.8.0" + "@docusaurus/logger" "3.8.0" + "@docusaurus/types" "3.8.0" + "@docusaurus/utils" "3.8.0" + babel-loader "^9.2.1" clean-css "^5.3.2" - cli-table3 "^0.6.3" - combine-promises "^1.1.0" - commander "^5.1.0" copy-webpack-plugin "^11.0.0" - core-js "^3.31.1" css-loader "^6.8.1" css-minimizer-webpack-plugin "^5.0.1" cssnano "^6.1.2" - del "^6.1.1" + file-loader "^6.2.0" + html-minifier-terser "^7.2.0" + mini-css-extract-plugin "^2.9.1" + null-loader "^4.0.1" + postcss "^8.4.26" + postcss-loader "^7.3.3" + postcss-preset-env "^10.1.0" + terser-webpack-plugin "^5.3.9" + tslib "^2.6.0" + url-loader "^4.1.1" + webpack "^5.95.0" + webpackbar "^6.0.1" + +"@docusaurus/core@3.8.0", "@docusaurus/core@^3.8.0": + version "3.8.0" + resolved "https://registry.yarnpkg.com/@docusaurus/core/-/core-3.8.0.tgz#79d5e1084415c8834a8a5cb87162ca13f52fe147" + integrity sha512-c7u6zFELmSGPEP9WSubhVDjgnpiHgDqMh1qVdCB7rTflh4Jx0msTYmMiO91Ez0KtHj4sIsDsASnjwfJ2IZp3Vw== + dependencies: + "@docusaurus/babel" "3.8.0" + "@docusaurus/bundler" "3.8.0" + "@docusaurus/logger" "3.8.0" + "@docusaurus/mdx-loader" "3.8.0" + "@docusaurus/utils" "3.8.0" + "@docusaurus/utils-common" "3.8.0" + "@docusaurus/utils-validation" "3.8.0" + boxen "^6.2.1" + chalk "^4.1.2" + chokidar "^3.5.3" + cli-table3 "^0.6.3" + combine-promises "^1.1.0" + commander "^5.1.0" + core-js "^3.31.1" detect-port "^1.5.1" escape-html "^1.0.3" eta "^2.2.0" eval "^0.1.8" - file-loader "^6.2.0" + execa "5.1.1" fs-extra "^11.1.1" - html-minifier-terser "^7.2.0" html-tags "^3.3.1" - html-webpack-plugin "^5.5.3" + html-webpack-plugin "^5.6.0" leven "^3.1.0" lodash "^4.17.21" - mini-css-extract-plugin "^2.7.6" + open "^8.4.0" p-map "^4.0.0" - postcss "^8.4.26" - postcss-loader "^7.3.3" prompts "^2.4.2" - react-dev-utils "^12.0.1" - react-helmet-async "^1.3.0" + react-helmet-async "npm:@slorber/react-helmet-async@1.3.0" react-loadable "npm:@docusaurus/react-loadable@6.0.0" react-loadable-ssr-addon-v5-slorber "^1.0.1" react-router "^5.3.4" react-router-config "^5.1.1" react-router-dom "^5.3.4" - rtl-detect "^1.0.4" semver "^7.5.4" - serve-handler "^6.1.5" - shelljs "^0.8.5" - terser-webpack-plugin "^5.3.9" + serve-handler "^6.1.6" + tinypool "^1.0.2" tslib "^2.6.0" update-notifier "^6.0.2" - url-loader "^4.1.1" - webpack "^5.88.1" - webpack-bundle-analyzer "^4.9.0" - webpack-dev-server "^4.15.1" - webpack-merge "^5.9.0" - webpackbar "^5.0.2" + webpack "^5.95.0" + webpack-bundle-analyzer "^4.10.2" + webpack-dev-server "^4.15.2" + webpack-merge "^6.0.1" -"@docusaurus/cssnano-preset@3.4.0": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/cssnano-preset/-/cssnano-preset-3.4.0.tgz#dc7922b3bbeabcefc9b60d0161680d81cf72c368" - integrity sha512-qwLFSz6v/pZHy/UP32IrprmH5ORce86BGtN0eBtG75PpzQJAzp9gefspox+s8IEOr0oZKuQ/nhzZ3xwyc3jYJQ== +"@docusaurus/cssnano-preset@3.8.0": + version "3.8.0" + resolved "https://registry.yarnpkg.com/@docusaurus/cssnano-preset/-/cssnano-preset-3.8.0.tgz#a70f19e2995be2299f5ef9c3da3e5d4d5c14bff2" + integrity sha512-UJ4hAS2T0R4WNy+phwVff2Q0L5+RXW9cwlH6AEphHR5qw3m/yacfWcSK7ort2pMMbDn8uGrD38BTm4oLkuuNoQ== dependencies: cssnano-preset-advanced "^6.1.2" postcss "^8.4.38" postcss-sort-media-queries "^5.2.0" tslib "^2.6.0" -"@docusaurus/logger@3.4.0": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/logger/-/logger-3.4.0.tgz#8b0ac05c7f3dac2009066e2f964dee8209a77403" - integrity sha512-bZwkX+9SJ8lB9kVRkXw+xvHYSMGG4bpYHKGXeXFvyVc79NMeeBSGgzd4TQLHH+DYeOJoCdl8flrFJVxlZ0wo/Q== +"@docusaurus/faster@^3.8.0": + version "3.8.0" + resolved "https://registry.yarnpkg.com/@docusaurus/faster/-/faster-3.8.0.tgz#2814c5ea4f19e10a6cebf9296b6f15f8a621bf61" + integrity sha512-v9+8rT2gw/4zIRBwc4fIVhrTH/yFVDQgJgyYZjqr3fgojOypdQCOwkN6Z8dOwTei4/zo+b/zDPB4x1UvghJZRg== + dependencies: + "@docusaurus/types" "3.8.0" + "@rspack/core" "^1.3.10" + "@swc/core" "^1.7.39" + "@swc/html" "^1.7.39" + browserslist "^4.24.2" + lightningcss "^1.27.0" + swc-loader "^0.2.6" + tslib "^2.6.0" + webpack "^5.95.0" + +"@docusaurus/logger@3.8.0": + version "3.8.0" + resolved "https://registry.yarnpkg.com/@docusaurus/logger/-/logger-3.8.0.tgz#c1abbb084a8058dc0047d57070fb9cd0241a679d" + integrity sha512-7eEMaFIam5Q+v8XwGqF/n0ZoCld4hV4eCCgQkfcN9Mq5inoZa6PHHW9Wu6lmgzoK5Kx3keEeABcO2SxwraoPDQ== dependencies: chalk "^4.1.2" tslib "^2.6.0" -"@docusaurus/mdx-loader@3.4.0": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/mdx-loader/-/mdx-loader-3.4.0.tgz#483d7ab57928fdbb5c8bd1678098721a930fc5f6" - integrity sha512-kSSbrrk4nTjf4d+wtBA9H+FGauf2gCax89kV8SUSJu3qaTdSIKdWERlngsiHaCFgZ7laTJ8a67UFf+xlFPtuTw== +"@docusaurus/mdx-loader@3.8.0": + version "3.8.0" + resolved "https://registry.yarnpkg.com/@docusaurus/mdx-loader/-/mdx-loader-3.8.0.tgz#2b225cd2b1159cc49b10b1cac63a927a8368274b" + integrity sha512-mDPSzssRnpjSdCGuv7z2EIAnPS1MHuZGTaRLwPn4oQwszu4afjWZ/60sfKjTnjBjI8Vl4OgJl2vMmfmiNDX4Ng== dependencies: - "@docusaurus/logger" "3.4.0" - "@docusaurus/utils" "3.4.0" - "@docusaurus/utils-validation" "3.4.0" + "@docusaurus/logger" "3.8.0" + "@docusaurus/utils" "3.8.0" + "@docusaurus/utils-validation" "3.8.0" "@mdx-js/mdx" "^3.0.0" "@slorber/remark-comment" "^1.0.0" escape-html "^1.0.3" estree-util-value-to-estree "^3.0.1" file-loader "^6.2.0" fs-extra "^11.1.1" - image-size "^1.0.2" + image-size "^2.0.2" mdast-util-mdx "^3.0.0" mdast-util-to-string "^4.0.0" rehype-raw "^7.0.0" @@ -1469,176 +2711,206 @@ vfile "^6.0.1" webpack "^5.88.1" -"@docusaurus/module-type-aliases@3.4.0": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/module-type-aliases/-/module-type-aliases-3.4.0.tgz#2653bde58fc1aa3dbc626a6c08cfb63a37ae1bb8" - integrity sha512-A1AyS8WF5Bkjnb8s+guTDuYmUiwJzNrtchebBHpc0gz0PyHJNMaybUlSrmJjHVcGrya0LKI4YcR3lBDQfXRYLw== +"@docusaurus/module-type-aliases@3.8.0", "@docusaurus/module-type-aliases@^3.8.0": + version "3.8.0" + resolved "https://registry.yarnpkg.com/@docusaurus/module-type-aliases/-/module-type-aliases-3.8.0.tgz#e487052c372538c5dcf2200999e13f328fa5ffaa" + integrity sha512-/uMb4Ipt5J/QnD13MpnoC/A4EYAe6DKNWqTWLlGrqsPJwJv73vSwkA25xnYunwfqWk0FlUQfGv/Swdh5eCCg7g== dependencies: - "@docusaurus/types" "3.4.0" + "@docusaurus/types" "3.8.0" "@types/history" "^4.7.11" "@types/react" "*" "@types/react-router-config" "*" "@types/react-router-dom" "*" - react-helmet-async "*" + react-helmet-async "npm:@slorber/react-helmet-async@1.3.0" react-loadable "npm:@docusaurus/react-loadable@6.0.0" -"@docusaurus/plugin-content-blog@3.4.0": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-blog/-/plugin-content-blog-3.4.0.tgz#6373632fdbababbda73a13c4a08f907d7de8f007" - integrity sha512-vv6ZAj78ibR5Jh7XBUT4ndIjmlAxkijM3Sx5MAAzC1gyv0vupDQNhzuFg1USQmQVj3P5I6bquk12etPV3LJ+Xw== +"@docusaurus/plugin-content-blog@3.8.0": + version "3.8.0" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-blog/-/plugin-content-blog-3.8.0.tgz#0c200b1fb821e09e9e975c45255e5ddfab06c392" + integrity sha512-0SlOTd9R55WEr1GgIXu+hhTT0hzARYx3zIScA5IzpdekZQesI/hKEa5LPHBd415fLkWMjdD59TaW/3qQKpJ0Lg== dependencies: - "@docusaurus/core" "3.4.0" - "@docusaurus/logger" "3.4.0" - "@docusaurus/mdx-loader" "3.4.0" - "@docusaurus/types" "3.4.0" - "@docusaurus/utils" "3.4.0" - "@docusaurus/utils-common" "3.4.0" - "@docusaurus/utils-validation" "3.4.0" - cheerio "^1.0.0-rc.12" + "@docusaurus/core" "3.8.0" + "@docusaurus/logger" "3.8.0" + "@docusaurus/mdx-loader" "3.8.0" + "@docusaurus/theme-common" "3.8.0" + "@docusaurus/types" "3.8.0" + "@docusaurus/utils" "3.8.0" + "@docusaurus/utils-common" "3.8.0" + "@docusaurus/utils-validation" "3.8.0" + cheerio "1.0.0-rc.12" feed "^4.2.2" fs-extra "^11.1.1" lodash "^4.17.21" - reading-time "^1.5.0" + schema-dts "^1.1.2" srcset "^4.0.0" tslib "^2.6.0" unist-util-visit "^5.0.0" utility-types "^3.10.0" webpack "^5.88.1" -"@docusaurus/plugin-content-docs@3.4.0", "@docusaurus/plugin-content-docs@^3.0.1": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-docs/-/plugin-content-docs-3.4.0.tgz#3088973f72169a2a6d533afccec7153c8720d332" - integrity sha512-HkUCZffhBo7ocYheD9oZvMcDloRnGhBMOZRyVcAQRFmZPmNqSyISlXA1tQCIxW+r478fty97XXAGjNYzBjpCsg== +"@docusaurus/plugin-content-docs@3.8.0": + version "3.8.0" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-docs/-/plugin-content-docs-3.8.0.tgz#6aedb1261da1f0c8c2fa11cfaa6df4577a9b7826" + integrity sha512-fRDMFLbUN6eVRXcjP8s3Y7HpAt9pzPYh1F/7KKXOCxvJhjjCtbon4VJW0WndEPInVz4t8QUXn5QZkU2tGVCE2g== dependencies: - "@docusaurus/core" "3.4.0" - "@docusaurus/logger" "3.4.0" - "@docusaurus/mdx-loader" "3.4.0" - "@docusaurus/module-type-aliases" "3.4.0" - "@docusaurus/types" "3.4.0" - "@docusaurus/utils" "3.4.0" - "@docusaurus/utils-common" "3.4.0" - "@docusaurus/utils-validation" "3.4.0" + "@docusaurus/core" "3.8.0" + "@docusaurus/logger" "3.8.0" + "@docusaurus/mdx-loader" "3.8.0" + "@docusaurus/module-type-aliases" "3.8.0" + "@docusaurus/theme-common" "3.8.0" + "@docusaurus/types" "3.8.0" + "@docusaurus/utils" "3.8.0" + "@docusaurus/utils-common" "3.8.0" + "@docusaurus/utils-validation" "3.8.0" "@types/react-router-config" "^5.0.7" combine-promises "^1.1.0" fs-extra "^11.1.1" js-yaml "^4.1.0" lodash "^4.17.21" + schema-dts "^1.1.2" tslib "^2.6.0" utility-types "^3.10.0" webpack "^5.88.1" -"@docusaurus/plugin-content-pages@3.4.0": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-pages/-/plugin-content-pages-3.4.0.tgz#1846172ca0355c7d32a67ef8377750ce02bbb8ad" - integrity sha512-h2+VN/0JjpR8fIkDEAoadNjfR3oLzB+v1qSXbIAKjQ46JAHx3X22n9nqS+BWSQnTnp1AjkjSvZyJMekmcwxzxg== +"@docusaurus/plugin-content-pages@3.8.0": + version "3.8.0" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-pages/-/plugin-content-pages-3.8.0.tgz#2db5f990872684c621665d0d0d8d9b5831fd2999" + integrity sha512-39EDx2y1GA0Pxfion5tQZLNJxL4gq6susd1xzetVBjVIQtwpCdyloOfQBAgX0FylqQxfJrYqL0DIUuq7rd7uBw== dependencies: - "@docusaurus/core" "3.4.0" - "@docusaurus/mdx-loader" "3.4.0" - "@docusaurus/types" "3.4.0" - "@docusaurus/utils" "3.4.0" - "@docusaurus/utils-validation" "3.4.0" + "@docusaurus/core" "3.8.0" + "@docusaurus/mdx-loader" "3.8.0" + "@docusaurus/types" "3.8.0" + "@docusaurus/utils" "3.8.0" + "@docusaurus/utils-validation" "3.8.0" fs-extra "^11.1.1" tslib "^2.6.0" webpack "^5.88.1" -"@docusaurus/plugin-debug@3.4.0": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-debug/-/plugin-debug-3.4.0.tgz#74e4ec5686fa314c26f3ac150bacadbba7f06948" - integrity sha512-uV7FDUNXGyDSD3PwUaf5YijX91T5/H9SX4ErEcshzwgzWwBtK37nUWPU3ZLJfeTavX3fycTOqk9TglpOLaWkCg== +"@docusaurus/plugin-css-cascade-layers@3.8.0": + version "3.8.0" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-css-cascade-layers/-/plugin-css-cascade-layers-3.8.0.tgz#a0741ae32917a88ce7ce76b6f472495fa4bf576d" + integrity sha512-/VBTNymPIxQB8oA3ZQ4GFFRYdH4ZxDRRBECxyjRyv486mfUPXfcdk+im4S5mKWa6EK2JzBz95IH/Wu0qQgJ5yQ== dependencies: - "@docusaurus/core" "3.4.0" - "@docusaurus/types" "3.4.0" - "@docusaurus/utils" "3.4.0" + "@docusaurus/core" "3.8.0" + "@docusaurus/types" "3.8.0" + "@docusaurus/utils-validation" "3.8.0" + tslib "^2.6.0" + +"@docusaurus/plugin-debug@3.8.0": + version "3.8.0" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-debug/-/plugin-debug-3.8.0.tgz#297c159ae99924e60042426d2ad6ee0d5e9126b3" + integrity sha512-teonJvJsDB9o2OnG6ifbhblg/PXzZvpUKHFgD8dOL1UJ58u0lk8o0ZOkvaYEBa9nDgqzoWrRk9w+e3qaG2mOhQ== + dependencies: + "@docusaurus/core" "3.8.0" + "@docusaurus/types" "3.8.0" + "@docusaurus/utils" "3.8.0" fs-extra "^11.1.1" - react-json-view-lite "^1.2.0" + react-json-view-lite "^2.3.0" tslib "^2.6.0" -"@docusaurus/plugin-google-analytics@3.4.0": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-google-analytics/-/plugin-google-analytics-3.4.0.tgz#5f59fc25329a59decc231936f6f9fb5663da3c55" - integrity sha512-mCArluxEGi3cmYHqsgpGGt3IyLCrFBxPsxNZ56Mpur0xSlInnIHoeLDH7FvVVcPJRPSQ9/MfRqLsainRw+BojA== +"@docusaurus/plugin-google-analytics@3.8.0": + version "3.8.0" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-google-analytics/-/plugin-google-analytics-3.8.0.tgz#fb97097af331beb13553a384081dc83607539b31" + integrity sha512-aKKa7Q8+3xRSRESipNvlFgNp3FNPELKhuo48Cg/svQbGNwidSHbZT03JqbW4cBaQnyyVchO1ttk+kJ5VC9Gx0w== dependencies: - "@docusaurus/core" "3.4.0" - "@docusaurus/types" "3.4.0" - "@docusaurus/utils-validation" "3.4.0" + "@docusaurus/core" "3.8.0" + "@docusaurus/types" "3.8.0" + "@docusaurus/utils-validation" "3.8.0" tslib "^2.6.0" -"@docusaurus/plugin-google-gtag@3.4.0": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-google-gtag/-/plugin-google-gtag-3.4.0.tgz#42489ac5fe1c83b5523ceedd5ef74f9aa8bc251b" - integrity sha512-Dsgg6PLAqzZw5wZ4QjUYc8Z2KqJqXxHxq3vIoyoBWiLEEfigIs7wHR+oiWUQy3Zk9MIk6JTYj7tMoQU0Jm3nqA== +"@docusaurus/plugin-google-gtag@3.8.0": + version "3.8.0" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-google-gtag/-/plugin-google-gtag-3.8.0.tgz#b5a60006c28ac582859a469fb92e53d383b0a055" + integrity sha512-ugQYMGF4BjbAW/JIBtVcp+9eZEgT9HRdvdcDudl5rywNPBA0lct+lXMG3r17s02rrhInMpjMahN3Yc9Cb3H5/g== dependencies: - "@docusaurus/core" "3.4.0" - "@docusaurus/types" "3.4.0" - "@docusaurus/utils-validation" "3.4.0" + "@docusaurus/core" "3.8.0" + "@docusaurus/types" "3.8.0" + "@docusaurus/utils-validation" "3.8.0" "@types/gtag.js" "^0.0.12" tslib "^2.6.0" -"@docusaurus/plugin-google-tag-manager@3.4.0": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-google-tag-manager/-/plugin-google-tag-manager-3.4.0.tgz#cebb03a5ffa1e70b37d95601442babea251329ff" - integrity sha512-O9tX1BTwxIhgXpOLpFDueYA9DWk69WCbDRrjYoMQtFHSkTyE7RhNgyjSPREUWJb9i+YUg3OrsvrBYRl64FCPCQ== +"@docusaurus/plugin-google-tag-manager@3.8.0": + version "3.8.0" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-google-tag-manager/-/plugin-google-tag-manager-3.8.0.tgz#612aa63e161fb273bf7db2591034c0142951727d" + integrity sha512-9juRWxbwZD3SV02Jd9QB6yeN7eu+7T4zB0bvJLcVQwi+am51wAxn2CwbdL0YCCX+9OfiXbADE8D8Q65Hbopu/w== dependencies: - "@docusaurus/core" "3.4.0" - "@docusaurus/types" "3.4.0" - "@docusaurus/utils-validation" "3.4.0" + "@docusaurus/core" "3.8.0" + "@docusaurus/types" "3.8.0" + "@docusaurus/utils-validation" "3.8.0" tslib "^2.6.0" -"@docusaurus/plugin-sitemap@3.4.0": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-sitemap/-/plugin-sitemap-3.4.0.tgz#b091d64d1e3c6c872050189999580187537bcbc6" - integrity sha512-+0VDvx9SmNrFNgwPoeoCha+tRoAjopwT0+pYO1xAbyLcewXSemq+eLxEa46Q1/aoOaJQ0qqHELuQM7iS2gp33Q== +"@docusaurus/plugin-sitemap@3.8.0": + version "3.8.0" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-sitemap/-/plugin-sitemap-3.8.0.tgz#a39e3b5aa2f059aba0052ed11a6b4fbf78ac0dad" + integrity sha512-fGpOIyJvNiuAb90nSJ2Gfy/hUOaDu6826e5w5UxPmbpCIc7KlBHNAZ5g4L4ZuHhc4hdfq4mzVBsQSnne+8Ze1g== dependencies: - "@docusaurus/core" "3.4.0" - "@docusaurus/logger" "3.4.0" - "@docusaurus/types" "3.4.0" - "@docusaurus/utils" "3.4.0" - "@docusaurus/utils-common" "3.4.0" - "@docusaurus/utils-validation" "3.4.0" + "@docusaurus/core" "3.8.0" + "@docusaurus/logger" "3.8.0" + "@docusaurus/types" "3.8.0" + "@docusaurus/utils" "3.8.0" + "@docusaurus/utils-common" "3.8.0" + "@docusaurus/utils-validation" "3.8.0" fs-extra "^11.1.1" sitemap "^7.1.1" tslib "^2.6.0" -"@docusaurus/preset-classic@3.4.0": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/preset-classic/-/preset-classic-3.4.0.tgz#6082a32fbb465b0cb2c2a50ebfc277cff2c0f139" - integrity sha512-Ohj6KB7siKqZaQhNJVMBBUzT3Nnp6eTKqO+FXO3qu/n1hJl3YLwVKTWBg28LF7MWrKu46UuYavwMRxud0VyqHg== +"@docusaurus/plugin-svgr@3.8.0": + version "3.8.0" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-svgr/-/plugin-svgr-3.8.0.tgz#6d2d43f14b32b4bb2dd8dc87a70c6e78754c1e85" + integrity sha512-kEDyry+4OMz6BWLG/lEqrNsL/w818bywK70N1gytViw4m9iAmoxCUT7Ri9Dgs7xUdzCHJ3OujolEmD88Wy44OA== dependencies: - "@docusaurus/core" "3.4.0" - "@docusaurus/plugin-content-blog" "3.4.0" - "@docusaurus/plugin-content-docs" "3.4.0" - "@docusaurus/plugin-content-pages" "3.4.0" - "@docusaurus/plugin-debug" "3.4.0" - "@docusaurus/plugin-google-analytics" "3.4.0" - "@docusaurus/plugin-google-gtag" "3.4.0" - "@docusaurus/plugin-google-tag-manager" "3.4.0" - "@docusaurus/plugin-sitemap" "3.4.0" - "@docusaurus/theme-classic" "3.4.0" - "@docusaurus/theme-common" "3.4.0" - "@docusaurus/theme-search-algolia" "3.4.0" - "@docusaurus/types" "3.4.0" + "@docusaurus/core" "3.8.0" + "@docusaurus/types" "3.8.0" + "@docusaurus/utils" "3.8.0" + "@docusaurus/utils-validation" "3.8.0" + "@svgr/core" "8.1.0" + "@svgr/webpack" "^8.1.0" + tslib "^2.6.0" + webpack "^5.88.1" -"@docusaurus/theme-classic@3.4.0": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/theme-classic/-/theme-classic-3.4.0.tgz#1b0f48edec3e3ec8927843554b9f11e5927b0e52" - integrity sha512-0IPtmxsBYv2adr1GnZRdMkEQt1YW6tpzrUPj02YxNpvJ5+ju4E13J5tB4nfdaen/tfR1hmpSPlTFPvTf4kwy8Q== +"@docusaurus/preset-classic@^3.8.0": + version "3.8.0" + resolved "https://registry.yarnpkg.com/@docusaurus/preset-classic/-/preset-classic-3.8.0.tgz#ac8bc17e3b7b443d8a24f2f1da0c0be396950fef" + integrity sha512-qOu6tQDOWv+rpTlKu+eJATCJVGnABpRCPuqf7LbEaQ1mNY//N/P8cHQwkpAU+aweQfarcZ0XfwCqRHJfjeSV/g== dependencies: - "@docusaurus/core" "3.4.0" - "@docusaurus/mdx-loader" "3.4.0" - "@docusaurus/module-type-aliases" "3.4.0" - "@docusaurus/plugin-content-blog" "3.4.0" - "@docusaurus/plugin-content-docs" "3.4.0" - "@docusaurus/plugin-content-pages" "3.4.0" - "@docusaurus/theme-common" "3.4.0" - "@docusaurus/theme-translations" "3.4.0" - "@docusaurus/types" "3.4.0" - "@docusaurus/utils" "3.4.0" - "@docusaurus/utils-common" "3.4.0" - "@docusaurus/utils-validation" "3.4.0" + "@docusaurus/core" "3.8.0" + "@docusaurus/plugin-content-blog" "3.8.0" + "@docusaurus/plugin-content-docs" "3.8.0" + "@docusaurus/plugin-content-pages" "3.8.0" + "@docusaurus/plugin-css-cascade-layers" "3.8.0" + "@docusaurus/plugin-debug" "3.8.0" + "@docusaurus/plugin-google-analytics" "3.8.0" + "@docusaurus/plugin-google-gtag" "3.8.0" + "@docusaurus/plugin-google-tag-manager" "3.8.0" + "@docusaurus/plugin-sitemap" "3.8.0" + "@docusaurus/plugin-svgr" "3.8.0" + "@docusaurus/theme-classic" "3.8.0" + "@docusaurus/theme-common" "3.8.0" + "@docusaurus/theme-search-algolia" "3.8.0" + "@docusaurus/types" "3.8.0" + +"@docusaurus/theme-classic@3.8.0": + version "3.8.0" + resolved "https://registry.yarnpkg.com/@docusaurus/theme-classic/-/theme-classic-3.8.0.tgz#6d44fb801b86a7c7af01cda0325af1a3300b3ac2" + integrity sha512-nQWFiD5ZjoT76OaELt2n33P3WVuuCz8Dt5KFRP2fCBo2r9JCLsp2GJjZpnaG24LZ5/arRjv4VqWKgpK0/YLt7g== + dependencies: + "@docusaurus/core" "3.8.0" + "@docusaurus/logger" "3.8.0" + "@docusaurus/mdx-loader" "3.8.0" + "@docusaurus/module-type-aliases" "3.8.0" + "@docusaurus/plugin-content-blog" "3.8.0" + "@docusaurus/plugin-content-docs" "3.8.0" + "@docusaurus/plugin-content-pages" "3.8.0" + "@docusaurus/theme-common" "3.8.0" + "@docusaurus/theme-translations" "3.8.0" + "@docusaurus/types" "3.8.0" + "@docusaurus/utils" "3.8.0" + "@docusaurus/utils-common" "3.8.0" + "@docusaurus/utils-validation" "3.8.0" "@mdx-js/react" "^3.0.0" clsx "^2.0.0" copy-text-to-clipboard "^3.2.0" - infima "0.2.0-alpha.43" + infima "0.2.0-alpha.45" lodash "^4.17.21" nprogress "^0.2.0" postcss "^8.4.26" @@ -1649,18 +2921,15 @@ tslib "^2.6.0" utility-types "^3.10.0" -"@docusaurus/theme-common@3.4.0", "@docusaurus/theme-common@^3.0.1": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/theme-common/-/theme-common-3.4.0.tgz#01f2b728de6cb57f6443f52fc30675cf12a5d49f" - integrity sha512-0A27alXuv7ZdCg28oPE8nH/Iz73/IUejVaCazqu9elS4ypjiLhK3KfzdSQBnL/g7YfHSlymZKdiOHEo8fJ0qMA== +"@docusaurus/theme-common@3.8.0": + version "3.8.0" + resolved "https://registry.yarnpkg.com/@docusaurus/theme-common/-/theme-common-3.8.0.tgz#102c385c3d1d3b7a6b52d1911c7e88c38d9a977e" + integrity sha512-YqV2vAWpXGLA+A3PMLrOMtqgTHJLDcT+1Caa6RF7N4/IWgrevy5diY8oIHFkXR/eybjcrFFjUPrHif8gSGs3Tw== dependencies: - "@docusaurus/mdx-loader" "3.4.0" - "@docusaurus/module-type-aliases" "3.4.0" - "@docusaurus/plugin-content-blog" "3.4.0" - "@docusaurus/plugin-content-docs" "3.4.0" - "@docusaurus/plugin-content-pages" "3.4.0" - "@docusaurus/utils" "3.4.0" - "@docusaurus/utils-common" "3.4.0" + "@docusaurus/mdx-loader" "3.8.0" + "@docusaurus/module-type-aliases" "3.8.0" + "@docusaurus/utils" "3.8.0" + "@docusaurus/utils-common" "3.8.0" "@types/history" "^4.7.11" "@types/react" "*" "@types/react-router-config" "*" @@ -1670,34 +2939,34 @@ tslib "^2.6.0" utility-types "^3.10.0" -"@docusaurus/theme-mermaid@3.4.0": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/theme-mermaid/-/theme-mermaid-3.4.0.tgz#ef1d2231d0858767f67538b4fafd7d0ce2a3e845" - integrity sha512-3w5QW0HEZ2O6x2w6lU3ZvOe1gNXP2HIoKDMJBil1VmLBc9PmpAG17VmfhI/p3L2etNmOiVs5GgniUqvn8AFEGQ== +"@docusaurus/theme-mermaid@^3.8.0": + version "3.8.0" + resolved "https://registry.yarnpkg.com/@docusaurus/theme-mermaid/-/theme-mermaid-3.8.0.tgz#f0720ec89fd386870f30978ba984b1b126ca92a5" + integrity sha512-ou0NJM37p4xrVuFaZp8qFe5Z/qBq9LuyRTP4KKRa0u2J3zC4f3saBJDgc56FyvvN1OsmU0189KGEPUjTr6hFxg== dependencies: - "@docusaurus/core" "3.4.0" - "@docusaurus/module-type-aliases" "3.4.0" - "@docusaurus/theme-common" "3.4.0" - "@docusaurus/types" "3.4.0" - "@docusaurus/utils-validation" "3.4.0" - mermaid "^10.4.0" + "@docusaurus/core" "3.8.0" + "@docusaurus/module-type-aliases" "3.8.0" + "@docusaurus/theme-common" "3.8.0" + "@docusaurus/types" "3.8.0" + "@docusaurus/utils-validation" "3.8.0" + mermaid ">=11.6.0" tslib "^2.6.0" -"@docusaurus/theme-search-algolia@3.4.0": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/theme-search-algolia/-/theme-search-algolia-3.4.0.tgz#c499bad71d668df0d0f15b0e5e33e2fc4e330fcc" - integrity sha512-aiHFx7OCw4Wck1z6IoShVdUWIjntC8FHCw9c5dR8r3q4Ynh+zkS8y2eFFunN/DL6RXPzpnvKCg3vhLQYJDmT9Q== +"@docusaurus/theme-search-algolia@3.8.0", "@docusaurus/theme-search-algolia@^3.8.0": + version "3.8.0" + resolved "https://registry.yarnpkg.com/@docusaurus/theme-search-algolia/-/theme-search-algolia-3.8.0.tgz#21c2f18e07a73d13ca3b44fcf0ae9aac33bef60f" + integrity sha512-GBZ5UOcPgiu6nUw153+0+PNWvFKweSnvKIL6Rp04H9olKb475jfKjAwCCtju5D2xs5qXHvCMvzWOg5o9f6DtuQ== dependencies: - "@docsearch/react" "^3.5.2" - "@docusaurus/core" "3.4.0" - "@docusaurus/logger" "3.4.0" - "@docusaurus/plugin-content-docs" "3.4.0" - "@docusaurus/theme-common" "3.4.0" - "@docusaurus/theme-translations" "3.4.0" - "@docusaurus/utils" "3.4.0" - "@docusaurus/utils-validation" "3.4.0" - algoliasearch "^4.18.0" - algoliasearch-helper "^3.13.3" + "@docsearch/react" "^3.9.0" + "@docusaurus/core" "3.8.0" + "@docusaurus/logger" "3.8.0" + "@docusaurus/plugin-content-docs" "3.8.0" + "@docusaurus/theme-common" "3.8.0" + "@docusaurus/theme-translations" "3.8.0" + "@docusaurus/utils" "3.8.0" + "@docusaurus/utils-validation" "3.8.0" + algoliasearch "^5.17.1" + algoliasearch-helper "^3.22.6" clsx "^2.0.0" eta "^2.2.0" fs-extra "^11.1.1" @@ -1705,15 +2974,30 @@ tslib "^2.6.0" utility-types "^3.10.0" -"@docusaurus/theme-translations@3.4.0": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/theme-translations/-/theme-translations-3.4.0.tgz#e6355d01352886c67e38e848b2542582ea3070af" - integrity sha512-zSxCSpmQCCdQU5Q4CnX/ID8CSUUI3fvmq4hU/GNP/XoAWtXo9SAVnM3TzpU8Gb//H3WCsT8mJcTfyOk3d9ftNg== +"@docusaurus/theme-translations@3.8.0": + version "3.8.0" + resolved "https://registry.yarnpkg.com/@docusaurus/theme-translations/-/theme-translations-3.8.0.tgz#deb64dccab74361624c3cb352a4949a7ac868c74" + integrity sha512-1DTy/snHicgkCkryWq54fZvsAglTdjTx4qjOXgqnXJ+DIty1B+aPQrAVUu8LiM+6BiILfmNxYsxhKTj+BS3PZg== dependencies: fs-extra "^11.1.1" tslib "^2.6.0" -"@docusaurus/types@3.4.0", "@docusaurus/types@^3.0.0": +"@docusaurus/types@3.8.0", "@docusaurus/types@^3.8.0": + version "3.8.0" + resolved "https://registry.yarnpkg.com/@docusaurus/types/-/types-3.8.0.tgz#f6cd31c4e3e392e0270b8137d7fe4365ea7a022e" + integrity sha512-RDEClpwNxZq02c+JlaKLWoS13qwWhjcNsi2wG1UpzmEnuti/z1Wx4SGpqbUqRPNSd8QWWePR8Cb7DvG0VN/TtA== + dependencies: + "@mdx-js/mdx" "^3.0.0" + "@types/history" "^4.7.11" + "@types/react" "*" + commander "^5.1.0" + joi "^17.9.2" + react-helmet-async "npm:@slorber/react-helmet-async@1.3.0" + utility-types "^3.10.0" + webpack "^5.95.0" + webpack-merge "^5.9.0" + +"@docusaurus/types@^3.0.0": version "3.4.0" resolved "https://registry.yarnpkg.com/@docusaurus/types/-/types-3.4.0.tgz#237c3f737e9db3f7c1a5935a3ef48d6eadde8292" integrity sha512-4jcDO8kXi5Cf9TcyikB/yKmz14f2RZ2qTRerbHAsS+5InE9ZgSLBNLsewtFTcTOXSVcbU3FoGOzcNWAmU1TR0A== @@ -1728,36 +3012,38 @@ webpack "^5.88.1" webpack-merge "^5.9.0" -"@docusaurus/utils-common@3.4.0": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/utils-common/-/utils-common-3.4.0.tgz#2a43fefd35b85ab9fcc6833187e66c15f8bfbbc6" - integrity sha512-NVx54Wr4rCEKsjOH5QEVvxIqVvm+9kh7q8aYTU5WzUU9/Hctd6aTrcZ3G0Id4zYJ+AeaG5K5qHA4CY5Kcm2iyQ== +"@docusaurus/utils-common@3.8.0": + version "3.8.0" + resolved "https://registry.yarnpkg.com/@docusaurus/utils-common/-/utils-common-3.8.0.tgz#2b1a6b1ec4a7fac62f1898d523d42f8cc4a8258f" + integrity sha512-3TGF+wVTGgQ3pAc9+5jVchES4uXUAhAt9pwv7uws4mVOxL4alvU3ue/EZ+R4XuGk94pDy7CNXjRXpPjlfZXQfw== dependencies: + "@docusaurus/types" "3.8.0" tslib "^2.6.0" -"@docusaurus/utils-validation@3.4.0", "@docusaurus/utils-validation@^3.0.1": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/utils-validation/-/utils-validation-3.4.0.tgz#0176f6e503ff45f4390ec2ecb69550f55e0b5eb7" - integrity sha512-hYQ9fM+AXYVTWxJOT1EuNaRnrR2WGpRdLDQG07O8UOpsvCPWUVOeo26Rbm0JWY2sGLfzAb+tvJ62yF+8F+TV0g== +"@docusaurus/utils-validation@3.8.0": + version "3.8.0" + resolved "https://registry.yarnpkg.com/@docusaurus/utils-validation/-/utils-validation-3.8.0.tgz#aa02e9d998e20998fbcaacd94873878bc3b9a4cd" + integrity sha512-MrnEbkigr54HkdFeg8e4FKc4EF+E9dlVwsY3XQZsNkbv3MKZnbHQ5LsNJDIKDROFe8PBf5C4qCAg5TPBpsjrjg== dependencies: - "@docusaurus/logger" "3.4.0" - "@docusaurus/utils" "3.4.0" - "@docusaurus/utils-common" "3.4.0" + "@docusaurus/logger" "3.8.0" + "@docusaurus/utils" "3.8.0" + "@docusaurus/utils-common" "3.8.0" fs-extra "^11.2.0" joi "^17.9.2" js-yaml "^4.1.0" lodash "^4.17.21" tslib "^2.6.0" -"@docusaurus/utils@3.4.0", "@docusaurus/utils@^3.0.1": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/utils/-/utils-3.4.0.tgz#c508e20627b7a55e2b541e4a28c95e0637d6a204" - integrity sha512-fRwnu3L3nnWaXOgs88BVBmG1yGjcQqZNHG+vInhEa2Sz2oQB+ZjbEMO5Rh9ePFpZ0YDiDUhpaVjwmS+AU2F14g== +"@docusaurus/utils@3.8.0": + version "3.8.0" + resolved "https://registry.yarnpkg.com/@docusaurus/utils/-/utils-3.8.0.tgz#92bad89d2a11f5f246196af153093b12cd79f9ac" + integrity sha512-2wvtG28ALCN/A1WCSLxPASFBFzXCnP0YKCAFIPcvEb6imNu1wg7ni/Svcp71b3Z2FaOFFIv4Hq+j4gD7gA0yfQ== dependencies: - "@docusaurus/logger" "3.4.0" - "@docusaurus/utils-common" "3.4.0" - "@svgr/webpack" "^8.1.0" + "@docusaurus/logger" "3.8.0" + "@docusaurus/types" "3.8.0" + "@docusaurus/utils-common" "3.8.0" escape-string-regexp "^4.0.0" + execa "5.1.1" file-loader "^6.2.0" fs-extra "^11.1.1" github-slugger "^1.5.0" @@ -1767,9 +3053,9 @@ js-yaml "^4.1.0" lodash "^4.17.21" micromatch "^4.0.5" + p-queue "^6.6.2" prompts "^2.4.2" resolve-pathname "^3.0.0" - shelljs "^0.8.5" tslib "^2.6.0" url-loader "^4.1.1" utility-types "^3.10.0" @@ -1815,6 +3101,25 @@ resolved "https://registry.yarnpkg.com/@hookform/error-message/-/error-message-2.0.1.tgz#6a37419106e13664ad6a29c9dae699ae6cd276b8" integrity sha512-U410sAr92xgxT1idlu9WWOVjndxLdgPUHEB8Schr27C9eh7/xUnITWpCMF93s+lGiG++D4JnbSnrb5A21AdSNg== +"@iconify/types@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@iconify/types/-/types-2.0.0.tgz#ab0e9ea681d6c8a1214f30cd741fe3a20cc57f57" + integrity sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg== + +"@iconify/utils@^2.1.33": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@iconify/utils/-/utils-2.3.0.tgz#1bbbf8c477ebe9a7cacaea78b1b7e8937f9cbfba" + integrity sha512-GmQ78prtwYW6EtzXRU1rY+KwOKfz32PD7iJh6Iyqw68GiKuoZ2A6pRtzWONz5VQJbp50mEjXh/7NkumtrAgRKA== + dependencies: + "@antfu/install-pkg" "^1.0.0" + "@antfu/utils" "^8.1.0" + "@iconify/types" "^2.0.0" + debug "^4.4.0" + globals "^15.14.0" + kolorist "^1.8.0" + local-pkg "^1.0.0" + mlly "^1.7.4" + "@isaacs/cliui@^8.0.2": version "8.0.2" resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550" @@ -1932,6 +3237,56 @@ dependencies: "@types/mdx" "^2.0.0" +"@mermaid-js/parser@^0.4.0": + version "0.4.0" + resolved "https://registry.yarnpkg.com/@mermaid-js/parser/-/parser-0.4.0.tgz#c1de1f5669f8fcbd0d0c9d124927d36ddc00d8a6" + integrity sha512-wla8XOWvQAwuqy+gxiZqY+c7FokraOTHRWMsbB4AgRx9Sy7zKslNyejy7E+a77qHfey5GXw/ik3IXv/NHMJgaA== + dependencies: + langium "3.3.1" + +"@module-federation/error-codes@0.14.0": + version "0.14.0" + resolved "https://registry.yarnpkg.com/@module-federation/error-codes/-/error-codes-0.14.0.tgz#d54581bfb998ce9ace4cb33f8795c644e461bfeb" + integrity sha512-GGk+EoeSACJikZZyShnLshtq9E2eCrDWbRiB4QAFXCX4oYmGgFfzXlx59vMNwqTKPJWxkEGnPYacJMcr2YYjag== + +"@module-federation/runtime-core@0.14.0": + version "0.14.0" + resolved "https://registry.yarnpkg.com/@module-federation/runtime-core/-/runtime-core-0.14.0.tgz#a00a3666cc25a8bb822a36552c631e6e3f7326cc" + integrity sha512-fGE1Ro55zIFDp/CxQuRhKQ1pJvG7P0qvRm2N+4i8z++2bgDjcxnCKUqDJ8lLD+JfJQvUJf0tuSsJPgevzueD4g== + dependencies: + "@module-federation/error-codes" "0.14.0" + "@module-federation/sdk" "0.14.0" + +"@module-federation/runtime-tools@0.14.0": + version "0.14.0" + resolved "https://registry.yarnpkg.com/@module-federation/runtime-tools/-/runtime-tools-0.14.0.tgz#f5b5f3d19605b6d7c90ed278dc00a5400f1aa49d" + integrity sha512-y/YN0c2DKsLETE+4EEbmYWjqF9G6ZwgZoDIPkaQ9p0pQu0V4YxzWfQagFFxR0RigYGuhJKmSU/rtNoHq+qF8jg== + dependencies: + "@module-federation/runtime" "0.14.0" + "@module-federation/webpack-bundler-runtime" "0.14.0" + +"@module-federation/runtime@0.14.0": + version "0.14.0" + resolved "https://registry.yarnpkg.com/@module-federation/runtime/-/runtime-0.14.0.tgz#e04012d2d928275fd00525904c0b4f8be514dc70" + integrity sha512-kR3cyHw/Y64SEa7mh4CHXOEQYY32LKLK75kJOmBroLNLO7/W01hMNAvGBYTedS7hWpVuefPk1aFZioy3q2VLdQ== + dependencies: + "@module-federation/error-codes" "0.14.0" + "@module-federation/runtime-core" "0.14.0" + "@module-federation/sdk" "0.14.0" + +"@module-federation/sdk@0.14.0": + version "0.14.0" + resolved "https://registry.yarnpkg.com/@module-federation/sdk/-/sdk-0.14.0.tgz#efa38341b7601f58967397cc630068f69691b931" + integrity sha512-lg/OWRsh18hsyTCamOOhEX546vbDiA2O4OggTxxH2wTGr156N6DdELGQlYIKfRdU/0StgtQS81Goc0BgDZlx9A== + +"@module-federation/webpack-bundler-runtime@0.14.0": + version "0.14.0" + resolved "https://registry.yarnpkg.com/@module-federation/webpack-bundler-runtime/-/webpack-bundler-runtime-0.14.0.tgz#21a82505f95fdb3cb202786f8dc20611b4d7f93c" + integrity sha512-POWS6cKBicAAQ3DNY5X7XEUSfOfUsRaBNxbuwEfSGlrkTE9UcWheO06QP2ndHi8tHQuUKcIHi2navhPkJ+k5xg== + dependencies: + "@module-federation/runtime" "0.14.0" + "@module-federation/sdk" "0.14.0" + "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" @@ -1953,6 +3308,95 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" +"@parcel/watcher-android-arm64@2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz#507f836d7e2042f798c7d07ad19c3546f9848ac1" + integrity sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA== + +"@parcel/watcher-darwin-arm64@2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz#3d26dce38de6590ef79c47ec2c55793c06ad4f67" + integrity sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw== + +"@parcel/watcher-darwin-x64@2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz#99f3af3869069ccf774e4ddfccf7e64fd2311ef8" + integrity sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg== + +"@parcel/watcher-freebsd-x64@2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz#14d6857741a9f51dfe51d5b08b7c8afdbc73ad9b" + integrity sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ== + +"@parcel/watcher-linux-arm-glibc@2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz#43c3246d6892381db473bb4f663229ad20b609a1" + integrity sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA== + +"@parcel/watcher-linux-arm-musl@2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz#663750f7090bb6278d2210de643eb8a3f780d08e" + integrity sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q== + +"@parcel/watcher-linux-arm64-glibc@2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz#ba60e1f56977f7e47cd7e31ad65d15fdcbd07e30" + integrity sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w== + +"@parcel/watcher-linux-arm64-musl@2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz#f7fbcdff2f04c526f96eac01f97419a6a99855d2" + integrity sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg== + +"@parcel/watcher-linux-x64-glibc@2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz#4d2ea0f633eb1917d83d483392ce6181b6a92e4e" + integrity sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A== + +"@parcel/watcher-linux-x64-musl@2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz#277b346b05db54f55657301dd77bdf99d63606ee" + integrity sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg== + +"@parcel/watcher-win32-arm64@2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz#7e9e02a26784d47503de1d10e8eab6cceb524243" + integrity sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw== + +"@parcel/watcher-win32-ia32@2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz#2d0f94fa59a873cdc584bf7f6b1dc628ddf976e6" + integrity sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ== + +"@parcel/watcher-win32-x64@2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz#ae52693259664ba6f2228fa61d7ee44b64ea0947" + integrity sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA== + +"@parcel/watcher@^2.4.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher/-/watcher-2.5.1.tgz#342507a9cfaaf172479a882309def1e991fb1200" + integrity sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg== + dependencies: + detect-libc "^1.0.3" + is-glob "^4.0.3" + micromatch "^4.0.5" + node-addon-api "^7.0.0" + optionalDependencies: + "@parcel/watcher-android-arm64" "2.5.1" + "@parcel/watcher-darwin-arm64" "2.5.1" + "@parcel/watcher-darwin-x64" "2.5.1" + "@parcel/watcher-freebsd-x64" "2.5.1" + "@parcel/watcher-linux-arm-glibc" "2.5.1" + "@parcel/watcher-linux-arm-musl" "2.5.1" + "@parcel/watcher-linux-arm64-glibc" "2.5.1" + "@parcel/watcher-linux-arm64-musl" "2.5.1" + "@parcel/watcher-linux-x64-glibc" "2.5.1" + "@parcel/watcher-linux-x64-musl" "2.5.1" + "@parcel/watcher-win32-arm64" "2.5.1" + "@parcel/watcher-win32-ia32" "2.5.1" + "@parcel/watcher-win32-x64" "2.5.1" + "@pkgjs/parseargs@^0.11.0": version "0.11.0" resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" @@ -2025,6 +3469,81 @@ redux-thunk "^2.4.2" reselect "^4.1.8" +"@rspack/binding-darwin-arm64@1.3.12": + version "1.3.12" + resolved "https://registry.yarnpkg.com/@rspack/binding-darwin-arm64/-/binding-darwin-arm64-1.3.12.tgz#2e7cc00b813dcb155572908d956ab1d75d9747a5" + integrity sha512-8hKjVTBeWPqkMzFPNWIh72oU9O3vFy3e88wRjMPImDCXBiEYrKqGTTLd/J0SO+efdL3SBD1rX1IvdJpxCv6Yrw== + +"@rspack/binding-darwin-x64@1.3.12": + version "1.3.12" + resolved "https://registry.yarnpkg.com/@rspack/binding-darwin-x64/-/binding-darwin-x64-1.3.12.tgz#cb148cc62658d74204621a695e1698cda82877f9" + integrity sha512-Sj4m+mCUxL7oCpdu7OmWT7fpBM7hywk5CM9RDc3D7StaBZbvNtNftafCrTZzTYKuZrKmemTh5SFzT5Tz7tf6GA== + +"@rspack/binding-linux-arm64-gnu@1.3.12": + version "1.3.12" + resolved "https://registry.yarnpkg.com/@rspack/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.3.12.tgz#79820857dfbd3819e0026da507c271531c013d0c" + integrity sha512-7MuOxf3/Mhv4mgFdLTvgnt/J+VouNR65DEhorth+RZm3LEWojgoFEphSAMAvpvAOpYSS68Sw4SqsOZi719ia2w== + +"@rspack/binding-linux-arm64-musl@1.3.12": + version "1.3.12" + resolved "https://registry.yarnpkg.com/@rspack/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.3.12.tgz#a794dfe8df6bf75af217597881592add6f6b046e" + integrity sha512-s6KKj20T9Z1bA8caIjU6EzJbwyDo1URNFgBAlafCT2UC6yX7flstDJJ38CxZacA9A2P24RuQK2/jPSZpWrTUFA== + +"@rspack/binding-linux-x64-gnu@1.3.12": + version "1.3.12" + resolved "https://registry.yarnpkg.com/@rspack/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.3.12.tgz#a0e23831a1374d9039a4b29e927cef58a485085c" + integrity sha512-0w/sRREYbRgHgWvs2uMEJSLfvzbZkPHUg6CMcYQGNVK6axYRot6jPyKetyFYA9pR5fB5rsXegpnFaZaVrRIK2g== + +"@rspack/binding-linux-x64-musl@1.3.12": + version "1.3.12" + resolved "https://registry.yarnpkg.com/@rspack/binding-linux-x64-musl/-/binding-linux-x64-musl-1.3.12.tgz#61620efda6a6805689890c130e6b38c1725baaf4" + integrity sha512-jEdxkPymkRxbijDRsBGdhopcbGXiXDg59lXqIRkVklqbDmZ/O6DHm7gImmlx5q9FoWbz0gqJuOKBz4JqWxjWVA== + +"@rspack/binding-win32-arm64-msvc@1.3.12": + version "1.3.12" + resolved "https://registry.yarnpkg.com/@rspack/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.3.12.tgz#46d874df8bd5b84e82ea83480969f7e0293ccd82" + integrity sha512-ZRvUCb3TDLClAqcTsl/o9UdJf0B5CgzAxgdbnYJbldyuyMeTUB4jp20OfG55M3C2Nute2SNhu2bOOp9Se5Ongw== + +"@rspack/binding-win32-ia32-msvc@1.3.12": + version "1.3.12" + resolved "https://registry.yarnpkg.com/@rspack/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.3.12.tgz#fec435e31e56f3d58b4fa52746643d1d593b2b89" + integrity sha512-1TKPjuXStPJr14f3ZHuv40Xc/87jUXx10pzVtrPnw+f3hckECHrbYU/fvbVzZyuXbsXtkXpYca6ygCDRJAoNeQ== + +"@rspack/binding-win32-x64-msvc@1.3.12": + version "1.3.12" + resolved "https://registry.yarnpkg.com/@rspack/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.3.12.tgz#33b73cbab75920cf8a92e7245a794970f8508b6c" + integrity sha512-lCR0JfnYKpV+a6r2A2FdxyUKUS4tajePgpPJN5uXDgMGwrDtRqvx+d0BHhwjFudQVJq9VVbRaL89s2MQ6u+xYw== + +"@rspack/binding@1.3.12": + version "1.3.12" + resolved "https://registry.yarnpkg.com/@rspack/binding/-/binding-1.3.12.tgz#0a8356fdbd89f08cda3e9bb8aff4ea781dfe972e" + integrity sha512-4Ic8lV0+LCBfTlH5aIOujIRWZOtgmG223zC4L3o8WY/+ESAgpdnK6lSSMfcYgRanYLAy3HOmFIp20jwskMpbAg== + optionalDependencies: + "@rspack/binding-darwin-arm64" "1.3.12" + "@rspack/binding-darwin-x64" "1.3.12" + "@rspack/binding-linux-arm64-gnu" "1.3.12" + "@rspack/binding-linux-arm64-musl" "1.3.12" + "@rspack/binding-linux-x64-gnu" "1.3.12" + "@rspack/binding-linux-x64-musl" "1.3.12" + "@rspack/binding-win32-arm64-msvc" "1.3.12" + "@rspack/binding-win32-ia32-msvc" "1.3.12" + "@rspack/binding-win32-x64-msvc" "1.3.12" + +"@rspack/core@^1.3.10": + version "1.3.12" + resolved "https://registry.yarnpkg.com/@rspack/core/-/core-1.3.12.tgz#68df0111cfac7e8f9dfa11a608ac8731181b5483" + integrity sha512-mAPmV4LPPRgxpouUrGmAE4kpF1NEWJGyM5coebsjK/zaCMSjw3mkdxiU2b5cO44oIi0Ifv5iGkvwbdrZOvMyFA== + dependencies: + "@module-federation/runtime-tools" "0.14.0" + "@rspack/binding" "1.3.12" + "@rspack/lite-tapable" "1.0.1" + caniuse-lite "^1.0.30001718" + +"@rspack/lite-tapable@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@rspack/lite-tapable/-/lite-tapable-1.0.1.tgz#d4540a5d28bd6177164bc0ba0bee4bdec0458591" + integrity sha512-VynGOEsVw2s8TAlLf/uESfrgfrq2+rcXB1muPJYBWbsm1Oa6r5qVQhjA5ggM6z/coYPrsVMgovl3Ff7Q7OCp1w== + "@sideway/address@^4.1.5": version "4.1.5" resolved "https://registry.yarnpkg.com/@sideway/address/-/address-4.1.5.tgz#4bc149a0076623ced99ca8208ba780d65a99b9d5" @@ -2172,84 +3691,152 @@ "@svgr/plugin-jsx" "8.1.0" "@svgr/plugin-svgo" "8.1.0" -"@swc/core-darwin-arm64@1.6.1": - version "1.6.1" - resolved "https://registry.yarnpkg.com/@swc/core-darwin-arm64/-/core-darwin-arm64-1.6.1.tgz#72d861fb7094b7a0004f4f300e2c5d4ea1549d9e" - integrity sha512-u6GdwOXsOEdNAdSI6nWq6G2BQw5HiSNIZVcBaH1iSvBnxZvWbnIKyDiZKaYnDwTLHLzig2GuUjjE2NaCJPy4jg== +"@swc/core-darwin-arm64@1.11.29": + version "1.11.29" + resolved "https://registry.yarnpkg.com/@swc/core-darwin-arm64/-/core-darwin-arm64-1.11.29.tgz#bf66e3f4f00e6fe9d95e8a33f780e6c40fca946d" + integrity sha512-whsCX7URzbuS5aET58c75Dloby3Gtj/ITk2vc4WW6pSDQKSPDuONsIcZ7B2ng8oz0K6ttbi4p3H/PNPQLJ4maQ== -"@swc/core-darwin-x64@1.6.1": - version "1.6.1" - resolved "https://registry.yarnpkg.com/@swc/core-darwin-x64/-/core-darwin-x64-1.6.1.tgz#8b7070fcee4a4570d0af245c4614ca4e492dfd5b" - integrity sha512-/tXwQibkDNLVbAtr7PUQI0iQjoB708fjhDDDfJ6WILSBVZ3+qs/LHjJ7jHwumEYxVq1XA7Fv2Q7SE/ZSQoWHcQ== +"@swc/core-darwin-x64@1.11.29": + version "1.11.29" + resolved "https://registry.yarnpkg.com/@swc/core-darwin-x64/-/core-darwin-x64-1.11.29.tgz#0a77d2d79ef2c789f9d40a86784bbf52c5f9877f" + integrity sha512-S3eTo/KYFk+76cWJRgX30hylN5XkSmjYtCBnM4jPLYn7L6zWYEPajsFLmruQEiTEDUg0gBEWLMNyUeghtswouw== -"@swc/core-linux-arm-gnueabihf@1.6.1": - version "1.6.1" - resolved "https://registry.yarnpkg.com/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.6.1.tgz#bea6d2e75127bbc65a664284f012ffa90c8325d5" - integrity sha512-aDgipxhJTms8iH78emHVutFR2c16LNhO+NTRCdYi+X4PyIn58/DyYTH6VDZ0AeEcS5f132ZFldU5AEgExwihXA== +"@swc/core-linux-arm-gnueabihf@1.11.29": + version "1.11.29" + resolved "https://registry.yarnpkg.com/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.11.29.tgz#80fa3a6a36034ffdbbba73e26c8f27cb13111a33" + integrity sha512-o9gdshbzkUMG6azldHdmKklcfrcMx+a23d/2qHQHPDLUPAN+Trd+sDQUYArK5Fcm7TlpG4sczz95ghN0DMkM7g== -"@swc/core-linux-arm64-gnu@1.6.1": - version "1.6.1" - resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.6.1.tgz#5c84d804ec23cf54b31c0bc0b4bdd30ec5d43ce8" - integrity sha512-XkJ+eO4zUKG5g458RyhmKPyBGxI0FwfWFgpfIj5eDybxYJ6s4HBT5MoxyBLorB5kMlZ0XoY/usUMobPVY3nL0g== +"@swc/core-linux-arm64-gnu@1.11.29": + version "1.11.29" + resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.11.29.tgz#42da87f445bc3e26da01d494246884006d9b9a1a" + integrity sha512-sLoaciOgUKQF1KX9T6hPGzvhOQaJn+3DHy4LOHeXhQqvBgr+7QcZ+hl4uixPKTzxk6hy6Hb0QOvQEdBAAR1gXw== -"@swc/core-linux-arm64-musl@1.6.1": - version "1.6.1" - resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.6.1.tgz#e167a350bec12caebc97304068c3ffbad6c398ce" - integrity sha512-dr6YbLBg/SsNxs1hDqJhxdcrS8dGMlOXJwXIrUvACiA8jAd6S5BxYCaqsCefLYXtaOmu0bbx1FB/evfodqB70Q== +"@swc/core-linux-arm64-musl@1.11.29": + version "1.11.29" + resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.11.29.tgz#c9cec610525dc9e9b11ef26319db3780812dfa54" + integrity sha512-PwjB10BC0N+Ce7RU/L23eYch6lXFHz7r3NFavIcwDNa/AAqywfxyxh13OeRy+P0cg7NDpWEETWspXeI4Ek8otw== -"@swc/core-linux-x64-gnu@1.6.1": - version "1.6.1" - resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.6.1.tgz#fdd4e1d63b3e53d195e2ddcb9cb5ad9f31995796" - integrity sha512-A0b/3V+yFy4LXh3O9umIE7LXPC7NBWdjl6AQYqymSMcMu0EOb1/iygA6s6uWhz9y3e172Hpb9b/CGsuD8Px/bg== +"@swc/core-linux-x64-gnu@1.11.29": + version "1.11.29" + resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.11.29.tgz#1cda2df38a4ab8905ba6ac3aa16e4ad710b6f2de" + integrity sha512-i62vBVoPaVe9A3mc6gJG07n0/e7FVeAvdD9uzZTtGLiuIfVfIBta8EMquzvf+POLycSk79Z6lRhGPZPJPYiQaA== -"@swc/core-linux-x64-musl@1.6.1": - version "1.6.1" - resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.6.1.tgz#81a312dd9e62da5f4c48e3cd23b6c6d28a31ac42" - integrity sha512-5dJjlzZXhC87nZZZWbpiDP8kBIO0ibis893F/rtPIQBI5poH+iJuA32EU3wN4/WFHeK4et8z6SGSVghPtWyk4g== +"@swc/core-linux-x64-musl@1.11.29": + version "1.11.29" + resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.11.29.tgz#5d634efff33f47c8d6addd84291ab606903d1cfd" + integrity sha512-YER0XU1xqFdK0hKkfSVX1YIyCvMDI7K07GIpefPvcfyNGs38AXKhb2byySDjbVxkdl4dycaxxhRyhQ2gKSlsFQ== -"@swc/core-win32-arm64-msvc@1.6.1": - version "1.6.1" - resolved "https://registry.yarnpkg.com/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.6.1.tgz#e131f579a69c5d807013e54ccb311e10caa27bcb" - integrity sha512-HBi1ZlwvfcUibLtT3g/lP57FaDPC799AD6InolB2KSgkqyBbZJ9wAXM8/CcH67GLIP0tZ7FqblrJTzGXxetTJQ== +"@swc/core-win32-arm64-msvc@1.11.29": + version "1.11.29" + resolved "https://registry.yarnpkg.com/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.11.29.tgz#bc54f2e3f8f180113b7a092b1ee1eaaab24df62b" + integrity sha512-po+WHw+k9g6FAg5IJ+sMwtA/fIUL3zPQ4m/uJgONBATCVnDDkyW6dBA49uHNVtSEvjvhuD8DVWdFP847YTcITw== -"@swc/core-win32-ia32-msvc@1.6.1": - version "1.6.1" - resolved "https://registry.yarnpkg.com/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.6.1.tgz#9f3d88cf0e826aa8222a695177a065ed2899eb21" - integrity sha512-AKqHohlWERclexar5y6ux4sQ8yaMejEXNxeKXm7xPhXrp13/1p4/I3E5bPVX/jMnvpm4HpcKSP0ee2WsqmhhPw== +"@swc/core-win32-ia32-msvc@1.11.29": + version "1.11.29" + resolved "https://registry.yarnpkg.com/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.11.29.tgz#f1df344c06283643d1fe66c6931b350347b73722" + integrity sha512-h+NjOrbqdRBYr5ItmStmQt6x3tnhqgwbj9YxdGPepbTDamFv7vFnhZR0YfB3jz3UKJ8H3uGJ65Zw1VsC+xpFkg== -"@swc/core-win32-x64-msvc@1.6.1": - version "1.6.1" - resolved "https://registry.yarnpkg.com/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.6.1.tgz#b2082710bc46c484a2c9f2e33a15973806e5031d" - integrity sha512-0dLdTLd+ONve8kgC5T6VQ2Y5G+OZ7y0ujjapnK66wpvCBM6BKYGdT/OKhZKZydrC5gUKaxFN6Y5oOt9JOFUrOQ== +"@swc/core-win32-x64-msvc@1.11.29": + version "1.11.29" + resolved "https://registry.yarnpkg.com/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.11.29.tgz#a6f9dc1df66c8db96d70091abedd78cc52544724" + integrity sha512-Q8cs2BDV9wqDvqobkXOYdC+pLUSEpX/KvI0Dgfun1F+LzuLotRFuDhrvkU9ETJA6OnD2+Fn/ieHgloiKA/Mn/g== -"@swc/core@^1.3.74": - version "1.6.1" - resolved "https://registry.yarnpkg.com/@swc/core/-/core-1.6.1.tgz#a899a205cfaa8e23f805451ef4787987e03b8920" - integrity sha512-Yz5uj5hNZpS5brLtBvKY0L4s2tBAbQ4TjmW8xF1EC3YLFxQRrUjMP49Zm1kp/KYyYvTkSaG48Ffj2YWLu9nChw== +"@swc/core@^1.7.39": + version "1.11.29" + resolved "https://registry.yarnpkg.com/@swc/core/-/core-1.11.29.tgz#bce20113c47fcd6251d06262b8b8c063f8e86a20" + integrity sha512-g4mThMIpWbNhV8G2rWp5a5/Igv8/2UFRJx2yImrLGMgrDDYZIopqZ/z0jZxDgqNA1QDx93rpwNF7jGsxVWcMlA== dependencies: "@swc/counter" "^0.1.3" - "@swc/types" "^0.1.8" + "@swc/types" "^0.1.21" optionalDependencies: - "@swc/core-darwin-arm64" "1.6.1" - "@swc/core-darwin-x64" "1.6.1" - "@swc/core-linux-arm-gnueabihf" "1.6.1" - "@swc/core-linux-arm64-gnu" "1.6.1" - "@swc/core-linux-arm64-musl" "1.6.1" - "@swc/core-linux-x64-gnu" "1.6.1" - "@swc/core-linux-x64-musl" "1.6.1" - "@swc/core-win32-arm64-msvc" "1.6.1" - "@swc/core-win32-ia32-msvc" "1.6.1" - "@swc/core-win32-x64-msvc" "1.6.1" + "@swc/core-darwin-arm64" "1.11.29" + "@swc/core-darwin-x64" "1.11.29" + "@swc/core-linux-arm-gnueabihf" "1.11.29" + "@swc/core-linux-arm64-gnu" "1.11.29" + "@swc/core-linux-arm64-musl" "1.11.29" + "@swc/core-linux-x64-gnu" "1.11.29" + "@swc/core-linux-x64-musl" "1.11.29" + "@swc/core-win32-arm64-msvc" "1.11.29" + "@swc/core-win32-ia32-msvc" "1.11.29" + "@swc/core-win32-x64-msvc" "1.11.29" "@swc/counter@^0.1.3": version "0.1.3" resolved "https://registry.yarnpkg.com/@swc/counter/-/counter-0.1.3.tgz#cc7463bd02949611c6329596fccd2b0ec782b0e9" integrity sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ== -"@swc/types@^0.1.8": - version "0.1.8" - resolved "https://registry.yarnpkg.com/@swc/types/-/types-0.1.8.tgz#2c81d107c86cfbd0c3a05ecf7bb54c50dfa58a95" - integrity sha512-RNFA3+7OJFNYY78x0FYwi1Ow+iF1eF5WvmfY1nXPOEH4R2p/D4Cr1vzje7dNAI2aLFqpv8Wyz4oKSWqIZArpQA== +"@swc/html-darwin-arm64@1.11.29": + version "1.11.29" + resolved "https://registry.yarnpkg.com/@swc/html-darwin-arm64/-/html-darwin-arm64-1.11.29.tgz#7bd6d10115ffe155ecd757387b5aff318b02b5a0" + integrity sha512-q53kn/HI0n/+pecsOB2gxqITbRAhtBG7VI520SIWuCGXHPsTQ/1VOrhLMNvyfw1xVhRyFal7BpAvfGUORCl0sw== + +"@swc/html-darwin-x64@1.11.29": + version "1.11.29" + resolved "https://registry.yarnpkg.com/@swc/html-darwin-x64/-/html-darwin-x64-1.11.29.tgz#ccafed56081932ffaa51be1788cef88eb9a144d1" + integrity sha512-YfQPjh5WoDqOxsA7vDOOSnxEPc1Ki4SuZ0ufR4t8jYdMOFsU3AhZQ/sgBZLpTzegBTutUn7/7yy8VSoFngeR7Q== + +"@swc/html-linux-arm-gnueabihf@1.11.29": + version "1.11.29" + resolved "https://registry.yarnpkg.com/@swc/html-linux-arm-gnueabihf/-/html-linux-arm-gnueabihf-1.11.29.tgz#e784a1a0f69034e9dd52a9019ff80f0d5eb91433" + integrity sha512-dC3aEv1mqAUkY9TiZWOE2IcYpvxJzw0LdvkDzGW5072JSlZZYQMqq2Llwg63LIp6qBlj1JLHMLnBqk7Ubatmjw== + +"@swc/html-linux-arm64-gnu@1.11.29": + version "1.11.29" + resolved "https://registry.yarnpkg.com/@swc/html-linux-arm64-gnu/-/html-linux-arm64-gnu-1.11.29.tgz#ef015d81d3a011d6c273a428eee130b3aac790b7" + integrity sha512-seo+lCiBUggTR9NsHE4qVC+7+XIfLHK7yxWiIsXb8nNAXDcqVZ0Rxv8O1Y1GTeJfUlcCt1koahCG2AeyWpYFBg== + +"@swc/html-linux-arm64-musl@1.11.29": + version "1.11.29" + resolved "https://registry.yarnpkg.com/@swc/html-linux-arm64-musl/-/html-linux-arm64-musl-1.11.29.tgz#b20e4b442287367c4c1d62db2b8065106542f432" + integrity sha512-bK8K6t3hHgaZZ1vMNaZ+8x42EWJPEX1Dx4zi6ulMhKa1uan+DjW5SiMlUg0an16fFSYfE+r9oFC4cFEbGP1o4Q== + +"@swc/html-linux-x64-gnu@1.11.29": + version "1.11.29" + resolved "https://registry.yarnpkg.com/@swc/html-linux-x64-gnu/-/html-linux-x64-gnu-1.11.29.tgz#c45505b3e22c02dc8bdef8b3da48ba855527a62c" + integrity sha512-34tSms5TkRUCr+J6uuSE/11ECcfIpp5R1ODuIgxZRUd/u88pQGKzLVNLWGPLw4b3cZSjnAn+PFJl7BtaYl0UyQ== + +"@swc/html-linux-x64-musl@1.11.29": + version "1.11.29" + resolved "https://registry.yarnpkg.com/@swc/html-linux-x64-musl/-/html-linux-x64-musl-1.11.29.tgz#86116e7db3cf02b1a627bc94c374d26ce6f9d68a" + integrity sha512-oJLLrX94ccaniWdQt8PH6K2u8aN/ehBo/YPg84LycFtaud/k73Fa1kh6Neq8vbWI4CugIWTl4LXWoHm+l+QYeA== + +"@swc/html-win32-arm64-msvc@1.11.29": + version "1.11.29" + resolved "https://registry.yarnpkg.com/@swc/html-win32-arm64-msvc/-/html-win32-arm64-msvc-1.11.29.tgz#cf7e57b8c0b52f7f93abc307b0cb78d8213b3c13" + integrity sha512-nw4TCFfA4YV6jicRdicJZPKW+ihOZPMKEG/4bj1/6HqXw1T2pXI070ASOLE0KOHYuoyV/jConEHfIjlU0olneA== + +"@swc/html-win32-ia32-msvc@1.11.29": + version "1.11.29" + resolved "https://registry.yarnpkg.com/@swc/html-win32-ia32-msvc/-/html-win32-ia32-msvc-1.11.29.tgz#61c91409c3fcdf942891c02aa2a2eac1892d1907" + integrity sha512-rO6X4qOofGpKV8pyZ7VblJn+J3PHEqeWHJkJfzwP7c04Flr1oLyuLbTU8lwf8enXrTAZqitHZs+OpofKcUwHEw== + +"@swc/html-win32-x64-msvc@1.11.29": + version "1.11.29" + resolved "https://registry.yarnpkg.com/@swc/html-win32-x64-msvc/-/html-win32-x64-msvc-1.11.29.tgz#0e78b507d2bf28315487655852008cdecfe84535" + integrity sha512-GSCihzBItEPJAeLzkAtw0ZGbxRGMsGt1Z1ugo0uHva1R3Eybkqu9qoax1tGAON+EJzeiHRqphhNgh8MVDpnKnQ== + +"@swc/html@^1.7.39": + version "1.11.29" + resolved "https://registry.yarnpkg.com/@swc/html/-/html-1.11.29.tgz#6e9e1b8ea65baa0d6f25cb883565a5e7d22d2858" + integrity sha512-Tsk/o6Eo3lDvHPGjLqVwXGEdC1bemGzByPWx/TrF5N7qEsanRblPeRcJzLl6LbWa80pRYIRB6T4VqdXXZqklaw== + dependencies: + "@swc/counter" "^0.1.3" + optionalDependencies: + "@swc/html-darwin-arm64" "1.11.29" + "@swc/html-darwin-x64" "1.11.29" + "@swc/html-linux-arm-gnueabihf" "1.11.29" + "@swc/html-linux-arm64-gnu" "1.11.29" + "@swc/html-linux-arm64-musl" "1.11.29" + "@swc/html-linux-x64-gnu" "1.11.29" + "@swc/html-linux-x64-musl" "1.11.29" + "@swc/html-win32-arm64-msvc" "1.11.29" + "@swc/html-win32-ia32-msvc" "1.11.29" + "@swc/html-win32-x64-msvc" "1.11.29" + +"@swc/types@^0.1.21": + version "0.1.21" + resolved "https://registry.yarnpkg.com/@swc/types/-/types-0.1.21.tgz#6fcadbeca1d8bc89e1ab3de4948cef12344a38c0" + integrity sha512-2YEtj5HJVbKivud9N4bpPBAyZhj4S2Ipe5LkUG94alTpr7in/GU/EARgPAd3BwU+YOmFVJC2+kjqhGRi3r0ZpQ== dependencies: "@swc/counter" "^0.1.3" @@ -2314,23 +3901,216 @@ dependencies: "@types/node" "*" -"@types/d3-scale-chromatic@^3.0.0": - version "3.0.3" - resolved "https://registry.yarnpkg.com/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.0.3.tgz#fc0db9c10e789c351f4c42d96f31f2e4df8f5644" - integrity sha512-laXM4+1o5ImZv3RpFAsTRn3TEkzqkytiOY0Dz0sq5cnd1dtNlk6sHLon4OvqaiJb28T0S/TdsBI3Sjsy+keJrw== +"@types/d3-array@*": + version "3.2.1" + resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-3.2.1.tgz#1f6658e3d2006c4fceac53fde464166859f8b8c5" + integrity sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg== -"@types/d3-scale@^4.0.3": - version "4.0.8" - resolved "https://registry.yarnpkg.com/@types/d3-scale/-/d3-scale-4.0.8.tgz#d409b5f9dcf63074464bf8ddfb8ee5a1f95945bb" - integrity sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ== +"@types/d3-axis@*": + version "3.0.6" + resolved "https://registry.yarnpkg.com/@types/d3-axis/-/d3-axis-3.0.6.tgz#e760e5765b8188b1defa32bc8bb6062f81e4c795" + integrity sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw== + dependencies: + "@types/d3-selection" "*" + +"@types/d3-brush@*": + version "3.0.6" + resolved "https://registry.yarnpkg.com/@types/d3-brush/-/d3-brush-3.0.6.tgz#c2f4362b045d472e1b186cdbec329ba52bdaee6c" + integrity sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A== + dependencies: + "@types/d3-selection" "*" + +"@types/d3-chord@*": + version "3.0.6" + resolved "https://registry.yarnpkg.com/@types/d3-chord/-/d3-chord-3.0.6.tgz#1706ca40cf7ea59a0add8f4456efff8f8775793d" + integrity sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg== + +"@types/d3-color@*": + version "3.1.3" + resolved "https://registry.yarnpkg.com/@types/d3-color/-/d3-color-3.1.3.tgz#368c961a18de721da8200e80bf3943fb53136af2" + integrity sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A== + +"@types/d3-contour@*": + version "3.0.6" + resolved "https://registry.yarnpkg.com/@types/d3-contour/-/d3-contour-3.0.6.tgz#9ada3fa9c4d00e3a5093fed0356c7ab929604231" + integrity sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg== + dependencies: + "@types/d3-array" "*" + "@types/geojson" "*" + +"@types/d3-delaunay@*": + version "6.0.4" + resolved "https://registry.yarnpkg.com/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz#185c1a80cc807fdda2a3fe960f7c11c4a27952e1" + integrity sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw== + +"@types/d3-dispatch@*": + version "3.0.6" + resolved "https://registry.yarnpkg.com/@types/d3-dispatch/-/d3-dispatch-3.0.6.tgz#096efdf55eb97480e3f5621ff9a8da552f0961e7" + integrity sha512-4fvZhzMeeuBJYZXRXrRIQnvUYfyXwYmLsdiN7XXmVNQKKw1cM8a5WdID0g1hVFZDqT9ZqZEY5pD44p24VS7iZQ== + +"@types/d3-drag@*": + version "3.0.7" + resolved "https://registry.yarnpkg.com/@types/d3-drag/-/d3-drag-3.0.7.tgz#b13aba8b2442b4068c9a9e6d1d82f8bcea77fc02" + integrity sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ== + dependencies: + "@types/d3-selection" "*" + +"@types/d3-dsv@*": + version "3.0.7" + resolved "https://registry.yarnpkg.com/@types/d3-dsv/-/d3-dsv-3.0.7.tgz#0a351f996dc99b37f4fa58b492c2d1c04e3dac17" + integrity sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g== + +"@types/d3-ease@*": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/d3-ease/-/d3-ease-3.0.2.tgz#e28db1bfbfa617076f7770dd1d9a48eaa3b6c51b" + integrity sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA== + +"@types/d3-fetch@*": + version "3.0.7" + resolved "https://registry.yarnpkg.com/@types/d3-fetch/-/d3-fetch-3.0.7.tgz#c04a2b4f23181aa376f30af0283dbc7b3b569980" + integrity sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA== + dependencies: + "@types/d3-dsv" "*" + +"@types/d3-force@*": + version "3.0.10" + resolved "https://registry.yarnpkg.com/@types/d3-force/-/d3-force-3.0.10.tgz#6dc8fc6e1f35704f3b057090beeeb7ac674bff1a" + integrity sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw== + +"@types/d3-format@*": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@types/d3-format/-/d3-format-3.0.4.tgz#b1e4465644ddb3fdf3a263febb240a6cd616de90" + integrity sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g== + +"@types/d3-geo@*": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@types/d3-geo/-/d3-geo-3.1.0.tgz#b9e56a079449174f0a2c8684a9a4df3f60522440" + integrity sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ== + dependencies: + "@types/geojson" "*" + +"@types/d3-hierarchy@*": + version "3.1.7" + resolved "https://registry.yarnpkg.com/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz#6023fb3b2d463229f2d680f9ac4b47466f71f17b" + integrity sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg== + +"@types/d3-interpolate@*": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz#412b90e84870285f2ff8a846c6eb60344f12a41c" + integrity sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA== + dependencies: + "@types/d3-color" "*" + +"@types/d3-path@*": + version "3.1.1" + resolved "https://registry.yarnpkg.com/@types/d3-path/-/d3-path-3.1.1.tgz#f632b380c3aca1dba8e34aa049bcd6a4af23df8a" + integrity sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg== + +"@types/d3-polygon@*": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/d3-polygon/-/d3-polygon-3.0.2.tgz#dfae54a6d35d19e76ac9565bcb32a8e54693189c" + integrity sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA== + +"@types/d3-quadtree@*": + version "3.0.6" + resolved "https://registry.yarnpkg.com/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz#d4740b0fe35b1c58b66e1488f4e7ed02952f570f" + integrity sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg== + +"@types/d3-random@*": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@types/d3-random/-/d3-random-3.0.3.tgz#ed995c71ecb15e0cd31e22d9d5d23942e3300cfb" + integrity sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ== + +"@types/d3-scale-chromatic@*": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz#dc6d4f9a98376f18ea50bad6c39537f1b5463c39" + integrity sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ== + +"@types/d3-scale@*": + version "4.0.9" + resolved "https://registry.yarnpkg.com/@types/d3-scale/-/d3-scale-4.0.9.tgz#57a2f707242e6fe1de81ad7bfcccaaf606179afb" + integrity sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw== dependencies: "@types/d3-time" "*" +"@types/d3-selection@*": + version "3.0.11" + resolved "https://registry.yarnpkg.com/@types/d3-selection/-/d3-selection-3.0.11.tgz#bd7a45fc0a8c3167a631675e61bc2ca2b058d4a3" + integrity sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w== + +"@types/d3-shape@*": + version "3.1.7" + resolved "https://registry.yarnpkg.com/@types/d3-shape/-/d3-shape-3.1.7.tgz#2b7b423dc2dfe69c8c93596e673e37443348c555" + integrity sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg== + dependencies: + "@types/d3-path" "*" + +"@types/d3-time-format@*": + version "4.0.3" + resolved "https://registry.yarnpkg.com/@types/d3-time-format/-/d3-time-format-4.0.3.tgz#d6bc1e6b6a7db69cccfbbdd4c34b70632d9e9db2" + integrity sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg== + "@types/d3-time@*": version "3.0.3" resolved "https://registry.yarnpkg.com/@types/d3-time/-/d3-time-3.0.3.tgz#3c186bbd9d12b9d84253b6be6487ca56b54f88be" integrity sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw== +"@types/d3-timer@*": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/d3-timer/-/d3-timer-3.0.2.tgz#70bbda77dc23aa727413e22e214afa3f0e852f70" + integrity sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw== + +"@types/d3-transition@*": + version "3.0.9" + resolved "https://registry.yarnpkg.com/@types/d3-transition/-/d3-transition-3.0.9.tgz#1136bc57e9ddb3c390dccc9b5ff3b7d2b8d94706" + integrity sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg== + dependencies: + "@types/d3-selection" "*" + +"@types/d3-zoom@*": + version "3.0.8" + resolved "https://registry.yarnpkg.com/@types/d3-zoom/-/d3-zoom-3.0.8.tgz#dccb32d1c56b1e1c6e0f1180d994896f038bc40b" + integrity sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw== + dependencies: + "@types/d3-interpolate" "*" + "@types/d3-selection" "*" + +"@types/d3@^7.4.3": + version "7.4.3" + resolved "https://registry.yarnpkg.com/@types/d3/-/d3-7.4.3.tgz#d4550a85d08f4978faf0a4c36b848c61eaac07e2" + integrity sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww== + dependencies: + "@types/d3-array" "*" + "@types/d3-axis" "*" + "@types/d3-brush" "*" + "@types/d3-chord" "*" + "@types/d3-color" "*" + "@types/d3-contour" "*" + "@types/d3-delaunay" "*" + "@types/d3-dispatch" "*" + "@types/d3-drag" "*" + "@types/d3-dsv" "*" + "@types/d3-ease" "*" + "@types/d3-fetch" "*" + "@types/d3-force" "*" + "@types/d3-format" "*" + "@types/d3-geo" "*" + "@types/d3-hierarchy" "*" + "@types/d3-interpolate" "*" + "@types/d3-path" "*" + "@types/d3-polygon" "*" + "@types/d3-quadtree" "*" + "@types/d3-random" "*" + "@types/d3-scale" "*" + "@types/d3-scale-chromatic" "*" + "@types/d3-selection" "*" + "@types/d3-shape" "*" + "@types/d3-time" "*" + "@types/d3-time-format" "*" + "@types/d3-timer" "*" + "@types/d3-transition" "*" + "@types/d3-zoom" "*" + "@types/debug@^4.0.0": version "4.1.12" resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.12.tgz#a155f21690871953410df4b6b6f53187f0500917" @@ -2338,7 +4118,7 @@ dependencies: "@types/ms" "*" -"@types/eslint-scope@^3.7.3": +"@types/eslint-scope@^3.7.3", "@types/eslint-scope@^3.7.7": version "3.7.7" resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.7.tgz#3108bd5f18b0cdb277c867b3dd449c9ed7079ac5" integrity sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg== @@ -2366,6 +4146,11 @@ resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4" integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw== +"@types/estree@^1.0.6": + version "1.0.7" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.7.tgz#4158d3105276773d5b7695cd4834b1722e4f37a8" + integrity sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ== + "@types/express-serve-static-core@*", "@types/express-serve-static-core@^4.17.33": version "4.19.3" resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.19.3.tgz#e469a13e4186c9e1c0418fb17be8bc8ff1b19a7a" @@ -2386,6 +4171,11 @@ "@types/qs" "*" "@types/serve-static" "*" +"@types/geojson@*": + version "7946.0.16" + resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.16.tgz#8ebe53d69efada7044454e3305c19017d97ced2a" + integrity sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg== + "@types/gtag.js@^0.0.12": version "0.0.12" resolved "https://registry.yarnpkg.com/@types/gtag.js/-/gtag.js-0.0.12.tgz#095122edca896689bdfcdd73b057e23064d23572" @@ -2459,7 +4249,7 @@ dependencies: "@types/istanbul-lib-report" "*" -"@types/json-schema@*", "@types/json-schema@^7.0.15", "@types/json-schema@^7.0.4", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": +"@types/json-schema@*", "@types/json-schema@^7.0.15", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": version "7.0.15" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== @@ -2512,11 +4302,6 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.45.tgz#2c0fafd78705e7a18b7906b5201a522719dc5190" integrity sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw== -"@types/parse-json@^4.0.0": - version "4.0.2" - resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.2.tgz#5950e50960793055845e956c427fc2b0d70c5239" - integrity sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw== - "@types/parse5@^6.0.0": version "6.0.3" resolved "https://registry.yarnpkg.com/@types/parse5/-/parse5-6.0.3.tgz#705bb349e789efa06f43f128cef51240753424cb" @@ -2629,6 +4414,11 @@ dependencies: "@types/node" "*" +"@types/trusted-types@^2.0.7": + version "2.0.7" + resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.7.tgz#baccb07a970b91707df3a3e8ba6896c57ead2d11" + integrity sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw== + "@types/unist@*", "@types/unist@^3.0.0": version "3.0.2" resolved "https://registry.yarnpkg.com/@types/unist/-/unist-3.0.2.tgz#6dd61e43ef60b34086287f83683a5c1b2dc53d20" @@ -2678,21 +4468,44 @@ "@webassemblyjs/helper-numbers" "1.11.6" "@webassemblyjs/helper-wasm-bytecode" "1.11.6" +"@webassemblyjs/ast@1.14.1", "@webassemblyjs/ast@^1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.14.1.tgz#a9f6a07f2b03c95c8d38c4536a1fdfb521ff55b6" + integrity sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ== + dependencies: + "@webassemblyjs/helper-numbers" "1.13.2" + "@webassemblyjs/helper-wasm-bytecode" "1.13.2" + "@webassemblyjs/floating-point-hex-parser@1.11.6": version "1.11.6" resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz#dacbcb95aff135c8260f77fa3b4c5fea600a6431" integrity sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw== +"@webassemblyjs/floating-point-hex-parser@1.13.2": + version "1.13.2" + resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz#fcca1eeddb1cc4e7b6eed4fc7956d6813b21b9fb" + integrity sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA== + "@webassemblyjs/helper-api-error@1.11.6": version "1.11.6" resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz#6132f68c4acd59dcd141c44b18cbebbd9f2fa768" integrity sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q== +"@webassemblyjs/helper-api-error@1.13.2": + version "1.13.2" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz#e0a16152248bc38daee76dd7e21f15c5ef3ab1e7" + integrity sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ== + "@webassemblyjs/helper-buffer@1.12.1": version "1.12.1" resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz#6df20d272ea5439bf20ab3492b7fb70e9bfcb3f6" integrity sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw== +"@webassemblyjs/helper-buffer@1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz#822a9bc603166531f7d5df84e67b5bf99b72b96b" + integrity sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA== + "@webassemblyjs/helper-numbers@1.11.6": version "1.11.6" resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz#cbce5e7e0c1bd32cf4905ae444ef64cea919f1b5" @@ -2702,11 +4515,25 @@ "@webassemblyjs/helper-api-error" "1.11.6" "@xtuc/long" "4.2.2" +"@webassemblyjs/helper-numbers@1.13.2": + version "1.13.2" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz#dbd932548e7119f4b8a7877fd5a8d20e63490b2d" + integrity sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA== + dependencies: + "@webassemblyjs/floating-point-hex-parser" "1.13.2" + "@webassemblyjs/helper-api-error" "1.13.2" + "@xtuc/long" "4.2.2" + "@webassemblyjs/helper-wasm-bytecode@1.11.6": version "1.11.6" resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz#bb2ebdb3b83aa26d9baad4c46d4315283acd51e9" integrity sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA== +"@webassemblyjs/helper-wasm-bytecode@1.13.2": + version "1.13.2" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz#e556108758f448aae84c850e593ce18a0eb31e0b" + integrity sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA== + "@webassemblyjs/helper-wasm-section@1.12.1": version "1.12.1" resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz#3da623233ae1a60409b509a52ade9bc22a37f7bf" @@ -2717,6 +4544,16 @@ "@webassemblyjs/helper-wasm-bytecode" "1.11.6" "@webassemblyjs/wasm-gen" "1.12.1" +"@webassemblyjs/helper-wasm-section@1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz#9629dda9c4430eab54b591053d6dc6f3ba050348" + integrity sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw== + dependencies: + "@webassemblyjs/ast" "1.14.1" + "@webassemblyjs/helper-buffer" "1.14.1" + "@webassemblyjs/helper-wasm-bytecode" "1.13.2" + "@webassemblyjs/wasm-gen" "1.14.1" + "@webassemblyjs/ieee754@1.11.6": version "1.11.6" resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz#bb665c91d0b14fffceb0e38298c329af043c6e3a" @@ -2724,6 +4561,13 @@ dependencies: "@xtuc/ieee754" "^1.2.0" +"@webassemblyjs/ieee754@1.13.2": + version "1.13.2" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz#1c5eaace1d606ada2c7fd7045ea9356c59ee0dba" + integrity sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw== + dependencies: + "@xtuc/ieee754" "^1.2.0" + "@webassemblyjs/leb128@1.11.6": version "1.11.6" resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.11.6.tgz#70e60e5e82f9ac81118bc25381a0b283893240d7" @@ -2731,11 +4575,23 @@ dependencies: "@xtuc/long" "4.2.2" +"@webassemblyjs/leb128@1.13.2": + version "1.13.2" + resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.13.2.tgz#57c5c3deb0105d02ce25fa3fd74f4ebc9fd0bbb0" + integrity sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw== + dependencies: + "@xtuc/long" "4.2.2" + "@webassemblyjs/utf8@1.11.6": version "1.11.6" resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.11.6.tgz#90f8bc34c561595fe156603be7253cdbcd0fab5a" integrity sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA== +"@webassemblyjs/utf8@1.13.2": + version "1.13.2" + resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.13.2.tgz#917a20e93f71ad5602966c2d685ae0c6c21f60f1" + integrity sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ== + "@webassemblyjs/wasm-edit@^1.12.1": version "1.12.1" resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz#9f9f3ff52a14c980939be0ef9d5df9ebc678ae3b" @@ -2750,6 +4606,20 @@ "@webassemblyjs/wasm-parser" "1.12.1" "@webassemblyjs/wast-printer" "1.12.1" +"@webassemblyjs/wasm-edit@^1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz#ac6689f502219b59198ddec42dcd496b1004d597" + integrity sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ== + dependencies: + "@webassemblyjs/ast" "1.14.1" + "@webassemblyjs/helper-buffer" "1.14.1" + "@webassemblyjs/helper-wasm-bytecode" "1.13.2" + "@webassemblyjs/helper-wasm-section" "1.14.1" + "@webassemblyjs/wasm-gen" "1.14.1" + "@webassemblyjs/wasm-opt" "1.14.1" + "@webassemblyjs/wasm-parser" "1.14.1" + "@webassemblyjs/wast-printer" "1.14.1" + "@webassemblyjs/wasm-gen@1.12.1": version "1.12.1" resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz#a6520601da1b5700448273666a71ad0a45d78547" @@ -2761,6 +4631,17 @@ "@webassemblyjs/leb128" "1.11.6" "@webassemblyjs/utf8" "1.11.6" +"@webassemblyjs/wasm-gen@1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz#991e7f0c090cb0bb62bbac882076e3d219da9570" + integrity sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg== + dependencies: + "@webassemblyjs/ast" "1.14.1" + "@webassemblyjs/helper-wasm-bytecode" "1.13.2" + "@webassemblyjs/ieee754" "1.13.2" + "@webassemblyjs/leb128" "1.13.2" + "@webassemblyjs/utf8" "1.13.2" + "@webassemblyjs/wasm-opt@1.12.1": version "1.12.1" resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz#9e6e81475dfcfb62dab574ac2dda38226c232bc5" @@ -2771,6 +4652,16 @@ "@webassemblyjs/wasm-gen" "1.12.1" "@webassemblyjs/wasm-parser" "1.12.1" +"@webassemblyjs/wasm-opt@1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz#e6f71ed7ccae46781c206017d3c14c50efa8106b" + integrity sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw== + dependencies: + "@webassemblyjs/ast" "1.14.1" + "@webassemblyjs/helper-buffer" "1.14.1" + "@webassemblyjs/wasm-gen" "1.14.1" + "@webassemblyjs/wasm-parser" "1.14.1" + "@webassemblyjs/wasm-parser@1.12.1", "@webassemblyjs/wasm-parser@^1.12.1": version "1.12.1" resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz#c47acb90e6f083391e3fa61d113650eea1e95937" @@ -2783,6 +4674,18 @@ "@webassemblyjs/leb128" "1.11.6" "@webassemblyjs/utf8" "1.11.6" +"@webassemblyjs/wasm-parser@1.14.1", "@webassemblyjs/wasm-parser@^1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz#b3e13f1893605ca78b52c68e54cf6a865f90b9fb" + integrity sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ== + dependencies: + "@webassemblyjs/ast" "1.14.1" + "@webassemblyjs/helper-api-error" "1.13.2" + "@webassemblyjs/helper-wasm-bytecode" "1.13.2" + "@webassemblyjs/ieee754" "1.13.2" + "@webassemblyjs/leb128" "1.13.2" + "@webassemblyjs/utf8" "1.13.2" + "@webassemblyjs/wast-printer@1.12.1": version "1.12.1" resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz#bcecf661d7d1abdaf989d8341a4833e33e2b31ac" @@ -2791,6 +4694,14 @@ "@webassemblyjs/ast" "1.12.1" "@xtuc/long" "4.2.2" +"@webassemblyjs/wast-printer@1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz#3bb3e9638a8ae5fdaf9610e7a06b4d9f9aa6fe07" + integrity sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw== + dependencies: + "@webassemblyjs/ast" "1.14.1" + "@xtuc/long" "4.2.2" + "@xtuc/ieee754@^1.2.0": version "1.2.0" resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790" @@ -2801,13 +4712,6 @@ resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d" integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== -abort-controller@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" - integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg== - dependencies: - event-target-shim "^5.0.0" - accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.8: version "1.3.8" resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" @@ -2838,7 +4742,12 @@ acorn@^8.0.0, acorn@^8.0.4, acorn@^8.11.0, acorn@^8.7.1, acorn@^8.8.2: resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.12.0.tgz#1627bfa2e058148036133b8d9b51a700663c294c" integrity sha512-RTvkC4w+KNXrM39/lWCUaG0IbRkWdCv7W/IOW9oU6SawyxulvkQy5HQPVTKxEjczcUvapcrw3cFx/60VN/NRNw== -address@^1.0.1, address@^1.1.2: +acorn@^8.14.0: + version "8.14.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.14.1.tgz#721d5dc10f7d5b5609a891773d47731796935dfb" + integrity sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg== + +address@^1.0.1: version "1.2.2" resolved "https://registry.yarnpkg.com/address/-/address-1.2.2.tgz#2b5248dac5485a6390532c6a517fda2e3faac89e" integrity sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA== @@ -2870,7 +4779,7 @@ ajv-formats@2.1.1, ajv-formats@^2.1.1: dependencies: ajv "^8.0.0" -ajv-keywords@^3.4.1, ajv-keywords@^3.5.2: +ajv-keywords@^3.5.2: version "3.5.2" resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== @@ -2892,7 +4801,7 @@ ajv@8.11.0: require-from-string "^2.0.2" uri-js "^4.2.2" -ajv@^6.12.2, ajv@^6.12.5: +ajv@^6.12.5: version "6.12.6" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== @@ -2912,33 +4821,38 @@ ajv@^8.0.0, ajv@^8.9.0: require-from-string "^2.0.2" uri-js "^4.4.1" -algoliasearch-helper@^3.13.3: - version "3.21.0" - resolved "https://registry.yarnpkg.com/algoliasearch-helper/-/algoliasearch-helper-3.21.0.tgz#d28fdb61199b5c229714788bfb812376b18aaf28" - integrity sha512-hjVOrL15I3Y3K8xG0icwG1/tWE+MocqBrhW6uVBWpU+/kVEMK0BnM2xdssj6mZM61eJ4iRxHR0djEI3ENOpR8w== +algoliasearch-helper@^3.22.6: + version "3.25.0" + resolved "https://registry.yarnpkg.com/algoliasearch-helper/-/algoliasearch-helper-3.25.0.tgz#15cc79ad7909db66b8bb5a5a9c38b40e3941fa2f" + integrity sha512-vQoK43U6HXA9/euCqLjvyNdM4G2Fiu/VFp4ae0Gau9sZeIKBPvUPnXfLYAe65Bg7PFuw03coeu5K6lTPSXRObw== dependencies: "@algolia/events" "^4.0.1" -algoliasearch@^4.18.0, algoliasearch@^4.19.1: - version "4.23.3" - resolved "https://registry.yarnpkg.com/algoliasearch/-/algoliasearch-4.23.3.tgz#e09011d0a3b0651444916a3e6bbcba064ec44b60" - integrity sha512-Le/3YgNvjW9zxIQMRhUHuhiUjAlKY/zsdZpfq4dlLqg6mEm0nL6yk+7f2hDOtLpxsgE4jSzDmvHL7nXdBp5feg== +algoliasearch@^5.14.2, algoliasearch@^5.17.1: + version "5.25.0" + resolved "https://registry.yarnpkg.com/algoliasearch/-/algoliasearch-5.25.0.tgz#7337b097deadeca0e6e985c0f8724abea189994f" + integrity sha512-n73BVorL4HIwKlfJKb4SEzAYkR3Buwfwbh+MYxg2mloFph2fFGV58E90QTzdbfzWrLn4HE5Czx/WTjI8fcHaMg== dependencies: - "@algolia/cache-browser-local-storage" "4.23.3" - "@algolia/cache-common" "4.23.3" - "@algolia/cache-in-memory" "4.23.3" - "@algolia/client-account" "4.23.3" - "@algolia/client-analytics" "4.23.3" - "@algolia/client-common" "4.23.3" - "@algolia/client-personalization" "4.23.3" - "@algolia/client-search" "4.23.3" - "@algolia/logger-common" "4.23.3" - "@algolia/logger-console" "4.23.3" - "@algolia/recommend" "4.23.3" - "@algolia/requester-browser-xhr" "4.23.3" - "@algolia/requester-common" "4.23.3" - "@algolia/requester-node-http" "4.23.3" - "@algolia/transporter" "4.23.3" + "@algolia/client-abtesting" "5.25.0" + "@algolia/client-analytics" "5.25.0" + "@algolia/client-common" "5.25.0" + "@algolia/client-insights" "5.25.0" + "@algolia/client-personalization" "5.25.0" + "@algolia/client-query-suggestions" "5.25.0" + "@algolia/client-search" "5.25.0" + "@algolia/ingestion" "1.25.0" + "@algolia/monitoring" "1.25.0" + "@algolia/recommend" "5.25.0" + "@algolia/requester-browser-xhr" "5.25.0" + "@algolia/requester-fetch" "5.25.0" + "@algolia/requester-node-http" "5.25.0" + +allof-merge@^0.6.6: + version "0.6.6" + resolved "https://registry.yarnpkg.com/allof-merge/-/allof-merge-0.6.6.tgz#1c675c7170e1b24bd3dc96db9c3459c0e7cfbea2" + integrity sha512-116eZBf2he0/J4Tl7EYMz96I5Anaeio+VL0j/H2yxW9CoYQAMMv8gYcwkVRoO7XfIOv/qzSTfVzDVGAYxKFi3g== + dependencies: + json-crawl "^0.5.3" ansi-align@^3.0.1: version "3.0.1" @@ -2947,6 +4861,13 @@ ansi-align@^3.0.1: dependencies: string-width "^4.1.0" +ansi-escapes@^4.3.2: + version "4.3.2" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" + integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== + dependencies: + type-fest "^0.21.3" + ansi-html-community@^0.0.8: version "0.0.8" resolved "https://registry.yarnpkg.com/ansi-html-community/-/ansi-html-community-0.0.8.tgz#69fbc4d6ccbe383f9736934ae34c3f8290f1bf41" @@ -3021,26 +4942,6 @@ array-union@^2.1.0: resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== -asn1.js@^4.10.1: - version "4.10.1" - resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-4.10.1.tgz#b9c2bf5805f1e64aadeed6df3a2bfafb5a73f5a0" - integrity sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw== - dependencies: - bn.js "^4.0.0" - inherits "^2.0.1" - minimalistic-assert "^1.0.0" - -assert@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/assert/-/assert-2.1.0.tgz#6d92a238d05dc02e7427c881fb8be81c8448b2dd" - integrity sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw== - dependencies: - call-bind "^1.0.2" - is-nan "^1.3.2" - object-is "^1.1.5" - object.assign "^4.1.4" - util "^0.12.5" - astring@^1.8.0: version "1.8.6" resolved "https://registry.yarnpkg.com/astring/-/astring-1.8.6.tgz#2c9c157cf1739d67561c56ba896e6948f6b93731" @@ -3061,7 +4962,7 @@ at-least-node@^1.0.0: resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2" integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== -autoprefixer@^10.4.13, autoprefixer@^10.4.14, autoprefixer@^10.4.19: +autoprefixer@^10.4.13, autoprefixer@^10.4.19: version "10.4.19" resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.19.tgz#ad25a856e82ee9d7898c59583c1afeb3fa65f89f" integrity sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew== @@ -3073,24 +4974,22 @@ autoprefixer@^10.4.13, autoprefixer@^10.4.14, autoprefixer@^10.4.19: picocolors "^1.0.0" postcss-value-parser "^4.2.0" -available-typed-arrays@^1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz#a5cc375d6a03c2efc87a553f3e0b1522def14846" - integrity sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ== +autoprefixer@^10.4.21: + version "10.4.21" + resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.21.tgz#77189468e7a8ad1d9a37fbc08efc9f480cf0a95d" + integrity sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ== dependencies: - possible-typed-array-names "^1.0.0" + browserslist "^4.24.4" + caniuse-lite "^1.0.30001702" + fraction.js "^4.3.7" + normalize-range "^0.1.2" + picocolors "^1.1.1" + postcss-value-parser "^4.2.0" -axios@^0.25.0: - version "0.25.0" - resolved "https://registry.yarnpkg.com/axios/-/axios-0.25.0.tgz#349cfbb31331a9b4453190791760a8d35b093e0a" - integrity sha512-cD8FOb0tRH3uuEe6+evtAbgJtfxr7ly3fQjYcMcuPlgkwVS9xboaVIpcDV+cYQe+yGykgwZCs1pzjntcGa6l5g== - dependencies: - follow-redirects "^1.14.7" - -babel-loader@^9.1.3: - version "9.1.3" - resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-9.1.3.tgz#3d0e01b4e69760cc694ee306fe16d358aa1c6f9a" - integrity sha512-xG3ST4DglodGf8qSwv0MdeWLhrDsw/32QMdTO5T1ZIp9gQur0HkCyFs7Awskr10JKXFXwpAhiCuYX5oGXnRGbw== +babel-loader@^9.2.1: + version "9.2.1" + resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-9.2.1.tgz#04c7835db16c246dd19ba0914418f3937797587b" + integrity sha512-fqe8naHt46e0yIdkjUZYqddSXfej3AHajX+CSO5X7oy0EmPc6o5Xh+RClNoHjnieWz9AW4kZxW9yyFMhVB1QLA== dependencies: find-cache-dir "^4.0.0" schema-utils "^4.0.0" @@ -3111,7 +5010,7 @@ babel-plugin-polyfill-corejs2@^0.4.10: "@babel/helper-define-polyfill-provider" "^0.6.2" semver "^6.3.1" -babel-plugin-polyfill-corejs3@^0.10.1, babel-plugin-polyfill-corejs3@^0.10.4: +babel-plugin-polyfill-corejs3@^0.10.4: version "0.10.4" resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.4.tgz#789ac82405ad664c20476d0233b485281deb9c77" integrity sha512-25J6I8NGfa5YkCDogHRID3fVCadIR8/pGl1/spvCkzb6lVn6SR3ojpx9nOn9iEBcUsjY24AmdKm5khcfKdylcg== @@ -3119,6 +5018,14 @@ babel-plugin-polyfill-corejs3@^0.10.1, babel-plugin-polyfill-corejs3@^0.10.4: "@babel/helper-define-polyfill-provider" "^0.6.1" core-js-compat "^3.36.1" +babel-plugin-polyfill-corejs3@^0.11.0: + version "0.11.1" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.11.1.tgz#4e4e182f1bb37c7ba62e2af81d8dd09df31344f6" + integrity sha512-yGCqvBT4rwMczo28xkH/noxJ6MZ4nJfkVYdoDaC/utLtWrXxv27HVrzAeSbqR8SxDsp46n0YF47EbHoixy6rXQ== + dependencies: + "@babel/helper-define-polyfill-provider" "^0.6.3" + core-js-compat "^3.40.0" + babel-plugin-polyfill-regenerator@^0.6.1: version "0.6.2" resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.2.tgz#addc47e240edd1da1058ebda03021f382bba785e" @@ -3165,16 +5072,6 @@ bl@^4.0.3: inherits "^2.0.4" readable-stream "^3.4.0" -bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.11.9: - version "4.12.0" - resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88" - integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA== - -bn.js@^5.0.0, bn.js@^5.2.1: - version "5.2.1" - resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.1.tgz#0bc527a6a0d18d0aa8d5b0538ce4a77dccfa7b70" - integrity sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ== - body-parser@1.20.2: version "1.20.2" resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.2.tgz#6feb0e21c4724d06de7ff38da36dad4f57a747fd" @@ -3256,74 +5153,7 @@ braces@^3.0.3, braces@~3.0.2: dependencies: fill-range "^7.1.1" -brorand@^1.0.1, brorand@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" - integrity sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w== - -browserify-aes@^1.0.4, browserify-aes@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.2.0.tgz#326734642f403dabc3003209853bb70ad428ef48" - integrity sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA== - dependencies: - buffer-xor "^1.0.3" - cipher-base "^1.0.0" - create-hash "^1.1.0" - evp_bytestokey "^1.0.3" - inherits "^2.0.1" - safe-buffer "^5.0.1" - -browserify-cipher@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/browserify-cipher/-/browserify-cipher-1.0.1.tgz#8d6474c1b870bfdabcd3bcfcc1934a10e94f15f0" - integrity sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w== - dependencies: - browserify-aes "^1.0.4" - browserify-des "^1.0.0" - evp_bytestokey "^1.0.0" - -browserify-des@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/browserify-des/-/browserify-des-1.0.2.tgz#3af4f1f59839403572f1c66204375f7a7f703e9c" - integrity sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A== - dependencies: - cipher-base "^1.0.1" - des.js "^1.0.0" - inherits "^2.0.1" - safe-buffer "^5.1.2" - -browserify-rsa@^4.0.0, browserify-rsa@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/browserify-rsa/-/browserify-rsa-4.1.0.tgz#b2fd06b5b75ae297f7ce2dc651f918f5be158c8d" - integrity sha512-AdEER0Hkspgno2aR97SAf6vi0y0k8NuOpGnVH3O99rcA5Q6sh8QxcngtHuJ6uXwnfAXNM4Gn1Gb7/MV1+Ymbog== - dependencies: - bn.js "^5.0.0" - randombytes "^2.0.1" - -browserify-sign@^4.0.0: - version "4.2.3" - resolved "https://registry.yarnpkg.com/browserify-sign/-/browserify-sign-4.2.3.tgz#7afe4c01ec7ee59a89a558a4b75bd85ae62d4208" - integrity sha512-JWCZW6SKhfhjJxO8Tyiiy+XYB7cqd2S5/+WeYHsKdNKFlCBhKbblba1A/HN/90YwtxKc8tCErjffZl++UNmGiw== - dependencies: - bn.js "^5.2.1" - browserify-rsa "^4.1.0" - create-hash "^1.2.0" - create-hmac "^1.1.7" - elliptic "^6.5.5" - hash-base "~3.0" - inherits "^2.0.4" - parse-asn1 "^5.1.7" - readable-stream "^2.3.8" - safe-buffer "^5.2.1" - -browserify-zlib@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/browserify-zlib/-/browserify-zlib-0.2.0.tgz#2869459d9aa3be245fe8fe2ca1f46e2e7f54d73f" - integrity sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA== - dependencies: - pako "~1.0.5" - -browserslist@^4.0.0, browserslist@^4.18.1, browserslist@^4.21.10, browserslist@^4.22.2, browserslist@^4.23.0: +browserslist@^4.0.0, browserslist@^4.21.10, browserslist@^4.22.2, browserslist@^4.23.0: version "4.23.1" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.23.1.tgz#ce4af0534b3d37db5c1a4ca98b9080f985041e96" integrity sha512-TUfofFo/KsK/bWZ9TWQ5O26tsWW4Uhmt8IYklbnUa70udB6P2wA7w7o4PY4muaEPBQaAX+CEnmmIA41NVHtPVw== @@ -3333,6 +5163,16 @@ browserslist@^4.0.0, browserslist@^4.18.1, browserslist@^4.21.10, browserslist@^ node-releases "^2.0.14" update-browserslist-db "^1.0.16" +browserslist@^4.24.0, browserslist@^4.24.2, browserslist@^4.24.4, browserslist@^4.24.5: + version "4.24.5" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.24.5.tgz#aa0f5b8560fe81fde84c6dcb38f759bafba0e11b" + integrity sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw== + dependencies: + caniuse-lite "^1.0.30001716" + electron-to-chromium "^1.5.149" + node-releases "^2.0.19" + update-browserslist-db "^1.1.3" + buffer-crc32@~0.2.3: version "0.2.13" resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" @@ -3343,11 +5183,6 @@ buffer-from@^1.0.0: resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== -buffer-xor@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9" - integrity sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ== - buffer@^5.2.1, buffer@^5.5.0: version "5.7.1" resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" @@ -3364,11 +5199,6 @@ buffer@^6.0.3: base64-js "^1.3.1" ieee754 "^1.2.1" -builtin-status-codes@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8" - integrity sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ== - bytes@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" @@ -3397,7 +5227,15 @@ cacheable-request@^10.2.8: normalize-url "^8.0.0" responselike "^3.0.0" -call-bind@^1.0.0, call-bind@^1.0.2, call-bind@^1.0.5, call-bind@^1.0.7: +call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6" + integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ== + dependencies: + es-errors "^1.3.0" + function-bind "^1.1.2" + +call-bind@^1.0.5, call-bind@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w== @@ -3408,6 +5246,14 @@ call-bind@^1.0.0, call-bind@^1.0.2, call-bind@^1.0.5, call-bind@^1.0.7: get-intrinsic "^1.2.4" set-function-length "^1.2.1" +call-bound@^1.0.2: + version "1.0.4" + resolved "https://registry.yarnpkg.com/call-bound/-/call-bound-1.0.4.tgz#238de935d2a2a692928c538c7ccfa91067fd062a" + integrity sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg== + dependencies: + call-bind-apply-helpers "^1.0.2" + get-intrinsic "^1.3.0" + call-me-maybe@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/call-me-maybe/-/call-me-maybe-1.0.2.tgz#03f964f19522ba643b1b0693acb9152fe2074baa" @@ -3456,6 +5302,11 @@ caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001599, caniuse-lite@^1.0.30001629: resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001702.tgz" integrity sha512-LoPe/D7zioC0REI5W73PeR1e1MLCipRGq/VkovJnd6Df+QVqT+vT33OXCp8QUd7kA7RZrHWxb1B36OQKI/0gOA== +caniuse-lite@^1.0.30001702, caniuse-lite@^1.0.30001716, caniuse-lite@^1.0.30001718: + version "1.0.30001718" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001718.tgz#dae13a9c80d517c30c6197515a96131c194d8f82" + integrity sha512-AflseV1ahcSunK53NfEs9gFWgOEmzr0f+kaMFA4xiLZlr9Hzt7HxcSpIFcnNCUkz6R6dWKa54rUz3HUmI3nVcw== + ccount@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/ccount/-/ccount-2.0.1.tgz#17a3bf82302e0870d6da43a01311a8bc02a3ecf5" @@ -3470,7 +5321,7 @@ chalk@^2.4.2: escape-string-regexp "^1.0.5" supports-color "^5.3.0" -chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.2: +chalk@^4.0.0, chalk@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== @@ -3525,7 +5376,7 @@ cheerio-select@^2.1.0: domhandler "^5.0.3" domutils "^3.0.1" -cheerio@^1.0.0-rc.12: +cheerio@1.0.0-rc.12: version "1.0.0-rc.12" resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.12.tgz#788bf7466506b1c6bf5fae51d24a2c4d62e47683" integrity sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q== @@ -3538,7 +5389,26 @@ cheerio@^1.0.0-rc.12: parse5 "^7.0.0" parse5-htmlparser2-tree-adapter "^7.0.0" -"chokidar@>=3.0.0 <4.0.0", chokidar@^3.4.2, chokidar@^3.5.3: +chevrotain-allstar@~0.3.0: + version "0.3.1" + resolved "https://registry.yarnpkg.com/chevrotain-allstar/-/chevrotain-allstar-0.3.1.tgz#b7412755f5d83cc139ab65810cdb00d8db40e6ca" + integrity sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw== + dependencies: + lodash-es "^4.17.21" + +chevrotain@~11.0.3: + version "11.0.3" + resolved "https://registry.yarnpkg.com/chevrotain/-/chevrotain-11.0.3.tgz#88ffc1fb4b5739c715807eaeedbbf200e202fc1b" + integrity sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw== + dependencies: + "@chevrotain/cst-dts-gen" "11.0.3" + "@chevrotain/gast" "11.0.3" + "@chevrotain/regexp-to-ast" "11.0.3" + "@chevrotain/types" "11.0.3" + "@chevrotain/utils" "11.0.3" + lodash-es "4.17.21" + +chokidar@^3.5.3: version "3.6.0" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== @@ -3553,6 +5423,13 @@ cheerio@^1.0.0-rc.12: optionalDependencies: fsevents "~2.3.2" +chokidar@^4.0.0: + version "4.0.3" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-4.0.3.tgz#7be37a4c03c9aee1ecfe862a4a23b2c70c205d30" + integrity sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA== + dependencies: + readdirp "^4.0.1" + chownr@^1.1.1: version "1.1.4" resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" @@ -3568,14 +5445,6 @@ ci-info@^3.2.0: resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.9.0.tgz#4279a62028a7b1f262f3473fc9605f5e218c59b4" integrity sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ== -cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.4.tgz#8760e4ecc272f4c363532f926d874aae2c1397de" - integrity sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q== - dependencies: - inherits "^2.0.1" - safe-buffer "^5.0.1" - clean-css@^5.2.2, clean-css@^5.3.2, clean-css@~5.3.2: version "5.3.3" resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-5.3.3.tgz#b330653cd3bd6b75009cc25c714cae7b93351ccd" @@ -3768,6 +5637,16 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== +confbox@^0.1.8: + version "0.1.8" + resolved "https://registry.yarnpkg.com/confbox/-/confbox-0.1.8.tgz#820d73d3b3c82d9bd910652c5d4d599ef8ff8b06" + integrity sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w== + +confbox@^0.2.1: + version "0.2.2" + resolved "https://registry.yarnpkg.com/confbox/-/confbox-0.2.2.tgz#8652f53961c74d9e081784beed78555974a9c110" + integrity sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ== + config-chain@^1.1.11: version "1.1.13" resolved "https://registry.yarnpkg.com/config-chain/-/config-chain-1.1.13.tgz#fad0795aa6a6cdaff9ed1b68e9dff94372c232f4" @@ -3792,20 +5671,10 @@ connect-history-api-fallback@^2.0.0: resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz#647264845251a0daf25b97ce87834cace0f5f1c8" integrity sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA== -consola@^2.15.3: - version "2.15.3" - resolved "https://registry.yarnpkg.com/consola/-/consola-2.15.3.tgz#2e11f98d6a4be71ff72e0bdf07bd23e12cb61550" - integrity sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw== - -console-browserify@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.2.0.tgz#67063cef57ceb6cf4993a2ab3a55840ae8c49336" - integrity sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA== - -constants-browserify@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75" - integrity sha512-xFxOwqIzR/e1k1gLiWEophSCMqXcwVHIH7akf7b/vxcUeGunlj3hvZaaqxwHsTgn+IndtkQJgSztIDWeumWJDQ== +consola@^3.2.3: + version "3.4.2" + resolved "https://registry.yarnpkg.com/consola/-/consola-3.4.2.tgz#5af110145397bb67afdab77013fdc34cae590ea7" + integrity sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA== content-disposition@0.5.2: version "0.5.2" @@ -3870,6 +5739,13 @@ core-js-compat@^3.31.0, core-js-compat@^3.36.1: dependencies: browserslist "^4.23.0" +core-js-compat@^3.40.0: + version "3.42.0" + resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.42.0.tgz#ce19c29706ee5806e26d3cb3c542d4cfc0ed51bb" + integrity sha512-bQasjMfyDGyaeWKBIu33lHh9qlSR0MFE/Nmc6nMjf/iU9b3rSMdAYz1Baxrv4lPdGUsTqZudHA4jIGSJy0SWZQ== + dependencies: + browserslist "^4.24.4" + core-js-pure@^3.30.2: version "3.37.1" resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.37.1.tgz#2b4b34281f54db06c9a9a5bd60105046900553bd" @@ -3892,16 +5768,12 @@ cose-base@^1.0.0: dependencies: layout-base "^1.0.0" -cosmiconfig@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-6.0.0.tgz#da4fee853c52f6b1e6935f41c1a2fc50bd4a9982" - integrity sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg== +cose-base@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/cose-base/-/cose-base-2.2.0.tgz#1c395c35b6e10bb83f9769ca8b817d614add5c01" + integrity sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g== dependencies: - "@types/parse-json" "^4.0.0" - import-fresh "^3.1.0" - parse-json "^5.0.0" - path-type "^4.0.0" - yaml "^1.7.2" + layout-base "^2.0.0" cosmiconfig@^8.1.3, cosmiconfig@^8.3.5: version "8.3.6" @@ -3913,37 +5785,6 @@ cosmiconfig@^8.1.3, cosmiconfig@^8.3.5: parse-json "^5.2.0" path-type "^4.0.0" -create-ecdh@^4.0.0: - version "4.0.4" - resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.4.tgz#d6e7f4bffa66736085a0762fd3a632684dabcc4e" - integrity sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A== - dependencies: - bn.js "^4.1.0" - elliptic "^6.5.3" - -create-hash@^1.1.0, create-hash@^1.1.2, create-hash@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.2.0.tgz#889078af11a63756bcfb59bd221996be3a9ef196" - integrity sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg== - dependencies: - cipher-base "^1.0.1" - inherits "^2.0.1" - md5.js "^1.3.4" - ripemd160 "^2.0.1" - sha.js "^2.4.0" - -create-hmac@^1.1.0, create-hmac@^1.1.4, create-hmac@^1.1.7: - version "1.1.7" - resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.7.tgz#69170c78b3ab957147b2b8b04572e47ead2243ff" - integrity sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg== - dependencies: - cipher-base "^1.0.3" - create-hash "^1.1.0" - inherits "^2.0.1" - ripemd160 "^2.0.0" - safe-buffer "^5.0.1" - sha.js "^2.4.8" - cross-fetch@3.1.5: version "3.1.5" resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.5.tgz#e1389f44d9e7ba767907f7af8454787952ab534f" @@ -3960,23 +5801,6 @@ cross-spawn@^7.0.0, cross-spawn@^7.0.3: shebang-command "^2.0.0" which "^2.0.1" -crypto-browserify@^3.12.0: - version "3.12.0" - resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec" - integrity sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg== - dependencies: - browserify-cipher "^1.0.0" - browserify-sign "^4.0.0" - create-ecdh "^4.0.0" - create-hash "^1.1.0" - create-hmac "^1.1.0" - diffie-hellman "^5.0.0" - inherits "^2.0.1" - pbkdf2 "^3.0.3" - public-encrypt "^4.0.0" - randombytes "^2.0.0" - randomfill "^1.0.3" - crypto-js@^4.1.1: version "4.2.0" resolved "https://registry.yarnpkg.com/crypto-js/-/crypto-js-4.2.0.tgz#4d931639ecdfd12ff80e8186dba6af2c2e856631" @@ -3989,11 +5813,27 @@ crypto-random-string@^4.0.0: dependencies: type-fest "^1.0.1" +css-blank-pseudo@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/css-blank-pseudo/-/css-blank-pseudo-7.0.1.tgz#32020bff20a209a53ad71b8675852b49e8d57e46" + integrity sha512-jf+twWGDf6LDoXDUode+nc7ZlrqfaNphrBIBrcmeP3D8yw1uPaix1gCC8LUQUGQ6CycuK2opkbFFWFuq/a94ag== + dependencies: + postcss-selector-parser "^7.0.0" + css-declaration-sorter@^7.2.0: version "7.2.0" resolved "https://registry.yarnpkg.com/css-declaration-sorter/-/css-declaration-sorter-7.2.0.tgz#6dec1c9523bc4a643e088aab8f09e67a54961024" integrity sha512-h70rUM+3PNFuaBDTLe8wF/cdWu+dOZmb7pJt8Z2sedYbAcQVQV/tEchueg3GWxwqS0cxtbxmaHEdkNACqcvsow== +css-has-pseudo@^7.0.2: + version "7.0.2" + resolved "https://registry.yarnpkg.com/css-has-pseudo/-/css-has-pseudo-7.0.2.tgz#fb42e8de7371f2896961e1f6308f13c2c7019b72" + integrity sha512-nzol/h+E0bId46Kn2dQH5VElaknX2Sr0hFuB/1EomdC7j+OISt2ZzK7EHX9DZDY53WbIVAR7FYKSO2XnSf07MQ== + dependencies: + "@csstools/selector-specificity" "^5.0.0" + postcss-selector-parser "^7.0.0" + postcss-value-parser "^4.2.0" + css-loader@^6.8.1: version "6.11.0" resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-6.11.0.tgz#33bae3bf6363d0a7c2cf9031c96c744ff54d85ba" @@ -4020,6 +5860,11 @@ css-minimizer-webpack-plugin@^5.0.1: schema-utils "^4.0.1" serialize-javascript "^6.0.1" +css-prefers-color-scheme@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/css-prefers-color-scheme/-/css-prefers-color-scheme-10.0.0.tgz#ba001b99b8105b8896ca26fc38309ddb2278bd3c" + integrity sha512-VCtXZAWivRglTZditUfB4StnsWr6YVZ2PRtuxQLKTNRdtAf8tpzaVPE9zXIF3VaSc7O70iK/j1+NXxyQCqdPjQ== + css-select@^4.1.3: version "4.3.0" resolved "https://registry.yarnpkg.com/css-select/-/css-select-4.3.0.tgz#db7129b2846662fd8628cfc496abb2b59e41529b" @@ -4063,6 +5908,11 @@ css-what@^6.0.1, css-what@^6.1.0: resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4" integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw== +cssdb@^8.3.0: + version "8.3.0" + resolved "https://registry.yarnpkg.com/cssdb/-/cssdb-8.3.0.tgz#940becad497b8509ad822a28fb0cfe54c969ccfe" + integrity sha512-c7bmItIg38DgGjSwDPZOYF/2o0QU/sSgkWOMyl8votOfgFuyiFKWPesmCGEsrGLxEA9uL540cp8LdaGEjUGsZQ== + cssesc@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" @@ -4149,10 +5999,17 @@ cytoscape-cose-bilkent@^4.1.0: dependencies: cose-base "^1.0.0" -cytoscape@^3.28.1: - version "3.29.2" - resolved "https://registry.yarnpkg.com/cytoscape/-/cytoscape-3.29.2.tgz#c99f42513c80a75e2e94858add32896c860202ac" - integrity sha512-2G1ycU28Nh7OHT9rkXRLpCDP30MKH1dXJORZuBhtEhEW7pKwgPi77ImqlCWinouyE1PNepIOGZBOrE84DG7LyQ== +cytoscape-fcose@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/cytoscape-fcose/-/cytoscape-fcose-2.2.0.tgz#e4d6f6490df4fab58ae9cea9e5c3ab8d7472f471" + integrity sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ== + dependencies: + cose-base "^2.2.0" + +cytoscape@^3.29.3: + version "3.32.0" + resolved "https://registry.yarnpkg.com/cytoscape/-/cytoscape-3.32.0.tgz#34bc2402c9bc7457ab7d9492745f034b7bf47644" + integrity sha512-5JHBC9n75kz5851jeklCPmZWcg3hUe6sjqJvyk3+hVqFaKcHwHgxsjeN1yLmggoUc6STbtm9/NQyabQehfjvWQ== "d3-array@1 - 2": version "2.12.1" @@ -4389,7 +6246,7 @@ d3-zoom@3: d3-selection "2 - 3" d3-transition "2 - 3" -d3@^7.4.0, d3@^7.8.2: +d3@^7.9.0: version "7.9.0" resolved "https://registry.yarnpkg.com/d3/-/d3-7.9.0.tgz#579e7acb3d749caf8860bd1741ae8d371070cd5d" integrity sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA== @@ -4425,25 +6282,25 @@ d3@^7.4.0, d3@^7.8.2: d3-transition "3" d3-zoom "3" -dagre-d3-es@7.0.10: - version "7.0.10" - resolved "https://registry.yarnpkg.com/dagre-d3-es/-/dagre-d3-es-7.0.10.tgz#19800d4be674379a3cd8c86a8216a2ac6827cadc" - integrity sha512-qTCQmEhcynucuaZgY5/+ti3X/rnszKZhEQH/ZdWdtP1tA/y3VoHJzcVrO9pjjJCNpigfscAtoUB5ONcd2wNn0A== +dagre-d3-es@7.0.11: + version "7.0.11" + resolved "https://registry.yarnpkg.com/dagre-d3-es/-/dagre-d3-es-7.0.11.tgz#2237e726c0577bfe67d1a7cfd2265b9ab2c15c40" + integrity sha512-tvlJLyQf834SylNKax8Wkzco/1ias1OPw8DcUMDE7oUIoSEW25riQVuiu/0OWEFqT0cxHT3Pa9/D82Jr47IONw== dependencies: - d3 "^7.8.2" + d3 "^7.9.0" lodash-es "^4.17.21" -dayjs@^1.11.7: - version "1.11.11" - resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.11.tgz#dfe0e9d54c5f8b68ccf8ca5f72ac603e7e5ed59e" - integrity sha512-okzr3f11N6WuqYtZSvm+F776mB41wRZMhKP+hc34YdW+KmtYYK9iqvHSwo2k9FEH3fhGXvOPV6yz2IcSrfRUDg== +dayjs@^1.11.13: + version "1.11.13" + resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.13.tgz#92430b0139055c3ebb60150aa13e860a4b5a366c" + integrity sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg== debounce@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/debounce/-/debounce-1.2.1.tgz#38881d8f4166a5c5848020c11827b834bcb3e0a5" integrity sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug== -debug@2.6.9, debug@^2.6.0: +debug@2.6.9: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== @@ -4464,6 +6321,13 @@ debug@4.3.4: dependencies: ms "2.1.2" +debug@^4.4.0: + version "4.4.1" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.1.tgz#e5a8bc6cbc4c6cd3e64308b0693a3d4fa550189b" + integrity sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ== + dependencies: + ms "^2.1.3" + decode-named-character-reference@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz#daabac9690874c394c81e4162a0304b35d824f0e" @@ -4483,7 +6347,7 @@ deep-extend@^0.6.0: resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== -deepmerge@^4.0.0, deepmerge@^4.2.2, deepmerge@^4.3.1: +deepmerge@^4.0.0, deepmerge@^4.3.1: version "4.3.1" resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a" integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A== @@ -4514,7 +6378,7 @@ define-lazy-prop@^2.0.0: resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz#3f7ae421129bcaaac9bc74905c98a0009ec9ee7f" integrity sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og== -define-properties@^1.1.3, define-properties@^1.2.1: +define-properties@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.1.tgz#10781cc616eb951a80a034bafcaa7377f6af2b6c" integrity sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg== @@ -4523,20 +6387,6 @@ define-properties@^1.1.3, define-properties@^1.2.1: has-property-descriptors "^1.0.0" object-keys "^1.1.1" -del@^6.1.1: - version "6.1.1" - resolved "https://registry.yarnpkg.com/del/-/del-6.1.1.tgz#3b70314f1ec0aa325c6b14eb36b95786671edb7a" - integrity sha512-ua8BhapfP0JUJKC/zV9yHHDW/rDoDxP4Zhn3AkA6/xT6gY7jYXJiaeyBZznYVujhZZET+UgcbZiQ7sN3WqcImg== - dependencies: - globby "^11.0.1" - graceful-fs "^4.2.4" - is-glob "^4.0.1" - is-path-cwd "^2.2.0" - is-path-inside "^3.0.2" - p-map "^4.0.0" - rimraf "^3.0.2" - slash "^3.0.0" - delaunator@5: version "5.0.1" resolved "https://registry.yarnpkg.com/delaunator/-/delaunator-5.0.1.tgz#39032b08053923e924d6094fe2cde1a99cc51278" @@ -4559,32 +6409,26 @@ dequal@^2.0.0: resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be" integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== -des.js@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.1.0.tgz#1d37f5766f3bbff4ee9638e871a8768c173b81da" - integrity sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg== - dependencies: - inherits "^2.0.1" - minimalistic-assert "^1.0.0" - destroy@1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== +detect-libc@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" + integrity sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg== + +detect-libc@^2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.4.tgz#f04715b8ba815e53b4d8109655b6508a6865a7e8" + integrity sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA== + detect-node@^2.0.4: version "2.1.0" resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.1.0.tgz#c9c70775a49c3d03bc2c06d9a73be550f978f8b1" integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g== -detect-port-alt@^1.1.6: - version "1.1.6" - resolved "https://registry.yarnpkg.com/detect-port-alt/-/detect-port-alt-1.1.6.tgz#24707deabe932d4a3cf621302027c2b266568275" - integrity sha512-5tQykt+LqfJFBEYaDITx7S7cR7mJ/zQmLXZ2qt5w04ainYZw6tBf9dBunMjVeVOdYVRUzUOE4HkY5J7+uttb5Q== - dependencies: - address "^1.0.1" - debug "^2.6.0" - detect-port@^1.5.1: version "1.6.1" resolved "https://registry.yarnpkg.com/detect-port/-/detect-port-1.6.1.tgz#45e4073997c5f292b957cb678fb0bb8ed4250a67" @@ -4615,15 +6459,6 @@ diff@^5.0.0: resolved "https://registry.yarnpkg.com/diff/-/diff-5.2.0.tgz#26ded047cd1179b78b9537d5ef725503ce1ae531" integrity sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A== -diffie-hellman@^5.0.0: - version "5.0.3" - resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875" - integrity sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg== - dependencies: - bn.js "^4.1.0" - miller-rabin "^4.0.0" - randombytes "^2.0.0" - dir-glob@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" @@ -4643,29 +6478,26 @@ dns-packet@^5.2.2: dependencies: "@leichtgewicht/ip-codec" "^2.0.1" -docusaurus-plugin-image-zoom@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/docusaurus-plugin-image-zoom/-/docusaurus-plugin-image-zoom-1.0.1.tgz#17afec39f2e630cac50a4ed3a8bbdad8d0aa8b9d" - integrity sha512-96IpSKUx2RWy3db9aZ0s673OQo5DWgV9UVWouS+CPOSIVEdCWh6HKmWf6tB9rsoaiIF3oNn9keiyv6neEyKb1Q== +docusaurus-plugin-image-zoom@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/docusaurus-plugin-image-zoom/-/docusaurus-plugin-image-zoom-3.0.1.tgz#76095fdc288b58d351d19bf902bd3c0a3113ec09" + integrity sha512-mQrqA99VpoMQJNbi02qkWAMVNC4+kwc6zLLMNzraHAJlwn+HrlUmZSEDcTwgn+H4herYNxHKxveE2WsYy73eGw== dependencies: - medium-zoom "^1.0.6" + medium-zoom "^1.1.0" validate-peer-dependencies "^2.2.0" -docusaurus-plugin-openapi-docs@3.0.1, docusaurus-plugin-openapi-docs@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/docusaurus-plugin-openapi-docs/-/docusaurus-plugin-openapi-docs-3.0.1.tgz#954fdc4103d7e47133aede210a98353b3e0f0f99" - integrity sha512-6SRqwey/TXMNu2G02mbWgxrifhpjGOjDr30N+58AR0Ytgc+HXMqlPAUIvTe+e7sOBfAtBbiNlmOWv5KSYIjf3w== +docusaurus-plugin-openapi-docs@4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/docusaurus-plugin-openapi-docs/-/docusaurus-plugin-openapi-docs-4.4.0.tgz#010b2bfc57aeac1b62c41bf1ef386dcc52c5e91f" + integrity sha512-VFW0euAyM6i6U6Q2WrNXkp1LnxQFGszZbmloMFYrs1qwBjPLkuHfQ4OJMXGDsGcGl4zNDJ9cwODmJlmdwl1hwg== dependencies: "@apidevtools/json-schema-ref-parser" "^11.5.4" - "@docusaurus/plugin-content-docs" "^3.0.1" - "@docusaurus/utils" "^3.0.1" - "@docusaurus/utils-validation" "^3.0.1" "@redocly/openapi-core" "^1.10.5" + allof-merge "^0.6.6" chalk "^4.1.2" clsx "^1.1.1" fs-extra "^9.0.1" json-pointer "^0.6.2" - json-schema-merge-allof "^0.8.1" json5 "^2.2.3" lodash "^4.17.20" mustache "^4.2.0" @@ -4675,13 +6507,6 @@ docusaurus-plugin-openapi-docs@3.0.1, docusaurus-plugin-openapi-docs@^3.0.1: swagger2openapi "^7.0.8" xml-formatter "^2.6.1" -docusaurus-plugin-sass@^0.2.3: - version "0.2.5" - resolved "https://registry.yarnpkg.com/docusaurus-plugin-sass/-/docusaurus-plugin-sass-0.2.5.tgz#6bfb8a227ac6265be685dcbc24ba1989e27b8005" - integrity sha512-Z+D0fLFUKcFpM+bqSUmqKIU+vO+YF1xoEQh5hoFreg2eMf722+siwXDD+sqtwU8E4MvVpuvsQfaHwODNlxJAEg== - dependencies: - sass-loader "^10.1.1" - docusaurus-theme-github-codeblock@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/docusaurus-theme-github-codeblock/-/docusaurus-theme-github-codeblock-2.0.2.tgz#88b7044b81f9091330e8e4a07a1bdc9114a9fb93" @@ -4689,25 +6514,25 @@ docusaurus-theme-github-codeblock@^2.0.2: dependencies: "@docusaurus/types" "^3.0.0" -docusaurus-theme-openapi-docs@3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/docusaurus-theme-openapi-docs/-/docusaurus-theme-openapi-docs-3.0.1.tgz#49789c63377f294e624a9632eddb8265a421020f" - integrity sha512-tqypV91tC3wuWj9O+4n0M/e5AgHOeMT2nvPj1tjlPkC7/dLinZvpwQStT4YDUPYSoHRseqxd7lhivFQHcmlryg== +docusaurus-theme-openapi-docs@4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/docusaurus-theme-openapi-docs/-/docusaurus-theme-openapi-docs-4.4.0.tgz#601eb34d43fa49c6fe1418f3fed06e3044ce377f" + integrity sha512-wmc2b946rqBcdjgEHi6Up7e8orasYk5RnIUerTfmZ/Hi006I8FIjMnJEmHAF6t5PbFiiYnlkB6vYK0CC5xBnCQ== dependencies: - "@docusaurus/theme-common" "^3.0.1" "@hookform/error-message" "^2.0.1" "@reduxjs/toolkit" "^1.7.1" + allof-merge "^0.6.6" + buffer "^6.0.3" clsx "^1.1.1" copy-text-to-clipboard "^3.1.0" crypto-js "^4.1.1" - docusaurus-plugin-openapi-docs "^3.0.1" - docusaurus-plugin-sass "^0.2.3" file-saver "^2.0.5" lodash "^4.17.20" - node-polyfill-webpack-plugin "^2.0.1" + pako "^2.1.0" postman-code-generators "^1.10.1" postman-collection "^4.4.0" prism-react-renderer "^2.3.0" + process "^0.11.10" react-hook-form "^7.43.8" react-live "^4.0.0" react-magic-dropzone "^1.0.1" @@ -4715,9 +6540,11 @@ docusaurus-theme-openapi-docs@3.0.1: react-modal "^3.15.1" react-redux "^7.2.0" rehype-raw "^6.1.1" - sass "^1.58.1" - sass-loader "^13.3.2" - webpack "^5.61.0" + remark-gfm "3.0.1" + sass "^1.80.4" + sass-loader "^16.0.2" + unist-util-visit "^5.0.0" + url "^0.11.1" xml-formatter "^2.6.1" dom-converter@^0.2.0: @@ -4745,11 +6572,6 @@ dom-serializer@^2.0.0: domhandler "^5.0.2" entities "^4.2.0" -domain-browser@^4.22.0: - version "4.23.0" - resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-4.23.0.tgz#427ebb91efcb070f05cffdfb8a4e9a6c25f8c94b" - integrity sha512-ArzcM/II1wCCujdCNyQjXrAFwS4mrLh4C7DZWlaI8mdh7h3BfKdNd3bKXITfl2PT9FtfQqaGvhi1vPRQPimjGA== - domelementtype@^2.0.1, domelementtype@^2.2.0, domelementtype@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d" @@ -4769,10 +6591,12 @@ domhandler@^5.0.2, domhandler@^5.0.3: dependencies: domelementtype "^2.3.0" -dompurify@^3.0.5: - version "3.1.5" - resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.1.5.tgz#2c6a113fc728682a0f55684b1388c58ddb79dc38" - integrity sha512-lwG+n5h8QNpxtyrJW/gJWckL+1/DQiYMX8f7t8Z2AZTPw1esVrqjI63i7Zc2Gz0aKzLVMYC1V1PL/ky+aY/NgA== +dompurify@^3.2.4: + version "3.2.6" + resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.2.6.tgz#ca040a6ad2b88e2a92dc45f38c79f84a714a1cad" + integrity sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ== + optionalDependencies: + "@types/trusted-types" "^2.0.7" domutils@^2.5.2, domutils@^2.8.0: version "2.8.0" @@ -4807,6 +6631,15 @@ dot-prop@^6.0.1: dependencies: is-obj "^2.0.0" +dunder-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a" + integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A== + dependencies: + call-bind-apply-helpers "^1.0.1" + es-errors "^1.3.0" + gopd "^1.2.0" + duplexer@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.2.tgz#3abe43aef3835f8ae077d136ddce0f276b0400e6" @@ -4827,23 +6660,10 @@ electron-to-chromium@^1.4.796: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.803.tgz#cf55808a5ee12e2a2778bbe8cdc941ef87c2093b" integrity sha512-61H9mLzGOCLLVsnLiRzCbc63uldP0AniRYPV3hbGVtONA1pI7qSGILdbofR7A8TMbOypDocEAjH/e+9k1QIe3g== -elkjs@^0.9.0: - version "0.9.3" - resolved "https://registry.yarnpkg.com/elkjs/-/elkjs-0.9.3.tgz#16711f8ceb09f1b12b99e971b138a8384a529161" - integrity sha512-f/ZeWvW/BCXbhGEf1Ujp29EASo/lk1FDnETgNKwJrsVvGZhUWCZyg3xLJjAsxfOmt8KjswHmI5EwCQcPMpOYhQ== - -elliptic@^6.5.3, elliptic@^6.5.5: - version "6.5.7" - resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.7.tgz#8ec4da2cb2939926a1b9a73619d768207e647c8b" - integrity sha512-ESVCtTwiA+XhY3wyh24QqRGBoP3rEdDUl3EDUUo9tft074fi19IrdpH7hLCMMP3CIj7jb3W96rn8lt/BqIlt5Q== - dependencies: - bn.js "^4.11.9" - brorand "^1.1.0" - hash.js "^1.0.0" - hmac-drbg "^1.0.1" - inherits "^2.0.4" - minimalistic-assert "^1.0.1" - minimalistic-crypto-utils "^1.0.1" +electron-to-chromium@^1.5.149: + version "1.5.158" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.158.tgz#e5f01fc7fdf810d9d223e30593e0839c306276d4" + integrity sha512-9vcp2xHhkvraY6AHw2WMi+GDSLPX42qe2xjYaVoZqFRJiOcilVQFq9mZmpuHEQpzlgGDelKlV7ZiGcmMsc8WxQ== emoji-regex@^8.0.0: version "8.0.0" @@ -4890,6 +6710,14 @@ enhanced-resolve@^5.17.0: graceful-fs "^4.2.4" tapable "^2.2.0" +enhanced-resolve@^5.17.1: + version "5.18.1" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz#728ab082f8b7b6836de51f1637aab5d3b9568faf" + integrity sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg== + dependencies: + graceful-fs "^4.2.4" + tapable "^2.2.0" + entities@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55" @@ -4914,6 +6742,11 @@ es-define-property@^1.0.0: dependencies: get-intrinsic "^1.2.4" +es-define-property@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa" + integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g== + es-errors@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" @@ -4924,6 +6757,13 @@ es-module-lexer@^1.2.1: resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.5.3.tgz#25969419de9c0b1fbe54279789023e8a9a788412" integrity sha512-i1gCgmR9dCl6Vil6UKPI/trA69s08g/syhiDK9TG0Nf1RJjjFI+AzoWW7sPufzkgYAn861skuCwJa0pIIHYxvg== +es-object-atoms@^1.0.0, es-object-atoms@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz#1c4f2c4837327597ce69d2ca190a7fdd172338c1" + integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA== + dependencies: + es-errors "^1.3.0" + es6-promise@^3.2.1: version "3.3.1" resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-3.3.1.tgz#a08cdde84ccdbf34d027a1451bc91d4bcd28a613" @@ -4934,6 +6774,11 @@ escalade@^3.1.1, escalade@^3.1.2: resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.2.tgz#54076e9ab29ea5bf3d8f1ed62acffbb88272df27" integrity sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA== +escalade@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" + integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== + escape-goat@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/escape-goat/-/escape-goat-4.0.0.tgz#9424820331b510b0666b98f7873fe11ac4aa8081" @@ -5095,30 +6940,17 @@ eval@^0.1.8: "@types/node" "*" require-like ">= 0.1.1" -event-target-shim@^5.0.0: - version "5.0.1" - resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" - integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== - -eventemitter3@^4.0.0: +eventemitter3@^4.0.0, eventemitter3@^4.0.4: version "4.0.7" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== -events@^3.2.0, events@^3.3.0: +events@^3.2.0: version "3.3.0" resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== -evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz#7fcbdb198dc71959432efe13842684e0525acb02" - integrity sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA== - dependencies: - md5.js "^1.3.4" - safe-buffer "^5.1.1" - -execa@^5.0.0: +execa@5.1.1, execa@^5.0.0: version "5.1.1" resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg== @@ -5175,6 +7007,11 @@ express@^4.17.3: utils-merge "1.0.1" vary "~1.1.2" +exsolve@^1.0.1: + version "1.0.5" + resolved "https://registry.yarnpkg.com/exsolve/-/exsolve-1.0.5.tgz#1f5b6b4fe82ad6b28a173ccb955a635d77859dcf" + integrity sha512-pz5dvkYYKQ1AHVrgOzBKWeP4u4FRb3a6DNK2ucr0OoNwYIU4QWsJ+NM36LLzORT+z845MzKHHhpXiUF5nvQoJg== + extend-shallow@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f" @@ -5229,13 +7066,6 @@ fast-safe-stringify@^2.0.7: resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz#c406a83b6e70d9e35ce3b30a81141df30aeba884" integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA== -fast-url-parser@1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/fast-url-parser/-/fast-url-parser-1.1.3.tgz#f4af3ea9f34d8a271cf58ad2b3759f431f0b318d" - integrity sha512-5jOCVXADYNuRkKFzNJ0dCCewsZiYo0dz8QNYljkOpFC6r2U4OBmKtvm/Tsuh4w1YYdDqDb31a8TVhBJ2OJKdqQ== - dependencies: - punycode "^1.3.2" - fastq@^1.6.0: version "1.17.1" resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.17.1.tgz#2a523f07a4e7b1e81a42b91b8bf2254107753b47" @@ -5271,6 +7101,13 @@ feed@^4.2.2: dependencies: xml-js "^1.6.11" +figures@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af" + integrity sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg== + dependencies: + escape-string-regexp "^1.0.5" + file-loader@^6.2.0: version "6.2.0" resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-6.2.0.tgz#baef7cf8e1840df325e4390b4484879480eebe4d" @@ -5289,11 +7126,6 @@ file-type@3.9.0: resolved "https://registry.yarnpkg.com/file-type/-/file-type-3.9.0.tgz#257a078384d1db8087bc449d107d52a52672b9e9" integrity sha512-RLoqTXE8/vPmMuTI88DAzhMYC99I8BWv7zYP4A1puo5HIjEJ5EX48ighy4ZyKMG9EDXxBgW6e++cn7d1xuFghA== -filesize@^8.0.6: - version "8.0.7" - resolved "https://registry.yarnpkg.com/filesize/-/filesize-8.0.7.tgz#695e70d80f4e47012c132d57a059e80c6b580bd8" - integrity sha512-pjmC+bkIF8XI7fWaH8KxHcZL3DPybs1roSKP4rKDvy20tAWwIObE4+JIseG2byfGKhud5ZnM4YSGKBz7Sh0ndQ== - fill-range@^7.1.1: version "7.1.1" resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" @@ -5301,11 +7133,6 @@ fill-range@^7.1.1: dependencies: to-regex-range "^5.0.1" -filter-obj@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/filter-obj/-/filter-obj-2.0.2.tgz#fff662368e505d69826abb113f0f6a98f56e9d5f" - integrity sha512-lO3ttPjHZRfjMcxWKb1j1eDhTFsu4meeR3lnMcnBFhk6RuLhvEiuALu2TlfL310ph4lCYYwgF/ElIjdP739tdg== - finalhandler@1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.2.0.tgz#7d23fe5731b207b4640e4fcd00aec1f9207a7b32" @@ -5327,21 +7154,6 @@ find-cache-dir@^4.0.0: common-path-prefix "^3.0.0" pkg-dir "^7.0.0" -find-up@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" - integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg== - dependencies: - locate-path "^3.0.0" - -find-up@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" - integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== - dependencies: - locate-path "^6.0.0" - path-exists "^4.0.0" - find-up@^6.3.0: version "6.3.0" resolved "https://registry.yarnpkg.com/find-up/-/find-up-6.3.0.tgz#2abab3d3280b2dc7ac10199ef324c4e002c8c790" @@ -5355,18 +7167,11 @@ flat@^5.0.2: resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241" integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== -follow-redirects@^1.0.0, follow-redirects@^1.14.7: +follow-redirects@^1.0.0: version "1.15.6" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b" integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA== -for-each@^0.3.3: - version "0.3.3" - resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" - integrity sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw== - dependencies: - is-callable "^1.1.3" - foreach@^2.0.4: version "2.0.6" resolved "https://registry.yarnpkg.com/foreach/-/foreach-2.0.6.tgz#87bcc8a1a0e74000ff2bf9802110708cfb02eb6e" @@ -5380,25 +7185,6 @@ foreground-child@^3.1.0: cross-spawn "^7.0.0" signal-exit "^4.0.1" -fork-ts-checker-webpack-plugin@^6.5.0: - version "6.5.3" - resolved "https://registry.yarnpkg.com/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-6.5.3.tgz#eda2eff6e22476a2688d10661688c47f611b37f3" - integrity sha512-SbH/l9ikmMWycd5puHJKTkZJKddF4iRLyW3DeZ08HTI7NGyLS38MXd/KGgeWumQO7YNQbW2u/NtPT2YowbPaGQ== - dependencies: - "@babel/code-frame" "^7.8.3" - "@types/json-schema" "^7.0.5" - chalk "^4.1.0" - chokidar "^3.4.2" - cosmiconfig "^6.0.0" - deepmerge "^4.2.2" - fs-extra "^9.0.0" - glob "^7.1.6" - memfs "^3.1.2" - minimatch "^3.0.4" - schema-utils "2.7.0" - semver "^7.3.2" - tapable "^1.0.0" - form-data-encoder@^2.1.2: version "2.1.4" resolved "https://registry.yarnpkg.com/form-data-encoder/-/form-data-encoder-2.1.4.tgz#261ea35d2a70d48d30ec7a9603130fa5515e9cd5" @@ -5438,7 +7224,7 @@ fs-extra@^11.1.1, fs-extra@^11.2.0: jsonfile "^6.0.1" universalify "^2.0.0" -fs-extra@^9.0.0, fs-extra@^9.0.1: +fs-extra@^9.0.1: version "9.1.0" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d" integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ== @@ -5489,11 +7275,35 @@ get-intrinsic@^1.1.3, get-intrinsic@^1.2.4: has-symbols "^1.0.3" hasown "^2.0.0" +get-intrinsic@^1.2.5, get-intrinsic@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01" + integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ== + dependencies: + call-bind-apply-helpers "^1.0.2" + es-define-property "^1.0.1" + es-errors "^1.3.0" + es-object-atoms "^1.1.1" + function-bind "^1.1.2" + get-proto "^1.0.1" + gopd "^1.2.0" + has-symbols "^1.1.0" + hasown "^2.0.2" + math-intrinsics "^1.1.0" + get-own-enumerable-property-symbols@^3.0.0: version "3.0.2" resolved "https://registry.yarnpkg.com/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz#b5fde77f22cbe35f390b4e089922c50bce6ef664" integrity sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g== +get-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1" + integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g== + dependencies: + dunder-proto "^1.0.1" + es-object-atoms "^1.0.0" + get-stream@^5.1.0: version "5.2.0" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3" @@ -5541,7 +7351,7 @@ glob@^10.3.10: minipass "^7.1.2" path-scurry "^1.11.1" -glob@^7.0.0, glob@^7.1.3, glob@^7.1.6: +glob@^7.0.0, glob@^7.1.3: version "7.2.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== @@ -5560,28 +7370,17 @@ global-dirs@^3.0.0: dependencies: ini "2.0.0" -global-modules@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-2.0.0.tgz#997605ad2345f27f51539bea26574421215c7780" - integrity sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A== - dependencies: - global-prefix "^3.0.0" - -global-prefix@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-3.0.0.tgz#fc85f73064df69f50421f47f883fe5b913ba9b97" - integrity sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg== - dependencies: - ini "^1.3.5" - kind-of "^6.0.2" - which "^1.3.1" - globals@^11.1.0: version "11.12.0" resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== -globby@^11.0.1, globby@^11.0.4, globby@^11.1.0: +globals@^15.14.0: + version "15.15.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-15.15.0.tgz#7c4761299d41c32b075715a4ce1ede7897ff72a8" + integrity sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg== + +globby@^11.1.0: version "11.1.0" resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== @@ -5611,6 +7410,11 @@ gopd@^1.0.1: dependencies: get-intrinsic "^1.1.3" +gopd@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" + integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== + got@^12.1.0: version "12.6.1" resolved "https://registry.yarnpkg.com/got/-/got-12.6.1.tgz#8869560d1383353204b5a9435f782df9c091f549" @@ -5662,6 +7466,11 @@ gzip-size@^6.0.0: dependencies: duplexer "^0.1.2" +hachure-fill@^0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/hachure-fill/-/hachure-fill-0.5.2.tgz#d19bc4cc8750a5962b47fb1300557a85fcf934cc" + integrity sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg== + handle-thing@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.1.tgz#857f79ce359580c340d43081cc648970d0bb234e" @@ -5694,44 +7503,17 @@ has-symbols@^1.0.3: resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== -has-tostringtag@^1.0.0, has-tostringtag@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc" - integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw== - dependencies: - has-symbols "^1.0.3" +has-symbols@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338" + integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ== has-yarn@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/has-yarn/-/has-yarn-3.0.0.tgz#c3c21e559730d1d3b57e28af1f30d06fac38147d" integrity sha512-IrsVwUHhEULx3R8f/aA8AHuEzAorplsab/v8HBzEiIukwq5i/EC+xmOW+HfP1OaDP+2JkgT1yILHN2O3UFIbcA== -hash-base@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.1.0.tgz#55c381d9e06e1d2997a883b4a3fddfe7f0d3af33" - integrity sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA== - dependencies: - inherits "^2.0.4" - readable-stream "^3.6.0" - safe-buffer "^5.2.0" - -hash-base@~3.0: - version "3.0.4" - resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.0.4.tgz#5fc8686847ecd73499403319a6b0a3f3f6ae4918" - integrity sha512-EeeoJKjTyt868liAlVmcv2ZsUfGHlE3Q+BICOXcZiwN3osr5Q/zFGYmTJpoIzuaSTAwndFy+GqhEwlU4L3j4Ow== - dependencies: - inherits "^2.0.1" - safe-buffer "^5.0.1" - -hash.js@^1.0.0, hash.js@^1.0.3: - version "1.1.7" - resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42" - integrity sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA== - dependencies: - inherits "^2.0.3" - minimalistic-assert "^1.0.1" - -hasown@^2.0.0: +hasown@^2.0.0, hasown@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== @@ -5966,15 +7748,6 @@ history@^4.9.0: tiny-warning "^1.0.0" value-equal "^1.0.1" -hmac-drbg@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" - integrity sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg== - dependencies: - hash.js "^1.0.3" - minimalistic-assert "^1.0.0" - minimalistic-crypto-utils "^1.0.1" - hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" @@ -6043,10 +7816,10 @@ html-void-elements@^3.0.0: resolved "https://registry.yarnpkg.com/html-void-elements/-/html-void-elements-3.0.0.tgz#fc9dbd84af9e747249034d4d62602def6517f1d7" integrity sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg== -html-webpack-plugin@^5.5.3: - version "5.6.0" - resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-5.6.0.tgz#50a8fa6709245608cb00e811eacecb8e0d7b7ea0" - integrity sha512-iwaY4wzbe48AfKLZ/Cc8k0L+FKG6oSNRaZ8x5A/T/IVDGyXcbHncM9TdDa93wn0FsSm82FhTKW7f3vS61thXAw== +html-webpack-plugin@^5.6.0: + version "5.6.3" + resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-5.6.3.tgz#a31145f0fee4184d53a794f9513147df1e653685" + integrity sha512-QSf1yjtSAsmf7rYBV7XX86uua4W/vkhIt0xNXKbsi2foEeW7vjJQz4bhnpL3xH+l1ryl1680uNv968Z+X6jSYg== dependencies: "@types/html-minifier-terser" "^6.0.0" html-minifier-terser "^6.0.2" @@ -6148,11 +7921,6 @@ http2-wrapper@^2.1.10: quick-lru "^5.1.1" resolve-alpn "^1.2.0" -https-browserify@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" - integrity sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg== - https-proxy-agent@5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" @@ -6195,24 +7963,22 @@ ignore@^5.2.0, ignore@^5.2.4: resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.1.tgz#5073e554cd42c5b33b394375f538b8593e34d4ef" integrity sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw== -image-size@^1.0.2: - version "1.2.1" - resolved "https://registry.yarnpkg.com/image-size/-/image-size-1.2.1.tgz#ee118aedfe666db1a6ee12bed5821cde3740276d" - integrity sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw== - dependencies: - queue "6.0.2" +image-size@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/image-size/-/image-size-2.0.2.tgz#84a7b43704db5736f364bf0d1b029821299b4bdc" + integrity sha512-IRqXKlaXwgSMAMtpNzZa1ZAe8m+Sa1770Dhk8VkSsP9LS+iHD62Zd8FQKs8fbPiagBE7BzoFX23cxFnwshpV6w== -immer@^9.0.21, immer@^9.0.7: +immer@^9.0.21: version "9.0.21" resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.21.tgz#1e025ea31a40f24fb064f1fef23e931496330176" integrity sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA== -immutable@^4.0.0: - version "4.3.6" - resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.6.tgz#6a05f7858213238e587fb83586ffa3b4b27f0447" - integrity sha512-Ju0+lEMyzMVZarkTn/gqRpdqd5dOPaz1mCZ0SH3JV6iFw81PldE/PEB1hWVEA288HPt4WXW8O7AWxB10M+03QQ== +immutable@^5.0.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/immutable/-/immutable-5.1.2.tgz#e8169476414505e5a4fa650107b65e1227d16d4b" + integrity sha512-qHKXW1q6liAk1Oys6umoaZbDRqjcjgSrbnrifHsfsttza7zcvRAsL7mMV6xWcyhwQy7Xj5v4hhbr6b+iDYwlmQ== -import-fresh@^3.1.0, import-fresh@^3.3.0: +import-fresh@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== @@ -6235,10 +8001,10 @@ indent-string@^4.0.0: resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== -infima@0.2.0-alpha.43: - version "0.2.0-alpha.43" - resolved "https://registry.yarnpkg.com/infima/-/infima-0.2.0-alpha.43.tgz#f7aa1d7b30b6c08afef441c726bac6150228cbe0" - integrity sha512-2uw57LvUqW0rK/SWYnd/2rRfxNA5DDNOh33jxF7fy46VWoNhGxiUQyVZHbBMjQ33mQem0cjdDVwgWVAmlRfgyQ== +infima@0.2.0-alpha.45: + version "0.2.0-alpha.45" + resolved "https://registry.yarnpkg.com/infima/-/infima-0.2.0-alpha.45.tgz#542aab5a249274d81679631b492973dd2c1e7466" + integrity sha512-uyH0zfr1erU1OohLk0fT4Rrb94AOhguWNOcD9uGrSpRvNB+6gZXUoJX5J0NtvzBO10YZ9PgvA4NFgt+fYg8ojw== inflight@^1.0.4: version "1.0.6" @@ -6248,7 +8014,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3, inherits@~2.0.4: +inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -6263,7 +8029,7 @@ ini@2.0.0: resolved "https://registry.yarnpkg.com/ini/-/ini-2.0.0.tgz#e5fd556ecdd5726be978fa1001862eacb0a94bc5" integrity sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA== -ini@^1.3.4, ini@^1.3.5, ini@~1.3.0: +ini@^1.3.4, ini@~1.3.0: version "1.3.8" resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== @@ -6323,14 +8089,6 @@ is-alphanumerical@^2.0.0: is-alphabetical "^2.0.0" is-decimal "^2.0.0" -is-arguments@^1.0.4: - version "1.1.1" - resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.1.tgz#15b3f88fda01f2a97fec84ca761a560f123efa9b" - integrity sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA== - dependencies: - call-bind "^1.0.2" - has-tostringtag "^1.0.0" - is-arrayish@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" @@ -6348,11 +8106,6 @@ is-buffer@^2.0.0: resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.5.tgz#ebc252e400d22ff8d77fa09888821a24a658c191" integrity sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ== -is-callable@^1.1.3: - version "1.2.7" - resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" - integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== - is-ci@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-3.0.1.tgz#db6ecbed1bd659c43dac0f45661e7674103d1867" @@ -6392,13 +8145,6 @@ is-fullwidth-code-point@^3.0.0: resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== -is-generator-function@^1.0.7: - version "1.0.10" - resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.10.tgz#f1558baf1ac17e0deea7c0415c438351ff2b3c72" - integrity sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A== - dependencies: - has-tostringtag "^1.0.0" - is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: version "4.0.3" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" @@ -6419,14 +8165,6 @@ is-installed-globally@^0.4.0: global-dirs "^3.0.0" is-path-inside "^3.0.2" -is-nan@^1.3.2: - version "1.3.2" - resolved "https://registry.yarnpkg.com/is-nan/-/is-nan-1.3.2.tgz#043a54adea31748b55b6cd4e09aadafa69bd9e1d" - integrity sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w== - dependencies: - call-bind "^1.0.0" - define-properties "^1.1.3" - is-npm@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-6.0.0.tgz#b59e75e8915543ca5d881ecff864077cba095261" @@ -6447,11 +8185,6 @@ is-obj@^2.0.0: resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-2.0.0.tgz#473fb05d973705e3fd9620545018ca8e22ef4982" integrity sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w== -is-path-cwd@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-2.2.0.tgz#67d43b82664a7b5191fd9119127eb300048a9fdb" - integrity sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ== - is-path-inside@^3.0.2: version "3.0.3" resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" @@ -6486,23 +8219,11 @@ is-regexp@^1.0.0: resolved "https://registry.yarnpkg.com/is-regexp/-/is-regexp-1.0.0.tgz#fd2d883545c46bac5a633e7b9a09e87fa2cb5069" integrity sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA== -is-root@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-root/-/is-root-2.1.0.tgz#809e18129cf1129644302a4f8544035d51984a9c" - integrity sha512-AGOriNp96vNBd3HtU+RzFEc75FfR5ymiYv8E553I71SCeXBiMsVDUtdio1OEFvrPyLIQ9tVR5RxXIFe5PUFjMg== - is-stream@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== -is-typed-array@^1.1.3: - version "1.1.13" - resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.13.tgz#d6c5ca56df62334959322d7d7dd1cca50debe229" - integrity sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw== - dependencies: - which-typed-array "^1.1.14" - is-typedarray@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" @@ -6585,7 +8306,7 @@ jiti@^1.20.0, jiti@^1.21.0: resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.21.6.tgz#6c7f7398dd4b3142767f9a168af2f317a428d268" integrity sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w== -joi@^17.6.0, joi@^17.9.2: +joi@^17.9.2: version "17.13.1" resolved "https://registry.yarnpkg.com/joi/-/joi-17.13.1.tgz#9c7b53dc3b44dd9ae200255cc3b398874918a6ca" integrity sha512-vaBlIKCyo4FCUtCm7Eu4QZd/q02bWcxfUO6YSXAZOWF6gzcLBeba8kwotUdYJjDLW8Cz8RywsSOqiNJZW0mNvg== @@ -6626,16 +8347,31 @@ jsesc@^2.5.1: resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== +jsesc@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.1.0.tgz#74d335a234f67ed19907fdadfac7ccf9d409825d" + integrity sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA== + jsesc@~0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" integrity sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA== +jsesc@~3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.0.2.tgz#bb8b09a6597ba426425f2e4a07245c3d00b9343e" + integrity sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g== + json-buffer@3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== +json-crawl@^0.5.3: + version "0.5.3" + resolved "https://registry.yarnpkg.com/json-crawl/-/json-crawl-0.5.3.tgz#3a2e1d308d4fc5a444902f1f94f4a9e03d584c6b" + integrity sha512-BEjjCw8c7SxzNK4orhlWD5cXQh8vCk2LqDr4WgQq4CV+5dvopeYwt1Tskg67SuSLKvoFH5g0yuYtg7rcfKV6YA== + json-parse-even-better-errors@^2.3.0, json-parse-even-better-errors@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" @@ -6655,7 +8391,7 @@ json-schema-compare@^0.2.2: dependencies: lodash "^4.17.4" -json-schema-merge-allof@0.8.1, json-schema-merge-allof@^0.8.1: +json-schema-merge-allof@0.8.1: version "0.8.1" resolved "https://registry.yarnpkg.com/json-schema-merge-allof/-/json-schema-merge-allof-0.8.1.tgz#ed2828cdd958616ff74f932830a26291789eaaf2" integrity sha512-CTUKmIlPJbsWfzRRnOXz+0MjIqvnleIXwFTzz+t9T86HnYX/Rozria6ZVGLktAU9e+NygNljveP+yxqtQp/Q4w== @@ -6702,7 +8438,7 @@ keyv@^4.5.3: dependencies: json-buffer "3.0.1" -khroma@^2.0.0: +khroma@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/khroma/-/khroma-2.1.0.tgz#45f2ce94ce231a437cf5b63c2e886e6eb42bbbb1" integrity sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw== @@ -6722,10 +8458,21 @@ kleur@^4.0.3: resolved "https://registry.yarnpkg.com/kleur/-/kleur-4.1.5.tgz#95106101795f7050c6c650f350c683febddb1780" integrity sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ== -klona@^2.0.4: - version "2.0.6" - resolved "https://registry.yarnpkg.com/klona/-/klona-2.0.6.tgz#85bffbf819c03b2f53270412420a4555ef882e22" - integrity sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA== +kolorist@^1.8.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/kolorist/-/kolorist-1.8.0.tgz#edddbbbc7894bc13302cdf740af6374d4a04743c" + integrity sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ== + +langium@3.3.1: + version "3.3.1" + resolved "https://registry.yarnpkg.com/langium/-/langium-3.3.1.tgz#da745a40d5ad8ee565090fed52eaee643be4e591" + integrity sha512-QJv/h939gDpvT+9SiLVlY7tZC3xB2qK57v0J04Sh9wpMb6MP1q8gB21L3WIo8T5P1MSMg3Ep14L7KkDCFG3y4w== + dependencies: + chevrotain "~11.0.3" + chevrotain-allstar "~0.3.0" + vscode-languageserver "~9.0.1" + vscode-languageserver-textdocument "~1.0.11" + vscode-uri "~3.0.8" latest-version@^7.0.0: version "7.0.0" @@ -6747,11 +8494,84 @@ layout-base@^1.0.0: resolved "https://registry.yarnpkg.com/layout-base/-/layout-base-1.0.2.tgz#1291e296883c322a9dd4c5dd82063721b53e26e2" integrity sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg== +layout-base@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/layout-base/-/layout-base-2.0.1.tgz#d0337913586c90f9c2c075292069f5c2da5dd285" + integrity sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg== + leven@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A== +lightningcss-darwin-arm64@1.30.1: + version "1.30.1" + resolved "https://registry.yarnpkg.com/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.1.tgz#3d47ce5e221b9567c703950edf2529ca4a3700ae" + integrity sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ== + +lightningcss-darwin-x64@1.30.1: + version "1.30.1" + resolved "https://registry.yarnpkg.com/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.1.tgz#e81105d3fd6330860c15fe860f64d39cff5fbd22" + integrity sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA== + +lightningcss-freebsd-x64@1.30.1: + version "1.30.1" + resolved "https://registry.yarnpkg.com/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.1.tgz#a0e732031083ff9d625c5db021d09eb085af8be4" + integrity sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig== + +lightningcss-linux-arm-gnueabihf@1.30.1: + version "1.30.1" + resolved "https://registry.yarnpkg.com/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.1.tgz#1f5ecca6095528ddb649f9304ba2560c72474908" + integrity sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q== + +lightningcss-linux-arm64-gnu@1.30.1: + version "1.30.1" + resolved "https://registry.yarnpkg.com/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.1.tgz#eee7799726103bffff1e88993df726f6911ec009" + integrity sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw== + +lightningcss-linux-arm64-musl@1.30.1: + version "1.30.1" + resolved "https://registry.yarnpkg.com/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.1.tgz#f2e4b53f42892feeef8f620cbb889f7c064a7dfe" + integrity sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ== + +lightningcss-linux-x64-gnu@1.30.1: + version "1.30.1" + resolved "https://registry.yarnpkg.com/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.1.tgz#2fc7096224bc000ebb97eea94aea248c5b0eb157" + integrity sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw== + +lightningcss-linux-x64-musl@1.30.1: + version "1.30.1" + resolved "https://registry.yarnpkg.com/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.1.tgz#66dca2b159fd819ea832c44895d07e5b31d75f26" + integrity sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ== + +lightningcss-win32-arm64-msvc@1.30.1: + version "1.30.1" + resolved "https://registry.yarnpkg.com/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.1.tgz#7d8110a19d7c2d22bfdf2f2bb8be68e7d1b69039" + integrity sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA== + +lightningcss-win32-x64-msvc@1.30.1: + version "1.30.1" + resolved "https://registry.yarnpkg.com/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.1.tgz#fd7dd008ea98494b85d24b4bea016793f2e0e352" + integrity sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg== + +lightningcss@^1.27.0: + version "1.30.1" + resolved "https://registry.yarnpkg.com/lightningcss/-/lightningcss-1.30.1.tgz#78e979c2d595bfcb90d2a8c0eb632fe6c5bfed5d" + integrity sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg== + dependencies: + detect-libc "^2.0.3" + optionalDependencies: + lightningcss-darwin-arm64 "1.30.1" + lightningcss-darwin-x64 "1.30.1" + lightningcss-freebsd-x64 "1.30.1" + lightningcss-linux-arm-gnueabihf "1.30.1" + lightningcss-linux-arm64-gnu "1.30.1" + lightningcss-linux-arm64-musl "1.30.1" + lightningcss-linux-x64-gnu "1.30.1" + lightningcss-linux-x64-musl "1.30.1" + lightningcss-win32-arm64-msvc "1.30.1" + lightningcss-win32-x64-msvc "1.30.1" + lilconfig@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.1.0.tgz#78e23ac89ebb7e1bfbf25b18043de756548e7f52" @@ -6791,25 +8611,14 @@ loader-utils@^2.0.0: emojis-list "^3.0.0" json5 "^2.1.2" -loader-utils@^3.2.0: - version "3.3.1" - resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-3.3.1.tgz#735b9a19fd63648ca7adbd31c2327dfe281304e5" - integrity sha512-FMJTLMXfCLMLfJxcX9PFqX5qD88Z5MRGaZCVzfuqeZSPsyiBzs+pahDQjbIWz2QIzPZz0NX9Zy4FX3lmK6YHIg== - -locate-path@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e" - integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A== +local-pkg@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/local-pkg/-/local-pkg-1.1.1.tgz#f5fe74a97a3bd3c165788ee08ca9fbe998dc58dd" + integrity sha512-WunYko2W1NcdfAFpuLUoucsgULmgDBRkdxHxWQ7mK0cQqwPiy8E1enjuRBrhLtZkB5iScJ1XIPdhVEFK8aOLSg== dependencies: - p-locate "^3.0.0" - path-exists "^3.0.0" - -locate-path@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" - integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== - dependencies: - p-locate "^5.0.0" + mlly "^1.7.4" + pkg-types "^2.0.1" + quansync "^0.2.8" locate-path@^7.1.0: version "7.2.0" @@ -6818,7 +8627,7 @@ locate-path@^7.1.0: dependencies: p-locate "^6.0.0" -lodash-es@^4.17.21: +lodash-es@4.17.21, lodash-es@^4.17.21: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee" integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw== @@ -6896,19 +8705,27 @@ markdown-extensions@^2.0.0: resolved "https://registry.yarnpkg.com/markdown-extensions/-/markdown-extensions-2.0.0.tgz#34bebc83e9938cae16e0e017e4a9814a8330d3c4" integrity sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q== +markdown-table@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/markdown-table/-/markdown-table-2.0.0.tgz#194a90ced26d31fe753d8b9434430214c011865b" + integrity sha512-Ezda85ToJUBhM6WGaG6veasyym+Tbs3cMAw/ZhOPqXiYsr0jgocBV3j3nx+4lk47plLlIqjwuTm/ywVI+zjJ/A== + dependencies: + repeat-string "^1.0.0" + markdown-table@^3.0.0: version "3.0.3" resolved "https://registry.yarnpkg.com/markdown-table/-/markdown-table-3.0.3.tgz#e6331d30e493127e031dd385488b5bd326e4a6bd" integrity sha512-Z1NL3Tb1M9wH4XESsCDEksWoKTdlUafKc4pt0GRwjUyXaCFZ+dc3g2erqB6zm3szA2IUSi7VnPI+o/9jnxh9hw== -md5.js@^1.3.4: - version "1.3.5" - resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f" - integrity sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg== - dependencies: - hash-base "^3.0.0" - inherits "^2.0.1" - safe-buffer "^5.1.2" +marked@^15.0.7: + version "15.0.12" + resolved "https://registry.yarnpkg.com/marked/-/marked-15.0.12.tgz#30722c7346e12d0a2d0207ab9b0c4f0102d86c4e" + integrity sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA== + +math-intrinsics@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9" + integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g== mdast-util-definitions@^5.0.0: version "5.1.2" @@ -6933,6 +8750,16 @@ mdast-util-directive@^3.0.0: stringify-entities "^4.0.0" unist-util-visit-parents "^6.0.0" +mdast-util-find-and-replace@^2.0.0: + version "2.2.2" + resolved "https://registry.yarnpkg.com/mdast-util-find-and-replace/-/mdast-util-find-and-replace-2.2.2.tgz#cc2b774f7f3630da4bd592f61966fecade8b99b1" + integrity sha512-MTtdFRz/eMDHXzeK6W3dO7mXUlF82Gom4y0oOgvHhh/HXZAGvIQDUvQ0SuUx+j2tv44b8xTHOm8K/9OoRFnXKw== + dependencies: + "@types/mdast" "^3.0.0" + escape-string-regexp "^5.0.0" + unist-util-is "^5.0.0" + unist-util-visit-parents "^5.0.0" + mdast-util-find-and-replace@^3.0.0, mdast-util-find-and-replace@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.1.tgz#a6fc7b62f0994e973490e45262e4bc07607b04e0" @@ -6943,7 +8770,7 @@ mdast-util-find-and-replace@^3.0.0, mdast-util-find-and-replace@^3.0.1: unist-util-is "^6.0.0" unist-util-visit-parents "^6.0.0" -mdast-util-from-markdown@^1.0.0, mdast-util-from-markdown@^1.1.0, mdast-util-from-markdown@^1.2.0, mdast-util-from-markdown@^1.3.0: +mdast-util-from-markdown@^1.0.0, mdast-util-from-markdown@^1.1.0, mdast-util-from-markdown@^1.2.0: version "1.3.1" resolved "https://registry.yarnpkg.com/mdast-util-from-markdown/-/mdast-util-from-markdown-1.3.1.tgz#9421a5a247f10d31d2faed2a30df5ec89ceafcf0" integrity sha512-4xTO/M8c82qBcnQc1tgpNtubGUW/Y1tBQ1B0i5CtSoelOLKFYlElIr3bvgREYYO5iRqbMY1YuqZng0GVOI8Qww== @@ -6991,6 +8818,16 @@ mdast-util-frontmatter@^2.0.0: mdast-util-to-markdown "^2.0.0" micromark-extension-frontmatter "^2.0.0" +mdast-util-gfm-autolink-literal@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-1.0.3.tgz#67a13abe813d7eba350453a5333ae1bc0ec05c06" + integrity sha512-My8KJ57FYEy2W2LyNom4n3E7hKTuQk/0SES0u16tjA9Z3oFkF4RrC/hPAPgjlSpezsOvI8ObcXcElo92wn5IGA== + dependencies: + "@types/mdast" "^3.0.0" + ccount "^2.0.0" + mdast-util-find-and-replace "^2.0.0" + micromark-util-character "^1.0.0" + mdast-util-gfm-autolink-literal@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.0.tgz#5baf35407421310a08e68c15e5d8821e8898ba2a" @@ -7002,6 +8839,15 @@ mdast-util-gfm-autolink-literal@^2.0.0: mdast-util-find-and-replace "^3.0.0" micromark-util-character "^2.0.0" +mdast-util-gfm-footnote@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-1.0.2.tgz#ce5e49b639c44de68d5bf5399877a14d5020424e" + integrity sha512-56D19KOGbE00uKVj3sgIykpwKL179QsVFwx/DCW0u/0+URsryacI4MAdNJl0dh+u2PSsD9FtxPFbHCzJ78qJFQ== + dependencies: + "@types/mdast" "^3.0.0" + mdast-util-to-markdown "^1.3.0" + micromark-util-normalize-identifier "^1.0.0" + mdast-util-gfm-footnote@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.0.0.tgz#25a1753c7d16db8bfd53cd84fe50562bd1e6d6a9" @@ -7013,6 +8859,14 @@ mdast-util-gfm-footnote@^2.0.0: mdast-util-to-markdown "^2.0.0" micromark-util-normalize-identifier "^2.0.0" +mdast-util-gfm-strikethrough@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-1.0.3.tgz#5470eb105b483f7746b8805b9b989342085795b7" + integrity sha512-DAPhYzTYrRcXdMjUtUjKvW9z/FNAMTdU0ORyMcbmkwYNbKocDpdk+PX1L1dQgOID/+vVs1uBQ7ElrBQfZ0cuiQ== + dependencies: + "@types/mdast" "^3.0.0" + mdast-util-to-markdown "^1.3.0" + mdast-util-gfm-strikethrough@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz#d44ef9e8ed283ac8c1165ab0d0dfd058c2764c16" @@ -7022,6 +8876,16 @@ mdast-util-gfm-strikethrough@^2.0.0: mdast-util-from-markdown "^2.0.0" mdast-util-to-markdown "^2.0.0" +mdast-util-gfm-table@^1.0.0: + version "1.0.7" + resolved "https://registry.yarnpkg.com/mdast-util-gfm-table/-/mdast-util-gfm-table-1.0.7.tgz#3552153a146379f0f9c4c1101b071d70bbed1a46" + integrity sha512-jjcpmNnQvrmN5Vx7y7lEc2iIOEytYv7rTvu+MeyAsSHTASGCCRA79Igg2uKssgOs1i1po8s3plW0sTu1wkkLGg== + dependencies: + "@types/mdast" "^3.0.0" + markdown-table "^3.0.0" + mdast-util-from-markdown "^1.0.0" + mdast-util-to-markdown "^1.3.0" + mdast-util-gfm-table@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz#7a435fb6223a72b0862b33afbd712b6dae878d38" @@ -7033,6 +8897,14 @@ mdast-util-gfm-table@^2.0.0: mdast-util-from-markdown "^2.0.0" mdast-util-to-markdown "^2.0.0" +mdast-util-gfm-task-list-item@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-1.0.2.tgz#b280fcf3b7be6fd0cc012bbe67a59831eb34097b" + integrity sha512-PFTA1gzfp1B1UaiJVyhJZA1rm0+Tzn690frc/L8vNX1Jop4STZgOE6bxUhnzdVSB+vm2GU1tIsuQcA9bxTQpMQ== + dependencies: + "@types/mdast" "^3.0.0" + mdast-util-to-markdown "^1.3.0" + mdast-util-gfm-task-list-item@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz#e68095d2f8a4303ef24094ab642e1047b991a936" @@ -7043,6 +8915,19 @@ mdast-util-gfm-task-list-item@^2.0.0: mdast-util-from-markdown "^2.0.0" mdast-util-to-markdown "^2.0.0" +mdast-util-gfm@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/mdast-util-gfm/-/mdast-util-gfm-2.0.2.tgz#e92f4d8717d74bdba6de57ed21cc8b9552e2d0b6" + integrity sha512-qvZ608nBppZ4icQlhQQIAdc6S3Ffj9RGmzwUKUWuEICFnd1LVkN3EktF7ZHAgfcEdvZB5owU9tQgt99e2TlLjg== + dependencies: + mdast-util-from-markdown "^1.0.0" + mdast-util-gfm-autolink-literal "^1.0.0" + mdast-util-gfm-footnote "^1.0.0" + mdast-util-gfm-strikethrough "^1.0.0" + mdast-util-gfm-table "^1.0.0" + mdast-util-gfm-task-list-item "^1.0.0" + mdast-util-to-markdown "^1.0.0" + mdast-util-gfm@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/mdast-util-gfm/-/mdast-util-gfm-3.0.0.tgz#3f2aecc879785c3cb6a81ff3a243dc11eca61095" @@ -7277,12 +9162,12 @@ media-typer@0.3.0: resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== -medium-zoom@^1.0.6: +medium-zoom@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/medium-zoom/-/medium-zoom-1.1.0.tgz#6efb6bbda861a02064ee71a2617a8dc4381ecc71" integrity sha512-ewyDsp7k4InCUp3jRmwHBRFGyjBimKps/AJLjRSox+2q/2H4p/PNpQf+pwONWlJiOudkBXtbdmVbFjqyybfTmQ== -memfs@^3.1.2, memfs@^3.4.3: +memfs@^3.4.3: version "3.6.0" resolved "https://registry.yarnpkg.com/memfs/-/memfs-3.6.0.tgz#d7a2110f86f79dd950a8b6df6d57bc984aa185f6" integrity sha512-EGowvkkgbMcIChjMTMkESFDbZeSh8xZ7kNSF0hAiAN4Jh6jgHCRS0Ga/+C8y6Au+oqpezRHCfPsmJ2+DwAgiwQ== @@ -7309,31 +9194,31 @@ merge2@^1.3.0, merge2@^1.4.1: resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== -mermaid@^10.4.0, mermaid@^10.9.1: - version "10.9.1" - resolved "https://registry.yarnpkg.com/mermaid/-/mermaid-10.9.1.tgz#5f582c23f3186c46c6aa673e59eeb46d741b2ea6" - integrity sha512-Mx45Obds5W1UkW1nv/7dHRsbfMM1aOKA2+Pxs/IGHNonygDHwmng8xTHyS9z4KWVi0rbko8gjiBmuwwXQ7tiNA== +mermaid@>=11.6.0: + version "11.6.0" + resolved "https://registry.yarnpkg.com/mermaid/-/mermaid-11.6.0.tgz#eee45cdc3087be561a19faf01745596d946bb575" + integrity sha512-PE8hGUy1LDlWIHWBP05SFdqUHGmRcCcK4IzpOKPE35eOw+G9zZgcnMpyunJVUEOgb//KBORPjysKndw8bFLuRg== dependencies: - "@braintree/sanitize-url" "^6.0.1" - "@types/d3-scale" "^4.0.3" - "@types/d3-scale-chromatic" "^3.0.0" - cytoscape "^3.28.1" + "@braintree/sanitize-url" "^7.0.4" + "@iconify/utils" "^2.1.33" + "@mermaid-js/parser" "^0.4.0" + "@types/d3" "^7.4.3" + cytoscape "^3.29.3" cytoscape-cose-bilkent "^4.1.0" - d3 "^7.4.0" + cytoscape-fcose "^2.2.0" + d3 "^7.9.0" d3-sankey "^0.12.3" - dagre-d3-es "7.0.10" - dayjs "^1.11.7" - dompurify "^3.0.5" - elkjs "^0.9.0" + dagre-d3-es "7.0.11" + dayjs "^1.11.13" + dompurify "^3.2.4" katex "^0.16.9" - khroma "^2.0.0" + khroma "^2.1.0" lodash-es "^4.17.21" - mdast-util-from-markdown "^1.3.0" - non-layered-tidy-tree-layout "^2.0.2" - stylis "^4.1.3" + marked "^15.0.7" + roughjs "^4.6.6" + stylis "^4.3.6" ts-dedent "^2.2.0" - uuid "^9.0.0" - web-worker "^1.2.0" + uuid "^11.1.0" methods@~1.1.2: version "1.1.2" @@ -7407,6 +9292,16 @@ micromark-extension-frontmatter@^2.0.0: micromark-util-symbol "^2.0.0" micromark-util-types "^2.0.0" +micromark-extension-gfm-autolink-literal@^1.0.0: + version "1.0.5" + resolved "https://registry.yarnpkg.com/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-1.0.5.tgz#5853f0e579bbd8ef9e39a7c0f0f27c5a063a66e7" + integrity sha512-z3wJSLrDf8kRDOh2qBtoTRD53vJ+CWIyo7uyZuxf/JAbNJjiHsOpG1y5wxk8drtv3ETAHutCu6N3thkOOgueWg== + dependencies: + micromark-util-character "^1.0.0" + micromark-util-sanitize-uri "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + micromark-extension-gfm-autolink-literal@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.0.0.tgz#f1e50b42e67d441528f39a67133eddde2bbabfd9" @@ -7417,6 +9312,20 @@ micromark-extension-gfm-autolink-literal@^2.0.0: micromark-util-symbol "^2.0.0" micromark-util-types "^2.0.0" +micromark-extension-gfm-footnote@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-1.1.2.tgz#05e13034d68f95ca53c99679040bc88a6f92fe2e" + integrity sha512-Yxn7z7SxgyGWRNa4wzf8AhYYWNrwl5q1Z8ii+CSTTIqVkmGZF1CElX2JI8g5yGoM3GAman9/PVCUFUSJ0kB/8Q== + dependencies: + micromark-core-commonmark "^1.0.0" + micromark-factory-space "^1.0.0" + micromark-util-character "^1.0.0" + micromark-util-normalize-identifier "^1.0.0" + micromark-util-sanitize-uri "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + uvu "^0.5.0" + micromark-extension-gfm-footnote@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.0.0.tgz#91afad310065a94b636ab1e9dab2c60d1aab953c" @@ -7431,6 +9340,18 @@ micromark-extension-gfm-footnote@^2.0.0: micromark-util-symbol "^2.0.0" micromark-util-types "^2.0.0" +micromark-extension-gfm-strikethrough@^1.0.0: + version "1.0.7" + resolved "https://registry.yarnpkg.com/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-1.0.7.tgz#c8212c9a616fa3bf47cb5c711da77f4fdc2f80af" + integrity sha512-sX0FawVE1o3abGk3vRjOH50L5TTLr3b5XMqnP9YDRb34M0v5OoZhG+OHFz1OffZ9dlwgpTBKaT4XW/AsUVnSDw== + dependencies: + micromark-util-chunked "^1.0.0" + micromark-util-classify-character "^1.0.0" + micromark-util-resolve-all "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + uvu "^0.5.0" + micromark-extension-gfm-strikethrough@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.0.0.tgz#6917db8e320da70e39ffbf97abdbff83e6783e61" @@ -7443,6 +9364,17 @@ micromark-extension-gfm-strikethrough@^2.0.0: micromark-util-symbol "^2.0.0" micromark-util-types "^2.0.0" +micromark-extension-gfm-table@^1.0.0: + version "1.0.7" + resolved "https://registry.yarnpkg.com/micromark-extension-gfm-table/-/micromark-extension-gfm-table-1.0.7.tgz#dcb46074b0c6254c3fc9cc1f6f5002c162968008" + integrity sha512-3ZORTHtcSnMQEKtAOsBQ9/oHp9096pI/UvdPtN7ehKvrmZZ2+bbWhi0ln+I9drmwXMt5boocn6OlwQzNXeVeqw== + dependencies: + micromark-factory-space "^1.0.0" + micromark-util-character "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + uvu "^0.5.0" + micromark-extension-gfm-table@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.0.0.tgz#2cf3fe352d9e089b7ef5fff003bdfe0da29649b7" @@ -7454,6 +9386,13 @@ micromark-extension-gfm-table@^2.0.0: micromark-util-symbol "^2.0.0" micromark-util-types "^2.0.0" +micromark-extension-gfm-tagfilter@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-1.0.2.tgz#aa7c4dd92dabbcb80f313ebaaa8eb3dac05f13a7" + integrity sha512-5XWB9GbAUSHTn8VPU8/1DBXMuKYT5uOgEjJb8gN3mW0PNW5OPHpSdojoqf+iq1xo7vWzw/P8bAHY0n6ijpXF7g== + dependencies: + micromark-util-types "^1.0.0" + micromark-extension-gfm-tagfilter@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz#f26d8a7807b5985fba13cf61465b58ca5ff7dc57" @@ -7461,6 +9400,17 @@ micromark-extension-gfm-tagfilter@^2.0.0: dependencies: micromark-util-types "^2.0.0" +micromark-extension-gfm-task-list-item@^1.0.0: + version "1.0.5" + resolved "https://registry.yarnpkg.com/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-1.0.5.tgz#b52ce498dc4c69b6a9975abafc18f275b9dde9f4" + integrity sha512-RMFXl2uQ0pNQy6Lun2YBYT9g9INXtWJULgbt01D/x8/6yJ2qpKyzdZD3pi6UIkzF++Da49xAelVKUeUMqd5eIQ== + dependencies: + micromark-factory-space "^1.0.0" + micromark-util-character "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + uvu "^0.5.0" + micromark-extension-gfm-task-list-item@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.0.1.tgz#ee8b208f1ced1eb9fb11c19a23666e59d86d4838" @@ -7472,6 +9422,20 @@ micromark-extension-gfm-task-list-item@^2.0.0: micromark-util-symbol "^2.0.0" micromark-util-types "^2.0.0" +micromark-extension-gfm@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/micromark-extension-gfm/-/micromark-extension-gfm-2.0.3.tgz#e517e8579949a5024a493e49204e884aa74f5acf" + integrity sha512-vb9OoHqrhCmbRidQv/2+Bc6pkP0FrtlhurxZofvOEy5o8RtuuvTq+RQ1Vw5ZDNrVraQZu3HixESqbG+0iKk/MQ== + dependencies: + micromark-extension-gfm-autolink-literal "^1.0.0" + micromark-extension-gfm-footnote "^1.0.0" + micromark-extension-gfm-strikethrough "^1.0.0" + micromark-extension-gfm-table "^1.0.0" + micromark-extension-gfm-tagfilter "^1.0.0" + micromark-extension-gfm-task-list-item "^1.0.0" + micromark-util-combine-extensions "^1.0.0" + micromark-util-types "^1.0.0" + micromark-extension-gfm@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz#3e13376ab95dd7a5cfd0e29560dfe999657b3c5b" @@ -8026,14 +9990,6 @@ micromatch@^4.0.2, micromatch@^4.0.4, micromatch@^4.0.5: braces "^3.0.3" picomatch "^2.3.1" -miller-rabin@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d" - integrity sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA== - dependencies: - bn.js "^4.0.0" - brorand "^1.0.1" - mime-db@1.48.0: version "1.48.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.48.0.tgz#e35b31045dd7eada3aaad537ed88a33afbef2d1d" @@ -8097,25 +10053,20 @@ mimic-response@^4.0.0: resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-4.0.0.tgz#35468b19e7c75d10f5165ea25e75a5ceea7cf70f" integrity sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg== -mini-css-extract-plugin@^2.7.6: - version "2.9.0" - resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-2.9.0.tgz#c73a1327ccf466f69026ac22a8e8fd707b78a235" - integrity sha512-Zs1YsZVfemekSZG+44vBsYTLQORkPMwnlv+aehcxK/NLKC+EGhDB39/YePYYqx/sTk6NnYpuqikhSn7+JIevTA== +mini-css-extract-plugin@^2.9.1: + version "2.9.2" + resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-2.9.2.tgz#966031b468917a5446f4c24a80854b2947503c5b" + integrity sha512-GJuACcS//jtq4kCtd5ii/M0SZf7OZRH+BxdqXZHaJfb8TJiVl+NgQRPwiYt2EuqeSkNydn/7vP+bcE27C5mb9w== dependencies: schema-utils "^4.0.0" tapable "^2.2.1" -minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1: +minimalistic-assert@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== -minimalistic-crypto-utils@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" - integrity sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg== - -minimatch@3.1.2, minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1: +minimatch@3.1.2, minimatch@^3.1.1: version "3.1.2" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== @@ -8136,7 +10087,7 @@ minimatch@^9.0.4: dependencies: brace-expansion "^2.0.1" -minimist@^1.2.0, minimist@^1.2.5: +minimist@^1.2.0: version "1.2.8" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== @@ -8151,6 +10102,16 @@ mkdirp-classic@^0.5.2: resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113" integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A== +mlly@^1.7.4: + version "1.7.4" + resolved "https://registry.yarnpkg.com/mlly/-/mlly-1.7.4.tgz#3d7295ea2358ec7a271eaa5d000a0f84febe100f" + integrity sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw== + dependencies: + acorn "^8.14.0" + pathe "^2.0.1" + pkg-types "^1.3.0" + ufo "^1.5.4" + mri@^1.1.0: version "1.2.0" resolved "https://registry.yarnpkg.com/mri/-/mri-1.2.0.tgz#6721480fec2a11a4889861115a48b6cbe7cc8f0b" @@ -8171,7 +10132,7 @@ ms@2.1.2: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== -ms@2.1.3: +ms@2.1.3, ms@^2.1.3: version "2.1.3" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== @@ -8221,6 +10182,11 @@ no-case@^3.0.4: lower-case "^2.0.2" tslib "^2.0.3" +node-addon-api@^7.0.0: + version "7.1.1" + resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-7.1.1.tgz#1aba6693b0f255258a049d621329329322aad558" + integrity sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ== + node-emoji@^2.1.0: version "2.1.3" resolved "https://registry.yarnpkg.com/node-emoji/-/node-emoji-2.1.3.tgz#93cfabb5cc7c3653aa52f29d6ffb7927d8047c06" @@ -8257,37 +10223,6 @@ node-forge@^1: resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3" integrity sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA== -node-polyfill-webpack-plugin@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/node-polyfill-webpack-plugin/-/node-polyfill-webpack-plugin-2.0.1.tgz#141d86f177103a8517c71d99b7c6a46edbb1bb58" - integrity sha512-ZUMiCnZkP1LF0Th2caY6J/eKKoA0TefpoVa68m/LQU1I/mE8rGt4fNYGgNuCcK+aG8P8P43nbeJ2RqJMOL/Y1A== - dependencies: - assert "^2.0.0" - browserify-zlib "^0.2.0" - buffer "^6.0.3" - console-browserify "^1.2.0" - constants-browserify "^1.0.0" - crypto-browserify "^3.12.0" - domain-browser "^4.22.0" - events "^3.3.0" - filter-obj "^2.0.2" - https-browserify "^1.0.0" - os-browserify "^0.3.0" - path-browserify "^1.0.1" - process "^0.11.10" - punycode "^2.1.1" - querystring-es3 "^0.2.1" - readable-stream "^4.0.0" - stream-browserify "^3.0.0" - stream-http "^3.2.0" - string_decoder "^1.3.0" - timers-browserify "^2.0.12" - tty-browserify "^0.0.1" - type-fest "^2.14.0" - url "^0.11.0" - util "^0.12.4" - vm-browserify "^1.1.2" - node-readfiles@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/node-readfiles/-/node-readfiles-0.2.0.tgz#dbbd4af12134e2e635c245ef93ffcf6f60673a5d" @@ -8300,10 +10235,10 @@ node-releases@^2.0.14: resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.14.tgz#2ffb053bceb8b2be8495ece1ab6ce600c4461b0b" integrity sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw== -non-layered-tidy-tree-layout@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/non-layered-tidy-tree-layout/-/non-layered-tidy-tree-layout-2.0.2.tgz#57d35d13c356643fc296a55fb11ac15e74da7804" - integrity sha512-gkXMxRzUH+PB0ax9dUN0yYF0S25BqeAYqhgMaLUFmpXLEk7Fcu8f4emJuOAY0V8kjDICxROIKsTAKsV/v355xw== +node-releases@^2.0.19: + version "2.0.19" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.19.tgz#9e445a52950951ec4d177d843af370b411caf314" + integrity sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw== normalize-path@^3.0.0, normalize-path@~3.0.0: version "3.0.0" @@ -8339,6 +10274,14 @@ nth-check@^2.0.1: dependencies: boolbase "^1.0.0" +null-loader@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/null-loader/-/null-loader-4.0.1.tgz#8e63bd3a2dd3c64236a4679428632edd0a6dbc6a" + integrity sha512-pxqVbi4U6N26lq+LmgIbB5XATP0VdZKOG25DhHi8btMmJJefGArFyDg1yc4U3hWCJbMqSrw0qyrz1UQX+qYXqg== + dependencies: + loader-utils "^2.0.0" + schema-utils "^3.0.0" + oas-kit-common@^1.0.8: version "1.0.8" resolved "https://registry.yarnpkg.com/oas-kit-common/-/oas-kit-common-1.0.8.tgz#6d8cacf6e9097967a4c7ea8bcbcbd77018e1f535" @@ -8412,20 +10355,17 @@ object-inspect@^1.13.1: resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.1.tgz#b96c6109324ccfef6b12216a956ca4dc2ff94bc2" integrity sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ== -object-is@^1.1.5: - version "1.1.6" - resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.6.tgz#1a6a53aed2dd8f7e6775ff870bea58545956ab07" - integrity sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q== - dependencies: - call-bind "^1.0.7" - define-properties "^1.2.1" +object-inspect@^1.13.3: + version "1.13.4" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.4.tgz#8375265e21bc20d0fa582c22e1b13485d6e00213" + integrity sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew== object-keys@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== -object.assign@^4.1.0, object.assign@^4.1.4: +object.assign@^4.1.0: version "4.1.5" resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.5.tgz#3a833f9ab7fdb80fc9e8d2300c803d216d8fdbb0" integrity sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ== @@ -8502,29 +10442,15 @@ opener@^1.5.2: resolved "https://registry.yarnpkg.com/opener/-/opener-1.5.2.tgz#5d37e1f35077b9dcac4301372271afdeb2a13598" integrity sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A== -os-browserify@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27" - integrity sha512-gjcpUc3clBf9+210TRaDWbf+rZZZEshZ+DlXMRCeAjp0xhTrnQsKHypIy1J3d5hKdUzj69t708EHtU8P6bUn0A== - p-cancelable@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-3.0.0.tgz#63826694b54d61ca1c20ebcb6d3ecf5e14cd8050" integrity sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw== -p-limit@^2.0.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" - integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== - dependencies: - p-try "^2.0.0" - -p-limit@^3.0.2: - version "3.1.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" - integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== - dependencies: - yocto-queue "^0.1.0" +p-finally@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" + integrity sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow== p-limit@^4.0.0: version "4.0.0" @@ -8533,20 +10459,6 @@ p-limit@^4.0.0: dependencies: yocto-queue "^1.0.0" -p-locate@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4" - integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ== - dependencies: - p-limit "^2.0.0" - -p-locate@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" - integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== - dependencies: - p-limit "^3.0.2" - p-locate@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-6.0.0.tgz#3da9a49d4934b901089dca3302fa65dc5a05c04f" @@ -8561,6 +10473,14 @@ p-map@^4.0.0: dependencies: aggregate-error "^3.0.0" +p-queue@^6.6.2: + version "6.6.2" + resolved "https://registry.yarnpkg.com/p-queue/-/p-queue-6.6.2.tgz#2068a9dcf8e67dd0ec3e7a2bcb76810faa85e426" + integrity sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ== + dependencies: + eventemitter3 "^4.0.4" + p-timeout "^3.2.0" + p-retry@^4.5.0: version "4.6.2" resolved "https://registry.yarnpkg.com/p-retry/-/p-retry-4.6.2.tgz#9baae7184057edd4e17231cee04264106e092a16" @@ -8569,10 +10489,12 @@ p-retry@^4.5.0: "@types/retry" "0.12.0" retry "^0.13.1" -p-try@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" - integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== +p-timeout@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-3.2.0.tgz#c7e17abc971d2a7962ef83626b35d635acf23dfe" + integrity sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg== + dependencies: + p-finally "^1.0.0" package-json@^8.1.0: version "8.1.1" @@ -8584,10 +10506,15 @@ package-json@^8.1.0: registry-url "^6.0.0" semver "^7.3.7" -pako@~1.0.5: - version "1.0.11" - resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf" - integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw== +package-manager-detector@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/package-manager-detector/-/package-manager-detector-1.3.0.tgz#b42d641c448826e03c2b354272456a771ce453c0" + integrity sha512-ZsEbbZORsyHuO00lY1kV3/t72yp6Ysay6Pd17ZAlNGuGwmWDLCJxFpRs0IzfXfj1o4icJOkUEioexFHzyPurSQ== + +pako@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/pako/-/pako-2.1.0.tgz#266cc37f98c7d883545d11335c00fbd4062c9a86" + integrity sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug== param-case@^3.0.4: version "3.0.4" @@ -8604,18 +10531,6 @@ parent-module@^1.0.0: dependencies: callsites "^3.0.0" -parse-asn1@^5.0.0, parse-asn1@^5.1.7: - version "5.1.7" - resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.7.tgz#73cdaaa822125f9647165625eb45f8a051d2df06" - integrity sha512-CTM5kuWR3sx9IFamcl5ErfPl6ea/N8IYwiJ+vpeB2g+1iknv7zBl5uPwbMbRVznRVbrNY6lGuDoE5b30grmbqg== - dependencies: - asn1.js "^4.10.1" - browserify-aes "^1.2.0" - evp_bytestokey "^1.0.3" - hash-base "~3.0" - pbkdf2 "^3.1.2" - safe-buffer "^5.2.1" - parse-entities@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/parse-entities/-/parse-entities-4.0.1.tgz#4e2a01111fb1c986549b944af39eeda258fc9e4e" @@ -8630,7 +10545,7 @@ parse-entities@^4.0.0: is-decimal "^2.0.0" is-hexadecimal "^2.0.0" -parse-json@^5.0.0, parse-json@^5.2.0: +parse-json@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== @@ -8683,15 +10598,10 @@ path-browserify@1.0.1, path-browserify@^1.0.1: resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-1.0.1.tgz#d98454a9c3753d5790860f16f68867b9e46be1fd" integrity sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g== -path-exists@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" - integrity sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ== - -path-exists@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" - integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== +path-data-parser@0.1.0, path-data-parser@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/path-data-parser/-/path-data-parser-0.1.0.tgz#8f5ba5cc70fc7becb3dcefaea08e2659aba60b8c" + integrity sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w== path-exists@^5.0.0: version "5.0.0" @@ -8743,10 +10653,10 @@ path-to-regexp@0.1.7: resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ== -path-to-regexp@2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-2.2.1.tgz#90b617025a16381a879bc82a38d4e8bdeb2bcf45" - integrity sha512-gu9bD6Ta5bwGrrU8muHzVOBFFREpp2iRkVfhBJahwJ6p6Xw20SjT0MxLnwkjOibQmGSYhiUnf2FLe7k+jcFmGQ== +path-to-regexp@3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-3.3.0.tgz#f7f31d32e8518c2660862b644414b6d5c63a611b" + integrity sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw== path-to-regexp@^1.7.0: version "1.8.0" @@ -8768,16 +10678,10 @@ path@0.12.7: process "^0.11.1" util "^0.10.3" -pbkdf2@^3.0.3, pbkdf2@^3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.1.2.tgz#dd822aa0887580e52f1a039dc3eda108efae3075" - integrity sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA== - dependencies: - create-hash "^1.1.2" - create-hmac "^1.1.4" - ripemd160 "^2.0.1" - safe-buffer "^5.0.1" - sha.js "^2.4.8" +pathe@^2.0.1, pathe@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/pathe/-/pathe-2.0.3.tgz#3ecbec55421685b70a9da872b2cff3e1cbed1716" + integrity sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w== pend@~1.2.0: version "1.2.0" @@ -8798,6 +10702,11 @@ picocolors@^1.0.0, picocolors@^1.0.1: resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.1.tgz#a8ad579b571952f0e5d25892de5445bcfe25aaa1" integrity sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew== +picocolors@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" + integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== + picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3, picomatch@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" @@ -8820,22 +10729,48 @@ pkg-dir@^7.0.0: dependencies: find-up "^6.3.0" -pkg-up@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/pkg-up/-/pkg-up-3.1.0.tgz#100ec235cc150e4fd42519412596a28512a0def5" - integrity sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA== +pkg-types@^1.3.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/pkg-types/-/pkg-types-1.3.1.tgz#bd7cc70881192777eef5326c19deb46e890917df" + integrity sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ== dependencies: - find-up "^3.0.0" + confbox "^0.1.8" + mlly "^1.7.4" + pathe "^2.0.1" + +pkg-types@^2.0.1: + version "2.1.0" + resolved "https://registry.yarnpkg.com/pkg-types/-/pkg-types-2.1.0.tgz#70c9e1b9c74b63fdde749876ee0aa007ea9edead" + integrity sha512-wmJwA+8ihJixSoHKxZJRBQG1oY8Yr9pGLzRmSsNms0iNWyHHAlZCa7mmKiFR10YPZuz/2k169JiS/inOjBCZ2A== + dependencies: + confbox "^0.2.1" + exsolve "^1.0.1" + pathe "^2.0.3" pluralize@^8.0.0: version "8.0.0" resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-8.0.0.tgz#1a6fa16a38d12a1901e0320fa017051c539ce3b1" integrity sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA== -possible-typed-array-names@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz#89bb63c6fada2c3e90adc4a647beeeb39cc7bf8f" - integrity sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q== +points-on-curve@0.2.0, points-on-curve@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/points-on-curve/-/points-on-curve-0.2.0.tgz#7dbb98c43791859434284761330fa893cb81b4d1" + integrity sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A== + +points-on-path@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/points-on-path/-/points-on-path-0.2.1.tgz#553202b5424c53bed37135b318858eacff85dd52" + integrity sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g== + dependencies: + path-data-parser "0.1.0" + points-on-curve "0.2.0" + +postcss-attribute-case-insensitive@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-7.0.1.tgz#0c4500e3bcb2141848e89382c05b5a31c23033a3" + integrity sha512-Uai+SupNSqzlschRyNx3kbCTWgY/2hcwtHEI/ej2LJWc9JJ77qKgGptd8DHwY1mXtZ7Aoh4z4yxfwMBue9eNgw== + dependencies: + postcss-selector-parser "^7.0.0" postcss-calc@^9.0.1: version "9.0.1" @@ -8845,6 +10780,40 @@ postcss-calc@^9.0.1: postcss-selector-parser "^6.0.11" postcss-value-parser "^4.2.0" +postcss-clamp@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/postcss-clamp/-/postcss-clamp-4.1.0.tgz#7263e95abadd8c2ba1bd911b0b5a5c9c93e02363" + integrity sha512-ry4b1Llo/9zz+PKC+030KUnPITTJAHeOwjfAyyB60eT0AorGLdzp52s31OsPRHRf8NchkgFoG2y6fCfn1IV1Ow== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-color-functional-notation@^7.0.10: + version "7.0.10" + resolved "https://registry.yarnpkg.com/postcss-color-functional-notation/-/postcss-color-functional-notation-7.0.10.tgz#f1e9c3e4371889dcdfeabfa8515464fd8338cedc" + integrity sha512-k9qX+aXHBiLTRrWoCJuUFI6F1iF6QJQUXNVWJVSbqZgj57jDhBlOvD8gNUGl35tgqDivbGLhZeW3Ongz4feuKA== + dependencies: + "@csstools/css-color-parser" "^3.0.10" + "@csstools/css-parser-algorithms" "^3.0.5" + "@csstools/css-tokenizer" "^3.0.4" + "@csstools/postcss-progressive-custom-properties" "^4.1.0" + "@csstools/utilities" "^2.0.0" + +postcss-color-hex-alpha@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/postcss-color-hex-alpha/-/postcss-color-hex-alpha-10.0.0.tgz#5dd3eba1f8facb4ea306cba6e3f7712e876b0c76" + integrity sha512-1kervM2cnlgPs2a8Vt/Qbe5cQ++N7rkYo/2rz2BkqJZIHQwaVuJgQH38REHrAi4uM0b1fqxMkWYmese94iMp3w== + dependencies: + "@csstools/utilities" "^2.0.0" + postcss-value-parser "^4.2.0" + +postcss-color-rebeccapurple@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/postcss-color-rebeccapurple/-/postcss-color-rebeccapurple-10.0.0.tgz#5ada28406ac47e0796dff4056b0a9d5a6ecead98" + integrity sha512-JFta737jSP+hdAIEhk1Vs0q0YF5P8fFcj+09pweS8ktuGuZ8pPlykHsk6mPxZ8awDl4TrcxUqJo9l1IhVr/OjQ== + dependencies: + "@csstools/utilities" "^2.0.0" + postcss-value-parser "^4.2.0" + postcss-colormin@^6.1.0: version "6.1.0" resolved "https://registry.yarnpkg.com/postcss-colormin/-/postcss-colormin-6.1.0.tgz#076e8d3fb291fbff7b10e6b063be9da42ff6488d" @@ -8863,6 +10832,44 @@ postcss-convert-values@^6.1.0: browserslist "^4.23.0" postcss-value-parser "^4.2.0" +postcss-custom-media@^11.0.6: + version "11.0.6" + resolved "https://registry.yarnpkg.com/postcss-custom-media/-/postcss-custom-media-11.0.6.tgz#6b450e5bfa209efb736830066682e6567bd04967" + integrity sha512-C4lD4b7mUIw+RZhtY7qUbf4eADmb7Ey8BFA2px9jUbwg7pjTZDl4KY4bvlUV+/vXQvzQRfiGEVJyAbtOsCMInw== + dependencies: + "@csstools/cascade-layer-name-parser" "^2.0.5" + "@csstools/css-parser-algorithms" "^3.0.5" + "@csstools/css-tokenizer" "^3.0.4" + "@csstools/media-query-list-parser" "^4.0.3" + +postcss-custom-properties@^14.0.5: + version "14.0.5" + resolved "https://registry.yarnpkg.com/postcss-custom-properties/-/postcss-custom-properties-14.0.5.tgz#a180444de695f6e11ee2390be93ff6537663e86c" + integrity sha512-UWf/vhMapZatv+zOuqlfLmYXeOhhHLh8U8HAKGI2VJ00xLRYoAJh4xv8iX6FB6+TLXeDnm0DBLMi00E0hodbQw== + dependencies: + "@csstools/cascade-layer-name-parser" "^2.0.5" + "@csstools/css-parser-algorithms" "^3.0.5" + "@csstools/css-tokenizer" "^3.0.4" + "@csstools/utilities" "^2.0.0" + postcss-value-parser "^4.2.0" + +postcss-custom-selectors@^8.0.5: + version "8.0.5" + resolved "https://registry.yarnpkg.com/postcss-custom-selectors/-/postcss-custom-selectors-8.0.5.tgz#9448ed37a12271d7ab6cb364b6f76a46a4a323e8" + integrity sha512-9PGmckHQswiB2usSO6XMSswO2yFWVoCAuih1yl9FVcwkscLjRKjwsjM3t+NIWpSU2Jx3eOiK2+t4vVTQaoCHHg== + dependencies: + "@csstools/cascade-layer-name-parser" "^2.0.5" + "@csstools/css-parser-algorithms" "^3.0.5" + "@csstools/css-tokenizer" "^3.0.4" + postcss-selector-parser "^7.0.0" + +postcss-dir-pseudo-class@^9.0.1: + version "9.0.1" + resolved "https://registry.yarnpkg.com/postcss-dir-pseudo-class/-/postcss-dir-pseudo-class-9.0.1.tgz#80d9e842c9ae9d29f6bf5fd3cf9972891d6cc0ca" + integrity sha512-tRBEK0MHYvcMUrAuYMEOa0zg9APqirBcgzi6P21OhxtJyJADo/SWBwY1CAwEohQ/6HDaa9jCjLRG7K3PVQYHEA== + dependencies: + postcss-selector-parser "^7.0.0" + postcss-discard-comments@^6.0.2: version "6.0.2" resolved "https://registry.yarnpkg.com/postcss-discard-comments/-/postcss-discard-comments-6.0.2.tgz#e768dcfdc33e0216380623652b0a4f69f4678b6c" @@ -8890,6 +10897,47 @@ postcss-discard-unused@^6.0.5: dependencies: postcss-selector-parser "^6.0.16" +postcss-double-position-gradients@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/postcss-double-position-gradients/-/postcss-double-position-gradients-6.0.2.tgz#185f8eab2db9cf4e34be69b5706c905895bb52ae" + integrity sha512-7qTqnL7nfLRyJK/AHSVrrXOuvDDzettC+wGoienURV8v2svNbu6zJC52ruZtHaO6mfcagFmuTGFdzRsJKB3k5Q== + dependencies: + "@csstools/postcss-progressive-custom-properties" "^4.1.0" + "@csstools/utilities" "^2.0.0" + postcss-value-parser "^4.2.0" + +postcss-focus-visible@^10.0.1: + version "10.0.1" + resolved "https://registry.yarnpkg.com/postcss-focus-visible/-/postcss-focus-visible-10.0.1.tgz#1f7904904368a2d1180b220595d77b6f8a957868" + integrity sha512-U58wyjS/I1GZgjRok33aE8juW9qQgQUNwTSdxQGuShHzwuYdcklnvK/+qOWX1Q9kr7ysbraQ6ht6r+udansalA== + dependencies: + postcss-selector-parser "^7.0.0" + +postcss-focus-within@^9.0.1: + version "9.0.1" + resolved "https://registry.yarnpkg.com/postcss-focus-within/-/postcss-focus-within-9.0.1.tgz#ac01ce80d3f2e8b2b3eac4ff84f8e15cd0057bc7" + integrity sha512-fzNUyS1yOYa7mOjpci/bR+u+ESvdar6hk8XNK/TRR0fiGTp2QT5N+ducP0n3rfH/m9I7H/EQU6lsa2BrgxkEjw== + dependencies: + postcss-selector-parser "^7.0.0" + +postcss-font-variant@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/postcss-font-variant/-/postcss-font-variant-5.0.0.tgz#efd59b4b7ea8bb06127f2d031bfbb7f24d32fa66" + integrity sha512-1fmkBaCALD72CK2a9i468mA/+tr9/1cBxRRMXOUaZqO43oWPR5imcyPjXwuv7PXbCid4ndlP5zWhidQVVa3hmA== + +postcss-gap-properties@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/postcss-gap-properties/-/postcss-gap-properties-6.0.0.tgz#d5ff0bdf923c06686499ed2b12e125fe64054fed" + integrity sha512-Om0WPjEwiM9Ru+VhfEDPZJAKWUd0mV1HmNXqp2C29z80aQ2uP9UVhLc7e3aYMIor/S5cVhoPgYQ7RtfeZpYTRw== + +postcss-image-set-function@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/postcss-image-set-function/-/postcss-image-set-function-7.0.0.tgz#538e94e16716be47f9df0573b56bbaca86e1da53" + integrity sha512-QL7W7QNlZuzOwBTeXEmbVckNt1FSmhQtbMRvGGqqU4Nf4xk6KUEQhAoWuMzwbSv5jxiRiSZ5Tv7eiDB9U87znA== + dependencies: + "@csstools/utilities" "^2.0.0" + postcss-value-parser "^4.2.0" + postcss-import@^15.1.0: version "15.1.0" resolved "https://registry.yarnpkg.com/postcss-import/-/postcss-import-15.1.0.tgz#41c64ed8cc0e23735a9698b3249ffdbf704adc70" @@ -8906,6 +10954,17 @@ postcss-js@^4.0.1: dependencies: camelcase-css "^2.0.1" +postcss-lab-function@^7.0.10: + version "7.0.10" + resolved "https://registry.yarnpkg.com/postcss-lab-function/-/postcss-lab-function-7.0.10.tgz#0537bd7245b935fc133298c8896bcbd160540cae" + integrity sha512-tqs6TCEv9tC1Riq6fOzHuHcZyhg4k3gIAMB8GGY/zA1ssGdm6puHMVE7t75aOSoFg7UD2wyrFFhbldiCMyyFTQ== + dependencies: + "@csstools/css-color-parser" "^3.0.10" + "@csstools/css-parser-algorithms" "^3.0.5" + "@csstools/css-tokenizer" "^3.0.4" + "@csstools/postcss-progressive-custom-properties" "^4.1.0" + "@csstools/utilities" "^2.0.0" + postcss-load-config@^4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-4.0.2.tgz#7159dcf626118d33e299f485d6afe4aff7c4a3e3" @@ -8923,6 +10982,13 @@ postcss-loader@^7.3.3: jiti "^1.20.0" semver "^7.5.4" +postcss-logical@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/postcss-logical/-/postcss-logical-8.1.0.tgz#4092b16b49e3ecda70c4d8945257da403d167228" + integrity sha512-pL1hXFQ2fEXNKiNiAgtfA005T9FBxky5zkX6s4GZM2D8RkVgRqz3f4g1JUoq925zXv495qk8UNldDwh8uGEDoA== + dependencies: + postcss-value-parser "^4.2.0" + postcss-merge-idents@^6.0.3: version "6.0.3" resolved "https://registry.yarnpkg.com/postcss-merge-idents/-/postcss-merge-idents-6.0.3.tgz#7b9c31c7bc823c94bec50f297f04e3c2b838ea65" @@ -9016,6 +11082,15 @@ postcss-nested@^6.0.1: dependencies: postcss-selector-parser "^6.0.11" +postcss-nesting@^13.0.1: + version "13.0.1" + resolved "https://registry.yarnpkg.com/postcss-nesting/-/postcss-nesting-13.0.1.tgz#c405796d7245a3e4c267a9956cacfe9670b5d43e" + integrity sha512-VbqqHkOBOt4Uu3G8Dm8n6lU5+9cJFxiuty9+4rcoyRPO9zZS1JIs6td49VIoix3qYqELHlJIn46Oih9SAKo+yQ== + dependencies: + "@csstools/selector-resolve-nested" "^3.0.0" + "@csstools/selector-specificity" "^5.0.0" + postcss-selector-parser "^7.0.0" + postcss-normalize-charset@^6.0.2: version "6.0.2" resolved "https://registry.yarnpkg.com/postcss-normalize-charset/-/postcss-normalize-charset-6.0.2.tgz#1ec25c435057a8001dac942942a95ffe66f721e1" @@ -9078,6 +11153,11 @@ postcss-normalize-whitespace@^6.0.2: dependencies: postcss-value-parser "^4.2.0" +postcss-opacity-percentage@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/postcss-opacity-percentage/-/postcss-opacity-percentage-3.0.0.tgz#0b0db5ed5db5670e067044b8030b89c216e1eb0a" + integrity sha512-K6HGVzyxUxd/VgZdX04DCtdwWJ4NGLG212US4/LA1TLAbHgmAsTWVR86o+gGIbFtnTkfOpb9sCRBx8K7HO66qQ== + postcss-ordered-values@^6.0.2: version "6.0.2" resolved "https://registry.yarnpkg.com/postcss-ordered-values/-/postcss-ordered-values-6.0.2.tgz#366bb663919707093451ab70c3f99c05672aaae5" @@ -9086,6 +11166,102 @@ postcss-ordered-values@^6.0.2: cssnano-utils "^4.0.2" postcss-value-parser "^4.2.0" +postcss-overflow-shorthand@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/postcss-overflow-shorthand/-/postcss-overflow-shorthand-6.0.0.tgz#f5252b4a2ee16c68cd8a9029edb5370c4a9808af" + integrity sha512-BdDl/AbVkDjoTofzDQnwDdm/Ym6oS9KgmO7Gr+LHYjNWJ6ExORe4+3pcLQsLA9gIROMkiGVjjwZNoL/mpXHd5Q== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-page-break@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/postcss-page-break/-/postcss-page-break-3.0.4.tgz#7fbf741c233621622b68d435babfb70dd8c1ee5f" + integrity sha512-1JGu8oCjVXLa9q9rFTo4MbeeA5FMe00/9C7lN4va606Rdb+HkxXtXsmEDrIraQ11fGz/WvKWa8gMuCKkrXpTsQ== + +postcss-place@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/postcss-place/-/postcss-place-10.0.0.tgz#ba36ee4786ca401377ced17a39d9050ed772e5a9" + integrity sha512-5EBrMzat2pPAxQNWYavwAfoKfYcTADJ8AXGVPcUZ2UkNloUTWzJQExgrzrDkh3EKzmAx1evfTAzF9I8NGcc+qw== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-preset-env@^10.1.0: + version "10.2.0" + resolved "https://registry.yarnpkg.com/postcss-preset-env/-/postcss-preset-env-10.2.0.tgz#ea95a6fc70efb1a26f81e5cf0ccdb321a3439b6e" + integrity sha512-cl13sPBbSqo1Q7Ryb19oT5NZO5IHFolRbIMdgDq4f9w1MHYiL6uZS7uSsjXJ1KzRIcX5BMjEeyxmAevVXENa3Q== + dependencies: + "@csstools/postcss-cascade-layers" "^5.0.1" + "@csstools/postcss-color-function" "^4.0.10" + "@csstools/postcss-color-mix-function" "^3.0.10" + "@csstools/postcss-color-mix-variadic-function-arguments" "^1.0.0" + "@csstools/postcss-content-alt-text" "^2.0.6" + "@csstools/postcss-exponential-functions" "^2.0.9" + "@csstools/postcss-font-format-keywords" "^4.0.0" + "@csstools/postcss-gamut-mapping" "^2.0.10" + "@csstools/postcss-gradients-interpolation-method" "^5.0.10" + "@csstools/postcss-hwb-function" "^4.0.10" + "@csstools/postcss-ic-unit" "^4.0.2" + "@csstools/postcss-initial" "^2.0.1" + "@csstools/postcss-is-pseudo-class" "^5.0.1" + "@csstools/postcss-light-dark-function" "^2.0.9" + "@csstools/postcss-logical-float-and-clear" "^3.0.0" + "@csstools/postcss-logical-overflow" "^2.0.0" + "@csstools/postcss-logical-overscroll-behavior" "^2.0.0" + "@csstools/postcss-logical-resize" "^3.0.0" + "@csstools/postcss-logical-viewport-units" "^3.0.4" + "@csstools/postcss-media-minmax" "^2.0.9" + "@csstools/postcss-media-queries-aspect-ratio-number-values" "^3.0.5" + "@csstools/postcss-nested-calc" "^4.0.0" + "@csstools/postcss-normalize-display-values" "^4.0.0" + "@csstools/postcss-oklab-function" "^4.0.10" + "@csstools/postcss-progressive-custom-properties" "^4.1.0" + "@csstools/postcss-random-function" "^2.0.1" + "@csstools/postcss-relative-color-syntax" "^3.0.10" + "@csstools/postcss-scope-pseudo-class" "^4.0.1" + "@csstools/postcss-sign-functions" "^1.1.4" + "@csstools/postcss-stepped-value-functions" "^4.0.9" + "@csstools/postcss-text-decoration-shorthand" "^4.0.2" + "@csstools/postcss-trigonometric-functions" "^4.0.9" + "@csstools/postcss-unset-value" "^4.0.0" + autoprefixer "^10.4.21" + browserslist "^4.24.5" + css-blank-pseudo "^7.0.1" + css-has-pseudo "^7.0.2" + css-prefers-color-scheme "^10.0.0" + cssdb "^8.3.0" + postcss-attribute-case-insensitive "^7.0.1" + postcss-clamp "^4.1.0" + postcss-color-functional-notation "^7.0.10" + postcss-color-hex-alpha "^10.0.0" + postcss-color-rebeccapurple "^10.0.0" + postcss-custom-media "^11.0.6" + postcss-custom-properties "^14.0.5" + postcss-custom-selectors "^8.0.5" + postcss-dir-pseudo-class "^9.0.1" + postcss-double-position-gradients "^6.0.2" + postcss-focus-visible "^10.0.1" + postcss-focus-within "^9.0.1" + postcss-font-variant "^5.0.0" + postcss-gap-properties "^6.0.0" + postcss-image-set-function "^7.0.0" + postcss-lab-function "^7.0.10" + postcss-logical "^8.1.0" + postcss-nesting "^13.0.1" + postcss-opacity-percentage "^3.0.0" + postcss-overflow-shorthand "^6.0.0" + postcss-page-break "^3.0.4" + postcss-place "^10.0.0" + postcss-pseudo-class-any-link "^10.0.1" + postcss-replace-overflow-wrap "^4.0.0" + postcss-selector-not "^8.0.1" + +postcss-pseudo-class-any-link@^10.0.1: + version "10.0.1" + resolved "https://registry.yarnpkg.com/postcss-pseudo-class-any-link/-/postcss-pseudo-class-any-link-10.0.1.tgz#06455431171bf44b84d79ebaeee9fd1c05946544" + integrity sha512-3el9rXlBOqTFaMFkWDOkHUTQekFIYnaQY55Rsp8As8QQkpiSgIYEcF/6Ond93oHiDsGb4kad8zjt+NPlOC1H0Q== + dependencies: + postcss-selector-parser "^7.0.0" + postcss-reduce-idents@^6.0.3: version "6.0.3" resolved "https://registry.yarnpkg.com/postcss-reduce-idents/-/postcss-reduce-idents-6.0.3.tgz#b0d9c84316d2a547714ebab523ec7d13704cd486" @@ -9108,6 +11284,18 @@ postcss-reduce-transforms@^6.0.2: dependencies: postcss-value-parser "^4.2.0" +postcss-replace-overflow-wrap@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/postcss-replace-overflow-wrap/-/postcss-replace-overflow-wrap-4.0.0.tgz#d2df6bed10b477bf9c52fab28c568b4b29ca4319" + integrity sha512-KmF7SBPphT4gPPcKZc7aDkweHiKEEO8cla/GjcBK+ckKxiZslIu3C4GCRW3DNfL0o7yW7kMQu9xlZ1kXRXLXtw== + +postcss-selector-not@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/postcss-selector-not/-/postcss-selector-not-8.0.1.tgz#f2df9c6ac9f95e9fe4416ca41a957eda16130172" + integrity sha512-kmVy/5PYVb2UOhy0+LqUYAhKj7DUGDpSWa5LZqlkWJaaAV+dxxsOG3+St0yNLu6vsKD7Dmqx+nWQt0iil89+WA== + dependencies: + postcss-selector-parser "^7.0.0" + postcss-selector-parser@^6.0.11, postcss-selector-parser@^6.0.16, postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4: version "6.1.0" resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.1.0.tgz#49694cb4e7c649299fea510a29fa6577104bcf53" @@ -9116,6 +11304,14 @@ postcss-selector-parser@^6.0.11, postcss-selector-parser@^6.0.16, postcss-select cssesc "^3.0.0" util-deprecate "^1.0.2" +postcss-selector-parser@^7.0.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz#4d6af97eba65d73bc4d84bcb343e865d7dd16262" + integrity sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA== + dependencies: + cssesc "^3.0.0" + util-deprecate "^1.0.2" + postcss-sort-media-queries@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/postcss-sort-media-queries/-/postcss-sort-media-queries-5.2.0.tgz#4556b3f982ef27d3bac526b99b6c0d3359a6cf97" @@ -9246,7 +11442,7 @@ pretty-time@^1.1.0: resolved "https://registry.yarnpkg.com/pretty-time/-/pretty-time-1.1.0.tgz#ffb7429afabb8535c346a34e41873adf3d74dd0e" integrity sha512-28iF6xPQrP8Oa6uxE6a1biz+lWeTOAPKggvjB8HAs6nVMKZwf5bG++632Dx614hIWgUPkgivRfG+a8uAXGTIbA== -prism-react-renderer@^2.0.6, prism-react-renderer@^2.1.0, prism-react-renderer@^2.3.0: +prism-react-renderer@^2.0.6, prism-react-renderer@^2.3.0: version "2.3.1" resolved "https://registry.yarnpkg.com/prism-react-renderer/-/prism-react-renderer-2.3.1.tgz#e59e5450052ede17488f6bc85de1553f584ff8d5" integrity sha512-Rdf+HzBLR7KYjzpJ1rSoxT9ioO85nZngQEoFIhL07XhtJHlCU3SOz0GJ6+qvMyQe0Se+BV3qpe6Yd/NmQF5Juw== @@ -9314,18 +11510,6 @@ proxy-from-env@1.1.0: resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== -public-encrypt@^4.0.0: - version "4.0.3" - resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.3.tgz#4fcc9d77a07e48ba7527e7cbe0de33d0701331e0" - integrity sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q== - dependencies: - bn.js "^4.1.0" - browserify-rsa "^4.0.0" - create-hash "^1.1.0" - parse-asn1 "^5.0.0" - randombytes "^2.0.1" - safe-buffer "^5.1.2" - pump@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" @@ -9334,7 +11518,7 @@ pump@^3.0.0: end-of-stream "^1.1.0" once "^1.3.1" -punycode@^1.3.2, punycode@^1.4.1: +punycode@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" integrity sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ== @@ -9384,50 +11568,35 @@ qs@6.11.0: dependencies: side-channel "^1.0.4" -qs@^6.11.2: - version "6.12.1" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.12.1.tgz#39422111ca7cbdb70425541cba20c7d7b216599a" - integrity sha512-zWmv4RSuB9r2mYQw3zxQuHWeU+42aKi1wWig/j4ele4ygELZ7PEO6MM7rim9oAQH2A5MWfsAVf/jPvTPgCbvUQ== +qs@^6.12.3: + version "6.14.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.14.0.tgz#c63fa40680d2c5c941412a0e899c89af60c0a930" + integrity sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w== dependencies: - side-channel "^1.0.6" + side-channel "^1.1.0" -querystring-es3@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73" - integrity sha512-773xhDQnZBMFobEiztv8LIl70ch5MSF/jUQVlhwFyBILqq96anmoctVIYz+ZRp0qbCKATTn6ev02M3r7Ga5vqA== +quansync@^0.2.8: + version "0.2.10" + resolved "https://registry.yarnpkg.com/quansync/-/quansync-0.2.10.tgz#32053cf166fa36511aae95fc49796116f2dc20e1" + integrity sha512-t41VRkMYbkHyCYmOvx/6URnN80H7k4X0lLdBMGsz+maAwrJQYB1djpV6vHrQIBE0WBSGqhtEHrK9U3DWWH8v7A== queue-microtask@^1.2.2: version "1.2.3" resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== -queue@6.0.2: - version "6.0.2" - resolved "https://registry.yarnpkg.com/queue/-/queue-6.0.2.tgz#b91525283e2315c7553d2efa18d83e76432fed65" - integrity sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA== - dependencies: - inherits "~2.0.3" - quick-lru@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932" integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA== -randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5, randombytes@^2.1.0: +randombytes@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== dependencies: safe-buffer "^5.1.0" -randomfill@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/randomfill/-/randomfill-1.0.4.tgz#c92196fc86ab42be983f1bf31778224931d61458" - integrity sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw== - dependencies: - randombytes "^2.0.5" - safe-buffer "^5.1.0" - range-parser@1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e" @@ -9474,36 +11643,6 @@ react-copy-to-clipboard@^5.1.0: copy-to-clipboard "^3.3.1" prop-types "^15.8.1" -react-dev-utils@^12.0.1: - version "12.0.1" - resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-12.0.1.tgz#ba92edb4a1f379bd46ccd6bcd4e7bc398df33e73" - integrity sha512-84Ivxmr17KjUupyqzFode6xKhjwuEJDROWKJy/BthkL7Wn6NJ8h4WE6k/exAv6ImS+0oZLRRW5j/aINMHyeGeQ== - dependencies: - "@babel/code-frame" "^7.16.0" - address "^1.1.2" - browserslist "^4.18.1" - chalk "^4.1.2" - cross-spawn "^7.0.3" - detect-port-alt "^1.1.6" - escape-string-regexp "^4.0.0" - filesize "^8.0.6" - find-up "^5.0.0" - fork-ts-checker-webpack-plugin "^6.5.0" - global-modules "^2.0.0" - globby "^11.0.4" - gzip-size "^6.0.0" - immer "^9.0.7" - is-root "^2.1.0" - loader-utils "^3.2.0" - open "^8.4.0" - pkg-up "^3.1.0" - prompts "^2.4.2" - react-error-overlay "^6.0.11" - recursive-readdir "^2.2.2" - shell-quote "^1.7.3" - strip-ansi "^6.0.1" - text-table "^0.2.0" - react-dom@^18.2.0: version "18.3.1" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.3.1.tgz#c2265d79511b57d479b3dd3fdfa51536494c5cb4" @@ -9512,12 +11651,7 @@ react-dom@^18.2.0: loose-envify "^1.1.0" scheduler "^0.23.2" -react-error-overlay@^6.0.11: - version "6.0.11" - resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.11.tgz#92835de5841c5cf08ba00ddd2d677b6d17ff9adb" - integrity sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg== - -react-fast-compare@^3.0.1, react-fast-compare@^3.2.0, react-fast-compare@^3.2.2: +react-fast-compare@^3.0.1, react-fast-compare@^3.2.0: version "3.2.2" resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.2.tgz#929a97a532304ce9fee4bcae44234f1ce2c21d49" integrity sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ== @@ -9527,19 +11661,10 @@ react-google-charts@^5.2.1: resolved "https://registry.yarnpkg.com/react-google-charts/-/react-google-charts-5.2.1.tgz#d9cbe8ed45d7c0fafefea5c7c3361bee76648454" integrity sha512-mCbPiObP8yWM5A9ogej7Qp3/HX4EzOwuEzUYvcfHtL98Xt4V/brD14KgfDzSNNtyD48MNXCpq5oVaYKt0ykQUQ== -react-helmet-async@*: - version "2.0.5" - resolved "https://registry.yarnpkg.com/react-helmet-async/-/react-helmet-async-2.0.5.tgz#cfc70cd7bb32df7883a8ed55502a1513747223ec" - integrity sha512-rYUYHeus+i27MvFE+Jaa4WsyBKGkL6qVgbJvSBoX8mbsWoABJXdEO0bZyi0F6i+4f0NuIb8AvqPMj3iXFHkMwg== - dependencies: - invariant "^2.2.4" - react-fast-compare "^3.2.2" - shallowequal "^1.1.0" - -react-helmet-async@^1.3.0: +react-helmet-async@^1.3.0, "react-helmet-async@npm:@slorber/react-helmet-async@1.3.0": version "1.3.0" - resolved "https://registry.yarnpkg.com/react-helmet-async/-/react-helmet-async-1.3.0.tgz#7bd5bf8c5c69ea9f02f6083f14ce33ef545c222e" - integrity sha512-9jZ57/dAn9t3q6hneQS0wukqC2ENOBgMNVEhb/ZG9ZSxUetzVIw4iAmEU38IaVg3QGYauQPhSeUTuIUtFglWpg== + resolved "https://registry.yarnpkg.com/@slorber/react-helmet-async/-/react-helmet-async-1.3.0.tgz#11fbc6094605cf60aa04a28c17e0aab894b4ecff" + integrity sha512-e9/OK8VhwUSc67diWI8Rb3I0YgI9/SBQtnhe9aEuK6MhZm7ntZZimXgwXnd8W96YTmSOb9M4d8LwhRZyhWr/1A== dependencies: "@babel/runtime" "^7.12.5" invariant "^2.2.4" @@ -9567,10 +11692,10 @@ react-is@^18.0.0: resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.3.1.tgz#e83557dc12eae63a99e003a46388b1dcbb44db7e" integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg== -react-json-view-lite@^1.2.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/react-json-view-lite/-/react-json-view-lite-1.4.0.tgz#0ff493245f4550abe5e1f1836f170fa70bb95914" - integrity sha512-wh6F6uJyYAmQ4fK0e8dSQMEWuvTs2Wr3el3sLD9bambX1+pSWUVXIz1RFaoy3TI1mZ0FqdpKq9YgbgTTgyrmXA== +react-json-view-lite@^2.3.0: + version "2.4.1" + resolved "https://registry.yarnpkg.com/react-json-view-lite/-/react-json-view-lite-2.4.1.tgz#0d06696a06aaf4a74e890302b76cf8cddcc45d60" + integrity sha512-fwFYknRIBxjbFm0kBDrzgBy1xa5tDg2LyXXBepC5f1b+MY3BUClMCsvanMPn089JbV1Eg3nZcrp0VCuH43aXnA== react-lifecycles-compat@^3.0.0: version "3.0.4" @@ -9708,7 +11833,7 @@ read-cache@^1.0.0: dependencies: pify "^2.3.0" -readable-stream@^2.0.1, readable-stream@^2.3.8: +readable-stream@^2.0.1: version "2.3.8" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== @@ -9721,7 +11846,7 @@ readable-stream@^2.0.1, readable-stream@^2.3.8: string_decoder "~1.1.1" util-deprecate "~1.0.1" -readable-stream@^3.0.6, readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.5.0, readable-stream@^3.6.0: +readable-stream@^3.0.6, readable-stream@^3.1.1, readable-stream@^3.4.0: version "3.6.2" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== @@ -9730,16 +11855,10 @@ readable-stream@^3.0.6, readable-stream@^3.1.1, readable-stream@^3.4.0, readable string_decoder "^1.1.1" util-deprecate "^1.0.1" -readable-stream@^4.0.0: - version "4.5.2" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-4.5.2.tgz#9e7fc4c45099baeed934bff6eb97ba6cf2729e09" - integrity sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g== - dependencies: - abort-controller "^3.0.0" - buffer "^6.0.3" - events "^3.3.0" - process "^0.11.10" - string_decoder "^1.3.0" +readdirp@^4.0.1: + version "4.1.2" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-4.1.2.tgz#eb85801435fbf2a7ee58f19e0921b068fc69948d" + integrity sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg== readdirp@~3.6.0: version "3.6.0" @@ -9748,11 +11867,6 @@ readdirp@~3.6.0: dependencies: picomatch "^2.2.1" -reading-time@^1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/reading-time/-/reading-time-1.5.0.tgz#d2a7f1b6057cb2e169beaf87113cc3411b5bc5bb" - integrity sha512-onYyVhBNr4CmAxFsKS7bz+uTLRakypIe4R+5A824vBSkQy/hB3fZepoVEf8OVAxzLvK+H/jm9TzpI3ETSm64Kg== - rechoir@^0.6.2: version "0.6.2" resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384" @@ -9760,13 +11874,6 @@ rechoir@^0.6.2: dependencies: resolve "^1.1.6" -recursive-readdir@^2.2.2: - version "2.2.3" - resolved "https://registry.yarnpkg.com/recursive-readdir/-/recursive-readdir-2.2.3.tgz#e726f328c0d69153bcabd5c322d3195252379372" - integrity sha512-8HrF5ZsXk5FAH9dgsx3BlUer73nIhuj+9OrQwEbLTPOBzGkL1lsFCR01am+v+0m2Cmbs1nP12hLDl5FA7EszKA== - dependencies: - minimatch "^3.0.5" - redux-thunk@^2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.4.2.tgz#b9d05d11994b99f7a91ea223e8b04cf0afa5ef3b" @@ -9791,6 +11898,13 @@ regenerate-unicode-properties@^10.1.0: dependencies: regenerate "^1.4.2" +regenerate-unicode-properties@^10.2.0: + version "10.2.0" + resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.0.tgz#626e39df8c372338ea9b8028d1f99dc3fd9c3db0" + integrity sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA== + dependencies: + regenerate "^1.4.2" + regenerate@^1.4.2: version "1.4.2" resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a" @@ -9820,6 +11934,18 @@ regexpu-core@^5.3.1: unicode-match-property-ecmascript "^2.0.0" unicode-match-property-value-ecmascript "^2.1.0" +regexpu-core@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-6.2.0.tgz#0e5190d79e542bf294955dccabae04d3c7d53826" + integrity sha512-H66BPQMrv+V16t8xtmq+UC0CBpiTBA60V8ibS1QVReIp8T1z8hwFxqcGzm9K6lgsN7sB5edVH8a+ze6Fqm4weA== + dependencies: + regenerate "^1.4.2" + regenerate-unicode-properties "^10.2.0" + regjsgen "^0.8.0" + regjsparser "^0.12.0" + unicode-match-property-ecmascript "^2.0.0" + unicode-match-property-value-ecmascript "^2.1.0" + registry-auth-token@^5.0.1: version "5.0.2" resolved "https://registry.yarnpkg.com/registry-auth-token/-/registry-auth-token-5.0.2.tgz#8b026cc507c8552ebbe06724136267e63302f756" @@ -9834,6 +11960,18 @@ registry-url@^6.0.0: dependencies: rc "1.2.8" +regjsgen@^0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.8.0.tgz#df23ff26e0c5b300a6470cad160a9d090c3a37ab" + integrity sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q== + +regjsparser@^0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.12.0.tgz#0e846df6c6530586429377de56e0475583b088dc" + integrity sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ== + dependencies: + jsesc "~3.0.2" + regjsparser@^0.9.1: version "0.9.1" resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.9.1.tgz#272d05aa10c7c1f67095b1ff0addae8442fc5709" @@ -9895,6 +12033,16 @@ remark-frontmatter@^5.0.0: micromark-extension-frontmatter "^2.0.0" unified "^11.0.0" +remark-gfm@3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/remark-gfm/-/remark-gfm-3.0.1.tgz#0b180f095e3036545e9dddac0e8df3fa5cfee54f" + integrity sha512-lEFDoi2PICJyNrACFOfDD3JlLkuSbOa5Wd8EPt06HUdptv8Gn0bxYTdbU/XXQ3swAPkEaGxxPN9cbnMHvVu1Ig== + dependencies: + "@types/mdast" "^3.0.0" + mdast-util-gfm "^2.0.0" + micromark-extension-gfm "^2.0.0" + unified "^10.0.0" + remark-gfm@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/remark-gfm/-/remark-gfm-4.0.0.tgz#aea777f0744701aa288b67d28c43565c7e8c35de" @@ -9975,6 +12123,11 @@ renderkid@^3.0.0: lodash "^4.17.21" strip-ansi "^6.0.1" +repeat-string@^1.0.0: + version "1.6.1" + resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" + integrity sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w== + require-directory@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" @@ -10055,23 +12208,20 @@ rimraf@3.0.2, rimraf@^3.0.2: dependencies: glob "^7.1.3" -ripemd160@^2.0.0, ripemd160@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.2.tgz#a1c1a6f624751577ba5d07914cbc92850585890c" - integrity sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA== - dependencies: - hash-base "^3.0.0" - inherits "^2.0.1" - robust-predicates@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/robust-predicates/-/robust-predicates-3.0.2.tgz#d5b28528c4824d20fc48df1928d41d9efa1ad771" integrity sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg== -rtl-detect@^1.0.4: - version "1.1.2" - resolved "https://registry.yarnpkg.com/rtl-detect/-/rtl-detect-1.1.2.tgz#ca7f0330af5c6bb626c15675c642ba85ad6273c6" - integrity sha512-PGMBq03+TTG/p/cRB7HCLKJ1MgDIi07+QU1faSjiYRfmY5UsAttV9Hs08jDAHVwcOwmVLcSJkpwyfXszVjWfIQ== +roughjs@^4.6.6: + version "4.6.6" + resolved "https://registry.yarnpkg.com/roughjs/-/roughjs-4.6.6.tgz#1059f49a5e0c80dee541a005b20cc322b222158b" + integrity sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ== + dependencies: + hachure-fill "^0.5.2" + path-data-parser "^0.1.0" + points-on-curve "^0.2.0" + points-on-path "^0.2.1" rtlcss@^4.1.0: version "4.1.1" @@ -10095,13 +12245,6 @@ rw@1: resolved "https://registry.yarnpkg.com/rw/-/rw-1.3.3.tgz#3f862dfa91ab766b14885ef4d01124bfda074fb4" integrity sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ== -rxjs@^7.5.4: - version "7.8.1" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.1.tgz#6f6f3d99ea8044291efd92e7c7fcf562c4057543" - integrity sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg== - dependencies: - tslib "^2.1.0" - sade@^1.7.3: version "1.8.1" resolved "https://registry.yarnpkg.com/sade/-/sade-1.8.1.tgz#0a78e81d658d394887be57d2a409bf703a3b2701" @@ -10114,7 +12257,7 @@ safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== -safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@^5.2.1, safe-buffer@~5.2.0: +safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.1.0, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== @@ -10124,32 +12267,23 @@ safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== -sass-loader@^10.1.1: - version "10.5.2" - resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-10.5.2.tgz#1ca30534fff296417b853c7597ca3b0bbe8c37d0" - integrity sha512-vMUoSNOUKJILHpcNCCyD23X34gve1TS7Rjd9uXHeKqhvBG39x6XbswFDtpbTElj6XdMFezoWhkh5vtKudf2cgQ== - dependencies: - klona "^2.0.4" - loader-utils "^2.0.0" - neo-async "^2.6.2" - schema-utils "^3.0.0" - semver "^7.3.2" - -sass-loader@^13.3.2: - version "13.3.3" - resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-13.3.3.tgz#60df5e858788cffb1a3215e5b92e9cba61e7e133" - integrity sha512-mt5YN2F1MOZr3d/wBRcZxeFgwgkH44wVc2zohO2YF6JiOMkiXe4BYRZpSu2sO1g71mo/j16txzUhsKZlqjVGzA== +sass-loader@^16.0.2: + version "16.0.5" + resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-16.0.5.tgz#257bc90119ade066851cafe7f2c3f3504c7cda98" + integrity sha512-oL+CMBXrj6BZ/zOq4os+UECPL+bWqt6OAC6DWS8Ln8GZRcMDjlJ4JC3FBDuHJdYaFWIdKNIBYmtZtK2MaMkNIw== dependencies: neo-async "^2.6.2" -sass@^1.58.1: - version "1.77.5" - resolved "https://registry.yarnpkg.com/sass/-/sass-1.77.5.tgz#5f9009820297521356e962c0bed13ee36710edfe" - integrity sha512-oDfX1mukIlxacPdQqNb6mV2tVCrnE+P3nVYioy72V5tlk56CPNcO4TCuFcaCRKKfJ1M3lH95CleRS+dVKL2qMg== +sass@^1.80.4: + version "1.89.0" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.89.0.tgz#6df72360c5c3ec2a9833c49adafe57b28206752d" + integrity sha512-ld+kQU8YTdGNjOLfRWBzewJpU5cwEv/h5yyqlSeJcj6Yh8U4TDA9UA5FPicqDz/xgRPWRSYIQNiFks21TbA9KQ== dependencies: - chokidar ">=3.0.0 <4.0.0" - immutable "^4.0.0" + chokidar "^4.0.0" + immutable "^5.0.2" source-map-js ">=0.6.2 <2.0.0" + optionalDependencies: + "@parcel/watcher" "^2.4.1" sax@^1.2.4: version "1.4.1" @@ -10163,14 +12297,10 @@ scheduler@^0.23.2: dependencies: loose-envify "^1.1.0" -schema-utils@2.7.0: - version "2.7.0" - resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.0.tgz#17151f76d8eae67fbbf77960c33c676ad9f4efc7" - integrity sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A== - dependencies: - "@types/json-schema" "^7.0.4" - ajv "^6.12.2" - ajv-keywords "^3.4.1" +schema-dts@^1.1.2: + version "1.1.5" + resolved "https://registry.yarnpkg.com/schema-dts/-/schema-dts-1.1.5.tgz#9237725d305bac3469f02b292a035107595dc324" + integrity sha512-RJr9EaCmsLzBX2NDiO5Z3ux2BVosNZN5jo0gWgsyKvxKIUL5R3swNvoorulAeL9kLB0iTSX7V6aokhla2m7xbg== schema-utils@^3.0.0, schema-utils@^3.1.1, schema-utils@^3.2.0: version "3.3.0" @@ -10191,6 +12321,16 @@ schema-utils@^4.0.0, schema-utils@^4.0.1: ajv-formats "^2.1.1" ajv-keywords "^5.1.0" +schema-utils@^4.3.0, schema-utils@^4.3.2: + version "4.3.2" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-4.3.2.tgz#0c10878bf4a73fd2b1dfd14b9462b26788c806ae" + integrity sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ== + dependencies: + "@types/json-schema" "^7.0.9" + ajv "^8.9.0" + ajv-formats "^2.1.1" + ajv-keywords "^5.1.0" + section-matter@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/section-matter/-/section-matter-1.0.0.tgz#e9041953506780ec01d59f292a19c7b850b84167" @@ -10238,7 +12378,7 @@ semver@^6.3.1: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== -semver@^7.3.2, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8, semver@^7.5.4: +semver@^7.3.5, semver@^7.3.7, semver@^7.3.8, semver@^7.5.4: version "7.6.2" resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.2.tgz#1e3b34759f896e8f14d6134732ce798aeb0c6e13" integrity sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w== @@ -10262,25 +12402,24 @@ send@0.18.0: range-parser "~1.2.1" statuses "2.0.1" -serialize-javascript@^6.0.0, serialize-javascript@^6.0.1: +serialize-javascript@^6.0.0, serialize-javascript@^6.0.1, serialize-javascript@^6.0.2: version "6.0.2" resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.2.tgz#defa1e055c83bf6d59ea805d8da862254eb6a6c2" integrity sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g== dependencies: randombytes "^2.1.0" -serve-handler@^6.1.5: - version "6.1.5" - resolved "https://registry.yarnpkg.com/serve-handler/-/serve-handler-6.1.5.tgz#a4a0964f5c55c7e37a02a633232b6f0d6f068375" - integrity sha512-ijPFle6Hwe8zfmBxJdE+5fta53fdIY0lHISJvuikXB3VYFafRjMRpOffSPvCYsbKyBA7pvy9oYr/BT1O3EArlg== +serve-handler@^6.1.6: + version "6.1.6" + resolved "https://registry.yarnpkg.com/serve-handler/-/serve-handler-6.1.6.tgz#50803c1d3e947cd4a341d617f8209b22bd76cfa1" + integrity sha512-x5RL9Y2p5+Sh3D38Fh9i/iQ5ZK+e4xuXRd/pGbM4D13tgo/MGwbttUk8emytcr1YYzBYs+apnUngBDFYfpjPuQ== dependencies: bytes "3.0.0" content-disposition "0.5.2" - fast-url-parser "1.1.3" mime-types "2.1.18" minimatch "3.1.2" path-is-inside "1.0.2" - path-to-regexp "2.2.1" + path-to-regexp "3.3.0" range-parser "1.2.0" serve-index@^1.9.1: @@ -10318,11 +12457,6 @@ set-function-length@^1.2.1: gopd "^1.0.1" has-property-descriptors "^1.0.2" -setimmediate@^1.0.4: - version "1.0.5" - resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" - integrity sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA== - setprototypeof@1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" @@ -10333,14 +12467,6 @@ setprototypeof@1.2.0: resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== -sha.js@^2.4.0, sha.js@^2.4.8: - version "2.4.11" - resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.11.tgz#37a5cf0b81ecbc6943de109ba2960d1b26584ae7" - integrity sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ== - dependencies: - inherits "^2.0.1" - safe-buffer "^5.0.1" - shallow-clone@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3" @@ -10365,12 +12491,12 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== -shell-quote@^1.7.3, shell-quote@^1.8.1: +shell-quote@^1.8.1: version "1.8.1" resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.1.tgz#6dbf4db75515ad5bac63b4f1894c3a154c766680" integrity sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA== -shelljs@0.8.5, shelljs@^0.8.5: +shelljs@0.8.5: version "0.8.5" resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.5.tgz#de055408d8361bed66c669d2f000538ced8ee20c" integrity sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow== @@ -10423,7 +12549,36 @@ should@^13.2.1: should-type-adaptors "^1.0.1" should-util "^1.0.0" -side-channel@^1.0.4, side-channel@^1.0.6: +side-channel-list@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/side-channel-list/-/side-channel-list-1.0.0.tgz#10cb5984263115d3b7a0e336591e290a830af8ad" + integrity sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA== + dependencies: + es-errors "^1.3.0" + object-inspect "^1.13.3" + +side-channel-map@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/side-channel-map/-/side-channel-map-1.0.1.tgz#d6bb6b37902c6fef5174e5f533fab4c732a26f42" + integrity sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + get-intrinsic "^1.2.5" + object-inspect "^1.13.3" + +side-channel-weakmap@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz#11dda19d5368e40ce9ec2bdc1fb0ecbc0790ecea" + integrity sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + get-intrinsic "^1.2.5" + object-inspect "^1.13.3" + side-channel-map "^1.0.1" + +side-channel@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2" integrity sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA== @@ -10433,6 +12588,17 @@ side-channel@^1.0.4, side-channel@^1.0.6: get-intrinsic "^1.2.4" object-inspect "^1.13.1" +side-channel@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.1.0.tgz#c3fcff9c4da932784873335ec9765fa94ff66bc9" + integrity sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw== + dependencies: + es-errors "^1.3.0" + object-inspect "^1.13.3" + side-channel-list "^1.0.0" + side-channel-map "^1.0.1" + side-channel-weakmap "^1.0.2" + signal-exit@^3.0.2, signal-exit@^3.0.3: version "3.0.7" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" @@ -10457,16 +12623,6 @@ sisteransi@^1.0.5: resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg== -sitemap@7.1.1: - version "7.1.1" - resolved "https://registry.yarnpkg.com/sitemap/-/sitemap-7.1.1.tgz#eeed9ad6d95499161a3eadc60f8c6dce4bea2bef" - integrity sha512-mK3aFtjz4VdJN0igpIJrinf3EO8U8mxOPsTBzSsy06UtjZQJ3YY3o3Xa7zSc5nMqcMrRwlChHZ18Kxg0caiPBg== - dependencies: - "@types/node" "^17.0.5" - "@types/sax" "^1.2.1" - arg "^5.0.0" - sax "^1.2.4" - sitemap@^7.1.1: version "7.1.2" resolved "https://registry.yarnpkg.com/sitemap/-/sitemap-7.1.2.tgz#6ce1deb43f6f177c68bc59cf93632f54e3ae6b72" @@ -10592,28 +12748,10 @@ statuses@2.0.1: resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== -std-env@^3.0.1: - version "3.7.0" - resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.7.0.tgz#c9f7386ced6ecf13360b6c6c55b8aaa4ef7481d2" - integrity sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg== - -stream-browserify@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-3.0.0.tgz#22b0a2850cdf6503e73085da1fc7b7d0c2122f2f" - integrity sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA== - dependencies: - inherits "~2.0.4" - readable-stream "^3.5.0" - -stream-http@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-3.2.0.tgz#1872dfcf24cb15752677e40e5c3f9cc1926028b5" - integrity sha512-Oq1bLqisTyK3TSCXpPbT4sdeYNdmyZJv1LxpEm2vu1ZhK89kSE5YXwZc3cWk0MagGaKriBh9mCFbVGtO+vY29A== - dependencies: - builtin-status-codes "^3.0.0" - inherits "^2.0.4" - readable-stream "^3.6.0" - xtend "^4.0.2" +std-env@^3.7.0: + version "3.9.0" + resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.9.0.tgz#1a6f7243b339dca4c9fd55e1c7504c77ef23e8f1" + integrity sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw== "string-width-cjs@npm:string-width@^4.2.0": version "4.2.3" @@ -10642,7 +12780,7 @@ string-width@^5.0.1, string-width@^5.1.2: emoji-regex "^9.2.2" strip-ansi "^7.0.1" -string_decoder@^1.1.1, string_decoder@^1.3.0: +string_decoder@^1.1.1: version "1.3.0" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== @@ -10736,10 +12874,10 @@ stylehacks@^6.1.1: browserslist "^4.23.0" postcss-selector-parser "^6.0.16" -stylis@^4.1.3: - version "4.3.2" - resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.3.2.tgz#8f76b70777dd53eb669c6f58c997bf0a9972e444" - integrity sha512-bhtUjWd/z6ltJiQwg0dUfxEJ+W+jdqQd8TbWLWyeIJHlnsqmGLRFFd8e5mA0AZi/zx90smXRlN66YMTcaSFifg== +stylis@^4.3.6: + version "4.3.6" + resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.3.6.tgz#7c7b97191cb4f195f03ecab7d52f7902ed378320" + integrity sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ== sucrase@^3.31.0, sucrase@^3.32.0: version "3.35.0" @@ -10815,7 +12953,7 @@ swagger2openapi@7.0.8, swagger2openapi@^7.0.8: yaml "^1.10.0" yargs "^17.0.1" -swc-loader@^0.2.3: +swc-loader@^0.2.6: version "0.2.6" resolved "https://registry.yarnpkg.com/swc-loader/-/swc-loader-0.2.6.tgz#bf0cba8eeff34bb19620ead81d1277fefaec6bc8" integrity sha512-9Zi9UP2YmDpgmQVbyOPJClY0dwf58JDyDMQ7uRc4krmc72twNI2fvlBWHLqVekBpPc7h5NJkGVT1zNDxFrqhvg== @@ -10850,11 +12988,6 @@ tailwindcss@^3.2.4: resolve "^1.22.2" sucrase "^3.32.0" -tapable@^1.0.0: - version "1.1.3" - resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2" - integrity sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA== - tapable@^2.0.0, tapable@^2.1.1, tapable@^2.2.0, tapable@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" @@ -10892,6 +13025,17 @@ terser-webpack-plugin@^5.3.10, terser-webpack-plugin@^5.3.9: serialize-javascript "^6.0.1" terser "^5.26.0" +terser-webpack-plugin@^5.3.11: + version "5.3.14" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz#9031d48e57ab27567f02ace85c7d690db66c3e06" + integrity sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw== + dependencies: + "@jridgewell/trace-mapping" "^0.3.25" + jest-worker "^27.4.5" + schema-utils "^4.3.0" + serialize-javascript "^6.0.2" + terser "^5.31.1" + terser@^5.10.0, terser@^5.15.1, terser@^5.26.0: version "5.31.1" resolved "https://registry.yarnpkg.com/terser/-/terser-5.31.1.tgz#735de3c987dd671e95190e6b98cfe2f07f3cf0d4" @@ -10902,10 +13046,15 @@ terser@^5.10.0, terser@^5.15.1, terser@^5.26.0: commander "^2.20.0" source-map-support "~0.5.20" -text-table@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" - integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== +terser@^5.31.1: + version "5.40.0" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.40.0.tgz#839a80db42bfee8340085f44ea99b5cba36c55c8" + integrity sha512-cfeKl/jjwSR5ar7d0FGmave9hFGJT8obyo0z+CrQOylLDbk7X81nPU6vq9VORa5jU30SkDnT2FXjLbR8HLP+xA== + dependencies: + "@jridgewell/source-map" "^0.3.3" + acorn "^8.14.0" + commander "^2.20.0" + source-map-support "~0.5.20" thenify-all@^1.0.0: version "1.6.0" @@ -10931,13 +13080,6 @@ thunky@^1.0.2: resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.1.0.tgz#5abaf714a9405db0504732bbccd2cedd9ef9537d" integrity sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA== -timers-browserify@^2.0.12: - version "2.0.12" - resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.12.tgz#44a45c11fbf407f34f97bccd1577c652361b00ee" - integrity sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ== - dependencies: - setimmediate "^1.0.4" - tiny-invariant@^1.0.2: version "1.3.3" resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.3.tgz#46680b7a873a0d5d10005995eb90a70d74d60127" @@ -10948,6 +13090,16 @@ tiny-warning@^1.0.0: resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754" integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA== +tinyexec@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/tinyexec/-/tinyexec-1.0.1.tgz#70c31ab7abbb4aea0a24f55d120e5990bfa1e0b1" + integrity sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw== + +tinypool@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/tinypool/-/tinypool-1.0.2.tgz#706193cc532f4c100f66aa00b01c42173d9051b2" + integrity sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA== + to-fast-properties@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" @@ -11005,22 +13157,22 @@ ts-interface-checker@^0.1.9: resolved "https://registry.yarnpkg.com/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz#784fd3d679722bc103b1b4b8030bcddb5db2a699" integrity sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA== -tslib@^2.0.3, tslib@^2.1.0, tslib@^2.6.0: +tslib@^2.0.3, tslib@^2.6.0: version "2.6.3" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.3.tgz#0438f810ad7a9edcde7a241c3d80db693c8cbfe0" integrity sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ== -tty-browserify@^0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.1.tgz#3f05251ee17904dfd0677546670db9651682b811" - integrity sha512-C3TaO7K81YvjCgQH9Q1S3R3P3BtN3RIM8n+OvX4il1K1zgE8ZhI0op7kClgkxtutIE8hQrcrHBXvIheqKUUCxw== +type-fest@^0.21.3: + version "0.21.3" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" + integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== type-fest@^1.0.1: version "1.4.0" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-1.4.0.tgz#e9fb813fe3bf1744ec359d55d1affefa76f14be1" integrity sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA== -type-fest@^2.13.0, type-fest@^2.14.0, type-fest@^2.5.0: +type-fest@^2.13.0, type-fest@^2.5.0: version "2.19.0" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.19.0.tgz#88068015bb33036a598b952e55e9311a60fd3a9b" integrity sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA== @@ -11040,6 +13192,11 @@ typedarray-to-buffer@^3.1.5: dependencies: is-typedarray "^1.0.0" +ufo@^1.5.4: + version "1.6.1" + resolved "https://registry.yarnpkg.com/ufo/-/ufo-1.6.1.tgz#ac2db1d54614d1b22c1d603e3aef44a85d8f146b" + integrity sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA== + unbzip2-stream@1.4.3: version "1.4.3" resolved "https://registry.yarnpkg.com/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz#b0da04c4371311df771cdc215e87f2130991ace7" @@ -11191,7 +13348,7 @@ unist-util-stringify-position@^4.0.0: dependencies: "@types/unist" "^3.0.0" -unist-util-visit-parents@^5.1.1: +unist-util-visit-parents@^5.0.0, unist-util-visit-parents@^5.1.1: version "5.1.3" resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz#b4520811b0ca34285633785045df7a8d6776cfeb" integrity sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg== @@ -11243,6 +13400,14 @@ update-browserslist-db@^1.0.16: escalade "^3.1.2" picocolors "^1.0.1" +update-browserslist-db@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz#348377dd245216f9e7060ff50b15a1b740b75420" + integrity sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw== + dependencies: + escalade "^3.2.0" + picocolors "^1.1.1" + update-notifier@^6.0.2: version "6.0.2" resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-6.0.2.tgz#a6990253dfe6d5a02bd04fbb6a61543f55026b60" @@ -11279,13 +13444,13 @@ url-loader@^4.1.1: mime-types "^2.1.27" schema-utils "^3.0.0" -url@^0.11.0: - version "0.11.3" - resolved "https://registry.yarnpkg.com/url/-/url-0.11.3.tgz#6f495f4b935de40ce4a0a52faee8954244f3d3ad" - integrity sha512-6hxOLGfZASQK/cijlZnZJTq8OXAkt/3YGfQX45vvMYXpZoo8NdWZcY73K108Jf759lS1Bv/8wXnHDTSz17dSRw== +url@^0.11.1: + version "0.11.4" + resolved "https://registry.yarnpkg.com/url/-/url-0.11.4.tgz#adca77b3562d56b72746e76b330b7f27b6721f3c" + integrity sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg== dependencies: punycode "^1.4.1" - qs "^6.11.2" + qs "^6.12.3" use-editable@^2.3.3: version "2.3.3" @@ -11304,17 +13469,6 @@ util@^0.10.3: dependencies: inherits "2.0.3" -util@^0.12.4, util@^0.12.5: - version "0.12.5" - resolved "https://registry.yarnpkg.com/util/-/util-0.12.5.tgz#5f17a6059b73db61a875668781a1c2b136bd6fbc" - integrity sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA== - dependencies: - inherits "^2.0.3" - is-arguments "^1.0.4" - is-generator-function "^1.0.7" - is-typed-array "^1.1.3" - which-typed-array "^1.1.2" - utila@~0.4: version "0.4.0" resolved "https://registry.yarnpkg.com/utila/-/utila-0.4.0.tgz#8a16a05d445657a3aea5eecc5b12a4fa5379772c" @@ -11335,10 +13489,10 @@ uuid@8.3.2, uuid@^8.3.2: resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== -uuid@^9.0.0: - version "9.0.1" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30" - integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA== +uuid@^11.1.0: + version "11.1.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-11.1.0.tgz#9549028be1753bb934fc96e2bca09bb4105ae912" + integrity sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A== uvu@^0.5.0: version "0.5.6" @@ -11449,21 +13603,40 @@ vfile@^6.0.0, vfile@^6.0.1: unist-util-stringify-position "^4.0.0" vfile-message "^4.0.0" -vm-browserify@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0" - integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ== +vscode-jsonrpc@8.2.0: + version "8.2.0" + resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz#f43dfa35fb51e763d17cd94dcca0c9458f35abf9" + integrity sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA== -wait-on@6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/wait-on/-/wait-on-6.0.1.tgz#16bbc4d1e4ebdd41c5b4e63a2e16dbd1f4e5601e" - integrity sha512-zht+KASY3usTY5u2LgaNqn/Cd8MukxLGjdcZxT2ns5QzDmTFc4XoWBgC+C/na+sMRZTuVygQoMYwdcVjHnYIVw== +vscode-languageserver-protocol@3.17.5: + version "3.17.5" + resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz#864a8b8f390835572f4e13bd9f8313d0e3ac4bea" + integrity sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg== dependencies: - axios "^0.25.0" - joi "^17.6.0" - lodash "^4.17.21" - minimist "^1.2.5" - rxjs "^7.5.4" + vscode-jsonrpc "8.2.0" + vscode-languageserver-types "3.17.5" + +vscode-languageserver-textdocument@~1.0.11: + version "1.0.12" + resolved "https://registry.yarnpkg.com/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz#457ee04271ab38998a093c68c2342f53f6e4a631" + integrity sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA== + +vscode-languageserver-types@3.17.5: + version "3.17.5" + resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz#3273676f0cf2eab40b3f44d085acbb7f08a39d8a" + integrity sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg== + +vscode-languageserver@~9.0.1: + version "9.0.1" + resolved "https://registry.yarnpkg.com/vscode-languageserver/-/vscode-languageserver-9.0.1.tgz#500aef82097eb94df90d008678b0b6b5f474015b" + integrity sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g== + dependencies: + vscode-languageserver-protocol "3.17.5" + +vscode-uri@~3.0.8: + version "3.0.8" + resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-3.0.8.tgz#1770938d3e72588659a172d0fd4642780083ff9f" + integrity sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw== warning@^4.0.3: version "4.0.3" @@ -11492,17 +13665,12 @@ web-namespaces@^2.0.0: resolved "https://registry.yarnpkg.com/web-namespaces/-/web-namespaces-2.0.1.tgz#1010ff7c650eccb2592cebeeaf9a1b253fd40692" integrity sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ== -web-worker@^1.2.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/web-worker/-/web-worker-1.3.0.tgz#e5f2df5c7fe356755a5fb8f8410d4312627e6776" - integrity sha512-BSR9wyRsy/KOValMgd5kMyr3JzpdeoR9KVId8u5GVlTTAtNChlsE4yTxeY7zMdNSyOmoKBv8NH2qeRY9Tg+IaA== - webidl-conversions@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== -webpack-bundle-analyzer@^4.9.0: +webpack-bundle-analyzer@^4.10.2: version "4.10.2" resolved "https://registry.yarnpkg.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.10.2.tgz#633af2862c213730be3dbdf40456db171b60d5bd" integrity sha512-vJptkMm9pk5si4Bv922ZbKLV8UTT4zib4FPgXMhgzUny0bfDDkLXAVQs3ly3fS4/TN9ROFtb0NFrm04UXFE/Vw== @@ -11531,7 +13699,7 @@ webpack-dev-middleware@^5.3.4: range-parser "^1.2.1" schema-utils "^4.0.0" -webpack-dev-server@^4.15.1: +webpack-dev-server@^4.15.2: version "4.15.2" resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-4.15.2.tgz#9e0c70a42a012560860adb186986da1248333173" integrity sha512-0XavAZbNJ5sDrCbkpWL8mia0o5WPOd2YGtxrEiZkBK9FjLppIUK2TgxK6qGD2P3hUXTJNNPVibrerKcx5WkR1g== @@ -11576,12 +13744,21 @@ webpack-merge@^5.9.0: flat "^5.0.2" wildcard "^2.0.0" +webpack-merge@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-6.0.1.tgz#50c776868e080574725abc5869bd6e4ef0a16c6a" + integrity sha512-hXXvrjtx2PLYx4qruKl+kyRSLc52V+cCvMxRjmKwoA+CBbbF5GfIBtR6kCvl0fYGqTUPKB+1ktVmTHqMOzgCBg== + dependencies: + clone-deep "^4.0.1" + flat "^5.0.2" + wildcard "^2.0.1" + webpack-sources@^3.2.3: version "3.2.3" resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde" integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== -webpack@^5.61.0, webpack@^5.88.1: +webpack@^5.88.1: version "5.92.0" resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.92.0.tgz#cc114c71e6851d220b1feaae90159ed52c876bdf" integrity sha512-Bsw2X39MYIgxouNATyVpCNVWBCuUwDgWtN78g6lSdPJRLaQ/PUVm/oXcaRAyY/sMFoKFQrsPeqvTizWtq7QPCA== @@ -11611,15 +13788,49 @@ webpack@^5.61.0, webpack@^5.88.1: watchpack "^2.4.1" webpack-sources "^3.2.3" -webpackbar@^5.0.2: - version "5.0.2" - resolved "https://registry.yarnpkg.com/webpackbar/-/webpackbar-5.0.2.tgz#d3dd466211c73852741dfc842b7556dcbc2b0570" - integrity sha512-BmFJo7veBDgQzfWXl/wwYXr/VFus0614qZ8i9znqcl9fnEdiVkdbi0TedLQ6xAK92HZHDJ0QmyQ0fmuZPAgCYQ== +webpack@^5.95.0: + version "5.99.9" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.99.9.tgz#d7de799ec17d0cce3c83b70744b4aedb537d8247" + integrity sha512-brOPwM3JnmOa+7kd3NsmOUOwbDAj8FT9xDsG3IW0MgbN9yZV7Oi/s/+MNQ/EcSMqw7qfoRyXPoeEWT8zLVdVGg== dependencies: - chalk "^4.1.0" - consola "^2.15.3" + "@types/eslint-scope" "^3.7.7" + "@types/estree" "^1.0.6" + "@types/json-schema" "^7.0.15" + "@webassemblyjs/ast" "^1.14.1" + "@webassemblyjs/wasm-edit" "^1.14.1" + "@webassemblyjs/wasm-parser" "^1.14.1" + acorn "^8.14.0" + browserslist "^4.24.0" + chrome-trace-event "^1.0.2" + enhanced-resolve "^5.17.1" + es-module-lexer "^1.2.1" + eslint-scope "5.1.1" + events "^3.2.0" + glob-to-regexp "^0.4.1" + graceful-fs "^4.2.11" + json-parse-even-better-errors "^2.3.1" + loader-runner "^4.2.0" + mime-types "^2.1.27" + neo-async "^2.6.2" + schema-utils "^4.3.2" + tapable "^2.1.1" + terser-webpack-plugin "^5.3.11" + watchpack "^2.4.1" + webpack-sources "^3.2.3" + +webpackbar@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/webpackbar/-/webpackbar-6.0.1.tgz#5ef57d3bf7ced8b19025477bc7496ea9d502076b" + integrity sha512-TnErZpmuKdwWBdMoexjio3KKX6ZtoKHRVvLIU0A47R0VVBDtx3ZyOJDktgYixhoJokZTYTt1Z37OkO9pnGJa9Q== + dependencies: + ansi-escapes "^4.3.2" + chalk "^4.1.2" + consola "^3.2.3" + figures "^3.2.0" + markdown-table "^2.0.0" pretty-time "^1.1.0" - std-env "^3.0.1" + std-env "^3.7.0" + wrap-ansi "^7.0.0" websocket-driver@>=0.5.1, websocket-driver@^0.7.4: version "0.7.4" @@ -11643,24 +13854,6 @@ whatwg-url@^5.0.0: tr46 "~0.0.3" webidl-conversions "^3.0.0" -which-typed-array@^1.1.14, which-typed-array@^1.1.2: - version "1.1.15" - resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.15.tgz#264859e9b11a649b388bfaaf4f767df1f779b38d" - integrity sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA== - dependencies: - available-typed-arrays "^1.0.7" - call-bind "^1.0.7" - for-each "^0.3.3" - gopd "^1.0.1" - has-tostringtag "^1.0.2" - -which@^1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" - integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== - dependencies: - isexe "^2.0.0" - which@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" @@ -11675,7 +13868,7 @@ widest-line@^4.0.1: dependencies: string-width "^5.0.1" -wildcard@^2.0.0: +wildcard@^2.0.0, wildcard@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.1.tgz#5ab10d02487198954836b6349f74fff961e10f67" integrity sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ== @@ -11761,11 +13954,6 @@ xml-parser-xo@^3.2.0: resolved "https://registry.yarnpkg.com/xml-parser-xo/-/xml-parser-xo-3.2.0.tgz#c633ab55cf1976d6b03ab4a6a85045093ac32b73" integrity sha512-8LRU6cq+d7mVsoDaMhnkkt3CTtAs4153p49fRo+HIB3I1FD1o5CeXRjRH29sQevIfVJIcPjKSsPU/+Ujhq09Rg== -xtend@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" - integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== - y18n@^5.0.5: version "5.0.8" resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" @@ -11786,7 +13974,7 @@ yaml-ast-parser@0.0.43: resolved "https://registry.yarnpkg.com/yaml-ast-parser/-/yaml-ast-parser-0.0.43.tgz#e8a23e6fb4c38076ab92995c5dca33f3d3d7c9bb" integrity sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A== -yaml@1.10.2, yaml@^1.10.0, yaml@^1.7.2: +yaml@1.10.2, yaml@^1.10.0: version "1.10.2" resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== @@ -11822,11 +14010,6 @@ yauzl@^2.10.0: buffer-crc32 "~0.2.3" fd-slicer "~1.1.0" -yocto-queue@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" - integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== - yocto-queue@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-1.0.0.tgz#7f816433fb2cbc511ec8bf7d263c3b58a1a3c251" From cdf1860083e02906d0399d5c210135ccb1fda242 Mon Sep 17 00:00:00 2001 From: Florian Forster Date: Mon, 2 Jun 2025 00:42:11 -0700 Subject: [PATCH 53/76] chore: remove unparsed md characters (#9983) This pull request includes a minor change to the `README.md` file. It removes a broken markdown link syntax for an image and replaces it with the correct image syntax to properly display the "New Login Showcase" image. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 285e50964c..3d33e20e57 100644 --- a/README.md +++ b/README.md @@ -193,7 +193,7 @@ Use [Console](https://zitadel.com/docs/guides/manage/console/overview) or our [A ### Login V2 Check out our new Login V2 version in our [typescript repository](https://github.com/zitadel/typescript) or in our [documentation](https://zitadel.com/docs/guides/integrate/login/hosted-login#hosted-login-version-2-beta) -[![New Login Showcase](https://github.com/user-attachments/assets/cb5c5212-128b-4dc9-b11d-cabfd3f73e26)] +![New Login Showcase](https://github.com/user-attachments/assets/cb5c5212-128b-4dc9-b11d-cabfd3f73e26) ## Security From b660d6ab9a2cd26c579693264d5d20a39ecc6c7d Mon Sep 17 00:00:00 2001 From: Silvan <27845747+adlerhurst@users.noreply.github.com> Date: Mon, 2 Jun 2025 10:16:13 +0200 Subject: [PATCH 54/76] fix(queue): reset projection list before each `Register` call (#10001) # Which Problems Are Solved if Zitadel was started using `start-from-init` or `start-from-setup` there were rare cases where a panic occured when `Notifications.LegacyEnabled` was set to false. The cause was a list which was not reset before refilling. # How the Problems Are Solved The list is now reset before each time it gets filled. # Additional Changes Ensure all contexts are canceled for the init and setup functions for `start-from-init- or `start-from-setup` commands. # Additional Context none --- cmd/start/start_from_init.go | 11 +++++++++-- cmd/start/start_from_setup.go | 7 ++++++- internal/notification/projections.go | 3 +++ 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/cmd/start/start_from_init.go b/cmd/start/start_from_init.go index 62d705b33c..41972e16ad 100644 --- a/cmd/start/start_from_init.go +++ b/cmd/start/start_from_init.go @@ -1,6 +1,8 @@ package start import ( + "context" + "github.com/spf13/cobra" "github.com/spf13/viper" "github.com/zitadel/logging" @@ -29,14 +31,19 @@ Requirements: masterKey, err := key.MasterKey(cmd) logging.OnError(err).Panic("No master key provided") - initialise.InitAll(cmd.Context(), initialise.MustNewConfig(viper.GetViper())) + initCtx, cancel := context.WithCancel(cmd.Context()) + initialise.InitAll(initCtx, initialise.MustNewConfig(viper.GetViper())) + cancel() err = setup.BindInitProjections(cmd) logging.OnError(err).Fatal("unable to bind \"init-projections\" flag") setupConfig := setup.MustNewConfig(viper.GetViper()) setupSteps := setup.MustNewSteps(viper.New()) - setup.Setup(cmd.Context(), setupConfig, setupSteps, masterKey) + + setupCtx, cancel := context.WithCancel(cmd.Context()) + setup.Setup(setupCtx, setupConfig, setupSteps, masterKey) + cancel() startConfig := MustNewConfig(viper.GetViper()) diff --git a/cmd/start/start_from_setup.go b/cmd/start/start_from_setup.go index a8b7295f2a..3e8a13705e 100644 --- a/cmd/start/start_from_setup.go +++ b/cmd/start/start_from_setup.go @@ -1,6 +1,8 @@ package start import ( + "context" + "github.com/spf13/cobra" "github.com/spf13/viper" "github.com/zitadel/logging" @@ -34,7 +36,10 @@ Requirements: setupConfig := setup.MustNewConfig(viper.GetViper()) setupSteps := setup.MustNewSteps(viper.New()) - setup.Setup(cmd.Context(), setupConfig, setupSteps, masterKey) + + setupCtx, cancel := context.WithCancel(cmd.Context()) + setup.Setup(setupCtx, setupConfig, setupSteps, masterKey) + cancel() startConfig := MustNewConfig(viper.GetViper()) diff --git a/internal/notification/projections.go b/internal/notification/projections.go index 7fda08135c..7fedaaf301 100644 --- a/internal/notification/projections.go +++ b/internal/notification/projections.go @@ -43,6 +43,9 @@ func Register( queue.ShouldStart() } + // make sure the slice does not contain old values + projections = nil + q := handlers.NewNotificationQueries(queries, es, externalDomain, externalPort, externalSecure, fileSystemPath, userEncryption, smtpEncryption, smsEncryption) c := newChannels(q) projections = append(projections, handlers.NewUserNotifier(ctx, projection.ApplyCustomConfig(userHandlerCustomConfig), commands, q, c, otpEmailTmpl, notificationWorkerConfig, queue)) From b46c41e4bf50af7f3873c6d0deb62206d77ec6e9 Mon Sep 17 00:00:00 2001 From: Iraq <66622793+kkrime@users.noreply.github.com> Date: Mon, 2 Jun 2025 10:40:19 +0200 Subject: [PATCH 55/76] fix(settings): fix for setting restricted languages (#9947) # Which Problems Are Solved Zitadel encounters a migration error when setting `restricted languages` and fails to start. # How the Problems Are Solved The problem is that there is a check that checks that at least one of the restricted languages is the same as the `default language`, however, in the `authz instance` (where the default language is pulled form) is never set. I've added code to set the `default language` in the `authz instance` # Additional Context - Closes https://github.com/zitadel/zitadel/issues/9787 --------- Co-authored-by: Livio Spring --- internal/api/authz/instance.go | 30 +++++++++++++++++---------- internal/command/instance.go | 27 +++++++++++++----------- internal/command/instance_test.go | 34 ++++++++++++++++--------------- 3 files changed, 52 insertions(+), 39 deletions(-) diff --git a/internal/api/authz/instance.go b/internal/api/authz/instance.go index 7ee8d605ca..0fe6d6c8aa 100644 --- a/internal/api/authz/instance.go +++ b/internal/api/authz/instance.go @@ -9,9 +9,7 @@ import ( "github.com/zitadel/zitadel/internal/feature" ) -var ( - emptyInstance = &instance{} -) +var emptyInstance = &instance{} type Instance interface { InstanceID() string @@ -33,13 +31,13 @@ type InstanceVerifier interface { } type instance struct { - id string - domain string - projectID string - appID string - clientID string - orgID string - features feature.Features + id string + projectID string + appID string + clientID string + orgID string + defaultLanguage language.Tag + features feature.Features } func (i *instance) Block() *bool { @@ -67,7 +65,7 @@ func (i *instance) ConsoleApplicationID() string { } func (i *instance) DefaultLanguage() language.Tag { - return language.Und + return i.defaultLanguage } func (i *instance) DefaultOrganisationID() string { @@ -106,6 +104,16 @@ func WithInstanceID(ctx context.Context, id string) context.Context { return context.WithValue(ctx, instanceKey, &instance{id: id}) } +func WithDefaultLanguage(ctx context.Context, defaultLanguage language.Tag) context.Context { + i, ok := ctx.Value(instanceKey).(*instance) + if !ok { + i = new(instance) + } + + i.defaultLanguage = defaultLanguage + return context.WithValue(ctx, instanceKey, i) +} + func WithConsole(ctx context.Context, projectID, appID string) context.Context { i, ok := ctx.Value(instanceKey).(*instance) if !ok { diff --git a/internal/command/instance.go b/internal/command/instance.go index d71be53468..cfafb1d298 100644 --- a/internal/command/instance.go +++ b/internal/command/instance.go @@ -221,7 +221,7 @@ func (c *Commands) SetUpInstance(ctx context.Context, setup *InstanceSetup) (str if err := setup.generateIDs(c.idGenerator); err != nil { return "", "", nil, nil, err } - ctx = contextWithInstanceSetupInfo(ctx, setup.zitadel.instanceID, setup.zitadel.projectID, setup.zitadel.consoleAppID, c.externalDomain) + ctx = contextWithInstanceSetupInfo(ctx, setup.zitadel.instanceID, setup.zitadel.projectID, setup.zitadel.consoleAppID, c.externalDomain, setup.DefaultLanguage) validations, pat, machineKey, err := setUpInstance(ctx, c, setup) if err != nil { @@ -255,19 +255,22 @@ func (c *Commands) SetUpInstance(ctx context.Context, setup *InstanceSetup) (str return setup.zitadel.instanceID, token, machineKey, details, nil } -func contextWithInstanceSetupInfo(ctx context.Context, instanceID, projectID, consoleAppID, externalDomain string) context.Context { - return authz.WithConsole( - authz.SetCtxData( - http.WithRequestedHost( - authz.WithInstanceID( - ctx, - instanceID), - externalDomain, +func contextWithInstanceSetupInfo(ctx context.Context, instanceID, projectID, consoleAppID, externalDomain string, defaultLanguage language.Tag) context.Context { + return authz.WithDefaultLanguage( + authz.WithConsole( + authz.SetCtxData( + http.WithRequestedHost( + authz.WithInstanceID( + ctx, + instanceID), + externalDomain, + ), + authz.CtxData{ResourceOwner: instanceID}, ), - authz.CtxData{ResourceOwner: instanceID}, + projectID, + consoleAppID, ), - projectID, - consoleAppID, + defaultLanguage, ) } diff --git a/internal/command/instance_test.go b/internal/command/instance_test.go index 16e51d844d..2b82818a7e 100644 --- a/internal/command/instance_test.go +++ b/internal/command/instance_test.go @@ -345,6 +345,7 @@ func instanceElementsEvents(ctx context.Context, instanceID, instanceName string instance.NewSecretGeneratorAddedEvent(ctx, &instanceAgg.Aggregate, domain.SecretGeneratorTypeOTPEmail, 8, 5*time.Minute, false, false, true, false), } } + func instanceElementsConfig() *SecretGenerators { return &SecretGenerators{ ClientSecret: &crypto.GeneratorConfig{Length: 64, IncludeLowerLetters: true, IncludeUpperLetters: true, IncludeDigits: true}, @@ -668,22 +669,23 @@ func TestCommandSide_setupMinimalInterfaces(t *testing.T) { eventstore: expectEventstore( slices.Concat( projectFilters(), - []expect{expectPush( - projectAddedEvents(context.Background(), - "INSTANCE", - "ORG", - "PROJECT", - "owner", - false, - )..., - ), + []expect{ + expectPush( + projectAddedEvents(context.Background(), + "INSTANCE", + "ORG", + "PROJECT", + "owner", + false, + )..., + ), }, )..., ), idGenerator: id_mock.NewIDGeneratorExpectIDs(t, projectClientIDs()...), }, args: args{ - ctx: contextWithInstanceSetupInfo(context.Background(), "INSTANCE", "PROJECT", "console-id", "DOMAIN"), + ctx: contextWithInstanceSetupInfo(context.Background(), "INSTANCE", "PROJECT", "console-id", "DOMAIN", language.Dutch), instanceAgg: instance.NewAggregate("INSTANCE"), orgAgg: org.NewAggregate("ORG"), owner: "owner", @@ -767,7 +769,7 @@ func TestCommandSide_setupAdmins(t *testing.T) { }, }, args: args{ - ctx: contextWithInstanceSetupInfo(context.Background(), "INSTANCE", "PROJECT", "console-id", "DOMAIN"), + ctx: contextWithInstanceSetupInfo(context.Background(), "INSTANCE", "PROJECT", "console-id", "DOMAIN", language.Dutch), instanceAgg: instance.NewAggregate("INSTANCE"), orgAgg: org.NewAggregate("ORG"), human: instanceSetupHumanConfig(), @@ -806,7 +808,7 @@ func TestCommandSide_setupAdmins(t *testing.T) { keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), }, args: args{ - ctx: contextWithInstanceSetupInfo(context.Background(), "INSTANCE", "PROJECT", "console-id", "DOMAIN"), + ctx: contextWithInstanceSetupInfo(context.Background(), "INSTANCE", "PROJECT", "console-id", "DOMAIN", language.Dutch), instanceAgg: instance.NewAggregate("INSTANCE"), orgAgg: org.NewAggregate("ORG"), machine: instanceSetupMachineConfig(), @@ -855,7 +857,7 @@ func TestCommandSide_setupAdmins(t *testing.T) { keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), }, args: args{ - ctx: contextWithInstanceSetupInfo(context.Background(), "INSTANCE", "PROJECT", "console-id", "DOMAIN"), + ctx: contextWithInstanceSetupInfo(context.Background(), "INSTANCE", "PROJECT", "console-id", "DOMAIN", language.Dutch), instanceAgg: instance.NewAggregate("INSTANCE"), orgAgg: org.NewAggregate("ORG"), machine: instanceSetupMachineConfig(), @@ -972,7 +974,7 @@ func TestCommandSide_setupDefaultOrg(t *testing.T) { keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), }, args: args{ - ctx: contextWithInstanceSetupInfo(context.Background(), "INSTANCE", "PROJECT", "console-id", "DOMAIN"), + ctx: contextWithInstanceSetupInfo(context.Background(), "INSTANCE", "PROJECT", "console-id", "DOMAIN", language.Dutch), instanceAgg: instance.NewAggregate("INSTANCE"), orgName: "ZITADEL", machine: &AddMachine{ @@ -1097,7 +1099,7 @@ func TestCommandSide_setupInstanceElements(t *testing.T) { ), }, args: args{ - ctx: contextWithInstanceSetupInfo(context.Background(), "INSTANCE", "PROJECT", "console-id", "DOMAIN"), + ctx: contextWithInstanceSetupInfo(context.Background(), "INSTANCE", "PROJECT", "console-id", "DOMAIN", language.Dutch), instanceAgg: instance.NewAggregate("INSTANCE"), setup: setupInstanceElementsConfig(), }, @@ -1183,7 +1185,7 @@ func TestCommandSide_setUpInstance(t *testing.T) { }, }, args: args{ - ctx: contextWithInstanceSetupInfo(context.Background(), "INSTANCE", "PROJECT", "console-id", "DOMAIN"), + ctx: contextWithInstanceSetupInfo(context.Background(), "INSTANCE", "PROJECT", "console-id", "DOMAIN", language.Dutch), setup: setupInstanceConfig(), }, res: res{ From b3d22dba0535f3818b69d2f8f62216f5e0365695 Mon Sep 17 00:00:00 2001 From: Silvan <27845747+adlerhurst@users.noreply.github.com> Date: Mon, 2 Jun 2025 17:29:56 +0200 Subject: [PATCH 56/76] docs(10016): cockroach compatibility (#10010) # Which Problems Are Solved If the sql statement of technical advisory 10016 gets executed on cockroach the following error is raised: ``` ERROR: WITH clause "fixed" does not return any columns SQLSTATE: 0A000 HINT: missing RETURNING clause? ``` # How the Problems Are Solved Fixed the statement by adding `returning` to statement --- docs/docs/support/advisory/a10016.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/docs/support/advisory/a10016.md b/docs/docs/support/advisory/a10016.md index 84dd1cd34c..38d73e6078 100644 --- a/docs/docs/support/advisory/a10016.md +++ b/docs/docs/support/advisory/a10016.md @@ -78,6 +78,7 @@ with and s.aggregate_id = b.aggregate_id and s.aggregate_type = b.aggregate_type and s.sequence = b.sequence + returning * ) select b.projection_name, From ae1a2e93c1e10a05d43762654ba8b4c8daed6a3d Mon Sep 17 00:00:00 2001 From: Iraq <66622793+kkrime@users.noreply.github.com> Date: Mon, 2 Jun 2025 18:27:53 +0200 Subject: [PATCH 57/76] feat(api): moving organization API resourced based (#9943) --- cmd/start/start.go | 2 +- docs/docusaurus.config.js | 8 + docs/sidebars.js | 13 + internal/api/grpc/instance/converter.go | 4 +- .../v2beta/integration_test/instance_test.go | 5 +- .../v2beta/integration_test/query_test.go | 5 +- internal/api/grpc/management/org_converter.go | 4 +- internal/api/grpc/management/user.go | 4 + internal/api/grpc/metadata/v2beta/metadata.go | 49 + internal/api/grpc/object/v2beta/converter.go | 55 + internal/api/grpc/org/v2beta/helper.go | 256 +++ .../org/v2beta/integration_test/org_test.go | 1815 ++++++++++++++++- internal/api/grpc/org/v2beta/org.go | 238 ++- internal/api/grpc/org/v2beta/org_test.go | 45 +- internal/api/grpc/org/v2beta/server.go | 4 + internal/query/org_metadata.go | 1 - proto/zitadel/admin.proto | 28 +- proto/zitadel/management.proto | 258 +-- proto/zitadel/metadata/v2beta/metadata.proto | 57 + proto/zitadel/org/v2beta/org.proto | 169 ++ proto/zitadel/org/v2beta/org_service.proto | 825 +++++++- 21 files changed, 3542 insertions(+), 303 deletions(-) create mode 100644 internal/api/grpc/metadata/v2beta/metadata.go create mode 100644 internal/api/grpc/org/v2beta/helper.go create mode 100644 proto/zitadel/metadata/v2beta/metadata.proto create mode 100644 proto/zitadel/org/v2beta/org.proto diff --git a/cmd/start/start.go b/cmd/start/start.go index af76b29e99..2fc1fb8413 100644 --- a/cmd/start/start.go +++ b/cmd/start/start.go @@ -470,7 +470,7 @@ func startAPIs( if err := apis.RegisterService(ctx, settings_v2beta.CreateServer(commands, queries)); err != nil { return nil, err } - if err := apis.RegisterService(ctx, org_v2beta.CreateServer(commands, queries, permissionCheck)); err != nil { + if err := apis.RegisterService(ctx, org_v2beta.CreateServer(config.SystemDefaults, commands, queries, permissionCheck)); err != nil { return nil, err } if err := apis.RegisterService(ctx, feature_v2beta.CreateServer(commands, queries)); err != nil { diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js index c161d38d9f..43830eafd0 100644 --- a/docs/docusaurus.config.js +++ b/docs/docusaurus.config.js @@ -342,6 +342,14 @@ module.exports = { categoryLinkSource: "auto", }, }, + org_v2beta: { + specPath: ".artifacts/openapi/zitadel/org/v2beta/org_service.swagger.json", + outputDir: "docs/apis/resources/org_service_v2beta", + sidebarOptions: { + groupPathsBy: "tag", + categoryLinkSource: "auto", + }, + }, project_v2beta: { specPath: ".artifacts/openapi/zitadel/project/v2beta/project_service.swagger.json", outputDir: "docs/apis/resources/project_service_v2", diff --git a/docs/sidebars.js b/docs/sidebars.js index b7a399ecf1..f9b97703e5 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -10,6 +10,7 @@ const sidebar_api_oidc_service_v2 = require("./docs/apis/resources/oidc_service_ const sidebar_api_settings_service_v2 = require("./docs/apis/resources/settings_service_v2/sidebar.ts").default const sidebar_api_feature_service_v2 = require("./docs/apis/resources/feature_service_v2/sidebar.ts").default const sidebar_api_org_service_v2 = require("./docs/apis/resources/org_service_v2/sidebar.ts").default +const sidebar_api_org_service_v2beta = require("./docs/apis/resources/org_service_v2beta/sidebar.ts").default const sidebar_api_idp_service_v2 = require("./docs/apis/resources/idp_service_v2/sidebar.ts").default const sidebar_api_actions_v2 = require("./docs/apis/resources/action_service_v2/sidebar.ts").default const sidebar_api_project_service_v2 = require("./docs/apis/resources/project_service_v2/sidebar.ts").default @@ -791,6 +792,18 @@ module.exports = { }, items: sidebar_api_org_service_v2, }, + { + type: "category", + label: "Organization (Beta)", + link: { + type: "generated-index", + title: "Organization Service beta API", + slug: "/apis/resources/org_service/v2beta", + description: + "This API is intended to manage organizations for ZITADEL. \n", + }, + items: sidebar_api_org_service_v2beta, + }, { type: "category", label: "Identity Provider", diff --git a/internal/api/grpc/instance/converter.go b/internal/api/grpc/instance/converter.go index 4094da4a77..b894a064ff 100644 --- a/internal/api/grpc/instance/converter.go +++ b/internal/api/grpc/instance/converter.go @@ -28,7 +28,7 @@ func InstanceToPb(instance *query.Instance) *instance_pb.Instance { Name: instance.Name, Domains: DomainsToPb(instance.Domains), Version: build.Version(), - State: instance_pb.State_STATE_RUNNING, //TODO: change when delete is implemented + State: instance_pb.State_STATE_RUNNING, // TODO: change when delete is implemented } } @@ -44,7 +44,7 @@ func InstanceDetailToPb(instance *query.Instance) *instance_pb.InstanceDetail { Name: instance.Name, Domains: DomainsToPb(instance.Domains), Version: build.Version(), - State: instance_pb.State_STATE_RUNNING, //TODO: change when delete is implemented + State: instance_pb.State_STATE_RUNNING, // TODO: change when delete is implemented } } diff --git a/internal/api/grpc/instance/v2beta/integration_test/instance_test.go b/internal/api/grpc/instance/v2beta/integration_test/instance_test.go index 5187bbc78d..ae277c6d13 100644 --- a/internal/api/grpc/instance/v2beta/integration_test/instance_test.go +++ b/internal/api/grpc/instance/v2beta/integration_test/instance_test.go @@ -9,10 +9,11 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/zitadel/zitadel/internal/integration" - instance "github.com/zitadel/zitadel/pkg/grpc/instance/v2beta" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" + + "github.com/zitadel/zitadel/internal/integration" + instance "github.com/zitadel/zitadel/pkg/grpc/instance/v2beta" ) func TestDeleteInstace(t *testing.T) { diff --git a/internal/api/grpc/instance/v2beta/integration_test/query_test.go b/internal/api/grpc/instance/v2beta/integration_test/query_test.go index 0828b006e3..e59a16a932 100644 --- a/internal/api/grpc/instance/v2beta/integration_test/query_test.go +++ b/internal/api/grpc/instance/v2beta/integration_test/query_test.go @@ -11,12 +11,13 @@ import ( "github.com/brianvoe/gofakeit/v6" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "github.com/zitadel/zitadel/internal/integration" filter "github.com/zitadel/zitadel/pkg/grpc/filter/v2beta" instance "github.com/zitadel/zitadel/pkg/grpc/instance/v2beta" "github.com/zitadel/zitadel/pkg/grpc/object/v2" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" ) func TestGetInstance(t *testing.T) { diff --git a/internal/api/grpc/management/org_converter.go b/internal/api/grpc/management/org_converter.go index 879b5e0763..03de84cdf4 100644 --- a/internal/api/grpc/management/org_converter.go +++ b/internal/api/grpc/management/org_converter.go @@ -26,7 +26,7 @@ func ListOrgDomainsRequestToModel(req *mgmt_pb.ListOrgDomainsRequest) (*query.Or Limit: limit, Asc: asc, }, - //SortingColumn: //TODO: sorting + // SortingColumn: //TODO: sorting Queries: queries, }, nil } @@ -89,7 +89,7 @@ func ListOrgMembersRequestToModel(ctx context.Context, req *mgmt_pb.ListOrgMembe Offset: offset, Limit: limit, Asc: asc, - //SortingColumn: //TODO: sorting + // SortingColumn: //TODO: sorting }, Queries: queries, }, diff --git a/internal/api/grpc/management/user.go b/internal/api/grpc/management/user.go index 5b82eb5afe..f318051e63 100644 --- a/internal/api/grpc/management/user.go +++ b/internal/api/grpc/management/user.go @@ -901,6 +901,7 @@ func (s *Server) ListHumanLinkedIDPs(ctx context.Context, req *mgmt_pb.ListHuman Details: obj_grpc.ToListDetails(res.Count, res.Sequence, res.LastRun), }, nil } + func (s *Server) RemoveHumanLinkedIDP(ctx context.Context, req *mgmt_pb.RemoveHumanLinkedIDPRequest) (*mgmt_pb.RemoveHumanLinkedIDPResponse, error) { objectDetails, err := s.command.RemoveUserIDPLink(ctx, RemoveHumanLinkedIDPRequestToDomain(ctx, req)) if err != nil { @@ -947,18 +948,21 @@ func cascadingIAMMembership(membership *query.IAMMembership) *command.CascadingI } return &command.CascadingIAMMembership{IAMID: membership.IAMID} } + func cascadingOrgMembership(membership *query.OrgMembership) *command.CascadingOrgMembership { if membership == nil { return nil } return &command.CascadingOrgMembership{OrgID: membership.OrgID} } + func cascadingProjectMembership(membership *query.ProjectMembership) *command.CascadingProjectMembership { if membership == nil { return nil } return &command.CascadingProjectMembership{ProjectID: membership.ProjectID} } + func cascadingProjectGrantMembership(membership *query.ProjectGrantMembership) *command.CascadingProjectGrantMembership { if membership == nil { return nil diff --git a/internal/api/grpc/metadata/v2beta/metadata.go b/internal/api/grpc/metadata/v2beta/metadata.go new file mode 100644 index 0000000000..57da21dfd2 --- /dev/null +++ b/internal/api/grpc/metadata/v2beta/metadata.go @@ -0,0 +1,49 @@ +package metadata + +import ( + "google.golang.org/protobuf/types/known/timestamppb" + + v2beta_object "github.com/zitadel/zitadel/internal/api/grpc/object/v2beta" + "github.com/zitadel/zitadel/internal/query" + "github.com/zitadel/zitadel/internal/zerrors" + meta_pb "github.com/zitadel/zitadel/pkg/grpc/metadata/v2beta" +) + +// code in this file is copied from internal/api/grpc/metadata/metadata.go + +func OrgMetadataListToPb(dataList []*query.OrgMetadata) []*meta_pb.Metadata { + mds := make([]*meta_pb.Metadata, len(dataList)) + for i, data := range dataList { + mds[i] = OrgMetadataToPb(data) + } + return mds +} + +func OrgMetadataToPb(data *query.OrgMetadata) *meta_pb.Metadata { + return &meta_pb.Metadata{ + Key: data.Key, + Value: data.Value, + CreationDate: timestamppb.New(data.CreationDate), + ChangeDate: timestamppb.New(data.ChangeDate), + } +} + +func OrgMetadataQueriesToQuery(queries []*meta_pb.MetadataQuery) (_ []query.SearchQuery, err error) { + q := make([]query.SearchQuery, len(queries)) + for i, query := range queries { + q[i], err = OrgMetadataQueryToQuery(query) + if err != nil { + return nil, err + } + } + return q, nil +} + +func OrgMetadataQueryToQuery(metadataQuery *meta_pb.MetadataQuery) (query.SearchQuery, error) { + switch q := metadataQuery.Query.(type) { + case *meta_pb.MetadataQuery_KeyQuery: + return query.NewOrgMetadataKeySearchQuery(q.KeyQuery.Key, v2beta_object.TextMethodToQuery(q.KeyQuery.Method)) + default: + return nil, zerrors.ThrowInvalidArgument(nil, "METAD-fdg23", "List.Query.Invalid") + } +} diff --git a/internal/api/grpc/object/v2beta/converter.go b/internal/api/grpc/object/v2beta/converter.go index 9b14bb677a..73d5f18843 100644 --- a/internal/api/grpc/object/v2beta/converter.go +++ b/internal/api/grpc/object/v2beta/converter.go @@ -9,6 +9,7 @@ import ( "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/query" object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta" + org_pb "github.com/zitadel/zitadel/pkg/grpc/org/v2beta" ) func DomainToDetailsPb(objectDetail *domain.ObjectDetails) *object.Details { @@ -34,6 +35,7 @@ func ToListDetails(response query.SearchResponse) *object.ListDetails { return details } + func ListQueryToQuery(query *object.ListQuery) (offset, limit uint64, asc bool) { if query == nil { return 0, 0, false @@ -73,3 +75,56 @@ func TextMethodToQuery(method object.TextQueryMethod) query.TextComparison { return -1 } } + +func ListQueryToModel(query *object.ListQuery) (offset, limit uint64, asc bool) { + if query == nil { + return 0, 0, false + } + return query.Offset, uint64(query.Limit), query.Asc +} + +func DomainsToPb(domains []*query.Domain) []*org_pb.Domain { + d := make([]*org_pb.Domain, len(domains)) + for i, domain := range domains { + d[i] = DomainToPb(domain) + } + return d +} + +func DomainToPb(d *query.Domain) *org_pb.Domain { + return &org_pb.Domain{ + OrganizationId: d.OrgID, + DomainName: d.Domain, + IsVerified: d.IsVerified, + IsPrimary: d.IsPrimary, + ValidationType: DomainValidationTypeFromModel(d.ValidationType), + } +} + +func DomainValidationTypeFromModel(validationType domain.OrgDomainValidationType) org_pb.DomainValidationType { + switch validationType { + case domain.OrgDomainValidationTypeDNS: + return org_pb.DomainValidationType_DOMAIN_VALIDATION_TYPE_DNS + case domain.OrgDomainValidationTypeHTTP: + return org_pb.DomainValidationType_DOMAIN_VALIDATION_TYPE_HTTP + case domain.OrgDomainValidationTypeUnspecified: + // added to please golangci-lint + return org_pb.DomainValidationType_DOMAIN_VALIDATION_TYPE_UNSPECIFIED + default: + return org_pb.DomainValidationType_DOMAIN_VALIDATION_TYPE_UNSPECIFIED + } +} + +func DomainValidationTypeToDomain(validationType org_pb.DomainValidationType) domain.OrgDomainValidationType { + switch validationType { + case org_pb.DomainValidationType_DOMAIN_VALIDATION_TYPE_HTTP: + return domain.OrgDomainValidationTypeHTTP + case org_pb.DomainValidationType_DOMAIN_VALIDATION_TYPE_DNS: + return domain.OrgDomainValidationTypeDNS + case org_pb.DomainValidationType_DOMAIN_VALIDATION_TYPE_UNSPECIFIED: + // added to please golangci-lint + return domain.OrgDomainValidationTypeUnspecified + default: + return domain.OrgDomainValidationTypeUnspecified + } +} diff --git a/internal/api/grpc/org/v2beta/helper.go b/internal/api/grpc/org/v2beta/helper.go new file mode 100644 index 0000000000..39bad0dae2 --- /dev/null +++ b/internal/api/grpc/org/v2beta/helper.go @@ -0,0 +1,256 @@ +package org + +import ( + "context" + + "google.golang.org/protobuf/types/known/timestamppb" + + // TODO fix below + filter "github.com/zitadel/zitadel/internal/api/grpc/filter/v2beta" + metadata "github.com/zitadel/zitadel/internal/api/grpc/metadata/v2beta" + v2beta_object "github.com/zitadel/zitadel/internal/api/grpc/object/v2beta" + "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/config/systemdefaults" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/eventstore/v1/models" + "github.com/zitadel/zitadel/internal/query" + "github.com/zitadel/zitadel/internal/zerrors" + v2beta "github.com/zitadel/zitadel/pkg/grpc/object/v2beta" + org "github.com/zitadel/zitadel/pkg/grpc/org/v2beta" + v2beta_org "github.com/zitadel/zitadel/pkg/grpc/org/v2beta" +) + +// NOTE: most of this code is copied from `internal/api/grpc/admin/*`, as we will eventually axe the previous versons of the API, +// we will have code duplication until then + +func listOrgRequestToModel(systemDefaults systemdefaults.SystemDefaults, request *v2beta_org.ListOrganizationsRequest) (*query.OrgSearchQueries, error) { + offset, limit, asc, err := filter.PaginationPbToQuery(systemDefaults, request.Pagination) + if err != nil { + return nil, err + } + queries, err := OrgQueriesToModel(request.Filter) + if err != nil { + return nil, err + } + return &query.OrgSearchQueries{ + SearchRequest: query.SearchRequest{ + Offset: offset, + Limit: limit, + SortingColumn: FieldNameToOrgColumn(request.SortingColumn), + Asc: asc, + }, + Queries: queries, + }, nil +} + +func OrganizationViewToPb(org *query.Org) *v2beta_org.Organization { + return &v2beta_org.Organization{ + Id: org.ID, + State: OrgStateToPb(org.State), + Name: org.Name, + PrimaryDomain: org.Domain, + CreationDate: timestamppb.New(org.CreationDate), + ChangedDate: timestamppb.New(org.ChangeDate), + } +} + +func OrgStateToPb(state domain.OrgState) v2beta_org.OrgState { + switch state { + case domain.OrgStateActive: + return v2beta_org.OrgState_ORG_STATE_ACTIVE + case domain.OrgStateInactive: + return v2beta_org.OrgState_ORG_STATE_INACTIVE + case domain.OrgStateRemoved: + // added to please golangci-lint + return v2beta_org.OrgState_ORG_STATE_REMOVED + case domain.OrgStateUnspecified: + // added to please golangci-lint + return v2beta_org.OrgState_ORG_STATE_UNSPECIFIED + default: + return v2beta_org.OrgState_ORG_STATE_UNSPECIFIED + } +} + +func createdOrganizationToPb(createdOrg *command.CreatedOrg) (_ *org.CreateOrganizationResponse, err error) { + admins := make([]*org.CreatedAdmin, len(createdOrg.CreatedAdmins)) + for i, admin := range createdOrg.CreatedAdmins { + admins[i] = &org.CreatedAdmin{ + UserId: admin.ID, + EmailCode: admin.EmailCode, + PhoneCode: admin.PhoneCode, + } + } + return &org.CreateOrganizationResponse{ + CreationDate: timestamppb.New(createdOrg.ObjectDetails.EventDate), + Id: createdOrg.ObjectDetails.ResourceOwner, + CreatedAdmins: admins, + }, nil +} + +func OrgViewsToPb(orgs []*query.Org) []*v2beta_org.Organization { + o := make([]*v2beta_org.Organization, len(orgs)) + for i, org := range orgs { + o[i] = OrganizationViewToPb(org) + } + return o +} + +func OrgQueriesToModel(queries []*v2beta_org.OrganizationSearchFilter) (_ []query.SearchQuery, err error) { + q := make([]query.SearchQuery, len(queries)) + for i, query := range queries { + q[i], err = OrgQueryToModel(query) + if err != nil { + return nil, err + } + } + return q, nil +} + +func OrgQueryToModel(apiQuery *v2beta_org.OrganizationSearchFilter) (query.SearchQuery, error) { + switch q := apiQuery.Filter.(type) { + case *v2beta_org.OrganizationSearchFilter_DomainFilter: + return query.NewOrgVerifiedDomainSearchQuery(v2beta_object.TextMethodToQuery(q.DomainFilter.Method), q.DomainFilter.Domain) + case *v2beta_org.OrganizationSearchFilter_NameFilter: + return query.NewOrgNameSearchQuery(v2beta_object.TextMethodToQuery(q.NameFilter.Method), q.NameFilter.Name) + case *v2beta_org.OrganizationSearchFilter_StateFilter: + return query.NewOrgStateSearchQuery(OrgStateToDomain(q.StateFilter.State)) + case *v2beta_org.OrganizationSearchFilter_IdFilter: + return query.NewOrgIDSearchQuery(q.IdFilter.Id) + default: + return nil, zerrors.ThrowInvalidArgument(nil, "ORG-vR9nC", "List.Query.Invalid") + } +} + +func OrgStateToDomain(state v2beta_org.OrgState) domain.OrgState { + switch state { + case v2beta_org.OrgState_ORG_STATE_ACTIVE: + return domain.OrgStateActive + case v2beta_org.OrgState_ORG_STATE_INACTIVE: + return domain.OrgStateInactive + case v2beta_org.OrgState_ORG_STATE_REMOVED: + // added to please golangci-lint + return domain.OrgStateRemoved + case v2beta_org.OrgState_ORG_STATE_UNSPECIFIED: + fallthrough + default: + return domain.OrgStateUnspecified + } +} + +func FieldNameToOrgColumn(fieldName v2beta_org.OrgFieldName) query.Column { + switch fieldName { + case v2beta_org.OrgFieldName_ORG_FIELD_NAME_NAME: + return query.OrgColumnName + case v2beta_org.OrgFieldName_ORG_FIELD_NAME_CREATION_DATE: + return query.OrgColumnCreationDate + case v2beta_org.OrgFieldName_ORG_FIELD_NAME_UNSPECIFIED: + return query.Column{} + default: + return query.Column{} + } +} + +func ListOrgDomainsRequestToModel(systemDefaults systemdefaults.SystemDefaults, request *org.ListOrganizationDomainsRequest) (*query.OrgDomainSearchQueries, error) { + offset, limit, asc, err := filter.PaginationPbToQuery(systemDefaults, request.Pagination) + if err != nil { + return nil, err + } + queries, err := DomainQueriesToModel(request.Filters) + if err != nil { + return nil, err + } + return &query.OrgDomainSearchQueries{ + SearchRequest: query.SearchRequest{ + Offset: offset, + Limit: limit, + Asc: asc, + }, + // SortingColumn: //TODO: sorting + Queries: queries, + }, nil +} + +func ListQueryToModel(query *v2beta.ListQuery) (offset, limit uint64, asc bool) { + if query == nil { + return 0, 0, false + } + return query.Offset, uint64(query.Limit), query.Asc +} + +func DomainQueriesToModel(queries []*v2beta_org.DomainSearchFilter) (_ []query.SearchQuery, err error) { + q := make([]query.SearchQuery, len(queries)) + for i, query := range queries { + q[i], err = DomainQueryToModel(query) + if err != nil { + return nil, err + } + } + return q, nil +} + +func DomainQueryToModel(searchQuery *v2beta_org.DomainSearchFilter) (query.SearchQuery, error) { + switch q := searchQuery.Filter.(type) { + case *v2beta_org.DomainSearchFilter_DomainNameFilter: + return query.NewOrgDomainDomainSearchQuery(v2beta_object.TextMethodToQuery(q.DomainNameFilter.Method), q.DomainNameFilter.Name) + default: + return nil, zerrors.ThrowInvalidArgument(nil, "ORG-Ags89", "List.Query.Invalid") + } +} + +func RemoveOrgDomainRequestToDomain(ctx context.Context, req *v2beta_org.DeleteOrganizationDomainRequest) *domain.OrgDomain { + return &domain.OrgDomain{ + ObjectRoot: models.ObjectRoot{ + AggregateID: req.OrganizationId, + }, + Domain: req.Domain, + } +} + +func GenerateOrgDomainValidationRequestToDomain(ctx context.Context, req *v2beta_org.GenerateOrganizationDomainValidationRequest) *domain.OrgDomain { + return &domain.OrgDomain{ + ObjectRoot: models.ObjectRoot{ + AggregateID: req.OrganizationId, + }, + Domain: req.Domain, + ValidationType: v2beta_object.DomainValidationTypeToDomain(req.Type), + } +} + +func ValidateOrgDomainRequestToDomain(ctx context.Context, req *v2beta_org.VerifyOrganizationDomainRequest) *domain.OrgDomain { + return &domain.OrgDomain{ + ObjectRoot: models.ObjectRoot{ + AggregateID: req.OrganizationId, + }, + Domain: req.Domain, + } +} + +func BulkSetOrgMetadataToDomain(req *v2beta_org.SetOrganizationMetadataRequest) []*domain.Metadata { + metadata := make([]*domain.Metadata, len(req.Metadata)) + for i, data := range req.Metadata { + metadata[i] = &domain.Metadata{ + Key: data.Key, + Value: data.Value, + } + } + return metadata +} + +func ListOrgMetadataToDomain(systemDefaults systemdefaults.SystemDefaults, request *v2beta_org.ListOrganizationMetadataRequest) (*query.OrgMetadataSearchQueries, error) { + offset, limit, asc, err := filter.PaginationPbToQuery(systemDefaults, request.Pagination) + if err != nil { + return nil, err + } + queries, err := metadata.OrgMetadataQueriesToQuery(request.Filter) + if err != nil { + return nil, err + } + return &query.OrgMetadataSearchQueries{ + SearchRequest: query.SearchRequest{ + Offset: offset, + Limit: limit, + Asc: asc, + }, + Queries: queries, + }, nil +} diff --git a/internal/api/grpc/org/v2beta/integration_test/org_test.go b/internal/api/grpc/org/v2beta/integration_test/org_test.go index a2b2bf6047..4e0ec26121 100644 --- a/internal/api/grpc/org/v2beta/integration_test/org_test.go +++ b/internal/api/grpc/org/v2beta/integration_test/org_test.go @@ -4,7 +4,9 @@ package org_test import ( "context" + "errors" "os" + "strings" "testing" "time" @@ -14,7 +16,10 @@ import ( "github.com/stretchr/testify/require" "github.com/zitadel/zitadel/internal/integration" + "github.com/zitadel/zitadel/pkg/grpc/admin" + v2beta_object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta" org "github.com/zitadel/zitadel/pkg/grpc/org/v2beta" + v2beta_org "github.com/zitadel/zitadel/pkg/grpc/org/v2beta" "github.com/zitadel/zitadel/pkg/grpc/user/v2" user_v2beta "github.com/zitadel/zitadel/pkg/grpc/user/v2beta" ) @@ -22,7 +27,7 @@ import ( var ( CTX context.Context Instance *integration.Instance - Client org.OrganizationServiceClient + Client v2beta_org.OrganizationServiceClient User *user.AddHumanUserResponse ) @@ -40,20 +45,21 @@ func TestMain(m *testing.M) { }()) } -func TestServer_AddOrganization(t *testing.T) { +func TestServer_CreateOrganization(t *testing.T) { idpResp := Instance.AddGenericOAuthProvider(CTX, Instance.DefaultOrg.Id) tests := []struct { name string ctx context.Context - req *org.AddOrganizationRequest - want *org.AddOrganizationResponse + req *v2beta_org.CreateOrganizationRequest + id string + want *v2beta_org.CreateOrganizationResponse wantErr bool }{ { name: "missing permission", - ctx: Instance.WithAuthorization(context.Background(), integration.UserTypeOrgOwner), - req: &org.AddOrganizationRequest{ + ctx: Instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + req: &v2beta_org.CreateOrganizationRequest{ Name: "name", Admins: nil, }, @@ -62,7 +68,7 @@ func TestServer_AddOrganization(t *testing.T) { { name: "empty name", ctx: CTX, - req: &org.AddOrganizationRequest{ + req: &v2beta_org.CreateOrganizationRequest{ Name: "", Admins: nil, }, @@ -71,34 +77,22 @@ func TestServer_AddOrganization(t *testing.T) { { name: "invalid admin type", ctx: CTX, - req: &org.AddOrganizationRequest{ + req: &v2beta_org.CreateOrganizationRequest{ Name: gofakeit.AppName(), - Admins: []*org.AddOrganizationRequest_Admin{ + Admins: []*v2beta_org.CreateOrganizationRequest_Admin{ {}, }, }, wantErr: true, }, - { - name: "no admin, custom org ID", - ctx: CTX, - req: &org.AddOrganizationRequest{ - Name: gofakeit.AppName(), - OrgId: gu.Ptr("custom-org-ID"), - }, - want: &org.AddOrganizationResponse{ - OrganizationId: "custom-org-ID", - CreatedAdmins: []*org.AddOrganizationResponse_CreatedAdmin{}, - }, - }, { name: "admin with init", ctx: CTX, - req: &org.AddOrganizationRequest{ + req: &v2beta_org.CreateOrganizationRequest{ Name: gofakeit.AppName(), - Admins: []*org.AddOrganizationRequest_Admin{ + Admins: []*v2beta_org.CreateOrganizationRequest_Admin{ { - UserType: &org.AddOrganizationRequest_Admin_Human{ + UserType: &v2beta_org.CreateOrganizationRequest_Admin_Human{ Human: &user_v2beta.AddHumanUserRequest{ Profile: &user_v2beta.SetHumanProfile{ GivenName: "firstname", @@ -115,9 +109,9 @@ func TestServer_AddOrganization(t *testing.T) { }, }, }, - want: &org.AddOrganizationResponse{ - OrganizationId: integration.NotEmpty, - CreatedAdmins: []*org.AddOrganizationResponse_CreatedAdmin{ + want: &v2beta_org.CreateOrganizationResponse{ + Id: integration.NotEmpty, + CreatedAdmins: []*v2beta_org.CreatedAdmin{ { UserId: integration.NotEmpty, EmailCode: gu.Ptr(integration.NotEmpty), @@ -129,14 +123,14 @@ func TestServer_AddOrganization(t *testing.T) { { name: "existing user and new human with idp", ctx: CTX, - req: &org.AddOrganizationRequest{ + req: &v2beta_org.CreateOrganizationRequest{ Name: gofakeit.AppName(), - Admins: []*org.AddOrganizationRequest_Admin{ + Admins: []*v2beta_org.CreateOrganizationRequest_Admin{ { - UserType: &org.AddOrganizationRequest_Admin_UserId{UserId: User.GetUserId()}, + UserType: &v2beta_org.CreateOrganizationRequest_Admin_UserId{UserId: User.GetUserId()}, }, { - UserType: &org.AddOrganizationRequest_Admin_Human{ + UserType: &v2beta_org.CreateOrganizationRequest_Admin_Human{ Human: &user_v2beta.AddHumanUserRequest{ Profile: &user_v2beta.SetHumanProfile{ GivenName: "firstname", @@ -160,8 +154,8 @@ func TestServer_AddOrganization(t *testing.T) { }, }, }, - want: &org.AddOrganizationResponse{ - CreatedAdmins: []*org.AddOrganizationResponse_CreatedAdmin{ + want: &v2beta_org.CreateOrganizationResponse{ + CreatedAdmins: []*v2beta_org.CreatedAdmin{ // a single admin is expected, because the first provided already exists { UserId: integration.NotEmpty, @@ -169,25 +163,36 @@ func TestServer_AddOrganization(t *testing.T) { }, }, }, + { + name: "create with ID", + ctx: CTX, + id: "custom_id", + req: &v2beta_org.CreateOrganizationRequest{ + Name: gofakeit.AppName(), + Id: gu.Ptr("custom_id"), + }, + want: &v2beta_org.CreateOrganizationResponse{}, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := Client.AddOrganization(tt.ctx, tt.req) + got, err := Client.CreateOrganization(tt.ctx, tt.req) if tt.wantErr { require.Error(t, err) return } require.NoError(t, err) + if tt.id != "" { + require.Equal(t, tt.id, got.Id) + } + // check details - assert.NotZero(t, got.GetDetails().GetSequence()) - gotCD := got.GetDetails().GetChangeDate().AsTime() + gotCD := got.GetCreationDate().AsTime() now := time.Now() assert.WithinRange(t, gotCD, now.Add(-time.Minute), now.Add(time.Minute)) - assert.NotEmpty(t, got.GetDetails().GetResourceOwner()) // organization id must be the same as the resourceOwner - assert.Equal(t, got.GetDetails().GetResourceOwner(), got.GetOrganizationId()) // check the admins require.Len(t, got.GetCreatedAdmins(), len(tt.want.GetCreatedAdmins())) @@ -199,7 +204,1739 @@ func TestServer_AddOrganization(t *testing.T) { } } -func assertCreatedAdmin(t *testing.T, expected, got *org.AddOrganizationResponse_CreatedAdmin) { +func TestServer_UpdateOrganization(t *testing.T) { + orgs, orgsName, err := createOrgs(CTX, Client, 1) + if err != nil { + assert.Fail(t, "unable to create org") + return + } + orgId := orgs[0].Id + orgName := orgsName[0] + + tests := []struct { + name string + ctx context.Context + req *v2beta_org.UpdateOrganizationRequest + want *v2beta_org.UpdateOrganizationResponse + wantErr bool + }{ + { + name: "update org with new name", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + req: &v2beta_org.UpdateOrganizationRequest{ + Id: orgId, + Name: "new org name", + }, + }, + { + name: "update org with same name", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + req: &v2beta_org.UpdateOrganizationRequest{ + Id: orgId, + Name: orgName, + }, + }, + { + name: "update org with non existent org id", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + req: &v2beta_org.UpdateOrganizationRequest{ + Id: "non existant org id", + // Name: "", + }, + wantErr: true, + }, + { + name: "update org with no id", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + req: &v2beta_org.UpdateOrganizationRequest{ + Id: "", + Name: orgName, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Client.UpdateOrganization(tt.ctx, tt.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + + // check details + gotCD := got.GetChangeDate().AsTime() + now := time.Now() + assert.WithinRange(t, gotCD, now.Add(-time.Minute), now.Add(time.Minute)) + }) + } +} + +func TestServer_ListOrganizations(t *testing.T) { + testStartTimestamp := time.Now() + ListOrgIinstance := integration.NewInstance(CTX) + listOrgIAmOwnerCtx := ListOrgIinstance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + listOrgClient := ListOrgIinstance.Client.OrgV2beta + + noOfOrgs := 3 + orgs, orgsName, err := createOrgs(listOrgIAmOwnerCtx, listOrgClient, noOfOrgs) + if err != nil { + require.NoError(t, err) + return + } + + // deactivat org[1] + _, err = listOrgClient.DeactivateOrganization(listOrgIAmOwnerCtx, &v2beta_org.DeactivateOrganizationRequest{ + Id: orgs[1].Id, + }) + require.NoError(t, err) + + tests := []struct { + name string + ctx context.Context + query []*v2beta_org.OrganizationSearchFilter + want []*v2beta_org.Organization + err error + }{ + { + name: "list organizations, without required permissions", + ctx: ListOrgIinstance.WithAuthorization(CTX, integration.UserTypeNoPermission), + err: errors.New("membership not found"), + }, + { + name: "list organizations happy path, no filter", + ctx: listOrgIAmOwnerCtx, + want: []*v2beta_org.Organization{ + { + // default org + Name: "testinstance", + }, + { + Id: orgs[0].Id, + Name: orgsName[0], + }, + { + Id: orgs[1].Id, + Name: orgsName[1], + }, + { + Id: orgs[2].Id, + Name: orgsName[2], + }, + }, + }, + { + name: "list organizations by id happy path", + ctx: listOrgIAmOwnerCtx, + query: []*v2beta_org.OrganizationSearchFilter{ + { + Filter: &v2beta_org.OrganizationSearchFilter_IdFilter{ + IdFilter: &v2beta_org.OrgIDFilter{ + Id: orgs[1].Id, + }, + }, + }, + }, + want: []*v2beta_org.Organization{ + { + Id: orgs[1].Id, + Name: orgsName[1], + }, + }, + }, + { + name: "list organizations by state active", + ctx: listOrgIAmOwnerCtx, + query: []*v2beta_org.OrganizationSearchFilter{ + { + Filter: &v2beta_org.OrganizationSearchFilter_StateFilter{ + StateFilter: &v2beta_org.OrgStateFilter{ + State: v2beta_org.OrgState_ORG_STATE_ACTIVE, + }, + }, + }, + }, + want: []*v2beta_org.Organization{ + { + // default org + Name: "testinstance", + }, + { + Id: orgs[0].Id, + Name: orgsName[0], + }, + { + Id: orgs[2].Id, + Name: orgsName[2], + }, + }, + }, + { + name: "list organizations by state inactive", + ctx: listOrgIAmOwnerCtx, + query: []*v2beta_org.OrganizationSearchFilter{ + { + Filter: &v2beta_org.OrganizationSearchFilter_StateFilter{ + StateFilter: &v2beta_org.OrgStateFilter{ + State: v2beta_org.OrgState_ORG_STATE_INACTIVE, + }, + }, + }, + }, + want: []*v2beta_org.Organization{ + { + Id: orgs[1].Id, + Name: orgsName[1], + }, + }, + }, + { + name: "list organizations by id bad id", + ctx: listOrgIAmOwnerCtx, + query: []*v2beta_org.OrganizationSearchFilter{ + { + Filter: &v2beta_org.OrganizationSearchFilter_IdFilter{ + IdFilter: &v2beta_org.OrgIDFilter{ + Id: "bad id", + }, + }, + }, + }, + }, + { + name: "list organizations specify org name equals", + ctx: listOrgIAmOwnerCtx, + query: []*v2beta_org.OrganizationSearchFilter{ + { + Filter: &v2beta_org.OrganizationSearchFilter_NameFilter{ + NameFilter: &v2beta_org.OrgNameFilter{ + Name: orgsName[1], + Method: v2beta_object.TextQueryMethod_TEXT_QUERY_METHOD_EQUALS, + }, + }, + }, + }, + want: []*v2beta_org.Organization{ + { + Id: orgs[1].Id, + Name: orgsName[1], + }, + }, + }, + { + name: "list organizations specify org name contains", + ctx: listOrgIAmOwnerCtx, + query: []*v2beta_org.OrganizationSearchFilter{ + { + Filter: &v2beta_org.OrganizationSearchFilter_NameFilter{ + NameFilter: &v2beta_org.OrgNameFilter{ + Name: func() string { + return orgsName[1][1 : len(orgsName[1])-2] + }(), + Method: v2beta_object.TextQueryMethod_TEXT_QUERY_METHOD_CONTAINS, + }, + }, + }, + }, + want: []*v2beta_org.Organization{ + { + Id: orgs[1].Id, + Name: orgsName[1], + }, + }, + }, + { + name: "list organizations specify org name contains IGNORE CASE", + ctx: listOrgIAmOwnerCtx, + query: []*v2beta_org.OrganizationSearchFilter{ + { + Filter: &v2beta_org.OrganizationSearchFilter_NameFilter{ + NameFilter: &v2beta_org.OrgNameFilter{ + Name: func() string { + return strings.ToUpper(orgsName[1][1 : len(orgsName[1])-2]) + }(), + Method: v2beta_object.TextQueryMethod_TEXT_QUERY_METHOD_CONTAINS_IGNORE_CASE, + }, + }, + }, + }, + want: []*v2beta_org.Organization{ + { + Id: orgs[1].Id, + Name: orgsName[1], + }, + }, + }, + { + name: "list organizations specify domain name equals", + ctx: listOrgIAmOwnerCtx, + query: []*v2beta_org.OrganizationSearchFilter{ + { + Filter: &org.OrganizationSearchFilter_DomainFilter{ + DomainFilter: &org.OrgDomainFilter{ + Domain: func() string { + listOrgRes, err := listOrgClient.ListOrganizations(listOrgIAmOwnerCtx, &v2beta_org.ListOrganizationsRequest{ + Filter: []*v2beta_org.OrganizationSearchFilter{ + { + Filter: &v2beta_org.OrganizationSearchFilter_IdFilter{ + IdFilter: &v2beta_org.OrgIDFilter{ + Id: orgs[1].Id, + }, + }, + }, + }, + }) + require.NoError(t, err) + domain := listOrgRes.Organizations[0].PrimaryDomain + return domain + }(), + Method: v2beta_object.TextQueryMethod_TEXT_QUERY_METHOD_EQUALS, + }, + }, + }, + }, + want: []*v2beta_org.Organization{ + { + Id: orgs[1].Id, + Name: orgsName[1], + }, + }, + }, + { + name: "list organizations specify domain name contains", + ctx: listOrgIAmOwnerCtx, + query: []*v2beta_org.OrganizationSearchFilter{ + { + Filter: &org.OrganizationSearchFilter_DomainFilter{ + DomainFilter: &org.OrgDomainFilter{ + Domain: func() string { + domain := strings.ToLower(strings.ReplaceAll(orgsName[1][1:len(orgsName[1])-2], " ", "-")) + return domain + }(), + Method: v2beta_object.TextQueryMethod_TEXT_QUERY_METHOD_CONTAINS, + }, + }, + }, + }, + want: []*v2beta_org.Organization{ + { + Id: orgs[1].Id, + Name: orgsName[1], + }, + }, + }, + { + name: "list organizations specify org name contains IGNORE CASE", + ctx: listOrgIAmOwnerCtx, + query: []*v2beta_org.OrganizationSearchFilter{ + { + Filter: &org.OrganizationSearchFilter_DomainFilter{ + DomainFilter: &org.OrgDomainFilter{ + Domain: func() string { + domain := strings.ToUpper(strings.ReplaceAll(orgsName[1][1:len(orgsName[1])-2], " ", "-")) + return domain + }(), + Method: v2beta_object.TextQueryMethod_TEXT_QUERY_METHOD_CONTAINS_IGNORE_CASE, + }, + }, + }, + }, + want: []*v2beta_org.Organization{ + { + Id: orgs[1].Id, + Name: orgsName[1], + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, 10*time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + got, err := listOrgClient.ListOrganizations(tt.ctx, &v2beta_org.ListOrganizationsRequest{ + Filter: tt.query, + }) + if tt.err != nil { + require.ErrorContains(t, err, tt.err.Error()) + return + } + require.NoError(ttt, err) + + require.Equal(ttt, uint64(len(tt.want)), got.Pagination.GetTotalResult()) + + foundOrgs := 0 + for _, got := range got.Organizations { + for _, org := range tt.want { + + // created/chagned date + gotCD := got.GetCreationDate().AsTime() + now := time.Now() + assert.WithinRange(ttt, gotCD, testStartTimestamp, now.Add(time.Minute)) + gotCD = got.GetChangedDate().AsTime() + assert.WithinRange(ttt, gotCD, testStartTimestamp, now.Add(time.Minute)) + + // default org + if org.Name == got.Name && got.Name == "testinstance" { + foundOrgs += 1 + continue + } + + if org.Name == got.Name && + org.Id == got.Id { + foundOrgs += 1 + } + } + } + require.Equal(ttt, len(tt.want), foundOrgs) + }, retryDuration, tick, "timeout waiting for expected organizations being created") + }) + } +} + +func TestServer_DeleteOrganization(t *testing.T) { + tests := []struct { + name string + ctx context.Context + createOrgFunc func() string + req *v2beta_org.DeleteOrganizationRequest + want *v2beta_org.DeleteOrganizationResponse + dontCheckTime bool + err error + }{ + { + name: "delete org no permission", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeNoPermission), + createOrgFunc: func() string { + orgs, _, err := createOrgs(CTX, Client, 1) + if err != nil { + assert.Fail(t, "unable to create org") + return "" + } + return orgs[0].Id + }, + req: &v2beta_org.DeleteOrganizationRequest{}, + err: errors.New("membership not found"), + }, + { + name: "delete org happy path", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + createOrgFunc: func() string { + orgs, _, err := createOrgs(CTX, Client, 1) + if err != nil { + assert.Fail(t, "unable to create org") + return "" + } + return orgs[0].Id + }, + req: &v2beta_org.DeleteOrganizationRequest{}, + }, + { + name: "delete already deleted org", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + createOrgFunc: func() string { + orgs, _, err := createOrgs(CTX, Client, 1) + if err != nil { + assert.Fail(t, "unable to create org") + return "" + } + // delete org + _, err = Client.DeleteOrganization(CTX, &v2beta_org.DeleteOrganizationRequest{Id: orgs[0].Id}) + require.NoError(t, err) + + return orgs[0].Id + }, + req: &v2beta_org.DeleteOrganizationRequest{}, + dontCheckTime: true, + }, + { + name: "delete non existent org", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + req: &v2beta_org.DeleteOrganizationRequest{ + Id: "non existent org id", + }, + dontCheckTime: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.createOrgFunc != nil { + tt.req.Id = tt.createOrgFunc() + } + + got, err := Client.DeleteOrganization(tt.ctx, tt.req) + if tt.err != nil { + require.Contains(t, err.Error(), tt.err.Error()) + return + } + require.NoError(t, err) + + // check details + gotCD := got.GetDeletionDate().AsTime() + if !tt.dontCheckTime { + now := time.Now() + assert.WithinRange(t, gotCD, now.Add(-time.Minute), now.Add(time.Minute)) + } + }) + } +} + +func TestServer_DeactivateReactivateNonExistentOrganization(t *testing.T) { + ctx := Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + + // deactivate non existent organization + _, err := Client.DeactivateOrganization(ctx, &v2beta_org.DeactivateOrganizationRequest{ + Id: "non existent organization", + }) + require.Contains(t, err.Error(), "Organisation not found") + + // reactivate non existent organization + _, err = Client.ActivateOrganization(ctx, &v2beta_org.ActivateOrganizationRequest{ + Id: "non existent organization", + }) + require.Contains(t, err.Error(), "Organisation not found") +} + +func TestServer_ActivateOrganization(t *testing.T) { + tests := []struct { + name string + ctx context.Context + testFunc func() string + err error + }{ + { + name: "Activate, happy path", + ctx: CTX, + testFunc: func() string { + // 1. create organization + orgs, _, err := createOrgs(CTX, Client, 1) + if err != nil { + assert.Fail(t, "unable to create orgs") + return "" + } + orgId := orgs[0].Id + + // 2. deactivate organization once + deactivate_res, err := Client.DeactivateOrganization(CTX, &v2beta_org.DeactivateOrganizationRequest{ + Id: orgId, + }) + require.NoError(t, err) + gotCD := deactivate_res.GetChangeDate().AsTime() + now := time.Now() + assert.WithinRange(t, gotCD, now.Add(-time.Minute), now.Add(time.Minute)) + + // 3. check organization state is deactivated + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, 10*time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + listOrgRes, err := Client.ListOrganizations(CTX, &v2beta_org.ListOrganizationsRequest{ + Filter: []*v2beta_org.OrganizationSearchFilter{ + { + Filter: &v2beta_org.OrganizationSearchFilter_IdFilter{ + IdFilter: &v2beta_org.OrgIDFilter{ + Id: orgId, + }, + }, + }, + }, + }) + require.NoError(ttt, err) + require.Equal(ttt, v2beta_org.OrgState_ORG_STATE_INACTIVE, listOrgRes.Organizations[0].State) + }, retryDuration, tick, "timeout waiting for expected organizations being created") + + return orgId + }, + }, + { + name: "Activate, no permission", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeNoPermission), + testFunc: func() string { + orgs, _, err := createOrgs(CTX, Client, 1) + if err != nil { + assert.Fail(t, "unable to create orgs") + return "" + } + orgId := orgs[0].Id + return orgId + }, + // BUG: this needs changing + err: errors.New("membership not found"), + }, + { + name: "Activate, not existing", + ctx: CTX, + testFunc: func() string { + return "non-existing-org-id" + }, + err: errors.New("Organisation not found"), + }, + { + name: "Activate, already activated", + ctx: CTX, + testFunc: func() string { + orgs, _, err := createOrgs(CTX, Client, 1) + if err != nil { + assert.Fail(t, "unable to create orgs") + return "" + } + orgId := orgs[0].Id + return orgId + }, + err: errors.New("Organisation is already active"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var orgId string + if tt.testFunc != nil { + orgId = tt.testFunc() + } + _, err := Client.ActivateOrganization(tt.ctx, &v2beta_org.ActivateOrganizationRequest{ + Id: orgId, + }) + if tt.err != nil { + require.Contains(t, err.Error(), tt.err.Error()) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestServer_DeactivateOrganization(t *testing.T) { + tests := []struct { + name string + ctx context.Context + testFunc func() string + err error + }{ + { + name: "Deactivate, happy path", + ctx: CTX, + testFunc: func() string { + // 1. create organization + orgs, _, err := createOrgs(CTX, Client, 1) + if err != nil { + assert.Fail(t, "unable to create orgs") + return "" + } + orgId := orgs[0].Id + + return orgId + }, + }, + { + name: "Deactivate, no permission", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeNoPermission), + testFunc: func() string { + orgs, _, err := createOrgs(CTX, Client, 1) + if err != nil { + assert.Fail(t, "unable to create orgs") + return "" + } + orgId := orgs[0].Id + return orgId + }, + // BUG: this needs changing + err: errors.New("membership not found"), + }, + { + name: "Deactivate, not existing", + ctx: CTX, + testFunc: func() string { + return "non-existing-org-id" + }, + err: errors.New("Organisation not found"), + }, + { + name: "Deactivate, already deactivated", + ctx: CTX, + testFunc: func() string { + // 1. create organization + orgs, _, err := createOrgs(CTX, Client, 1) + if err != nil { + assert.Fail(t, "unable to create orgs") + return "" + } + orgId := orgs[0].Id + + // 2. deactivate organization once + deactivate_res, err := Client.DeactivateOrganization(CTX, &v2beta_org.DeactivateOrganizationRequest{ + Id: orgId, + }) + require.NoError(t, err) + gotCD := deactivate_res.GetChangeDate().AsTime() + now := time.Now() + assert.WithinRange(t, gotCD, now.Add(-time.Minute), now.Add(time.Minute)) + + // 3. check organization state is deactivated + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, 10*time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + listOrgRes, err := Client.ListOrganizations(CTX, &v2beta_org.ListOrganizationsRequest{ + Filter: []*v2beta_org.OrganizationSearchFilter{ + { + Filter: &v2beta_org.OrganizationSearchFilter_IdFilter{ + IdFilter: &v2beta_org.OrgIDFilter{ + Id: orgId, + }, + }, + }, + }, + }) + require.NoError(ttt, err) + require.Equal(ttt, v2beta_org.OrgState_ORG_STATE_INACTIVE, listOrgRes.Organizations[0].State) + }, retryDuration, tick, "timeout waiting for expected organizations being created") + + return orgId + }, + err: errors.New("Organisation is already deactivated"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var orgId string + orgId = tt.testFunc() + _, err := Client.DeactivateOrganization(tt.ctx, &v2beta_org.DeactivateOrganizationRequest{ + Id: orgId, + }) + if tt.err != nil { + require.Contains(t, err.Error(), tt.err.Error()) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestServer_AddOrganizationDomain(t *testing.T) { + tests := []struct { + name string + ctx context.Context + domain string + testFunc func() string + err error + }{ + { + name: "add org domain, happy path", + domain: gofakeit.URL(), + testFunc: func() string { + orgs, _, err := createOrgs(CTX, Client, 1) + if err != nil { + assert.Fail(t, "unable to create org") + return "" + } + orgId := orgs[0].Id + return orgId + }, + }, + { + name: "add org domain, twice", + domain: gofakeit.URL(), + testFunc: func() string { + // 1. create organization + orgs, _, err := createOrgs(CTX, Client, 1) + if err != nil { + assert.Fail(t, "unable to create org") + return "" + } + orgId := orgs[0].Id + + domain := gofakeit.URL() + // 2. add domain + addOrgDomainRes, err := Client.AddOrganizationDomain(CTX, &v2beta_org.AddOrganizationDomainRequest{ + OrganizationId: orgId, + Domain: domain, + }) + require.NoError(t, err) + // check details + gotCD := addOrgDomainRes.GetCreationDate().AsTime() + now := time.Now() + assert.WithinRange(t, gotCD, now.Add(-time.Minute), now.Add(time.Minute)) + + // check domain added + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, 10*time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + queryRes, err := Client.ListOrganizationDomains(CTX, &v2beta_org.ListOrganizationDomainsRequest{ + OrganizationId: orgId, + }) + require.NoError(t, err) + found := false + for _, res := range queryRes.Domains { + if res.DomainName == domain { + found = true + } + } + require.True(t, found, "unable to find added domain") + }, retryDuration, tick, "timeout waiting for expected organizations being created") + + return orgId + }, + }, + { + name: "add org domain to non existent org", + domain: gofakeit.URL(), + testFunc: func() string { + return "non-existing-org-id" + }, + // BUG: should return a error + err: nil, + }, + } + + for _, tt := range tests { + var orgId string + t.Run(tt.name, func(t *testing.T) { + orgId = tt.testFunc() + }) + addOrgDomainRes, err := Client.AddOrganizationDomain(CTX, &v2beta_org.AddOrganizationDomainRequest{ + OrganizationId: orgId, + Domain: tt.domain, + }) + if tt.err != nil { + require.Contains(t, err.Error(), tt.err.Error()) + } else { + require.NoError(t, err) + // check details + gotCD := addOrgDomainRes.GetCreationDate().AsTime() + now := time.Now() + assert.WithinRange(t, gotCD, now.Add(-time.Minute), now.Add(time.Minute)) + } + } +} + +func TestServer_ListOrganizationDomains(t *testing.T) { + domain := gofakeit.URL() + tests := []struct { + name string + ctx context.Context + domain string + testFunc func() string + err error + }{ + { + name: "list org domain, happy path", + domain: domain, + testFunc: func() string { + // 1. create organization + orgs, _, err := createOrgs(CTX, Client, 1) + if err != nil { + assert.Fail(t, "unable to create org") + return "" + } + orgId := orgs[0].Id + // 2. add domain + addOrgDomainRes, err := Client.AddOrganizationDomain(CTX, &v2beta_org.AddOrganizationDomainRequest{ + OrganizationId: orgId, + Domain: domain, + }) + require.NoError(t, err) + // check details + gotCD := addOrgDomainRes.GetCreationDate().AsTime() + now := time.Now() + assert.WithinRange(t, gotCD, now.Add(-time.Minute), now.Add(time.Minute)) + + return orgId + }, + }, + } + + for _, tt := range tests { + var orgId string + t.Run(tt.name, func(t *testing.T) { + orgId = tt.testFunc() + }) + + var err error + var queryRes *v2beta_org.ListOrganizationDomainsResponse + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, 10*time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + queryRes, err = Client.ListOrganizationDomains(CTX, &v2beta_org.ListOrganizationDomainsRequest{ + OrganizationId: orgId, + }) + require.NoError(t, err) + found := false + for _, res := range queryRes.Domains { + if res.DomainName == tt.domain { + found = true + } + } + require.True(t, found, "unable to find added domain") + }, retryDuration, tick, "timeout waiting for adding domain") + + } +} + +func TestServer_DeleteOerganizationDomain(t *testing.T) { + domain := gofakeit.URL() + tests := []struct { + name string + ctx context.Context + domain string + testFunc func() string + err error + }{ + { + name: "delete org domain, happy path", + domain: domain, + testFunc: func() string { + // 1. create organization + orgs, _, err := createOrgs(CTX, Client, 1) + if err != nil { + assert.Fail(t, "unable to create org") + return "" + } + orgId := orgs[0].Id + + // 2. add domain + addOrgDomainRes, err := Client.AddOrganizationDomain(CTX, &v2beta_org.AddOrganizationDomainRequest{ + OrganizationId: orgId, + Domain: domain, + }) + require.NoError(t, err) + // check details + gotCD := addOrgDomainRes.GetCreationDate().AsTime() + now := time.Now() + assert.WithinRange(t, gotCD, now.Add(-time.Minute), now.Add(time.Minute)) + + // check domain added + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, 10*time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + queryRes, err := Client.ListOrganizationDomains(CTX, &v2beta_org.ListOrganizationDomainsRequest{ + OrganizationId: orgId, + }) + require.NoError(t, err) + found := false + for _, res := range queryRes.Domains { + if res.DomainName == domain { + found = true + } + } + require.True(t, found, "unable to find added domain") + }, retryDuration, tick, "timeout waiting for expected organizations being created") + + return orgId + }, + }, + { + name: "delete org domain, twice", + domain: gofakeit.URL(), + testFunc: func() string { + // 1. create organization + orgs, _, err := createOrgs(CTX, Client, 1) + if err != nil { + assert.Fail(t, "unable to create org") + return "" + } + orgId := orgs[0].Id + + domain := gofakeit.URL() + // 2. add domain + addOrgDomainRes, err := Client.AddOrganizationDomain(CTX, &v2beta_org.AddOrganizationDomainRequest{ + OrganizationId: orgId, + Domain: domain, + }) + require.NoError(t, err) + // check details + gotCD := addOrgDomainRes.GetCreationDate().AsTime() + now := time.Now() + assert.WithinRange(t, gotCD, now.Add(-time.Minute), now.Add(time.Minute)) + + // check domain added + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, 10*time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + queryRes, err := Client.ListOrganizationDomains(CTX, &v2beta_org.ListOrganizationDomainsRequest{ + OrganizationId: orgId, + }) + require.NoError(t, err) + found := false + for _, res := range queryRes.Domains { + if res.DomainName == domain { + found = true + } + } + require.True(t, found, "unable to find added domain") + }, retryDuration, tick, "timeout waiting for expected organizations being created") + + _, err = Client.DeleteOrganizationDomain(CTX, &v2beta_org.DeleteOrganizationDomainRequest{ + OrganizationId: orgId, + Domain: domain, + }) + require.NoError(t, err) + + return orgId + }, + err: errors.New("Domain doesn't exist on organization"), + }, + { + name: "delete org domain to non existent org", + domain: gofakeit.URL(), + testFunc: func() string { + return "non-existing-org-id" + }, + // BUG: + err: errors.New("Domain doesn't exist on organization"), + }, + } + + for _, tt := range tests { + var orgId string + t.Run(tt.name, func(t *testing.T) { + orgId = tt.testFunc() + }) + + _, err := Client.DeleteOrganizationDomain(CTX, &v2beta_org.DeleteOrganizationDomainRequest{ + OrganizationId: orgId, + Domain: tt.domain, + }) + + if tt.err != nil { + require.Contains(t, err.Error(), tt.err.Error()) + } else { + require.NoError(t, err) + } + } +} + +func TestServer_AddListDeleteOrganizationDomain(t *testing.T) { + tests := []struct { + name string + testFunc func() + }{ + { + name: "add org domain, re-add org domain", + testFunc: func() { + // 1. create organization + orgs, _, err := createOrgs(CTX, Client, 1) + if err != nil { + assert.Fail(t, "unable to create org") + return + } + orgId := orgs[0].Id + // ctx := Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + + domain := gofakeit.URL() + // 2. add domain + addOrgDomainRes, err := Client.AddOrganizationDomain(CTX, &v2beta_org.AddOrganizationDomainRequest{ + OrganizationId: orgId, + Domain: domain, + }) + require.NoError(t, err) + // check details + gotCD := addOrgDomainRes.GetCreationDate().AsTime() + now := time.Now() + assert.WithinRange(t, gotCD, now.Add(-time.Minute), now.Add(time.Minute)) + + // 3. re-add domain + _, err = Client.AddOrganizationDomain(CTX, &v2beta_org.AddOrganizationDomainRequest{ + OrganizationId: orgId, + Domain: domain, + }) + // TODO remove error for adding already existing domain + // require.NoError(t, err) + require.Contains(t, err.Error(), "Errors.Already.Exists") + // check details + // gotCD = addOrgDomainRes.GetDetails().GetChangeDate().AsTime() + // now = time.Now() + // assert.WithinRange(t, gotCD, now.Add(-time.Minute), now.Add(time.Minute)) + + // 4. check domain is added + queryRes, err := Client.ListOrganizationDomains(CTX, &v2beta_org.ListOrganizationDomainsRequest{ + OrganizationId: orgId, + }) + require.NoError(t, err) + found := false + for _, res := range queryRes.Domains { + if res.DomainName == domain { + found = true + } + } + require.True(t, found, "unable to find added domain") + }, + }, + { + name: "add org domain, delete org domain, re-delete org domain", + testFunc: func() { + // 1. create organization + orgs, _, err := createOrgs(CTX, Client, 1) + if err != nil { + assert.Fail(t, "unable to create org") + return + } + orgId := orgs[0].Id + + domain := gofakeit.URL() + // 2. add domain + addOrgDomainRes, err := Client.AddOrganizationDomain(CTX, &v2beta_org.AddOrganizationDomainRequest{ + OrganizationId: orgId, + Domain: domain, + }) + require.NoError(t, err) + // check details + gotCD := addOrgDomainRes.GetCreationDate().AsTime() + now := time.Now() + assert.WithinRange(t, gotCD, now.Add(-time.Minute), now.Add(time.Minute)) + + // 2. delete organisation domain + deleteOrgDomainRes, err := Client.DeleteOrganizationDomain(CTX, &v2beta_org.DeleteOrganizationDomainRequest{ + OrganizationId: orgId, + Domain: domain, + }) + require.NoError(t, err) + // check details + gotCD = deleteOrgDomainRes.GetDeletionDate().AsTime() + now = time.Now() + assert.WithinRange(t, gotCD, now.Add(-time.Minute), now.Add(time.Minute)) + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, 10*time.Minute) + require.EventuallyWithT(t, func(t *assert.CollectT) { + // 3. check organization domain deleted + queryRes, err := Client.ListOrganizationDomains(CTX, &v2beta_org.ListOrganizationDomainsRequest{ + OrganizationId: orgId, + }) + require.NoError(t, err) + found := false + for _, res := range queryRes.Domains { + if res.DomainName == domain { + found = true + } + } + require.False(t, found, "deleted domain found") + }, retryDuration, tick, "timeout waiting for expected organizations being created") + + // 4. redelete organisation domain + _, err = Client.DeleteOrganizationDomain(CTX, &v2beta_org.DeleteOrganizationDomainRequest{ + OrganizationId: orgId, + Domain: domain, + }) + // TODO remove error for deleting org domain already deleted + // require.NoError(t, err) + require.Contains(t, err.Error(), "Domain doesn't exist on organization") + // check details + // gotCD = deleteOrgDomainRes.GetDetails().GetChangeDate().AsTime() + // now = time.Now() + // assert.WithinRange(t, gotCD, now.Add(-time.Minute), now.Add(time.Minute)) + + // 5. check organization domain deleted + queryRes, err := Client.ListOrganizationDomains(CTX, &v2beta_org.ListOrganizationDomainsRequest{ + OrganizationId: orgId, + }) + require.NoError(t, err) + found := false + for _, res := range queryRes.Domains { + if res.DomainName == domain { + found = true + } + } + require.False(t, found, "deleted domain found") + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.testFunc() + }) + } +} + +func TestServer_ValidateOrganizationDomain(t *testing.T) { + orgs, _, err := createOrgs(CTX, Client, 1) + if err != nil { + assert.Fail(t, "unable to create org") + return + } + orgId := orgs[0].Id + + _, err = Instance.Client.Admin.UpdateDomainPolicy(CTX, &admin.UpdateDomainPolicyRequest{ + ValidateOrgDomains: true, + }) + if err != nil && !strings.Contains(err.Error(), "Organisation is already deactivated") { + require.NoError(t, err) + } + + domain := gofakeit.URL() + _, err = Client.AddOrganizationDomain(CTX, &v2beta_org.AddOrganizationDomainRequest{ + OrganizationId: orgId, + Domain: domain, + }) + require.NoError(t, err) + + tests := []struct { + name string + ctx context.Context + req *v2beta_org.GenerateOrganizationDomainValidationRequest + err error + }{ + { + name: "validate org http happy path", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + req: &v2beta_org.GenerateOrganizationDomainValidationRequest{ + OrganizationId: orgId, + Domain: domain, + Type: org.DomainValidationType_DOMAIN_VALIDATION_TYPE_HTTP, + }, + }, + { + name: "validate org http non existnetn org id", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + req: &v2beta_org.GenerateOrganizationDomainValidationRequest{ + OrganizationId: "non existent org id", + Domain: domain, + Type: org.DomainValidationType_DOMAIN_VALIDATION_TYPE_HTTP, + }, + // BUG: this should be 'organization does not exist' + err: errors.New("Domain doesn't exist on organization"), + }, + { + name: "validate org dns happy path", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + req: &v2beta_org.GenerateOrganizationDomainValidationRequest{ + OrganizationId: orgId, + Domain: domain, + Type: org.DomainValidationType_DOMAIN_VALIDATION_TYPE_DNS, + }, + }, + { + name: "validate org dns non existnetn org id", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + req: &v2beta_org.GenerateOrganizationDomainValidationRequest{ + OrganizationId: "non existent org id", + Domain: domain, + Type: org.DomainValidationType_DOMAIN_VALIDATION_TYPE_DNS, + }, + // BUG: this should be 'organization does not exist' + err: errors.New("Domain doesn't exist on organization"), + }, + { + name: "validate org non existnetn domain", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + req: &v2beta_org.GenerateOrganizationDomainValidationRequest{ + OrganizationId: orgId, + Domain: "non existent domain", + Type: org.DomainValidationType_DOMAIN_VALIDATION_TYPE_HTTP, + }, + err: errors.New("Domain doesn't exist on organization"), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Client.GenerateOrganizationDomainValidation(tt.ctx, tt.req) + if tt.err != nil { + require.Contains(t, err.Error(), tt.err.Error()) + return + } + require.NoError(t, err) + + require.NotEmpty(t, got.Token) + require.Contains(t, got.Url, domain) + }) + } +} + +func TestServer_SetOrganizationMetadata(t *testing.T) { + orgs, _, err := createOrgs(CTX, Client, 1) + if err != nil { + assert.Fail(t, "unable to create org") + return + } + orgId := orgs[0].Id + + tests := []struct { + name string + ctx context.Context + setupFunc func() + orgId string + key string + value string + err error + }{ + { + name: "set org metadata", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + orgId: orgId, + key: "key1", + value: "value1", + }, + { + name: "set org metadata on non existant org", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + orgId: "non existant orgid", + key: "key2", + value: "value2", + err: errors.New("Organisation not found"), + }, + { + name: "update org metadata", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + setupFunc: func() { + _, err := Client.SetOrganizationMetadata(CTX, &v2beta_org.SetOrganizationMetadataRequest{ + OrganizationId: orgId, + Metadata: []*v2beta_org.Metadata{ + { + Key: "key3", + Value: []byte("value3"), + }, + }, + }) + require.NoError(t, err) + }, + orgId: orgId, + key: "key4", + value: "value4", + }, + { + name: "update org metadata with same value", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + setupFunc: func() { + _, err := Client.SetOrganizationMetadata(CTX, &v2beta_org.SetOrganizationMetadataRequest{ + OrganizationId: orgId, + Metadata: []*v2beta_org.Metadata{ + { + Key: "key5", + Value: []byte("value5"), + }, + }, + }) + require.NoError(t, err) + }, + orgId: orgId, + key: "key5", + value: "value5", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.setupFunc != nil { + tt.setupFunc() + } + got, err := Client.SetOrganizationMetadata(tt.ctx, &v2beta_org.SetOrganizationMetadataRequest{ + OrganizationId: tt.orgId, + Metadata: []*v2beta_org.Metadata{ + { + Key: tt.key, + Value: []byte(tt.value), + }, + }, + }) + if tt.err != nil { + require.Contains(t, err.Error(), tt.err.Error()) + return + } + require.NoError(t, err) + + // check details + gotCD := got.GetSetDate().AsTime() + now := time.Now() + assert.WithinRange(t, gotCD, now.Add(-time.Minute), now.Add(time.Minute)) + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, 10*time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + // check metadata + listMetadataRes, err := Client.ListOrganizationMetadata(tt.ctx, &v2beta_org.ListOrganizationMetadataRequest{ + OrganizationId: orgId, + }) + require.NoError(t, err) + foundMetadata := false + foundMetadataKeyCount := 0 + for _, res := range listMetadataRes.Metadata { + if res.Key == tt.key { + foundMetadataKeyCount += 1 + } + if res.Key == tt.key && + string(res.Value) == tt.value { + foundMetadata = true + } + } + require.True(ttt, foundMetadata, "unable to find added metadata") + require.Equal(ttt, 1, foundMetadataKeyCount, "same metadata key found multiple times") + }, retryDuration, tick, "timeout waiting for expected organizations being created") + }) + } +} + +func TestServer_ListOrganizationMetadata(t *testing.T) { + orgs, _, err := createOrgs(CTX, Client, 1) + if err != nil { + assert.Fail(t, "unable to create org") + return + } + orgId := orgs[0].Id + + tests := []struct { + name string + ctx context.Context + setupFunc func() + orgId string + keyValuPars []struct { + key string + value string + } + }{ + { + name: "list org metadata happy path", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + setupFunc: func() { + _, err := Client.SetOrganizationMetadata(CTX, &v2beta_org.SetOrganizationMetadataRequest{ + OrganizationId: orgId, + Metadata: []*v2beta_org.Metadata{ + { + Key: "key1", + Value: []byte("value1"), + }, + }, + }) + require.NoError(t, err) + }, + orgId: orgId, + keyValuPars: []struct{ key, value string }{ + { + key: "key1", + value: "value1", + }, + }, + }, + { + name: "list multiple org metadata happy path", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + setupFunc: func() { + _, err := Client.SetOrganizationMetadata(CTX, &v2beta_org.SetOrganizationMetadataRequest{ + OrganizationId: orgId, + Metadata: []*v2beta_org.Metadata{ + { + Key: "key2", + Value: []byte("value2"), + }, + { + Key: "key3", + Value: []byte("value3"), + }, + { + Key: "key4", + Value: []byte("value4"), + }, + }, + }) + require.NoError(t, err) + }, + orgId: orgId, + keyValuPars: []struct{ key, value string }{ + { + key: "key2", + value: "value2", + }, + { + key: "key3", + value: "value3", + }, + { + key: "key4", + value: "value4", + }, + }, + }, + { + name: "list org metadata for non existent org", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + orgId: "non existent orgid", + keyValuPars: []struct{ key, value string }{}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.setupFunc != nil { + tt.setupFunc() + } + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, 10*time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + got, err := Client.ListOrganizationMetadata(tt.ctx, &v2beta_org.ListOrganizationMetadataRequest{ + OrganizationId: tt.orgId, + }) + require.NoError(t, err) + + foundMetadataCount := 0 + for _, kv := range tt.keyValuPars { + for _, res := range got.Metadata { + if res.Key == kv.key && + string(res.Value) == kv.value { + foundMetadataCount += 1 + } + } + } + require.Equal(t, len(tt.keyValuPars), foundMetadataCount) + }, retryDuration, tick, "timeout waiting for expected organizations being created") + }) + } +} + +func TestServer_DeleteOrganizationMetadata(t *testing.T) { + orgs, _, err := createOrgs(CTX, Client, 1) + if err != nil { + assert.Fail(t, "unable to create org") + return + } + orgId := orgs[0].Id + + tests := []struct { + name string + ctx context.Context + setupFunc func() + orgId string + metadataToDelete []struct { + key string + value string + } + metadataToRemain []struct { + key string + value string + } + err error + }{ + { + name: "delete org metadata happy path", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + setupFunc: func() { + _, err := Client.SetOrganizationMetadata(CTX, &v2beta_org.SetOrganizationMetadataRequest{ + OrganizationId: orgId, + Metadata: []*v2beta_org.Metadata{ + { + Key: "key1", + Value: []byte("value1"), + }, + }, + }) + require.NoError(t, err) + }, + orgId: orgId, + metadataToDelete: []struct{ key, value string }{ + { + key: "key1", + value: "value1", + }, + }, + }, + { + name: "delete multiple org metadata happy path", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + setupFunc: func() { + _, err := Client.SetOrganizationMetadata(CTX, &v2beta_org.SetOrganizationMetadataRequest{ + OrganizationId: orgId, + Metadata: []*v2beta_org.Metadata{ + { + Key: "key2", + Value: []byte("value2"), + }, + { + Key: "key3", + Value: []byte("value3"), + }, + }, + }) + require.NoError(t, err) + }, + orgId: orgId, + metadataToDelete: []struct{ key, value string }{ + { + key: "key2", + value: "value2", + }, + { + key: "key3", + value: "value3", + }, + }, + }, + { + name: "delete some org metadata but not all", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + setupFunc: func() { + _, err := Client.SetOrganizationMetadata(CTX, &v2beta_org.SetOrganizationMetadataRequest{ + OrganizationId: orgId, + Metadata: []*v2beta_org.Metadata{ + { + Key: "key4", + Value: []byte("value4"), + }, + // key5 should not be deleted + { + Key: "key5", + Value: []byte("value5"), + }, + { + Key: "key6", + Value: []byte("value6"), + }, + }, + }) + require.NoError(t, err) + }, + orgId: orgId, + metadataToDelete: []struct{ key, value string }{ + { + key: "key4", + value: "value4", + }, + { + key: "key6", + value: "value6", + }, + }, + metadataToRemain: []struct{ key, value string }{ + { + key: "key5", + value: "value5", + }, + }, + }, + { + name: "delete org metadata that does not exist", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + setupFunc: func() { + _, err := Client.SetOrganizationMetadata(CTX, &v2beta_org.SetOrganizationMetadataRequest{ + OrganizationId: orgId, + Metadata: []*v2beta_org.Metadata{ + { + Key: "key88", + Value: []byte("value74"), + }, + { + Key: "key5888", + Value: []byte("value8885"), + }, + }, + }) + require.NoError(t, err) + }, + orgId: orgId, + // TODO: this error message needs to be either removed or changed + err: errors.New("Metadata list is empty"), + }, + { + name: "delete org metadata for org that does not exist", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + setupFunc: func() { + _, err := Client.SetOrganizationMetadata(CTX, &v2beta_org.SetOrganizationMetadataRequest{ + OrganizationId: orgId, + Metadata: []*v2beta_org.Metadata{ + { + Key: "key88", + Value: []byte("value74"), + }, + { + Key: "key5888", + Value: []byte("value8885"), + }, + }, + }) + require.NoError(t, err) + }, + orgId: "non existant org id", + // TODO: this error message needs to be either removed or changed + err: errors.New("Metadata list is empty"), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.setupFunc != nil { + tt.setupFunc() + } + + // check metadata exists + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, 10*time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + listOrgMetadataRes, err := Client.ListOrganizationMetadata(tt.ctx, &v2beta_org.ListOrganizationMetadataRequest{ + OrganizationId: tt.orgId, + }) + require.NoError(ttt, err) + foundMetadataCount := 0 + for _, kv := range tt.metadataToDelete { + for _, res := range listOrgMetadataRes.Metadata { + if res.Key == kv.key && + string(res.Value) == kv.value { + foundMetadataCount += 1 + } + } + } + require.Equal(ttt, len(tt.metadataToDelete), foundMetadataCount) + }, retryDuration, tick, "timeout waiting for expected organizations being created") + + keys := make([]string, len(tt.metadataToDelete)) + for i, kvp := range tt.metadataToDelete { + keys[i] = kvp.key + } + + // run delete + _, err = Client.DeleteOrganizationMetadata(tt.ctx, &v2beta_org.DeleteOrganizationMetadataRequest{ + OrganizationId: tt.orgId, + Keys: keys, + }) + if tt.err != nil { + require.Contains(t, err.Error(), tt.err.Error()) + return + } + require.NoError(t, err) + + retryDuration, tick = integration.WaitForAndTickWithMaxDuration(CTX, 10*time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + // check metadata was definitely deleted + listOrgMetadataRes, err := Client.ListOrganizationMetadata(tt.ctx, &v2beta_org.ListOrganizationMetadataRequest{ + OrganizationId: tt.orgId, + }) + require.NoError(ttt, err) + foundMetadataCount := 0 + for _, kv := range tt.metadataToDelete { + for _, res := range listOrgMetadataRes.Metadata { + if res.Key == kv.key && + string(res.Value) == kv.value { + foundMetadataCount += 1 + } + } + } + require.Equal(ttt, foundMetadataCount, 0) + }, retryDuration, tick, "timeout waiting for expected organizations being created") + + // check metadata that should not be delted was not deleted + listOrgMetadataRes, err := Client.ListOrganizationMetadata(tt.ctx, &v2beta_org.ListOrganizationMetadataRequest{ + OrganizationId: tt.orgId, + }) + require.NoError(t, err) + foundMetadataCount := 0 + for _, kv := range tt.metadataToRemain { + for _, res := range listOrgMetadataRes.Metadata { + if res.Key == kv.key && + string(res.Value) == kv.value { + foundMetadataCount += 1 + } + } + } + require.Equal(t, len(tt.metadataToRemain), foundMetadataCount) + }) + } +} + +func createOrgs(ctx context.Context, client v2beta_org.OrganizationServiceClient, noOfOrgs int) ([]*v2beta_org.CreateOrganizationResponse, []string, error) { + var err error + orgs := make([]*v2beta_org.CreateOrganizationResponse, noOfOrgs) + orgsName := make([]string, noOfOrgs) + + for i := range noOfOrgs { + orgName := gofakeit.Name() + orgsName[i] = orgName + orgs[i], err = client.CreateOrganization(ctx, + &v2beta_org.CreateOrganizationRequest{ + Name: orgName, + }, + ) + if err != nil { + return nil, nil, err + } + } + + return orgs, orgsName, nil +} + +func assertCreatedAdmin(t *testing.T, expected, got *v2beta_org.CreatedAdmin) { if expected.GetUserId() != "" { assert.NotEmpty(t, got.GetUserId()) } else { diff --git a/internal/api/grpc/org/v2beta/org.go b/internal/api/grpc/org/v2beta/org.go index 39730f827e..66198757cb 100644 --- a/internal/api/grpc/org/v2beta/org.go +++ b/internal/api/grpc/org/v2beta/org.go @@ -2,16 +2,23 @@ package org import ( "context" + "errors" + "google.golang.org/protobuf/types/known/timestamppb" + + metadata "github.com/zitadel/zitadel/internal/api/grpc/metadata/v2beta" object "github.com/zitadel/zitadel/internal/api/grpc/object/v2beta" user "github.com/zitadel/zitadel/internal/api/grpc/user/v2beta" "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/query" "github.com/zitadel/zitadel/internal/zerrors" + filter "github.com/zitadel/zitadel/pkg/grpc/filter/v2beta" org "github.com/zitadel/zitadel/pkg/grpc/org/v2beta" + v2beta_org "github.com/zitadel/zitadel/pkg/grpc/org/v2beta" ) -func (s *Server) AddOrganization(ctx context.Context, request *org.AddOrganizationRequest) (*org.AddOrganizationResponse, error) { - orgSetup, err := addOrganizationRequestToCommand(request) +func (s *Server) CreateOrganization(ctx context.Context, request *v2beta_org.CreateOrganizationRequest) (*v2beta_org.CreateOrganizationResponse, error) { + orgSetup, err := createOrganizationRequestToCommand(request) if err != nil { return nil, err } @@ -22,8 +29,182 @@ func (s *Server) AddOrganization(ctx context.Context, request *org.AddOrganizati return createdOrganizationToPb(createdOrg) } -func addOrganizationRequestToCommand(request *org.AddOrganizationRequest) (*command.OrgSetup, error) { - admins, err := addOrganizationRequestAdminsToCommand(request.GetAdmins()) +func (s *Server) UpdateOrganization(ctx context.Context, request *v2beta_org.UpdateOrganizationRequest) (*v2beta_org.UpdateOrganizationResponse, error) { + org, err := s.command.ChangeOrg(ctx, request.Id, request.Name) + if err != nil { + return nil, err + } + + return &v2beta_org.UpdateOrganizationResponse{ + ChangeDate: timestamppb.New(org.EventDate), + }, nil +} + +func (s *Server) ListOrganizations(ctx context.Context, request *v2beta_org.ListOrganizationsRequest) (*v2beta_org.ListOrganizationsResponse, error) { + queries, err := listOrgRequestToModel(s.systemDefaults, request) + if err != nil { + return nil, err + } + orgs, err := s.query.SearchOrgs(ctx, queries, s.checkPermission) + if err != nil { + return nil, err + } + return &v2beta_org.ListOrganizationsResponse{ + Organizations: OrgViewsToPb(orgs.Orgs), + Pagination: &filter.PaginationResponse{ + TotalResult: orgs.Count, + AppliedLimit: uint64(request.GetPagination().GetLimit()), + }, + }, nil +} + +func (s *Server) DeleteOrganization(ctx context.Context, request *v2beta_org.DeleteOrganizationRequest) (*v2beta_org.DeleteOrganizationResponse, error) { + details, err := s.command.RemoveOrg(ctx, request.Id) + if err != nil { + var notFoundError *zerrors.NotFoundError + if errors.As(err, ¬FoundError) { + return &v2beta_org.DeleteOrganizationResponse{}, nil + } + return nil, err + } + return &v2beta_org.DeleteOrganizationResponse{ + DeletionDate: timestamppb.New(details.EventDate), + }, nil +} + +func (s *Server) SetOrganizationMetadata(ctx context.Context, request *v2beta_org.SetOrganizationMetadataRequest) (*v2beta_org.SetOrganizationMetadataResponse, error) { + result, err := s.command.BulkSetOrgMetadata(ctx, request.OrganizationId, BulkSetOrgMetadataToDomain(request)...) + if err != nil { + return nil, err + } + return &org.SetOrganizationMetadataResponse{ + SetDate: timestamppb.New(result.EventDate), + }, nil +} + +func (s *Server) ListOrganizationMetadata(ctx context.Context, request *v2beta_org.ListOrganizationMetadataRequest) (*v2beta_org.ListOrganizationMetadataResponse, error) { + metadataQueries, err := ListOrgMetadataToDomain(s.systemDefaults, request) + if err != nil { + return nil, err + } + res, err := s.query.SearchOrgMetadata(ctx, true, request.OrganizationId, metadataQueries, false) + if err != nil { + return nil, err + } + return &v2beta_org.ListOrganizationMetadataResponse{ + Metadata: metadata.OrgMetadataListToPb(res.Metadata), + Pagination: &filter.PaginationResponse{ + TotalResult: res.Count, + AppliedLimit: uint64(request.GetPagination().GetLimit()), + }, + }, nil +} + +func (s *Server) DeleteOrganizationMetadata(ctx context.Context, request *v2beta_org.DeleteOrganizationMetadataRequest) (*v2beta_org.DeleteOrganizationMetadataResponse, error) { + result, err := s.command.BulkRemoveOrgMetadata(ctx, request.OrganizationId, request.Keys...) + if err != nil { + return nil, err + } + return &v2beta_org.DeleteOrganizationMetadataResponse{ + DeletionDate: timestamppb.New(result.EventDate), + }, nil +} + +func (s *Server) DeactivateOrganization(ctx context.Context, request *org.DeactivateOrganizationRequest) (*org.DeactivateOrganizationResponse, error) { + objectDetails, err := s.command.DeactivateOrg(ctx, request.Id) + if err != nil { + return nil, err + } + return &org.DeactivateOrganizationResponse{ + ChangeDate: timestamppb.New(objectDetails.EventDate), + }, nil +} + +func (s *Server) ActivateOrganization(ctx context.Context, request *org.ActivateOrganizationRequest) (*org.ActivateOrganizationResponse, error) { + objectDetails, err := s.command.ReactivateOrg(ctx, request.Id) + if err != nil { + return nil, err + } + return &org.ActivateOrganizationResponse{ + ChangeDate: timestamppb.New(objectDetails.EventDate), + }, err +} + +func (s *Server) AddOrganizationDomain(ctx context.Context, request *org.AddOrganizationDomainRequest) (*org.AddOrganizationDomainResponse, error) { + userIDs, err := s.getClaimedUserIDsOfOrgDomain(ctx, request.Domain, request.OrganizationId) + if err != nil { + return nil, err + } + details, err := s.command.AddOrgDomain(ctx, request.OrganizationId, request.Domain, userIDs) + if err != nil { + return nil, err + } + return &org.AddOrganizationDomainResponse{ + CreationDate: timestamppb.New(details.EventDate), + }, nil +} + +func (s *Server) ListOrganizationDomains(ctx context.Context, req *org.ListOrganizationDomainsRequest) (*org.ListOrganizationDomainsResponse, error) { + queries, err := ListOrgDomainsRequestToModel(s.systemDefaults, req) + if err != nil { + return nil, err + } + orgIDQuery, err := query.NewOrgDomainOrgIDSearchQuery(req.OrganizationId) + if err != nil { + return nil, err + } + queries.Queries = append(queries.Queries, orgIDQuery) + + domains, err := s.query.SearchOrgDomains(ctx, queries, false) + if err != nil { + return nil, err + } + return &org.ListOrganizationDomainsResponse{ + Domains: object.DomainsToPb(domains.Domains), + Pagination: &filter.PaginationResponse{ + TotalResult: domains.Count, + AppliedLimit: uint64(req.GetPagination().GetLimit()), + }, + }, nil +} + +func (s *Server) DeleteOrganizationDomain(ctx context.Context, req *org.DeleteOrganizationDomainRequest) (*org.DeleteOrganizationDomainResponse, error) { + details, err := s.command.RemoveOrgDomain(ctx, RemoveOrgDomainRequestToDomain(ctx, req)) + if err != nil { + return nil, err + } + return &org.DeleteOrganizationDomainResponse{ + DeletionDate: timestamppb.New(details.EventDate), + }, err +} + +func (s *Server) GenerateOrganizationDomainValidation(ctx context.Context, req *org.GenerateOrganizationDomainValidationRequest) (*org.GenerateOrganizationDomainValidationResponse, error) { + token, url, err := s.command.GenerateOrgDomainValidation(ctx, GenerateOrgDomainValidationRequestToDomain(ctx, req)) + if err != nil { + return nil, err + } + return &org.GenerateOrganizationDomainValidationResponse{ + Token: token, + Url: url, + }, nil +} + +func (s *Server) VerifyOrganizationDomain(ctx context.Context, request *org.VerifyOrganizationDomainRequest) (*org.VerifyOrganizationDomainResponse, error) { + userIDs, err := s.getClaimedUserIDsOfOrgDomain(ctx, request.Domain, request.OrganizationId) + if err != nil { + return nil, err + } + details, err := s.command.ValidateOrgDomain(ctx, ValidateOrgDomainRequestToDomain(ctx, request), userIDs) + if err != nil { + return nil, err + } + return &org.VerifyOrganizationDomainResponse{ + ChangeDate: timestamppb.New(details.EventDate), + }, nil +} + +func createOrganizationRequestToCommand(request *v2beta_org.CreateOrganizationRequest) (*command.OrgSetup, error) { + admins, err := createOrganizationRequestAdminsToCommand(request.GetAdmins()) if err != nil { return nil, err } @@ -31,14 +212,14 @@ func addOrganizationRequestToCommand(request *org.AddOrganizationRequest) (*comm Name: request.GetName(), CustomDomain: "", Admins: admins, - OrgID: request.GetOrgId(), + OrgID: request.GetId(), }, nil } -func addOrganizationRequestAdminsToCommand(requestAdmins []*org.AddOrganizationRequest_Admin) (admins []*command.OrgSetupAdmin, err error) { +func createOrganizationRequestAdminsToCommand(requestAdmins []*v2beta_org.CreateOrganizationRequest_Admin) (admins []*command.OrgSetupAdmin, err error) { admins = make([]*command.OrgSetupAdmin, len(requestAdmins)) for i, admin := range requestAdmins { - admins[i], err = addOrganizationRequestAdminToCommand(admin) + admins[i], err = createOrganizationRequestAdminToCommand(admin) if err != nil { return nil, err } @@ -46,14 +227,14 @@ func addOrganizationRequestAdminsToCommand(requestAdmins []*org.AddOrganizationR return admins, nil } -func addOrganizationRequestAdminToCommand(admin *org.AddOrganizationRequest_Admin) (*command.OrgSetupAdmin, error) { +func createOrganizationRequestAdminToCommand(admin *v2beta_org.CreateOrganizationRequest_Admin) (*command.OrgSetupAdmin, error) { switch a := admin.GetUserType().(type) { - case *org.AddOrganizationRequest_Admin_UserId: + case *v2beta_org.CreateOrganizationRequest_Admin_UserId: return &command.OrgSetupAdmin{ ID: a.UserId, Roles: admin.GetRoles(), }, nil - case *org.AddOrganizationRequest_Admin_Human: + case *v2beta_org.CreateOrganizationRequest_Admin_Human: human, err := user.AddUserRequestToAddHuman(a.Human) if err != nil { return nil, err @@ -63,22 +244,31 @@ func addOrganizationRequestAdminToCommand(admin *org.AddOrganizationRequest_Admi Roles: admin.GetRoles(), }, nil default: - return nil, zerrors.ThrowUnimplementedf(nil, "ORGv2-SD2r1", "userType oneOf %T in method AddOrganization not implemented", a) + return nil, zerrors.ThrowUnimplementedf(nil, "ORGv2-SL2r8", "userType oneOf %T in method AddOrganization not implemented", a) } } -func createdOrganizationToPb(createdOrg *command.CreatedOrg) (_ *org.AddOrganizationResponse, err error) { - admins := make([]*org.AddOrganizationResponse_CreatedAdmin, len(createdOrg.CreatedAdmins)) - for i, admin := range createdOrg.CreatedAdmins { - admins[i] = &org.AddOrganizationResponse_CreatedAdmin{ - UserId: admin.ID, - EmailCode: admin.EmailCode, - PhoneCode: admin.PhoneCode, - } +func (s *Server) getClaimedUserIDsOfOrgDomain(ctx context.Context, orgDomain, orgID string) ([]string, error) { + queries := make([]query.SearchQuery, 0, 2) + loginName, err := query.NewUserPreferredLoginNameSearchQuery("@"+orgDomain, query.TextEndsWithIgnoreCase) + if err != nil { + return nil, err } - return &org.AddOrganizationResponse{ - Details: object.DomainToDetailsPb(createdOrg.ObjectDetails), - OrganizationId: createdOrg.ObjectDetails.ResourceOwner, - CreatedAdmins: admins, - }, nil + queries = append(queries, loginName) + if orgID != "" { + owner, err := query.NewUserResourceOwnerSearchQuery(orgID, query.TextNotEquals) + if err != nil { + return nil, err + } + queries = append(queries, owner) + } + users, err := s.query.SearchUsers(ctx, &query.UserSearchQueries{Queries: queries}, nil) + if err != nil { + return nil, err + } + userIDs := make([]string, len(users.Users)) + for i, user := range users.Users { + userIDs[i] = user.ID + } + return userIDs, nil } diff --git a/internal/api/grpc/org/v2beta/org_test.go b/internal/api/grpc/org/v2beta/org_test.go index 57ed05dfb2..2047f665a1 100644 --- a/internal/api/grpc/org/v2beta/org_test.go +++ b/internal/api/grpc/org/v2beta/org_test.go @@ -12,14 +12,13 @@ import ( "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/zerrors" - object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta" org "github.com/zitadel/zitadel/pkg/grpc/org/v2beta" user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta" ) -func Test_addOrganizationRequestToCommand(t *testing.T) { +func Test_createOrganizationRequestToCommand(t *testing.T) { type args struct { - request *org.AddOrganizationRequest + request *org.CreateOrganizationRequest } tests := []struct { name string @@ -30,21 +29,21 @@ func Test_addOrganizationRequestToCommand(t *testing.T) { { name: "nil user", args: args{ - request: &org.AddOrganizationRequest{ + request: &org.CreateOrganizationRequest{ Name: "name", - Admins: []*org.AddOrganizationRequest_Admin{ + Admins: []*org.CreateOrganizationRequest_Admin{ {}, }, }, }, - wantErr: zerrors.ThrowUnimplementedf(nil, "ORGv2-SD2r1", "userType oneOf %T in method AddOrganization not implemented", nil), + wantErr: zerrors.ThrowUnimplementedf(nil, "ORGv2-SL2r8", "userType oneOf %T in method AddOrganization not implemented", nil), }, { name: "custom org ID", args: args{ - request: &org.AddOrganizationRequest{ - Name: "custom org ID", - OrgId: gu.Ptr("org-ID"), + request: &org.CreateOrganizationRequest{ + Name: "custom org ID", + Id: gu.Ptr("org-ID"), }, }, want: &command.OrgSetup{ @@ -57,11 +56,11 @@ func Test_addOrganizationRequestToCommand(t *testing.T) { { name: "user ID", args: args{ - request: &org.AddOrganizationRequest{ + request: &org.CreateOrganizationRequest{ Name: "name", - Admins: []*org.AddOrganizationRequest_Admin{ + Admins: []*org.CreateOrganizationRequest_Admin{ { - UserType: &org.AddOrganizationRequest_Admin_UserId{ + UserType: &org.CreateOrganizationRequest_Admin_UserId{ UserId: "userID", }, Roles: nil, @@ -82,11 +81,11 @@ func Test_addOrganizationRequestToCommand(t *testing.T) { { name: "human user", args: args{ - request: &org.AddOrganizationRequest{ + request: &org.CreateOrganizationRequest{ Name: "name", - Admins: []*org.AddOrganizationRequest_Admin{ + Admins: []*org.CreateOrganizationRequest_Admin{ { - UserType: &org.AddOrganizationRequest_Admin_Human{ + UserType: &org.CreateOrganizationRequest_Admin_Human{ Human: &user.AddHumanUserRequest{ Profile: &user.SetHumanProfile{ GivenName: "firstname", @@ -124,7 +123,7 @@ func Test_addOrganizationRequestToCommand(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := addOrganizationRequestToCommand(tt.args.request) + got, err := createOrganizationRequestToCommand(tt.args.request) require.ErrorIs(t, err, tt.wantErr) assert.Equal(t, tt.want, got) }) @@ -139,7 +138,7 @@ func Test_createdOrganizationToPb(t *testing.T) { tests := []struct { name string args args - want *org.AddOrganizationResponse + want *org.CreateOrganizationResponse wantErr error }{ { @@ -160,14 +159,10 @@ func Test_createdOrganizationToPb(t *testing.T) { }, }, }, - want: &org.AddOrganizationResponse{ - Details: &object.Details{ - Sequence: 1, - ChangeDate: timestamppb.New(now), - ResourceOwner: "orgID", - }, - OrganizationId: "orgID", - CreatedAdmins: []*org.AddOrganizationResponse_CreatedAdmin{ + want: &org.CreateOrganizationResponse{ + CreationDate: timestamppb.New(now), + Id: "orgID", + CreatedAdmins: []*org.CreatedAdmin{ { UserId: "id", EmailCode: gu.Ptr("emailCode"), diff --git a/internal/api/grpc/org/v2beta/server.go b/internal/api/grpc/org/v2beta/server.go index 89dba81702..b7e8d4994f 100644 --- a/internal/api/grpc/org/v2beta/server.go +++ b/internal/api/grpc/org/v2beta/server.go @@ -6,6 +6,7 @@ import ( "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/grpc/server" "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/config/systemdefaults" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/query" org "github.com/zitadel/zitadel/pkg/grpc/org/v2beta" @@ -15,6 +16,7 @@ var _ org.OrganizationServiceServer = (*Server)(nil) type Server struct { org.UnimplementedOrganizationServiceServer + systemDefaults systemdefaults.SystemDefaults command *command.Commands query *query.Queries checkPermission domain.PermissionCheck @@ -23,11 +25,13 @@ type Server struct { type Config struct{} func CreateServer( + systemDefaults systemdefaults.SystemDefaults, command *command.Commands, query *query.Queries, checkPermission domain.PermissionCheck, ) *Server { return &Server{ + systemDefaults: systemDefaults, command: command, query: query, checkPermission: checkPermission, diff --git a/internal/query/org_metadata.go b/internal/query/org_metadata.go index 84b204de2b..fe61ad51d9 100644 --- a/internal/query/org_metadata.go +++ b/internal/query/org_metadata.go @@ -194,7 +194,6 @@ func prepareOrgMetadataQuery() (sq.SelectBuilder, func(*sql.Row) (*OrgMetadata, &m.Key, &m.Value, ) - if err != nil { if errors.Is(err, sql.ErrNoRows) { return nil, zerrors.ThrowNotFound(err, "QUERY-Rph32", "Errors.Metadata.NotFound") diff --git a/proto/zitadel/admin.proto b/proto/zitadel/admin.proto index 1e7f3b7407..d8c88d540b 100644 --- a/proto/zitadel/admin.proto +++ b/proto/zitadel/admin.proto @@ -307,7 +307,6 @@ service AdminService { }; } - // Deprecated: Use [ListCustomDomains](apis/resources/instance_service_v2/instance-service-list-custom-domains.api.mdx) instead to list custom domains rpc ListInstanceDomains(ListInstanceDomainsRequest) returns (ListInstanceDomainsResponse) { option (google.api.http) = { post: "/domains/_search"; @@ -320,12 +319,10 @@ service AdminService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Instance"; summary: "List Instance Domains"; - description: "Returns a list of domains that are configured for this ZITADEL instance. These domains are the URLs where ZITADEL is running."; - deprecated: true; + description: "Returns a list of domains that are configured for this ZITADEL instance. These domains are the URLs where ZITADEL is running." }; } - // Deprecated: Use [ListTrustedDomains](apis/resources/instance_service_v2/instance-service-list-trusted-domains.api.mdx) instead to list trusted domains rpc ListInstanceTrustedDomains(ListInstanceTrustedDomainsRequest) returns (ListInstanceTrustedDomainsResponse) { option (google.api.http) = { post: "/trusted_domains/_search"; @@ -338,12 +335,10 @@ service AdminService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Instance"; summary: "List Instance Trusted Domains"; - description: "Returns a list of domains that are configured for this ZITADEL instance. These domains are trusted to be used as public hosts."; - deprecated: true; + description: "Returns a list of domains that are configured for this ZITADEL instance. These domains are trusted to be used as public hosts." }; } - // Deprecated: Use [AddTrustedDomain](apis/resources/instance_service_v2/instance-service-add-trusted-domain.api.mdx) instead to add a trusted domain rpc AddInstanceTrustedDomain(AddInstanceTrustedDomainRequest) returns (AddInstanceTrustedDomainResponse) { option (google.api.http) = { post: "/trusted_domains"; @@ -357,12 +352,10 @@ service AdminService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Instance"; summary: "Add an Instance Trusted Domain"; - description: "Returns a list of domains that are configured for this ZITADEL instance. These domains are trusted to be used as public hosts."; - deprecated: true; + description: "Returns a list of domains that are configured for this ZITADEL instance. These domains are trusted to be used as public hosts." }; } - // Deprecated: Use [RemoveTrustedDomain](apis/resources/instance_service_v2/instance-service-remove-trusted-domain.api.mdx) instead to remove a trusted domain rpc RemoveInstanceTrustedDomain(RemoveInstanceTrustedDomainRequest) returns (RemoveInstanceTrustedDomainResponse) { option (google.api.http) = { delete: "/trusted_domains/{domain}"; @@ -375,8 +368,7 @@ service AdminService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Instance"; summary: "Remove an Instance Trusted Domain"; - description: "Returns a list of domains that are configured for this ZITADEL instance. These domains are trusted to be used as public hosts."; - deprecated: true; + description: "Returns a list of domains that are configured for this ZITADEL instance. These domains are trusted to be used as public hosts." }; } @@ -1245,6 +1237,7 @@ service AdminService { }; } + // Deprecated: use ListOrganization [apis/resources/org_service_v2beta/organization-service-list-organizations.api.mdx] API instead rpc ListOrgs(ListOrgsRequest) returns (ListOrgsResponse) { option (google.api.http) = { post: "/orgs/_search"; @@ -1264,7 +1257,8 @@ service AdminService { value: { description: "list of organizations matching the query"; }; - }; + } + deprecated: true; responses: { key: "400"; value: { @@ -1279,6 +1273,7 @@ service AdminService { }; } + // Deprecated: use CreateOrganization [apis/resources/org_service_v2beta/organization-service-create-organization.api.mdx] API instead rpc SetUpOrg(SetUpOrgRequest) returns (SetUpOrgResponse) { option (google.api.http) = { post: "/orgs/_setup"; @@ -1298,7 +1293,8 @@ service AdminService { value: { description: "org, user and user membership were created successfully"; }; - }; + } + deprecated: true; responses: { key: "400"; value: { @@ -1313,6 +1309,7 @@ service AdminService { }; } + // Deprecated: use DeleteOrganization [apis/resources/org_service_v2beta/organization-service-delete-organization.api.mdx] API instead rpc RemoveOrg(RemoveOrgRequest) returns (RemoveOrgResponse) { option (google.api.http) = { delete: "/orgs/{org_id}" @@ -1330,7 +1327,8 @@ service AdminService { value: { description: "org removed successfully"; }; - }; + } + deprecated: true; responses: { key: "400"; value: { diff --git a/proto/zitadel/management.proto b/proto/zitadel/management.proto index 3018ebe600..34a8384d39 100644 --- a/proto/zitadel/management.proto +++ b/proto/zitadel/management.proto @@ -2119,6 +2119,7 @@ service ManagementService { }; } + // Deprecated: use CreateOrganization [apis/resources/org_service_v2beta/organization-service-create-organization.api.mdx] API instead rpc AddOrg(AddOrgRequest) returns (AddOrgResponse) { option (google.api.http) = { post: "/orgs" @@ -2133,6 +2134,7 @@ service ManagementService { tags: "Organizations"; summary: "Create Organization"; description: "Create a new organization. Based on the given name a domain will be generated to be able to identify users within an organization." + deprecated: true parameters: { headers: { name: "x-zitadel-orgid"; @@ -2144,6 +2146,7 @@ service ManagementService { }; } + // Deprecated: use UpdateOrganization [apis/resources/org_service_v2beta/organization-service-update-organization.api.mdx] API instead rpc UpdateOrg(UpdateOrgRequest) returns (UpdateOrgResponse) { option (google.api.http) = { put: "/orgs/me" @@ -2158,6 +2161,7 @@ service ManagementService { tags: "Organizations"; summary: "Update Organization"; description: "Change the name of the organization." + deprecated: true parameters: { headers: { name: "x-zitadel-orgid"; @@ -2169,6 +2173,7 @@ service ManagementService { }; } + // Deprecated: use DeactivateOrganization [apis/resources/org_service_v2beta/organization-service-deactivate-organization.api.mdx] API instead rpc DeactivateOrg(DeactivateOrgRequest) returns (DeactivateOrgResponse) { option (google.api.http) = { post: "/orgs/me/_deactivate" @@ -2183,6 +2188,7 @@ service ManagementService { tags: "Organizations"; summary: "Deactivate Organization"; description: "Sets the state of my organization to deactivated. Users of this organization will not be able to log in." + deprecated: true parameters: { headers: { name: "x-zitadel-orgid"; @@ -2194,6 +2200,7 @@ service ManagementService { }; } + // Deprecated: use ActivateOrganization [apis/resources/org_service_v2beta/organization-service-activate-organization.api.mdx] API instead rpc ReactivateOrg(ReactivateOrgRequest) returns (ReactivateOrgResponse) { option (google.api.http) = { post: "/orgs/me/_reactivate" @@ -2208,6 +2215,7 @@ service ManagementService { tags: "Organizations"; summary: "Reactivate Organization"; description: "Set the state of my organization to active. The state of the organization has to be deactivated to perform the request. Users of this organization will be able to log in again." + deprecated: true parameters: { headers: { name: "x-zitadel-orgid"; @@ -2219,6 +2227,7 @@ service ManagementService { }; } + // Deprecated: use DeleteOrganization [apis/resources/org_service_v2beta/organization-service-delete-organization.api.mdx] API instead rpc RemoveOrg(RemoveOrgRequest) returns (RemoveOrgResponse) { option (google.api.http) = { delete: "/orgs/me" @@ -2232,6 +2241,7 @@ service ManagementService { tags: "Organizations"; summary: "Delete Organization"; description: "Deletes my organization and all its resources (Users, Projects, Grants to and from the org). Users of this organization will not be able to log in." + deprecated: true parameters: { headers: { name: "x-zitadel-orgid"; @@ -2243,6 +2253,7 @@ service ManagementService { }; } + // Deprecated: use SetOrganizationMetadata [apis/resources/org_service_v2beta/organization-service-set-organization-metadata.api.mdx] API instead rpc SetOrgMetadata(SetOrgMetadataRequest) returns (SetOrgMetadataResponse) { option (google.api.http) = { post: "/metadata/{key}" @@ -2258,6 +2269,7 @@ service ManagementService { tags: "Organization Metadata"; summary: "Set Organization Metadata"; description: "This endpoint either adds or updates a metadata value for the requested key. Make sure the value is base64 encoded." + deprecated: true parameters: { headers: { name: "x-zitadel-orgid"; @@ -2269,6 +2281,7 @@ service ManagementService { }; } + // Deprecated: use SetOrganizationMetadata [apis/resources/org_service_v2beta/organization-service-set-organization-metadata.api.mdx] API instead rpc BulkSetOrgMetadata(BulkSetOrgMetadataRequest) returns (BulkSetOrgMetadataResponse) { option (google.api.http) = { post: "/metadata/_bulk" @@ -2284,6 +2297,7 @@ service ManagementService { tags: "Organization Metadata"; summary: "Bulk Set Organization Metadata"; description: "This endpoint sets a list of metadata to the organization. Make sure the values are base64 encoded." + deprecated: true parameters: { headers: { name: "x-zitadel-orgid"; @@ -2295,6 +2309,7 @@ service ManagementService { }; } + // Deprecated: use ListOrganizationMetadata [apis/resources/org_service_v2beta/organization-service-list-organization-metadata.api.mdx] API instead rpc ListOrgMetadata(ListOrgMetadataRequest) returns (ListOrgMetadataResponse) { option (google.api.http) = { post: "/metadata/_search" @@ -2310,6 +2325,7 @@ service ManagementService { tags: "Organization Metadata"; summary: "Search Organization Metadata"; description: "Get the metadata of an organization filtered by your query." + deprecated: true parameters: { headers: { name: "x-zitadel-orgid"; @@ -2321,6 +2337,7 @@ service ManagementService { }; } + // Deprecated: use ListOrganizationMetadata [apis/resources/org_service_v2beta/organization-service-list-organization-metadata.api.mdx] API instead rpc GetOrgMetadata(GetOrgMetadataRequest) returns (GetOrgMetadataResponse) { option (google.api.http) = { get: "/metadata/{key}" @@ -2335,6 +2352,7 @@ service ManagementService { tags: "Organization Metadata"; summary: "Get Organization Metadata By Key"; description: "Get a metadata object from an organization by a specific key." + deprecated: true parameters: { headers: { name: "x-zitadel-orgid"; @@ -2346,6 +2364,7 @@ service ManagementService { }; } + // Deprecated: use DeleteOrganizationMetadata [apis/resources/org_service_v2beta/organization-service-delete-organization-metadata.api.mdx] API instead rpc RemoveOrgMetadata(RemoveOrgMetadataRequest) returns (RemoveOrgMetadataResponse) { option (google.api.http) = { delete: "/metadata/{key}" @@ -2360,6 +2379,7 @@ service ManagementService { tags: "Organization Metadata"; summary: "Delete Organization Metadata By Key"; description: "Remove a metadata object from an organization with a specific key." + deprecated: true parameters: { headers: { name: "x-zitadel-orgid"; @@ -2371,6 +2391,7 @@ service ManagementService { }; } + // Deprecated: use DeleteOrganizationMetadata [apis/resources/org_service_v2beta/organization-service-delete-organization-metadata.api.mdx] API instead rpc BulkRemoveOrgMetadata(BulkRemoveOrgMetadataRequest) returns (BulkRemoveOrgMetadataResponse) { option (google.api.http) = { delete: "/metadata/_bulk" @@ -2384,6 +2405,7 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Organizations"; tags: "Organization Metadata"; + deprecated: true summary: "Bulk Delete Metadata"; description: "Remove a list of metadata objects from an organization with a list of keys." parameters: { @@ -2397,31 +2419,7 @@ service ManagementService { }; } - rpc ListOrgDomains(ListOrgDomainsRequest) returns (ListOrgDomainsResponse) { - option (google.api.http) = { - post: "/orgs/me/domains/_search" - body: "*" - }; - - option (zitadel.v1.auth_option) = { - permission: "org.read" - }; - - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - tags: "Organizations"; - summary: "Search Domains"; - description: "Returns the list of registered domains of an organization. The domains are used to identify to which organization a user belongs." - parameters: { - headers: { - name: "x-zitadel-orgid"; - description: "The default is always the organization of the requesting user. If you like to get/set a result of another organization include the header. Make sure the user has permission to access the requested data."; - type: STRING, - required: false; - }; - }; - }; - } - + // Deprecated: use AddOrganizationDomain [apis/resources/org_service_v2beta/organization-service-add-organization-domain.api.mdx] API instead rpc AddOrgDomain(AddOrgDomainRequest) returns (AddOrgDomainResponse) { option (google.api.http) = { post: "/orgs/me/domains" @@ -2436,6 +2434,7 @@ service ManagementService { tags: "Organizations"; summary: "Add Domain"; description: "Add a new domain to an organization. The domains are used to identify to which organization a user belongs." + deprecated: true parameters: { headers: { name: "x-zitadel-orgid"; @@ -2447,6 +2446,34 @@ service ManagementService { }; } + // Deprecated: use ListOrganizationDomains [apis/resources/org_service_v2beta/organization-service-list-organization-domains.api.mdx] API instead + rpc ListOrgDomains(ListOrgDomainsRequest) returns (ListOrgDomainsResponse) { + option (google.api.http) = { + post: "/orgs/me/domains/_search" + body: "*" + }; + + option (zitadel.v1.auth_option) = { + permission: "org.read" + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + tags: "Organizations"; + summary: "Search Domains"; + description: "Returns the list of registered domains of an organization. The domains are used to identify to which organization a user belongs." + deprecated: true + parameters: { + headers: { + name: "x-zitadel-orgid"; + description: "The default is always the organization of the requesting user. If you like to get/set a result of another organization include the header. Make sure the user has permission to access the requested data."; + type: STRING, + required: false; + }; + }; + }; + } + + // Deprecated: use DeleteOrganizationDomain [apis/resources/org_service_v2beta/organization-service-delete-organization-domain.api.mdx] API instead rpc RemoveOrgDomain(RemoveOrgDomainRequest) returns (RemoveOrgDomainResponse) { option (google.api.http) = { delete: "/orgs/me/domains/{domain}" @@ -2460,6 +2487,7 @@ service ManagementService { tags: "Organizations"; summary: "Remove Domain"; description: "Delete a new domain from an organization. The domains are used to identify to which organization a user belongs. If the uses use the domain for login, this will not be possible afterwards. They have to use another domain instead." + deprecated: true parameters: { headers: { name: "x-zitadel-orgid"; @@ -2471,6 +2499,7 @@ service ManagementService { }; } + // Deprecated: use GenerateOrganizationDomainValidation [apis/resources/org_service_v2beta/organization-service-generate-organization-domain-validation.api.mdx] API instead rpc GenerateOrgDomainValidation(GenerateOrgDomainValidationRequest) returns (GenerateOrgDomainValidationResponse) { option (google.api.http) = { post: "/orgs/me/domains/{domain}/validation/_generate" @@ -2485,6 +2514,7 @@ service ManagementService { tags: "Organizations"; summary: "Generate Domain Verification"; description: "Generate a new file to be able to verify your domain with DNS or HTTP challenge." + deprecated: true parameters: { headers: { name: "x-zitadel-orgid"; @@ -2496,6 +2526,7 @@ service ManagementService { }; } + // Deprecated: use VerifyOrganizationDomain [apis/resources/org_service_v2beta/organization-service-verify-organization-domain.api.mdx] API instead rpc ValidateOrgDomain(ValidateOrgDomainRequest) returns (ValidateOrgDomainResponse) { option (google.api.http) = { post: "/orgs/me/domains/{domain}/validation/_validate" @@ -2510,6 +2541,7 @@ service ManagementService { tags: "Organizations"; summary: "Verify Domain"; description: "Make sure you have added the required verification to your domain, depending on the method you have chosen (HTTP or DNS challenge). ZITADEL will check it and set the domain as verified if it was successful. A verify domain has to be unique." + deprecated: true parameters: { headers: { name: "x-zitadel-orgid"; @@ -2678,11 +2710,6 @@ service ManagementService { }; } - // Get Project By ID - // - // Deprecated: [Get Project](apis/resources/project_service_v2/project-service-get-project.api.mdx) to get project by ID. - // - // Returns a project owned by the organization (no granted projects). A Project is a vessel for different applications sharing the same role context. rpc GetProjectByID(GetProjectByIDRequest) returns (GetProjectByIDResponse) { option (google.api.http) = { get: "/projects/{id}" @@ -2695,7 +2722,8 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Projects"; - deprecated: true; + summary: "Get Project By ID"; + description: "Returns a project owned by the organization (no granted projects). A Project is a vessel for different applications sharing the same role context." parameters: { headers: { name: "x-zitadel-orgid"; @@ -2707,11 +2735,6 @@ service ManagementService { }; } - // Get Granted Project By ID - // - // Deprecated: [List Projects](apis/resources/project_service_v2/project-service-list-projects.api.mdx) to get granted projects. - // - // Returns a project owned by another organization and granted to my organization. A Project is a vessel for different applications sharing the same role context. rpc GetGrantedProjectByID(GetGrantedProjectByIDRequest) returns (GetGrantedProjectByIDResponse) { option (google.api.http) = { get: "/granted_projects/{project_id}/grants/{grant_id}" @@ -2724,7 +2747,8 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Projects"; - deprecated: true; + summary: "Get Granted Project By ID"; + description: "Returns a project owned by another organization and granted to my organization. A Project is a vessel for different applications sharing the same role context." parameters: { headers: { name: "x-zitadel-orgid"; @@ -2736,11 +2760,6 @@ service ManagementService { }; } - // List Projects - // - // Deprecated: [List Projects](apis/resources/project_service_v2/project-service-list-projects.api.mdx) to list all projects and granted projects. - // - // Lists projects my organization is the owner of (no granted projects). A Project is a vessel for different applications sharing the same role context. rpc ListProjects(ListProjectsRequest) returns (ListProjectsResponse) { option (google.api.http) = { post: "/projects/_search" @@ -2753,7 +2772,8 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Projects"; - deprecated: true; + summary: "Search Project"; + description: "Lists projects my organization is the owner of (no granted projects). A Project is a vessel for different applications sharing the same role context." parameters: { headers: { name: "x-zitadel-orgid"; @@ -2765,11 +2785,6 @@ service ManagementService { }; } - // List Granted Projects - // - // Deprecated: [List Projects](apis/resources/project_service_v2/project-service-list-projects.api.mdx) to list all projects and granted projects. - // - // Lists projects my organization got granted from another organization. A Project is a vessel for different applications sharing the same role context. rpc ListGrantedProjects(ListGrantedProjectsRequest) returns (ListGrantedProjectsResponse) { option (google.api.http) = { post: "/granted_projects/_search" @@ -2782,7 +2797,8 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Projects"; - deprecated: true; + summary: "Search Granted Project"; + description: "Lists projects my organization got granted from another organization. A Project is a vessel for different applications sharing the same role context." parameters: { headers: { name: "x-zitadel-orgid"; @@ -2844,11 +2860,6 @@ service ManagementService { }; } - // Create Project - // - // Deprecated: [Create Project](apis/resources/project_service_v2/project-service-create-project.api.mdx) to create a project. - // - // Create a new project. A Project is a vessel for different applications sharing the same role context. rpc AddProject(AddProjectRequest) returns (AddProjectResponse) { option (google.api.http) = { post: "/projects" @@ -2861,7 +2872,8 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Projects"; - deprecated: true; + summary: "Create Project"; + description: "Create a new project. A Project is a vessel for different applications sharing the same role context." parameters: { headers: { name: "x-zitadel-orgid"; @@ -2873,11 +2885,6 @@ service ManagementService { }; } - // Update Project - // - // Deprecated: [Update Project](apis/resources/project_service_v2/project-service-update-project.api.mdx) to update a project. - // - // Update a project and its settings. A Project is a vessel for different applications sharing the same role context. rpc UpdateProject(UpdateProjectRequest) returns (UpdateProjectResponse) { option (google.api.http) = { put: "/projects/{id}" @@ -2891,7 +2898,8 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Projects"; - deprecated: true; + summary: "Update Project"; + description: "Update a project and its settings. A Project is a vessel for different applications sharing the same role context." parameters: { headers: { name: "x-zitadel-orgid"; @@ -2903,11 +2911,6 @@ service ManagementService { }; } - // Deactivate Project - // - // Deprecated: [Deactivate Project](apis/resources/project_service_v2/project-service-deactivate-project.api.mdx) to deactivate a project. - // - // Set the state of a project to deactivated. Request returns an error if the project is already deactivated. rpc DeactivateProject(DeactivateProjectRequest) returns (DeactivateProjectResponse) { option (google.api.http) = { post: "/projects/{id}/_deactivate" @@ -2921,7 +2924,8 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Projects"; - deprecated: true; + summary: "Deactivate Project"; + description: "Set the state of a project to deactivated. Request returns an error if the project is already deactivated." parameters: { headers: { name: "x-zitadel-orgid"; @@ -2933,11 +2937,6 @@ service ManagementService { }; } - // Activate Project - // - // Deprecated: [Activate Project](apis/resources/project_service_v2/project-service-activate-project.api.mdx) to activate a project. - // - // Set the state of a project to active. Request returns an error if the project is not deactivated. rpc ReactivateProject(ReactivateProjectRequest) returns (ReactivateProjectResponse) { option (google.api.http) = { post: "/projects/{id}/_reactivate" @@ -2951,7 +2950,8 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Projects"; - deprecated: true; + summary: "Reactivate Project"; + description: "Set the state of a project to active. Request returns an error if the project is not deactivated." parameters: { headers: { name: "x-zitadel-orgid"; @@ -2963,11 +2963,6 @@ service ManagementService { }; } - // Remove Project - // - // Deprecated: [Delete Project](apis/resources/project_service_v2/project-service-delete-project.api.mdx) to remove a project. - // - // Project and all its sub-resources like project grants, applications, roles and user grants will be removed. rpc RemoveProject(RemoveProjectRequest) returns (RemoveProjectResponse) { option (google.api.http) = { delete: "/projects/{id}" @@ -2980,7 +2975,8 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Projects"; - deprecated: true; + summary: "Remove Project"; + description: "Project and all its sub-resources like project grants, applications, roles and user grants will be removed." parameters: { headers: { name: "x-zitadel-orgid"; @@ -2992,11 +2988,6 @@ service ManagementService { }; } - // Search Project Roles - // - // Deprecated: [List Project Roles](apis/resources/project_service_v2/project-service-list-project-roles.api.mdx) to get project roles. - // - // Returns all roles of a project matching the search query. rpc ListProjectRoles(ListProjectRolesRequest) returns (ListProjectRolesResponse) { option (google.api.http) = { post: "/projects/{project_id}/roles/_search" @@ -3010,7 +3001,8 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Project Roles"; - deprecated: true; + summary: "Search Project Roles"; + description: "Returns all roles of a project matching the search query." parameters: { headers: { name: "x-zitadel-orgid"; @@ -3022,11 +3014,6 @@ service ManagementService { }; } - // Add Project Role - // - // Deprecated: [Add Project Role](apis/resources/project_service_v2/project-service-add-project-role.api.mdx) to add a project role. - // - // Add a new project role to a project. The key must be unique within the project.\n\nDeprecated: please use user service v2 AddProjectRole. rpc AddProjectRole(AddProjectRoleRequest) returns (AddProjectRoleResponse) { option (google.api.http) = { post: "/projects/{project_id}/roles" @@ -3040,7 +3027,8 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Project Roles"; - deprecated: true; + summary: "Add Project Role"; + description: "Add a new project role to a project. The key must be unique within the project." parameters: { headers: { name: "x-zitadel-orgid"; @@ -3052,11 +3040,6 @@ service ManagementService { }; } - // Bulk add Project Role - // - // Deprecated: [Add Project Role](apis/resources/project_service_v2/project-service-add-project-role.api.mdx) to add a project role. - // - // Add a list of roles to a project. The keys must be unique within the project. rpc BulkAddProjectRoles(BulkAddProjectRolesRequest) returns (BulkAddProjectRolesResponse) { option (google.api.http) = { post: "/projects/{project_id}/roles/_bulk" @@ -3070,7 +3053,8 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Project Roles"; - deprecated: true; + summary: "Bulk Add Project Role"; + description: "Add a list of roles to a project. The keys must be unique within the project." parameters: { headers: { name: "x-zitadel-orgid"; @@ -3082,11 +3066,6 @@ service ManagementService { }; } - // Update Project Role - // - // Deprecated: [Update Project Role](apis/resources/project_service_v2/project-service-update-project-role.api.mdx) to update a project role. - // - // Change a project role. The key is not editable. If a key should change, remove the role and create a new one. rpc UpdateProjectRole(UpdateProjectRoleRequest) returns (UpdateProjectRoleResponse) { option (google.api.http) = { put: "/projects/{project_id}/roles/{role_key}" @@ -3100,7 +3079,8 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Project Roles"; - deprecated: true; + summary: "Change Project Role"; + description: "Change a project role. The key is not editable. If a key should change, remove the role and create a new one." parameters: { headers: { name: "x-zitadel-orgid"; @@ -3112,11 +3092,6 @@ service ManagementService { }; } - // Remove Project Role - // - // Deprecated: [Delete Project Role](apis/resources/project_service_v2/project-service-update-project-role.api.mdx) to remove a project role. - // - // Removes the role from the project and on every resource it has a dependency. This includes project grants and user grants. rpc RemoveProjectRole(RemoveProjectRoleRequest) returns (RemoveProjectRoleResponse) { option (google.api.http) = { delete: "/projects/{project_id}/roles/{role_key}" @@ -3129,7 +3104,8 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Project Roles"; - deprecated: true; + summary: "Remove Project Role"; + description: "Removes the role from the project and on every resource it has a dependency. This includes project grants and user grants." parameters: { headers: { name: "x-zitadel-orgid"; @@ -3793,11 +3769,6 @@ service ManagementService { }; } - // Get Project Grant By ID - // - // Deprecated: [List Project Grants](apis/resources/project_service_v2/project-service-list-project-grants.api.mdx) to get a project grant. - // - // Returns a project grant. A project grant is when the organization grants its project to another organization. rpc GetProjectGrantByID(GetProjectGrantByIDRequest) returns (GetProjectGrantByIDResponse) { option (google.api.http) = { get: "/projects/{project_id}/grants/{grant_id}" @@ -3809,7 +3780,8 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Projects"; - deprecated: true; + summary: "Project Grant By ID"; + description: "Returns a project grant. A project grant is when the organization grants its project to another organization." parameters: { headers: { name: "x-zitadel-orgid"; @@ -3821,11 +3793,6 @@ service ManagementService { }; } - // List Project Grants - // - // Deprecated: [List Project Grants](apis/resources/project_service_v2/project-service-list-project-grants.api.mdx) to list project grants. - // - // Returns a list of project grants for a specific project. A project grant is when the organization grants its project to another organization. rpc ListProjectGrants(ListProjectGrantsRequest) returns (ListProjectGrantsResponse) { option (google.api.http) = { post: "/projects/{project_id}/grants/_search" @@ -3839,7 +3806,8 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Project Grants"; - deprecated: true; + summary: "Search Project Grants from Project"; + description: "Returns a list of project grants for a specific project. A project grant is when the organization grants its project to another organization." parameters: { headers: { name: "x-zitadel-orgid"; @@ -3851,11 +3819,6 @@ service ManagementService { }; } - // Search Project Grants - // - // Deprecated: [List Project Grants](apis/resources/project_service_v2/project-service-list-project-grants.api.mdx) to list project grants. - // - // Returns a list of project grants. A project grant is when the organization grants its project to another organization. rpc ListAllProjectGrants(ListAllProjectGrantsRequest) returns (ListAllProjectGrantsResponse) { option (google.api.http) = { post: "/projectgrants/_search" @@ -3868,7 +3831,8 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Project Grants"; - deprecated: true; + summary: "Search Project Grants"; + description: "Returns a list of project grants. A project grant is when the organization grants its project to another organization." parameters: { headers: { name: "x-zitadel-orgid"; @@ -3880,11 +3844,6 @@ service ManagementService { }; } - // Add Project Grant - // - // Deprecated: [Create Project Grant](apis/resources/project_service_v2/project-service-create-project-grant.api.mdx) to add a project grant. - // - // Grant a project to another organization. The project grant will allow the granted organization to access the project and manage the authorizations for its users. Project Grant will be listed in the granted project of the granted organization. rpc AddProjectGrant(AddProjectGrantRequest) returns (AddProjectGrantResponse) { option (google.api.http) = { post: "/projects/{project_id}/grants" @@ -3897,7 +3856,8 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Project Grants"; - deprecated: true; + summary: "Add Project Grant"; + description: "Grant a project to another organization. The project grant will allow the granted organization to access the project and manage the authorizations for its users. Project Grant will be listed in the granted project of the granted organization" parameters: { headers: { name: "x-zitadel-orgid"; @@ -3909,11 +3869,6 @@ service ManagementService { }; } - // Update Project Grant - // - // Deprecated: [Update Project Grant](apis/resources/project_service_v2/project-service-update-project-grant.api.mdx) to update a project grant. - // - // Change the roles of the project that is granted to another organization. The project grant will allow the granted organization to access the project and manage the authorizations for its users. Project Grant will be listed in the granted project of the granted organization. rpc UpdateProjectGrant(UpdateProjectGrantRequest) returns (UpdateProjectGrantResponse) { option (google.api.http) = { put: "/projects/{project_id}/grants/{grant_id}" @@ -3926,7 +3881,8 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Project Grants"; - deprecated: true; + summary: "Change Project Grant"; + description: "Change the roles of the project that is granted to another organization. The project grant will allow the granted organization to access the project and manage the authorizations for its users. Project Grant will be listed in the granted project of the granted organization" parameters: { headers: { name: "x-zitadel-orgid"; @@ -3938,11 +3894,6 @@ service ManagementService { }; } - // Deactivate Project Grant - // - // Deprecated: [Deactivate Project Grant](apis/resources/project_service_v2/project-service-deactivate-project-grant.api.mdx) to deactivate a project grant. - // - // Set the state of the project grant to deactivated. The grant has to be active to be able to deactivate. rpc DeactivateProjectGrant(DeactivateProjectGrantRequest) returns (DeactivateProjectGrantResponse) { option (google.api.http) = { post: "/projects/{project_id}/grants/{grant_id}/_deactivate" @@ -3955,7 +3906,8 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Project Grants"; - deprecated: true; + summary: "Deactivate Project Grant"; + description: "Set the state of the project grant to deactivated. The grant has to be active to be able to deactivate." parameters: { headers: { name: "x-zitadel-orgid"; @@ -3967,11 +3919,6 @@ service ManagementService { }; } - // Reactivate Project Grant - // - // Deprecated: [Activate Project Grant](apis/resources/project_service_v2/project-service-activate-project-grant.api.mdx) to activate a project grant. - // - // Set the state of the project grant to active. The grant has to be deactivated to be able to reactivate. rpc ReactivateProjectGrant(ReactivateProjectGrantRequest) returns (ReactivateProjectGrantResponse) { option (google.api.http) = { post: "/projects/{project_id}/grants/{grant_id}/_reactivate" @@ -3984,7 +3931,8 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Project Grants"; - deprecated: true; + summary: "Reactivate Project Grant"; + description: "Set the state of the project grant to active. The grant has to be deactivated to be able to reactivate." parameters: { headers: { name: "x-zitadel-orgid"; @@ -3996,11 +3944,6 @@ service ManagementService { }; } - // Remove Project Grant - // - // Deprecated: [Delete Project Grant](apis/resources/project_service_v2/project-service-delete-project-grant.api.mdx) to remove a project grant. - // - // Remove a project grant. All user grants for this project grant will also be removed. A user will not have access to the project afterward (if permissions are checked). rpc RemoveProjectGrant(RemoveProjectGrantRequest) returns (RemoveProjectGrantResponse) { option (google.api.http) = { delete: "/projects/{project_id}/grants/{grant_id}" @@ -4012,7 +3955,8 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Project Grants"; - deprecated: true; + summary: "Remove Project Grant"; + description: "Remove a project grant. All user grants for this project grant will also be removed. A user will not have access to the project afterward (if permissions are checked)." parameters: { headers: { name: "x-zitadel-orgid"; diff --git a/proto/zitadel/metadata/v2beta/metadata.proto b/proto/zitadel/metadata/v2beta/metadata.proto new file mode 100644 index 0000000000..87fcc51869 --- /dev/null +++ b/proto/zitadel/metadata/v2beta/metadata.proto @@ -0,0 +1,57 @@ +syntax = "proto3"; + +import "zitadel/object/v2beta/object.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; +import "validate/validate.proto"; +import "google/protobuf/timestamp.proto"; + +package zitadel.metadata.v2beta; + +option go_package ="github.com/zitadel/zitadel/pkg/grpc/metadata/v2beta"; + +message Metadata { + google.protobuf.Timestamp creation_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; + google.protobuf.Timestamp change_date = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; + string key = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "metadata key", + example: "\"key1\""; + } + ]; + bytes value = 4 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "metadata value is base64 encoded, make sure to decode to get the value", + example: "\"VGhpcyBpcyBteSBmaXJzdCB2YWx1ZQ==\""; + } + ]; +} + +message MetadataQuery { + oneof query { + option (validate.required) = true; + MetadataKeyQuery key_query = 1; + } +} + +message MetadataKeyQuery { + string key = 1 [ + (validate.rules).string = {max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"key\"" + } + ]; + zitadel.object.v2beta.TextQueryMethod method = 2 [ + (validate.rules).enum.defined_only = true, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "defines which text equality method is used"; + } + ]; +} diff --git a/proto/zitadel/org/v2beta/org.proto b/proto/zitadel/org/v2beta/org.proto new file mode 100644 index 0000000000..08cf47e820 --- /dev/null +++ b/proto/zitadel/org/v2beta/org.proto @@ -0,0 +1,169 @@ +syntax = "proto3"; + +package zitadel.org.v2beta; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/org/v2beta;org"; + +import "protoc-gen-openapiv2/options/annotations.proto"; +import "validate/validate.proto"; +import "zitadel/object/v2beta/object.proto"; +import "google/protobuf/timestamp.proto"; + +message Organization { + // Unique identifier of the organization. + string id = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629023906488334\"" + } + ]; + + // The timestamp of the organization was created. + google.protobuf.Timestamp creation_date = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; + + // The timestamp of the verification of the organization domain. + google.protobuf.Timestamp changed_date = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; + + // Current state of the organization, for example active, inactive and deleted. + OrgState state = 4; + + // Name of the organization. + string name = 5 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"ZITADEL\""; + } + ]; + // Primary domain used in the organization. + string primary_domain = 6 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"zitadel.cloud\""; + } + ]; +} + +enum OrgState { + ORG_STATE_UNSPECIFIED = 0; + ORG_STATE_ACTIVE = 1; + ORG_STATE_INACTIVE = 2; + ORG_STATE_REMOVED = 3; +} + +enum OrgFieldName { + ORG_FIELD_NAME_UNSPECIFIED = 0; + ORG_FIELD_NAME_NAME = 1; + ORG_FIELD_NAME_CREATION_DATE = 2; +} + +message OrganizationSearchFilter{ + oneof filter { + option (validate.required) = true; + + OrgNameFilter name_filter = 1; + OrgDomainFilter domain_filter = 2; + OrgStateFilter state_filter = 3; + OrgIDFilter id_filter = 4; + } +} +message OrgNameFilter { + // Organization name. + string name = 1 [ + (validate.rules).string = {max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"ZITADEL\""; + } + ]; + // Defines which text equality method is used. + zitadel.object.v2beta.TextQueryMethod method = 2 [ + (validate.rules).enum.defined_only = true + ]; +} + +message OrgDomainFilter { + // The domain. + string domain = 1 [ + (validate.rules).string = {max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"zitadel.cloud\""; + } + ]; + // Defines which text equality method is used. + zitadel.object.v2beta.TextQueryMethod method = 2 [ + (validate.rules).enum.defined_only = true + ]; +} + +message OrgStateFilter { + // Current state of the organization. + OrgState state = 1 [ + (validate.rules).enum.defined_only = true + ]; +} + +message OrgIDFilter { + // The Organization id. + string id = 1 [ + (validate.rules).string = {max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629023906488334\"" + } + ]; +} + +// from proto/zitadel/org.proto +message DomainSearchFilter { + oneof filter { + option (validate.required) = true; + DomainNameFilter domain_name_filter = 1; + } +} + +// from proto/zitadel/org.proto +message DomainNameFilter { + // The domain. + string name = 1 [ + (validate.rules).string = {max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"zitadel.cloud\""; + } + ]; + // Defines which text equality method is used. + zitadel.object.v2beta.TextQueryMethod method = 2 [ + (validate.rules).enum.defined_only = true + ]; +} + +// from proto/zitadel/org.proto +message Domain { + // The Organization id. + string organization_id = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629023906488334\"" + } + ]; + // The domain name. + string domain_name = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"zitadel.com\""; + } + ]; + // Defines if the domain is verified. + bool is_verified = 3; + // Defines if the domain is the primary domain. + bool is_primary = 4; + // Defines the protocol the domain was validated with. + DomainValidationType validation_type = 5; +} + +// from proto/zitadel/org.proto +enum DomainValidationType { + DOMAIN_VALIDATION_TYPE_UNSPECIFIED = 0; + DOMAIN_VALIDATION_TYPE_HTTP = 1; + DOMAIN_VALIDATION_TYPE_DNS = 2; +} diff --git a/proto/zitadel/org/v2beta/org_service.proto b/proto/zitadel/org/v2beta/org_service.proto index e303b676d7..28c823a89b 100644 --- a/proto/zitadel/org/v2beta/org_service.proto +++ b/proto/zitadel/org/v2beta/org_service.proto @@ -6,24 +6,22 @@ package zitadel.org.v2beta; import "zitadel/object/v2beta/object.proto"; import "zitadel/protoc_gen_zitadel/v2/options.proto"; import "zitadel/user/v2beta/auth.proto"; -import "zitadel/user/v2beta/email.proto"; -import "zitadel/user/v2beta/phone.proto"; -import "zitadel/user/v2beta/idp.proto"; -import "zitadel/user/v2beta/password.proto"; -import "zitadel/user/v2beta/user.proto"; +import "zitadel/org/v2beta/org.proto"; +import "zitadel/metadata/v2beta/metadata.proto"; import "zitadel/user/v2beta/user_service.proto"; import "google/api/annotations.proto"; import "google/api/field_behavior.proto"; -import "google/protobuf/duration.proto"; import "google/protobuf/struct.proto"; import "protoc-gen-openapiv2/options/annotations.proto"; import "validate/validate.proto"; +import "google/protobuf/timestamp.proto"; +import "zitadel/filter/v2beta/filter.proto"; option go_package = "github.com/zitadel/zitadel/pkg/grpc/org/v2beta;org"; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { info: { - title: "User Service"; + title: "Organization Service (Beta)"; version: "2.0-beta"; description: "This API is intended to manage organizations in a ZITADEL instance. This project is in beta state. It can AND will continue breaking until the services provide the same functionality as the current login."; contact:{ @@ -111,8 +109,13 @@ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { service OrganizationService { - // Create a new organization and grant the user(s) permission to manage it - rpc AddOrganization(AddOrganizationRequest) returns (AddOrganizationResponse) { + // Create Organization + // + // Create a new organization with an administrative user. If no specific roles are sent for the users, they will be granted the role ORG_OWNER. + // + // Required permission: + // - `org.create` + rpc CreateOrganization(CreateOrganizationRequest) returns (CreateOrganizationResponse) { option (google.api.http) = { post: "/v2beta/organizations" body: "*" @@ -122,34 +125,411 @@ service OrganizationService { auth_option: { permission: "org.create" } - http_response: { - success_code: 201 - } }; - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Create an Organization"; - description: "Create a new organization with an administrative user. If no specific roles are sent for the users, they will be granted the role ORG_OWNER." + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { responses: { - key: "200" + key: "200"; value: { - description: "OK"; + description: "Organization created successfully"; + }; + }; + responses: { + key: "409" + value: { + description: "The organization to create already exists."; } }; }; } + + // Update Organization + // + // Change the name of the organization. + // + // Required permission: + // - `org.write` + rpc UpdateOrganization(UpdateOrganizationRequest) returns (UpdateOrganizationResponse) { + option (google.api.http) = { + post: "/v2beta/organizations/{id}" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "org.write" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "Organization created successfully"; + }; + }; + responses: { + key: "404" + value: { + description: "Organisation's not found"; + } + }; + responses: { + key: "409" + value: { + description: "Organisation's name already taken"; + } + }; + }; + + } + + // List Organizations + // + // Returns a list of organizations that match the requesting filters. All filters are applied with an AND condition. + // + // Required permission: + // - `iam.read` + rpc ListOrganizations(ListOrganizationsRequest) returns (ListOrganizationsResponse) { + option (google.api.http) = { + post: "/v2beta/organizations/search"; + body: "*"; + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "iam.read"; + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + }; + }; + } + + // Delete Organization + // + // Deletes the organization and all its resources (Users, Projects, Grants to and from the org). Users of this organization will not be able to log in. + // + // Required permission: + // - `org.delete` + rpc DeleteOrganization(DeleteOrganizationRequest) returns (DeleteOrganizationResponse) { + option (google.api.http) = { + delete: "/v2beta/organizations/{id}" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "org.delete"; + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "Organization created successfully"; + }; + }; + responses: { + key: "404" + value: { + description: "Organisation's not found"; + } + }; + }; + } + + // Set Organization Metadata + // + // Adds or updates a metadata value for the requested key. Make sure the value is base64 encoded. + // + // Required permission: + // - `org.write` + rpc SetOrganizationMetadata(SetOrganizationMetadataRequest) returns (SetOrganizationMetadataResponse) { + option (google.api.http) = { + post: "/v2beta/organizations/{organization_id}/metadata" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "org.write" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + }; + responses: { + // TODO This needs to chagne to 404 + key: "400" + value: { + description: "Organisation's not found"; + } + }; + }; + } + + // List Organization Metadata + // + // List metadata of an organization filtered by query. + // + // Required permission: + // - `org.read` + rpc ListOrganizationMetadata(ListOrganizationMetadataRequest) returns (ListOrganizationMetadataResponse ) { + option (google.api.http) = { + post: "/v2beta/organizations/{organization_id}/metadata/search" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { auth_option: { + permission: "org.read" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + }; + }; + + } + + // Delete Organization Metadata + // + // Delete metadata objects from an organization with a specific key. + // + // Required permission: + // - `org.write` + rpc DeleteOrganizationMetadata(DeleteOrganizationMetadataRequest) returns (DeleteOrganizationMetadataResponse) { + option (google.api.http) = { + delete: "/v2beta/organizations/{organization_id}/metadata" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "org.write" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + }; + }; + + } + + // Add Organization Domain + // + // Add a new domain to an organization. The domains are used to identify to which organization a user belongs. + // + // Required permission: + // - `org.write` + rpc AddOrganizationDomain(AddOrganizationDomainRequest) returns (AddOrganizationDomainResponse) { + option (google.api.http) = { + post: "/v2beta/organizations/{organization_id}/domains" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "org.write" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + }; + responses: { + key: "409" + value: { + description: "Domain already exists"; + } + }; + }; + + } + + // List Organization Domains + // + // Returns the list of registered domains of an organization. The domains are used to identify to which organization a user belongs. + // + // Required permission: + // - `org.read` + rpc ListOrganizationDomains(ListOrganizationDomainsRequest) returns (ListOrganizationDomainsResponse) { + option (google.api.http) = { + post: "/v2beta/organizations/{organization_id}/domains/search" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "org.read" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + }; + }; + + } + + // Delete Organization Domain + // + // Delete a new domain from an organization. The domains are used to identify to which organization a user belongs. If the uses use the domain for login, this will not be possible afterwards. They have to use another domain instead. + // + // Required permission: + // - `org.write` + rpc DeleteOrganizationDomain(DeleteOrganizationDomainRequest) returns (DeleteOrganizationDomainResponse) { + option (google.api.http) = { + delete: "/v2beta/organizations/{organization_id}/domains" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "org.write" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + }; + }; + + } + + // Generate Organization Domain Validation + // + // Generate a new file to be able to verify your domain with DNS or HTTP challenge. + // + // Required permission: + // - `org.write` + rpc GenerateOrganizationDomainValidation(GenerateOrganizationDomainValidationRequest) returns (GenerateOrganizationDomainValidationResponse) { + option (google.api.http) = { + post: "/v2beta/organizations/{organization_id}/domains/validation/generate" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "org.write" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + }; + responses: { + key: "404" + value: { + description: "Domain doesn't exist on organization"; + } + }; + }; + } + + // Verify Organization Domain + // + // Make sure you have added the required verification to your domain, depending on the method you have chosen (HTTP or DNS challenge). ZITADEL will check it and set the domain as verified if it was successful. A verify domain has to be unique. + // + // Required permission: + // - `org.write` + rpc VerifyOrganizationDomain(VerifyOrganizationDomainRequest) returns (VerifyOrganizationDomainResponse) { + option (google.api.http) = { + post: "/v2beta/organizations/{organization_id}/domains/validation/verify" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "org.write" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + }; + }; + } + + // Deactivate Organization + // + // Sets the state of my organization to deactivated. Users of this organization will not be able to log in. + // + // Required permission: + // - `org.write` + rpc DeactivateOrganization(DeactivateOrganizationRequest) returns (DeactivateOrganizationResponse) { + option (google.api.http) = { + post: "/v2beta/organizations/{id}/deactivate" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "org.write" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + }; + }; + + } + + // Activate Organization + // + // Set the state of my organization to active. The state of the organization has to be deactivated to perform the request. Users of this organization will be able to log in again. + // + // Required permission: + // - `org.write` + rpc ActivateOrganization(ActivateOrganizationRequest) returns (ActivateOrganizationResponse) { + option (google.api.http) = { + post: "/v2beta/organizations/{id}/activate" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "org.write" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + }; + }; + + } + + } -message AddOrganizationRequest{ +message CreateOrganizationRequest{ + // The Admin for the newly created Organization. message Admin { oneof user_type{ string user_id = 1; zitadel.user.v2beta.AddHumanUserRequest human = 2; } - // specify Org Member Roles for the provided user (default is ORG_OWNER if roles are empty) + // specify Organization Member Roles for the provided user (default is ORG_OWNER if roles are empty) repeated string roles = 3; } + // name of the Organization to be created. string name = 1 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, @@ -159,24 +539,403 @@ message AddOrganizationRequest{ example: "\"ZITADEL\""; } ]; - repeated Admin admins = 2; - // optionally set your own id unique for the organization. - optional string org_id = 3 [ - (validate.rules).string = {min_len: 1, max_len: 200}, + // Optionally set your own id unique for the organization. + optional string id = 2 [ + (validate.rules).string = {min_len: 1, max_len: 200 }, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; max_length: 200; - example: "\"d654e6ba-70a3-48ef-a95d-37c8d8a7901a\""; + example: "\"69629012906488334\""; + } + ]; + // Additional Admins for the Organization. + repeated Admin admins = 3; +} + +message CreatedAdmin { + string user_id = 1; + optional string email_code = 2; + optional string phone_code = 3; +} + +message CreateOrganizationResponse{ + // The timestamp of the organization was created. + google.protobuf.Timestamp creation_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; + + // Organization ID of the newly created organization. + string id = 2 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"69629012906488334\""; + } + ]; + + // The admins created for the Organization + repeated CreatedAdmin created_admins = 3; +} + +message UpdateOrganizationRequest { + // Organization Id for the Organization to be updated + string id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"69629012906488334\""; + } + ]; + + // New Name for the Organization to be updated + string name = 2 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"Customer 1\""; } ]; } -message AddOrganizationResponse{ - message CreatedAdmin { - string user_id = 1; - optional string email_code = 2; - optional string phone_code = 3; - } - zitadel.object.v2beta.Details details = 1; - string organization_id = 2; - repeated CreatedAdmin created_admins = 3; +message UpdateOrganizationResponse { + // The timestamp of the update to the organization. + google.protobuf.Timestamp change_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message ListOrganizationsRequest { + // List limitations and ordering. + optional zitadel.filter.v2beta.PaginationRequest pagination = 1; + // the field the result is sorted + zitadel.org.v2beta.OrgFieldName sorting_column = 2; + // Define the criteria to query for. + // repeated ProjectRoleQuery filters = 4; + repeated zitadel.org.v2beta.OrganizationSearchFilter filter = 3; +} + +message ListOrganizationsResponse { + // Pagination of the Organizations results + zitadel.filter.v2beta.PaginationResponse pagination = 1; + // The Organizations requested + repeated zitadel.org.v2beta.Organization organizations = 2; +} + +message DeleteOrganizationRequest { + + // Organization Id for the Organization to be deleted + string id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629023906488334\""; + min_length: 1; + max_length: 200; + } + ]; +} + +message DeleteOrganizationResponse { + // The timestamp of the deletion of the organization. + google.protobuf.Timestamp deletion_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message DeactivateOrganizationRequest { + // Organization Id for the Organization to be deactivated + string id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629023906488334\""; + min_length: 1; + max_length: 200; + } + ]; +} + +message DeactivateOrganizationResponse { + // The timestamp of the deactivation of the organization. + google.protobuf.Timestamp change_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message ActivateOrganizationRequest { + // Organization Id for the Organization to be activated + string id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629023906488334\""; + min_length: 1; + max_length: 200; + } + ]; +} + +message ActivateOrganizationResponse { + // The timestamp of the activation of the organization. + google.protobuf.Timestamp change_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message AddOrganizationDomainRequest { + // Organization Id for the Organization for which the domain is to be added to. + string organization_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"69629012906488334\""; + } + ]; + // The domain you want to add to the organization. + string domain = 2 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"testdomain.com\""; + } + ]; +} + +message AddOrganizationDomainResponse { + // The timestamp of the organization was created. + google.protobuf.Timestamp creation_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; +} + +message ListOrganizationDomainsRequest { + // Organization Id for the Organization which domains are to be listed. + string organization_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"69629012906488334\""; + } + ]; + + // List limitations and ordering. + optional zitadel.filter.v2beta.PaginationRequest pagination = 2; + // Define the criteria to query for. + repeated DomainSearchFilter filters = 3; +} + +message ListOrganizationDomainsResponse { + // Pagination of the Organizations domain results. + zitadel.filter.v2beta.PaginationResponse pagination = 1; + // The domains requested. + repeated Domain domains = 2; +} + +message DeleteOrganizationDomainRequest { + // Organization Id for the Organization which domain is to be deleted. + string organization_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"69629012906488334\""; + } + ]; + string domain = 2 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"testdomain.com\""; + } + ]; +} + +message DeleteOrganizationDomainResponse { + // The timestamp of the deletion of the organization domain. + google.protobuf.Timestamp deletion_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message GenerateOrganizationDomainValidationRequest { + // Organization Id for the Organization which doman to be validated. + string organization_id = 1 [ + + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"69629012906488334\""; + } + ]; + // The domain which to be deleted. + string domain = 2 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"testdomain.com\""; + } + ]; + DomainValidationType type = 3 [(validate.rules).enum = {defined_only: true, not_in: [0]}]; +} + +message GenerateOrganizationDomainValidationResponse { + // The token verify domain. + string token = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"ofSBHsSAVHAoTIE4Iv2gwhaYhTjcY5QX\""; + } + ]; + // URL used to verify the domain. + string url = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"https://testdomain.com/.well-known/zitadel-challenge/ofSBHsSAVHAoTIE4Iv2gwhaYhTjcY5QX\""; + } + ]; +} + +message VerifyOrganizationDomainRequest { + // Organization Id for the Organization doman to be verified. + string organization_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"69629012906488334\""; + } + ]; + // Organization Id for the Organization doman to be verified. + string domain = 2 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"testdomain.com\""; + } + ]; +} + +message VerifyOrganizationDomainResponse { + // The timestamp of the verification of the organization domain. + google.protobuf.Timestamp change_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message Metadata { + // Key in the metadata key/value pair. + string key = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; + // Value in the metadata key/value pair. + bytes value = 2 [(validate.rules).bytes = {min_len: 1, max_len: 500000}]; +} +message SetOrganizationMetadataRequest{ + // Organization Id for the Organization doman to be verified. + string organization_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"69629012906488334\""; + } + ]; + // Metadata to set. + repeated Metadata metadata = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + title: "Medata (Key/Value)" + description: "The values have to be base64 encoded."; + example: "[{\"key\": \"test1\", \"value\": \"VGhpcyBpcyBteSBmaXJzdCB2YWx1ZQ==\"}, {\"key\": \"test2\", \"value\": \"VGhpcyBpcyBteSBzZWNvbmQgdmFsdWU=\"}]" + } + ]; +} + +message SetOrganizationMetadataResponse{ + // The timestamp of the update of the organization metadata. + google.protobuf.Timestamp set_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message ListOrganizationMetadataRequest { + // Organization ID of Orgalization which metadata is to be listed. + string organization_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"69629012906488334\""; + } + ]; + + // List limitations and ordering. + optional zitadel.filter.v2beta.PaginationRequest pagination = 2; + // Define the criteria to query for. + repeated zitadel.metadata.v2beta.MetadataQuery filter = 3; +} + +message ListOrganizationMetadataResponse { + // Pagination of the Organizations metadata results. + zitadel.filter.v2beta.PaginationResponse pagination = 1; + // The Organization metadata requested. + repeated zitadel.metadata.v2beta.Metadata metadata = 2; +} + +message DeleteOrganizationMetadataRequest { + // Organization ID of Orgalization which metadata is to be deleted is stored on. + string organization_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"69629012906488334\""; + } + ]; + // The keys for the Organization metadata to be deleted. + repeated string keys = 2 [(validate.rules).repeated.items.string = {min_len: 1, max_len: 200}]; +} + +message DeleteOrganizationMetadataResponse{ + // The timestamp of the deletiion of the organization metadata. + google.protobuf.Timestamp deletion_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; } From 15902f5bc79047dd1bf6083e60047a4acccad353 Mon Sep 17 00:00:00 2001 From: Livio Spring Date: Tue, 3 Jun 2025 14:48:15 +0200 Subject: [PATCH 58/76] fix(cache): prevent org cache overwrite by other instances (#10012) # Which Problems Are Solved A customer reported that randomly certain login flows, such as automatic redirect to the only configured IdP would not work. During the investigation it was discovered that they used that same primary domain on two different instances. As they used the domain for preselecting the organization, one would always overwrite the other in the cache. Since The organization and especially it's policies could not be retrieved on the other instance, it would fallback to the default organization settings, where the external login and the corresponding IdP were not configured. # How the Problems Are Solved Include the instance id in the cache key for organizations to prevent overwrites. # Additional Changes None # Additional Context - found because of a support request - requires backport to 2.70.x, 2.71.x and 3.x --- internal/query/org.go | 22 +++++++++++++++++----- internal/query/org_test.go | 4 ++++ internal/v2/readmodel/org.go | 2 ++ 3 files changed, 23 insertions(+), 5 deletions(-) diff --git a/internal/query/org.go b/internal/query/org.go index dfe90ad9f8..e2d9e205da 100644 --- a/internal/query/org.go +++ b/internal/query/org.go @@ -12,6 +12,7 @@ import ( "github.com/zitadel/zitadel/internal/api/authz" domain_pkg "github.com/zitadel/zitadel/internal/domain" + es "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/eventstore/handler/v2" "github.com/zitadel/zitadel/internal/feature" "github.com/zitadel/zitadel/internal/query/projection" @@ -77,6 +78,8 @@ type Org struct { ResourceOwner string State domain_pkg.OrgState Sequence uint64 + // instanceID is used to create a unique cache key for the org + instanceID string Name string Domain string @@ -122,7 +125,7 @@ func (q *Queries) OrgByID(ctx context.Context, shouldTriggerBulk bool, id string ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - if org, ok := q.caches.org.Get(ctx, orgIndexByID, id); ok { + if org, ok := q.caches.org.Get(ctx, orgIndexByID, orgCacheKey(authz.GetInstance(ctx).InstanceID(), id)); ok { return org, nil } defer func() { @@ -159,6 +162,7 @@ func (q *Queries) OrgByID(ctx context.Context, shouldTriggerBulk bool, id string ResourceOwner: foundOrg.Owner, State: domain_pkg.OrgState(foundOrg.State.State), Sequence: uint64(foundOrg.Sequence), + instanceID: foundOrg.InstanceID, Name: foundOrg.Name, Domain: foundOrg.PrimaryDomain.Domain, }, nil @@ -195,7 +199,7 @@ func (q *Queries) OrgByPrimaryDomain(ctx context.Context, domain string) (org *O ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - org, ok := q.caches.org.Get(ctx, orgIndexByPrimaryDomain, domain) + org, ok := q.caches.org.Get(ctx, orgIndexByPrimaryDomain, orgCacheKey(authz.GetInstance(ctx).InstanceID(), domain)) if ok { return org, nil } @@ -430,6 +434,7 @@ func prepareOrgQuery() (sq.SelectBuilder, func(*sql.Row) (*Org, error)) { OrgColumnResourceOwner.identifier(), OrgColumnState.identifier(), OrgColumnSequence.identifier(), + OrgColumnInstanceID.identifier(), OrgColumnName.identifier(), OrgColumnDomain.identifier(), ). @@ -444,6 +449,7 @@ func prepareOrgQuery() (sq.SelectBuilder, func(*sql.Row) (*Org, error)) { &o.ResourceOwner, &o.State, &o.Sequence, + &o.instanceID, &o.Name, &o.Domain, ) @@ -521,15 +527,21 @@ const ( func (o *Org) Keys(index orgIndex) []string { switch index { case orgIndexByID: - return []string{o.ID} + return []string{orgCacheKey(o.instanceID, o.ID)} case orgIndexByPrimaryDomain: - return []string{o.Domain} + return []string{orgCacheKey(o.instanceID, o.Domain)} case orgIndexUnspecified: } return nil } +func orgCacheKey(instanceID, key string) string { + return instanceID + "-" + key +} + func (c *Caches) registerOrgInvalidation() { - invalidate := cacheInvalidationFunc(c.org, orgIndexByID, getAggregateID) + invalidate := cacheInvalidationFunc(c.org, orgIndexByID, func(aggregate *es.Aggregate) string { + return orgCacheKey(aggregate.InstanceID, aggregate.ID) + }) projection.OrgProjection.RegisterCacheInvalidation(invalidate) } diff --git a/internal/query/org_test.go b/internal/query/org_test.go index d704d2901a..635594e7fd 100644 --- a/internal/query/org_test.go +++ b/internal/query/org_test.go @@ -50,6 +50,7 @@ var ( ` projections.orgs1.resource_owner,` + ` projections.orgs1.org_state,` + ` projections.orgs1.sequence,` + + ` projections.orgs1.instance_id,` + ` projections.orgs1.name,` + ` projections.orgs1.primary_domain` + ` FROM projections.orgs1` @@ -60,6 +61,7 @@ var ( "resource_owner", "org_state", "sequence", + "instance_id", "name", "primary_domain", } @@ -242,6 +244,7 @@ func Test_OrgPrepares(t *testing.T) { "ro", domain.OrgStateActive, uint64(20211108), + "instance-id", "org-name", "zitadel.ch", }, @@ -254,6 +257,7 @@ func Test_OrgPrepares(t *testing.T) { ResourceOwner: "ro", State: domain.OrgStateActive, Sequence: 20211108, + instanceID: "instance-id", Name: "org-name", Domain: "zitadel.ch", }, diff --git a/internal/v2/readmodel/org.go b/internal/v2/readmodel/org.go index 94bcb21537..ce61ef69b0 100644 --- a/internal/v2/readmodel/org.go +++ b/internal/v2/readmodel/org.go @@ -18,6 +18,7 @@ type Org struct { CreationDate time.Time ChangeDate time.Time Owner string + InstanceID string } func NewOrg(id string) *Org { @@ -60,6 +61,7 @@ func (rm *Org) Reduce(events ...*eventstore.StorageEvent) error { } rm.Sequence = event.Sequence rm.ChangeDate = event.CreatedAt + rm.InstanceID = event.Aggregate.Instance } if err := rm.State.Reduce(events...); err != nil { return err From b8ff83454e8cf08f8969db94266c44a0ee158b06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabienne=20B=C3=BChler?= Date: Tue, 3 Jun 2025 15:44:04 +0200 Subject: [PATCH 59/76] docs: product roadmap and zitadel versions (#9838) # Which Problems Are Solved The current public roadmap can be hard to understand for customers and it doesn't show the timelines for the different versions. which results in a lot of requests. It only outlines what is already fixed on the timeline, but doesn't give any possibilities to outline future topics / features, which not yet have a timeline # How the Problems Are Solved A new roadmap page is added - Outline for each version when it will have which state - Outline different zitadel versions with its features, deprecations, breaking changes, etc. - Show future topics, which are not yet on the roadmap --- docs/docs/apis/introduction.mdx | 2 +- docs/docs/concepts/architecture/software.md | 2 +- docs/docs/guides/integrate/actions/usage.md | 2 +- .../guides/integrate/login-ui/device-auth.mdx | 4 +- .../integrate/login-ui/oidc-standard.mdx | 2 +- .../integrate/login-ui/saml-standard.mdx | 2 +- .../guides/integrate/login/login-users.mdx | 4 +- .../guides/integrate/login/oidc/webkeys.md | 2 +- docs/docs/guides/manage/customize/branding.md | 2 +- docs/docs/product/_beta-ga.mdx | 1 + docs/docs/product/_breaking-changes.mdx | 1 + docs/docs/product/_deprecated.mdx | 1 + docs/docs/product/_new-feature.mdx | 1 + docs/docs/product/_sdk_v3.mdx | 32 + docs/docs/product/release-cycle.mdx | 63 ++ docs/docs/product/roadmap.mdx | 714 ++++++++++++++++++ docs/sidebars.js | 15 +- docs/src/css/custom.css | 4 + docs/static/img/product/release-cycle.png | Bin 0 -> 102080 bytes 19 files changed, 842 insertions(+), 12 deletions(-) create mode 100644 docs/docs/product/_beta-ga.mdx create mode 100644 docs/docs/product/_breaking-changes.mdx create mode 100644 docs/docs/product/_deprecated.mdx create mode 100644 docs/docs/product/_new-feature.mdx create mode 100644 docs/docs/product/_sdk_v3.mdx create mode 100644 docs/docs/product/release-cycle.mdx create mode 100644 docs/docs/product/roadmap.mdx create mode 100644 docs/static/img/product/release-cycle.png diff --git a/docs/docs/apis/introduction.mdx b/docs/docs/apis/introduction.mdx index e05a7e84b3..905adfc0fb 100644 --- a/docs/docs/apis/introduction.mdx +++ b/docs/docs/apis/introduction.mdx @@ -45,7 +45,7 @@ The [OIDC Playground](https://zitadel.com/playgrounds/oidc) is for testing OpenI ### Custom -ZITADEL allows to authenticate users by creating a session with the [Session API](/docs/apis/resources/session_service_v2), get OIDC authentication request details with the [OIDC service API](/docs/apis/resources/oidc_service) or get SAML request details with the [SAML service API](/docs/apis/resources/saml_service). +ZITADEL allows to authenticate users by creating a session with the [Session API](/docs/apis/resources/session_service_v2), get OIDC authentication request details with the [OIDC service API](/docs/apis/resources/oidc_service_v2) or get SAML request details with the [SAML service API](/docs/apis/resources/saml_service_v2). User authorizations can be [retrieved as roles from our APIs](/docs/guides/integrate/retrieve-user-roles). Refer to our guide to learn how to [build your own login UI](/docs/guides/integrate/login-ui) diff --git a/docs/docs/concepts/architecture/software.md b/docs/docs/concepts/architecture/software.md index dc6f2b56c7..01bacfe12d 100644 --- a/docs/docs/concepts/architecture/software.md +++ b/docs/docs/concepts/architecture/software.md @@ -147,5 +147,5 @@ Zitadel currently supports PostgreSQL. Make sure to read our [Production Guide](/docs/self-hosting/manage/production#prefer-postgresql) before you decide on using one of them. :::info -Zitadel v2 supported CockroachDB and PostgreSQL. Zitadel v3 only supports PostgreSQL. Please refer to [the mirror guide](cli/mirror) to migrate to PostgreSQL. +Zitadel v2 supported CockroachDB and PostgreSQL. Zitadel v3 only supports PostgreSQL. Please refer to [the mirror guide](/docs/self-hosting/manage/cli/mirror) to migrate to PostgreSQL. ::: \ No newline at end of file diff --git a/docs/docs/guides/integrate/actions/usage.md b/docs/docs/guides/integrate/actions/usage.md index ba512ae549..e21fb4935d 100644 --- a/docs/docs/guides/integrate/actions/usage.md +++ b/docs/docs/guides/integrate/actions/usage.md @@ -371,7 +371,7 @@ The API documentation to create a target can be found [here](/apis/resources/act To ensure the integrity of request content, each call includes a 'ZITADEL-Signature' in the headers. This header contains an HMAC value computed from the request content and a timestamp, which can be used to time out requests. The logic for this process is provided in 'pkg/actions/signing.go'. The goal is to verify that the HMAC value in the header matches the HMAC value computed by the Target, ensuring that the sent and received requests are identical. Each Target resource now contains also a Signing Key, which gets generated and returned when a Target is [created](/apis/resources/action_service_v2/action-service-create-target), -and can also be newly generated when a Target is [patched](/apis/resources/action_service_v2/action-service-patch-target). +and can also be newly generated when a Target is [patched](/apis/resources/action_service_v2/action-service-update-target). For an example on how to check the signature, [refer to the example](/guides/integrate/actions/testing-request-signature). diff --git a/docs/docs/guides/integrate/login-ui/device-auth.mdx b/docs/docs/guides/integrate/login-ui/device-auth.mdx index f60fad1310..32984a17ff 100644 --- a/docs/docs/guides/integrate/login-ui/device-auth.mdx +++ b/docs/docs/guides/integrate/login-ui/device-auth.mdx @@ -102,7 +102,7 @@ Present the user with the information of the device authorization request and al ### Perform Login After you have initialized the OIDC flow you can implement the login. -Implement all the steps you like the user the go trough by [creating](/docs/apis/resources/session_service_v2/session-service-create-session) and [updating](/docs/apis/resources/session_service/session-service-set-session) the user-session. +Implement all the steps you like the user the go trough by [creating](/docs/apis/resources/session_service_v2/session-service-create-session) and [updating](/docs/apis/resources/session_service_v2/session-service-set-session) the user-session. Read the following resources for more information about the different checks: - [Username and Password](./username-password) @@ -117,7 +117,7 @@ On the create and update user session request you will always get a session toke The latest session token has to be sent to the following request: -Read more about the [Authorize or Deny Device Authorization Request Documentation](/docs/apis/resources/oidc_service_v2/oidc-service-authorize-device-authorization) +Read more about the [Authorize or Deny Device Authorization Request Documentation](/docs/apis/resources/oidc_service_v2/oidc-service-authorize-or-deny-device-authorization) Make sure that the authorization header is from an account which is permitted to finalize the Auth Request through the `IAM_LOGIN_CLIENT` role. ```bash diff --git a/docs/docs/guides/integrate/login-ui/oidc-standard.mdx b/docs/docs/guides/integrate/login-ui/oidc-standard.mdx index c96338fbf0..92068e5116 100644 --- a/docs/docs/guides/integrate/login-ui/oidc-standard.mdx +++ b/docs/docs/guides/integrate/login-ui/oidc-standard.mdx @@ -80,7 +80,7 @@ Response Example: ### Perform Login After you have initialized the OIDC flow you can implement the login. -Implement all the steps you like the user the go trough by [creating](/docs/apis/resources/session_service_v2/session-service-create-session) and [updating](/docs/apis/resources/session_service/session-service-set-session) the user-session. +Implement all the steps you like the user the go trough by [creating](/docs/apis/resources/session_service_v2/session-service-create-session) and [updating](/docs/apis/resources/session_service_v2/session-service-set-session) the user-session. Read the following resources for more information about the different checks: - [Username and Password](./username-password) diff --git a/docs/docs/guides/integrate/login-ui/saml-standard.mdx b/docs/docs/guides/integrate/login-ui/saml-standard.mdx index 8114350d5d..5196f6c81a 100644 --- a/docs/docs/guides/integrate/login-ui/saml-standard.mdx +++ b/docs/docs/guides/integrate/login-ui/saml-standard.mdx @@ -77,7 +77,7 @@ Response Example: ### Perform Login After you have initialized the SAML flow you can implement the login. -Implement all the steps you like the user to go trough by [creating](/docs/apis/resources/session_service_v2/session-service-create-session) and [updating](/docs/apis/resources/session_service/session-service-set-session) the user-session. +Implement all the steps you like the user to go trough by [creating](/docs/apis/resources/session_service_v2/session-service-create-session) and [updating](/docs/apis/resources/session_service_v2/session-service-set-session) the user-session. Read the following resources for more information about the different checks: - [Username and Password](./username-password) diff --git a/docs/docs/guides/integrate/login/login-users.mdx b/docs/docs/guides/integrate/login/login-users.mdx index de1dacfe24..13439ac1db 100644 --- a/docs/docs/guides/integrate/login/login-users.mdx +++ b/docs/docs/guides/integrate/login/login-users.mdx @@ -25,7 +25,7 @@ The identity provider is not part of the original application, but a standalone The user will authenticate using their credentials. After successful authentication, the user will be redirected back to the original application. -If you want to read more about authenticating with OIDC, head over to our comprehensive [OpenID Connect Guide](/docs/integrate/login/oidc). +If you want to read more about authenticating with OIDC, head over to our comprehensive [OpenID Connect Guide](/docs/guides/integrate/login/oidc). ### Authenticate users with SAML @@ -54,7 +54,7 @@ Note that SAML might not be suitable for mobile applications. In case you want to integrate a mobile application, use OpenID Connect or our Session API. There are more [differences between SAML and OIDC](https://zitadel.com/blog/saml-vs-oidc) that you might want to consider. -If you want to read more about authenticating with SAML, head over to our comprehensive [SAML Guide](/docs/integrate/login/saml). +If you want to read more about authenticating with SAML, head over to our comprehensive [SAML Guide](/docs/guides/integrate/login/saml). ## ZITADEL's Session API diff --git a/docs/docs/guides/integrate/login/oidc/webkeys.md b/docs/docs/guides/integrate/login/oidc/webkeys.md index a66cae61a9..62f62a90e0 100644 --- a/docs/docs/guides/integrate/login/oidc/webkeys.md +++ b/docs/docs/guides/integrate/login/oidc/webkeys.md @@ -85,7 +85,7 @@ The same counts for [zitadel/oidc](https://github.com/zitadel/oidc) Go library. ## Web Key management -ZITADEL provides a resource based [web keys API](/docs/apis/resources/webkey_service_v3). +ZITADEL provides a resource based [web keys API](/docs/apis/resources/webkey_service_v2). The API allows the creation, activation, deletion and listing of web keys. All public keys that are stored for an instance are served on the [JWKS endpoint](#json-web-key-set). Applications need public keys for token verification and not all applications are capable of on-demand diff --git a/docs/docs/guides/manage/customize/branding.md b/docs/docs/guides/manage/customize/branding.md index 14c18705f6..c5ec0a8838 100644 --- a/docs/docs/guides/manage/customize/branding.md +++ b/docs/docs/guides/manage/customize/branding.md @@ -46,7 +46,7 @@ If you like to trigger your settings for your applications you have different po Send a [reserved scope](/apis/openidoauth/scopes) with your [authorization request](../../integrate/login/oidc/login-users#auth-request) to trigger your organization. The primary domain scope will restrict the login to your organization, so only users of your own organization will be able to login. -You can use our [OpenID Authentication Request Playground](/oidc-playground) to learn more about how to trigger an [organization's policies and branding](/oidc-playground#organization-policies-and-branding). +You can use our [OpenID Authentication Request Playground](https://zitadel.com/playgrounds/oidc) to learn more about how to trigger an [organization's policies and branding](https://zitadel.com/playgrounds/oidc#organization-policies-and-branding). ### 2. Setting on your Project diff --git a/docs/docs/product/_beta-ga.mdx b/docs/docs/product/_beta-ga.mdx new file mode 100644 index 0000000000..229f94cdf5 --- /dev/null +++ b/docs/docs/product/_beta-ga.mdx @@ -0,0 +1 @@ +This describes the progression of features from a limited, pre-release testing phase (Beta) to their official, stable, and publicly available version (General Availability), ready for widespread use, with the specific transitions listed below. \ No newline at end of file diff --git a/docs/docs/product/_breaking-changes.mdx b/docs/docs/product/_breaking-changes.mdx new file mode 100644 index 0000000000..dd903a0f2e --- /dev/null +++ b/docs/docs/product/_breaking-changes.mdx @@ -0,0 +1 @@ +These are modifications to existing functionalities that may require users to alter their current implementation or usage to ensure continued compatibility; see the list below for specifics. \ No newline at end of file diff --git a/docs/docs/product/_deprecated.mdx b/docs/docs/product/_deprecated.mdx new file mode 100644 index 0000000000..e5848d68f0 --- /dev/null +++ b/docs/docs/product/_deprecated.mdx @@ -0,0 +1 @@ +This announces that specific existing features are being phased out and are scheduled for future removal, often because they have become outdated or are being replaced by an improved alternative; please see the deprecated items listed below. \ No newline at end of file diff --git a/docs/docs/product/_new-feature.mdx b/docs/docs/product/_new-feature.mdx new file mode 100644 index 0000000000..1877a1d690 --- /dev/null +++ b/docs/docs/product/_new-feature.mdx @@ -0,0 +1 @@ +These introduce brand-new functionalities or capabilities, expanding the product's offerings and value to users, as detailed below. \ No newline at end of file diff --git a/docs/docs/product/_sdk_v3.mdx b/docs/docs/product/_sdk_v3.mdx new file mode 100644 index 0000000000..76040640a2 --- /dev/null +++ b/docs/docs/product/_sdk_v3.mdx @@ -0,0 +1,32 @@ +import NewFeature from './_new-feature.mdx'; + +An initial version of our Software Development Kit (SDK) will be published. +To better align our versioning with the [ZITADEL core](#zitadel-core), the SDK will be released as version 3.x. +This strategic versioning will ensure a more consistent and intuitive development experience across our entire ecosystem. + +
+ New Features + + + +
+ Machine User Authentication Methods + + This feature introduces robust and standardized authentication methods for your machine users, enabling secure automated access to your resources. + + Choose from the following authentication methods: + - **Private Key JWT Authentication**: Enhance security by using asymmetric cryptography. A client with a registered public key can generate and sign a JSON Web Token (JWT) with its private key to authenticate. + - **Client Credentials Grant**: A simple and direct method for machine-to-machine authentication where the client confidentially provides its credentials to the authorization server in exchange for an access token. + - **Personal Access Tokens (PATs)**: Ideal for individual developers or specific scripts, PATs offer a convenient way to create long-lived, revocable tokens with specific scopes, acting as a substitute for a password. + +
+ +
+ Zitadel APIs Wrapper + + This SDK provides a convenient client for interacting with the ZITADEL APIs, simplifying how you manage resources within your instance. + + Currently, the client is tailored for machine-to-machine communication, enabling machine users to authenticate and manage ZITADEL resources programmatically. + Please note that this initial version is focused on API calls for automated tasks and does not yet include support for human user authentication flows like OAuth or OIDC. +
+
diff --git a/docs/docs/product/release-cycle.mdx b/docs/docs/product/release-cycle.mdx new file mode 100644 index 0000000000..f38c81a36d --- /dev/null +++ b/docs/docs/product/release-cycle.mdx @@ -0,0 +1,63 @@ +--- +title: Release Cycle +sidebar_label: Release Cycle +--- + +We release a new major version of our software every three months. This predictable schedule allows us to introduce significant features and enhancements in a structured way. + +This cadence provides enough time for thorough development and rigorous testing. Before each stable release, we engage with our community and customers to test and stabilize the new version. This ensures high quality and reliability. For our customers, this approach creates a clear and manageable upgrade path. + +While major changes are reserved for these three-month releases, we address urgent needs by backporting smaller updates, such as critical bug and security fixes, to earlier versions. This allows us to provide essential updates without altering the predictable rhythm of our major release cycle. + + +![Release Cycle](/img/product/release-cycle.png) + +## Preparation + +The first quarter of our cycle is for Preparation and Planning, where we create the blueprint for the upcoming major release. +During this time, we define the core architecture, map out the implementation strategy, and finalize the design for the new features. + + +## Implementation + +The second month is the Implementation and Development Phase, where our engineers build the features defined in the planning stage. + +During this period, we focus on writing the code for the new enhancements. +We also integrate accepted contributions from our community and create the necessary documentation alongside the development work. +This phase concludes when the new version is feature-complete and ready to enter the testing phase. + +## Release Candidate (RC) + +The first month of the third quarter is for the Release Candidate (RC) and Stabilization Phase. +At the beginning of this month, we publish a Release Candidate version. +This is a feature-complete version that we believe is ready for public release, made available to our customers and community for widespread testing. + +This phase is critical for ensuring the quality of the final release. We have two main objectives: +- **Community Feedback and Bug Fixing**: This is when we rely on your feedback. By testing the RC in your own environments, you help us find and fix bugs and other issues we may have missed. Your active participation is crucial for stabilizing the new version. +- **Enhanced Internal Testing**: While the community provides feedback, our internal teams conduct enhanced quality assurance. This includes in-depth feature validation, rigorous testing of upgrade paths from previous versions, and comprehensive performance and benchmark testing. + +The goal of this phase is to use both community feedback and internal testing to ensure the new release is robust, bug-free, and performs well, so our customers can upgrade with confidence. + +## General Availability (GA) / Stable + +Following the month-long Release Candidate and Stabilization phase, we publish the official General Availability (GA) / Stable Release. +This is the final, production-ready version of our software that has been thoroughly tested by both our internal teams and the community. + +This release is available to everyone, and we recommend that customers begin reviewing the official upgrade path for their production environments. +The deployment of this new major version to our cloud services also happens at this time. + +**Ongoing Maintenance: Minor and Patch Releases** +Once a major version becomes stable, we provide ongoing support through back-porting. This means we carefully select and apply critical updates from our main development track to the stable release, ensuring it remains secure and reliable. These updates are delivered in two ways: + +- Minor Releases: These include simple features and enhancements from the next release cycle that are safe to add, requiring no major refactoring or large database migrations. +- Patch Releases: These are focused exclusively on high-priority bug and security fixes to address critical issues promptly. + +This process ensures that you can benefit from the stability of a major release while still receiving important updates and fixes in a timely manner. + +## Deprecated + +Each major version is actively supported for a full release cycle after its launch. This means that approximately six months after its initial stable release, a version enters its deprecation period. + +Once a version is deprecated, we strongly encourage all self-hosted customers to upgrade to a newer version as soon as possible to continue receiving the latest features, improvements, and bug fixes. + +For our enterprise customers, we may offer extended support by providing critical security fixes for a deprecated version beyond the standard six-month lifecycle. This extended support is evaluated on a case-by-case basis to ensure a secure and manageable transition for large-scale deployments. \ No newline at end of file diff --git a/docs/docs/product/roadmap.mdx b/docs/docs/product/roadmap.mdx new file mode 100644 index 0000000000..b83efedb10 --- /dev/null +++ b/docs/docs/product/roadmap.mdx @@ -0,0 +1,714 @@ +--- +title: Zitadel Release Versions and Roadmap +sidebar_label: Release Versions and Roadmap +--- + + +import NewFeature from './_new-feature.mdx'; +import BreakingChanges from './_breaking-changes.mdx'; +import Deprecated from './_deprecated.mdx'; +import BetaToGA from './_beta-ga.mdx'; +import SDKv3 from './_sdk_v3.mdx'; + + +## Timeline and Overview + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
20252026
Q1Q2Q3Q4Q1Q2Q3Q4
JanFebMarAprMayJunJulAugSepOctNovDecJanFebMarAprMayJunJulAugSepOctNovDec
Zitadel Versions
[v2.x](/docs/product/roadmap#v2x)GA / Stable Deprecated
[v3.x](/docs/product/roadmap#v3x)ImplementationRCGA / Stable Deprecated
[v4.x](/docs/product/roadmap#v4x)ImplementationRCGA / Stable Deprecated
[v5.x](/docs/product/roadmap#v5x)ImplementationRCGA / Stable Deprecated
+ +For more detailed description about the different stages and the release cycle check out the following Page: [Release Cycle](/docs/product/release-cycle) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
25-Q125-Q225-Q325-Q4
Zitadel Core
+ [v2.x](/docs/product/roadmap#v2x) + + [v3.x](/docs/product/roadmap#v3x) +
    +
  • + Actions V2 +
  • +
  • + Removed CockroachDB Support +
  • +
  • + License Change +
  • +
  • + Login v2 +
      +
    • + Initial Release +
    • +
    • + All standard authentication methods +
    • +
    • + OIDC & SAML +
    • +
    +
  • +
+
+ [v4.x](/docs/product/roadmap#v4x) +
    +
  • Resource API
  • +
  • + Login v2 as default + +
      +
    • + Device Authorization Flow +
    • +
    • + LDAP IDP +
    • +
    • + JWT IDP +
    • +
    • + Custom Login UI Texts +
    • +
    +
  • +
+ +
+ [v5.x](/docs/product/roadmap#v5x) +
    +
  • Analytics
  • +
  • User Groups
  • +
  • User Uniqueness on Instance Level
  • +
  • Remove Required Fields from User
  • +
+
Zitadel SDKs
+ + + + +
    +
  • + [Initial Version of PHP SDK](/docs/product/roadmap#v3x-1) +
  • +
  • + [Initial Version of Java SDK](/docs/product/roadmap#v3x-2) +
  • +
  • + [Initial Version of Ruby SDK](/docs/product/roadmap#v3x-3) +
  • +
  • + [Initial Version of Python SDK](/docs/product/roadmap#v3x-4) +
  • +
+
+
+ +## Zitadel Core + +Check out all [Zitadel Release Versions](https://github.com/zitadel/zitadel/releases) + +### v2.x + +**Current State**: General Availability / Stable + +**Release**: [v2.x](https://github.com/zitadel/zitadel/releases?q=v2.&expanded=true) + +In Zitadel versions 2.x and earlier, new releases were deployed with a minimum frequency of every two weeks. +This practice resulted in a significant number of individual versions. +To review the features and bug fixes for these releases, please consult the linked release information provided above. + +### v3.x + +ZITADEL v3 is here, bringing key changes designed to empower your identity management experience. +This release transitions our licensing to AGPLv3, reinforcing our commitment to open and collaborative development. +We've streamlined our database support by removing CockroachDB. +Excitingly, v3 introduces the foundational elements for Actions V2, opening up a world of possibilities for tailoring and extending ZITADEL to perfectly fit your unique use cases. + +**Current State**: General Availability / Stable + +**Release**: [v3.x](https://github.com/zitadel/zitadel/releases?q=v3.&expanded=true) + +**Blog**: [Zitadel v3: AGPL License, Streamlined Releases, and Platform Updates](https://zitadel.com/blog/zitadel-v3-announcement) + + +
+ New Features + + + +
+ Actions V2 + + Zitadel Actions V2 empowers you to customize Zitadel's workflows by executing your own logic at specific points. You define external Endpoints containing your code and configure Targets and Executions within Zitadel to trigger them based on various conditions and events. + + Why we built it: To provide greater flexibility and control, allowing you to implement custom business rules, automate tasks, enrich user data, control access, and integrate with other systems seamlessly. Actions V2 enables you to tailor Zitadel precisely to your unique needs. + + Read more in our [documentation](https://zitadel.com/docs/concepts/features/actions_v2) +
+ +
+ License Change Apache 2.0 to AGPL3 + + Zitadel is switching to the AGPL 3.0 license to ensure the project's sustainability and encourage community contributions from commercial users, while keeping the core free and open source. + + Read more about our [decision](https://zitadel.com/blog/apache-to-agpl) +
+
+ +
+ Breaking Changes + + + +
+ CockroachDB Support removed + + After careful consideration, we have made the decision to discontinue support for CockroachDB in Zitadel v3 and beyond. + While CockroachDB is an excellent distributed SQL database, supporting multiple database backends has increased our maintenance burden and complicated our testing matrix. + Check out our [migration guide](https://zitadel.com/docs/self-hosting/manage/cli/mirror) to migrate from CockroachDB to PostgreSQL. + + More details can be found [here](https://github.com/zitadel/zitadel/issues/9414) +
+ +
+ Actions API v3 alpha removed + + With the current release we have published the Actions V2 API as a beta version, and got rid of the previously published alpha API. + Check out the [new API](http://localhost:3000/docs/apis/resources/action_service_v2) + +
+
+ +### v4.x + +**Current State**: Implementation + + +
+ New Features + + + +
+ Resource API (v2) + + We are revamping our APIs to improve the developer experience. + Currently, our use-case-based APIs are complex and inconsistent, causing confusion and slowing down integration. + To fix this, we're shifting to a resource-based approach. + This means developers will use consistent endpoints (e.g., /users) to manage resources, regardless of their own role. + This change, along with standardized naming and improved documentation, will simplify integration, accelerate development, and create a more intuitive experience for our customers and community. + + Resources integrated in this release: + - Instances + - Organizations + - Projects + - Users + + For more details read the [Github Issue](https://github.com/zitadel/zitadel/issues/6305) +
+ +
+ Login V2 + + Our new login UI has been enhanced with additional features, bringing it to feature parity with Version 1. + +
+ Device Authorization Flow + + The Device Authorization Grant is an OAuth 2.0 flow designed for devices that have limited input capabilities (like smart TVs, gaming consoles, or IoT devices) or lack a browser. + + Read our docs about how to integrate your application using the [Device Authorization Flow](https://zitadel.com/docs/guides/integrate/login/oidc/device-authorization) +
+ +
+ LDAP IDP + + This feature enables users to log in using their existing LDAP (Lightweight Directory Access Protocol) credentials. + It integrates your system with an LDAP directory, allowing it to act as an Identity Provider (IdP) solely for authentication purposes. + This means users can securely access the service with their familiar LDAP username and password, streamlining the login process. +
+ +
+ JWT IDP + + This "JSON Web Token Identity Provider (JWT IdP)" feature allows you to use an existing JSON Web Token (JWT) from another system (like a Web Application Firewall managing a session) as a federated identity for authentication in new applications managed by ZITADEL. + + Essentially, it enables session reuse by letting ZITADEL trust and validate a JWT issued by an external source. This allows users already authenticated in an existing system to seamlessly access new applications without re-logging in. + + Read more in our docs about how to login users with [JWT IDP](https://zitadel.com/docs/guides/integrate/identity-providers/jwt_idp) +
+ +
+ Custom Login UI Texts + + This feature provides customers with the flexibility to personalize the user experience by customizing various text elements across different screens of the login UI. Administrators can modify default messages, labels, and instructions to align with their branding, provide specific guidance, or cater to unique regional or organizational needs, ensuring a more tailored and intuitive authentication process for their users. +
+
+
+ + +
+ General Availability + + + +
+ Hosted Login v2 + + We're officially moving our new Login UI v2 from beta to General Availability. + Starting now, it will be the default login experience for all new customers. + With this release, 8.0we are also focused on implementing previously missing features, such as device authorization and LDAP IDP support, to make the new UI fully feature-complete. + + - [Hosted Login V2](http://localhost:3000/docs/guides/integrate/login/hosted-login#hosted-login-version-2-beta) +
+ +
+ Web Keys + + Web Keys in ZITADEL are used to sign and verify JSON Web Tokens (JWT). + ID tokens are created, signed and returned by ZITADEL when a OpenID connect (OIDC) or OAuth2 authorization flow completes and a user is authenticated. + Based on customer and community feedback, we've updated our key management system. You now have full manual control over key generation and rotation, instead of the previous automatic process. + + Read the full description about Web Keys in our [Documentation](https://zitadel.com/docs/guides/integrate/login/oidc/webkeys). +
+ +
+ SCIM 2.0 Server - User Resource + + The Zitadel SCIM v2 service provider interface enables seamless integration of identity and access management (IAM) systems with Zitadel, following the System for Cross-domain Identity Management (SCIM) v2.0 specification. + This interface allows standardized management of IAM resources, making it easier to automate user provisioning and deprovisioning. + + - [SCIM 2.0 API](https://zitadel.com/docs/apis/scim2) + - [Manage Users Guide](https://zitadel.com/docs/guides/manage/user/scim2) +
+ + +
+ Token Exchange (Impersonation) + + The Token Exchange grant implements [RFC 8693, OAuth 2.0 Token Exchange](https://www.rfc-editor.org/rfc/rfc8693) and can be used to exchange tokens to a different scope, audience or subject. + Changing the subject of an authenticated token is called impersonation or delegation. + Read more in our [Impersonation and delegation using Token Exchange](https://zitadel.com/docs/guides/integrate/token-exchange) Guide +
+ +
+ Caches + + ZITADEL supports the use of a caches to speed up the lookup of frequently needed objects. + As opposed to HTTP caches which might reside between ZITADEL and end-user applications, the cache build into ZITADEL uses active invalidation when an object gets updated. + Another difference is that HTTP caches only cache the result of a complete request and the built-in cache stores objects needed for the internal business logic. + For example, each request made to ZITADEL needs to retrieve and set instance information in middleware. + + Read more about Zitadel Caches [here](https://zitadel.com/docs/self-hosting/manage/cache) +
+
+ +### v5.x + +**Current State**: Planning + +
+ New Features + + + +
+ Analytics + + We provide comprehensive and insightful analytics capabilities that empower you with the information needed to understand platform usage, monitor system health, and make data-driven decisions. + +
+ Daily Active Users (DAU) & Monthly Active Users (MAU) + + Administrators need to track user activity to understand platform usage and identify trends. + This feature provides basic metrics for daily and monthly active users, allowing for filtering by date range and scope (instance-wide or within a specific organization). + The metrics should ensure that each user is counted only once per day or month, respectively, regardless of how many actions they performed. + This minimal feature serves as a foundation for future expansion into more detailed analytics. + + For more details track our [github issue](https://github.com/zitadel/zitadel/issues/7506). +
+ +
+ Resource Count Metrics + + To effectively manage a Zitadel instance, administrators need to understand resource utilization. + This feature provides metrics for resource counts, including organizations, users (with filtering options), projects, applications, and authorizations. + For users, we will offer filters to retrieve the total count, counts per organization, and counts by user type (human or machine). + These metrics will provide administrators with valuable insights into the scale and complexity of their Zitadel instance. + + + For more details track our [github issue](https://github.com/zitadel/zitadel/issues/9709). +
+ +
+ Operational Metrics + + To empower customers to better manage and optimize their Zitadel instances, we will provide access to detailed operational metrics. + This data will help customers identify potential issues, optimize performance, and ensure the stability of their deployments. + The provided data will encompass basic system information, infrastructure details, configuration settings, error reports, and the health status of various Zitadel components, accessible via a user interface or an API. + + + For more details track our [github issue](https://github.com/zitadel/zitadel/issues/9476). + +
+
+ +
+ User Groups + + Administrators will be able to define groups within an organization and assign users to these groups. + More details about the feature can be found [here](https://github.com/zitadel/zitadel/issues/9702) +
+ +
+ User Uniqueness on Organization Level + + Administrators will be able to define weather users should be unique across the instance or within an organization. + This allows managing users independently and avoids conflicts due to shared user identifiers. + Example: The user with the username user@gmail.com can be created in the Organization "Customer A" and "Customer B" if uniqueness is defined on the organization level. + + Stay updated on the progress and details on our [GitHub Issue](https://github.com/zitadel/zitadel/issues/9535) +
+ + +
+ Remove Required Fields + + Currently, the user creation process requires several fields, such as email, first name, and last name, which can be restrictive in certain scenarios. This feature allows administrators to create users with only a username, making other fields optional. + This provides flexibility for systems that don't require complete user profiles upon initial creation for example simplified onboarding flows. + + For more details check out our [GitHub Issue](https://github.com/zitadel/zitadel/issues/4386) +
+
+ +
+ Feature Deprecation + + + +
+ Actions V1 +
+
+ +
+ Breaking Changes + + + +
+ Hosted Login v1 will be removed +
+ + +
+ Zitadel APIs v1 will be removed +
+
+ + +### v6.x + +
+ New Features + + + +
+ Basic Threat Detection Framework + + This initial version of our Threat Detection Framework is designed to enhance the security of your account by identifying and challenging potentially anomalous user behavior. + When the system detects unusual activity, it will present a challenge, such as a reCAPTCHA, to verify that the user is legitimate and not a bot or malicious actor. + Security administrators will also have the ability to revoke user sessions based on the output of the threat detection model, providing a crucial tool to mitigate potential security risks in real-time. + + We are beginning with a straightforward reCAPTCHA-style challenge to build and refine the core framework. + This foundational step will allow us to gather insights into how the system performs and how it can be improved. + Future iterations will build upon this groundwork to incorporate more sophisticated detection methods and a wider range of challenge and response mechanisms, ensuring an increasingly robust and intelligent security posture for all users. + + More details can be found in the (GitHub Issue](https://github.com/zitadel/zitadel/issues/9707) +
+ +
+ SCIM Outbound + + Automate user provisioning to your external applications with our new SCIM Client. + This feature ensures users are automatically created in downstream systems before their first SSO login, preventing access issues and streamlining onboarding. + + It also synchronizes user lifecycle events, so changes like deactivations or deletions are instantly reflected across all connected applications for consistent and secure access management. + The initial release will focus on provisioning the user resource. + + More details can be found in the (GitHub Issue](https://github.com/zitadel/zitadel/issues/6601) +
+ +
+ Analytics + + We provide comprehensive and insightful analytics capabilities that empower you with the information needed to understand platform usage, monitor system health, and make data-driven decisions. + +
+ Login Insights: Successful and Failed Login Metrics + + To enhance security monitoring and gain insights into user authentication patterns, administrators need access to login metrics. + This feature provides data on successful and failed login attempts, allowing for filtering by time range and level (overall instance, within a specific organization, or for a particular application). + This will enable administrators to detect suspicious login activity, analyze authentication trends, and proactively address potential security concerns. + + For more details track our [GitHub issue](https://github.com/zitadel/zitadel/issues/9711). +
+
+ +
+ Impersonation: External Token Exchange + + This feature expands our existing impersonation capabilities to support seamless and secure integration with external, third-party applications. + Currently, our platform supports impersonation for internal use cases, allowing administrators or support staff to obtain a temporary token for an end-user to troubleshoot issues or provide assistance within applications that already use ZITADEL for authentication. (You can find more details in our [existing documentation](/docs/guides/integrate/token-exchange)). + + The next evolution of this feature will focus on external applications. + This enables scenarios where a user, already authenticated in a third-party system (like their primary e-banking portal), can seamlessly access a connected application that is secured by ZITADEL without needing to log in again. + + For example, a user in their e-banking app could click to open an integrated "Budget Planning" tool that relies on ZITADEL for access. + Using a secure token exchange, the budget app will grant the user a valid session on their behalf, creating a smooth, uninterrupted user experience while maintaining a high level of security. + This enhancement bridges the authentication gap between external platforms and ZITADEL-powered applications. +
+
+ +### Future Vision / Upcoming Features + +#### Fine Grained Authorization + +We're planning the future of Zitadel and fine-grained authorization is high on our list. +While Zitadel already offers strong role-based access (RBAC), we know many of you need more granular control. + +**What is Fine-Grained Authorization?** + +It's about moving beyond broad roles to define precise access based on: + +- Attributes (ABAC): User details (department, location), resource characteristics (sensitivity), or context (time of day). +- Relationships (ReBAC): Connections between users and resources (e.g., "owner" of a document, "manager" of a team). +- Policies (PBAC): Explicit rules combining attributes and relationships. + +**Why Explore This?** + +Fine-grained authorization can offer: +- Tighter Security: Minimize access to only what's essential. +- Greater Flexibility: Adapt to complex and dynamic business rules. +- Easier Compliance: Meet strict regulatory demands. +- Scalable Permissions: Manage access effectively as you grow. + +**We Need Your Input!** 🗣️ + +As we explore the best way to bring this to Zitadel, tell us: +- Your Use Cases: Where do you need more detailed access control than standard roles provide? +- Preferred Models: Are you thinking attribute-based, relationship-based, or something else? +- Integration Preferences: + - A fully integrated solution within Zitadel? + - Or integration with existing authorization vendors (e.g. openFGA, cerbos, etc.)? + +Your feedback is crucial for shaping our roadmap. + +🔗 Share your thoughts and needs in our [discussion forum](https://discord.com/channels/927474939156643850/1368861057669533736) + +#### Threat Detection + +We're taking the next step in securing your applications by exploring a new Threat Detection framework for Zitadel. +Our goal is to proactively identify and stop malicious activity in real-time. + +**Our First Step: A Modern reCAPTCHA Alternative** +We will begin by building a system to detect and mitigate malicious bots, serving as a smart, privacy-focused alternative to CAPTCHA. +This initial use case will help us combat credential stuffing, spam registrations, and other automated attacks, forming the foundation of our larger framework. + +**How We Envision It** + +Our exploration is focused on creating an intelligent system that: +- **Analyzes Signals**: Gathers data points like IP reputation, device characteristics, and user behavior to spot suspicious activity. +- **Uses AI/**: Trains models to distinguish between legitimate users and bots, reducing friction for real users. +- **Mitigates Threats**: Enables flexible responses when a threat is detected, such as blocking the attempt, requiring MFA, or sending an alert. + +**Help Us Shape the Future** 🤝 + +As we design this framework, we need to know: +- What are your biggest security threats today? +- What kind of automated responses (e.g., block, notification) would be most useful for you? +- What are your key privacy or compliance concerns regarding threat detection? + +Your feedback will directly influence our development and ensure we build a solution that truly meets your needs. + +🔗 Join the conversation and share your insights [here](https://discord.com/channels/927474939156643850/1375383775164235806) + + +#### The Role of AI in Zitadel + +As we look to the future, we believe Artificial Intelligence will be a critical tool for enhancing both user experience and security within Zitadel. +Our vision for AI is focused on two key areas: providing intelligent, contextual assistance and building a collective defense against emerging threats. + +1. **AI-Powered Support** + + We want you to get fast, accurate answers to your questions without ever having to leave your workflow. + To achieve this, we are integrating an AI-powered support assistant trained on our knowledge base, including our documentation, tutorials, and community discussions. + + Our rollout is planned in phases to ensure we deliver a helpful experience: + - **Phase 1 (Happening Now)**: We are currently testing a preliminary version of our AI bot within our [community channels](https://discord.com/channels/927474939156643850/1357076488825995477). This allows us to gather real-world questions and answers, refining the AI's accuracy and helpfulness based on direct feedback. + - **Phase 2 (Next Steps)**: Once we are confident in its capabilities, we will integrate this AI assistant directly into our documentation. You'll be able to ask complex questions and get immediate, well-sourced answers. + - **Phase 3 (The Ultimate Goal)**: The final step is to embed the assistant directly into the Zitadel Console/Customer Portal. Imagine getting help based on the exact context of what you're doing—whether you're configuring an action, setting up a new organization, or integrating social login. + +2. **Decentralized AI for Threat Detection** + + Security threats are constantly evolving. + A threat vector that targets one customer today might target another tomorrow. + We believe in the power of collective intelligence to provide proactive security for everyone. + + This leads to our second major AI initiative: **decentralized model training** for our Threat Detection framework. + + Here’s how it would work: + - **Collective Data, Anonymously**: Customers across our cloud and self-hosted environments experience different user behaviors and threat vectors. We plan to offer an opt-in system where anonymized, non-sensitive data (like behavioral patterns and threat signals) can be collected from participating instances. + - **Centralized Training**: This collective, anonymized data will be used to train powerful, next-generation AI security models. With a much larger and more diverse dataset, these models can learn to identify subtle and emerging threats far more effectively than a model trained on a single instance's data. + - **Shared Protection**: These constantly improving models would then be distributed to all participating Zitadel instances. + + The result is a powerful security network effect. You could receive protection from a threat vector you haven't even experienced yet, simply because the system learned from an attack on another member of the community. + + + +## Zitadel Ecosystem + +### PHP SDK + +GitHub Repository: [PHP SDK](https://github.com/zitadel/client-php) + +#### v3.x + + + +### Java SDK + +GitHub Repository: [Java SDK](https://github.com/zitadel/client-java) + +#### v3.x + + + +### Ruby SDK + +GitHub Repository: [Ruby SDK](https://github.com/zitadel/client-ruby) + +#### v3.x + + + +### Python SDK + +GitHub Repository: [Python SDK](https://github.com/zitadel/client-python) + +#### v3.x + + diff --git a/docs/sidebars.js b/docs/sidebars.js index f9b97703e5..1bd53ed1b3 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -183,7 +183,6 @@ module.exports = { items: [ "guides/manage/user/reg-create-user", "guides/manage/customize/user-metadata", - "guides/manage/customize/user-schema", "guides/manage/user/scim2", ], }, @@ -611,6 +610,20 @@ module.exports = { }, ], }, + { + type: "category", + label: "Product Information", + collapsed: true, + items: [ + "product/roadmap", + "product/release-cycle", + { + type: "link", + label: "Changelog", + href: "https://zitadel.com/changelog", + }, + ], + }, { type: "category", label: "Support", diff --git a/docs/src/css/custom.css b/docs/src/css/custom.css index 50035f2541..11d4208695 100644 --- a/docs/src/css/custom.css +++ b/docs/src/css/custom.css @@ -641,3 +641,7 @@ p strong { .zitadel-lifecycle-deprecated { text-decoration: line-through; } + +table#zitadel-versions td { + vertical-align: top; +} \ No newline at end of file diff --git a/docs/static/img/product/release-cycle.png b/docs/static/img/product/release-cycle.png new file mode 100644 index 0000000000000000000000000000000000000000..7017566ab7087d14af651d9e94f4b99caecac804 GIT binary patch literal 102080 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 literal 0 HcmV?d00001 From b9c1cdf4adb8b4a7737b8f8d5b6a5abd270ad5e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Tue, 3 Jun 2025 17:15:30 +0300 Subject: [PATCH 60/76] feat(projections): resource counters (#9979) # Which Problems Are Solved Add the ability to keep track of the current counts of projection resources. We want to prevent calling `SELECT COUNT(*)` on tables, as that forces a full scan and sudden spikes of DB resource uses. # How the Problems Are Solved - A resource_counts table is added - Triggers that increment and decrement the counted values on inserts and deletes - Triggers that delete all counts of a table when the source table is TRUNCATEd. This is not in the business logic, but prevents wrong counts in case someone want to force a re-projection. - Triggers that delete all counts if the parent resource is deleted - Script to pre-populate the resource_counts table when a new source table is added. The triggers are reusable for any type of resource, in case we choose to add more in the future. Counts are aggregated by a given parent. Currently only `instance` and `organization` are defined as possible parent. This can later be extended to other types, such as `project`, should the need arise. I deliberately chose to use `parent_id` to distinguish from the de-factor `resource_owner` which is usually an organization ID. For example: - For users the parent is an organization and the `parent_id` matches `resource_owner`. - For organizations the parent is an instance, but the `resource_owner` is the `org_id`. In this case the `parent_id` is the `instance_id`. - Applications would have a similar problem, where the parent is a project, but the `resource_owner` is the `org_id` # Additional Context Closes https://github.com/zitadel/zitadel/issues/9957 --- cmd/setup/57.go | 27 ++ cmd/setup/57.sql | 106 ++++++++ cmd/setup/config.go | 1 + cmd/setup/setup.go | 3 + cmd/setup/trigger_steps.go | 125 +++++++++ internal/domain/count_trigger.go | 9 + internal/domain/countparenttype_enumer.go | 109 ++++++++ internal/domain/secretgeneratortype_enumer.go | 93 +++++-- internal/migration/count_trigger.sql | 43 +++ .../delete_parent_counts_trigger.sql | 13 + internal/migration/migration.go | 5 +- internal/migration/trigger.go | 127 +++++++++ internal/migration/trigger_test.go | 253 ++++++++++++++++++ internal/query/resource_counts.go | 61 +++++ internal/query/resource_counts_list.sql | 12 + internal/query/resource_counts_test.go | 109 ++++++++ 16 files changed, 1080 insertions(+), 16 deletions(-) create mode 100644 cmd/setup/57.go create mode 100644 cmd/setup/57.sql create mode 100644 cmd/setup/trigger_steps.go create mode 100644 internal/domain/count_trigger.go create mode 100644 internal/domain/countparenttype_enumer.go create mode 100644 internal/migration/count_trigger.sql create mode 100644 internal/migration/delete_parent_counts_trigger.sql create mode 100644 internal/migration/trigger.go create mode 100644 internal/migration/trigger_test.go create mode 100644 internal/query/resource_counts.go create mode 100644 internal/query/resource_counts_list.sql create mode 100644 internal/query/resource_counts_test.go diff --git a/cmd/setup/57.go b/cmd/setup/57.go new file mode 100644 index 0000000000..4c52018f1e --- /dev/null +++ b/cmd/setup/57.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 57.sql + createResourceCounts string +) + +type CreateResourceCounts struct { + dbClient *database.DB +} + +func (mig *CreateResourceCounts) Execute(ctx context.Context, _ eventstore.Event) error { + _, err := mig.dbClient.ExecContext(ctx, createResourceCounts) + return err +} + +func (mig *CreateResourceCounts) String() string { + return "57_create_resource_counts" +} diff --git a/cmd/setup/57.sql b/cmd/setup/57.sql new file mode 100644 index 0000000000..f2f0a40202 --- /dev/null +++ b/cmd/setup/57.sql @@ -0,0 +1,106 @@ +CREATE TABLE IF NOT EXISTS projections.resource_counts +( + id SERIAL PRIMARY KEY, -- allows for easy pagination + instance_id TEXT NOT NULL, + table_name TEXT NOT NULL, -- needed for trigger matching, not in reports + parent_type TEXT NOT NULL, + parent_id TEXT NOT NULL, + resource_name TEXT NOT NULL, -- friendly name for reporting + updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), + amount INTEGER NOT NULL DEFAULT 1 CHECK (amount >= 0), + + UNIQUE (instance_id, parent_type, parent_id, table_name) +); + +-- count_resource is a trigger function which increases or decreases the count of a resource. +-- When creating the trigger the following required arguments (TG_ARGV) can be passed: +-- 1. The type of the parent +-- 2. The column name of the instance id +-- 3. The column name of the owner id +-- 4. The name of the resource +CREATE OR REPLACE FUNCTION projections.count_resource() + RETURNS trigger + LANGUAGE 'plpgsql' VOLATILE +AS $$ +DECLARE + -- trigger variables + tg_table_name TEXT := TG_TABLE_SCHEMA || '.' || TG_TABLE_NAME; + tg_parent_type TEXT := TG_ARGV[0]; + tg_instance_id_column TEXT := TG_ARGV[1]; + tg_parent_id_column TEXT := TG_ARGV[2]; + tg_resource_name TEXT := TG_ARGV[3]; + + tg_instance_id TEXT; + tg_parent_id TEXT; + + select_ids TEXT := format('SELECT ($1).%I, ($1).%I', tg_instance_id_column, tg_parent_id_column); +BEGIN + IF (TG_OP = 'INSERT') THEN + EXECUTE select_ids INTO tg_instance_id, tg_parent_id USING NEW; + + INSERT INTO projections.resource_counts(instance_id, table_name, parent_type, parent_id, resource_name) + VALUES (tg_instance_id, tg_table_name, tg_parent_type, tg_parent_id, tg_resource_name) + ON CONFLICT (instance_id, table_name, parent_type, parent_id) DO + UPDATE SET updated_at = now(), amount = projections.resource_counts.amount + 1; + + RETURN NEW; + ELSEIF (TG_OP = 'DELETE') THEN + EXECUTE select_ids INTO tg_instance_id, tg_parent_id USING OLD; + + UPDATE projections.resource_counts + SET updated_at = now(), amount = amount - 1 + WHERE instance_id = tg_instance_id + AND table_name = tg_table_name + AND parent_type = tg_parent_type + AND parent_id = tg_parent_id + AND resource_name = tg_resource_name + AND amount > 0; -- prevent check failure on negative amount. + + RETURN OLD; + END IF; +END +$$; + +-- delete_table_counts removes all resource counts for a TRUNCATED table. +CREATE OR REPLACE FUNCTION projections.delete_table_counts() + RETURNS trigger + LANGUAGE 'plpgsql' +AS $$ +DECLARE + -- trigger variables + tg_table_name TEXT := TG_TABLE_SCHEMA || '.' || TG_TABLE_NAME; +BEGIN + DELETE FROM projections.resource_counts + WHERE table_name = tg_table_name; +END +$$; + +-- delete_parent_counts removes all resource counts for a deleted parent. +-- 1. The type of the parent +-- 2. The column name of the instance id +-- 3. The column name of the owner id +CREATE OR REPLACE FUNCTION projections.delete_parent_counts() + RETURNS trigger + LANGUAGE 'plpgsql' +AS $$ +DECLARE + -- trigger variables + tg_parent_type TEXT := TG_ARGV[0]; + tg_instance_id_column TEXT := TG_ARGV[1]; + tg_parent_id_column TEXT := TG_ARGV[2]; + + tg_instance_id TEXT; + tg_parent_id TEXT; + + select_ids TEXT := format('SELECT ($1).%I, ($1).%I', tg_instance_id_column, tg_parent_id_column); +BEGIN + EXECUTE select_ids INTO tg_instance_id, tg_parent_id USING OLD; + + DELETE FROM projections.resource_counts + WHERE instance_id = tg_instance_id + AND parent_type = tg_parent_type + AND parent_id = tg_parent_id; + + RETURN OLD; +END +$$; diff --git a/cmd/setup/config.go b/cmd/setup/config.go index bd2abde9ea..dd59ba3f07 100644 --- a/cmd/setup/config.go +++ b/cmd/setup/config.go @@ -153,6 +153,7 @@ type Steps struct { s54InstancePositionIndex *InstancePositionIndex s55ExecutionHandlerStart *ExecutionHandlerStart s56IDPTemplate6SAMLFederatedLogout *IDPTemplate6SAMLFederatedLogout + s57CreateResourceCounts *CreateResourceCounts } func MustNewSteps(v *viper.Viper) *Steps { diff --git a/cmd/setup/setup.go b/cmd/setup/setup.go index c84976f282..1465180a6b 100644 --- a/cmd/setup/setup.go +++ b/cmd/setup/setup.go @@ -215,6 +215,7 @@ func Setup(ctx context.Context, config *Config, steps *Steps, masterKey string) steps.s54InstancePositionIndex = &InstancePositionIndex{dbClient: dbClient} steps.s55ExecutionHandlerStart = &ExecutionHandlerStart{dbClient: dbClient} steps.s56IDPTemplate6SAMLFederatedLogout = &IDPTemplate6SAMLFederatedLogout{dbClient: dbClient} + steps.s57CreateResourceCounts = &CreateResourceCounts{dbClient: dbClient} err = projection.Create(ctx, dbClient, eventstoreClient, config.Projections, nil, nil, nil) logging.OnError(err).Fatal("unable to start projections") @@ -260,6 +261,7 @@ func Setup(ctx context.Context, config *Config, steps *Steps, masterKey string) steps.s54InstancePositionIndex, steps.s55ExecutionHandlerStart, steps.s56IDPTemplate6SAMLFederatedLogout, + steps.s57CreateResourceCounts, } { setupErr = executeMigration(ctx, eventstoreClient, step, "migration failed") if setupErr != nil { @@ -296,6 +298,7 @@ func Setup(ctx context.Context, config *Config, steps *Steps, masterKey string) client: dbClient, }, } + repeatableSteps = append(repeatableSteps, triggerSteps(dbClient)...) for _, repeatableStep := range repeatableSteps { setupErr = executeMigration(ctx, eventstoreClient, repeatableStep, "unable to migrate repeatable step") diff --git a/cmd/setup/trigger_steps.go b/cmd/setup/trigger_steps.go new file mode 100644 index 0000000000..163a8fdb59 --- /dev/null +++ b/cmd/setup/trigger_steps.go @@ -0,0 +1,125 @@ +package setup + +import ( + "fmt" + + "github.com/zitadel/zitadel/internal/database" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/migration" + "github.com/zitadel/zitadel/internal/query/projection" +) + +// triggerSteps defines the repeatable migrations that set up triggers +// for counting resources in the database. +func triggerSteps(db *database.DB) []migration.RepeatableMigration { + return []migration.RepeatableMigration{ + // Delete parent count triggers for instances and organizations + migration.DeleteParentCountsTrigger(db, + projection.InstanceProjectionTable, + domain.CountParentTypeInstance, + projection.InstanceColumnID, + projection.InstanceColumnID, + "instance", + ), + migration.DeleteParentCountsTrigger(db, + projection.OrgProjectionTable, + domain.CountParentTypeOrganization, + projection.OrgColumnInstanceID, + projection.OrgColumnID, + "organization", + ), + + // Count triggers for all the resources + migration.CountTrigger(db, + projection.OrgProjectionTable, + domain.CountParentTypeInstance, + projection.OrgColumnInstanceID, + projection.OrgColumnInstanceID, + "organization", + ), + migration.CountTrigger(db, + projection.ProjectProjectionTable, + domain.CountParentTypeOrganization, + projection.ProjectColumnInstanceID, + projection.ProjectColumnResourceOwner, + "project", + ), + migration.CountTrigger(db, + projection.UserTable, + domain.CountParentTypeOrganization, + projection.UserInstanceIDCol, + projection.UserResourceOwnerCol, + "user", + ), + migration.CountTrigger(db, + projection.InstanceMemberProjectionTable, + domain.CountParentTypeInstance, + projection.MemberInstanceID, + projection.MemberResourceOwner, + "iam_admin", + ), + migration.CountTrigger(db, + projection.IDPTable, + domain.CountParentTypeInstance, + projection.IDPInstanceIDCol, + projection.IDPInstanceIDCol, + "identity_provider", + ), + migration.CountTrigger(db, + projection.IDPTemplateLDAPTable, + domain.CountParentTypeInstance, + projection.LDAPInstanceIDCol, + projection.LDAPInstanceIDCol, + "identity_provider_ldap", + ), + migration.CountTrigger(db, + projection.ActionTable, + domain.CountParentTypeInstance, + projection.ActionInstanceIDCol, + projection.ActionInstanceIDCol, + "action_v1", + ), + migration.CountTrigger(db, + projection.ExecutionTable, + domain.CountParentTypeInstance, + projection.ExecutionInstanceIDCol, + projection.ExecutionInstanceIDCol, + "execution", + ), + migration.CountTrigger(db, + fmt.Sprintf("%s_%s", projection.ExecutionTable, projection.ExecutionTargetSuffix), + domain.CountParentTypeInstance, + projection.ExecutionTargetInstanceIDCol, + projection.ExecutionTargetInstanceIDCol, + "execution_target", + ), + migration.CountTrigger(db, + projection.LoginPolicyTable, + domain.CountParentTypeInstance, + projection.LoginPolicyInstanceIDCol, + projection.LoginPolicyInstanceIDCol, + "login_policy", + ), + migration.CountTrigger(db, + projection.PasswordComplexityTable, + domain.CountParentTypeInstance, + projection.ComplexityPolicyInstanceIDCol, + projection.ComplexityPolicyInstanceIDCol, + "password_complexity_policy", + ), + migration.CountTrigger(db, + projection.PasswordAgeTable, + domain.CountParentTypeInstance, + projection.AgePolicyInstanceIDCol, + projection.AgePolicyInstanceIDCol, + "password_expiry_policy", + ), + migration.CountTrigger(db, + projection.LockoutPolicyTable, + domain.CountParentTypeInstance, + projection.LockoutPolicyInstanceIDCol, + projection.LockoutPolicyInstanceIDCol, + "lockout_policy", + ), + } +} diff --git a/internal/domain/count_trigger.go b/internal/domain/count_trigger.go new file mode 100644 index 0000000000..a29d125fe9 --- /dev/null +++ b/internal/domain/count_trigger.go @@ -0,0 +1,9 @@ +package domain + +//go:generate enumer -type CountParentType -transform lower -trimprefix CountParentType -sql +type CountParentType int + +const ( + CountParentTypeInstance CountParentType = iota + CountParentTypeOrganization +) diff --git a/internal/domain/countparenttype_enumer.go b/internal/domain/countparenttype_enumer.go new file mode 100644 index 0000000000..8691d97e62 --- /dev/null +++ b/internal/domain/countparenttype_enumer.go @@ -0,0 +1,109 @@ +// Code generated by "enumer -type CountParentType -transform lower -trimprefix CountParentType -sql"; DO NOT EDIT. + +package domain + +import ( + "database/sql/driver" + "fmt" + "strings" +) + +const _CountParentTypeName = "instanceorganization" + +var _CountParentTypeIndex = [...]uint8{0, 8, 20} + +const _CountParentTypeLowerName = "instanceorganization" + +func (i CountParentType) String() string { + if i < 0 || i >= CountParentType(len(_CountParentTypeIndex)-1) { + return fmt.Sprintf("CountParentType(%d)", i) + } + return _CountParentTypeName[_CountParentTypeIndex[i]:_CountParentTypeIndex[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 _CountParentTypeNoOp() { + var x [1]struct{} + _ = x[CountParentTypeInstance-(0)] + _ = x[CountParentTypeOrganization-(1)] +} + +var _CountParentTypeValues = []CountParentType{CountParentTypeInstance, CountParentTypeOrganization} + +var _CountParentTypeNameToValueMap = map[string]CountParentType{ + _CountParentTypeName[0:8]: CountParentTypeInstance, + _CountParentTypeLowerName[0:8]: CountParentTypeInstance, + _CountParentTypeName[8:20]: CountParentTypeOrganization, + _CountParentTypeLowerName[8:20]: CountParentTypeOrganization, +} + +var _CountParentTypeNames = []string{ + _CountParentTypeName[0:8], + _CountParentTypeName[8:20], +} + +// CountParentTypeString retrieves an enum value from the enum constants string name. +// Throws an error if the param is not part of the enum. +func CountParentTypeString(s string) (CountParentType, error) { + if val, ok := _CountParentTypeNameToValueMap[s]; ok { + return val, nil + } + + if val, ok := _CountParentTypeNameToValueMap[strings.ToLower(s)]; ok { + return val, nil + } + return 0, fmt.Errorf("%s does not belong to CountParentType values", s) +} + +// CountParentTypeValues returns all values of the enum +func CountParentTypeValues() []CountParentType { + return _CountParentTypeValues +} + +// CountParentTypeStrings returns a slice of all String values of the enum +func CountParentTypeStrings() []string { + strs := make([]string, len(_CountParentTypeNames)) + copy(strs, _CountParentTypeNames) + return strs +} + +// IsACountParentType returns "true" if the value is listed in the enum definition. "false" otherwise +func (i CountParentType) IsACountParentType() bool { + for _, v := range _CountParentTypeValues { + if i == v { + return true + } + } + return false +} + +func (i CountParentType) Value() (driver.Value, error) { + return i.String(), nil +} + +func (i *CountParentType) Scan(value interface{}) error { + if value == nil { + return nil + } + + var str string + switch v := value.(type) { + case []byte: + str = string(v) + case string: + str = v + case fmt.Stringer: + str = v.String() + default: + return fmt.Errorf("invalid value of CountParentType: %[1]T(%[1]v)", value) + } + + val, err := CountParentTypeString(str) + if err != nil { + return err + } + + *i = val + return nil +} diff --git a/internal/domain/secretgeneratortype_enumer.go b/internal/domain/secretgeneratortype_enumer.go index f819bafc1f..db66715670 100644 --- a/internal/domain/secretgeneratortype_enumer.go +++ b/internal/domain/secretgeneratortype_enumer.go @@ -4,11 +4,14 @@ package domain import ( "fmt" + "strings" ) -const _SecretGeneratorTypeName = "unspecifiedinit_codeverify_email_codeverify_phone_codeverify_domainpassword_reset_codepasswordless_init_codeapp_secretotpsmsotp_emailinvite_codesecret_generator_type_count" +const _SecretGeneratorTypeName = "unspecifiedinit_codeverify_email_codeverify_phone_codeverify_domainpassword_reset_codepasswordless_init_codeapp_secretotpsmsotp_emailinvite_codesigning_keysecret_generator_type_count" -var _SecretGeneratorTypeIndex = [...]uint8{0, 11, 20, 37, 54, 67, 86, 108, 118, 124, 133, 144, 171} +var _SecretGeneratorTypeIndex = [...]uint8{0, 11, 20, 37, 54, 67, 86, 108, 118, 124, 133, 144, 155, 182} + +const _SecretGeneratorTypeLowerName = "unspecifiedinit_codeverify_email_codeverify_phone_codeverify_domainpassword_reset_codepasswordless_init_codeapp_secretotpsmsotp_emailinvite_codesigning_keysecret_generator_type_count" func (i SecretGeneratorType) String() string { if i < 0 || i >= SecretGeneratorType(len(_SecretGeneratorTypeIndex)-1) { @@ -17,21 +20,70 @@ func (i SecretGeneratorType) String() string { return _SecretGeneratorTypeName[_SecretGeneratorTypeIndex[i]:_SecretGeneratorTypeIndex[i+1]] } -var _SecretGeneratorTypeValues = []SecretGeneratorType{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11} +// An "invalid array index" compiler error signifies that the constant values have changed. +// Re-run the stringer command to generate them again. +func _SecretGeneratorTypeNoOp() { + var x [1]struct{} + _ = x[SecretGeneratorTypeUnspecified-(0)] + _ = x[SecretGeneratorTypeInitCode-(1)] + _ = x[SecretGeneratorTypeVerifyEmailCode-(2)] + _ = x[SecretGeneratorTypeVerifyPhoneCode-(3)] + _ = x[SecretGeneratorTypeVerifyDomain-(4)] + _ = x[SecretGeneratorTypePasswordResetCode-(5)] + _ = x[SecretGeneratorTypePasswordlessInitCode-(6)] + _ = x[SecretGeneratorTypeAppSecret-(7)] + _ = x[SecretGeneratorTypeOTPSMS-(8)] + _ = x[SecretGeneratorTypeOTPEmail-(9)] + _ = x[SecretGeneratorTypeInviteCode-(10)] + _ = x[SecretGeneratorTypeSigningKey-(11)] + _ = x[secretGeneratorTypeCount-(12)] +} + +var _SecretGeneratorTypeValues = []SecretGeneratorType{SecretGeneratorTypeUnspecified, SecretGeneratorTypeInitCode, SecretGeneratorTypeVerifyEmailCode, SecretGeneratorTypeVerifyPhoneCode, SecretGeneratorTypeVerifyDomain, SecretGeneratorTypePasswordResetCode, SecretGeneratorTypePasswordlessInitCode, SecretGeneratorTypeAppSecret, SecretGeneratorTypeOTPSMS, SecretGeneratorTypeOTPEmail, SecretGeneratorTypeInviteCode, SecretGeneratorTypeSigningKey, secretGeneratorTypeCount} var _SecretGeneratorTypeNameToValueMap = map[string]SecretGeneratorType{ - _SecretGeneratorTypeName[0:11]: 0, - _SecretGeneratorTypeName[11:20]: 1, - _SecretGeneratorTypeName[20:37]: 2, - _SecretGeneratorTypeName[37:54]: 3, - _SecretGeneratorTypeName[54:67]: 4, - _SecretGeneratorTypeName[67:86]: 5, - _SecretGeneratorTypeName[86:108]: 6, - _SecretGeneratorTypeName[108:118]: 7, - _SecretGeneratorTypeName[118:124]: 8, - _SecretGeneratorTypeName[124:133]: 9, - _SecretGeneratorTypeName[133:144]: 10, - _SecretGeneratorTypeName[144:171]: 11, + _SecretGeneratorTypeName[0:11]: SecretGeneratorTypeUnspecified, + _SecretGeneratorTypeLowerName[0:11]: SecretGeneratorTypeUnspecified, + _SecretGeneratorTypeName[11:20]: SecretGeneratorTypeInitCode, + _SecretGeneratorTypeLowerName[11:20]: SecretGeneratorTypeInitCode, + _SecretGeneratorTypeName[20:37]: SecretGeneratorTypeVerifyEmailCode, + _SecretGeneratorTypeLowerName[20:37]: SecretGeneratorTypeVerifyEmailCode, + _SecretGeneratorTypeName[37:54]: SecretGeneratorTypeVerifyPhoneCode, + _SecretGeneratorTypeLowerName[37:54]: SecretGeneratorTypeVerifyPhoneCode, + _SecretGeneratorTypeName[54:67]: SecretGeneratorTypeVerifyDomain, + _SecretGeneratorTypeLowerName[54:67]: SecretGeneratorTypeVerifyDomain, + _SecretGeneratorTypeName[67:86]: SecretGeneratorTypePasswordResetCode, + _SecretGeneratorTypeLowerName[67:86]: SecretGeneratorTypePasswordResetCode, + _SecretGeneratorTypeName[86:108]: SecretGeneratorTypePasswordlessInitCode, + _SecretGeneratorTypeLowerName[86:108]: SecretGeneratorTypePasswordlessInitCode, + _SecretGeneratorTypeName[108:118]: SecretGeneratorTypeAppSecret, + _SecretGeneratorTypeLowerName[108:118]: SecretGeneratorTypeAppSecret, + _SecretGeneratorTypeName[118:124]: SecretGeneratorTypeOTPSMS, + _SecretGeneratorTypeLowerName[118:124]: SecretGeneratorTypeOTPSMS, + _SecretGeneratorTypeName[124:133]: SecretGeneratorTypeOTPEmail, + _SecretGeneratorTypeLowerName[124:133]: SecretGeneratorTypeOTPEmail, + _SecretGeneratorTypeName[133:144]: SecretGeneratorTypeInviteCode, + _SecretGeneratorTypeLowerName[133:144]: SecretGeneratorTypeInviteCode, + _SecretGeneratorTypeName[144:155]: SecretGeneratorTypeSigningKey, + _SecretGeneratorTypeLowerName[144:155]: SecretGeneratorTypeSigningKey, + _SecretGeneratorTypeName[155:182]: secretGeneratorTypeCount, + _SecretGeneratorTypeLowerName[155:182]: secretGeneratorTypeCount, +} + +var _SecretGeneratorTypeNames = []string{ + _SecretGeneratorTypeName[0:11], + _SecretGeneratorTypeName[11:20], + _SecretGeneratorTypeName[20:37], + _SecretGeneratorTypeName[37:54], + _SecretGeneratorTypeName[54:67], + _SecretGeneratorTypeName[67:86], + _SecretGeneratorTypeName[86:108], + _SecretGeneratorTypeName[108:118], + _SecretGeneratorTypeName[118:124], + _SecretGeneratorTypeName[124:133], + _SecretGeneratorTypeName[133:144], + _SecretGeneratorTypeName[144:155], + _SecretGeneratorTypeName[155:182], } // SecretGeneratorTypeString retrieves an enum value from the enum constants string name. @@ -40,6 +92,10 @@ func SecretGeneratorTypeString(s string) (SecretGeneratorType, error) { if val, ok := _SecretGeneratorTypeNameToValueMap[s]; ok { return val, nil } + + if val, ok := _SecretGeneratorTypeNameToValueMap[strings.ToLower(s)]; ok { + return val, nil + } return 0, fmt.Errorf("%s does not belong to SecretGeneratorType values", s) } @@ -48,6 +104,13 @@ func SecretGeneratorTypeValues() []SecretGeneratorType { return _SecretGeneratorTypeValues } +// SecretGeneratorTypeStrings returns a slice of all String values of the enum +func SecretGeneratorTypeStrings() []string { + strs := make([]string, len(_SecretGeneratorTypeNames)) + copy(strs, _SecretGeneratorTypeNames) + return strs +} + // IsASecretGeneratorType returns "true" if the value is listed in the enum definition. "false" otherwise func (i SecretGeneratorType) IsASecretGeneratorType() bool { for _, v := range _SecretGeneratorTypeValues { diff --git a/internal/migration/count_trigger.sql b/internal/migration/count_trigger.sql new file mode 100644 index 0000000000..4b521094ab --- /dev/null +++ b/internal/migration/count_trigger.sql @@ -0,0 +1,43 @@ +{{ define "count_trigger" -}} +CREATE OR REPLACE TRIGGER count_{{ .Resource }} + AFTER INSERT OR DELETE + ON {{ .Table }} + FOR EACH ROW + EXECUTE FUNCTION projections.count_resource( + '{{ .ParentType }}', + '{{ .InstanceIDColumn }}', + '{{ .ParentIDColumn }}', + '{{ .Resource }}' + ); + +CREATE OR REPLACE TRIGGER truncate_{{ .Resource }}_counts + AFTER TRUNCATE + ON {{ .Table }} + FOR EACH STATEMENT + EXECUTE FUNCTION projections.delete_table_counts(); + +-- Prevent inserts and deletes while we populate the counts. +LOCK TABLE {{ .Table }} IN SHARE MODE; + +-- Populate the resource counts for the existing data in the table. +INSERT INTO projections.resource_counts( + instance_id, + table_name, + parent_type, + parent_id, + resource_name, + amount +) +SELECT + {{ .InstanceIDColumn }}, + '{{ .Table }}', + '{{ .ParentType }}', + {{ .ParentIDColumn }}, + '{{ .Resource }}', + COUNT(*) AS amount +FROM {{ .Table }} +GROUP BY ({{ .InstanceIDColumn }}, {{ .ParentIDColumn }}) +ON CONFLICT (instance_id, table_name, parent_type, parent_id) DO +UPDATE SET updated_at = now(), amount = EXCLUDED.amount; + +{{- end -}} diff --git a/internal/migration/delete_parent_counts_trigger.sql b/internal/migration/delete_parent_counts_trigger.sql new file mode 100644 index 0000000000..a2e9df6626 --- /dev/null +++ b/internal/migration/delete_parent_counts_trigger.sql @@ -0,0 +1,13 @@ +{{ define "delete_parent_counts_trigger" -}} + +CREATE OR REPLACE TRIGGER delete_parent_counts_trigger + AFTER DELETE + ON {{ .Table }} + FOR EACH ROW + EXECUTE FUNCTION projections.delete_parent_counts( + '{{ .ParentType }}', + '{{ .InstanceIDColumn }}', + '{{ .ParentIDColumn }}' + ); + +{{- end -}} diff --git a/internal/migration/migration.go b/internal/migration/migration.go index a2224340a7..3aeb2f0612 100644 --- a/internal/migration/migration.go +++ b/internal/migration/migration.go @@ -36,7 +36,10 @@ type errCheckerMigration interface { type RepeatableMigration interface { Migration - Check(lastRun map[string]interface{}) bool + + // Check if the migration should be executed again. + // True will repeat the migration, false will not. + Check(lastRun map[string]any) bool } func Migrate(ctx context.Context, es *eventstore.Eventstore, migration Migration) (err error) { diff --git a/internal/migration/trigger.go b/internal/migration/trigger.go new file mode 100644 index 0000000000..bd06afd5c5 --- /dev/null +++ b/internal/migration/trigger.go @@ -0,0 +1,127 @@ +package migration + +import ( + "context" + "embed" + "fmt" + "strings" + "text/template" + + "github.com/mitchellh/mapstructure" + + "github.com/zitadel/zitadel/internal/database" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/eventstore" +) + +const ( + countTriggerTmpl = "count_trigger" + deleteParentCountsTmpl = "delete_parent_counts_trigger" +) + +var ( + //go:embed *.sql + templateFS embed.FS + templates = template.Must(template.ParseFS(templateFS, "*.sql")) +) + +// CountTrigger registers the existing projections.count_trigger function. +// The trigger than takes care of keeping count of existing +// rows in the source table. +// It also pre-populates the projections.resource_counts table with +// the counts for the given table. +// +// During the population of the resource_counts table, +// the source table is share-locked to prevent concurrent modifications. +// Projection handlers will be halted until the lock is released. +// SELECT statements are not blocked by the lock. +// +// This migration repeats when any of the arguments are changed, +// such as renaming of a projection table. +func CountTrigger( + db *database.DB, + table string, + parentType domain.CountParentType, + instanceIDColumn string, + parentIDColumn string, + resource string, +) RepeatableMigration { + return &triggerMigration{ + triggerConfig: triggerConfig{ + Table: table, + ParentType: parentType.String(), + InstanceIDColumn: instanceIDColumn, + ParentIDColumn: parentIDColumn, + Resource: resource, + }, + db: db, + templateName: countTriggerTmpl, + } +} + +// DeleteParentCountsTrigger +// +// This migration repeats when any of the arguments are changed, +// such as renaming of a projection table. +func DeleteParentCountsTrigger( + db *database.DB, + table string, + parentType domain.CountParentType, + instanceIDColumn string, + parentIDColumn string, + resource string, +) RepeatableMigration { + return &triggerMigration{ + triggerConfig: triggerConfig{ + Table: table, + ParentType: parentType.String(), + InstanceIDColumn: instanceIDColumn, + ParentIDColumn: parentIDColumn, + Resource: resource, + }, + db: db, + templateName: deleteParentCountsTmpl, + } +} + +type triggerMigration struct { + triggerConfig + db *database.DB + templateName string +} + +// String implements [Migration] and [fmt.Stringer]. +func (m *triggerMigration) String() string { + return fmt.Sprintf("repeatable_%s_%s", m.Resource, m.templateName) +} + +// Execute implements [Migration] +func (m *triggerMigration) Execute(ctx context.Context, _ eventstore.Event) error { + var query strings.Builder + err := templates.ExecuteTemplate(&query, m.templateName, m.triggerConfig) + if err != nil { + return fmt.Errorf("%s: execute trigger template: %w", m, err) + } + _, err = m.db.ExecContext(ctx, query.String()) + if err != nil { + return fmt.Errorf("%s: exec trigger query: %w", m, err) + } + return nil +} + +type triggerConfig struct { + Table string `json:"table,omitempty" mapstructure:"table"` + ParentType string `json:"parent_type,omitempty" mapstructure:"parent_type"` + InstanceIDColumn string `json:"instance_id_column,omitempty" mapstructure:"instance_id_column"` + ParentIDColumn string `json:"parent_id_column,omitempty" mapstructure:"parent_id_column"` + Resource string `json:"resource,omitempty" mapstructure:"resource"` +} + +// Check implements [RepeatableMigration]. +func (c *triggerConfig) Check(lastRun map[string]any) bool { + var dst triggerConfig + if err := mapstructure.Decode(lastRun, &dst); err != nil { + panic(err) + } + return dst != *c +} diff --git a/internal/migration/trigger_test.go b/internal/migration/trigger_test.go new file mode 100644 index 0000000000..5799526428 --- /dev/null +++ b/internal/migration/trigger_test.go @@ -0,0 +1,253 @@ +package migration + +import ( + "context" + "regexp" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/zitadel/zitadel/internal/database" +) + +const ( + expCountTriggerQuery = `CREATE OR REPLACE TRIGGER count_resource + AFTER INSERT OR DELETE + ON table + FOR EACH ROW + EXECUTE FUNCTION projections.count_resource( + 'instance', + 'instance_id', + 'parent_id', + 'resource' + ); + +CREATE OR REPLACE TRIGGER truncate_resource_counts + AFTER TRUNCATE + ON table + FOR EACH STATEMENT + EXECUTE FUNCTION projections.delete_table_counts(); + +-- Prevent inserts and deletes while we populate the counts. +LOCK TABLE table IN SHARE MODE; + +-- Populate the resource counts for the existing data in the table. +INSERT INTO projections.resource_counts( + instance_id, + table_name, + parent_type, + parent_id, + resource_name, + amount +) +SELECT + instance_id, + 'table', + 'instance', + parent_id, + 'resource', + COUNT(*) AS amount +FROM table +GROUP BY (instance_id, parent_id) +ON CONFLICT (instance_id, table_name, parent_type, parent_id) DO +UPDATE SET updated_at = now(), amount = EXCLUDED.amount;` + + expDeleteParentCountsQuery = `CREATE OR REPLACE TRIGGER delete_parent_counts_trigger + AFTER DELETE + ON table + FOR EACH ROW + EXECUTE FUNCTION projections.delete_parent_counts( + 'instance', + 'instance_id', + 'parent_id' + );` +) + +func Test_triggerMigration_Execute(t *testing.T) { + type fields struct { + triggerConfig triggerConfig + templateName string + } + tests := []struct { + name string + fields fields + expects func(sqlmock.Sqlmock) + wantErr bool + }{ + { + name: "template error", + fields: fields{ + triggerConfig: triggerConfig{ + Table: "table", + ParentType: "instance", + InstanceIDColumn: "instance_id", + ParentIDColumn: "parent_id", + Resource: "resource", + }, + templateName: "foo", + }, + expects: func(_ sqlmock.Sqlmock) {}, + wantErr: true, + }, + { + name: "db error", + fields: fields{ + triggerConfig: triggerConfig{ + Table: "table", + ParentType: "instance", + InstanceIDColumn: "instance_id", + ParentIDColumn: "parent_id", + Resource: "resource", + }, + templateName: countTriggerTmpl, + }, + expects: func(mock sqlmock.Sqlmock) { + mock.ExpectExec(regexp.QuoteMeta(expCountTriggerQuery)). + WillReturnError(assert.AnError) + }, + wantErr: true, + }, + { + name: "count trigger", + fields: fields{ + triggerConfig: triggerConfig{ + Table: "table", + ParentType: "instance", + InstanceIDColumn: "instance_id", + ParentIDColumn: "parent_id", + Resource: "resource", + }, + templateName: countTriggerTmpl, + }, + expects: func(mock sqlmock.Sqlmock) { + mock.ExpectExec(regexp.QuoteMeta(expCountTriggerQuery)). + WithoutArgs(). + WillReturnResult( + sqlmock.NewResult(1, 1), + ) + }, + }, + { + name: "count trigger", + fields: fields{ + triggerConfig: triggerConfig{ + Table: "table", + ParentType: "instance", + InstanceIDColumn: "instance_id", + ParentIDColumn: "parent_id", + Resource: "resource", + }, + templateName: deleteParentCountsTmpl, + }, + expects: func(mock sqlmock.Sqlmock) { + mock.ExpectExec(regexp.QuoteMeta(expDeleteParentCountsQuery)). + WithoutArgs(). + WillReturnResult( + sqlmock.NewResult(1, 1), + ) + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + db, mock, err := sqlmock.New() + require.NoError(t, err) + defer func() { + err := mock.ExpectationsWereMet() + require.NoError(t, err) + }() + defer db.Close() + tt.expects(mock) + mock.ExpectClose() + + m := &triggerMigration{ + db: &database.DB{ + DB: db, + }, + triggerConfig: tt.fields.triggerConfig, + templateName: tt.fields.templateName, + } + err = m.Execute(context.Background(), nil) + if tt.wantErr { + assert.Error(t, err) + return + } + require.NoError(t, err) + }) + } +} + +func Test_triggerConfig_Check(t *testing.T) { + type fields struct { + Table string + ParentType string + InstanceIDColumn string + ParentIDColumn string + Resource string + } + type args struct { + lastRun map[string]any + } + tests := []struct { + name string + fields fields + args args + want bool + }{ + { + name: "should", + fields: fields{ + Table: "users2", + ParentType: "instance", + InstanceIDColumn: "instance_id", + ParentIDColumn: "parent_id", + Resource: "user", + }, + args: args{ + lastRun: map[string]any{ + "table": "users1", + "parent_type": "instance", + "instance_id_column": "instance_id", + "parent_id_column": "parent_id", + "resource": "user", + }, + }, + want: true, + }, + { + name: "should not", + fields: fields{ + Table: "users1", + ParentType: "instance", + InstanceIDColumn: "instance_id", + ParentIDColumn: "parent_id", + Resource: "user", + }, + args: args{ + lastRun: map[string]any{ + "table": "users1", + "parent_type": "instance", + "instance_id_column": "instance_id", + "parent_id_column": "parent_id", + "resource": "user", + }, + }, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &triggerConfig{ + Table: tt.fields.Table, + ParentType: tt.fields.ParentType, + InstanceIDColumn: tt.fields.InstanceIDColumn, + ParentIDColumn: tt.fields.ParentIDColumn, + Resource: tt.fields.Resource, + } + got := c.Check(tt.args.lastRun) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/internal/query/resource_counts.go b/internal/query/resource_counts.go new file mode 100644 index 0000000000..9d486e0b90 --- /dev/null +++ b/internal/query/resource_counts.go @@ -0,0 +1,61 @@ +package query + +import ( + "context" + "database/sql" + _ "embed" + "time" + + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/telemetry/tracing" + "github.com/zitadel/zitadel/internal/zerrors" +) + +var ( + //go:embed resource_counts_list.sql + resourceCountsListQuery string +) + +type ResourceCount struct { + ID int // Primary key, used for pagination + InstanceID string + TableName string + ParentType domain.CountParentType + ParentID string + Resource string + UpdatedAt time.Time + Amount int +} + +// ListResourceCounts retrieves all resource counts. +// It supports pagination using lastID and limit parameters. +// +// TODO: Currently only a proof of concept, filters may be implemented later if required. +func (q *Queries) ListResourceCounts(ctx context.Context, lastID, limit int) (result []ResourceCount, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + err = q.client.QueryContext(ctx, func(rows *sql.Rows) error { + for rows.Next() { + var count ResourceCount + err := rows.Scan( + &count.ID, + &count.InstanceID, + &count.TableName, + &count.ParentType, + &count.ParentID, + &count.Resource, + &count.UpdatedAt, + &count.Amount) + if err != nil { + return zerrors.ThrowInternal(err, "QUERY-2f4g5", "Errors.Internal") + } + result = append(result, count) + } + return nil + }, resourceCountsListQuery, lastID, limit) + if err != nil { + return nil, zerrors.ThrowInternal(err, "QUERY-3f4g5", "Errors.Internal") + } + return result, nil +} diff --git a/internal/query/resource_counts_list.sql b/internal/query/resource_counts_list.sql new file mode 100644 index 0000000000..0d4abf87eb --- /dev/null +++ b/internal/query/resource_counts_list.sql @@ -0,0 +1,12 @@ +SELECT id, + instance_id, + table_name, + parent_type, + parent_id, + resource_name, + updated_at, + amount +FROM projections.resource_counts +WHERE id > $1 +ORDER BY id +LIMIT $2; diff --git a/internal/query/resource_counts_test.go b/internal/query/resource_counts_test.go new file mode 100644 index 0000000000..2829a660ef --- /dev/null +++ b/internal/query/resource_counts_test.go @@ -0,0 +1,109 @@ +package query + +import ( + "context" + _ "embed" + "regexp" + "testing" + "time" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/zitadel/zitadel/internal/database" + "github.com/zitadel/zitadel/internal/domain" +) + +func TestQueries_ListResourceCounts(t *testing.T) { + columns := []string{"id", "instance_id", "table_name", "parent_type", "parent_id", "resource_name", "updated_at", "amount"} + type args struct { + lastID int + limit int + } + tests := []struct { + name string + args args + expects func(sqlmock.Sqlmock) + wantResult []ResourceCount + wantErr bool + }{ + { + name: "query error", + args: args{ + lastID: 0, + limit: 10, + }, + expects: func(mock sqlmock.Sqlmock) { + mock.ExpectQuery(regexp.QuoteMeta(resourceCountsListQuery)). + WithArgs(0, 10). + WillReturnError(assert.AnError) + }, + wantErr: true, + }, + { + name: "success", + args: args{ + lastID: 0, + limit: 10, + }, + expects: func(mock sqlmock.Sqlmock) { + mock.ExpectQuery(regexp.QuoteMeta(resourceCountsListQuery)). + WithArgs(0, 10). + WillReturnRows( + sqlmock.NewRows(columns). + AddRow(1, "instance_1", "table", "instance", "parent_1", "resource_name", time.Unix(1, 2), 5). + AddRow(2, "instance_2", "table", "instance", "parent_2", "resource_name", time.Unix(1, 2), 6), + ) + }, + wantResult: []ResourceCount{ + { + ID: 1, + InstanceID: "instance_1", + TableName: "table", + ParentType: domain.CountParentTypeInstance, + ParentID: "parent_1", + Resource: "resource_name", + UpdatedAt: time.Unix(1, 2), + Amount: 5, + }, + { + ID: 2, + InstanceID: "instance_2", + TableName: "table", + ParentType: domain.CountParentTypeInstance, + ParentID: "parent_2", + Resource: "resource_name", + UpdatedAt: time.Unix(1, 2), + Amount: 6, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + db, mock, err := sqlmock.New() + require.NoError(t, err) + defer func() { + err := mock.ExpectationsWereMet() + require.NoError(t, err) + }() + defer db.Close() + tt.expects(mock) + mock.ExpectClose() + q := &Queries{ + client: &database.DB{ + DB: db, + }, + } + + gotResult, err := q.ListResourceCounts(context.Background(), tt.args.lastID, tt.args.limit) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + assert.Equal(t, tt.wantResult, gotResult, "ListResourceCounts() result mismatch") + }) + } +} From 1e5ffd41c9a624df49d2f743ca199330b9463c91 Mon Sep 17 00:00:00 2001 From: Silvan <27845747+adlerhurst@users.noreply.github.com> Date: Tue, 3 Jun 2025 17:18:16 +0200 Subject: [PATCH 61/76] docs(10016): improve understanding of output (#10014) # Which Problems Are Solved The output of the sql statement of tech advisory was unclear on how the data should be compared # How the Problems Are Solved An additional column is added to the output to show the effective difference of the old and new position. --- docs/docs/support/advisory/a10016.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/docs/support/advisory/a10016.md b/docs/docs/support/advisory/a10016.md index 38d73e6078..6272eaa81d 100644 --- a/docs/docs/support/advisory/a10016.md +++ b/docs/docs/support/advisory/a10016.md @@ -87,12 +87,13 @@ select b.aggregate_type, b.sequence, b.old_position, - b.new_position + b.new_position, + b.old_position - b.new_position difference 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: +If the output from the above looks reasonable, for example not a huge number in the `difference` column, commit the transaction: ```sql commit; From e2a61a60029783f9a29bf7b71f2ac3d8fd39bb78 Mon Sep 17 00:00:00 2001 From: Livio Spring Date: Wed, 4 Jun 2025 08:41:10 +0200 Subject: [PATCH 62/76] docs(api): remove unreleased services from api reference (#10015) # Which Problems Are Solved As we migrate resources to the new API, whenever a an implementation got merged, the API reference was added to the docs sidenav. As these new services and their implementation are not yet released, it can be confusing for developers as the corresponding endpoints return 404 or unimplemented errors. # How the Problems Are Solved Currently we just remove it from the sidenav and will add it once they're released. We're looking into a proper solution for the API references. # Additional Changes None # Additional Context None --- docs/sidebars.js | 41 ----------------------------------------- 1 file changed, 41 deletions(-) diff --git a/docs/sidebars.js b/docs/sidebars.js index 1bd53ed1b3..d7ebb80f5b 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -805,18 +805,6 @@ module.exports = { }, items: sidebar_api_org_service_v2, }, - { - type: "category", - label: "Organization (Beta)", - link: { - type: "generated-index", - title: "Organization Service beta API", - slug: "/apis/resources/org_service/v2beta", - description: - "This API is intended to manage organizations for ZITADEL. \n", - }, - items: sidebar_api_org_service_v2beta, - }, { type: "category", label: "Identity Provider", @@ -868,35 +856,6 @@ module.exports = { }, items: sidebar_api_actions_v2, }, - { - type: "category", - label: "Project (Beta)", - link: { - type: "generated-index", - title: "Project Service API (Beta)", - slug: "/apis/resources/project_service_v2", - description: - "This API is intended to manage projects and subresources for ZITADEL. \n"+ - "\n" + - "This service is in beta state. It can AND will continue breaking until a stable version is released.", - }, - items: sidebar_api_project_service_v2, - label: "Instance (Beta)", - link: { - type: "generated-index", - title: "Instance Service API (Beta)", - slug: "/apis/resources/instance_service_v2", - description: - "This API is intended to manage instances, custom domains and trusted domains in ZITADEL.\n" + - "\n" + - "This service is in beta state. It can AND will continue breaking until a stable version is released.\n"+ - "\n" + - "This v2 of the API provides the same functionalities as the v1, but organised on a per resource basis.\n" + - "The whole functionality related to domains (custom and trusted) has been moved under this instance API." - , - }, - items: sidebar_api_instance_service_v2, - }, ], }, { From 8fc11a7366dcaf24a11d3c4fd26e86f5e61d4d1f Mon Sep 17 00:00:00 2001 From: Elio Bischof Date: Wed, 4 Jun 2025 09:17:23 +0200 Subject: [PATCH 63/76] feat: user api requests to resource API (#9794) # Which Problems Are Solved This pull request addresses a significant gap in the user service v2 API, which currently lacks methods for managing machine users. # How the Problems Are Solved This PR adds new API endpoints to the user service v2 to manage machine users including their secret, keys and personal access tokens. Additionally, there's now a CreateUser and UpdateUser endpoints which allow to create either a human or machine user and update them. The existing `CreateHumanUser` endpoint has been deprecated along the corresponding management service endpoints. For details check the additional context section. # Additional Context - Closes https://github.com/zitadel/zitadel/issues/9349 ## More details - API changes: https://github.com/zitadel/zitadel/pull/9680 - Implementation: https://github.com/zitadel/zitadel/pull/9763 - Tests: https://github.com/zitadel/zitadel/pull/9771 ## Follow-ups - Metadata: support managing user metadata using resource API https://github.com/zitadel/zitadel/pull/10005 - Machine token type: support managing the machine token type (migrate to new enum with zero value unspecified?) --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Livio Spring --- API_DESIGN.md | 4 +- cmd/start/start.go | 2 +- go.mod | 1 + internal/api/grpc/admin/import.go | 2 +- internal/api/grpc/filter/v2/converter.go | 50 + .../grpc/management/project_application.go | 2 +- internal/api/grpc/management/user.go | 13 +- .../project/v2beta/integration/query_test.go | 148 +- internal/api/grpc/project/v2beta/query.go | 23 +- internal/api/grpc/user/v2/human.go | 187 ++ internal/api/grpc/user/v2/human_test.go | 254 +++ .../user/v2/integration_test/email_test.go | 4 +- .../grpc/user/v2/integration_test/key_test.go | 659 +++++++ .../user/v2/integration_test/password_test.go | 2 +- .../grpc/user/v2/integration_test/pat_test.go | 615 ++++++ .../user/v2/integration_test/phone_test.go | 4 +- .../user/v2/integration_test/secret_test.go | 347 ++++ .../user/v2/integration_test/user_test.go | 1711 ++++++++++++++++- internal/api/grpc/user/v2/key.go | 62 + internal/api/grpc/user/v2/key_query.go | 124 ++ internal/api/grpc/user/v2/machine.go | 58 + internal/api/grpc/user/v2/machine_test.go | 62 + internal/api/grpc/user/v2/pat.go | 56 + internal/api/grpc/user/v2/pat_query.go | 123 ++ internal/api/grpc/user/v2/secret.go | 39 + internal/api/grpc/user/v2/server.go | 16 +- internal/api/grpc/user/v2/user.go | 105 +- .../grpc/user/v2/{query.go => user_query.go} | 0 internal/api/scim/resources/user.go | 1 - internal/command/instance_member.go | 2 +- internal/command/org_member.go | 2 +- ..._ower_model.go => resource_owner_model.go} | 0 internal/command/user.go | 24 +- internal/command/user_machine.go | 48 +- internal/command/user_machine_key.go | 24 +- internal/command/user_machine_model.go | 13 +- internal/command/user_machine_secret.go | 22 +- internal/command/user_machine_secret_test.go | 8 +- internal/command/user_machine_test.go | 238 ++- .../command/user_personal_access_token.go | 20 +- internal/command/user_test.go | 2 +- internal/command/user_v2.go | 2 - internal/command/user_v2_human.go | 64 +- internal/command/user_v2_human_test.go | 8 +- internal/command/user_v2_invite_test.go | 1 + internal/command/user_v2_machine.go | 94 + internal/command/user_v2_machine_test.go | 260 +++ internal/command/user_v2_model.go | 8 + internal/domain/permission.go | 2 +- internal/eventstore/write_model.go | 26 +- internal/integration/client.go | 40 + internal/query/authn_key.go | 146 +- internal/query/authn_key_test.go | 8 + internal/query/projection/authn_key.go | 3 + .../projection/user_personal_access_token.go | 2 + internal/query/user.go | 14 +- internal/query/user_personal_access_token.go | 60 +- internal/repository/user/machine.go | 7 +- 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 + proto/buf.yaml | 2 +- proto/zitadel/filter/v2/filter.proto | 96 + proto/zitadel/filter/v2beta/filter.proto | 36 +- proto/zitadel/management.proto | 201 +- proto/zitadel/project/v2beta/query.proto | 66 +- proto/zitadel/user/v2/email.proto | 2 +- proto/zitadel/user/v2/key.proto | 69 + proto/zitadel/user/v2/pat.proto | 70 + proto/zitadel/user/v2/user_service.proto | 1186 +++++++++++- 86 files changed, 7033 insertions(+), 536 deletions(-) create mode 100644 internal/api/grpc/filter/v2/converter.go create mode 100644 internal/api/grpc/user/v2/human.go create mode 100644 internal/api/grpc/user/v2/human_test.go create mode 100644 internal/api/grpc/user/v2/integration_test/key_test.go create mode 100644 internal/api/grpc/user/v2/integration_test/pat_test.go create mode 100644 internal/api/grpc/user/v2/integration_test/secret_test.go create mode 100644 internal/api/grpc/user/v2/key.go create mode 100644 internal/api/grpc/user/v2/key_query.go create mode 100644 internal/api/grpc/user/v2/machine.go create mode 100644 internal/api/grpc/user/v2/machine_test.go create mode 100644 internal/api/grpc/user/v2/pat.go create mode 100644 internal/api/grpc/user/v2/pat_query.go create mode 100644 internal/api/grpc/user/v2/secret.go rename internal/api/grpc/user/v2/{query.go => user_query.go} (100%) rename internal/command/{resource_ower_model.go => resource_owner_model.go} (100%) create mode 100644 internal/command/user_v2_machine.go create mode 100644 internal/command/user_v2_machine_test.go create mode 100644 proto/zitadel/filter/v2/filter.proto create mode 100644 proto/zitadel/user/v2/key.proto create mode 100644 proto/zitadel/user/v2/pat.proto diff --git a/API_DESIGN.md b/API_DESIGN.md index 9e77657ab0..11b7766a49 100644 --- a/API_DESIGN.md +++ b/API_DESIGN.md @@ -206,6 +206,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. +On the other hand, types that always follow the same pattern and are used in multiple resources, such as `IDFilter`, `TimestampFilter` or `InIDsFilter` SHOULD be globalized and reused. + ##### Re-using messages Prevent reusing messages for the creation and the retrieval of a resource. @@ -271,7 +273,7 @@ Additionally, state changes, specific actions or operations that do not fit into The API uses OAuth 2 for authorization. There are corresponding middlewares that check the access token for validity and automatically return an error if the token is invalid. -Permissions grated to the user might be organization specific and can therefore only be checked based on the queried resource. +Permissions granted to the user might be organization specific and can therefore only be checked based on the queried resource. In such case, the API does not check the permissions itself but relies on the checks of the functions that are called by the API. If the permission can be checked by the API itself, e.g. if the permission is instance wide, it can be annotated on the endpoint in the proto file (see below). In any case, the required permissions need to be documented in the [API documentation](#documentation). diff --git a/cmd/start/start.go b/cmd/start/start.go index 2fc1fb8413..8820480f0c 100644 --- a/cmd/start/start.go +++ b/cmd/start/start.go @@ -461,7 +461,7 @@ func startAPIs( if err := apis.RegisterService(ctx, user_v2beta.CreateServer(commands, queries, keys.User, keys.IDPConfig, idp.CallbackURL(), idp.SAMLRootURL(), assets.AssetAPI(), permissionCheck)); err != nil { return nil, err } - if err := apis.RegisterService(ctx, user_v2.CreateServer(commands, queries, keys.User, keys.IDPConfig, idp.CallbackURL(), idp.SAMLRootURL(), assets.AssetAPI(), permissionCheck)); err != nil { + if err := apis.RegisterService(ctx, user_v2.CreateServer(config.SystemDefaults, commands, queries, keys.User, keys.IDPConfig, idp.CallbackURL(), idp.SAMLRootURL(), assets.AssetAPI(), permissionCheck)); err != nil { return nil, err } if err := apis.RegisterService(ctx, session_v2beta.CreateServer(commands, queries, permissionCheck)); err != nil { diff --git a/go.mod b/go.mod index c1cbf2dd77..21a7fe9f16 100644 --- a/go.mod +++ b/go.mod @@ -34,6 +34,7 @@ require ( github.com/go-webauthn/webauthn v0.10.2 github.com/goccy/go-json v0.10.5 github.com/golang/protobuf v1.5.4 + github.com/google/go-cmp v0.7.0 github.com/gorilla/csrf v1.7.2 github.com/gorilla/mux v1.8.1 github.com/gorilla/schema v1.4.1 diff --git a/internal/api/grpc/admin/import.go b/internal/api/grpc/admin/import.go index 119afe9fc0..41a1e39081 100644 --- a/internal/api/grpc/admin/import.go +++ b/internal/api/grpc/admin/import.go @@ -510,7 +510,7 @@ func importMachineUsers(ctx context.Context, s *Server, errors *[]*admin_pb.Impo } for _, user := range org.GetMachineUsers() { logging.Debugf("import user: %s", user.GetUserId()) - _, err := s.command.AddMachine(ctx, management.AddMachineUserRequestToCommand(user.GetUser(), org.GetOrgId())) + _, err := s.command.AddMachine(ctx, management.AddMachineUserRequestToCommand(user.GetUser(), org.GetOrgId()), nil) if err != nil { *errors = append(*errors, &admin_pb.ImportDataError{Type: "machine_user", Id: user.GetUserId(), Message: err.Error()}) if isCtxTimeout(ctx) { diff --git a/internal/api/grpc/filter/v2/converter.go b/internal/api/grpc/filter/v2/converter.go new file mode 100644 index 0000000000..7a7d7cd8d7 --- /dev/null +++ b/internal/api/grpc/filter/v2/converter.go @@ -0,0 +1,50 @@ +package filter + +import ( + "fmt" + + "github.com/zitadel/zitadel/internal/config/systemdefaults" + "github.com/zitadel/zitadel/internal/query" + "github.com/zitadel/zitadel/internal/zerrors" + "github.com/zitadel/zitadel/pkg/grpc/filter/v2" +) + +func TimestampMethodPbToQuery(method filter.TimestampFilterMethod) query.TimestampComparison { + switch method { + case filter.TimestampFilterMethod_TIMESTAMP_FILTER_METHOD_EQUALS: + return query.TimestampEquals + case filter.TimestampFilterMethod_TIMESTAMP_FILTER_METHOD_BEFORE: + return query.TimestampLess + case filter.TimestampFilterMethod_TIMESTAMP_FILTER_METHOD_AFTER: + return query.TimestampGreater + case filter.TimestampFilterMethod_TIMESTAMP_FILTER_METHOD_BEFORE_OR_EQUALS: + return query.TimestampLessOrEquals + case filter.TimestampFilterMethod_TIMESTAMP_FILTER_METHOD_AFTER_OR_EQUALS: + return query.TimestampGreaterOrEquals + default: + return -1 + } +} + +func PaginationPbToQuery(defaults systemdefaults.SystemDefaults, query *filter.PaginationRequest) (offset, limit uint64, asc bool, err error) { + limit = defaults.DefaultQueryLimit + if query == nil { + return 0, limit, asc, nil + } + offset = query.Offset + asc = query.Asc + if defaults.MaxQueryLimit > 0 && uint64(query.Limit) > defaults.MaxQueryLimit { + return 0, 0, false, zerrors.ThrowInvalidArgumentf(fmt.Errorf("given: %d, allowed: %d", query.Limit, defaults.MaxQueryLimit), "QUERY-4M0fs", "Errors.Query.LimitExceeded") + } + if query.Limit > 0 { + limit = uint64(query.Limit) + } + return offset, limit, asc, nil +} + +func QueryToPaginationPb(request query.SearchRequest, response query.SearchResponse) *filter.PaginationResponse { + return &filter.PaginationResponse{ + AppliedLimit: request.Limit, + TotalResult: response.Count, + } +} diff --git a/internal/api/grpc/management/project_application.go b/internal/api/grpc/management/project_application.go index 3a0e1d5f92..ab49905409 100644 --- a/internal/api/grpc/management/project_application.go +++ b/internal/api/grpc/management/project_application.go @@ -271,7 +271,7 @@ func (s *Server) ListAppKeys(ctx context.Context, req *mgmt_pb.ListAppKeysReques if err != nil { return nil, err } - keys, err := s.query.SearchAuthNKeys(ctx, queries, false) + keys, err := s.query.SearchAuthNKeys(ctx, queries, query.JoinFilterApp, nil) if err != nil { return nil, err } diff --git a/internal/api/grpc/management/user.go b/internal/api/grpc/management/user.go index f318051e63..ae1040cd1e 100644 --- a/internal/api/grpc/management/user.go +++ b/internal/api/grpc/management/user.go @@ -297,7 +297,7 @@ func (s *Server) ImportHumanUser(ctx context.Context, req *mgmt_pb.ImportHumanUs func (s *Server) AddMachineUser(ctx context.Context, req *mgmt_pb.AddMachineUserRequest) (*mgmt_pb.AddMachineUserResponse, error) { machine := AddMachineUserRequestToCommand(req, authz.GetCtxData(ctx).OrgID) - objectDetails, err := s.command.AddMachine(ctx, machine) + objectDetails, err := s.command.AddMachine(ctx, machine, nil) if err != nil { return nil, err } @@ -752,11 +752,11 @@ func (s *Server) GetMachineKeyByIDs(ctx context.Context, req *mgmt_pb.GetMachine } func (s *Server) ListMachineKeys(ctx context.Context, req *mgmt_pb.ListMachineKeysRequest) (*mgmt_pb.ListMachineKeysResponse, error) { - query, err := ListMachineKeysRequestToQuery(ctx, req) + q, err := ListMachineKeysRequestToQuery(ctx, req) if err != nil { return nil, err } - result, err := s.query.SearchAuthNKeys(ctx, query, false) + result, err := s.query.SearchAuthNKeys(ctx, q, query.JoinFilterUserMachine, nil) if err != nil { return nil, err } @@ -774,7 +774,6 @@ func (s *Server) AddMachineKey(ctx context.Context, req *mgmt_pb.AddMachineKeyRe if err != nil { return nil, err } - // Return key details only if the pubkey wasn't supplied, otherwise the user already has // private key locally var keyDetails []byte @@ -821,7 +820,7 @@ func (s *Server) GenerateMachineSecret(ctx context.Context, req *mgmt_pb.Generat } func (s *Server) RemoveMachineSecret(ctx context.Context, req *mgmt_pb.RemoveMachineSecretRequest) (*mgmt_pb.RemoveMachineSecretResponse, error) { - objectDetails, err := s.command.RemoveMachineSecret(ctx, req.UserId, authz.GetCtxData(ctx).OrgID) + objectDetails, err := s.command.RemoveMachineSecret(ctx, req.UserId, authz.GetCtxData(ctx).OrgID, nil) if err != nil { return nil, err } @@ -839,7 +838,7 @@ func (s *Server) GetPersonalAccessTokenByIDs(ctx context.Context, req *mgmt_pb.G if err != nil { return nil, err } - token, err := s.query.PersonalAccessTokenByID(ctx, true, req.TokenId, false, resourceOwner, aggregateID) + token, err := s.query.PersonalAccessTokenByID(ctx, true, req.TokenId, resourceOwner, aggregateID) if err != nil { return nil, err } @@ -853,7 +852,7 @@ func (s *Server) ListPersonalAccessTokens(ctx context.Context, req *mgmt_pb.List if err != nil { return nil, err } - result, err := s.query.SearchPersonalAccessTokens(ctx, queries, false) + result, err := s.query.SearchPersonalAccessTokens(ctx, queries, nil) if err != nil { return nil, err } diff --git a/internal/api/grpc/project/v2beta/integration/query_test.go b/internal/api/grpc/project/v2beta/integration/query_test.go index f959bfe2f8..517f103628 100644 --- a/internal/api/grpc/project/v2beta/integration/query_test.go +++ b/internal/api/grpc/project/v2beta/integration/query_test.go @@ -168,8 +168,8 @@ func TestServer_ListProjects(t *testing.T) { orgID := instance.DefaultOrg.GetId() resp := instance.CreateProject(iamOwnerCtx, t, orgID, name, false, false) request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{ - InProjectIdsFilter: &project.InProjectIDsFilter{ - ProjectIds: []string{resp.GetId()}, + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{resp.GetId()}, }, } }, @@ -188,8 +188,8 @@ func TestServer_ListProjects(t *testing.T) { orgID := instance.DefaultOrg.GetId() resp := instance.CreateProject(iamOwnerCtx, t, orgID, name, false, false) request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{ - InProjectIdsFilter: &project.InProjectIDsFilter{ - ProjectIds: []string{resp.GetId()}, + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{resp.GetId()}, }, } }, @@ -208,8 +208,8 @@ func TestServer_ListProjects(t *testing.T) { orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) resp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), name, false, false) request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{ - InProjectIdsFilter: &project.InProjectIDsFilter{ - ProjectIds: []string{resp.GetId()}, + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{resp.GetId()}, }, } }, @@ -232,8 +232,8 @@ func TestServer_ListProjects(t *testing.T) { req: &project.ListProjectsRequest{ Filters: []*project.ProjectSearchFilter{ {Filter: &project.ProjectSearchFilter_InProjectIdsFilter{ - InProjectIdsFilter: &project.InProjectIDsFilter{ - ProjectIds: []string{"notfound"}, + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{"notfound"}, }, }, }, @@ -255,8 +255,8 @@ func TestServer_ListProjects(t *testing.T) { orgID := instance.DefaultOrg.GetId() response.Projects[0] = createProject(iamOwnerCtx, instance, t, orgID, false, false) request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{ - InProjectIdsFilter: &project.InProjectIDsFilter{ - ProjectIds: []string{response.Projects[0].GetId()}, + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{response.Projects[0].GetId()}, }, } }, @@ -317,8 +317,8 @@ func TestServer_ListProjects(t *testing.T) { response.Projects[1] = createProject(iamOwnerCtx, instance, t, orgID, true, false) response.Projects[0] = createProject(iamOwnerCtx, instance, t, orgID, false, true) request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{ - InProjectIdsFilter: &project.InProjectIDsFilter{ - ProjectIds: []string{response.Projects[0].GetId(), response.Projects[1].GetId(), response.Projects[2].GetId()}, + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{response.Projects[0].GetId(), response.Projects[1].GetId(), response.Projects[2].GetId()}, }, } }, @@ -349,8 +349,8 @@ func TestServer_ListProjects(t *testing.T) { resp2 := createProject(iamOwnerCtx, instance, t, orgID, true, false) resp3 := createProject(iamOwnerCtx, instance, t, orgResp.GetOrganizationId(), false, true) request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{ - InProjectIdsFilter: &project.InProjectIDsFilter{ - ProjectIds: []string{resp1.GetId(), resp2.GetId(), resp3.GetId()}, + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{resp1.GetId(), resp2.GetId(), resp3.GetId()}, }, } @@ -379,8 +379,8 @@ func TestServer_ListProjects(t *testing.T) { projectResp := createProject(iamOwnerCtx, instance, t, orgID, true, true) response.Projects[3] = projectResp request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{ - InProjectIdsFilter: &project.InProjectIDsFilter{ - ProjectIds: []string{projectResp.GetId()}, + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{projectResp.GetId()}, }, } response.Projects[2] = createGrantedProject(iamOwnerCtx, instance, t, projectResp) @@ -416,7 +416,7 @@ func TestServer_ListProjects(t *testing.T) { response.Projects[1] = grantedProjectResp response.Projects[0] = createProject(iamOwnerCtx, instance, t, *grantedProjectResp.GrantedOrganizationId, true, true) request.Filters[0].Filter = &project.ProjectSearchFilter_ProjectOrganizationIdFilter{ - ProjectOrganizationIdFilter: &project.ProjectOrganizationIDFilter{ProjectOrganizationId: *grantedProjectResp.GrantedOrganizationId}, + ProjectOrganizationIdFilter: &filter.IDFilter{Id: *grantedProjectResp.GrantedOrganizationId}, } }, req: &project.ListProjectsRequest{ @@ -445,7 +445,7 @@ func TestServer_ListProjects(t *testing.T) { grantedProjectResp := createGrantedProject(iamOwnerCtx, instance, t, projectResp) response.Projects[0] = createProject(iamOwnerCtx, instance, t, *grantedProjectResp.GrantedOrganizationId, true, true) request.Filters[0].Filter = &project.ProjectSearchFilter_ProjectResourceOwnerFilter{ - ProjectResourceOwnerFilter: &project.ProjectResourceOwnerFilter{ProjectResourceOwner: *grantedProjectResp.GrantedOrganizationId}, + ProjectResourceOwnerFilter: &filter.IDFilter{Id: *grantedProjectResp.GrantedOrganizationId}, } }, req: &project.ListProjectsRequest{ @@ -513,8 +513,8 @@ func TestServer_ListProjects_PermissionV2(t *testing.T) { dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) { resp := createProject(iamOwnerCtx, instancePermissionV2, t, orgID, false, false) request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{ - InProjectIdsFilter: &project.InProjectIDsFilter{ - ProjectIds: []string{resp.GetId()}, + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{resp.GetId()}, }, } }, @@ -531,8 +531,8 @@ func TestServer_ListProjects_PermissionV2(t *testing.T) { dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) { resp := createProject(iamOwnerCtx, instancePermissionV2, t, orgID, false, false) request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{ - InProjectIdsFilter: &project.InProjectIDsFilter{ - ProjectIds: []string{resp.GetId()}, + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{resp.GetId()}, }, } }, @@ -550,8 +550,8 @@ func TestServer_ListProjects_PermissionV2(t *testing.T) { orgResp := instancePermissionV2.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) resp := createProject(iamOwnerCtx, instancePermissionV2, t, orgResp.GetOrganizationId(), false, false) request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{ - InProjectIdsFilter: &project.InProjectIDsFilter{ - ProjectIds: []string{resp.GetId()}, + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{resp.GetId()}, }, } }, @@ -574,8 +574,8 @@ func TestServer_ListProjects_PermissionV2(t *testing.T) { req: &project.ListProjectsRequest{ Filters: []*project.ProjectSearchFilter{ {Filter: &project.ProjectSearchFilter_InProjectIdsFilter{ - InProjectIdsFilter: &project.InProjectIDsFilter{ - ProjectIds: []string{"notfound"}, + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{"notfound"}, }, }, }, @@ -596,8 +596,8 @@ func TestServer_ListProjects_PermissionV2(t *testing.T) { dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) { response.Projects[0] = createProject(iamOwnerCtx, instancePermissionV2, t, orgID, false, false) request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{ - InProjectIdsFilter: &project.InProjectIDsFilter{ - ProjectIds: []string{response.Projects[0].GetId()}, + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{response.Projects[0].GetId()}, }, } }, @@ -650,8 +650,8 @@ func TestServer_ListProjects_PermissionV2(t *testing.T) { response.Projects[1] = createProject(iamOwnerCtx, instancePermissionV2, t, orgID, true, false) response.Projects[0] = createProject(iamOwnerCtx, instancePermissionV2, t, orgID, false, true) request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{ - InProjectIdsFilter: &project.InProjectIDsFilter{ - ProjectIds: []string{response.Projects[0].GetId(), response.Projects[1].GetId(), response.Projects[2].GetId()}, + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{response.Projects[0].GetId(), response.Projects[1].GetId(), response.Projects[2].GetId()}, }, } }, @@ -679,8 +679,8 @@ func TestServer_ListProjects_PermissionV2(t *testing.T) { projectResp := createProject(iamOwnerCtx, instancePermissionV2, t, orgID, true, true) response.Projects[3] = projectResp request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{ - InProjectIdsFilter: &project.InProjectIDsFilter{ - ProjectIds: []string{projectResp.GetId()}, + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{projectResp.GetId()}, }, } response.Projects[2] = createGrantedProject(iamOwnerCtx, instancePermissionV2, t, projectResp) @@ -715,7 +715,7 @@ func TestServer_ListProjects_PermissionV2(t *testing.T) { response.Projects[1] = grantedProjectResp response.Projects[0] = createProject(iamOwnerCtx, instancePermissionV2, t, *grantedProjectResp.GrantedOrganizationId, true, true) request.Filters[0].Filter = &project.ProjectSearchFilter_ProjectOrganizationIdFilter{ - ProjectOrganizationIdFilter: &project.ProjectOrganizationIDFilter{ProjectOrganizationId: *grantedProjectResp.GrantedOrganizationId}, + ProjectOrganizationIdFilter: &filter.IDFilter{Id: *grantedProjectResp.GrantedOrganizationId}, } }, req: &project.ListProjectsRequest{ @@ -743,7 +743,7 @@ func TestServer_ListProjects_PermissionV2(t *testing.T) { grantedProjectResp := createGrantedProject(iamOwnerCtx, instancePermissionV2, t, projectResp) response.Projects[0] = createProject(iamOwnerCtx, instancePermissionV2, t, *grantedProjectResp.GrantedOrganizationId, true, true) request.Filters[0].Filter = &project.ProjectSearchFilter_ProjectResourceOwnerFilter{ - ProjectResourceOwnerFilter: &project.ProjectResourceOwnerFilter{ProjectResourceOwner: *grantedProjectResp.GrantedOrganizationId}, + ProjectResourceOwnerFilter: &filter.IDFilter{Id: *grantedProjectResp.GrantedOrganizationId}, } }, req: &project.ListProjectsRequest{ @@ -770,8 +770,8 @@ func TestServer_ListProjects_PermissionV2(t *testing.T) { resp2 := createProject(iamOwnerCtx, instancePermissionV2, t, orgID, true, false) resp3 := createProject(iamOwnerCtx, instancePermissionV2, t, orgResp.GetOrganizationId(), false, true) request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{ - InProjectIdsFilter: &project.InProjectIDsFilter{ - ProjectIds: []string{resp1.GetId(), resp2.GetId(), resp3.GetId()}, + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{resp1.GetId(), resp2.GetId(), resp3.GetId()}, }, } @@ -882,15 +882,13 @@ func TestServer_ListProjectGrants(t *testing.T) { dep: func(request *project.ListProjectGrantsRequest, response *project.ListProjectGrantsResponse) { projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), false, false) request.Filters[0].Filter = &project.ProjectGrantSearchFilter_InProjectIdsFilter{ - InProjectIdsFilter: &project.InProjectIDsFilter{ - ProjectIds: []string{projectResp.GetId()}, + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{projectResp.GetId()}, }, } grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) request.Filters[1].Filter = &project.ProjectGrantSearchFilter_ProjectGrantResourceOwnerFilter{ - ProjectGrantResourceOwnerFilter: &project.ProjectGrantResourceOwnerFilter{ - ProjectGrantResourceOwner: grantedOrg.GetOrganizationId(), - }, + ProjectGrantResourceOwnerFilter: &filter.IDFilter{Id: grantedOrg.GetOrganizationId()}, } instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) @@ -908,15 +906,13 @@ func TestServer_ListProjectGrants(t *testing.T) { dep: func(request *project.ListProjectGrantsRequest, response *project.ListProjectGrantsResponse) { projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), false, false) request.Filters[0].Filter = &project.ProjectGrantSearchFilter_InProjectIdsFilter{ - InProjectIdsFilter: &project.InProjectIDsFilter{ - ProjectIds: []string{projectResp.GetId()}, + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{projectResp.GetId()}, }, } grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) request.Filters[1].Filter = &project.ProjectGrantSearchFilter_ProjectGrantResourceOwnerFilter{ - ProjectGrantResourceOwnerFilter: &project.ProjectGrantResourceOwnerFilter{ - ProjectGrantResourceOwner: grantedOrg.GetOrganizationId(), - }, + ProjectGrantResourceOwnerFilter: &filter.IDFilter{Id: grantedOrg.GetOrganizationId()}, } instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) @@ -934,8 +930,8 @@ func TestServer_ListProjectGrants(t *testing.T) { req: &project.ListProjectGrantsRequest{ Filters: []*project.ProjectGrantSearchFilter{ {Filter: &project.ProjectGrantSearchFilter_InProjectIdsFilter{ - InProjectIdsFilter: &project.InProjectIDsFilter{ - ProjectIds: []string{"notfound"}, + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{"notfound"}, }, }, }, @@ -958,8 +954,8 @@ func TestServer_ListProjectGrants(t *testing.T) { orgID := instance.DefaultOrg.GetId() projectResp := instance.CreateProject(iamOwnerCtx, t, orgID, name, false, false) request.Filters[0].Filter = &project.ProjectGrantSearchFilter_InProjectIdsFilter{ - InProjectIdsFilter: &project.InProjectIDsFilter{ - ProjectIds: []string{projectResp.GetId()}, + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{projectResp.GetId()}, }, } @@ -988,8 +984,8 @@ func TestServer_ListProjectGrants(t *testing.T) { orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), name, false, false) request.Filters[0].Filter = &project.ProjectGrantSearchFilter_InProjectIdsFilter{ - InProjectIdsFilter: &project.InProjectIDsFilter{ - ProjectIds: []string{projectResp.GetId()}, + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{projectResp.GetId()}, }, } @@ -1016,8 +1012,8 @@ func TestServer_ListProjectGrants(t *testing.T) { orgID := instance.DefaultOrg.GetId() projectResp := instance.CreateProject(iamOwnerCtx, t, orgID, name, false, false) request.Filters[0].Filter = &project.ProjectGrantSearchFilter_InProjectIdsFilter{ - InProjectIdsFilter: &project.InProjectIDsFilter{ - ProjectIds: []string{projectResp.GetId()}, + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{projectResp.GetId()}, }, } @@ -1053,8 +1049,8 @@ func TestServer_ListProjectGrants(t *testing.T) { project2Resp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), name2, false, false) project3Resp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), name3, false, false) request.Filters[0].Filter = &project.ProjectGrantSearchFilter_InProjectIdsFilter{ - InProjectIdsFilter: &project.InProjectIDsFilter{ - ProjectIds: []string{project1Resp.GetId(), project2Resp.GetId(), project3Resp.GetId()}, + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{project1Resp.GetId(), project2Resp.GetId(), project3Resp.GetId()}, }, } @@ -1084,8 +1080,8 @@ func TestServer_ListProjectGrants(t *testing.T) { orgID := instance.DefaultOrg.GetId() projectResp := instance.CreateProject(iamOwnerCtx, t, orgID, name, false, false) request.Filters[0].Filter = &project.ProjectGrantSearchFilter_InProjectIdsFilter{ - InProjectIdsFilter: &project.InProjectIDsFilter{ - ProjectIds: []string{projectResp.GetId()}, + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{projectResp.GetId()}, }, } projectRoleResp := addProjectRole(iamOwnerCtx, instance, t, projectResp.GetId()) @@ -1114,8 +1110,8 @@ func TestServer_ListProjectGrants(t *testing.T) { orgID := instance.DefaultOrg.GetId() projectResp := instance.CreateProject(iamOwnerCtx, t, orgID, name, false, false) request.Filters[0].Filter = &project.ProjectGrantSearchFilter_InProjectIdsFilter{ - InProjectIdsFilter: &project.InProjectIDsFilter{ - ProjectIds: []string{projectResp.GetId()}, + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{projectResp.GetId()}, }, } projectRoleResp := addProjectRole(iamOwnerCtx, instance, t, projectResp.GetId()) @@ -1189,15 +1185,13 @@ func TestServer_ListProjectGrants_PermissionV2(t *testing.T) { dep: func(request *project.ListProjectGrantsRequest, response *project.ListProjectGrantsResponse) { projectResp := instancePermissionV2.CreateProject(iamOwnerCtx, t, instancePermissionV2.DefaultOrg.GetId(), gofakeit.AppName(), false, false) request.Filters[0].Filter = &project.ProjectGrantSearchFilter_InProjectIdsFilter{ - InProjectIdsFilter: &project.InProjectIDsFilter{ - ProjectIds: []string{projectResp.GetId()}, + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{projectResp.GetId()}, }, } grantedOrg := instancePermissionV2.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) request.Filters[1].Filter = &project.ProjectGrantSearchFilter_ProjectGrantResourceOwnerFilter{ - ProjectGrantResourceOwnerFilter: &project.ProjectGrantResourceOwnerFilter{ - ProjectGrantResourceOwner: grantedOrg.GetOrganizationId(), - }, + ProjectGrantResourceOwnerFilter: &filter.IDFilter{Id: grantedOrg.GetOrganizationId()}, } instancePermissionV2.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) @@ -1215,15 +1209,13 @@ func TestServer_ListProjectGrants_PermissionV2(t *testing.T) { dep: func(request *project.ListProjectGrantsRequest, response *project.ListProjectGrantsResponse) { projectResp := instancePermissionV2.CreateProject(iamOwnerCtx, t, instancePermissionV2.DefaultOrg.GetId(), gofakeit.AppName(), false, false) request.Filters[0].Filter = &project.ProjectGrantSearchFilter_InProjectIdsFilter{ - InProjectIdsFilter: &project.InProjectIDsFilter{ - ProjectIds: []string{projectResp.GetId()}, + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{projectResp.GetId()}, }, } grantedOrg := instancePermissionV2.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) request.Filters[1].Filter = &project.ProjectGrantSearchFilter_ProjectGrantResourceOwnerFilter{ - ProjectGrantResourceOwnerFilter: &project.ProjectGrantResourceOwnerFilter{ - ProjectGrantResourceOwner: grantedOrg.GetOrganizationId(), - }, + ProjectGrantResourceOwnerFilter: &filter.IDFilter{Id: grantedOrg.GetOrganizationId()}, } instancePermissionV2.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) @@ -1243,8 +1235,8 @@ func TestServer_ListProjectGrants_PermissionV2(t *testing.T) { orgID := instancePermissionV2.DefaultOrg.GetId() projectResp := instancePermissionV2.CreateProject(iamOwnerCtx, t, orgID, name, false, false) request.Filters[0].Filter = &project.ProjectGrantSearchFilter_InProjectIdsFilter{ - InProjectIdsFilter: &project.InProjectIDsFilter{ - ProjectIds: []string{projectResp.GetId()}, + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{projectResp.GetId()}, }, } @@ -1273,8 +1265,8 @@ func TestServer_ListProjectGrants_PermissionV2(t *testing.T) { orgResp := instancePermissionV2.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) projectResp := instancePermissionV2.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), name, false, false) request.Filters[0].Filter = &project.ProjectGrantSearchFilter_InProjectIdsFilter{ - InProjectIdsFilter: &project.InProjectIDsFilter{ - ProjectIds: []string{projectResp.GetId()}, + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{projectResp.GetId()}, }, } @@ -1301,8 +1293,8 @@ func TestServer_ListProjectGrants_PermissionV2(t *testing.T) { orgID := instancePermissionV2.DefaultOrg.GetId() projectResp := instancePermissionV2.CreateProject(iamOwnerCtx, t, orgID, name, false, false) request.Filters[0].Filter = &project.ProjectGrantSearchFilter_InProjectIdsFilter{ - InProjectIdsFilter: &project.InProjectIDsFilter{ - ProjectIds: []string{projectResp.GetId()}, + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{projectResp.GetId()}, }, } @@ -1338,8 +1330,8 @@ func TestServer_ListProjectGrants_PermissionV2(t *testing.T) { project2Resp := instancePermissionV2.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), name2, false, false) project3Resp := instancePermissionV2.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), name3, false, false) request.Filters[0].Filter = &project.ProjectGrantSearchFilter_InProjectIdsFilter{ - InProjectIdsFilter: &project.InProjectIDsFilter{ - ProjectIds: []string{project1Resp.GetId(), project2Resp.GetId(), project3Resp.GetId()}, + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{project1Resp.GetId(), project2Resp.GetId(), project3Resp.GetId()}, }, } diff --git a/internal/api/grpc/project/v2beta/query.go b/internal/api/grpc/project/v2beta/query.go index 1cdf9eefbd..42b69a480e 100644 --- a/internal/api/grpc/project/v2beta/query.go +++ b/internal/api/grpc/project/v2beta/query.go @@ -9,6 +9,7 @@ import ( "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/query" "github.com/zitadel/zitadel/internal/zerrors" + filter_pb "github.com/zitadel/zitadel/pkg/grpc/filter/v2beta" project_pb "github.com/zitadel/zitadel/pkg/grpc/project/v2beta" ) @@ -109,20 +110,20 @@ func projectNameFilterToQuery(q *project_pb.ProjectNameFilter) (query.SearchQuer return query.NewGrantedProjectNameSearchQuery(filter.TextMethodPbToQuery(q.Method), q.GetProjectName()) } -func projectInIDsFilterToQuery(q *project_pb.InProjectIDsFilter) (query.SearchQuery, error) { - return query.NewGrantedProjectIDSearchQuery(q.ProjectIds) +func projectInIDsFilterToQuery(q *filter_pb.InIDsFilter) (query.SearchQuery, error) { + return query.NewGrantedProjectIDSearchQuery(q.Ids) } -func projectResourceOwnerFilterToQuery(q *project_pb.ProjectResourceOwnerFilter) (query.SearchQuery, error) { - return query.NewGrantedProjectResourceOwnerSearchQuery(q.ProjectResourceOwner) +func projectResourceOwnerFilterToQuery(q *filter_pb.IDFilter) (query.SearchQuery, error) { + return query.NewGrantedProjectResourceOwnerSearchQuery(q.Id) } -func projectOrganizationIDFilterToQuery(q *project_pb.ProjectOrganizationIDFilter) (query.SearchQuery, error) { - return query.NewGrantedProjectOrganizationIDSearchQuery(q.ProjectOrganizationId) +func projectOrganizationIDFilterToQuery(q *filter_pb.IDFilter) (query.SearchQuery, error) { + return query.NewGrantedProjectOrganizationIDSearchQuery(q.Id) } -func projectGrantResourceOwnerFilterToQuery(q *project_pb.ProjectGrantResourceOwnerFilter) (query.SearchQuery, error) { - return query.NewGrantedProjectGrantResourceOwnerSearchQuery(q.ProjectGrantResourceOwner) +func projectGrantResourceOwnerFilterToQuery(q *filter_pb.IDFilter) (query.SearchQuery, error) { + return query.NewGrantedProjectGrantResourceOwnerSearchQuery(q.Id) } func grantedProjectsToPb(projects []*query.GrantedProject) []*project_pb.Project { @@ -283,11 +284,11 @@ func projectGrantFilterToModel(filter *project_pb.ProjectGrantSearchFilter) (que case *project_pb.ProjectGrantSearchFilter_RoleKeyFilter: return query.NewProjectGrantRoleKeySearchQuery(q.RoleKeyFilter.Key) case *project_pb.ProjectGrantSearchFilter_InProjectIdsFilter: - return query.NewProjectGrantProjectIDsSearchQuery(q.InProjectIdsFilter.ProjectIds) + return query.NewProjectGrantProjectIDsSearchQuery(q.InProjectIdsFilter.Ids) case *project_pb.ProjectGrantSearchFilter_ProjectResourceOwnerFilter: - return query.NewProjectGrantResourceOwnerSearchQuery(q.ProjectResourceOwnerFilter.ProjectResourceOwner) + return query.NewProjectGrantResourceOwnerSearchQuery(q.ProjectResourceOwnerFilter.Id) case *project_pb.ProjectGrantSearchFilter_ProjectGrantResourceOwnerFilter: - return query.NewProjectGrantGrantedOrgIDSearchQuery(q.ProjectGrantResourceOwnerFilter.ProjectGrantResourceOwner) + return query.NewProjectGrantGrantedOrgIDSearchQuery(q.ProjectGrantResourceOwnerFilter.Id) default: return nil, zerrors.ThrowInvalidArgument(nil, "PROJECT-M099f", "List.Query.Invalid") } diff --git a/internal/api/grpc/user/v2/human.go b/internal/api/grpc/user/v2/human.go new file mode 100644 index 0000000000..d8a0891396 --- /dev/null +++ b/internal/api/grpc/user/v2/human.go @@ -0,0 +1,187 @@ +package user + +import ( + "context" + "io" + + "golang.org/x/text/language" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/zerrors" + legacyobject "github.com/zitadel/zitadel/pkg/grpc/object/v2" + "github.com/zitadel/zitadel/pkg/grpc/user/v2" +) + +func (s *Server) createUserTypeHuman(ctx context.Context, humanPb *user.CreateUserRequest_Human, orgId string, userName, userId *string) (*user.CreateUserResponse, error) { + addHumanPb := &user.AddHumanUserRequest{ + Username: userName, + UserId: userId, + Organization: &legacyobject.Organization{ + Org: &legacyobject.Organization_OrgId{OrgId: orgId}, + }, + Profile: humanPb.Profile, + Email: humanPb.Email, + Phone: humanPb.Phone, + IdpLinks: humanPb.IdpLinks, + TotpSecret: humanPb.TotpSecret, + } + switch pwType := humanPb.GetPasswordType().(type) { + case *user.CreateUserRequest_Human_HashedPassword: + addHumanPb.PasswordType = &user.AddHumanUserRequest_HashedPassword{ + HashedPassword: pwType.HashedPassword, + } + case *user.CreateUserRequest_Human_Password: + addHumanPb.PasswordType = &user.AddHumanUserRequest_Password{ + Password: pwType.Password, + } + default: + // optional password is not set + } + newHuman, err := AddUserRequestToAddHuman(addHumanPb) + if err != nil { + return nil, err + } + if err = s.command.AddUserHuman( + ctx, + orgId, + newHuman, + false, + s.userCodeAlg, + ); err != nil { + return nil, err + } + return &user.CreateUserResponse{ + Id: newHuman.ID, + CreationDate: timestamppb.New(newHuman.Details.EventDate), + EmailCode: newHuman.EmailCode, + PhoneCode: newHuman.PhoneCode, + }, nil +} + +func (s *Server) updateUserTypeHuman(ctx context.Context, humanPb *user.UpdateUserRequest_Human, userId string, userName *string) (*user.UpdateUserResponse, error) { + cmd, err := updateHumanUserToCommand(userId, userName, humanPb) + if err != nil { + return nil, err + } + if err = s.command.ChangeUserHuman(ctx, cmd, s.userCodeAlg); err != nil { + return nil, err + } + return &user.UpdateUserResponse{ + ChangeDate: timestamppb.New(cmd.Details.EventDate), + EmailCode: cmd.EmailCode, + PhoneCode: cmd.PhoneCode, + }, nil +} + +func updateHumanUserToCommand(userId string, userName *string, human *user.UpdateUserRequest_Human) (*command.ChangeHuman, error) { + phone := human.GetPhone() + if phone != nil && phone.Phone == "" && phone.GetVerification() != nil { + return nil, zerrors.ThrowInvalidArgument(nil, "USERv2-4f3d6", "Errors.User.Phone.VerifyingRemovalIsNotSupported") + } + email, err := setHumanEmailToEmail(human.Email, userId) + if err != nil { + return nil, err + } + return &command.ChangeHuman{ + ID: userId, + Username: userName, + Profile: SetHumanProfileToProfile(human.Profile), + Email: email, + Phone: setHumanPhoneToPhone(human.Phone, true), + Password: setHumanPasswordToPassword(human.Password), + }, nil +} + +func updateHumanUserRequestToChangeHuman(req *user.UpdateHumanUserRequest) (*command.ChangeHuman, error) { + email, err := setHumanEmailToEmail(req.Email, req.GetUserId()) + if err != nil { + return nil, err + } + changeHuman := &command.ChangeHuman{ + ID: req.GetUserId(), + Username: req.Username, + Email: email, + Phone: setHumanPhoneToPhone(req.Phone, false), + Password: setHumanPasswordToPassword(req.Password), + } + if profile := req.GetProfile(); profile != nil { + var firstName *string + if profile.GivenName != "" { + firstName = &profile.GivenName + } + var lastName *string + if profile.FamilyName != "" { + lastName = &profile.FamilyName + } + changeHuman.Profile = SetHumanProfileToProfile(&user.UpdateUserRequest_Human_Profile{ + GivenName: firstName, + FamilyName: lastName, + NickName: profile.NickName, + DisplayName: profile.DisplayName, + PreferredLanguage: profile.PreferredLanguage, + Gender: profile.Gender, + }) + } + return changeHuman, nil +} + +func SetHumanProfileToProfile(profile *user.UpdateUserRequest_Human_Profile) *command.Profile { + if profile == nil { + return nil + } + return &command.Profile{ + FirstName: profile.GivenName, + LastName: profile.FamilyName, + NickName: profile.NickName, + DisplayName: profile.DisplayName, + PreferredLanguage: ifNotNilPtr(profile.PreferredLanguage, language.Make), + Gender: ifNotNilPtr(profile.Gender, genderToDomain), + } +} + +func setHumanEmailToEmail(email *user.SetHumanEmail, userID string) (*command.Email, error) { + if email == nil { + return nil, nil + } + var urlTemplate string + if email.GetSendCode() != nil && email.GetSendCode().UrlTemplate != nil { + urlTemplate = *email.GetSendCode().UrlTemplate + if err := domain.RenderConfirmURLTemplate(io.Discard, urlTemplate, userID, "code", "orgID"); err != nil { + return nil, err + } + } + return &command.Email{ + Address: domain.EmailAddress(email.Email), + Verified: email.GetIsVerified(), + ReturnCode: email.GetReturnCode() != nil, + URLTemplate: urlTemplate, + }, nil +} + +func setHumanPhoneToPhone(phone *user.SetHumanPhone, withRemove bool) *command.Phone { + if phone == nil { + return nil + } + number := phone.GetPhone() + return &command.Phone{ + Number: domain.PhoneNumber(number), + Verified: phone.GetIsVerified(), + ReturnCode: phone.GetReturnCode() != nil, + Remove: withRemove && number == "", + } +} + +func setHumanPasswordToPassword(password *user.SetPassword) *command.Password { + if password == nil { + return nil + } + return &command.Password{ + PasswordCode: password.GetVerificationCode(), + OldPassword: password.GetCurrentPassword(), + Password: password.GetPassword().GetPassword(), + EncodedPasswordHash: password.GetHashedPassword().GetHash(), + ChangeRequired: password.GetPassword().GetChangeRequired() || password.GetHashedPassword().GetChangeRequired(), + } +} diff --git a/internal/api/grpc/user/v2/human_test.go b/internal/api/grpc/user/v2/human_test.go new file mode 100644 index 0000000000..52e5371dcc --- /dev/null +++ b/internal/api/grpc/user/v2/human_test.go @@ -0,0 +1,254 @@ +package user + +import ( + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/muhlemmer/gu" + "github.com/stretchr/testify/assert" + "golang.org/x/text/language" + + "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/pkg/grpc/user/v2" +) + +func Test_patchHumanUserToCommand(t *testing.T) { + type args struct { + userId string + userName *string + human *user.UpdateUserRequest_Human + } + tests := []struct { + name string + args args + want *command.ChangeHuman + wantErr assert.ErrorAssertionFunc + }{{ + name: "single property", + args: args{ + userId: "userId", + human: &user.UpdateUserRequest_Human{ + Profile: &user.UpdateUserRequest_Human_Profile{ + GivenName: gu.Ptr("givenName"), + }, + }, + }, + want: &command.ChangeHuman{ + ID: "userId", + Profile: &command.Profile{ + FirstName: gu.Ptr("givenName"), + }, + }, + wantErr: assert.NoError, + }, { + name: "all properties", + args: args{ + userId: "userId", + userName: gu.Ptr("userName"), + human: &user.UpdateUserRequest_Human{ + Profile: &user.UpdateUserRequest_Human_Profile{ + GivenName: gu.Ptr("givenName"), + FamilyName: gu.Ptr("familyName"), + NickName: gu.Ptr("nickName"), + DisplayName: gu.Ptr("displayName"), + PreferredLanguage: gu.Ptr("en-US"), + Gender: gu.Ptr(user.Gender_GENDER_FEMALE), + }, + Email: &user.SetHumanEmail{ + Email: "email@example.com", + Verification: &user.SetHumanEmail_IsVerified{ + IsVerified: true, + }, + }, + Phone: &user.SetHumanPhone{ + Phone: "+123456789", + Verification: &user.SetHumanPhone_IsVerified{ + IsVerified: true, + }, + }, + Password: &user.SetPassword{ + Verification: &user.SetPassword_CurrentPassword{ + CurrentPassword: "currentPassword", + }, + PasswordType: &user.SetPassword_Password{ + Password: &user.Password{ + Password: "newPassword", + ChangeRequired: true, + }, + }, + }, + }, + }, + want: &command.ChangeHuman{ + ID: "userId", + Username: gu.Ptr("userName"), + Profile: &command.Profile{ + FirstName: gu.Ptr("givenName"), + LastName: gu.Ptr("familyName"), + NickName: gu.Ptr("nickName"), + DisplayName: gu.Ptr("displayName"), + PreferredLanguage: &language.AmericanEnglish, + Gender: gu.Ptr(domain.GenderFemale), + }, + Email: &command.Email{ + Address: "email@example.com", + Verified: true, + }, + Phone: &command.Phone{ + Number: "+123456789", + Verified: true, + }, + Password: &command.Password{ + OldPassword: "currentPassword", + Password: "newPassword", + ChangeRequired: true, + }, + }, + wantErr: assert.NoError, + }, { + name: "set email and request code", + args: args{ + userId: "userId", + human: &user.UpdateUserRequest_Human{ + Email: &user.SetHumanEmail{ + Email: "email@example.com", + Verification: &user.SetHumanEmail_ReturnCode{ + ReturnCode: &user.ReturnEmailVerificationCode{}, + }, + }, + }, + }, + want: &command.ChangeHuman{ + ID: "userId", + Email: &command.Email{ + Address: "email@example.com", + ReturnCode: true, + }, + }, + wantErr: assert.NoError, + }, { + name: "set email and send code", + args: args{ + userId: "userId", + human: &user.UpdateUserRequest_Human{ + Email: &user.SetHumanEmail{ + Email: "email@example.com", + Verification: &user.SetHumanEmail_SendCode{ + SendCode: &user.SendEmailVerificationCode{}, + }, + }, + }, + }, + want: &command.ChangeHuman{ + ID: "userId", + Email: &command.Email{ + Address: "email@example.com", + }, + }, + wantErr: assert.NoError, + }, { + name: "set email and send code with template", + args: args{ + userId: "userId", + human: &user.UpdateUserRequest_Human{ + Email: &user.SetHumanEmail{ + Email: "email@example.com", + Verification: &user.SetHumanEmail_SendCode{ + SendCode: &user.SendEmailVerificationCode{ + UrlTemplate: gu.Ptr("Code: {{.Code}}"), + }, + }, + }, + }, + }, + want: &command.ChangeHuman{ + ID: "userId", + Email: &command.Email{ + Address: "email@example.com", + URLTemplate: "Code: {{.Code}}", + }, + }, + wantErr: assert.NoError, + }, { + name: "set phone and request code", + args: args{ + userId: "userId", + human: &user.UpdateUserRequest_Human{ + Phone: &user.SetHumanPhone{ + Phone: "+123456789", + Verification: &user.SetHumanPhone_ReturnCode{ + ReturnCode: &user.ReturnPhoneVerificationCode{}, + }, + }, + }, + }, + want: &command.ChangeHuman{ + ID: "userId", + Phone: &command.Phone{ + Number: "+123456789", + ReturnCode: true, + }, + }, + wantErr: assert.NoError, + }, { + name: "set phone and send code", + args: args{ + userId: "userId", + human: &user.UpdateUserRequest_Human{ + Phone: &user.SetHumanPhone{ + Phone: "+123456789", + Verification: &user.SetHumanPhone_SendCode{ + SendCode: &user.SendPhoneVerificationCode{}, + }, + }, + }, + }, + want: &command.ChangeHuman{ + ID: "userId", + Phone: &command.Phone{ + Number: "+123456789", + }, + }, + wantErr: assert.NoError, + }, { + name: "remove phone, ok", + args: args{ + userId: "userId", + human: &user.UpdateUserRequest_Human{ + Phone: &user.SetHumanPhone{}, + }, + }, + want: &command.ChangeHuman{ + ID: "userId", + Phone: &command.Phone{ + Remove: true, + }, + }, + wantErr: assert.NoError, + }, { + name: "remove phone with verification, error", + args: args{ + userId: "userId", + human: &user.UpdateUserRequest_Human{ + Phone: &user.SetHumanPhone{ + Verification: &user.SetHumanPhone_ReturnCode{}, + }, + }, + }, + wantErr: assert.Error, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := updateHumanUserToCommand(tt.args.userId, tt.args.userName, tt.args.human) + if !tt.wantErr(t, err, fmt.Sprintf("patchHumanUserToCommand(%v, %v, %v)", tt.args.userId, tt.args.userName, tt.args.human)) { + return + } + if diff := cmp.Diff(tt.want, got, cmpopts.EquateComparable(language.Tag{})); diff != "" { + t.Errorf("patchHumanUserToCommand() mismatch (-want +got):\n%s", diff) + } + }) + } +} diff --git a/internal/api/grpc/user/v2/integration_test/email_test.go b/internal/api/grpc/user/v2/integration_test/email_test.go index ad63c2ce5e..ad68ef5c5a 100644 --- a/internal/api/grpc/user/v2/integration_test/email_test.go +++ b/internal/api/grpc/user/v2/integration_test/email_test.go @@ -10,13 +10,13 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "google.golang.org/protobuf/types/known/timestamppb" - + "github.com/zitadel/zitadel/internal/integration" "github.com/zitadel/zitadel/pkg/grpc/object/v2" "github.com/zitadel/zitadel/pkg/grpc/user/v2" ) -func TestServer_SetEmail(t *testing.T) { +func TestServer_Deprecated_SetEmail(t *testing.T) { userID := Instance.CreateHumanUser(CTX).GetUserId() tests := []struct { diff --git a/internal/api/grpc/user/v2/integration_test/key_test.go b/internal/api/grpc/user/v2/integration_test/key_test.go new file mode 100644 index 0000000000..e85903b2cb --- /dev/null +++ b/internal/api/grpc/user/v2/integration_test/key_test.go @@ -0,0 +1,659 @@ +//go:build integration + +package user_test + +import ( + "context" + "fmt" + "slices" + "testing" + "time" + + "github.com/brianvoe/gofakeit/v6" + "github.com/google/go-cmp/cmp" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/testing/protocmp" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/integration" + "github.com/zitadel/zitadel/pkg/grpc/filter/v2" + "github.com/zitadel/zitadel/pkg/grpc/user/v2" +) + +func TestServer_AddKey(t *testing.T) { + resp := Instance.CreateUserTypeMachine(IamCTX) + userId := resp.GetId() + expirationDate := timestamppb.New(time.Now().Add(time.Hour * 24)) + type args struct { + req *user.AddKeyRequest + prepare func(request *user.AddKeyRequest) error + } + tests := []struct { + name string + args args + wantErr bool + wantEmtpyKey bool + }{ + { + name: "add key, user not existing", + args: args{ + &user.AddKeyRequest{ + UserId: "notexisting", + ExpirationDate: expirationDate, + }, + func(request *user.AddKeyRequest) error { return nil }, + }, + wantErr: true, + }, + { + name: "generate key pair, ok", + args: args{ + &user.AddKeyRequest{ + ExpirationDate: expirationDate, + }, + func(request *user.AddKeyRequest) error { + request.UserId = userId + return nil + }, + }, + }, + { + name: "add valid public key, ok", + args: args{ + &user.AddKeyRequest{ + ExpirationDate: expirationDate, + // This is the public key of the tester system user. This must be valid. + PublicKey: []byte(` +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzi+FFSJL7f5yw4KTwzgM +P34ePGycm/M+kT0M7V4Cgx5V3EaDIvTQKTLfBaEB45zb9LtjIXzDw0rXRoS2hO6t +h+CYQCz3KCvh09C0IzxZiB2IS3H/aT+5Bx9EFY+vnAkZjccbyG5YNRvmtOlnvIeI +H7qZ0tEwkPfF5GEZNPJPtmy3UGV7iofdVQS1xRj73+aMw5rvH4D8IdyiAC3VekIb +pt0Vj0SUX3DwKtog337BzTiPk3aXRF0sbFhQoqdJRI8NqgZjCwjq9yfI5tyxYswn ++JGzHGdHvW3idODlmwEt5K2pasiRIWK2OGfq+w0EcltQHabuqEPgZlmhCkRdNfix +BwIDAQAB +-----END PUBLIC KEY----- +`), + }, + func(request *user.AddKeyRequest) error { + request.UserId = userId + return nil + }, + }, + wantEmtpyKey: true, + }, + { + name: "add invalid public key, error", + args: args{ + &user.AddKeyRequest{ + ExpirationDate: expirationDate, + PublicKey: []byte(` +-----BEGIN PUBLIC KEY----- +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 +-----END PUBLIC KEY----- +`), + }, + func(request *user.AddKeyRequest) error { + request.UserId = userId + return nil + }, + }, + wantErr: true, + }, + { + name: "add key human, error", + args: args{ + &user.AddKeyRequest{ + ExpirationDate: expirationDate, + }, + func(request *user.AddKeyRequest) error { + resp := Instance.CreateUserTypeHuman(IamCTX) + request.UserId = resp.Id + return nil + }, + }, + wantErr: true, + }, + { + name: "add another key, ok", + args: args{ + &user.AddKeyRequest{ + ExpirationDate: expirationDate, + }, + func(request *user.AddKeyRequest) error { + request.UserId = userId + _, err := Client.AddKey(IamCTX, &user.AddKeyRequest{ + ExpirationDate: expirationDate, + UserId: userId, + }) + return err + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + now := time.Now() + err := tt.args.prepare(tt.args.req) + require.NoError(t, err) + got, err := Client.AddKey(CTX, tt.args.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + assert.NotEmpty(t, got.KeyId, "key id is empty") + if tt.wantEmtpyKey { + assert.Empty(t, got.KeyContent, "key content is not empty") + } else { + assert.NotEmpty(t, got.KeyContent, "key content is empty") + } + creationDate := got.CreationDate.AsTime() + assert.Greater(t, creationDate, now, "creation date is before the test started") + assert.Less(t, creationDate, time.Now(), "creation date is in the future") + }) + } +} + +func TestServer_AddKey_Permission(t *testing.T) { + OrgCTX := CTX + otherOrg := Instance.CreateOrganization(IamCTX, fmt.Sprintf("AddKey-%s", gofakeit.AppName()), gofakeit.Email()) + otherOrgUser, err := Client.CreateUser(IamCTX, &user.CreateUserRequest{ + OrganizationId: otherOrg.OrganizationId, + UserType: &user.CreateUserRequest_Machine_{ + Machine: &user.CreateUserRequest_Machine{ + Name: gofakeit.Name(), + }, + }, + }) + require.NoError(t, err) + request := &user.AddKeyRequest{ + ExpirationDate: timestamppb.New(time.Now().Add(time.Hour * 24)), + UserId: otherOrgUser.GetId(), + } + type args struct { + ctx context.Context + req *user.AddKeyRequest + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "system, ok", + args: args{SystemCTX, request}, + }, + { + name: "instance, ok", + args: args{IamCTX, request}, + }, + { + name: "org, error", + args: args{OrgCTX, request}, + wantErr: true, + }, + { + name: "user, error", + args: args{UserCTX, request}, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + now := time.Now() + require.NoError(t, err) + got, err := Client.AddKey(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + assert.NotEmpty(t, got.KeyId, "key id is empty") + assert.NotEmpty(t, got.KeyContent, "key content is empty") + creationDate := got.CreationDate.AsTime() + assert.Greater(t, creationDate, now, "creation date is before the test started") + assert.Less(t, creationDate, time.Now(), "creation date is in the future") + }) + } +} + +func TestServer_RemoveKey(t *testing.T) { + resp := Instance.CreateUserTypeMachine(IamCTX) + userId := resp.GetId() + expirationDate := timestamppb.New(time.Now().Add(time.Hour * 24)) + type args struct { + req *user.RemoveKeyRequest + prepare func(request *user.RemoveKeyRequest) error + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "remove key, user not existing", + args: args{ + &user.RemoveKeyRequest{ + UserId: "notexisting", + }, + func(request *user.RemoveKeyRequest) error { + key, err := Client.AddKey(IamCTX, &user.AddKeyRequest{ + ExpirationDate: expirationDate, + UserId: userId, + }) + request.KeyId = key.GetKeyId() + return err + }, + }, + wantErr: true, + }, + { + name: "remove key, not existing", + args: args{ + &user.RemoveKeyRequest{ + KeyId: "notexisting", + }, + func(request *user.RemoveKeyRequest) error { + request.UserId = userId + return nil + }, + }, + wantErr: true, + }, + { + name: "remove key, ok", + args: args{ + &user.RemoveKeyRequest{}, + func(request *user.RemoveKeyRequest) error { + key, err := Client.AddKey(IamCTX, &user.AddKeyRequest{ + ExpirationDate: expirationDate, + UserId: userId, + }) + request.KeyId = key.GetKeyId() + request.UserId = userId + return err + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + now := time.Now() + err := tt.args.prepare(tt.args.req) + require.NoError(t, err) + got, err := Client.RemoveKey(CTX, tt.args.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + deletionDate := got.DeletionDate.AsTime() + assert.Greater(t, deletionDate, now, "creation date is before the test started") + assert.Less(t, deletionDate, time.Now(), "creation date is in the future") + }) + } +} + +func TestServer_RemoveKey_Permission(t *testing.T) { + OrgCTX := CTX + otherOrg := Instance.CreateOrganization(IamCTX, fmt.Sprintf("RemoveKey-%s", gofakeit.AppName()), gofakeit.Email()) + otherOrgUser, err := Client.CreateUser(IamCTX, &user.CreateUserRequest{ + OrganizationId: otherOrg.OrganizationId, + UserType: &user.CreateUserRequest_Machine_{ + Machine: &user.CreateUserRequest_Machine{ + Name: gofakeit.Name(), + }, + }, + }) + request := &user.RemoveKeyRequest{ + UserId: otherOrgUser.GetId(), + } + prepare := func(request *user.RemoveKeyRequest) error { + key, err := Client.AddKey(IamCTX, &user.AddKeyRequest{ + ExpirationDate: timestamppb.New(time.Now().Add(time.Hour * 24)), + UserId: otherOrgUser.GetId(), + }) + request.KeyId = key.GetKeyId() + return err + } + require.NoError(t, err) + type args struct { + ctx context.Context + req *user.RemoveKeyRequest + prepare func(request *user.RemoveKeyRequest) error + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "system, ok", + args: args{SystemCTX, request, prepare}, + }, + { + name: "instance, ok", + args: args{IamCTX, request, prepare}, + }, + { + name: "org, error", + args: args{OrgCTX, request, prepare}, + wantErr: true, + }, + { + name: "user, error", + args: args{UserCTX, request, prepare}, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + now := time.Now() + require.NoError(t, tt.args.prepare(tt.args.req)) + got, err := Client.RemoveKey(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + assert.NotEmpty(t, got.DeletionDate, "client key is empty") + creationDate := got.DeletionDate.AsTime() + assert.Greater(t, creationDate, now, "creation date is before the test started") + assert.Less(t, creationDate, time.Now(), "creation date is in the future") + }) + } +} + +func TestServer_ListKeys(t *testing.T) { + type args struct { + ctx context.Context + req *user.ListKeysRequest + } + type testCase struct { + name string + args args + want *user.ListKeysResponse + } + OrgCTX := CTX + otherOrg := Instance.CreateOrganization(SystemCTX, fmt.Sprintf("ListKeys-%s", gofakeit.AppName()), gofakeit.Email()) + otherOrgUser, err := Client.CreateUser(SystemCTX, &user.CreateUserRequest{ + OrganizationId: otherOrg.OrganizationId, + UserType: &user.CreateUserRequest_Machine_{ + Machine: &user.CreateUserRequest_Machine{ + Name: gofakeit.Name(), + }, + }, + }) + require.NoError(t, err) + otherOrgUserId := otherOrgUser.GetId() + otherUserId := Instance.CreateUserTypeMachine(SystemCTX).GetId() + onlySinceTestStartFilter := &user.KeysSearchFilter{Filter: &user.KeysSearchFilter_CreatedDateFilter{CreatedDateFilter: &filter.TimestampFilter{ + Timestamp: timestamppb.Now(), + Method: filter.TimestampFilterMethod_TIMESTAMP_FILTER_METHOD_AFTER_OR_EQUALS, + }}} + myOrgId := Instance.DefaultOrg.GetId() + myUserId := Instance.Users.Get(integration.UserTypeNoPermission).ID + expiresInADay := time.Now().Truncate(time.Hour).Add(time.Hour * 24) + myDataPoint := setupKeyDataPoint(t, myUserId, myOrgId, expiresInADay) + otherUserDataPoint := setupKeyDataPoint(t, otherUserId, myOrgId, expiresInADay) + otherOrgDataPointExpiringSoon := setupKeyDataPoint(t, otherOrgUserId, otherOrg.OrganizationId, time.Now().Truncate(time.Hour).Add(time.Hour)) + otherOrgDataPointExpiringLate := setupKeyDataPoint(t, otherOrgUserId, otherOrg.OrganizationId, expiresInADay.Add(time.Hour*24*30)) + sortingColumnExpirationDate := user.KeyFieldName_KEY_FIELD_NAME_KEY_EXPIRATION_DATE + awaitKeys(t, onlySinceTestStartFilter, + otherOrgDataPointExpiringSoon.GetId(), + otherOrgDataPointExpiringLate.GetId(), + otherUserDataPoint.GetId(), + myDataPoint.GetId(), + ) + tests := []testCase{ + { + name: "list all, instance", + args: args{ + IamCTX, + &user.ListKeysRequest{Filters: []*user.KeysSearchFilter{onlySinceTestStartFilter}}, + }, + want: &user.ListKeysResponse{ + Result: []*user.Key{ + otherOrgDataPointExpiringLate, + otherOrgDataPointExpiringSoon, + otherUserDataPoint, + myDataPoint, + }, + Pagination: &filter.PaginationResponse{ + TotalResult: 4, + AppliedLimit: 100, + }, + }, + }, + { + name: "list all, org", + args: args{ + OrgCTX, + &user.ListKeysRequest{Filters: []*user.KeysSearchFilter{onlySinceTestStartFilter}}, + }, + want: &user.ListKeysResponse{ + Result: []*user.Key{ + otherUserDataPoint, + myDataPoint, + }, + Pagination: &filter.PaginationResponse{ + TotalResult: 2, + AppliedLimit: 100, + }, + }, + }, + { + name: "list all, user", + args: args{ + UserCTX, + &user.ListKeysRequest{Filters: []*user.KeysSearchFilter{onlySinceTestStartFilter}}, + }, + want: &user.ListKeysResponse{ + Result: []*user.Key{ + myDataPoint, + }, + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + }, + }, + { + name: "list by id", + args: args{ + IamCTX, + &user.ListKeysRequest{ + Filters: []*user.KeysSearchFilter{ + onlySinceTestStartFilter, + { + Filter: &user.KeysSearchFilter_KeyIdFilter{ + KeyIdFilter: &filter.IDFilter{Id: otherOrgDataPointExpiringSoon.Id}, + }, + }, + }, + }, + }, + want: &user.ListKeysResponse{ + Result: []*user.Key{ + otherOrgDataPointExpiringSoon, + }, + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + }, + }, + { + name: "list all from other org", + args: args{ + IamCTX, + &user.ListKeysRequest{ + Filters: []*user.KeysSearchFilter{ + onlySinceTestStartFilter, + { + Filter: &user.KeysSearchFilter_OrganizationIdFilter{ + OrganizationIdFilter: &filter.IDFilter{Id: otherOrg.OrganizationId}, + }, + }, + }, + }, + }, + want: &user.ListKeysResponse{ + Result: []*user.Key{ + otherOrgDataPointExpiringLate, + otherOrgDataPointExpiringSoon, + }, + Pagination: &filter.PaginationResponse{ + TotalResult: 2, + AppliedLimit: 100, + }, + }, + }, + { + name: "sort by next expiration dates", + args: args{ + IamCTX, + &user.ListKeysRequest{ + Pagination: &filter.PaginationRequest{ + Asc: true, + }, + SortingColumn: &sortingColumnExpirationDate, + Filters: []*user.KeysSearchFilter{ + onlySinceTestStartFilter, + {Filter: &user.KeysSearchFilter_OrganizationIdFilter{OrganizationIdFilter: &filter.IDFilter{Id: otherOrg.OrganizationId}}}, + }, + }, + }, + want: &user.ListKeysResponse{ + Result: []*user.Key{ + otherOrgDataPointExpiringSoon, + otherOrgDataPointExpiringLate, + }, + Pagination: &filter.PaginationResponse{ + TotalResult: 2, + AppliedLimit: 100, + }, + }, + }, + { + name: "get page", + args: args{ + IamCTX, + &user.ListKeysRequest{ + Pagination: &filter.PaginationRequest{ + Offset: 2, + Limit: 2, + Asc: true, + }, + Filters: []*user.KeysSearchFilter{ + onlySinceTestStartFilter, + }, + }, + }, + want: &user.ListKeysResponse{ + Result: []*user.Key{ + otherOrgDataPointExpiringSoon, + otherOrgDataPointExpiringLate, + }, + Pagination: &filter.PaginationResponse{ + TotalResult: 4, + AppliedLimit: 2, + }, + }, + }, + { + name: "empty list", + args: args{ + UserCTX, + &user.ListKeysRequest{ + Filters: []*user.KeysSearchFilter{ + { + Filter: &user.KeysSearchFilter_KeyIdFilter{ + KeyIdFilter: &filter.IDFilter{Id: otherUserDataPoint.Id}, + }, + }, + }, + }, + }, + want: &user.ListKeysResponse{ + Result: []*user.Key{}, + Pagination: &filter.PaginationResponse{ + TotalResult: 0, + AppliedLimit: 100, + }, + }, + }, + } + t.Run("with permission flag v2", func(t *testing.T) { + setPermissionCheckV2Flag(t, true) + defer setPermissionCheckV2Flag(t, false) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Client.ListKeys(tt.args.ctx, tt.args.req) + require.NoError(t, err) + assert.Len(t, got.Result, len(tt.want.Result)) + if diff := cmp.Diff(tt.want, got, protocmp.Transform()); diff != "" { + t.Errorf("ListKeys() mismatch (-want +got):\n%s", diff) + } + }) + } + }) + t.Run("without permission flag v2", func(t *testing.T) { + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Client.ListKeys(tt.args.ctx, tt.args.req) + require.NoError(t, err) + assert.Len(t, got.Result, len(tt.want.Result)) + // ignore the total result, as this is a known bug with the in-memory permission checks. + // The command can't know how many keys exist in the system if the SQL statement has a limit. + // This is fixed, once the in-memory permission checks are removed with https://github.com/zitadel/zitadel/issues/9188 + tt.want.Pagination.TotalResult = got.Pagination.TotalResult + if diff := cmp.Diff(tt.want, got, protocmp.Transform()); diff != "" { + t.Errorf("ListKeys() mismatch (-want +got):\n%s", diff) + } + }) + } + }) +} + +func setupKeyDataPoint(t *testing.T, userId, orgId string, expirationDate time.Time) *user.Key { + expirationDatePb := timestamppb.New(expirationDate) + newKey, err := Client.AddKey(SystemCTX, &user.AddKeyRequest{ + UserId: userId, + ExpirationDate: expirationDatePb, + PublicKey: nil, + }) + require.NoError(t, err) + return &user.Key{ + CreationDate: newKey.CreationDate, + ChangeDate: newKey.CreationDate, + Id: newKey.GetKeyId(), + UserId: userId, + OrganizationId: orgId, + ExpirationDate: expirationDatePb, + } +} + +func awaitKeys(t *testing.T, sinceTestStartFilter *user.KeysSearchFilter, keyIds ...string) { + sortingColumn := user.KeyFieldName_KEY_FIELD_NAME_ID + slices.Sort(keyIds) + require.EventuallyWithT(t, func(collect *assert.CollectT) { + result, err := Client.ListKeys(SystemCTX, &user.ListKeysRequest{ + Filters: []*user.KeysSearchFilter{sinceTestStartFilter}, + SortingColumn: &sortingColumn, + Pagination: &filter.PaginationRequest{ + Asc: true, + }, + }) + require.NoError(t, err) + if !assert.Len(collect, result.Result, len(keyIds)) { + return + } + for i := range keyIds { + keyId := keyIds[i] + require.Equal(collect, keyId, result.Result[i].GetId()) + } + }, 5*time.Second, time.Second, "key not created in time") +} diff --git a/internal/api/grpc/user/v2/integration_test/password_test.go b/internal/api/grpc/user/v2/integration_test/password_test.go index 0cd0da7454..258cdaf78d 100644 --- a/internal/api/grpc/user/v2/integration_test/password_test.go +++ b/internal/api/grpc/user/v2/integration_test/password_test.go @@ -104,7 +104,7 @@ func TestServer_RequestPasswordReset(t *testing.T) { } } -func TestServer_SetPassword(t *testing.T) { +func TestServer_Deprecated_SetPassword(t *testing.T) { type args struct { ctx context.Context req *user.SetPasswordRequest diff --git a/internal/api/grpc/user/v2/integration_test/pat_test.go b/internal/api/grpc/user/v2/integration_test/pat_test.go new file mode 100644 index 0000000000..ce974e0407 --- /dev/null +++ b/internal/api/grpc/user/v2/integration_test/pat_test.go @@ -0,0 +1,615 @@ +//go:build integration + +package user_test + +import ( + "context" + "fmt" + "slices" + "testing" + "time" + + "github.com/brianvoe/gofakeit/v6" + "github.com/google/go-cmp/cmp" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/testing/protocmp" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/integration" + "github.com/zitadel/zitadel/pkg/grpc/filter/v2" + "github.com/zitadel/zitadel/pkg/grpc/user/v2" +) + +func TestServer_AddPersonalAccessToken(t *testing.T) { + resp := Instance.CreateUserTypeMachine(IamCTX) + userId := resp.GetId() + expirationDate := timestamppb.New(time.Now().Add(time.Hour * 24)) + type args struct { + req *user.AddPersonalAccessTokenRequest + prepare func(request *user.AddPersonalAccessTokenRequest) error + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "add pat, user not existing", + args: args{ + &user.AddPersonalAccessTokenRequest{ + UserId: "notexisting", + ExpirationDate: expirationDate, + }, + func(request *user.AddPersonalAccessTokenRequest) error { return nil }, + }, + wantErr: true, + }, + { + name: "add pat, ok", + args: args{ + &user.AddPersonalAccessTokenRequest{ + ExpirationDate: expirationDate, + }, + func(request *user.AddPersonalAccessTokenRequest) error { + request.UserId = userId + return nil + }, + }, + }, + { + name: "add pat human, not ok", + args: args{ + &user.AddPersonalAccessTokenRequest{ + ExpirationDate: expirationDate, + }, + func(request *user.AddPersonalAccessTokenRequest) error { + resp := Instance.CreateUserTypeHuman(IamCTX) + request.UserId = resp.Id + return nil + }, + }, + wantErr: true, + }, + { + name: "add another pat, ok", + args: args{ + &user.AddPersonalAccessTokenRequest{ + ExpirationDate: expirationDate, + }, + func(request *user.AddPersonalAccessTokenRequest) error { + request.UserId = userId + _, err := Client.AddPersonalAccessToken(IamCTX, &user.AddPersonalAccessTokenRequest{ + ExpirationDate: expirationDate, + UserId: userId, + }) + return err + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + now := time.Now() + err := tt.args.prepare(tt.args.req) + require.NoError(t, err) + got, err := Client.AddPersonalAccessToken(CTX, tt.args.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + assert.NotEmpty(t, got.TokenId, "id is empty") + assert.NotEmpty(t, got.Token, "token is empty") + creationDate := got.CreationDate.AsTime() + assert.Greater(t, creationDate, now, "creation date is before the test started") + assert.Less(t, creationDate, time.Now(), "creation date is in the future") + }) + } +} + +func TestServer_AddPersonalAccessToken_Permission(t *testing.T) { + OrgCTX := CTX + otherOrg := Instance.CreateOrganization(IamCTX, fmt.Sprintf("AddPersonalAccessToken-%s", gofakeit.AppName()), gofakeit.Email()) + otherOrgUser, err := Client.CreateUser(IamCTX, &user.CreateUserRequest{ + OrganizationId: otherOrg.OrganizationId, + UserType: &user.CreateUserRequest_Machine_{ + Machine: &user.CreateUserRequest_Machine{ + Name: gofakeit.Name(), + }, + }, + }) + require.NoError(t, err) + request := &user.AddPersonalAccessTokenRequest{ + ExpirationDate: timestamppb.New(time.Now().Add(time.Hour * 24)), + UserId: otherOrgUser.GetId(), + } + type args struct { + ctx context.Context + req *user.AddPersonalAccessTokenRequest + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "system, ok", + args: args{SystemCTX, request}, + }, + { + name: "instance, ok", + args: args{IamCTX, request}, + }, + { + name: "org, error", + args: args{OrgCTX, request}, + wantErr: true, + }, + { + name: "user, error", + args: args{UserCTX, request}, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + now := time.Now() + require.NoError(t, err) + got, err := Client.AddPersonalAccessToken(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + assert.NotEmpty(t, got.TokenId, "id is empty") + assert.NotEmpty(t, got.Token, "token is empty") + creationDate := got.CreationDate.AsTime() + assert.Greater(t, creationDate, now, "creation date is before the test started") + assert.Less(t, creationDate, time.Now(), "creation date is in the future") + }) + } +} + +func TestServer_RemovePersonalAccessToken(t *testing.T) { + resp := Instance.CreateUserTypeMachine(IamCTX) + userId := resp.GetId() + expirationDate := timestamppb.New(time.Now().Add(time.Hour * 24)) + type args struct { + req *user.RemovePersonalAccessTokenRequest + prepare func(request *user.RemovePersonalAccessTokenRequest) error + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "remove pat, user not existing", + args: args{ + &user.RemovePersonalAccessTokenRequest{ + UserId: "notexisting", + }, + func(request *user.RemovePersonalAccessTokenRequest) error { + pat, err := Client.AddPersonalAccessToken(CTX, &user.AddPersonalAccessTokenRequest{ + ExpirationDate: expirationDate, + UserId: userId, + }) + request.TokenId = pat.GetTokenId() + return err + }, + }, + wantErr: true, + }, + { + name: "remove pat, not existing", + args: args{ + &user.RemovePersonalAccessTokenRequest{ + TokenId: "notexisting", + }, + func(request *user.RemovePersonalAccessTokenRequest) error { + request.UserId = userId + return nil + }, + }, + wantErr: true, + }, + { + name: "remove pat, ok", + args: args{ + &user.RemovePersonalAccessTokenRequest{}, + func(request *user.RemovePersonalAccessTokenRequest) error { + pat, err := Client.AddPersonalAccessToken(CTX, &user.AddPersonalAccessTokenRequest{ + ExpirationDate: expirationDate, + UserId: userId, + }) + request.TokenId = pat.GetTokenId() + request.UserId = userId + return err + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + now := time.Now() + err := tt.args.prepare(tt.args.req) + require.NoError(t, err) + got, err := Client.RemovePersonalAccessToken(CTX, tt.args.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + deletionDate := got.DeletionDate.AsTime() + assert.Greater(t, deletionDate, now, "creation date is before the test started") + assert.Less(t, deletionDate, time.Now(), "creation date is in the future") + }) + } +} + +func TestServer_RemovePersonalAccessToken_Permission(t *testing.T) { + otherOrg := Instance.CreateOrganization(IamCTX, fmt.Sprintf("RemovePersonalAccessToken-%s", gofakeit.AppName()), gofakeit.Email()) + otherOrgUser, err := Client.CreateUser(IamCTX, &user.CreateUserRequest{ + OrganizationId: otherOrg.OrganizationId, + UserType: &user.CreateUserRequest_Machine_{ + Machine: &user.CreateUserRequest_Machine{ + Name: gofakeit.Name(), + }, + }, + }) + request := &user.RemovePersonalAccessTokenRequest{ + UserId: otherOrgUser.GetId(), + } + prepare := func(request *user.RemovePersonalAccessTokenRequest) error { + pat, err := Client.AddPersonalAccessToken(IamCTX, &user.AddPersonalAccessTokenRequest{ + ExpirationDate: timestamppb.New(time.Now().Add(time.Hour * 24)), + UserId: otherOrgUser.GetId(), + }) + request.TokenId = pat.GetTokenId() + return err + } + require.NoError(t, err) + type args struct { + ctx context.Context + req *user.RemovePersonalAccessTokenRequest + prepare func(request *user.RemovePersonalAccessTokenRequest) error + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "system, ok", + args: args{SystemCTX, request, prepare}, + }, + { + name: "instance, ok", + args: args{IamCTX, request, prepare}, + }, + { + name: "org, error", + args: args{CTX, request, prepare}, + wantErr: true, + }, + { + name: "user, error", + args: args{UserCTX, request, prepare}, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + now := time.Now() + require.NoError(t, tt.args.prepare(tt.args.req)) + got, err := Client.RemovePersonalAccessToken(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + assert.NotEmpty(t, got.DeletionDate, "client pat is empty") + creationDate := got.DeletionDate.AsTime() + assert.Greater(t, creationDate, now, "creation date is before the test started") + assert.Less(t, creationDate, time.Now(), "creation date is in the future") + }) + } +} + +func TestServer_ListPersonalAccessTokens(t *testing.T) { + type args struct { + ctx context.Context + req *user.ListPersonalAccessTokensRequest + } + type testCase struct { + name string + args args + want *user.ListPersonalAccessTokensResponse + } + OrgCTX := CTX + otherOrg := Instance.CreateOrganization(SystemCTX, fmt.Sprintf("ListPersonalAccessTokens-%s", gofakeit.AppName()), gofakeit.Email()) + otherOrgUser, err := Client.CreateUser(SystemCTX, &user.CreateUserRequest{ + OrganizationId: otherOrg.OrganizationId, + UserType: &user.CreateUserRequest_Machine_{ + Machine: &user.CreateUserRequest_Machine{ + Name: gofakeit.Name(), + }, + }, + }) + require.NoError(t, err) + otherOrgUserId := otherOrgUser.GetId() + otherUserId := Instance.CreateUserTypeMachine(SystemCTX).GetId() + onlySinceTestStartFilter := &user.PersonalAccessTokensSearchFilter{Filter: &user.PersonalAccessTokensSearchFilter_CreatedDateFilter{CreatedDateFilter: &filter.TimestampFilter{ + Timestamp: timestamppb.Now(), + Method: filter.TimestampFilterMethod_TIMESTAMP_FILTER_METHOD_AFTER_OR_EQUALS, + }}} + myOrgId := Instance.DefaultOrg.GetId() + myUserId := Instance.Users.Get(integration.UserTypeNoPermission).ID + expiresInADay := time.Now().Truncate(time.Hour).Add(time.Hour * 24) + myDataPoint := setupPATDataPoint(t, myUserId, myOrgId, expiresInADay) + otherUserDataPoint := setupPATDataPoint(t, otherUserId, myOrgId, expiresInADay) + otherOrgDataPointExpiringSoon := setupPATDataPoint(t, otherOrgUserId, otherOrg.OrganizationId, time.Now().Truncate(time.Hour).Add(time.Hour)) + otherOrgDataPointExpiringLate := setupPATDataPoint(t, otherOrgUserId, otherOrg.OrganizationId, expiresInADay.Add(time.Hour*24*30)) + sortingColumnExpirationDate := user.PersonalAccessTokenFieldName_PERSONAL_ACCESS_TOKEN_FIELD_NAME_EXPIRATION_DATE + awaitPersonalAccessTokens(t, + onlySinceTestStartFilter, + otherOrgDataPointExpiringSoon.GetId(), + otherOrgDataPointExpiringLate.GetId(), + otherUserDataPoint.GetId(), + myDataPoint.GetId(), + ) + tests := []testCase{ + { + name: "list all, instance", + args: args{ + IamCTX, + &user.ListPersonalAccessTokensRequest{ + Filters: []*user.PersonalAccessTokensSearchFilter{onlySinceTestStartFilter}, + }, + }, + want: &user.ListPersonalAccessTokensResponse{ + Result: []*user.PersonalAccessToken{ + otherOrgDataPointExpiringLate, + otherOrgDataPointExpiringSoon, + otherUserDataPoint, + myDataPoint, + }, + Pagination: &filter.PaginationResponse{ + TotalResult: 4, + AppliedLimit: 100, + }, + }, + }, + { + name: "list all, org", + args: args{ + OrgCTX, + &user.ListPersonalAccessTokensRequest{ + Filters: []*user.PersonalAccessTokensSearchFilter{onlySinceTestStartFilter}, + }, + }, + want: &user.ListPersonalAccessTokensResponse{ + Result: []*user.PersonalAccessToken{ + otherUserDataPoint, + myDataPoint, + }, + Pagination: &filter.PaginationResponse{ + TotalResult: 2, + AppliedLimit: 100, + }, + }, + }, + { + name: "list all, user", + args: args{ + UserCTX, + &user.ListPersonalAccessTokensRequest{ + Filters: []*user.PersonalAccessTokensSearchFilter{onlySinceTestStartFilter}, + }, + }, + want: &user.ListPersonalAccessTokensResponse{ + Result: []*user.PersonalAccessToken{ + myDataPoint, + }, + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + }, + }, + { + name: "list by id", + args: args{ + IamCTX, + &user.ListPersonalAccessTokensRequest{ + Filters: []*user.PersonalAccessTokensSearchFilter{ + onlySinceTestStartFilter, + { + Filter: &user.PersonalAccessTokensSearchFilter_TokenIdFilter{ + TokenIdFilter: &filter.IDFilter{Id: otherOrgDataPointExpiringSoon.Id}, + }, + }, + }, + }, + }, + want: &user.ListPersonalAccessTokensResponse{ + Result: []*user.PersonalAccessToken{ + otherOrgDataPointExpiringSoon, + }, + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + }, + }, + { + name: "list all from other org", + args: args{ + IamCTX, + &user.ListPersonalAccessTokensRequest{ + Filters: []*user.PersonalAccessTokensSearchFilter{ + onlySinceTestStartFilter, + { + Filter: &user.PersonalAccessTokensSearchFilter_OrganizationIdFilter{ + OrganizationIdFilter: &filter.IDFilter{Id: otherOrg.OrganizationId}, + }, + }}, + }, + }, + want: &user.ListPersonalAccessTokensResponse{ + Result: []*user.PersonalAccessToken{ + otherOrgDataPointExpiringLate, + otherOrgDataPointExpiringSoon, + }, + Pagination: &filter.PaginationResponse{ + TotalResult: 2, + AppliedLimit: 100, + }, + }, + }, + { + name: "sort by next expiration dates", + args: args{ + IamCTX, + &user.ListPersonalAccessTokensRequest{ + Pagination: &filter.PaginationRequest{ + Asc: true, + }, + SortingColumn: &sortingColumnExpirationDate, + Filters: []*user.PersonalAccessTokensSearchFilter{ + onlySinceTestStartFilter, + {Filter: &user.PersonalAccessTokensSearchFilter_OrganizationIdFilter{OrganizationIdFilter: &filter.IDFilter{Id: otherOrg.OrganizationId}}}, + }, + }, + }, + want: &user.ListPersonalAccessTokensResponse{ + Result: []*user.PersonalAccessToken{ + otherOrgDataPointExpiringSoon, + otherOrgDataPointExpiringLate, + }, + Pagination: &filter.PaginationResponse{ + TotalResult: 2, + AppliedLimit: 100, + }, + }, + }, + { + name: "get page", + args: args{ + IamCTX, + &user.ListPersonalAccessTokensRequest{ + Pagination: &filter.PaginationRequest{ + Offset: 2, + Limit: 2, + Asc: true, + }, + Filters: []*user.PersonalAccessTokensSearchFilter{ + onlySinceTestStartFilter, + }, + }, + }, + want: &user.ListPersonalAccessTokensResponse{ + Result: []*user.PersonalAccessToken{ + otherOrgDataPointExpiringSoon, + otherOrgDataPointExpiringLate, + }, + Pagination: &filter.PaginationResponse{ + TotalResult: 4, + AppliedLimit: 2, + }, + }, + }, + { + name: "empty list", + args: args{ + UserCTX, + &user.ListPersonalAccessTokensRequest{ + Filters: []*user.PersonalAccessTokensSearchFilter{ + { + Filter: &user.PersonalAccessTokensSearchFilter_TokenIdFilter{ + TokenIdFilter: &filter.IDFilter{Id: otherUserDataPoint.Id}, + }, + }, + }, + }, + }, + want: &user.ListPersonalAccessTokensResponse{ + Result: []*user.PersonalAccessToken{}, + Pagination: &filter.PaginationResponse{ + TotalResult: 0, + AppliedLimit: 100, + }, + }, + }, + } + t.Run("with permission flag v2", func(t *testing.T) { + setPermissionCheckV2Flag(t, true) + defer setPermissionCheckV2Flag(t, false) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Client.ListPersonalAccessTokens(tt.args.ctx, tt.args.req) + require.NoError(t, err) + assert.Len(t, got.Result, len(tt.want.Result)) + if diff := cmp.Diff(tt.want, got, protocmp.Transform()); diff != "" { + t.Errorf("ListPersonalAccessTokens() mismatch (-want +got):\n%s", diff) + } + }) + } + }) + t.Run("without permission flag v2", func(t *testing.T) { + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Client.ListPersonalAccessTokens(tt.args.ctx, tt.args.req) + require.NoError(t, err) + assert.Len(t, got.Result, len(tt.want.Result)) + // ignore the total result, as this is a known bug with the in-memory permission checks. + // The command can't know how many keys exist in the system if the SQL statement has a limit. + // This is fixed, once the in-memory permission checks are removed with https://github.com/zitadel/zitadel/issues/9188 + tt.want.Pagination.TotalResult = got.Pagination.TotalResult + if diff := cmp.Diff(tt.want, got, protocmp.Transform()); diff != "" { + t.Errorf("ListPersonalAccessTokens() mismatch (-want +got):\n%s", diff) + } + }) + } + }) +} + +func setupPATDataPoint(t *testing.T, userId, orgId string, expirationDate time.Time) *user.PersonalAccessToken { + expirationDatePb := timestamppb.New(expirationDate) + newPersonalAccessToken, err := Client.AddPersonalAccessToken(SystemCTX, &user.AddPersonalAccessTokenRequest{ + UserId: userId, + ExpirationDate: expirationDatePb, + }) + require.NoError(t, err) + return &user.PersonalAccessToken{ + CreationDate: newPersonalAccessToken.CreationDate, + ChangeDate: newPersonalAccessToken.CreationDate, + Id: newPersonalAccessToken.GetTokenId(), + UserId: userId, + OrganizationId: orgId, + ExpirationDate: expirationDatePb, + } +} + +func awaitPersonalAccessTokens(t *testing.T, sinceTestStartFilter *user.PersonalAccessTokensSearchFilter, patIds ...string) { + sortingColumn := user.PersonalAccessTokenFieldName_PERSONAL_ACCESS_TOKEN_FIELD_NAME_ID + slices.Sort(patIds) + require.EventuallyWithT(t, func(collect *assert.CollectT) { + result, err := Client.ListPersonalAccessTokens(SystemCTX, &user.ListPersonalAccessTokensRequest{ + Filters: []*user.PersonalAccessTokensSearchFilter{sinceTestStartFilter}, + SortingColumn: &sortingColumn, + Pagination: &filter.PaginationRequest{ + Asc: true, + }, + }) + require.NoError(t, err) + if !assert.Len(collect, result.Result, len(patIds)) { + return + } + for i := range patIds { + patId := patIds[i] + require.Equal(collect, patId, result.Result[i].GetId()) + } + }, 5*time.Second, time.Second, "pat not created in time") +} diff --git a/internal/api/grpc/user/v2/integration_test/phone_test.go b/internal/api/grpc/user/v2/integration_test/phone_test.go index 49050c5fe6..b87f9a9f28 100644 --- a/internal/api/grpc/user/v2/integration_test/phone_test.go +++ b/internal/api/grpc/user/v2/integration_test/phone_test.go @@ -17,7 +17,7 @@ import ( "github.com/zitadel/zitadel/pkg/grpc/user/v2" ) -func TestServer_SetPhone(t *testing.T) { +func TestServer_Deprecated_SetPhone(t *testing.T) { userID := Instance.CreateHumanUser(CTX).GetUserId() tests := []struct { @@ -249,7 +249,7 @@ func TestServer_VerifyPhone(t *testing.T) { } } -func TestServer_RemovePhone(t *testing.T) { +func TestServer_Deprecated_RemovePhone(t *testing.T) { userResp := Instance.CreateHumanUser(CTX) failResp := Instance.CreateHumanUserNoPhone(CTX) otherUser := Instance.CreateHumanUser(CTX).GetUserId() diff --git a/internal/api/grpc/user/v2/integration_test/secret_test.go b/internal/api/grpc/user/v2/integration_test/secret_test.go new file mode 100644 index 0000000000..8ff537b1fd --- /dev/null +++ b/internal/api/grpc/user/v2/integration_test/secret_test.go @@ -0,0 +1,347 @@ +//go:build integration + +package user_test + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/brianvoe/gofakeit/v6" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/zitadel/zitadel/pkg/grpc/user/v2" +) + +func TestServer_AddSecret(t *testing.T) { + type args struct { + ctx context.Context + req *user.AddSecretRequest + prepare func(request *user.AddSecretRequest) error + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "add secret, user not existing", + args: args{ + CTX, + &user.AddSecretRequest{ + UserId: "notexisting", + }, + func(request *user.AddSecretRequest) error { return nil }, + }, + wantErr: true, + }, + { + name: "add secret, ok", + args: args{ + CTX, + &user.AddSecretRequest{}, + func(request *user.AddSecretRequest) error { + resp := Instance.CreateUserTypeMachine(CTX) + request.UserId = resp.GetId() + return nil + }, + }, + }, + { + name: "add secret human, not ok", + args: args{ + CTX, + &user.AddSecretRequest{}, + func(request *user.AddSecretRequest) error { + resp := Instance.CreateUserTypeMachine(CTX) + request.UserId = resp.GetId() + return nil + }, + }, + }, + { + name: "overwrite secret, ok", + args: args{ + CTX, + &user.AddSecretRequest{}, + func(request *user.AddSecretRequest) error { + resp := Instance.CreateUserTypeMachine(CTX) + request.UserId = resp.GetId() + _, err := Client.AddSecret(CTX, &user.AddSecretRequest{ + UserId: resp.GetId(), + }) + return err + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + now := time.Now() + err := tt.args.prepare(tt.args.req) + require.NoError(t, err) + got, err := Client.AddSecret(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + assert.NotEmpty(t, got.ClientSecret, "client secret is empty") + creationDate := got.CreationDate.AsTime() + assert.Greater(t, creationDate, now, "creation date is before the test started") + assert.Less(t, creationDate, time.Now(), "creation date is in the future") + }) + } +} + +func TestServer_AddSecret_Permission(t *testing.T) { + otherOrg := Instance.CreateOrganization(IamCTX, fmt.Sprintf("AddSecret-%s", gofakeit.AppName()), gofakeit.Email()) + otherOrgUser, err := Instance.Client.UserV2.CreateUser(IamCTX, &user.CreateUserRequest{ + OrganizationId: otherOrg.OrganizationId, + UserType: &user.CreateUserRequest_Machine_{ + Machine: &user.CreateUserRequest_Machine{ + Name: gofakeit.Name(), + }, + }, + }) + require.NoError(t, err) + + type args struct { + ctx context.Context + req *user.AddSecretRequest + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "system, ok", + args: args{ + SystemCTX, + &user.AddSecretRequest{ + UserId: otherOrgUser.GetId(), + }, + }, + }, + { + name: "instance, ok", + args: args{ + IamCTX, + &user.AddSecretRequest{ + UserId: otherOrgUser.GetId(), + }, + }, + }, + { + name: "org, error", + args: args{ + CTX, + &user.AddSecretRequest{ + UserId: otherOrgUser.GetId(), + }, + }, + wantErr: true, + }, + { + name: "user, error", + args: args{ + UserCTX, + &user.AddSecretRequest{ + UserId: otherOrgUser.GetId(), + }, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + now := time.Now() + require.NoError(t, err) + got, err := Client.AddSecret(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + assert.NotEmpty(t, got.ClientSecret, "client secret is empty") + creationDate := got.CreationDate.AsTime() + assert.Greater(t, creationDate, now, "creation date is before the test started") + assert.Less(t, creationDate, time.Now(), "creation date is in the future") + }) + } +} + +func TestServer_RemoveSecret(t *testing.T) { + type args struct { + ctx context.Context + req *user.RemoveSecretRequest + prepare func(request *user.RemoveSecretRequest) error + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "remove secret, user not existing", + args: args{ + CTX, + &user.RemoveSecretRequest{ + UserId: "notexisting", + }, + func(request *user.RemoveSecretRequest) error { return nil }, + }, + wantErr: true, + }, + { + name: "remove secret, not existing", + args: args{ + CTX, + &user.RemoveSecretRequest{}, + func(request *user.RemoveSecretRequest) error { + resp := Instance.CreateUserTypeMachine(CTX) + request.UserId = resp.GetId() + return nil + }, + }, + wantErr: true, + }, + { + name: "remove secret, ok", + args: args{ + CTX, + &user.RemoveSecretRequest{}, + func(request *user.RemoveSecretRequest) error { + resp := Instance.CreateUserTypeMachine(CTX) + request.UserId = resp.GetId() + _, err := Instance.Client.UserV2.AddSecret(CTX, &user.AddSecretRequest{ + UserId: resp.GetId(), + }) + return err + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + now := time.Now() + err := tt.args.prepare(tt.args.req) + require.NoError(t, err) + got, err := Client.RemoveSecret(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + deletionDate := got.DeletionDate.AsTime() + assert.Greater(t, deletionDate, now, "creation date is before the test started") + assert.Less(t, deletionDate, time.Now(), "creation date is in the future") + }) + } +} + +func TestServer_RemoveSecret_Permission(t *testing.T) { + otherOrg := Instance.CreateOrganization(IamCTX, fmt.Sprintf("RemoveSecret-%s", gofakeit.AppName()), gofakeit.Email()) + otherOrgUser, err := Instance.Client.UserV2.CreateUser(IamCTX, &user.CreateUserRequest{ + OrganizationId: otherOrg.OrganizationId, + UserType: &user.CreateUserRequest_Machine_{ + Machine: &user.CreateUserRequest_Machine{ + Name: gofakeit.Name(), + }, + }, + }) + require.NoError(t, err) + + type args struct { + ctx context.Context + req *user.RemoveSecretRequest + prepare func(request *user.RemoveSecretRequest) error + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "system, ok", + args: args{ + SystemCTX, + &user.RemoveSecretRequest{ + UserId: otherOrgUser.GetId(), + }, + func(request *user.RemoveSecretRequest) error { + _, err := Instance.Client.UserV2.AddSecret(IamCTX, &user.AddSecretRequest{ + UserId: otherOrgUser.GetId(), + }) + return err + }, + }, + }, + { + name: "instance, ok", + args: args{ + IamCTX, + &user.RemoveSecretRequest{ + UserId: otherOrgUser.GetId(), + }, + func(request *user.RemoveSecretRequest) error { + _, err := Instance.Client.UserV2.AddSecret(IamCTX, &user.AddSecretRequest{ + UserId: otherOrgUser.GetId(), + }) + return err + }, + }, + }, + { + name: "org, error", + args: args{ + CTX, + &user.RemoveSecretRequest{ + UserId: otherOrgUser.GetId(), + }, + func(request *user.RemoveSecretRequest) error { + _, err := Instance.Client.UserV2.AddSecret(IamCTX, &user.AddSecretRequest{ + UserId: otherOrgUser.GetId(), + }) + return err + }, + }, + wantErr: true, + }, + { + name: "user, error", + args: args{ + UserCTX, + &user.RemoveSecretRequest{ + UserId: otherOrgUser.GetId(), + }, + func(request *user.RemoveSecretRequest) error { + _, err := Instance.Client.UserV2.AddSecret(IamCTX, &user.AddSecretRequest{ + UserId: otherOrgUser.GetId(), + }) + return err + }, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + now := time.Now() + require.NoError(t, tt.args.prepare(tt.args.req)) + got, err := Client.RemoveSecret(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + assert.NotEmpty(t, got.DeletionDate, "client secret is empty") + creationDate := got.DeletionDate.AsTime() + assert.Greater(t, creationDate, now, "creation date is before the test started") + assert.Less(t, creationDate, time.Now(), "creation date is in the future") + }) + } +} 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 4cf4ab21f8..4eee44ab44 100644 --- a/internal/api/grpc/user/v2/integration_test/user_test.go +++ b/internal/api/grpc/user/v2/integration_test/user_test.go @@ -57,7 +57,7 @@ func TestMain(m *testing.M) { }()) } -func TestServer_AddHumanUser(t *testing.T) { +func TestServer_Deprecated_AddHumanUser(t *testing.T) { idpResp := Instance.AddGenericOAuthProvider(IamCTX, Instance.DefaultOrg.Id) type args struct { ctx context.Context @@ -652,6 +652,7 @@ func TestServer_AddHumanUser(t *testing.T) { t.Run(tt.name, func(t *testing.T) { userID := fmt.Sprint(time.Now().UnixNano() + int64(i)) tt.args.req.UserId = &userID + // In order to prevent unique constraint errors, we set the email to a unique value if email := tt.args.req.GetEmail(); email != nil { email.Email = fmt.Sprintf("%s@me.now", userID) } @@ -666,7 +667,6 @@ func TestServer_AddHumanUser(t *testing.T) { return } require.NoError(t, err) - assert.Equal(t, tt.want.GetUserId(), got.GetUserId()) if tt.want.GetEmailCode() != "" { assert.NotEmpty(t, got.GetEmailCode()) @@ -683,7 +683,7 @@ func TestServer_AddHumanUser(t *testing.T) { } } -func TestServer_AddHumanUser_Permission(t *testing.T) { +func TestServer_Deprecated_AddHumanUser_Permission(t *testing.T) { newOrgOwnerEmail := gofakeit.Email() newOrg := Instance.CreateOrganization(IamCTX, fmt.Sprintf("AddHuman-%s", gofakeit.AppName()), newOrgOwnerEmail) type args struct { @@ -876,7 +876,7 @@ func TestServer_AddHumanUser_Permission(t *testing.T) { } } -func TestServer_UpdateHumanUser(t *testing.T) { +func TestServer_Deprecated_UpdateHumanUser(t *testing.T) { type args struct { ctx context.Context req *user.UpdateHumanUserRequest @@ -1237,7 +1237,7 @@ func TestServer_UpdateHumanUser(t *testing.T) { } } -func TestServer_UpdateHumanUser_Permission(t *testing.T) { +func TestServer_Deprecated_UpdateHumanUser_Permission(t *testing.T) { newOrgOwnerEmail := gofakeit.Email() newOrg := Instance.CreateOrganization(IamCTX, fmt.Sprintf("UpdateHuman-%s", gofakeit.AppName()), newOrgOwnerEmail) newUserID := newOrg.CreatedAdmins[0].GetUserId() @@ -1834,15 +1834,26 @@ func TestServer_DeleteUser(t *testing.T) { 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(), + removeUser, err := Client.CreateUser(CTX, &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + UserType: &user.CreateUserRequest_Human_{ + Human: &user.CreateUserRequest_Human{ + Profile: &user.SetHumanProfile{ + GivenName: "givenName", + FamilyName: "familyName", + }, + Email: &user.SetHumanEmail{ + Email: gofakeit.Email(), + Verification: &user.SetHumanEmail_IsVerified{IsVerified: true}, + }, + }, + }, }) - 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) + request.UserId = removeUser.Id + Instance.RegisterUserPasskey(CTX, removeUser.Id) + _, token, _, _ := Instance.CreateVerifiedWebAuthNSession(t, CTX, removeUser.Id) + return integration.WithAuthorizationToken(UserCTX, token) }, }, want: &user.DeleteUserResponse{ @@ -3610,7 +3621,6 @@ func TestServer_HumanMFAInitSkipped(t *testing.T) { t.Run(tt.name, func(t *testing.T) { err := tt.args.prepare(tt.args.req) require.NoError(t, err) - got, err := Client.HumanMFAInitSkipped(tt.args.ctx, tt.args.req) if tt.wantErr { require.Error(t, err) @@ -3624,3 +3634,1678 @@ func TestServer_HumanMFAInitSkipped(t *testing.T) { }) } } + +func TestServer_CreateUser(t *testing.T) { + type args struct { + ctx context.Context + req *user.CreateUserRequest + } + type testCase struct { + args args + want *user.CreateUserResponse + wantErr bool + } + tests := []struct { + name string + testCase func(runId string) testCase + }{ + { + name: "default verification", + testCase: func(runId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + email := username + "@example.com" + return testCase{ + args: args{ + CTX, + &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + Username: &username, + UserType: &user.CreateUserRequest_Human_{ + Human: &user.CreateUserRequest_Human{ + Profile: &user.SetHumanProfile{ + GivenName: "Donald", + FamilyName: "Duck", + }, + Email: &user.SetHumanEmail{ + Email: email, + }, + }, + }, + }, + }, + want: &user.CreateUserResponse{ + Id: "is generated", + }, + wantErr: false, + } + }, + }, + { + name: "return email verification code", + testCase: func(runId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + email := username + "@example.com" + return testCase{ + args: args{ + CTX, + &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + Username: &username, + UserType: &user.CreateUserRequest_Human_{ + Human: &user.CreateUserRequest_Human{ + Profile: &user.SetHumanProfile{ + GivenName: "Donald", + FamilyName: "Duck", + }, + Email: &user.SetHumanEmail{ + Email: email, + Verification: &user.SetHumanEmail_ReturnCode{ + ReturnCode: &user.ReturnEmailVerificationCode{}, + }, + }, + }, + }, + }, + }, + want: &user.CreateUserResponse{ + Id: "is generated", + EmailCode: gu.Ptr("something"), + }, + } + }, + }, + { + name: "custom template", + testCase: func(runId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + email := username + "@example.com" + return testCase{ + args: args{ + CTX, + &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + Username: &username, + UserType: &user.CreateUserRequest_Human_{ + Human: &user.CreateUserRequest_Human{ + Profile: &user.SetHumanProfile{ + GivenName: "Donald", + FamilyName: "Duck", + }, + Email: &user.SetHumanEmail{ + Email: email, + Verification: &user.SetHumanEmail_SendCode{ + SendCode: &user.SendEmailVerificationCode{ + UrlTemplate: gu.Ptr("https://example.com/email/verify?userID={{.UserID}}&code={{.Code}}&orgID={{.OrgID}}"), + }, + }, + }, + }, + }, + }, + }, + want: &user.CreateUserResponse{ + Id: "is generated", + }, + } + }, + }, + { + name: "return phone verification code", + testCase: func(runId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + email := username + "@example.com" + return testCase{ + args: args{ + CTX, + &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + Username: &username, + UserType: &user.CreateUserRequest_Human_{ + Human: &user.CreateUserRequest_Human{ + Profile: &user.SetHumanProfile{ + GivenName: "Donald", + FamilyName: "Duck", + }, + Email: &user.SetHumanEmail{ + Email: email, + }, + Phone: &user.SetHumanPhone{ + Phone: "+41791234567", + Verification: &user.SetHumanPhone_ReturnCode{ + ReturnCode: &user.ReturnPhoneVerificationCode{}, + }, + }, + }, + }, + }, + }, + want: &user.CreateUserResponse{ + Id: "is generated", + PhoneCode: gu.Ptr("something"), + }, + } + }, + }, + { + name: "custom template error", + testCase: func(runId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + email := username + "@example.com" + return testCase{ + args: args{ + CTX, + &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + Username: &username, + UserType: &user.CreateUserRequest_Human_{ + Human: &user.CreateUserRequest_Human{ + Profile: &user.SetHumanProfile{ + GivenName: "Donald", + FamilyName: "Duck", + }, + Email: &user.SetHumanEmail{ + Email: email, + Verification: &user.SetHumanEmail_SendCode{ + SendCode: &user.SendEmailVerificationCode{ + UrlTemplate: gu.Ptr("{{"), + }, + }, + }, + }, + }, + }, + }, + wantErr: true, + } + }, + }, + { + name: "missing REQUIRED profile", + testCase: func(runId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + email := username + "@example.com" + return testCase{ + args: args{ + CTX, + &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + Username: &username, + UserType: &user.CreateUserRequest_Human_{ + Human: &user.CreateUserRequest_Human{ + Email: &user.SetHumanEmail{ + Email: email, + Verification: &user.SetHumanEmail_ReturnCode{ + ReturnCode: &user.ReturnEmailVerificationCode{}, + }, + }, + }, + }, + }, + }, + wantErr: true, + } + }, + }, + { + name: "missing REQUIRED email", + testCase: func(runId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + return testCase{ + args: args{ + CTX, + &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + Username: &username, + UserType: &user.CreateUserRequest_Human_{ + Human: &user.CreateUserRequest_Human{ + Profile: &user.SetHumanProfile{ + GivenName: "Donald", + FamilyName: "Duck", + }, + }, + }, + }, + }, + wantErr: true, + } + }, + }, + { + name: "missing empty email", + testCase: func(runId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + return testCase{ + args: args{ + CTX, + &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + Username: &username, + UserType: &user.CreateUserRequest_Human_{ + Human: &user.CreateUserRequest_Human{ + Profile: &user.SetHumanProfile{ + GivenName: "Donald", + FamilyName: "Duck", + }, + Email: &user.SetHumanEmail{}, + }, + }, + }, + }, + wantErr: true, + } + }, + }, + { + name: "missing idp", + testCase: func(runId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + email := username + "@example.com" + return testCase{ + args: args{ + CTX, + &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + Username: &username, + UserType: &user.CreateUserRequest_Human_{ + Human: &user.CreateUserRequest_Human{ + Profile: &user.SetHumanProfile{ + GivenName: "Donald", + FamilyName: "Duck", + }, + Email: &user.SetHumanEmail{ + Email: email, + Verification: &user.SetHumanEmail_IsVerified{ + IsVerified: true, + }, + }, + IdpLinks: []*user.IDPLink{ + { + IdpId: "idpID", + UserId: "userID", + UserName: "username", + }, + }, + }, + }, + }, + }, + wantErr: true, + } + }, + }, + { + name: "with idp", + testCase: func(runId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + email := username + "@example.com" + idpResp := Instance.AddGenericOAuthProvider(IamCTX, Instance.DefaultOrg.Id) + return testCase{ + args: args{ + CTX, + &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + Username: &username, + UserType: &user.CreateUserRequest_Human_{ + Human: &user.CreateUserRequest_Human{ + Profile: &user.SetHumanProfile{ + GivenName: "Donald", + FamilyName: "Duck", + }, + Email: &user.SetHumanEmail{ + Email: email, + Verification: &user.SetHumanEmail_IsVerified{ + IsVerified: true, + }, + }, + IdpLinks: []*user.IDPLink{ + { + IdpId: idpResp.Id, + UserId: "userID", + UserName: "username", + }, + }, + }, + }, + }, + }, + want: &user.CreateUserResponse{ + Id: "is generated", + }, + } + }, + }, + { + name: "with totp", + testCase: func(runId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + email := username + "@example.com" + return testCase{ + args: args{ + CTX, + &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + Username: &username, + UserType: &user.CreateUserRequest_Human_{ + Human: &user.CreateUserRequest_Human{ + Profile: &user.SetHumanProfile{ + GivenName: "Donald", + FamilyName: "Duck", + }, + Email: &user.SetHumanEmail{ + Email: email, + Verification: &user.SetHumanEmail_IsVerified{ + IsVerified: true, + }, + }, + TotpSecret: gu.Ptr("secret"), + }, + }, + }, + }, + want: &user.CreateUserResponse{ + Id: "is generated", + }, + } + }, + }, + { + name: "password not complexity conform", + testCase: func(runId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + email := username + "@example.com" + return testCase{ + args: args{ + CTX, + &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + Username: &username, + UserType: &user.CreateUserRequest_Human_{ + Human: &user.CreateUserRequest_Human{ + Profile: &user.SetHumanProfile{ + GivenName: "Donald", + FamilyName: "Duck", + NickName: gu.Ptr("Dukkie"), + DisplayName: gu.Ptr("Donald Duck"), + PreferredLanguage: gu.Ptr("en"), + Gender: user.Gender_GENDER_DIVERSE.Enum(), + }, + Email: &user.SetHumanEmail{ + Email: email, + }, + PasswordType: &user.CreateUserRequest_Human_Password{ + Password: &user.Password{ + Password: "insufficient", + }, + }, + }, + }, + }, + }, + wantErr: true, + } + }, + }, + { + name: "hashed password", + testCase: func(runId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + email := username + "@example.com" + return testCase{ + args: args{ + CTX, + &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + Username: &username, + UserType: &user.CreateUserRequest_Human_{ + Human: &user.CreateUserRequest_Human{ + Profile: &user.SetHumanProfile{ + GivenName: "Donald", + FamilyName: "Duck", + }, + Email: &user.SetHumanEmail{ + Email: email, + }, + PasswordType: &user.CreateUserRequest_Human_HashedPassword{ + HashedPassword: &user.HashedPassword{ + Hash: "$2y$12$hXUrnqdq1RIIYZ2HPytIIe5lXdIvbhqrTvdPsSF7o.jFh817Z6lwm", + }, + }, + }, + }, + }, + }, + want: &user.CreateUserResponse{ + Id: "is generated", + }, + } + }, + }, + { + name: "unsupported hashed password", + testCase: func(runId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + email := username + "@example.com" + return testCase{ + args: args{ + CTX, + &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + Username: &username, + UserType: &user.CreateUserRequest_Human_{ + Human: &user.CreateUserRequest_Human{ + Profile: &user.SetHumanProfile{ + GivenName: "Donald", + FamilyName: "Duck", + }, + Email: &user.SetHumanEmail{ + Email: email, + }, + PasswordType: &user.CreateUserRequest_Human_HashedPassword{ + HashedPassword: &user.HashedPassword{ + Hash: "$scrypt$ln=16,r=8,p=1$cmFuZG9tc2FsdGlzaGFyZA$Rh+NnJNo1I6nRwaNqbDm6kmADswD1+7FTKZ7Ln9D8nQ", + }, + }, + }, + }, + }, + }, + wantErr: true, + } + }, + }, + { + name: "human default username", + testCase: func(runId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + email := username + "@example.com" + return testCase{ + args: args{ + CTX, + &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + UserType: &user.CreateUserRequest_Human_{ + Human: &user.CreateUserRequest_Human{ + Profile: &user.SetHumanProfile{ + GivenName: "Donald", + FamilyName: "Duck", + }, + Email: &user.SetHumanEmail{ + Email: email, + }, + }, + }, + }, + }, + want: &user.CreateUserResponse{ + Id: "is generated", + }, + } + }, + }, + { + name: "machine user", + testCase: func(runId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + return testCase{ + args: args{ + CTX, + &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + Username: &username, + UserType: &user.CreateUserRequest_Machine_{ + Machine: &user.CreateUserRequest_Machine{ + Name: "donald", + }, + }, + }, + }, + want: &user.CreateUserResponse{ + Id: "is generated", + }, + } + }, + }, + { + name: "machine default username to generated id", + testCase: func(runId string) testCase { + return testCase{ + args: args{ + CTX, + &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + UserType: &user.CreateUserRequest_Machine_{ + Machine: &user.CreateUserRequest_Machine{ + Name: "donald", + }, + }, + }, + }, + want: &user.CreateUserResponse{ + Id: "is generated", + }, + } + }, + }, + { + name: "machine default username to given id", + testCase: func(runId string) testCase { + return testCase{ + args: args{ + CTX, + &user.CreateUserRequest{ + UserId: &runId, + OrganizationId: Instance.DefaultOrg.Id, + UserType: &user.CreateUserRequest_Machine_{ + Machine: &user.CreateUserRequest_Machine{ + Name: "donald", + }, + }, + }, + }, + want: &user.CreateUserResponse{ + Id: runId, + }, + } + }, + }, + { + name: "org does not exist human, error", + testCase: func(runId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + email := username + "@example.com" + return testCase{ + args: args{ + CTX, + &user.CreateUserRequest{ + OrganizationId: "does not exist", + Username: &username, + UserType: &user.CreateUserRequest_Human_{ + Human: &user.CreateUserRequest_Human{ + Profile: &user.SetHumanProfile{ + GivenName: "Donald", + FamilyName: "Duck", + }, + Email: &user.SetHumanEmail{ + Email: email, + }, + }, + }, + }, + }, + wantErr: true, + } + }, + }, + { + name: "org does not exist machine, error", + testCase: func(runId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + return testCase{ + args: args{ + CTX, + &user.CreateUserRequest{ + OrganizationId: "does not exist", + Username: &username, + UserType: &user.CreateUserRequest_Machine_{ + Machine: &user.CreateUserRequest_Machine{ + Name: gofakeit.Name(), + }, + }, + }, + }, + wantErr: true, + } + }, + }, + } + for i, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + now := time.Now() + runId := fmt.Sprint(now.UnixNano() + int64(i)) + test := tt.testCase(runId) + got, err := Client.CreateUser(test.args.ctx, test.args.req) + if test.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + creationDate := got.CreationDate.AsTime() + assert.Greater(t, creationDate, now, "creation date is before the test started") + assert.Less(t, creationDate, time.Now(), "creation date is in the future") + if test.want.GetEmailCode() != "" { + assert.NotEmpty(t, got.GetEmailCode(), "email code is empty") + } else { + assert.Empty(t, got.GetEmailCode(), "email code is not empty") + } + if test.want.GetPhoneCode() != "" { + assert.NotEmpty(t, got.GetPhoneCode(), "phone code is empty") + } else { + assert.Empty(t, got.GetPhoneCode(), "phone code is not empty") + } + if test.want.GetId() == "is generated" { + assert.Len(t, got.GetId(), 18, "ID is not 18 characters") + } else { + assert.Equal(t, test.want.GetId(), got.GetId(), "ID is not the same") + } + }) + } +} + +func TestServer_CreateUser_And_Compare(t *testing.T) { + type args struct { + ctx context.Context + req *user.CreateUserRequest + } + type testCase struct { + name string + args args + assert func(t *testing.T, createResponse *user.CreateUserResponse, getResponse *user.GetUserByIDResponse) + } + tests := []struct { + name string + testCase func(runId string) testCase + }{{ + name: "human given username", + testCase: func(runId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + email := username + "@example.com" + return testCase{ + args: args{ + ctx: CTX, + req: &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + Username: &username, + UserType: &user.CreateUserRequest_Human_{ + Human: &user.CreateUserRequest_Human{ + Profile: &user.SetHumanProfile{ + GivenName: "Donald", + FamilyName: "Duck", + }, + Email: &user.SetHumanEmail{ + Email: email, + }, + }, + }, + }, + }, + assert: func(t *testing.T, _ *user.CreateUserResponse, getResponse *user.GetUserByIDResponse) { + assert.Equal(t, username, getResponse.GetUser().GetUsername()) + }, + } + }, + }, { + name: "human username default to email", + testCase: func(runId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + email := username + "@example.com" + return testCase{ + args: args{ + ctx: CTX, + req: &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + UserType: &user.CreateUserRequest_Human_{ + Human: &user.CreateUserRequest_Human{ + Profile: &user.SetHumanProfile{ + GivenName: "Donald", + FamilyName: "Duck", + }, + Email: &user.SetHumanEmail{ + Email: email, + }, + }, + }, + }, + }, + assert: func(t *testing.T, _ *user.CreateUserResponse, getResponse *user.GetUserByIDResponse) { + assert.Equal(t, email, getResponse.GetUser().GetUsername()) + }, + } + }, + }, { + name: "machine username given", + testCase: func(runId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + return testCase{ + args: args{ + ctx: CTX, + req: &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + Username: &username, + UserType: &user.CreateUserRequest_Machine_{ + Machine: &user.CreateUserRequest_Machine{ + Name: "donald", + }, + }, + }, + }, + assert: func(t *testing.T, _ *user.CreateUserResponse, getResponse *user.GetUserByIDResponse) { + assert.Equal(t, username, getResponse.GetUser().GetUsername()) + }, + } + }, + }, { + name: "machine username default to generated id", + testCase: func(runId string) testCase { + return testCase{ + args: args{ + ctx: CTX, + req: &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + UserType: &user.CreateUserRequest_Machine_{ + Machine: &user.CreateUserRequest_Machine{ + Name: "donald", + }, + }, + }, + }, + assert: func(t *testing.T, createResponse *user.CreateUserResponse, getResponse *user.GetUserByIDResponse) { + assert.Equal(t, createResponse.GetId(), getResponse.GetUser().GetUsername()) + }, + } + }, + }, { + name: "machine username default to given id", + testCase: func(runId string) testCase { + return testCase{ + args: args{ + ctx: CTX, + req: &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + UserId: &runId, + UserType: &user.CreateUserRequest_Machine_{ + Machine: &user.CreateUserRequest_Machine{ + Name: "donald", + }, + }, + }, + }, + assert: func(t *testing.T, createResponse *user.CreateUserResponse, getResponse *user.GetUserByIDResponse) { + assert.Equal(t, runId, getResponse.GetUser().GetUsername()) + }, + } + }, + }} + for i, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + now := time.Now() + runId := fmt.Sprint(now.UnixNano() + int64(i)) + test := tt.testCase(runId) + createResponse, err := Client.CreateUser(test.args.ctx, test.args.req) + require.NoError(t, err) + Instance.TriggerUserByID(test.args.ctx, createResponse.GetId()) + getResponse, err := Client.GetUserByID(test.args.ctx, &user.GetUserByIDRequest{ + UserId: createResponse.GetId(), + }) + require.NoError(t, err) + test.assert(t, createResponse, getResponse) + }) + } +} + +func TestServer_CreateUser_Permission(t *testing.T) { + newOrgOwnerEmail := gofakeit.Email() + newOrg := Instance.CreateOrganization(IamCTX, fmt.Sprintf("AddHuman-%s", gofakeit.AppName()), newOrgOwnerEmail) + type args struct { + ctx context.Context + req *user.CreateUserRequest + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "human system, ok", + args: args{ + SystemCTX, + &user.CreateUserRequest{ + OrganizationId: newOrg.GetOrganizationId(), + UserType: &user.CreateUserRequest_Human_{ + Human: &user.CreateUserRequest_Human{ + Profile: &user.SetHumanProfile{ + GivenName: "Donald", + FamilyName: "Duck", + }, + Email: &user.SetHumanEmail{ + Email: "this is overwritten with a unique address", + }, + }, + }, + }, + }, + }, + { + name: "human instance, ok", + args: args{ + IamCTX, + &user.CreateUserRequest{ + OrganizationId: newOrg.GetOrganizationId(), + UserType: &user.CreateUserRequest_Human_{ + Human: &user.CreateUserRequest_Human{ + Profile: &user.SetHumanProfile{ + GivenName: "Donald", + FamilyName: "Duck", + }, + Email: &user.SetHumanEmail{ + Email: "this is overwritten with a unique address", + }, + }, + }, + }, + }, + }, + { + name: "human org, error", + args: args{ + CTX, + &user.CreateUserRequest{ + OrganizationId: newOrg.GetOrganizationId(), + UserType: &user.CreateUserRequest_Human_{ + Human: &user.CreateUserRequest_Human{ + Profile: &user.SetHumanProfile{ + GivenName: "Donald", + FamilyName: "Duck", + }, + Email: &user.SetHumanEmail{ + Email: "this is overwritten with a unique address", + }, + }, + }, + }, + }, + wantErr: true, + }, + { + name: "human user, error", + args: args{ + UserCTX, + &user.CreateUserRequest{ + OrganizationId: newOrg.GetOrganizationId(), + UserType: &user.CreateUserRequest_Human_{ + Human: &user.CreateUserRequest_Human{ + Profile: &user.SetHumanProfile{ + GivenName: "Donald", + FamilyName: "Duck", + }, + Email: &user.SetHumanEmail{ + Email: "this is overwritten with a unique address", + }, + }, + }, + }, + }, + wantErr: true, + }, + { + name: "machine system, ok", + args: args{ + SystemCTX, + &user.CreateUserRequest{ + OrganizationId: newOrg.GetOrganizationId(), + UserType: &user.CreateUserRequest_Machine_{ + Machine: &user.CreateUserRequest_Machine{ + Name: "donald", + }, + }, + }, + }, + }, + { + name: "machine instance, ok", + args: args{ + IamCTX, + &user.CreateUserRequest{ + OrganizationId: newOrg.GetOrganizationId(), + UserType: &user.CreateUserRequest_Machine_{ + Machine: &user.CreateUserRequest_Machine{ + Name: "donald", + }, + }, + }, + }, + }, + { + name: "machine org, error", + args: args{ + CTX, + &user.CreateUserRequest{ + OrganizationId: newOrg.GetOrganizationId(), + UserType: &user.CreateUserRequest_Machine_{ + Machine: &user.CreateUserRequest_Machine{ + Name: "donald", + }, + }, + }, + }, + wantErr: true, + }, + { + name: "machine user, error", + args: args{ + UserCTX, + &user.CreateUserRequest{ + OrganizationId: newOrg.GetOrganizationId(), + UserType: &user.CreateUserRequest_Machine_{ + Machine: &user.CreateUserRequest_Machine{ + Name: "donald", + }, + }, + }, + }, + wantErr: true, + }, + } + for i, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + userID := fmt.Sprint(time.Now().UnixNano() + int64(i)) + tt.args.req.UserId = &userID + if email := tt.args.req.GetHuman().GetEmail(); email != nil { + email.Email = fmt.Sprintf("%s@example.com", userID) + } + _, err := Client.CreateUser(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + }) + } +} + +func TestServer_UpdateUserTypeHuman(t *testing.T) { + type args struct { + ctx context.Context + req *user.UpdateUserRequest + } + type testCase struct { + args args + want *user.UpdateUserResponse + wantErr bool + } + tests := []struct { + name string + testCase func(runId, userId string) testCase + }{ + { + name: "default verification", + testCase: func(runId, userId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + email := username + "@example.com" + return testCase{ + args: args{ + CTX, + &user.UpdateUserRequest{ + UserId: userId, + UserType: &user.UpdateUserRequest_Human_{ + Human: &user.UpdateUserRequest_Human{ + Email: &user.SetHumanEmail{ + Email: email, + }, + }, + }, + }, + }, + want: &user.UpdateUserResponse{}, + wantErr: false, + } + }, + }, + { + name: "return email verification code", + testCase: func(runId, userId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + email := username + "@example.com" + return testCase{ + args: args{ + CTX, + &user.UpdateUserRequest{ + UserId: userId, + UserType: &user.UpdateUserRequest_Human_{ + Human: &user.UpdateUserRequest_Human{ + Email: &user.SetHumanEmail{ + Email: email, + Verification: &user.SetHumanEmail_ReturnCode{ + ReturnCode: &user.ReturnEmailVerificationCode{}, + }, + }, + }, + }, + }, + }, + want: &user.UpdateUserResponse{ + EmailCode: gu.Ptr("something"), + }, + } + }, + }, + { + name: "custom template", + testCase: func(runId, userId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + email := username + "@example.com" + return testCase{ + args: args{ + CTX, + &user.UpdateUserRequest{ + UserId: userId, + UserType: &user.UpdateUserRequest_Human_{ + Human: &user.UpdateUserRequest_Human{ + Email: &user.SetHumanEmail{ + Email: email, + Verification: &user.SetHumanEmail_SendCode{ + SendCode: &user.SendEmailVerificationCode{ + UrlTemplate: gu.Ptr("https://example.com/email/verify?userID={{.UserID}}&code={{.Code}}&orgID={{.OrgID}}"), + }, + }, + }, + }, + }, + }, + }, + want: &user.UpdateUserResponse{}, + } + }, + }, + { + name: "return phone verification code", + testCase: func(runId, userId string) testCase { + return testCase{ + args: args{ + CTX, + &user.UpdateUserRequest{ + UserId: userId, + UserType: &user.UpdateUserRequest_Human_{ + Human: &user.UpdateUserRequest_Human{ + Phone: &user.SetHumanPhone{ + Phone: "+41791234568", + Verification: &user.SetHumanPhone_ReturnCode{ + ReturnCode: &user.ReturnPhoneVerificationCode{}, + }, + }, + }, + }, + }, + }, + want: &user.UpdateUserResponse{ + PhoneCode: gu.Ptr("something"), + }, + } + }, + }, + { + name: "custom template error", + testCase: func(runId, userId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + email := username + "@example.com" + return testCase{ + args: args{ + CTX, + &user.UpdateUserRequest{ + UserId: userId, + UserType: &user.UpdateUserRequest_Human_{ + Human: &user.UpdateUserRequest_Human{ + Email: &user.SetHumanEmail{ + Email: email, + Verification: &user.SetHumanEmail_SendCode{ + SendCode: &user.SendEmailVerificationCode{ + UrlTemplate: gu.Ptr("{{"), + }, + }, + }, + }, + }, + }, + }, + wantErr: true, + } + }, + }, + { + name: "missing empty email", + testCase: func(runId, userId string) testCase { + return testCase{ + args: args{ + CTX, + &user.UpdateUserRequest{ + UserId: userId, + UserType: &user.UpdateUserRequest_Human_{ + Human: &user.UpdateUserRequest_Human{ + Email: &user.SetHumanEmail{}, + }, + }, + }, + }, + wantErr: true, + } + }, + }, + { + name: "password not complexity conform", + testCase: func(runId, userId string) testCase { + return testCase{ + args: args{ + CTX, + &user.UpdateUserRequest{ + UserId: userId, + UserType: &user.UpdateUserRequest_Human_{ + Human: &user.UpdateUserRequest_Human{ + Password: &user.SetPassword{ + PasswordType: &user.SetPassword_Password{ + Password: &user.Password{ + Password: "insufficient", + }, + }, + }, + }, + }, + }, + }, + wantErr: true, + } + }, + }, + { + name: "hashed password", + testCase: func(runId, userId string) testCase { + return testCase{ + args: args{ + CTX, + &user.UpdateUserRequest{ + UserId: userId, + UserType: &user.UpdateUserRequest_Human_{ + Human: &user.UpdateUserRequest_Human{ + Password: &user.SetPassword{ + PasswordType: &user.SetPassword_HashedPassword{ + HashedPassword: &user.HashedPassword{ + Hash: "$2y$12$hXUrnqdq1RIIYZ2HPytIIe5lXdIvbhqrTvdPsSF7o.jFh817Z6lwm", + }, + }, + }, + }, + }, + }, + }, + want: &user.UpdateUserResponse{}, + } + }, + }, + { + name: "unsupported hashed password", + testCase: func(runId, userId string) testCase { + return testCase{ + args: args{ + CTX, + &user.UpdateUserRequest{ + UserId: userId, + UserType: &user.UpdateUserRequest_Human_{ + Human: &user.UpdateUserRequest_Human{ + Password: &user.SetPassword{ + PasswordType: &user.SetPassword_HashedPassword{ + HashedPassword: &user.HashedPassword{ + Hash: "$scrypt$ln=16,r=8,p=1$cmFuZG9tc2FsdGlzaGFyZA$Rh+NnJNo1I6nRwaNqbDm6kmADswD1+7FTKZ7Ln9D8nQ", + }, + }, + }, + }, + }, + }, + }, + wantErr: true, + } + }, + }, + { + name: "update human user with machine fields, error", + testCase: func(runId, userId string) testCase { + return testCase{ + args: args{ + CTX, + &user.UpdateUserRequest{ + UserId: userId, + UserType: &user.UpdateUserRequest_Machine_{ + Machine: &user.UpdateUserRequest_Machine{ + Name: &runId, + }, + }, + }, + }, + wantErr: true, + } + }, + }, + } + for i, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + now := time.Now() + runId := fmt.Sprint(now.UnixNano() + int64(i)) + userId := Instance.CreateUserTypeHuman(CTX).GetId() + test := tt.testCase(runId, userId) + got, err := Client.UpdateUser(test.args.ctx, test.args.req) + if test.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + changeDate := got.ChangeDate.AsTime() + assert.Greater(t, changeDate, now, "change date is before the test started") + assert.Less(t, changeDate, time.Now(), "change date is in the future") + if test.want.GetEmailCode() != "" { + assert.NotEmpty(t, got.GetEmailCode(), "email code is empty") + } else { + assert.Empty(t, got.GetEmailCode(), "email code is not empty") + } + if test.want.GetPhoneCode() != "" { + assert.NotEmpty(t, got.GetPhoneCode(), "phone code is empty") + } else { + assert.Empty(t, got.GetPhoneCode(), "phone code is not empty") + } + }) + } +} + +func TestServer_UpdateUserTypeMachine(t *testing.T) { + type args struct { + ctx context.Context + req *user.UpdateUserRequest + } + type testCase struct { + args args + wantErr bool + } + tests := []struct { + name string + testCase func(runId, userId string) testCase + }{ + { + name: "update machine, ok", + testCase: func(runId, userId string) testCase { + return testCase{ + args: args{ + CTX, + &user.UpdateUserRequest{ + UserId: userId, + UserType: &user.UpdateUserRequest_Machine_{ + Machine: &user.UpdateUserRequest_Machine{ + Name: gu.Ptr("donald"), + }, + }, + }, + }, + } + }, + }, + { + name: "update machine user with human fields, error", + testCase: func(runId, userId string) testCase { + return testCase{ + args: args{ + CTX, + &user.UpdateUserRequest{ + UserId: userId, + UserType: &user.UpdateUserRequest_Human_{ + Human: &user.UpdateUserRequest_Human{ + Profile: &user.UpdateUserRequest_Human_Profile{ + GivenName: gu.Ptr("Donald"), + }, + }, + }, + }, + }, + wantErr: true, + } + }, + }, + } + for i, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + now := time.Now() + runId := fmt.Sprint(now.UnixNano() + int64(i)) + userId := Instance.CreateUserTypeMachine(CTX).GetId() + test := tt.testCase(runId, userId) + got, err := Client.UpdateUser(test.args.ctx, test.args.req) + if test.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + changeDate := got.ChangeDate.AsTime() + assert.Greater(t, changeDate, now, "change date is before the test started") + assert.Less(t, changeDate, time.Now(), "change date is in the future") + }) + } +} + +func TestServer_UpdateUser_And_Compare(t *testing.T) { + type args struct { + ctx context.Context + create *user.CreateUserRequest + update *user.UpdateUserRequest + } + type testCase struct { + args args + assert func(t *testing.T, getResponse *user.GetUserByIDResponse) + } + tests := []struct { + name string + testCase func(runId string) testCase + }{{ + name: "human remove phone", + testCase: func(runId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + email := username + "@example.com" + return testCase{ + args: args{ + ctx: CTX, + create: &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + UserId: &runId, + UserType: &user.CreateUserRequest_Human_{ + Human: &user.CreateUserRequest_Human{ + Profile: &user.SetHumanProfile{ + GivenName: "Donald", + FamilyName: "Duck", + }, + Email: &user.SetHumanEmail{ + Email: email, + }, + Phone: &user.SetHumanPhone{ + Phone: "+1234567890", + }, + }, + }, + }, + update: &user.UpdateUserRequest{ + UserId: runId, + UserType: &user.UpdateUserRequest_Human_{ + Human: &user.UpdateUserRequest_Human{ + Phone: &user.SetHumanPhone{}, + }, + }, + }, + }, + assert: func(t *testing.T, getResponse *user.GetUserByIDResponse) { + assert.Empty(t, getResponse.GetUser().GetHuman().GetPhone().GetPhone(), "phone is not empty") + }, + } + }, + }, { + name: "human username", + testCase: func(runId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + email := username + "@example.com" + return testCase{ + args: args{ + ctx: CTX, + create: &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + UserId: &runId, + UserType: &user.CreateUserRequest_Human_{ + Human: &user.CreateUserRequest_Human{ + Profile: &user.SetHumanProfile{ + GivenName: "Donald", + FamilyName: "Duck", + }, + Email: &user.SetHumanEmail{ + Email: email, + }, + }, + }, + }, + update: &user.UpdateUserRequest{ + UserId: runId, + Username: &username, + UserType: &user.UpdateUserRequest_Human_{ + Human: &user.UpdateUserRequest_Human{}, + }, + }, + }, + assert: func(t *testing.T, getResponse *user.GetUserByIDResponse) { + assert.Equal(t, username, getResponse.GetUser().GetUsername()) + }, + } + }, + }, { + name: "machine username", + testCase: func(runId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + return testCase{ + args: args{ + ctx: CTX, + create: &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + UserId: &runId, + UserType: &user.CreateUserRequest_Machine_{ + Machine: &user.CreateUserRequest_Machine{ + Name: "Donald", + }, + }, + }, + update: &user.UpdateUserRequest{ + UserId: runId, + Username: &username, + UserType: &user.UpdateUserRequest_Machine_{ + Machine: &user.UpdateUserRequest_Machine{}, + }, + }, + }, + assert: func(t *testing.T, getResponse *user.GetUserByIDResponse) { + assert.Equal(t, username, getResponse.GetUser().GetUsername()) + }, + } + }, + }} + for i, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + now := time.Now() + runId := fmt.Sprint(now.UnixNano() + int64(i)) + test := tt.testCase(runId) + createResponse, err := Client.CreateUser(test.args.ctx, test.args.create) + require.NoError(t, err) + _, err = Client.UpdateUser(test.args.ctx, test.args.update) + require.NoError(t, err) + Instance.TriggerUserByID(test.args.ctx, createResponse.GetId()) + getResponse, err := Client.GetUserByID(test.args.ctx, &user.GetUserByIDRequest{ + UserId: createResponse.GetId(), + }) + require.NoError(t, err) + test.assert(t, getResponse) + }) + } +} + +func TestServer_UpdateUser_Permission(t *testing.T) { + newOrgOwnerEmail := gofakeit.Email() + newOrg := Instance.CreateOrganization(IamCTX, fmt.Sprintf("AddHuman-%s", gofakeit.AppName()), newOrgOwnerEmail) + newHumanUserID := newOrg.CreatedAdmins[0].GetUserId() + machineUserResp, err := Instance.Client.UserV2.CreateUser(IamCTX, &user.CreateUserRequest{ + OrganizationId: newOrg.OrganizationId, + UserType: &user.CreateUserRequest_Machine_{ + Machine: &user.CreateUserRequest_Machine{ + Name: "Donald", + }, + }, + }) + require.NoError(t, err) + newMachineUserID := machineUserResp.GetId() + Instance.TriggerUserByID(IamCTX, newMachineUserID) + type args struct { + ctx context.Context + req *user.UpdateUserRequest + } + type testCase struct { + args args + wantErr bool + } + tests := []struct { + name string + testCase func() testCase + }{ + { + name: "human, system, ok", + testCase: func() testCase { + return testCase{ + args: args{ + SystemCTX, + &user.UpdateUserRequest{ + UserId: newHumanUserID, + UserType: &user.UpdateUserRequest_Human_{ + Human: &user.UpdateUserRequest_Human{ + Profile: &user.UpdateUserRequest_Human_Profile{ + GivenName: gu.Ptr("Donald"), + }, + }, + }, + }, + }, + } + }, + }, + { + name: "human instance, ok", + testCase: func() testCase { + return testCase{ + args: args{ + IamCTX, + &user.UpdateUserRequest{ + UserId: newHumanUserID, + UserType: &user.UpdateUserRequest_Human_{ + Human: &user.UpdateUserRequest_Human{ + Profile: &user.UpdateUserRequest_Human_Profile{ + GivenName: gu.Ptr("Donald"), + }, + }, + }, + }, + }, + } + }, + }, + { + name: "human org, error", + testCase: func() testCase { + return testCase{ + args: args{ + CTX, + &user.UpdateUserRequest{ + UserId: newHumanUserID, + UserType: &user.UpdateUserRequest_Human_{ + Human: &user.UpdateUserRequest_Human{ + Profile: &user.UpdateUserRequest_Human_Profile{ + GivenName: gu.Ptr("Donald"), + }, + }, + }, + }, + }, + wantErr: true, + } + }, + }, + { + name: "human user, error", + testCase: func() testCase { + return testCase{ + args: args{ + UserCTX, + &user.UpdateUserRequest{ + UserId: newHumanUserID, + UserType: &user.UpdateUserRequest_Human_{ + Human: &user.UpdateUserRequest_Human{ + Profile: &user.UpdateUserRequest_Human_Profile{ + GivenName: gu.Ptr("Donald"), + }, + }, + }, + }, + }, + wantErr: true, + } + }, + }, + { + name: "machine system, ok", + testCase: func() testCase { + return testCase{ + args: args{ + SystemCTX, + &user.UpdateUserRequest{ + UserId: newMachineUserID, + UserType: &user.UpdateUserRequest_Machine_{ + Machine: &user.UpdateUserRequest_Machine{ + Name: gu.Ptr("Donald"), + }, + }, + }, + }, + } + }, + }, + { + name: "machine instance, ok", + testCase: func() testCase { + return testCase{ + args: args{ + IamCTX, + &user.UpdateUserRequest{ + UserId: newMachineUserID, + UserType: &user.UpdateUserRequest_Machine_{ + Machine: &user.UpdateUserRequest_Machine{ + Name: gu.Ptr("Donald"), + }, + }, + }, + }, + } + }, + }, + { + name: "machine org, error", + testCase: func() testCase { + return testCase{ + args: args{ + CTX, + &user.UpdateUserRequest{ + UserId: newMachineUserID, + UserType: &user.UpdateUserRequest_Machine_{ + Machine: &user.UpdateUserRequest_Machine{ + Name: gu.Ptr("Donald"), + }, + }, + }, + }, + wantErr: true, + } + }, + }, + { + name: "machine user, error", + testCase: func() testCase { + return testCase{ + args: args{ + UserCTX, + &user.UpdateUserRequest{ + UserId: newMachineUserID, + UserType: &user.UpdateUserRequest_Machine_{ + Machine: &user.UpdateUserRequest_Machine{ + Name: gu.Ptr("Donald"), + }, + }, + }, + }, + wantErr: true, + } + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + test := tt.testCase() + _, err := Client.UpdateUser(test.args.ctx, test.args.req) + if test.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + }) + } +} diff --git a/internal/api/grpc/user/v2/key.go b/internal/api/grpc/user/v2/key.go new file mode 100644 index 0000000000..59dab44248 --- /dev/null +++ b/internal/api/grpc/user/v2/key.go @@ -0,0 +1,62 @@ +package user + +import ( + "context" + + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/eventstore/v1/models" + "github.com/zitadel/zitadel/pkg/grpc/user/v2" +) + +func (s *Server) AddKey(ctx context.Context, req *user.AddKeyRequest) (*user.AddKeyResponse, error) { + newMachineKey := &command.MachineKey{ + ObjectRoot: models.ObjectRoot{ + AggregateID: req.UserId, + }, + ExpirationDate: req.GetExpirationDate().AsTime(), + Type: domain.AuthNKeyTypeJSON, + PermissionCheck: s.command.NewPermissionCheckUserWrite(ctx), + } + newMachineKey.PublicKey = req.PublicKey + + pubkeySupplied := len(newMachineKey.PublicKey) > 0 + details, err := s.command.AddUserMachineKey(ctx, newMachineKey) + if err != nil { + return nil, err + } + // Return key details only if the pubkey wasn't supplied, otherwise the user already has + // private key locally + var keyDetails []byte + if !pubkeySupplied { + var err error + keyDetails, err = newMachineKey.Detail() + if err != nil { + return nil, err + } + } + return &user.AddKeyResponse{ + KeyId: newMachineKey.KeyID, + KeyContent: keyDetails, + CreationDate: timestamppb.New(details.EventDate), + }, nil +} + +func (s *Server) RemoveKey(ctx context.Context, req *user.RemoveKeyRequest) (*user.RemoveKeyResponse, error) { + machineKey := &command.MachineKey{ + ObjectRoot: models.ObjectRoot{ + AggregateID: req.UserId, + }, + PermissionCheck: s.command.NewPermissionCheckUserWrite(ctx), + KeyID: req.KeyId, + } + objectDetails, err := s.command.RemoveUserMachineKey(ctx, machineKey) + if err != nil { + return nil, err + } + return &user.RemoveKeyResponse{ + DeletionDate: timestamppb.New(objectDetails.EventDate), + }, nil +} diff --git a/internal/api/grpc/user/v2/key_query.go b/internal/api/grpc/user/v2/key_query.go new file mode 100644 index 0000000000..da4f47decf --- /dev/null +++ b/internal/api/grpc/user/v2/key_query.go @@ -0,0 +1,124 @@ +package user + +import ( + "context" + + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/api/grpc/filter/v2" + "github.com/zitadel/zitadel/internal/query" + "github.com/zitadel/zitadel/internal/zerrors" + filter_pb "github.com/zitadel/zitadel/pkg/grpc/filter/v2" + "github.com/zitadel/zitadel/pkg/grpc/user/v2" +) + +func (s *Server) ListKeys(ctx context.Context, req *user.ListKeysRequest) (*user.ListKeysResponse, error) { + offset, limit, asc, err := filter.PaginationPbToQuery(s.systemDefaults, req.Pagination) + if err != nil { + return nil, err + } + + filters, err := keyFiltersToQueries(req.Filters) + if err != nil { + return nil, err + } + search := &query.AuthNKeySearchQueries{ + SearchRequest: query.SearchRequest{ + Offset: offset, + Limit: limit, + Asc: asc, + SortingColumn: authnKeyFieldNameToSortingColumn(req.SortingColumn), + }, + Queries: filters, + } + result, err := s.query.SearchAuthNKeys(ctx, search, query.JoinFilterUserMachine, s.checkPermission) + if err != nil { + return nil, err + } + resp := &user.ListKeysResponse{ + Result: make([]*user.Key, len(result.AuthNKeys)), + Pagination: filter.QueryToPaginationPb(search.SearchRequest, result.SearchResponse), + } + for i, key := range result.AuthNKeys { + resp.Result[i] = &user.Key{ + CreationDate: timestamppb.New(key.CreationDate), + ChangeDate: timestamppb.New(key.ChangeDate), + Id: key.ID, + UserId: key.AggregateID, + OrganizationId: key.ResourceOwner, + ExpirationDate: timestamppb.New(key.Expiration), + } + } + return resp, nil +} + +func keyFiltersToQueries(filters []*user.KeysSearchFilter) (_ []query.SearchQuery, err error) { + q := make([]query.SearchQuery, len(filters)) + for i, filter := range filters { + q[i], err = keyFilterToQuery(filter) + if err != nil { + return nil, err + } + } + return q, nil +} + +func keyFilterToQuery(filter *user.KeysSearchFilter) (query.SearchQuery, error) { + switch q := filter.Filter.(type) { + case *user.KeysSearchFilter_CreatedDateFilter: + return authnKeyCreatedFilterToQuery(q.CreatedDateFilter) + case *user.KeysSearchFilter_ExpirationDateFilter: + return authnKeyExpirationFilterToQuery(q.ExpirationDateFilter) + case *user.KeysSearchFilter_KeyIdFilter: + return authnKeyIdFilterToQuery(q.KeyIdFilter) + case *user.KeysSearchFilter_UserIdFilter: + return authnKeyUserIdFilterToQuery(q.UserIdFilter) + case *user.KeysSearchFilter_OrganizationIdFilter: + return authnKeyOrgIdFilterToQuery(q.OrganizationIdFilter) + default: + return nil, zerrors.ThrowInvalidArgument(nil, "GRPC-vR9nC", "List.Query.Invalid") + } +} + +func authnKeyIdFilterToQuery(f *filter_pb.IDFilter) (query.SearchQuery, error) { + return query.NewAuthNKeyIDQuery(f.Id) +} + +func authnKeyUserIdFilterToQuery(f *filter_pb.IDFilter) (query.SearchQuery, error) { + return query.NewAuthNKeyIdentifyerQuery(f.Id) +} + +func authnKeyOrgIdFilterToQuery(f *filter_pb.IDFilter) (query.SearchQuery, error) { + return query.NewAuthNKeyResourceOwnerQuery(f.Id) +} + +func authnKeyCreatedFilterToQuery(f *filter_pb.TimestampFilter) (query.SearchQuery, error) { + return query.NewAuthNKeyCreationDateQuery(f.Timestamp.AsTime(), filter.TimestampMethodPbToQuery(f.Method)) +} + +func authnKeyExpirationFilterToQuery(f *filter_pb.TimestampFilter) (query.SearchQuery, error) { + return query.NewAuthNKeyExpirationDateDateQuery(f.Timestamp.AsTime(), filter.TimestampMethodPbToQuery(f.Method)) +} + +// authnKeyFieldNameToSortingColumn defaults to the creation date because this ensures deterministic pagination +func authnKeyFieldNameToSortingColumn(field *user.KeyFieldName) query.Column { + if field == nil { + return query.AuthNKeyColumnCreationDate + } + switch *field { + case user.KeyFieldName_KEY_FIELD_NAME_UNSPECIFIED: + return query.AuthNKeyColumnCreationDate + case user.KeyFieldName_KEY_FIELD_NAME_ID: + return query.AuthNKeyColumnID + case user.KeyFieldName_KEY_FIELD_NAME_USER_ID: + return query.AuthNKeyColumnIdentifier + case user.KeyFieldName_KEY_FIELD_NAME_ORGANIZATION_ID: + return query.AuthNKeyColumnResourceOwner + case user.KeyFieldName_KEY_FIELD_NAME_CREATED_DATE: + return query.AuthNKeyColumnCreationDate + case user.KeyFieldName_KEY_FIELD_NAME_KEY_EXPIRATION_DATE: + return query.AuthNKeyColumnExpiration + default: + return query.AuthNKeyColumnCreationDate + } +} diff --git a/internal/api/grpc/user/v2/machine.go b/internal/api/grpc/user/v2/machine.go new file mode 100644 index 0000000000..010ba75678 --- /dev/null +++ b/internal/api/grpc/user/v2/machine.go @@ -0,0 +1,58 @@ +package user + +import ( + "context" + + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/eventstore/v1/models" + "github.com/zitadel/zitadel/pkg/grpc/user/v2" +) + +func (s *Server) createUserTypeMachine(ctx context.Context, machinePb *user.CreateUserRequest_Machine, orgId, userName, userId string) (*user.CreateUserResponse, error) { + cmd := &command.Machine{ + Username: userName, + Name: machinePb.Name, + Description: machinePb.GetDescription(), + AccessTokenType: domain.OIDCTokenTypeBearer, + ObjectRoot: models.ObjectRoot{ + ResourceOwner: orgId, + AggregateID: userId, + }, + } + details, err := s.command.AddMachine( + ctx, + cmd, + s.command.NewPermissionCheckUserWrite(ctx), + command.AddMachineWithUsernameToIDFallback(), + ) + if err != nil { + return nil, err + } + return &user.CreateUserResponse{ + Id: cmd.AggregateID, + CreationDate: timestamppb.New(details.EventDate), + }, nil +} + +func (s *Server) updateUserTypeMachine(ctx context.Context, machinePb *user.UpdateUserRequest_Machine, userId string, userName *string) (*user.UpdateUserResponse, error) { + cmd := updateMachineUserToCommand(userId, userName, machinePb) + err := s.command.ChangeUserMachine(ctx, cmd) + if err != nil { + return nil, err + } + return &user.UpdateUserResponse{ + ChangeDate: timestamppb.New(cmd.Details.EventDate), + }, nil +} + +func updateMachineUserToCommand(userId string, userName *string, machine *user.UpdateUserRequest_Machine) *command.ChangeMachine { + return &command.ChangeMachine{ + ID: userId, + Username: userName, + Name: machine.Name, + Description: machine.Description, + } +} diff --git a/internal/api/grpc/user/v2/machine_test.go b/internal/api/grpc/user/v2/machine_test.go new file mode 100644 index 0000000000..96d77d8fa2 --- /dev/null +++ b/internal/api/grpc/user/v2/machine_test.go @@ -0,0 +1,62 @@ +package user + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/muhlemmer/gu" + "golang.org/x/text/language" + + "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/pkg/grpc/user/v2" +) + +func Test_patchMachineUserToCommand(t *testing.T) { + type args struct { + userId string + userName *string + machine *user.UpdateUserRequest_Machine + } + tests := []struct { + name string + args args + want *command.ChangeMachine + }{{ + name: "single property", + args: args{ + userId: "userId", + machine: &user.UpdateUserRequest_Machine{ + Name: gu.Ptr("name"), + }, + }, + want: &command.ChangeMachine{ + ID: "userId", + Name: gu.Ptr("name"), + }, + }, { + name: "all properties", + args: args{ + userId: "userId", + userName: gu.Ptr("userName"), + machine: &user.UpdateUserRequest_Machine{ + Name: gu.Ptr("name"), + Description: gu.Ptr("description"), + }, + }, + want: &command.ChangeMachine{ + ID: "userId", + Username: gu.Ptr("userName"), + Name: gu.Ptr("name"), + Description: gu.Ptr("description"), + }, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := updateMachineUserToCommand(tt.args.userId, tt.args.userName, tt.args.machine) + if diff := cmp.Diff(tt.want, got, cmpopts.EquateComparable(language.Tag{})); diff != "" { + t.Errorf("patchMachineUserToCommand() mismatch (-want +got):\n%s", diff) + } + }) + } +} diff --git a/internal/api/grpc/user/v2/pat.go b/internal/api/grpc/user/v2/pat.go new file mode 100644 index 0000000000..54f6e99367 --- /dev/null +++ b/internal/api/grpc/user/v2/pat.go @@ -0,0 +1,56 @@ +package user + +import ( + "context" + + "github.com/zitadel/oidc/v3/pkg/oidc" + "google.golang.org/protobuf/types/known/timestamppb" + + z_oidc "github.com/zitadel/zitadel/internal/api/oidc" + "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/eventstore/v1/models" + "github.com/zitadel/zitadel/pkg/grpc/user/v2" +) + +func (s *Server) AddPersonalAccessToken(ctx context.Context, req *user.AddPersonalAccessTokenRequest) (*user.AddPersonalAccessTokenResponse, error) { + newPat := &command.PersonalAccessToken{ + ObjectRoot: models.ObjectRoot{ + AggregateID: req.UserId, + }, + PermissionCheck: s.command.NewPermissionCheckUserWrite(ctx), + ExpirationDate: req.ExpirationDate.AsTime(), + Scopes: []string{ + oidc.ScopeOpenID, + oidc.ScopeProfile, + z_oidc.ScopeUserMetaData, + z_oidc.ScopeResourceOwner, + }, + AllowedUserType: domain.UserTypeMachine, + } + details, err := s.command.AddPersonalAccessToken(ctx, newPat) + if err != nil { + return nil, err + } + return &user.AddPersonalAccessTokenResponse{ + CreationDate: timestamppb.New(details.EventDate), + TokenId: newPat.TokenID, + Token: newPat.Token, + }, nil +} + +func (s *Server) RemovePersonalAccessToken(ctx context.Context, req *user.RemovePersonalAccessTokenRequest) (*user.RemovePersonalAccessTokenResponse, error) { + objectDetails, err := s.command.RemovePersonalAccessToken(ctx, &command.PersonalAccessToken{ + TokenID: req.TokenId, + ObjectRoot: models.ObjectRoot{ + AggregateID: req.UserId, + }, + PermissionCheck: s.command.NewPermissionCheckUserWrite(ctx), + }) + if err != nil { + return nil, err + } + return &user.RemovePersonalAccessTokenResponse{ + DeletionDate: timestamppb.New(objectDetails.EventDate), + }, nil +} diff --git a/internal/api/grpc/user/v2/pat_query.go b/internal/api/grpc/user/v2/pat_query.go new file mode 100644 index 0000000000..6bbd44d511 --- /dev/null +++ b/internal/api/grpc/user/v2/pat_query.go @@ -0,0 +1,123 @@ +package user + +import ( + "context" + + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/api/grpc/filter/v2" + "github.com/zitadel/zitadel/internal/query" + "github.com/zitadel/zitadel/internal/zerrors" + filter_pb "github.com/zitadel/zitadel/pkg/grpc/filter/v2" + "github.com/zitadel/zitadel/pkg/grpc/user/v2" +) + +func (s *Server) ListPersonalAccessTokens(ctx context.Context, req *user.ListPersonalAccessTokensRequest) (*user.ListPersonalAccessTokensResponse, error) { + offset, limit, asc, err := filter.PaginationPbToQuery(s.systemDefaults, req.Pagination) + if err != nil { + return nil, err + } + filters, err := patFiltersToQueries(req.Filters) + if err != nil { + return nil, err + } + search := &query.PersonalAccessTokenSearchQueries{ + SearchRequest: query.SearchRequest{ + Offset: offset, + Limit: limit, + Asc: asc, + SortingColumn: authnPersonalAccessTokenFieldNameToSortingColumn(req.SortingColumn), + }, + Queries: filters, + } + result, err := s.query.SearchPersonalAccessTokens(ctx, search, s.checkPermission) + if err != nil { + return nil, err + } + resp := &user.ListPersonalAccessTokensResponse{ + Result: make([]*user.PersonalAccessToken, len(result.PersonalAccessTokens)), + Pagination: filter.QueryToPaginationPb(search.SearchRequest, result.SearchResponse), + } + for i, pat := range result.PersonalAccessTokens { + resp.Result[i] = &user.PersonalAccessToken{ + CreationDate: timestamppb.New(pat.CreationDate), + ChangeDate: timestamppb.New(pat.ChangeDate), + Id: pat.ID, + UserId: pat.UserID, + OrganizationId: pat.ResourceOwner, + ExpirationDate: timestamppb.New(pat.Expiration), + } + } + return resp, nil +} + +func patFiltersToQueries(filters []*user.PersonalAccessTokensSearchFilter) (_ []query.SearchQuery, err error) { + q := make([]query.SearchQuery, len(filters)) + for i, filter := range filters { + q[i], err = patFilterToQuery(filter) + if err != nil { + return nil, err + } + } + return q, nil +} + +func patFilterToQuery(filter *user.PersonalAccessTokensSearchFilter) (query.SearchQuery, error) { + switch q := filter.Filter.(type) { + case *user.PersonalAccessTokensSearchFilter_CreatedDateFilter: + return authnPersonalAccessTokenCreatedFilterToQuery(q.CreatedDateFilter) + case *user.PersonalAccessTokensSearchFilter_ExpirationDateFilter: + return authnPersonalAccessTokenExpirationFilterToQuery(q.ExpirationDateFilter) + case *user.PersonalAccessTokensSearchFilter_TokenIdFilter: + return authnPersonalAccessTokenIdFilterToQuery(q.TokenIdFilter) + case *user.PersonalAccessTokensSearchFilter_UserIdFilter: + return authnPersonalAccessTokenUserIdFilterToQuery(q.UserIdFilter) + case *user.PersonalAccessTokensSearchFilter_OrganizationIdFilter: + return authnPersonalAccessTokenOrgIdFilterToQuery(q.OrganizationIdFilter) + default: + return nil, zerrors.ThrowInvalidArgument(nil, "GRPC-vR9nC", "List.Query.Invalid") + } +} + +func authnPersonalAccessTokenIdFilterToQuery(f *filter_pb.IDFilter) (query.SearchQuery, error) { + return query.NewPersonalAccessTokenIDQuery(f.Id) +} + +func authnPersonalAccessTokenUserIdFilterToQuery(f *filter_pb.IDFilter) (query.SearchQuery, error) { + return query.NewPersonalAccessTokenUserIDSearchQuery(f.Id) +} + +func authnPersonalAccessTokenOrgIdFilterToQuery(f *filter_pb.IDFilter) (query.SearchQuery, error) { + return query.NewPersonalAccessTokenResourceOwnerSearchQuery(f.Id) +} + +func authnPersonalAccessTokenCreatedFilterToQuery(f *filter_pb.TimestampFilter) (query.SearchQuery, error) { + return query.NewPersonalAccessTokenCreationDateQuery(f.Timestamp.AsTime(), filter.TimestampMethodPbToQuery(f.Method)) +} + +func authnPersonalAccessTokenExpirationFilterToQuery(f *filter_pb.TimestampFilter) (query.SearchQuery, error) { + return query.NewPersonalAccessTokenExpirationDateDateQuery(f.Timestamp.AsTime(), filter.TimestampMethodPbToQuery(f.Method)) +} + +// authnPersonalAccessTokenFieldNameToSortingColumn defaults to the creation date because this ensures deterministic pagination +func authnPersonalAccessTokenFieldNameToSortingColumn(field *user.PersonalAccessTokenFieldName) query.Column { + if field == nil { + return query.PersonalAccessTokenColumnCreationDate + } + switch *field { + case user.PersonalAccessTokenFieldName_PERSONAL_ACCESS_TOKEN_FIELD_NAME_UNSPECIFIED: + return query.PersonalAccessTokenColumnCreationDate + case user.PersonalAccessTokenFieldName_PERSONAL_ACCESS_TOKEN_FIELD_NAME_ID: + return query.PersonalAccessTokenColumnID + case user.PersonalAccessTokenFieldName_PERSONAL_ACCESS_TOKEN_FIELD_NAME_USER_ID: + return query.PersonalAccessTokenColumnUserID + case user.PersonalAccessTokenFieldName_PERSONAL_ACCESS_TOKEN_FIELD_NAME_ORGANIZATION_ID: + return query.PersonalAccessTokenColumnResourceOwner + case user.PersonalAccessTokenFieldName_PERSONAL_ACCESS_TOKEN_FIELD_NAME_CREATED_DATE: + return query.PersonalAccessTokenColumnCreationDate + case user.PersonalAccessTokenFieldName_PERSONAL_ACCESS_TOKEN_FIELD_NAME_EXPIRATION_DATE: + return query.PersonalAccessTokenColumnExpiration + default: + return query.PersonalAccessTokenColumnCreationDate + } +} diff --git a/internal/api/grpc/user/v2/secret.go b/internal/api/grpc/user/v2/secret.go new file mode 100644 index 0000000000..1d54e1dde8 --- /dev/null +++ b/internal/api/grpc/user/v2/secret.go @@ -0,0 +1,39 @@ +package user + +import ( + "context" + + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/pkg/grpc/user/v2" +) + +func (s *Server) AddSecret(ctx context.Context, req *user.AddSecretRequest) (*user.AddSecretResponse, error) { + newSecret := &command.GenerateMachineSecret{ + PermissionCheck: s.command.NewPermissionCheckUserWrite(ctx), + } + details, err := s.command.GenerateMachineSecret(ctx, req.UserId, "", newSecret) + if err != nil { + return nil, err + } + return &user.AddSecretResponse{ + CreationDate: timestamppb.New(details.EventDate), + ClientSecret: newSecret.ClientSecret, + }, nil +} + +func (s *Server) RemoveSecret(ctx context.Context, req *user.RemoveSecretRequest) (*user.RemoveSecretResponse, error) { + details, err := s.command.RemoveMachineSecret( + ctx, + req.UserId, + "", + s.command.NewPermissionCheckUserWrite(ctx), + ) + if err != nil { + return nil, err + } + return &user.RemoveSecretResponse{ + DeletionDate: timestamppb.New(details.EventDate), + }, nil +} diff --git a/internal/api/grpc/user/v2/server.go b/internal/api/grpc/user/v2/server.go index 9272ea27ee..e3c7e8011e 100644 --- a/internal/api/grpc/user/v2/server.go +++ b/internal/api/grpc/user/v2/server.go @@ -8,6 +8,7 @@ import ( "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/grpc/server" "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/config/systemdefaults" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/query" @@ -18,12 +19,13 @@ var _ user.UserServiceServer = (*Server)(nil) type Server struct { user.UnimplementedUserServiceServer - command *command.Commands - query *query.Queries - userCodeAlg crypto.EncryptionAlgorithm - idpAlg crypto.EncryptionAlgorithm - idpCallback func(ctx context.Context) string - samlRootURL func(ctx context.Context, idpID string) string + systemDefaults systemdefaults.SystemDefaults + command *command.Commands + query *query.Queries + userCodeAlg crypto.EncryptionAlgorithm + idpAlg crypto.EncryptionAlgorithm + idpCallback func(ctx context.Context) string + samlRootURL func(ctx context.Context, idpID string) string assetAPIPrefix func(context.Context) string @@ -33,6 +35,7 @@ type Server struct { type Config struct{} func CreateServer( + systemDefaults systemdefaults.SystemDefaults, command *command.Commands, query *query.Queries, userCodeAlg crypto.EncryptionAlgorithm, @@ -43,6 +46,7 @@ func CreateServer( checkPermission domain.PermissionCheck, ) *Server { return &Server{ + systemDefaults: systemDefaults, command: command, query: query, userCodeAlg: userCodeAlg, diff --git a/internal/api/grpc/user/v2/user.go b/internal/api/grpc/user/v2/user.go index 0f958f0d40..6b4b2da75b 100644 --- a/internal/api/grpc/user/v2/user.go +++ b/internal/api/grpc/user/v2/user.go @@ -11,6 +11,7 @@ import ( "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/query" + "github.com/zitadel/zitadel/internal/zerrors" "github.com/zitadel/zitadel/pkg/grpc/user/v2" ) @@ -117,7 +118,7 @@ func genderToDomain(gender user.Gender) domain.Gender { } func (s *Server) UpdateHumanUser(ctx context.Context, req *user.UpdateHumanUserRequest) (_ *user.UpdateHumanUserResponse, err error) { - human, err := UpdateUserRequestToChangeHuman(req) + human, err := updateHumanUserRequestToChangeHuman(req) if err != nil { return nil, err } @@ -181,86 +182,6 @@ func ifNotNilPtr[v, p any](value *v, conv func(v) p) *p { return &pVal } -func UpdateUserRequestToChangeHuman(req *user.UpdateHumanUserRequest) (*command.ChangeHuman, error) { - email, err := SetHumanEmailToEmail(req.Email, req.GetUserId()) - if err != nil { - return nil, err - } - return &command.ChangeHuman{ - ID: req.GetUserId(), - Username: req.Username, - Profile: SetHumanProfileToProfile(req.Profile), - Email: email, - Phone: SetHumanPhoneToPhone(req.Phone), - Password: SetHumanPasswordToPassword(req.Password), - }, nil -} - -func SetHumanProfileToProfile(profile *user.SetHumanProfile) *command.Profile { - if profile == nil { - return nil - } - var firstName *string - if profile.GivenName != "" { - firstName = &profile.GivenName - } - var lastName *string - if profile.FamilyName != "" { - lastName = &profile.FamilyName - } - return &command.Profile{ - FirstName: firstName, - LastName: lastName, - NickName: profile.NickName, - DisplayName: profile.DisplayName, - PreferredLanguage: ifNotNilPtr(profile.PreferredLanguage, language.Make), - Gender: ifNotNilPtr(profile.Gender, genderToDomain), - } -} - -func SetHumanEmailToEmail(email *user.SetHumanEmail, userID string) (*command.Email, error) { - if email == nil { - return nil, nil - } - var urlTemplate string - if email.GetSendCode() != nil && email.GetSendCode().UrlTemplate != nil { - urlTemplate = *email.GetSendCode().UrlTemplate - if err := domain.RenderConfirmURLTemplate(io.Discard, urlTemplate, userID, "code", "orgID"); err != nil { - return nil, err - } - } - return &command.Email{ - Address: domain.EmailAddress(email.Email), - Verified: email.GetIsVerified(), - ReturnCode: email.GetReturnCode() != nil, - URLTemplate: urlTemplate, - }, nil -} - -func SetHumanPhoneToPhone(phone *user.SetHumanPhone) *command.Phone { - if phone == nil { - return nil - } - return &command.Phone{ - Number: domain.PhoneNumber(phone.GetPhone()), - Verified: phone.GetIsVerified(), - ReturnCode: phone.GetReturnCode() != nil, - } -} - -func SetHumanPasswordToPassword(password *user.SetPassword) *command.Password { - if password == nil { - return nil - } - return &command.Password{ - PasswordCode: password.GetVerificationCode(), - OldPassword: password.GetCurrentPassword(), - Password: password.GetPassword().GetPassword(), - EncodedPasswordHash: password.GetHashedPassword().GetHash(), - ChangeRequired: password.GetPassword().GetChangeRequired() || password.GetHashedPassword().GetChangeRequired(), - } -} - func (s *Server) DeleteUser(ctx context.Context, req *user.DeleteUserRequest) (_ *user.DeleteUserResponse, err error) { memberships, grants, err := s.removeUserDependencies(ctx, req.GetUserId()) if err != nil { @@ -482,3 +403,25 @@ func (s *Server) HumanMFAInitSkipped(ctx context.Context, req *user.HumanMFAInit Details: object.DomainToDetailsPb(details), }, nil } + +func (s *Server) CreateUser(ctx context.Context, req *user.CreateUserRequest) (*user.CreateUserResponse, error) { + switch userType := req.GetUserType().(type) { + case *user.CreateUserRequest_Human_: + return s.createUserTypeHuman(ctx, userType.Human, req.OrganizationId, req.Username, req.UserId) + case *user.CreateUserRequest_Machine_: + return s.createUserTypeMachine(ctx, userType.Machine, req.OrganizationId, req.GetUsername(), req.GetUserId()) + default: + return nil, zerrors.ThrowInternal(nil, "", "user type is not implemented") + } +} + +func (s *Server) UpdateUser(ctx context.Context, req *user.UpdateUserRequest) (*user.UpdateUserResponse, error) { + switch userType := req.GetUserType().(type) { + case *user.UpdateUserRequest_Human_: + return s.updateUserTypeHuman(ctx, userType.Human, req.UserId, req.Username) + case *user.UpdateUserRequest_Machine_: + return s.updateUserTypeMachine(ctx, userType.Machine, req.UserId, req.Username) + default: + return nil, zerrors.ThrowUnimplemented(nil, "", "user type is not implemented") + } +} diff --git a/internal/api/grpc/user/v2/query.go b/internal/api/grpc/user/v2/user_query.go similarity index 100% rename from internal/api/grpc/user/v2/query.go rename to internal/api/grpc/user/v2/user_query.go diff --git a/internal/api/scim/resources/user.go b/internal/api/scim/resources/user.go index bc8d864994..13baed5d51 100644 --- a/internal/api/scim/resources/user.go +++ b/internal/api/scim/resources/user.go @@ -203,7 +203,6 @@ func (h *UsersHandler) Delete(ctx context.Context, id string) error { if err != nil { return err } - _, err = h.command.RemoveUserV2(ctx, id, authz.GetCtxData(ctx).OrgID, memberships, grants...) return err } diff --git a/internal/command/instance_member.go b/internal/command/instance_member.go index ee9bf15f84..a33635e8f5 100644 --- a/internal/command/instance_member.go +++ b/internal/command/instance_member.go @@ -22,7 +22,7 @@ func (c *Commands) AddInstanceMemberCommand(a *instance.Aggregate, userID string return nil, zerrors.ThrowInvalidArgument(nil, "INSTANCE-4m0fS", "Errors.IAM.MemberInvalid") } return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { - if exists, err := ExistsUser(ctx, filter, userID, ""); err != nil || !exists { + if exists, err := ExistsUser(ctx, filter, userID, "", false); err != nil || !exists { return nil, zerrors.ThrowPreconditionFailed(err, "INSTA-GSXOn", "Errors.User.NotFound") } if isMember, err := IsInstanceMember(ctx, filter, a.ID, userID); err != nil || isMember { diff --git a/internal/command/org_member.go b/internal/command/org_member.go index ae9bef2151..bf1ae91d8a 100644 --- a/internal/command/org_member.go +++ b/internal/command/org_member.go @@ -28,7 +28,7 @@ func (c *Commands) AddOrgMemberCommand(a *org.Aggregate, userID string, roles .. ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - if exists, err := ExistsUser(ctx, filter, userID, ""); err != nil || !exists { + if exists, err := ExistsUser(ctx, filter, userID, "", false); err != nil || !exists { return nil, zerrors.ThrowPreconditionFailed(err, "ORG-GoXOn", "Errors.User.NotFound") } if isMember, err := IsOrgMember(ctx, filter, a.ID, userID); err != nil || isMember { diff --git a/internal/command/resource_ower_model.go b/internal/command/resource_owner_model.go similarity index 100% rename from internal/command/resource_ower_model.go rename to internal/command/resource_owner_model.go diff --git a/internal/command/user.go b/internal/command/user.go index 6b65aa83ec..0db4fda328 100644 --- a/internal/command/user.go +++ b/internal/command/user.go @@ -353,21 +353,27 @@ func (c *Commands) userWriteModelByID(ctx context.Context, userID, resourceOwner return writeModel, nil } -func ExistsUser(ctx context.Context, filter preparation.FilterToQueryReducer, id, resourceOwner string) (exists bool, err error) { +func ExistsUser(ctx context.Context, filter preparation.FilterToQueryReducer, id, resourceOwner string, machineOnly bool) (exists bool, err error) { + eventTypes := []eventstore.EventType{ + user.MachineAddedEventType, + user.UserRemovedType, + } + if !machineOnly { + eventTypes = append(eventTypes, + user.HumanRegisteredType, + user.UserV1RegisteredType, + user.HumanAddedType, + user.UserV1AddedType, + ) + } events, err := filter(ctx, eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). ResourceOwner(resourceOwner). OrderAsc(). AddQuery(). AggregateTypes(user.AggregateType). AggregateIDs(id). - EventTypes( - user.HumanRegisteredType, - user.UserV1RegisteredType, - user.HumanAddedType, - user.UserV1AddedType, - user.MachineAddedEventType, - user.UserRemovedType, - ).Builder()) + EventTypes(eventTypes...). + Builder()) if err != nil { return false, err } diff --git a/internal/command/user_machine.go b/internal/command/user_machine.go index 1ec32450ac..7c8fd89eac 100644 --- a/internal/command/user_machine.go +++ b/internal/command/user_machine.go @@ -25,6 +25,7 @@ type Machine struct { Name string Description string AccessTokenType domain.OIDCTokenType + PermissionCheck PermissionCheck } func (m *Machine) IsZero() bool { @@ -33,8 +34,8 @@ func (m *Machine) IsZero() bool { func AddMachineCommand(a *user.Aggregate, machine *Machine) preparation.Validation { return func() (_ preparation.CreateCommands, err error) { - if a.ResourceOwner == "" { - return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-xiown2", "Errors.ResourceOwnerMissing") + if a.ResourceOwner == "" && machine.PermissionCheck == nil { + return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-xiown3", "Errors.ResourceOwnerMissing") } if a.ID == "" { return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-p0p2mi", "Errors.User.UserIDMissing") @@ -49,7 +50,7 @@ func AddMachineCommand(a *user.Aggregate, machine *Machine) preparation.Validati ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - writeModel, err := getMachineWriteModel(ctx, a.ID, a.ResourceOwner, filter) + writeModel, err := getMachineWriteModel(ctx, a.ID, a.ResourceOwner, filter, machine.PermissionCheck) if err != nil { return nil, err } @@ -67,7 +68,18 @@ func AddMachineCommand(a *user.Aggregate, machine *Machine) preparation.Validati } } -func (c *Commands) AddMachine(ctx context.Context, machine *Machine) (_ *domain.ObjectDetails, err error) { +type addMachineOption func(context.Context, *Machine) error + +func AddMachineWithUsernameToIDFallback() addMachineOption { + return func(ctx context.Context, m *Machine) error { + if m.Username == "" { + m.Username = m.AggregateID + } + return nil + } +} + +func (c *Commands) AddMachine(ctx context.Context, machine *Machine, check PermissionCheck, options ...addMachineOption) (_ *domain.ObjectDetails, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() @@ -80,6 +92,16 @@ func (c *Commands) AddMachine(ctx context.Context, machine *Machine) (_ *domain. } agg := user.NewAggregate(machine.AggregateID, machine.ResourceOwner) + for _, option := range options { + if err = option(ctx, machine); err != nil { + return nil, err + } + } + if check != nil { + if err = check(machine.ResourceOwner, machine.AggregateID); err != nil { + return nil, err + } + } cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, AddMachineCommand(agg, machine)) if err != nil { return nil, err @@ -97,6 +119,7 @@ func (c *Commands) AddMachine(ctx context.Context, machine *Machine) (_ *domain. }, nil } +// Deprecated: use ChangeUserMachine instead func (c *Commands) ChangeMachine(ctx context.Context, machine *Machine) (*domain.ObjectDetails, error) { agg := user.NewAggregate(machine.AggregateID, machine.ResourceOwner) cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, changeMachineCommand(agg, machine)) @@ -118,24 +141,21 @@ func (c *Commands) ChangeMachine(ctx context.Context, machine *Machine) (*domain func changeMachineCommand(a *user.Aggregate, machine *Machine) preparation.Validation { return func() (_ preparation.CreateCommands, err error) { - if a.ResourceOwner == "" { + if a.ResourceOwner == "" && machine.PermissionCheck == nil { return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-xiown3", "Errors.ResourceOwnerMissing") } if a.ID == "" { return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-p0p3mi", "Errors.User.UserIDMissing") } return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { - writeModel, err := getMachineWriteModel(ctx, a.ID, a.ResourceOwner, filter) + writeModel, err := getMachineWriteModel(ctx, a.ID, a.ResourceOwner, filter, machine.PermissionCheck) if err != nil { return nil, err } if !isUserStateExists(writeModel.UserState) { return nil, zerrors.ThrowNotFound(nil, "COMMAND-5M0od", "Errors.User.NotFound") } - changedEvent, hasChanged, err := writeModel.NewChangedEvent(ctx, &a.Aggregate, machine.Name, machine.Description, machine.AccessTokenType) - if err != nil { - return nil, err - } + changedEvent, hasChanged := writeModel.NewChangedEvent(ctx, &a.Aggregate, machine.Name, machine.Description, machine.AccessTokenType) if !hasChanged { return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-2n8vs", "Errors.User.NotChanged") } @@ -147,10 +167,9 @@ func changeMachineCommand(a *user.Aggregate, machine *Machine) preparation.Valid } } -func getMachineWriteModel(ctx context.Context, userID, resourceOwner string, filter preparation.FilterToQueryReducer) (_ *MachineWriteModel, err error) { +func getMachineWriteModel(ctx context.Context, userID, resourceOwner string, filter preparation.FilterToQueryReducer, permissionCheck PermissionCheck) (_ *MachineWriteModel, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - writeModel := NewMachineWriteModel(userID, resourceOwner) events, err := filter(ctx, writeModel.Query()) if err != nil { @@ -161,5 +180,10 @@ func getMachineWriteModel(ctx context.Context, userID, resourceOwner string, fil } writeModel.AppendEvents(events...) err = writeModel.Reduce() + if permissionCheck != nil { + if err := permissionCheck(writeModel.ResourceOwner, writeModel.AggregateID); err != nil { + return nil, err + } + } return writeModel, err } diff --git a/internal/command/user_machine_key.go b/internal/command/user_machine_key.go index 8a0f0f437b..d628bf4c2d 100644 --- a/internal/command/user_machine_key.go +++ b/internal/command/user_machine_key.go @@ -15,12 +15,14 @@ import ( ) type AddMachineKey struct { - Type domain.AuthNKeyType - ExpirationDate time.Time + Type domain.AuthNKeyType + ExpirationDate time.Time + PermissionCheck PermissionCheck } type MachineKey struct { models.ObjectRoot + PermissionCheck PermissionCheck KeyID string Type domain.AuthNKeyType @@ -64,7 +66,7 @@ func (key *MachineKey) Detail() ([]byte, error) { } func (key *MachineKey) content() error { - if key.ResourceOwner == "" { + if key.PermissionCheck == nil && key.ResourceOwner == "" { return zerrors.ThrowInvalidArgument(nil, "COMMAND-kqpoix", "Errors.ResourceOwnerMissing") } if key.AggregateID == "" { @@ -91,7 +93,7 @@ func (key *MachineKey) valid() (err error) { } func (key *MachineKey) checkAggregate(ctx context.Context, filter preparation.FilterToQueryReducer) error { - if exists, err := ExistsUser(ctx, filter, key.AggregateID, key.ResourceOwner); err != nil || !exists { + if exists, err := ExistsUser(ctx, filter, key.AggregateID, key.ResourceOwner, true); err != nil || !exists { return zerrors.ThrowPreconditionFailed(err, "COMMAND-bnipwm1", "Errors.User.NotFound") } return nil @@ -142,7 +144,7 @@ func prepareAddUserMachineKey(machineKey *MachineKey, keySize int) preparation.V return nil, err } } - writeModel, err := getMachineKeyWriteModelByID(ctx, filter, machineKey.AggregateID, machineKey.KeyID, machineKey.ResourceOwner) + writeModel, err := getMachineKeyWriteModelByID(ctx, filter, machineKey.AggregateID, machineKey.KeyID, machineKey.ResourceOwner, machineKey.PermissionCheck) if err != nil { return nil, err } @@ -186,7 +188,7 @@ func prepareRemoveUserMachineKey(machineKey *MachineKey) preparation.Validation return nil, err } return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { - writeModel, err := getMachineKeyWriteModelByID(ctx, filter, machineKey.AggregateID, machineKey.KeyID, machineKey.ResourceOwner) + writeModel, err := getMachineKeyWriteModelByID(ctx, filter, machineKey.AggregateID, machineKey.KeyID, machineKey.ResourceOwner, machineKey.PermissionCheck) if err != nil { return nil, err } @@ -204,16 +206,18 @@ func prepareRemoveUserMachineKey(machineKey *MachineKey) preparation.Validation } } -func getMachineKeyWriteModelByID(ctx context.Context, filter preparation.FilterToQueryReducer, userID, keyID, resourceOwner string) (_ *MachineKeyWriteModel, err error) { +func getMachineKeyWriteModelByID(ctx context.Context, filter preparation.FilterToQueryReducer, userID, keyID, resourceOwner string, permissionCheck PermissionCheck) (_ *MachineKeyWriteModel, err error) { writeModel := NewMachineKeyWriteModel(userID, keyID, resourceOwner) events, err := filter(ctx, writeModel.Query()) if err != nil { return nil, err } - if len(events) == 0 { - return writeModel, nil - } writeModel.AppendEvents(events...) err = writeModel.Reduce() + if permissionCheck != nil { + if err := permissionCheck(writeModel.ResourceOwner, writeModel.AggregateID); err != nil { + return nil, err + } + } return writeModel, err } diff --git a/internal/command/user_machine_model.go b/internal/command/user_machine_model.go index b7dfb02d32..1ed6c8ca58 100644 --- a/internal/command/user_machine_model.go +++ b/internal/command/user_machine_model.go @@ -2,7 +2,6 @@ package command import ( "context" - "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" @@ -106,9 +105,8 @@ func (wm *MachineWriteModel) NewChangedEvent( name, description string, accessTokenType domain.OIDCTokenType, -) (*user.MachineChangedEvent, bool, error) { +) (*user.MachineChangedEvent, bool) { changes := make([]user.MachineChanges, 0) - var err error if wm.Name != name { changes = append(changes, user.ChangeName(name)) @@ -120,11 +118,8 @@ func (wm *MachineWriteModel) NewChangedEvent( changes = append(changes, user.ChangeAccessTokenType(accessTokenType)) } if len(changes) == 0 { - return nil, false, nil + return nil, false } - changeEvent, err := user.NewMachineChangedEvent(ctx, aggregate, changes) - if err != nil { - return nil, false, err - } - return changeEvent, true, nil + changeEvent := user.NewMachineChangedEvent(ctx, aggregate, changes) + return changeEvent, true } diff --git a/internal/command/user_machine_secret.go b/internal/command/user_machine_secret.go index 3349fc90a5..34e9c0c5cc 100644 --- a/internal/command/user_machine_secret.go +++ b/internal/command/user_machine_secret.go @@ -11,7 +11,8 @@ import ( ) type GenerateMachineSecret struct { - ClientSecret string + PermissionCheck PermissionCheck + ClientSecret string } func (c *Commands) GenerateMachineSecret(ctx context.Context, userID string, resourceOwner string, set *GenerateMachineSecret) (*domain.ObjectDetails, error) { @@ -35,14 +36,14 @@ func (c *Commands) GenerateMachineSecret(ctx context.Context, userID string, res func (c *Commands) prepareGenerateMachineSecret(a *user.Aggregate, set *GenerateMachineSecret) preparation.Validation { return func() (_ preparation.CreateCommands, err error) { - if a.ResourceOwner == "" { - return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-x0992n", "Errors.ResourceOwnerMissing") + if a.ResourceOwner == "" && set.PermissionCheck == nil { + return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-0qp2hus", "Errors.ResourceOwnerMissing") } if a.ID == "" { return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-bzoqjs", "Errors.User.UserIDMissing") } return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { - writeModel, err := getMachineWriteModel(ctx, a.ID, a.ResourceOwner, filter) + writeModel, err := getMachineWriteModel(ctx, a.ID, a.ResourceOwner, filter, set.PermissionCheck) if err != nil { return nil, err } @@ -62,9 +63,10 @@ func (c *Commands) prepareGenerateMachineSecret(a *user.Aggregate, set *Generate } } -func (c *Commands) RemoveMachineSecret(ctx context.Context, userID string, resourceOwner string) (*domain.ObjectDetails, error) { +func (c *Commands) RemoveMachineSecret(ctx context.Context, userID string, resourceOwner string, permissionCheck PermissionCheck) (*domain.ObjectDetails, error) { agg := user.NewAggregate(userID, resourceOwner) - cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, prepareRemoveMachineSecret(agg)) + //nolint:staticcheck + cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, prepareRemoveMachineSecret(agg, permissionCheck)) if err != nil { return nil, err } @@ -81,16 +83,16 @@ func (c *Commands) RemoveMachineSecret(ctx context.Context, userID string, resou }, nil } -func prepareRemoveMachineSecret(a *user.Aggregate) preparation.Validation { +func prepareRemoveMachineSecret(a *user.Aggregate, check PermissionCheck) preparation.Validation { return func() (_ preparation.CreateCommands, err error) { - if a.ResourceOwner == "" { - return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-0qp2hus", "Errors.ResourceOwnerMissing") + if a.ResourceOwner == "" && check == nil { + return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-x0992n", "Errors.ResourceOwnerMissing") } if a.ID == "" { return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-bzosjs", "Errors.User.UserIDMissing") } return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { - writeModel, err := getMachineWriteModel(ctx, a.ID, a.ResourceOwner, filter) + writeModel, err := getMachineWriteModel(ctx, a.ID, a.ResourceOwner, filter, check) if err != nil { return nil, err } diff --git a/internal/command/user_machine_secret_test.go b/internal/command/user_machine_secret_test.go index 4c6d16960c..8e839efe07 100644 --- a/internal/command/user_machine_secret_test.go +++ b/internal/command/user_machine_secret_test.go @@ -44,7 +44,7 @@ func TestCommandSide_GenerateMachineSecret(t *testing.T) { ctx: context.Background(), userID: "", resourceOwner: "org1", - set: nil, + set: new(GenerateMachineSecret), }, res: res{ err: zerrors.IsErrorInvalidArgument, @@ -59,7 +59,7 @@ func TestCommandSide_GenerateMachineSecret(t *testing.T) { ctx: context.Background(), userID: "user1", resourceOwner: "", - set: nil, + set: new(GenerateMachineSecret), }, res: res{ err: zerrors.IsErrorInvalidArgument, @@ -76,7 +76,7 @@ func TestCommandSide_GenerateMachineSecret(t *testing.T) { ctx: context.Background(), userID: "user1", resourceOwner: "org1", - set: nil, + set: new(GenerateMachineSecret), }, res: res{ err: zerrors.IsPreconditionFailed, @@ -289,7 +289,7 @@ func TestCommandSide_RemoveMachineSecret(t *testing.T) { r := &Commands{ eventstore: tt.fields.eventstore, } - got, err := r.RemoveMachineSecret(tt.args.ctx, tt.args.userID, tt.args.resourceOwner) + got, err := r.RemoveMachineSecret(tt.args.ctx, tt.args.userID, tt.args.resourceOwner, nil) if tt.res.err == nil { assert.NoError(t, err) } diff --git a/internal/command/user_machine_test.go b/internal/command/user_machine_test.go index c7b4b8caf4..19548ae9c6 100644 --- a/internal/command/user_machine_test.go +++ b/internal/command/user_machine_test.go @@ -24,6 +24,8 @@ func TestCommandSide_AddMachine(t *testing.T) { type args struct { ctx context.Context machine *Machine + check PermissionCheck + options func(*Commands) []addMachineOption } type res struct { want *domain.ObjectDetails @@ -194,14 +196,242 @@ func TestCommandSide_AddMachine(t *testing.T) { }, }, }, + { + name: "with username fallback to given username", + fields: fields{ + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "aggregateID"), + eventstore: eventstoreExpect( + t, + expectFilter(), + expectFilter( + eventFromEventPusher( + org.NewDomainPolicyAddedEvent(context.Background(), + &user.NewAggregate("aggregateID", "org1").Aggregate, + true, + true, + true, + ), + ), + ), + expectPush( + user.NewMachineAddedEvent(context.Background(), + &user.NewAggregate("aggregateID", "org1").Aggregate, + "username", + "name", + "", + true, + domain.OIDCTokenTypeBearer, + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + machine: &Machine{ + ObjectRoot: models.ObjectRoot{ + ResourceOwner: "org1", + }, + Name: "name", + Username: "username", + }, + options: func(commands *Commands) []addMachineOption { + return []addMachineOption{ + AddMachineWithUsernameToIDFallback(), + } + }, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "with username fallback to generated id", + fields: fields{ + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "aggregateID"), + eventstore: eventstoreExpect( + t, + expectFilter(), + expectFilter( + eventFromEventPusher( + org.NewDomainPolicyAddedEvent(context.Background(), + &user.NewAggregate("aggregateID", "org1").Aggregate, + true, + true, + true, + ), + ), + ), + expectPush( + user.NewMachineAddedEvent(context.Background(), + &user.NewAggregate("aggregateID", "org1").Aggregate, + "aggregateID", + "name", + "", + true, + domain.OIDCTokenTypeBearer, + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + machine: &Machine{ + ObjectRoot: models.ObjectRoot{ + ResourceOwner: "org1", + }, + Name: "name", + }, + options: func(commands *Commands) []addMachineOption { + return []addMachineOption{ + AddMachineWithUsernameToIDFallback(), + } + }, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "with username fallback to given id", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + expectFilter( + eventFromEventPusher( + org.NewDomainPolicyAddedEvent(context.Background(), + &user.NewAggregate("aggregateID", "org1").Aggregate, + true, + true, + true, + ), + ), + ), + expectPush( + user.NewMachineAddedEvent(context.Background(), + &user.NewAggregate("aggregateID", "org1").Aggregate, + "aggregateID", + "name", + "", + true, + domain.OIDCTokenTypeBearer, + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + machine: &Machine{ + ObjectRoot: models.ObjectRoot{ + ResourceOwner: "org1", + AggregateID: "aggregateID", + }, + Name: "name", + }, + options: func(commands *Commands) []addMachineOption { + return []addMachineOption{ + AddMachineWithUsernameToIDFallback(), + } + }, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "with succeeding permission check, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + expectFilter( + eventFromEventPusher( + org.NewDomainPolicyAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + true, + true, + true, + ), + ), + ), + expectPush( + user.NewMachineAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "name", + "description", + true, + domain.OIDCTokenTypeBearer, + ), + ), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), + }, + args: args{ + ctx: context.Background(), + machine: &Machine{ + ObjectRoot: models.ObjectRoot{ + ResourceOwner: "org1", + }, + Description: "description", + Name: "name", + Username: "username", + }, + check: func(resourceOwner, aggregateID string) error { + return nil + }, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "with failing permission check, error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), + }, + args: args{ + ctx: context.Background(), + machine: &Machine{ + ObjectRoot: models.ObjectRoot{ + ResourceOwner: "org1", + }, + Description: "description", + Name: "name", + Username: "username", + }, + check: func(resourceOwner, aggregateID string) error { + return zerrors.ThrowPermissionDenied(nil, "", "") + }, + }, + res: res{ + err: zerrors.IsPermissionDenied, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, - idGenerator: tt.fields.idGenerator, + eventstore: tt.fields.eventstore, + idGenerator: tt.fields.idGenerator, + checkPermission: newMockPermissionCheckAllowed(), } - got, err := r.AddMachine(tt.args.ctx, tt.args.machine) + var options []addMachineOption + if tt.args.options != nil { + options = tt.args.options(r) + } + got, err := r.AddMachine(tt.args.ctx, tt.args.machine, tt.args.check, options...) if tt.res.err == nil { assert.NoError(t, err) } @@ -391,7 +621,7 @@ func TestCommandSide_ChangeMachine(t *testing.T) { } func newMachineChangedEvent(ctx context.Context, userID, resourceOwner, name, description string) *user.MachineChangedEvent { - event, _ := user.NewMachineChangedEvent(ctx, + event := user.NewMachineChangedEvent(ctx, &user.NewAggregate(userID, resourceOwner).Aggregate, []user.MachineChanges{ user.ChangeName(name), diff --git a/internal/command/user_personal_access_token.go b/internal/command/user_personal_access_token.go index 0faf85d5eb..f37953f3d6 100644 --- a/internal/command/user_personal_access_token.go +++ b/internal/command/user_personal_access_token.go @@ -21,6 +21,7 @@ type AddPat struct { type PersonalAccessToken struct { models.ObjectRoot + PermissionCheck PermissionCheck ExpirationDate time.Time Scopes []string @@ -43,7 +44,7 @@ func NewPersonalAccessToken(resourceOwner string, userID string, expirationDate } func (pat *PersonalAccessToken) content() error { - if pat.ResourceOwner == "" { + if pat.ResourceOwner == "" && pat.PermissionCheck == nil { return zerrors.ThrowInvalidArgument(nil, "COMMAND-xs0k2n", "Errors.ResourceOwnerMissing") } if pat.AggregateID == "" { @@ -109,11 +110,10 @@ func prepareAddPersonalAccessToken(pat *PersonalAccessToken, algorithm crypto.En if err := pat.checkAggregate(ctx, filter); err != nil { return nil, err } - writeModel, err := getPersonalAccessTokenWriteModelByID(ctx, filter, pat.AggregateID, pat.TokenID, pat.ResourceOwner) + writeModel, err := getPersonalAccessTokenWriteModelByID(ctx, filter, pat.AggregateID, pat.TokenID, pat.ResourceOwner, pat.PermissionCheck) if err != nil { return nil, err } - pat.Token, err = createToken(algorithm, writeModel.TokenID, writeModel.AggregateID) if err != nil { return nil, err @@ -155,7 +155,7 @@ func prepareRemovePersonalAccessToken(pat *PersonalAccessToken) preparation.Vali return nil, err } return func(ctx context.Context, filter preparation.FilterToQueryReducer) (_ []eventstore.Command, err error) { - writeModel, err := getPersonalAccessTokenWriteModelByID(ctx, filter, pat.AggregateID, pat.TokenID, pat.ResourceOwner) + writeModel, err := getPersonalAccessTokenWriteModelByID(ctx, filter, pat.AggregateID, pat.TokenID, pat.ResourceOwner, pat.PermissionCheck) if err != nil { return nil, err } @@ -181,16 +181,18 @@ func createToken(algorithm crypto.EncryptionAlgorithm, tokenID, userID string) ( return base64.RawURLEncoding.EncodeToString(encrypted), nil } -func getPersonalAccessTokenWriteModelByID(ctx context.Context, filter preparation.FilterToQueryReducer, userID, tokenID, resourceOwner string) (_ *PersonalAccessTokenWriteModel, err error) { +func getPersonalAccessTokenWriteModelByID(ctx context.Context, filter preparation.FilterToQueryReducer, userID, tokenID, resourceOwner string, check PermissionCheck) (_ *PersonalAccessTokenWriteModel, err error) { writeModel := NewPersonalAccessTokenWriteModel(userID, tokenID, resourceOwner) events, err := filter(ctx, writeModel.Query()) if err != nil { return nil, err } - if len(events) == 0 { - return writeModel, nil - } writeModel.AppendEvents(events...) - err = writeModel.Reduce() + if err = writeModel.Reduce(); err != nil { + return nil, err + } + if check != nil { + err = check(writeModel.ResourceOwner, writeModel.AggregateID) + } return writeModel, err } diff --git a/internal/command/user_test.go b/internal/command/user_test.go index 9abae187c1..6a1597fc8b 100644 --- a/internal/command/user_test.go +++ b/internal/command/user_test.go @@ -1813,7 +1813,7 @@ func TestExistsUser(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - gotExists, err := ExistsUser(context.Background(), tt.args.filter, tt.args.id, tt.args.resourceOwner) + gotExists, err := ExistsUser(context.Background(), tt.args.filter, tt.args.id, tt.args.resourceOwner, false) if (err != nil) != tt.wantErr { t.Errorf("ExistsUser() error = %v, wantErr %v", err, tt.wantErr) return diff --git a/internal/command/user_v2.go b/internal/command/user_v2.go index 5f8e8d6ff5..be10fd03fe 100644 --- a/internal/command/user_v2.go +++ b/internal/command/user_v2.go @@ -132,7 +132,6 @@ func (c *Commands) RemoveUserV2(ctx context.Context, userID, resourceOwner strin if userID == "" { return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-vaipl7s13l", "Errors.User.UserIDMissing") } - existingUser, err := c.userRemoveWriteModel(ctx, userID, resourceOwner) if err != nil { return nil, err @@ -143,7 +142,6 @@ func (c *Commands) RemoveUserV2(ctx context.Context, userID, resourceOwner strin if err := c.checkPermissionDeleteUser(ctx, existingUser.ResourceOwner, existingUser.AggregateID); err != nil { return nil, err } - domainPolicy, err := c.domainPolicyWriteModel(ctx, existingUser.ResourceOwner) if err != nil { return nil, zerrors.ThrowPreconditionFailed(err, "COMMAND-l40ykb3xh2", "Errors.Org.DomainPolicy.NotExisting") diff --git a/internal/command/user_v2_human.go b/internal/command/user_v2_human.go index f88e2017d5..0945ae7578 100644 --- a/internal/command/user_v2_human.go +++ b/internal/command/user_v2_human.go @@ -5,6 +5,7 @@ import ( "golang.org/x/text/language" + "github.com/zitadel/zitadel/internal/command/preparation" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" @@ -121,7 +122,10 @@ func (c *Commands) AddUserHuman(ctx context.Context, resourceOwner string, human if resourceOwner == "" { return zerrors.ThrowInvalidArgument(nil, "COMMA-095xh8fll1", "Errors.Internal") } - + if human.Details == nil { + human.Details = &domain.ObjectDetails{} + } + human.Details.ResourceOwner = resourceOwner if err := human.Validate(c.userPasswordHasher); err != nil { return err } @@ -132,7 +136,12 @@ func (c *Commands) AddUserHuman(ctx context.Context, resourceOwner string, human return err } } - + // check for permission to create user on resourceOwner + if !human.Register { + if err := c.checkPermissionUpdateUser(ctx, resourceOwner, human.ID); err != nil { + return err + } + } // only check if user is already existing existingHuman, err := c.userExistsWriteModel( ctx, @@ -144,12 +153,6 @@ func (c *Commands) AddUserHuman(ctx context.Context, resourceOwner string, human if isUserStateExists(existingHuman.UserState) { return zerrors.ThrowPreconditionFailed(nil, "COMMAND-7yiox1isql", "Errors.User.AlreadyExisting") } - // check for permission to create user on resourceOwner - if !human.Register { - if err := c.checkPermission(ctx, domain.PermissionUserWrite, resourceOwner, human.ID); err != nil { - return err - } - } // add resourceowner for the events with the aggregate existingHuman.ResourceOwner = resourceOwner @@ -161,6 +164,7 @@ func (c *Commands) AddUserHuman(ctx context.Context, resourceOwner string, human if err = c.userValidateDomain(ctx, resourceOwner, human.Username, domainPolicy.UserLoginMustBeDomain); err != nil { return err } + var createCmd humanCreationCommand if human.Register { createCmd = user.NewHumanRegisteredEvent( @@ -203,17 +207,33 @@ func (c *Commands) AddUserHuman(ctx context.Context, resourceOwner string, human return err } - cmds := make([]eventstore.Command, 0, 3) - cmds = append(cmds, createCmd) - - cmds, err = c.addHumanCommandEmail(ctx, filter, cmds, existingHuman.Aggregate(), human, alg, allowInitMail) + cmds, err := c.addUserHumanCommands(ctx, filter, existingHuman, human, allowInitMail, alg, createCmd) if err != nil { return err } + if len(cmds) == 0 { + human.Details = writeModelToObjectDetails(&existingHuman.WriteModel) + return nil + } + err = c.pushAppendAndReduce(ctx, existingHuman, cmds...) + if err != nil { + return err + } + human.Details = writeModelToObjectDetails(&existingHuman.WriteModel) + return nil +} + +func (c *Commands) addUserHumanCommands(ctx context.Context, filter preparation.FilterToQueryReducer, existingHuman *UserV2WriteModel, human *AddHuman, allowInitMail bool, alg crypto.EncryptionAlgorithm, addUserCommand eventstore.Command) ([]eventstore.Command, error) { + cmds := []eventstore.Command{addUserCommand} + var err error + cmds, err = c.addHumanCommandEmail(ctx, filter, cmds, existingHuman.Aggregate(), human, alg, allowInitMail) + if err != nil { + return nil, err + } cmds, err = c.addHumanCommandPhone(ctx, filter, cmds, existingHuman.Aggregate(), human, alg) if err != nil { - return err + return nil, err } for _, metadataEntry := range human.Metadata { @@ -227,7 +247,7 @@ func (c *Commands) AddUserHuman(ctx context.Context, resourceOwner string, human for _, link := range human.Links { cmd, err := addLink(ctx, filter, existingHuman.Aggregate(), link) if err != nil { - return err + return nil, err } cmds = append(cmds, cmd) } @@ -235,7 +255,7 @@ func (c *Commands) AddUserHuman(ctx context.Context, resourceOwner string, human if human.TOTPSecret != "" { encryptedSecret, err := crypto.Encrypt([]byte(human.TOTPSecret), c.multifactors.OTP.CryptoMFA) if err != nil { - return err + return nil, err } cmds = append(cmds, user.NewHumanOTPAddedEvent(ctx, &existingHuman.Aggregate().Aggregate, encryptedSecret), @@ -246,18 +266,7 @@ func (c *Commands) AddUserHuman(ctx context.Context, resourceOwner string, human if human.SetInactive { cmds = append(cmds, user.NewUserDeactivatedEvent(ctx, &existingHuman.Aggregate().Aggregate)) } - - if len(cmds) == 0 { - human.Details = writeModelToObjectDetails(&existingHuman.WriteModel) - return nil - } - - err = c.pushAppendAndReduce(ctx, existingHuman, cmds...) - if err != nil { - return err - } - human.Details = writeModelToObjectDetails(&existingHuman.WriteModel) - return nil + return cmds, nil } func (c *Commands) ChangeUserHuman(ctx context.Context, human *ChangeHuman, alg crypto.EncryptionAlgorithm) (err error) { @@ -341,7 +350,6 @@ func (c *Commands) ChangeUserHuman(ctx context.Context, human *ChangeHuman, alg if human.State != nil { // only allow toggling between active and inactive // any other target state is not supported - // the existing human's state has to be the switch { case isUserStateActive(*human.State): if isUserStateActive(existingHuman.UserState) { diff --git a/internal/command/user_v2_human_test.go b/internal/command/user_v2_human_test.go index 2b4399fb2a..e44e182b92 100644 --- a/internal/command/user_v2_human_test.go +++ b/internal/command/user_v2_human_test.go @@ -302,9 +302,7 @@ func TestCommandSide_AddUserHuman(t *testing.T) { { name: "add human (with initial code), no permission", fields: fields{ - eventstore: expectEventstore( - expectFilter(), - ), + eventstore: expectEventstore(), checkPermission: newMockPermissionCheckNotAllowed(), idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), newCode: mockEncryptedCode("userinit", time.Hour), @@ -326,9 +324,7 @@ func TestCommandSide_AddUserHuman(t *testing.T) { codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), }, res: res{ - err: func(err error) bool { - return errors.Is(err, zerrors.ThrowPermissionDenied(nil, "AUTHZ-HKJD33", "Errors.PermissionDenied")) - }, + err: zerrors.IsPermissionDenied, }, }, { diff --git a/internal/command/user_v2_invite_test.go b/internal/command/user_v2_invite_test.go index 04c00d876e..75bd3157db 100644 --- a/internal/command/user_v2_invite_test.go +++ b/internal/command/user_v2_invite_test.go @@ -352,6 +352,7 @@ func TestCommands_ResendInviteCode(t *testing.T) { "user does not exist", fields{ eventstore: expectEventstore( + // The write model doesn't query any events expectFilter(), ), checkPermission: newMockPermissionCheckAllowed(), diff --git a/internal/command/user_v2_machine.go b/internal/command/user_v2_machine.go new file mode 100644 index 0000000000..34079b7e6f --- /dev/null +++ b/internal/command/user_v2_machine.go @@ -0,0 +1,94 @@ +package command + +import ( + "context" + + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/repository/user" + "github.com/zitadel/zitadel/internal/telemetry/tracing" + "github.com/zitadel/zitadel/internal/zerrors" +) + +type ChangeMachine struct { + ID string + ResourceOwner string + Username *string + Name *string + Description *string + + // Details are set after a successful execution of the command + Details *domain.ObjectDetails +} + +func (h *ChangeMachine) Changed() bool { + if h.Username != nil { + return true + } + if h.Name != nil { + return true + } + if h.Description != nil { + return true + } + return false +} + +func (c *Commands) ChangeUserMachine(ctx context.Context, machine *ChangeMachine) (err error) { + existingMachine, err := c.UserMachineWriteModel( + ctx, + machine.ID, + machine.ResourceOwner, + false, + ) + if err != nil { + return err + } + if machine.Changed() { + if err := c.checkPermissionUpdateUser(ctx, existingMachine.ResourceOwner, existingMachine.AggregateID); err != nil { + return err + } + } + + cmds := make([]eventstore.Command, 0) + if machine.Username != nil { + cmds, err = c.changeUsername(ctx, cmds, existingMachine, *machine.Username) + if err != nil { + return err + } + } + var machineChanges []user.MachineChanges + if machine.Name != nil && *machine.Name != existingMachine.Name { + machineChanges = append(machineChanges, user.ChangeName(*machine.Name)) + } + if machine.Description != nil && *machine.Description != existingMachine.Description { + machineChanges = append(machineChanges, user.ChangeDescription(*machine.Description)) + } + if len(machineChanges) > 0 { + cmds = append(cmds, user.NewMachineChangedEvent(ctx, &existingMachine.Aggregate().Aggregate, machineChanges)) + } + if len(cmds) == 0 { + machine.Details = writeModelToObjectDetails(&existingMachine.WriteModel) + return nil + } + err = c.pushAppendAndReduce(ctx, existingMachine, cmds...) + if err != nil { + return err + } + machine.Details = writeModelToObjectDetails(&existingMachine.WriteModel) + return nil +} + +func (c *Commands) UserMachineWriteModel(ctx context.Context, userID, resourceOwner string, metadataWM bool) (writeModel *UserV2WriteModel, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + writeModel = NewUserMachineWriteModel(userID, resourceOwner, metadataWM) + err = c.eventstore.FilterToQueryReducer(ctx, writeModel) + if err != nil { + return nil, err + } + if !isUserStateExists(writeModel.UserState) { + return nil, zerrors.ThrowNotFound(nil, "COMMAND-ugjs0upun6", "Errors.User.NotFound") + } + return writeModel, nil +} diff --git a/internal/command/user_v2_machine_test.go b/internal/command/user_v2_machine_test.go new file mode 100644 index 0000000000..14df4bfae7 --- /dev/null +++ b/internal/command/user_v2_machine_test.go @@ -0,0 +1,260 @@ +package command + +import ( + "context" + "errors" + "testing" + "time" + + "github.com/muhlemmer/gu" + "github.com/stretchr/testify/assert" + + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/repository/org" + "github.com/zitadel/zitadel/internal/repository/user" + "github.com/zitadel/zitadel/internal/zerrors" +) + +func TestCommandSide_ChangeUserMachine(t *testing.T) { + type fields struct { + eventstore func(t *testing.T) *eventstore.Eventstore + checkPermission domain.PermissionCheck + } + type args struct { + ctx context.Context + orgID string + machine *ChangeMachine + } + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + + userAgg := user.NewAggregate("user1", "org1") + userAddedEvent := user.NewMachineAddedEvent(context.Background(), + &userAgg.Aggregate, + "username", + "name", + "description", + true, + domain.OIDCTokenTypeBearer, + ) + + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "change machine username, no permission", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher(userAddedEvent), + ), + ), + checkPermission: newMockPermissionCheckNotAllowed(), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + machine: &ChangeMachine{ + Username: gu.Ptr("changed"), + }, + }, + res: res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowPermissionDenied(nil, "AUTHZ-HKJD33", "Errors.PermissionDenied")) + }, + }, + }, + { + name: "change machine username, not found", + fields: fields{ + eventstore: expectEventstore( + expectFilter(), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + machine: &ChangeMachine{ + Username: gu.Ptr("changed"), + }, + }, + res: res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowNotFound(nil, "COMMAND-ugjs0upun6", "Errors.User.NotFound")) + }, + }, + }, + { + name: "change machine username, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher(userAddedEvent), + ), + expectFilter( + eventFromEventPusher( + org.NewDomainPolicyAddedEvent(context.Background(), + &userAgg.Aggregate, + true, + true, + true, + ), + ), + ), + expectPush( + user.NewUsernameChangedEvent(context.Background(), + &userAgg.Aggregate, + "username", + "changed", + true, + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + machine: &ChangeMachine{ + Username: gu.Ptr("changed"), + }, + }, + res: res{ + want: &domain.ObjectDetails{ + Sequence: 0, + EventDate: time.Time{}, + ResourceOwner: "org1", + }, + }, + }, + { + name: "change machine username, no change", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher(userAddedEvent), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + machine: &ChangeMachine{ + Username: gu.Ptr("username"), + }, + }, + res: res{ + want: &domain.ObjectDetails{ + Sequence: 0, + EventDate: time.Time{}, + ResourceOwner: "org1", + }, + }, + }, + { + name: "change machine description, no permission", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher(userAddedEvent), + ), + ), + checkPermission: newMockPermissionCheckNotAllowed(), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + machine: &ChangeMachine{ + Description: gu.Ptr("changed"), + }, + }, + res: res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowPermissionDenied(nil, "AUTHZ-HKJD33", "Errors.PermissionDenied")) + }, + }, + }, + { + name: "change machine description, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher(userAddedEvent), + ), + expectPush( + user.NewMachineChangedEvent(context.Background(), + &userAgg.Aggregate, + []user.MachineChanges{ + user.ChangeDescription("changed"), + }, + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + machine: &ChangeMachine{ + Description: gu.Ptr("changed"), + }, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "change machine description, no change", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher(userAddedEvent), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + machine: &ChangeMachine{ + Description: gu.Ptr("description"), + }, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore(t), + checkPermission: tt.fields.checkPermission, + } + err := r.ChangeUserMachine(tt.args.ctx, tt.args.machine) + if tt.res.err == nil { + if !assert.NoError(t, err) { + t.FailNow() + } + } else if !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + return + } + if tt.res.err == nil { + assertObjectDetails(t, tt.res.want, tt.args.machine.Details) + } + }) + } +} diff --git a/internal/command/user_v2_model.go b/internal/command/user_v2_model.go index 214a2a5f9d..92346bf3b6 100644 --- a/internal/command/user_v2_model.go +++ b/internal/command/user_v2_model.go @@ -118,6 +118,14 @@ func NewUserHumanWriteModel(userID, resourceOwner string, profileWM, emailWM, ph return newUserV2WriteModel(userID, resourceOwner, opts...) } +func NewUserMachineWriteModel(userID, resourceOwner string, metadataListWM bool) *UserV2WriteModel { + opts := []UserV2WMOption{WithMachine(), WithState()} + if metadataListWM { + opts = append(opts, WithMetadata()) + } + return newUserV2WriteModel(userID, resourceOwner, opts...) +} + func newUserV2WriteModel(userID, resourceOwner string, opts ...UserV2WMOption) *UserV2WriteModel { wm := &UserV2WriteModel{ WriteModel: eventstore.WriteModel{ diff --git a/internal/domain/permission.go b/internal/domain/permission.go index fd300f63b9..bb569955f5 100644 --- a/internal/domain/permission.go +++ b/internal/domain/permission.go @@ -24,7 +24,7 @@ func (p *Permissions) appendPermission(ctxID, permission string) { p.Permissions = append(p.Permissions, permission) } -type PermissionCheck func(ctx context.Context, permission, orgID, resourceID string) (err error) +type PermissionCheck func(ctx context.Context, permission, resourceOwnerID, aggregateID string) (err error) const ( PermissionUserWrite = "user.write" diff --git a/internal/eventstore/write_model.go b/internal/eventstore/write_model.go index 277e65ed82..965fb16d0e 100644 --- a/internal/eventstore/write_model.go +++ b/internal/eventstore/write_model.go @@ -1,6 +1,8 @@ package eventstore -import "time" +import ( + "time" +) // WriteModel is the minimum representation of a command side write model. // It implements a basic reducer @@ -27,21 +29,25 @@ func (wm *WriteModel) Reduce() error { return nil } + latestEvent := wm.Events[len(wm.Events)-1] if wm.AggregateID == "" { - wm.AggregateID = wm.Events[0].Aggregate().ID - } - if wm.ResourceOwner == "" { - wm.ResourceOwner = wm.Events[0].Aggregate().ResourceOwner - } - if wm.InstanceID == "" { - wm.InstanceID = wm.Events[0].Aggregate().InstanceID + wm.AggregateID = latestEvent.Aggregate().ID } - wm.ProcessedSequence = wm.Events[len(wm.Events)-1].Sequence() - wm.ChangeDate = wm.Events[len(wm.Events)-1].CreatedAt() + if wm.ResourceOwner == "" { + wm.ResourceOwner = latestEvent.Aggregate().ResourceOwner + } + + if wm.InstanceID == "" { + wm.InstanceID = latestEvent.Aggregate().InstanceID + } + + wm.ProcessedSequence = latestEvent.Sequence() + wm.ChangeDate = latestEvent.CreatedAt() // all events processed and not needed anymore wm.Events = nil wm.Events = []Event{} + return nil } diff --git a/internal/integration/client.go b/internal/integration/client.go index 320809a7e8..3bf794f5f6 100644 --- a/internal/integration/client.go +++ b/internal/integration/client.go @@ -141,6 +141,7 @@ func (c *Client) pollHealth(ctx context.Context) (err error) { } } +// Deprecated: use CreateUserTypeHuman instead func (i *Instance) CreateHumanUser(ctx context.Context) *user_v2.AddHumanUserResponse { resp, err := i.Client.UserV2.AddHumanUser(ctx, &user_v2.AddHumanUserRequest{ Organization: &object.Organization{ @@ -172,6 +173,7 @@ func (i *Instance) CreateHumanUser(ctx context.Context) *user_v2.AddHumanUserRes return resp } +// Deprecated: user CreateUserTypeHuman instead func (i *Instance) CreateHumanUserNoPhone(ctx context.Context) *user_v2.AddHumanUserResponse { resp, err := i.Client.UserV2.AddHumanUser(ctx, &user_v2.AddHumanUserRequest{ Organization: &object.Organization{ @@ -197,6 +199,7 @@ func (i *Instance) CreateHumanUserNoPhone(ctx context.Context) *user_v2.AddHuman return resp } +// Deprecated: user CreateUserTypeHuman instead func (i *Instance) CreateHumanUserWithTOTP(ctx context.Context, secret string) *user_v2.AddHumanUserResponse { resp, err := i.Client.UserV2.AddHumanUser(ctx, &user_v2.AddHumanUserRequest{ Organization: &object.Organization{ @@ -229,6 +232,43 @@ func (i *Instance) CreateHumanUserWithTOTP(ctx context.Context, secret string) * return resp } +func (i *Instance) CreateUserTypeHuman(ctx context.Context) *user_v2.CreateUserResponse { + resp, err := i.Client.UserV2.CreateUser(ctx, &user_v2.CreateUserRequest{ + OrganizationId: i.DefaultOrg.GetId(), + UserType: &user_v2.CreateUserRequest_Human_{ + Human: &user_v2.CreateUserRequest_Human{ + Profile: &user_v2.SetHumanProfile{ + GivenName: "Mickey", + FamilyName: "Mouse", + }, + Email: &user_v2.SetHumanEmail{ + Email: fmt.Sprintf("%d@mouse.com", time.Now().UnixNano()), + Verification: &user_v2.SetHumanEmail_ReturnCode{ + ReturnCode: &user_v2.ReturnEmailVerificationCode{}, + }, + }, + }, + }, + }) + logging.OnError(err).Panic("create human user") + i.TriggerUserByID(ctx, resp.GetId()) + return resp +} + +func (i *Instance) CreateUserTypeMachine(ctx context.Context) *user_v2.CreateUserResponse { + resp, err := i.Client.UserV2.CreateUser(ctx, &user_v2.CreateUserRequest{ + OrganizationId: i.DefaultOrg.GetId(), + UserType: &user_v2.CreateUserRequest_Machine_{ + Machine: &user_v2.CreateUserRequest_Machine{ + Name: "machine", + }, + }, + }) + logging.OnError(err).Panic("create machine user") + i.TriggerUserByID(ctx, resp.GetId()) + return resp +} + // TriggerUserByID makes sure the user projection gets triggered after creation. func (i *Instance) TriggerUserByID(ctx context.Context, users ...string) { var wg sync.WaitGroup diff --git a/internal/query/authn_key.go b/internal/query/authn_key.go index 8075422e63..ffbe38e7ae 100644 --- a/internal/query/authn_key.go +++ b/internal/query/authn_key.go @@ -5,6 +5,7 @@ import ( "database/sql" _ "embed" "errors" + "slices" "time" sq "github.com/Masterminds/squirrel" @@ -18,6 +19,14 @@ import ( "github.com/zitadel/zitadel/internal/zerrors" ) +func keysCheckPermission(ctx context.Context, keys *AuthNKeys, permissionCheck domain.PermissionCheck) { + keys.AuthNKeys = slices.DeleteFunc(keys.AuthNKeys, + func(key *AuthNKey) bool { + return userCheckPermission(ctx, key.ResourceOwner, key.AggregateID, permissionCheck) != nil + }, + ) +} + var ( authNKeyTable = table{ name: projection.AuthNKeyTable, @@ -84,6 +93,7 @@ type AuthNKeys struct { type AuthNKey struct { ID string + AggregateID string CreationDate time.Time ChangeDate time.Time ResourceOwner string @@ -124,12 +134,47 @@ func (q *AuthNKeySearchQueries) toQuery(query sq.SelectBuilder) sq.SelectBuilder return query } -func (q *Queries) SearchAuthNKeys(ctx context.Context, queries *AuthNKeySearchQueries, withOwnerRemoved bool) (authNKeys *AuthNKeys, err error) { +type JoinFilter int + +const ( + JoinFilterUnspecified JoinFilter = iota + JoinFilterApp + JoinFilterUserMachine +) + +// SearchAuthNKeys returns machine or app keys, depending on the join filter. +// If permissionCheck is nil, the keys are not filtered. +// If permissionCheck is not nil and the PermissionCheckV2 feature flag is false, the returned keys are filtered in-memory by the given permission check. +// If permissionCheck is not nil and the PermissionCheckV2 feature flag is true, the returned keys are filtered in the database. +func (q *Queries) SearchAuthNKeys(ctx context.Context, queries *AuthNKeySearchQueries, filter JoinFilter, permissionCheck domain.PermissionCheck) (authNKeys *AuthNKeys, err error) { + permissionCheckV2 := PermissionV2(ctx, permissionCheck) + keys, err := q.searchAuthNKeys(ctx, queries, filter, permissionCheckV2) + if err != nil { + return nil, err + } + if permissionCheck != nil && !authz.GetFeatures(ctx).PermissionCheckV2 { + keysCheckPermission(ctx, keys, permissionCheck) + } + return keys, nil +} + +func (q *Queries) searchAuthNKeys(ctx context.Context, queries *AuthNKeySearchQueries, joinFilter JoinFilter, permissionCheckV2 bool) (authNKeys *AuthNKeys, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() query, scan := prepareAuthNKeysQuery() query = queries.toQuery(query) + switch joinFilter { + case JoinFilterUnspecified: + // Select all authN keys + case JoinFilterApp: + joinCol := ProjectColumnID + query = query.Join(joinCol.table.identifier() + " ON " + AuthNKeyColumnIdentifier.identifier() + " = " + joinCol.identifier()) + case JoinFilterUserMachine: + joinCol := MachineUserIDCol + query = query.Join(joinCol.table.identifier() + " ON " + AuthNKeyColumnIdentifier.identifier() + " = " + joinCol.identifier()) + query = userPermissionCheckV2WithCustomColumns(ctx, query, permissionCheckV2, queries.Queries, AuthNKeyColumnResourceOwner, AuthNKeyColumnIdentifier) + } eq := sq.Eq{ AuthNKeyColumnEnabled.identifier(): true, AuthNKeyColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), @@ -249,6 +294,22 @@ func NewAuthNKeyObjectIDQuery(id string) (SearchQuery, error) { return NewTextQuery(AuthNKeyColumnObjectID, id, TextEquals) } +func NewAuthNKeyIDQuery(id string) (SearchQuery, error) { + return NewTextQuery(AuthNKeyColumnID, id, TextEquals) +} + +func NewAuthNKeyIdentifyerQuery(id string) (SearchQuery, error) { + return NewTextQuery(AuthNKeyColumnIdentifier, id, TextEquals) +} + +func NewAuthNKeyCreationDateQuery(ts time.Time, compare TimestampComparison) (SearchQuery, error) { + return NewTimestampQuery(AuthNKeyColumnCreationDate, ts, compare) +} + +func NewAuthNKeyExpirationDateDateQuery(ts time.Time, compare TimestampComparison) (SearchQuery, error) { + return NewTimestampQuery(AuthNKeyColumnExpiration, ts, compare) +} + //go:embed authn_key_user.sql var authNKeyUserQuery string @@ -288,49 +349,52 @@ func (q *Queries) GetAuthNKeyUser(ctx context.Context, keyID, userID string) (_ } func prepareAuthNKeysQuery() (sq.SelectBuilder, func(rows *sql.Rows) (*AuthNKeys, error)) { - return sq.Select( - AuthNKeyColumnID.identifier(), - AuthNKeyColumnCreationDate.identifier(), - AuthNKeyColumnChangeDate.identifier(), - AuthNKeyColumnResourceOwner.identifier(), - AuthNKeyColumnSequence.identifier(), - AuthNKeyColumnExpiration.identifier(), - AuthNKeyColumnType.identifier(), - countColumn.identifier(), - ).From(authNKeyTable.identifier()). - PlaceholderFormat(sq.Dollar), - func(rows *sql.Rows) (*AuthNKeys, error) { - authNKeys := make([]*AuthNKey, 0) - var count uint64 - for rows.Next() { - authNKey := new(AuthNKey) - err := rows.Scan( - &authNKey.ID, - &authNKey.CreationDate, - &authNKey.ChangeDate, - &authNKey.ResourceOwner, - &authNKey.Sequence, - &authNKey.Expiration, - &authNKey.Type, - &count, - ) - if err != nil { - return nil, err - } - authNKeys = append(authNKeys, authNKey) - } + query := sq.Select( + AuthNKeyColumnID.identifier(), + AuthNKeyColumnAggregateID.identifier(), + AuthNKeyColumnCreationDate.identifier(), + AuthNKeyColumnChangeDate.identifier(), + AuthNKeyColumnResourceOwner.identifier(), + AuthNKeyColumnSequence.identifier(), + AuthNKeyColumnExpiration.identifier(), + AuthNKeyColumnType.identifier(), + countColumn.identifier(), + ).From(authNKeyTable.identifier()). + PlaceholderFormat(sq.Dollar) - if err := rows.Close(); err != nil { - return nil, zerrors.ThrowInternal(err, "QUERY-Dgfn3", "Errors.Query.CloseRows") + return query, func(rows *sql.Rows) (*AuthNKeys, error) { + authNKeys := make([]*AuthNKey, 0) + var count uint64 + for rows.Next() { + authNKey := new(AuthNKey) + err := rows.Scan( + &authNKey.ID, + &authNKey.AggregateID, + &authNKey.CreationDate, + &authNKey.ChangeDate, + &authNKey.ResourceOwner, + &authNKey.Sequence, + &authNKey.Expiration, + &authNKey.Type, + &count, + ) + if err != nil { + return nil, err } - - return &AuthNKeys{ - AuthNKeys: authNKeys, - SearchResponse: SearchResponse{ - Count: count, - }, - }, nil + authNKeys = append(authNKeys, authNKey) } + + if err := rows.Close(); err != nil { + return nil, zerrors.ThrowInternal(err, "QUERY-Dgfn3", "Errors.Query.CloseRows") + } + + return &AuthNKeys{ + AuthNKeys: authNKeys, + SearchResponse: SearchResponse{ + Count: count, + }, + }, nil + } } func prepareAuthNKeyQuery() (sq.SelectBuilder, func(row *sql.Row) (*AuthNKey, error)) { diff --git a/internal/query/authn_key_test.go b/internal/query/authn_key_test.go index c7441f8dae..b7c66cc665 100644 --- a/internal/query/authn_key_test.go +++ b/internal/query/authn_key_test.go @@ -19,6 +19,7 @@ import ( var ( prepareAuthNKeysStmt = `SELECT projections.authn_keys2.id,` + + ` projections.authn_keys2.aggregate_id,` + ` projections.authn_keys2.creation_date,` + ` projections.authn_keys2.change_date,` + ` projections.authn_keys2.resource_owner,` + @@ -29,6 +30,7 @@ var ( ` FROM projections.authn_keys2` prepareAuthNKeysCols = []string{ "id", + "aggregate_id", "creation_date", "change_date", "resource_owner", @@ -120,6 +122,7 @@ func Test_AuthNKeyPrepares(t *testing.T) { [][]driver.Value{ { "id", + "aggId", testNow, testNow, "ro", @@ -137,6 +140,7 @@ func Test_AuthNKeyPrepares(t *testing.T) { AuthNKeys: []*AuthNKey{ { ID: "id", + AggregateID: "aggId", CreationDate: testNow, ChangeDate: testNow, ResourceOwner: "ro", @@ -157,6 +161,7 @@ func Test_AuthNKeyPrepares(t *testing.T) { [][]driver.Value{ { "id-1", + "aggId-1", testNow, testNow, "ro", @@ -166,6 +171,7 @@ func Test_AuthNKeyPrepares(t *testing.T) { }, { "id-2", + "aggId-2", testNow, testNow, "ro", @@ -183,6 +189,7 @@ func Test_AuthNKeyPrepares(t *testing.T) { AuthNKeys: []*AuthNKey{ { ID: "id-1", + AggregateID: "aggId-1", CreationDate: testNow, ChangeDate: testNow, ResourceOwner: "ro", @@ -192,6 +199,7 @@ func Test_AuthNKeyPrepares(t *testing.T) { }, { ID: "id-2", + AggregateID: "aggId-2", CreationDate: testNow, ChangeDate: testNow, ResourceOwner: "ro", diff --git a/internal/query/projection/authn_key.go b/internal/query/projection/authn_key.go index e2229ad332..a287701cfb 100644 --- a/internal/query/projection/authn_key.go +++ b/internal/query/projection/authn_key.go @@ -62,6 +62,9 @@ func (*authNKeyProjection) Init() *old_handler.Check { handler.NewPrimaryKey(AuthNKeyInstanceIDCol, AuthNKeyIDCol), handler.WithIndex(handler.NewIndex("enabled", []string{AuthNKeyEnabledCol})), handler.WithIndex(handler.NewIndex("identifier", []string{AuthNKeyIdentifierCol})), + handler.WithIndex(handler.NewIndex("resource_owner", []string{AuthNKeyResourceOwnerCol})), + handler.WithIndex(handler.NewIndex("creation_date", []string{AuthNKeyCreationDateCol})), + handler.WithIndex(handler.NewIndex("expiration_date", []string{AuthNKeyExpirationCol})), ), ) } diff --git a/internal/query/projection/user_personal_access_token.go b/internal/query/projection/user_personal_access_token.go index 0efb5d6412..610ca9c4e2 100644 --- a/internal/query/projection/user_personal_access_token.go +++ b/internal/query/projection/user_personal_access_token.go @@ -56,6 +56,8 @@ func (*personalAccessTokenProjection) Init() *old_handler.Check { handler.WithIndex(handler.NewIndex("user_id", []string{PersonalAccessTokenColumnUserID})), handler.WithIndex(handler.NewIndex("resource_owner", []string{PersonalAccessTokenColumnResourceOwner})), handler.WithIndex(handler.NewIndex("owner_removed", []string{PersonalAccessTokenColumnOwnerRemoved})), + handler.WithIndex(handler.NewIndex("creation_date", []string{PersonalAccessTokenColumnCreationDate})), + handler.WithIndex(handler.NewIndex("expiration_date", []string{PersonalAccessTokenColumnExpiration})), ), ) } diff --git a/internal/query/user.go b/internal/query/user.go index 3d47847cac..6844982f07 100644 --- a/internal/query/user.go +++ b/internal/query/user.go @@ -132,16 +132,20 @@ func usersCheckPermission(ctx context.Context, users *Users, permissionCheck dom ) } -func userPermissionCheckV2(ctx context.Context, query sq.SelectBuilder, enabled bool, queries *UserSearchQueries) sq.SelectBuilder { +func userPermissionCheckV2(ctx context.Context, query sq.SelectBuilder, enabled bool, filters []SearchQuery) sq.SelectBuilder { + return userPermissionCheckV2WithCustomColumns(ctx, query, enabled, filters, UserResourceOwnerCol, UserIDCol) +} + +func userPermissionCheckV2WithCustomColumns(ctx context.Context, query sq.SelectBuilder, enabled bool, filters []SearchQuery, userResourceOwnerCol, userID Column) sq.SelectBuilder { if !enabled { return query } join, args := PermissionClause( ctx, - UserResourceOwnerCol, + userResourceOwnerCol, domain.PermissionUserRead, - SingleOrgPermissionOption(queries.Queries), - OwnedRowsPermissionOption(UserIDCol), + SingleOrgPermissionOption(filters), + OwnedRowsPermissionOption(userID), ) return query.JoinClause(join, args...) } @@ -637,7 +641,7 @@ func (q *Queries) searchUsers(ctx context.Context, queries *UserSearchQueries, p defer func() { span.EndWithError(err) }() query, scan := prepareUsersQuery() - query = userPermissionCheckV2(ctx, query, permissionCheckV2, queries) + query = userPermissionCheckV2(ctx, query, permissionCheckV2, queries.Queries) stmt, args, err := queries.toQuery(query).Where(sq.Eq{ UserInstanceIDCol.identifier(): authz.GetInstance(ctx).InstanceID(), }).ToSql() diff --git a/internal/query/user_personal_access_token.go b/internal/query/user_personal_access_token.go index 8ea33f51a4..61d349961c 100644 --- a/internal/query/user_personal_access_token.go +++ b/internal/query/user_personal_access_token.go @@ -4,6 +4,7 @@ import ( "context" "database/sql" "errors" + "slices" "time" sq "github.com/Masterminds/squirrel" @@ -11,12 +12,21 @@ import ( "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/database" + "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore/handler/v2" "github.com/zitadel/zitadel/internal/query/projection" "github.com/zitadel/zitadel/internal/telemetry/tracing" "github.com/zitadel/zitadel/internal/zerrors" ) +func patsCheckPermission(ctx context.Context, tokens *PersonalAccessTokens, permissionCheck domain.PermissionCheck) { + tokens.PersonalAccessTokens = slices.DeleteFunc(tokens.PersonalAccessTokens, + func(token *PersonalAccessToken) bool { + return userCheckPermission(ctx, token.ResourceOwner, token.UserID, permissionCheck) != nil + }, + ) +} + var ( personalAccessTokensTable = table{ name: projection.PersonalAccessTokenProjectionTable, @@ -86,7 +96,7 @@ type PersonalAccessTokenSearchQueries struct { Queries []SearchQuery } -func (q *Queries) PersonalAccessTokenByID(ctx context.Context, shouldTriggerBulk bool, id string, withOwnerRemoved bool, queries ...SearchQuery) (pat *PersonalAccessToken, err error) { +func (q *Queries) PersonalAccessTokenByID(ctx context.Context, shouldTriggerBulk bool, id string, queries ...SearchQuery) (pat *PersonalAccessToken, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() @@ -102,11 +112,9 @@ func (q *Queries) PersonalAccessTokenByID(ctx context.Context, shouldTriggerBulk query = q.toQuery(query) } eq := sq.Eq{ - PersonalAccessTokenColumnID.identifier(): id, - PersonalAccessTokenColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), - } - if !withOwnerRemoved { - eq[PersonalAccessTokenColumnOwnerRemoved.identifier()] = false + PersonalAccessTokenColumnID.identifier(): id, + PersonalAccessTokenColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), + PersonalAccessTokenColumnOwnerRemoved.identifier(): false, } stmt, args, err := query.Where(eq).ToSql() if err != nil { @@ -123,18 +131,34 @@ func (q *Queries) PersonalAccessTokenByID(ctx context.Context, shouldTriggerBulk return pat, nil } -func (q *Queries) SearchPersonalAccessTokens(ctx context.Context, queries *PersonalAccessTokenSearchQueries, withOwnerRemoved bool) (personalAccessTokens *PersonalAccessTokens, err error) { +// SearchPersonalAccessTokens returns personal access token resources. +// If permissionCheck is nil, the PATs are not filtered. +// If permissionCheck is not nil and the PermissionCheckV2 feature flag is false, the returned PATs are filtered in-memory by the given permission check. +// If permissionCheck is not nil and the PermissionCheckV2 feature flag is true, the returned PATs are filtered in the database. +func (q *Queries) SearchPersonalAccessTokens(ctx context.Context, queries *PersonalAccessTokenSearchQueries, permissionCheck domain.PermissionCheck) (authNKeys *PersonalAccessTokens, err error) { + permissionCheckV2 := PermissionV2(ctx, permissionCheck) + keys, err := q.searchPersonalAccessTokens(ctx, queries, permissionCheckV2) + if err != nil { + return nil, err + } + if permissionCheck != nil && !authz.GetFeatures(ctx).PermissionCheckV2 { + patsCheckPermission(ctx, keys, permissionCheck) + } + return keys, nil +} + +func (q *Queries) searchPersonalAccessTokens(ctx context.Context, queries *PersonalAccessTokenSearchQueries, permissionCheckV2 bool) (personalAccessTokens *PersonalAccessTokens, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() query, scan := preparePersonalAccessTokensQuery() + query = queries.toQuery(query) + query = userPermissionCheckV2WithCustomColumns(ctx, query, permissionCheckV2, queries.Queries, PersonalAccessTokenColumnResourceOwner, PersonalAccessTokenColumnUserID) eq := sq.Eq{ - PersonalAccessTokenColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), + PersonalAccessTokenColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), + PersonalAccessTokenColumnOwnerRemoved.identifier(): false, } - if !withOwnerRemoved { - eq[PersonalAccessTokenColumnOwnerRemoved.identifier()] = false - } - stmt, args, err := queries.toQuery(query).Where(eq).ToSql() + stmt, args, err := query.Where(eq).ToSql() if err != nil { return nil, zerrors.ThrowInvalidArgument(err, "QUERY-Hjw2w", "Errors.Query.InvalidRequest") } @@ -160,6 +184,18 @@ func NewPersonalAccessTokenUserIDSearchQuery(value string) (SearchQuery, error) return NewTextQuery(PersonalAccessTokenColumnUserID, value, TextEquals) } +func NewPersonalAccessTokenIDQuery(id string) (SearchQuery, error) { + return NewTextQuery(PersonalAccessTokenColumnID, id, TextEquals) +} + +func NewPersonalAccessTokenCreationDateQuery(ts time.Time, compare TimestampComparison) (SearchQuery, error) { + return NewTimestampQuery(PersonalAccessTokenColumnCreationDate, ts, compare) +} + +func NewPersonalAccessTokenExpirationDateDateQuery(ts time.Time, compare TimestampComparison) (SearchQuery, error) { + return NewTimestampQuery(PersonalAccessTokenColumnExpiration, ts, compare) +} + func (r *PersonalAccessTokenSearchQueries) AppendMyResourceOwnerQuery(orgID string) error { query, err := NewPersonalAccessTokenResourceOwnerSearchQuery(orgID) if err != nil { diff --git a/internal/repository/user/machine.go b/internal/repository/user/machine.go index d76290931a..a466f92fe3 100644 --- a/internal/repository/user/machine.go +++ b/internal/repository/user/machine.go @@ -88,10 +88,7 @@ func NewMachineChangedEvent( ctx context.Context, aggregate *eventstore.Aggregate, changes []MachineChanges, -) (*MachineChangedEvent, error) { - if len(changes) == 0 { - return nil, zerrors.ThrowPreconditionFailed(nil, "USER-3M9fs", "Errors.NoChangesFound") - } +) *MachineChangedEvent { changeEvent := &MachineChangedEvent{ BaseEvent: *eventstore.NewBaseEventForPush( ctx, @@ -102,7 +99,7 @@ func NewMachineChangedEvent( for _, change := range changes { change(changeEvent) } - return changeEvent, nil + return changeEvent } type MachineChanges func(event *MachineChangedEvent) diff --git a/internal/static/i18n/bg.yaml b/internal/static/i18n/bg.yaml index 8254b82b45..d58b2eb64a 100644 --- a/internal/static/i18n/bg.yaml +++ b/internal/static/i18n/bg.yaml @@ -117,6 +117,7 @@ Errors: AlreadyVerified: Телефонът вече е потвърден Empty: Телефонът е празен NotChanged: Телефонът не е сменен + VerifyingRemovalIsNotSupported: Премахването на проверката не се поддържа Address: NotFound: Адресът не е намерен NotChanged: Адресът не е променен diff --git a/internal/static/i18n/cs.yaml b/internal/static/i18n/cs.yaml index bb4172fbff..d248ce4ca7 100644 --- a/internal/static/i18n/cs.yaml +++ b/internal/static/i18n/cs.yaml @@ -115,6 +115,7 @@ Errors: AlreadyVerified: Telefon již ověřen Empty: Telefon je prázdný NotChanged: Telefon nezměněn + VerifyingRemovalIsNotSupported: Ověření odstranění telefonu není podporováno Address: NotFound: Adresa nenalezena NotChanged: Adresa nezměněna diff --git a/internal/static/i18n/de.yaml b/internal/static/i18n/de.yaml index a24ce7c933..96edf57456 100644 --- a/internal/static/i18n/de.yaml +++ b/internal/static/i18n/de.yaml @@ -115,6 +115,7 @@ Errors: AlreadyVerified: Telefonnummer bereits verifiziert Empty: Telefonnummer ist leer NotChanged: Telefonnummer wurde nicht geändert + VerifyingRemovalIsNotSupported: Verifizieren der Telefonnummer Entfernung wird nicht unterstützt Address: NotFound: Adresse nicht gefunden NotChanged: Adresse wurde nicht geändert diff --git a/internal/static/i18n/en.yaml b/internal/static/i18n/en.yaml index e8f2781de1..0f512defe4 100644 --- a/internal/static/i18n/en.yaml +++ b/internal/static/i18n/en.yaml @@ -116,6 +116,7 @@ Errors: AlreadyVerified: Phone already verified Empty: Phone is empty NotChanged: Phone not changed + VerifyingRemovalIsNotSupported: Verifying phone removal is not supported Address: NotFound: Address not found NotChanged: Address not changed diff --git a/internal/static/i18n/es.yaml b/internal/static/i18n/es.yaml index b91d055f70..8c901f8ebe 100644 --- a/internal/static/i18n/es.yaml +++ b/internal/static/i18n/es.yaml @@ -115,6 +115,7 @@ Errors: AlreadyVerified: El teléfono ya se verificó Empty: El teléfono está vacío NotChanged: El teléfono no ha cambiado + VerifyingRemovalIsNotSupported: La verificación de eliminación no está soportada Address: NotFound: Dirección no encontrada NotChanged: La dirección no ha cambiado diff --git a/internal/static/i18n/fr.yaml b/internal/static/i18n/fr.yaml index 98f2bee9a0..2a2a51d7c4 100644 --- a/internal/static/i18n/fr.yaml +++ b/internal/static/i18n/fr.yaml @@ -115,6 +115,7 @@ Errors: AlreadyVerified: Téléphone déjà vérifié Empty: Téléphone est vide NotChanged: Téléphone n'a pas changé + VerifyingRemovalIsNotSupported: La vérification de la suppression n'est pas prise en charge Address: NotFound: Adresse non trouvée NotChanged: L'adresse n'a pas changé diff --git a/internal/static/i18n/hu.yaml b/internal/static/i18n/hu.yaml index 5becd6e606..a4cc908fa2 100644 --- a/internal/static/i18n/hu.yaml +++ b/internal/static/i18n/hu.yaml @@ -115,6 +115,7 @@ Errors: AlreadyVerified: Telefon már ellenőrizve Empty: A telefon mező üres NotChanged: Telefon nem lett megváltoztatva + VerifyingRemovalIsNotSupported: A telefon eltávolításának ellenőrzése nem támogatott Address: NotFound: Cím nem található NotChanged: Cím nem lett megváltoztatva diff --git a/internal/static/i18n/id.yaml b/internal/static/i18n/id.yaml index 0108d7618b..c9187020f7 100644 --- a/internal/static/i18n/id.yaml +++ b/internal/static/i18n/id.yaml @@ -115,6 +115,7 @@ Errors: AlreadyVerified: Telepon sudah diverifikasi Empty: Telepon kosong NotChanged: Telepon tidak berubah + VerifyingRemovalIsNotSupported: Verifikasi penghapusan tidak didukung Address: NotFound: Alamat tidak ditemukan NotChanged: Alamat tidak berubah diff --git a/internal/static/i18n/it.yaml b/internal/static/i18n/it.yaml index 750c48471a..d1dccef4c7 100644 --- a/internal/static/i18n/it.yaml +++ b/internal/static/i18n/it.yaml @@ -115,6 +115,7 @@ Errors: AlreadyVerified: Telefono già verificato Empty: Il telefono è vuoto NotChanged: Telefono non cambiato + VerifyingRemovalIsNotSupported: La rimozione della verifica non è supportata Address: NotFound: Indirizzo non trovato NotChanged: Indirizzo non cambiato diff --git a/internal/static/i18n/ja.yaml b/internal/static/i18n/ja.yaml index fcd7920999..4b0f2ea203 100644 --- a/internal/static/i18n/ja.yaml +++ b/internal/static/i18n/ja.yaml @@ -116,6 +116,7 @@ Errors: AlreadyVerified: 電話番号はすでに認証済みです Empty: 電話番号が空です NotChanged: 電話番号が変更されていません + VerifyingRemovalIsNotSupported: 電話番号の削除を検証することはできません Address: NotFound: 住所が見つかりません NotChanged: 住所は変更されていません diff --git a/internal/static/i18n/ko.yaml b/internal/static/i18n/ko.yaml index d83af62235..2c87aa1f97 100644 --- a/internal/static/i18n/ko.yaml +++ b/internal/static/i18n/ko.yaml @@ -116,6 +116,7 @@ Errors: AlreadyVerified: 전화번호가 이미 인증되었습니다 Empty: 전화번호가 비어 있습니다 NotChanged: 전화번호가 변경되지 않았습니다 + VerifyingRemovalIsNotSupported: 전화번호 제거를 확인하는 것은 지원되지 않습니다 Address: NotFound: 주소를 찾을 수 없습니다 NotChanged: 주소가 변경되지 않았습니다 diff --git a/internal/static/i18n/mk.yaml b/internal/static/i18n/mk.yaml index 7126925279..64ae87a618 100644 --- a/internal/static/i18n/mk.yaml +++ b/internal/static/i18n/mk.yaml @@ -115,6 +115,7 @@ Errors: AlreadyVerified: Телефонскиот број веќе е верифициран Empty: Телефонскиот број е празен NotChanged: Телефонскиот број не е променет + VerifyingRemovalIsNotSupported: Отстранувањето на верификацијата не е поддржано Address: NotFound: Адресата не е пронајдена NotChanged: Адресата не е променета diff --git a/internal/static/i18n/nl.yaml b/internal/static/i18n/nl.yaml index a398e4b770..dc9fd83721 100644 --- a/internal/static/i18n/nl.yaml +++ b/internal/static/i18n/nl.yaml @@ -115,6 +115,7 @@ Errors: AlreadyVerified: Telefoon is al geverifieerd Empty: Telefoon is leeg NotChanged: Telefoon niet veranderd + VerifyingRemovalIsNotSupported: Verwijderen van verificatie is niet ondersteund Address: NotFound: Adres niet gevonden NotChanged: Adres niet veranderd diff --git a/internal/static/i18n/pl.yaml b/internal/static/i18n/pl.yaml index 049a189930..4952345510 100644 --- a/internal/static/i18n/pl.yaml +++ b/internal/static/i18n/pl.yaml @@ -115,6 +115,7 @@ Errors: AlreadyVerified: Numer telefonu już zweryfikowany Empty: Numer telefonu jest pusty NotChanged: Numer telefonu nie zmieniony + VerifyingRemovalIsNotSupported: Usunięcie weryfikacji nie jest obsługiwane Address: NotFound: Adres nie znaleziony NotChanged: Adres nie zmieniony diff --git a/internal/static/i18n/pt.yaml b/internal/static/i18n/pt.yaml index 09a5fc02c5..e5fc785d0c 100644 --- a/internal/static/i18n/pt.yaml +++ b/internal/static/i18n/pt.yaml @@ -115,6 +115,7 @@ Errors: AlreadyVerified: O telefone já foi verificado Empty: O telefone está vazio NotChanged: Telefone não alterado + VerifyingRemovalIsNotSupported: Remoção de verificação não suportada Address: NotFound: Endereço não encontrado NotChanged: Endereço não alterado diff --git a/internal/static/i18n/ro.yaml b/internal/static/i18n/ro.yaml index 9010e57032..ece4680de6 100644 --- a/internal/static/i18n/ro.yaml +++ b/internal/static/i18n/ro.yaml @@ -116,6 +116,7 @@ Errors: AlreadyVerified: Numărul de telefon este deja verificat Empty: Numărul de telefon este gol NotChanged: Numărul de telefon nu a fost schimbat + VerifyingRemovalIsNotSupported: Verificarea eliminării nu este acceptată Address: NotFound: Adresa nu a fost găsită NotChanged: Adresa nu a fost schimbată diff --git a/internal/static/i18n/ru.yaml b/internal/static/i18n/ru.yaml index 38b2847637..a2efd25322 100644 --- a/internal/static/i18n/ru.yaml +++ b/internal/static/i18n/ru.yaml @@ -116,6 +116,7 @@ Errors: AlreadyVerified: Телефон уже подтверждён Empty: Телефон пуст NotChanged: Телефон не менялся + VerifyingRemovalIsNotSupported: Удаление телефона не поддерживается Address: NotFound: Адрес не найден NotChanged: Адрес не изменён diff --git a/internal/static/i18n/sv.yaml b/internal/static/i18n/sv.yaml index ed4b863886..be40ceba3c 100644 --- a/internal/static/i18n/sv.yaml +++ b/internal/static/i18n/sv.yaml @@ -115,6 +115,7 @@ Errors: AlreadyVerified: Mobilnr redan verifierad Empty: Mobilnr är tom NotChanged: Mobilnr ändrades inte + VerifyingRemovalIsNotSupported: Verifiering av borttagning stöds inte Address: NotFound: Adress hittades inte NotChanged: Adress ändrades inte diff --git a/internal/static/i18n/zh.yaml b/internal/static/i18n/zh.yaml index 03aa168a50..930fcaddae 100644 --- a/internal/static/i18n/zh.yaml +++ b/internal/static/i18n/zh.yaml @@ -115,6 +115,7 @@ Errors: AlreadyVerified: 手机号码已经验证 Empty: 电话号码是空的 NotChanged: 电话号码没有改变 + VerifyingRemovalIsNotSupported: 验证手机号码删除不受支持 Address: NotFound: 找不到地址 NotChanged: 地址没有改变 diff --git a/proto/buf.yaml b/proto/buf.yaml index 31bc7b4ccc..abe35b3055 100644 --- a/proto/buf.yaml +++ b/proto/buf.yaml @@ -40,4 +40,4 @@ lint: - zitadel/system.proto - zitadel/text.proto - zitadel/user.proto - - zitadel/v1.proto \ No newline at end of file + - zitadel/v1.proto diff --git a/proto/zitadel/filter/v2/filter.proto b/proto/zitadel/filter/v2/filter.proto new file mode 100644 index 0000000000..3817324d31 --- /dev/null +++ b/proto/zitadel/filter/v2/filter.proto @@ -0,0 +1,96 @@ +syntax = "proto3"; + +package zitadel.filter.v2; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/filter/v2;filter"; + +import "google/protobuf/timestamp.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; +import "validate/validate.proto"; + +enum TextFilterMethod { + TEXT_FILTER_METHOD_EQUALS = 0; + TEXT_FILTER_METHOD_EQUALS_IGNORE_CASE = 1; + TEXT_FILTER_METHOD_STARTS_WITH = 2; + TEXT_FILTER_METHOD_STARTS_WITH_IGNORE_CASE = 3; + TEXT_FILTER_METHOD_CONTAINS = 4; + TEXT_FILTER_METHOD_CONTAINS_IGNORE_CASE = 5; + TEXT_FILTER_METHOD_ENDS_WITH = 6; + TEXT_FILTER_METHOD_ENDS_WITH_IGNORE_CASE = 7; +} + +enum ListFilterMethod { + LIST_FILTER_METHOD_IN = 0; +} + +enum TimestampFilterMethod { + TIMESTAMP_FILTER_METHOD_EQUALS = 0; + TIMESTAMP_FILTER_METHOD_AFTER = 1; + TIMESTAMP_FILTER_METHOD_AFTER_OR_EQUALS = 2; + TIMESTAMP_FILTER_METHOD_BEFORE = 3; + TIMESTAMP_FILTER_METHOD_BEFORE_OR_EQUALS = 4; +} + +message PaginationRequest { + // Starting point for retrieval, in combination of offset used to query a set list of objects. + uint64 offset = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "0"; + } + ]; + // 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 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "10"; + } + ]; + // 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 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "false"; + } + ]; +} + +message PaginationResponse { + // Absolute number of objects matching the query, regardless of applied limit. + uint64 total_result = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "100"; + } + ]; + // Applied limit from query, defines maximum amount of objects per request, to compare if all objects are returned. + uint64 applied_limit = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "100"; + } + ]; +} + +message IDFilter { + // Only return resources that belong to this id. + string id = 1 [ + (validate.rules).string = {max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + max_length: 200; + example: "\"69629023906488337\""; + } + ]; +} + +message TimestampFilter { + // Filter resources by timestamp. + google.protobuf.Timestamp timestamp = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; + // Defines the condition (e.g., equals, before, after) that the timestamp of the retrieved resources should match. + TimestampFilterMethod method = 2 [ + (validate.rules).enum.defined_only = true + ]; +} diff --git a/proto/zitadel/filter/v2beta/filter.proto b/proto/zitadel/filter/v2beta/filter.proto index 6aae583cde..2265fa4125 100644 --- a/proto/zitadel/filter/v2beta/filter.proto +++ b/proto/zitadel/filter/v2beta/filter.proto @@ -6,6 +6,7 @@ option go_package = "github.com/zitadel/zitadel/pkg/grpc/filter/v2beta;filter"; import "google/protobuf/timestamp.proto"; import "protoc-gen-openapiv2/options/annotations.proto"; +import "validate/validate.proto"; enum TextFilterMethod { TEXT_FILTER_METHOD_EQUALS = 0; @@ -56,4 +57,37 @@ message PaginationResponse { example: "\"100\""; } ]; -} \ No newline at end of file +} + +message IDFilter { + // Only return resources that belong to this id. + string id = 1 [ + (validate.rules).string = {max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + max_length: 200; + example: "\"69629023906488337\""; + } + ]; +} + +message TimestampFilter { + // Filter resources by timestamp. + google.protobuf.Timestamp timestamp = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; + // Defines the condition (e.g., equals, before, after) that the timestamp of the retrieved resources should match. + TimestampFilterMethod method = 2 [ + (validate.rules).enum.defined_only = true + ]; +} + +message InIDsFilter { + // Defines the ids to query for. + repeated string ids = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "[\"69629023906488334\",\"69622366012355662\"]"; + } + ]; +} diff --git a/proto/zitadel/management.proto b/proto/zitadel/management.proto index 34a8384d39..8cd0b22759 100644 --- a/proto/zitadel/management.proto +++ b/proto/zitadel/management.proto @@ -432,7 +432,11 @@ service ManagementService { }; } - // Deprecated: use ImportHumanUser + // Create User (Human) + // + // Deprecated: use [ImportHumanUser](apis/resources/mgmt/management-service-import-human-user.api.mdx) instead. + // + // Create a new user with the type human. The newly created user will get an initialization email if either the email address is not marked as verified or no password is set. If a password is set the user will not be requested to set a new one on the first login. rpc AddHumanUser(AddHumanUserRequest) returns (AddHumanUserResponse) { option (google.api.http) = { post: "/users/human" @@ -444,10 +448,8 @@ service ManagementService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Deprecated: Create User (Human)"; - description: "Create a new user with the type human. The newly created user will get an initialization email if either the email address is not marked as verified or no password is set. If a password is set the user will not be requested to set a new one on the first login.\n\nDeprecated: use ImportHumanUser" - tags: "Users"; deprecated: true; + tags: "Users"; parameters: { headers: { name: "x-zitadel-orgid"; @@ -459,7 +461,11 @@ service ManagementService { }; } - // Deprecated: please use user service v2 AddHumanUser + // Create/Import User (Human) + // + // Deprecated: use [UpdateHumanUser](apis/resources/user_service_v2/user-service-update-human-user.api.mdx) instead. + // + // Create/import a new user with the type human. The newly created user will get an initialization email if either the email address is not marked as verified or no password is set. If a password is set the user will not be requested to set a new one on the first login. rpc ImportHumanUser(ImportHumanUserRequest) returns (ImportHumanUserResponse) { option (google.api.http) = { post: "/users/human/_import" @@ -471,11 +477,9 @@ service ManagementService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Create/Import User (Human)"; - description: "Create/import a new user with the type human. The newly created user will get an initialization email if either the email address is not marked as verified or no password is set. If a password is set the user will not be requested to set a new one on the first login.\n\nDeprecated: please use user service v2 [AddHumanUser](apis/resources/user_service_v2/user-service-add-human-user.api.mdx)" + deprecated: true; tags: "Users"; tags: "User Human" - deprecated: true; parameters: { headers: { name: "x-zitadel-orgid"; @@ -487,6 +491,11 @@ service ManagementService { }; } + // Create User (Machine) + // + // Deprecated: use [user service v2 CreateUser](apis/resources/user_service_v2/user-service-create-user.api.mdx) to create a user of type machine instead. + // + // Create a new user with the type machine for your API, service or device. These users are used for non-interactive authentication flows. rpc AddMachineUser(AddMachineUserRequest) returns (AddMachineUserResponse) { option (google.api.http) = { post: "/users/machine" @@ -498,8 +507,7 @@ service ManagementService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Create User (Machine)"; - description: "Create a new user with the type machine for your API, service or device. These users are used for non-interactive authentication flows." + deprecated: true; tags: "Users"; tags: "User Machine"; responses: { @@ -683,7 +691,11 @@ service ManagementService { }; } - // Deprecated: please use user service v2 UpdateHumanUser + // Change user name + // + // Deprecated: use [user service v2 UpdateUser](apis/resources/user_service_v2/user-service-update-user.api.mdx) instead. + // + // Change the username of the user. Be aware that the user has to log in with the newly added username afterward rpc UpdateUserName(UpdateUserNameRequest) returns (UpdateUserNameResponse) { option (google.api.http) = { put: "/users/{user_id}/username" @@ -695,8 +707,6 @@ service ManagementService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Change user name"; - description: "Change the username of the user. Be aware that the user has to log in with the newly added username afterward.\n\nDeprecated: please use user service v2 UpdateHumanUser" tags: "Users"; deprecated: true; responses: { @@ -903,7 +913,11 @@ service ManagementService { }; } - // Deprecated: please use user service v2 UpdateHumanUser + // Update User Profile (Human) + // + // Deprecated: use [user service v2 UpdateHumanUser](apis/resources/user_service_v2/user-service-update-human-user.api.mdx) instead. + // + // Update the profile information from a user. The profile includes basic information like first_name and last_name. rpc UpdateHumanProfile(UpdateHumanProfileRequest) returns (UpdateHumanProfileResponse) { option (google.api.http) = { put: "/users/{user_id}/profile" @@ -915,11 +929,9 @@ service ManagementService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Update User Profile (Human)"; - description: "Update the profile information from a user. The profile includes basic information like first_name and last_name.\n\nDeprecated: please use user service v2 UpdateHumanUser" + deprecated: true; tags: "Users"; tags: "User Human"; - deprecated: true; responses: { key: "200" value: { @@ -970,7 +982,11 @@ service ManagementService { }; } - // Deprecated: please use user service v2 SetEmail + // Update User Email (Human) + // + // Deprecated: use [user service v2 SetEmail](apis/resources/user_service_v2/user-service-set-email.api.mdx) instead. + // + // Change the email address of a user. If the state is set to not verified, the user will get a verification email. rpc UpdateHumanEmail(UpdateHumanEmailRequest) returns (UpdateHumanEmailResponse) { option (google.api.http) = { put: "/users/{user_id}/email" @@ -982,8 +998,6 @@ service ManagementService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Update User Email (Human)"; - description: "Change the email address of a user. If the state is set to not verified, the user will get a verification email.\n\nDeprecated: please use user service v2 SetEmail" tags: "Users"; tags: "User Human"; deprecated: true; @@ -1039,7 +1053,11 @@ service ManagementService { }; } - // Deprecated: please use user service v2 ResendEmailCode + // Resend User Email Verification + // + // Deprecated: use [user service v2 ResendEmailCode](apis/resources/user_service_v2/user-service-resend-email-code.api.mdx) instead. + // + // Resend the email verification notification to the given email address of the user. rpc ResendHumanEmailVerification(ResendHumanEmailVerificationRequest) returns (ResendHumanEmailVerificationResponse) { option (google.api.http) = { post: "/users/{user_id}/email/_resend_verification" @@ -1051,8 +1069,6 @@ service ManagementService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Resend User Email Verification"; - description: "Resend the email verification notification to the given email address of the user.\n\nDeprecated: please use user service v2 ResendEmailCode" tags: "Users"; tags: "User Human"; deprecated: true; @@ -1106,7 +1122,11 @@ service ManagementService { }; } - // Deprecated: please use user service v2 SetPhone + // Update User Phone (Human) + // + // Deprecated: use [user service v2 SetPhone](apis/resources/user_service_v2/user-service-update-user.api.mdx) instead. + // + // Change the phone number of a user. If the state is set to not verified, the user will get an SMS to verify (if a notification provider is configured). The phone number is only for informational purposes and to send messages, not for Authentication (2FA). rpc UpdateHumanPhone(UpdateHumanPhoneRequest) returns (UpdateHumanPhoneResponse) { option (google.api.http) = { put: "/users/{user_id}/phone" @@ -1118,8 +1138,6 @@ service ManagementService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Update User Phone (Human)"; - description: "Change the phone number of a user. If the state is set to not verified, the user will get an SMS to verify (if a notification provider is configured). The phone number is only for informational purposes and to send messages, not for Authentication (2FA).\n\nDeprecated: please use user service v2 SetPhone" tags: "Users"; tags: "User Human"; deprecated: true; @@ -1140,7 +1158,11 @@ service ManagementService { }; } - // Deprecated: please use user service v2 SetPhone + // Remove User Phone (Human) + // + // Deprecated: use user service v2 [user service v2 SetPhone](apis/resources/user_service_v2/user-service-set-phone.api.mdx) instead. + // + // Remove the configured phone number of a user. rpc RemoveHumanPhone(RemoveHumanPhoneRequest) returns (RemoveHumanPhoneResponse) { option (google.api.http) = { delete: "/users/{user_id}/phone" @@ -1151,8 +1173,6 @@ service ManagementService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Remove User Phone (Human)"; - description: "Remove the configured phone number of a user.\n\nDeprecated: please use user service v2 SetPhone" tags: "Users"; tags: "User Human"; deprecated: true; @@ -1173,7 +1193,11 @@ service ManagementService { }; } - // Deprecated: please use user service v2 ResendPhoneCode + // Resend User Phone Verification + // + // Deprecated: use user service v2 [user service v2 ResendPhoneCode](apis/resources/user_service_v2/user-service-resend-phone-code.api.mdx) instead. + // + // Resend the notification for the verification of the phone number, to the number stored on the user. rpc ResendHumanPhoneVerification(ResendHumanPhoneVerificationRequest) returns (ResendHumanPhoneVerificationResponse) { option (google.api.http) = { post: "/users/{user_id}/phone/_resend_verification" @@ -1185,8 +1209,6 @@ service ManagementService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Resend User Phone Verification"; - description: "Resend the notification for the verification of the phone number, to the number stored on the user.\n\nDeprecated: please use user service v2 ResendPhoneCode" tags: "Users"; tags: "User Human"; deprecated: true; @@ -1238,7 +1260,9 @@ service ManagementService { }; } - // Deprecated: please use user service v2 SetPassword + // Set Human Initial Password + // + // Deprecated: use [user service v2 SetPassword](apis/resources/user_service_v2/user-service-set-password.api.mdx) instead. rpc SetHumanInitialPassword(SetHumanInitialPasswordRequest) returns (SetHumanInitialPasswordResponse) { option (google.api.http) = { post: "/users/{user_id}/password/_initialize" @@ -1252,7 +1276,6 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Users"; tags: "User Human"; - summary: "Set Human Initial Password\n\nDeprecated: please use user service v2 SetPassword"; deprecated: true; parameters: { headers: { @@ -1265,7 +1288,9 @@ service ManagementService { }; } - // Deprecated: please use user service v2 SetPassword + // Set User Password + // + // Deprecated: use [user service v2 SetPassword](apis/resources/user_service_v2/user-service-set-password.api.mdx) instead. rpc SetHumanPassword(SetHumanPasswordRequest) returns (SetHumanPasswordResponse) { option (google.api.http) = { post: "/users/{user_id}/password" @@ -1277,8 +1302,6 @@ service ManagementService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Set User Password"; - description: "Set a new password for a user. Per default, the user has to change the password on the next login. You can set no_change_required to true, to avoid the change on the next login.\n\nDeprecated: please use user service v2 SetPassword" tags: "Users"; tags: "User Human"; deprecated: true; @@ -1299,7 +1322,11 @@ service ManagementService { }; } - // Deprecated: please use user service v2 PasswordReset + // Send Reset Password Notification + // + // Deprecated: use [user service v2 PasswordReset](apis/resources/user_service_v2/user-service-password-reset.api.mdx) instead. + // + // The user will receive an email with a link to change the password. rpc SendHumanResetPasswordNotification(SendHumanResetPasswordNotificationRequest) returns (SendHumanResetPasswordNotificationResponse) { option (google.api.http) = { post: "/users/{user_id}/password/_reset" @@ -1311,8 +1338,6 @@ service ManagementService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Send Reset Password Notification"; - description: "The user will receive an email with a link to change the password.\n\nDeprecated: please use user service v2 PasswordReset" tags: "Users"; tags: "User Human"; deprecated: true; @@ -1629,6 +1654,11 @@ service ManagementService { }; } + // Update Machine User + // + // Deprecated: use [user service v2 UpdateUser](apis/resources/user_service_v2/user-service-update-user.api.mdx) to update a user of type machine instead. + // + // Change a service account/machine user. It is used for accounts with non-interactive authentication possibilities. rpc UpdateMachine(UpdateMachineRequest) returns (UpdateMachineResponse) { option (google.api.http) = { put: "/users/{user_id}/machine" @@ -1640,8 +1670,7 @@ service ManagementService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Update Machine User"; - description: "Change a service account/machine user. It is used for accounts with non-interactive authentication possibilities." + deprecated: true; tags: "Users"; tags: "User Machine"; responses: { @@ -1661,6 +1690,11 @@ service ManagementService { }; } + // Create Secret for Machine User + // + // Deprecated: use [user service v2 AddSecret](apis/resources/user_service_v2/user-service-add-secret.api.mdx) instead. + // + // Create a new secret for a machine user/service account. It is used to authenticate the user (client credential grant). rpc GenerateMachineSecret(GenerateMachineSecretRequest) returns (GenerateMachineSecretResponse) { option (google.api.http) = { put: "/users/{user_id}/secret" @@ -1672,8 +1706,7 @@ service ManagementService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Create Secret for Machine User"; - description: "Create a new secret for a machine user/service account. It is used to authenticate the user (client credential grant)." + deprecated: true; tags: "Users"; tags: "User Machine"; responses: { @@ -1693,6 +1726,11 @@ service ManagementService { }; } + // Delete Secret of Machine User + // + // Deprecated: use [user service v2 RemoveSecret](apis/resources/user_service_v2/user-service-remove-secret.api.mdx) instead. + // + // Delete a secret of a machine user/service account. The user will not be able to authenticate with the secret afterward. rpc RemoveMachineSecret(RemoveMachineSecretRequest) returns (RemoveMachineSecretResponse) { option (google.api.http) = { delete: "/users/{user_id}/secret" @@ -1703,8 +1741,7 @@ service ManagementService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Delete Secret of Machine User"; - description: "Delete a secret of a machine user/service account. The user will not be able to authenticate with the secret afterward." + deprecated: true; tags: "Users"; tags: "User Machine"; responses: { @@ -1724,6 +1761,11 @@ service ManagementService { }; } + // Get Machine user Key By ID + // + // Deprecated: use [user service v2 ListKeys](apis/resources/user_service_v2/user-service-list-keys.api.mdx) instead. + // + // Get a specific Key of a machine user by its id. Machine keys are used to authenticate with jwt profile authentication. rpc GetMachineKeyByIDs(GetMachineKeyByIDsRequest) returns (GetMachineKeyByIDsResponse) { option (google.api.http) = { get: "/users/{user_id}/keys/{key_id}" @@ -1734,8 +1776,7 @@ service ManagementService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Get Machine user Key By ID"; - description: "Get a specific Key of a machine user by its id. Machine keys are used to authenticate with jwt profile authentication." + deprecated: true; tags: "Users"; tags: "User Machine"; responses: { @@ -1755,6 +1796,11 @@ service ManagementService { }; } + // Get Machine user Key By ID + // + // Deprecated: use [user service v2 ListKeys](apis/resources/user_service_v2/user-service-list-keys.api.mdx) instead. + // + // Get the list of keys of a machine user. Machine keys are used to authenticate with jwt profile authentication. rpc ListMachineKeys(ListMachineKeysRequest) returns (ListMachineKeysResponse) { option (google.api.http) = { post: "/users/{user_id}/keys/_search" @@ -1766,8 +1812,7 @@ service ManagementService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Get Machine user Key By ID"; - description: "Get the list of keys of a machine user. Machine keys are used to authenticate with jwt profile authentication." + deprecated: true; tags: "Users"; tags: "User Machine"; responses: { @@ -1787,6 +1832,14 @@ service ManagementService { }; } + // Create Key for machine user + // + // Deprecated: use [user service v2 AddKey](apis/resources/user_service_v2/user-service-add-key.api.mdx) instead. + // + // If a public key is not supplied, a new key is generated and will be returned in the response. + // Make sure to store the returned key. + // If an RSA public key is supplied, the private key is omitted from the response. + // Machine keys are used to authenticate with jwt profile. rpc AddMachineKey(AddMachineKeyRequest) returns (AddMachineKeyResponse) { option (google.api.http) = { post: "/users/{user_id}/keys" @@ -1798,8 +1851,7 @@ service ManagementService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Create Key for machine user"; - description: "If a public key is not supplied, a new key is generated and will be returned in the response. Make sure to store the returned key. If an RSA public key is supplied, the private key is omitted from the response. Machine keys are used to authenticate with jwt profile." + deprecated: true; tags: "Users"; tags: "User Machine"; responses: { @@ -1819,6 +1871,12 @@ service ManagementService { }; } + // Delete Key for machine user + // + // Deprecated: use [user service v2 RemoveKey](apis/resources/user_service_v2/user-service-remove-key.api.mdx) instead. + // + // Delete a specific key from a user. + // The user will not be able to authenticate with that key afterward. rpc RemoveMachineKey(RemoveMachineKeyRequest) returns (RemoveMachineKeyResponse) { option (google.api.http) = { delete: "/users/{user_id}/keys/{key_id}" @@ -1829,8 +1887,7 @@ service ManagementService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Delete Key for machine user"; - description: "Delete a specific key from a user. The user will not be able to authenticate with that key afterward." + deprecated: true; tags: "Users"; tags: "User Machine"; responses: { @@ -1850,6 +1907,11 @@ service ManagementService { }; } + // Get Personal-Access-Token (PAT) by ID + // + // Deprecated: use [user service v2 ListPersonalAccessTokens](apis/resources/user_service_v2/user-service-list-personal-access-tokens.api.mdx) instead. + // + // Returns the PAT for a user, currently only available for machine users/service accounts. PATs are ready-to-use tokens and can be sent directly in the authentication header. rpc GetPersonalAccessTokenByIDs(GetPersonalAccessTokenByIDsRequest) returns (GetPersonalAccessTokenByIDsResponse) { option (google.api.http) = { get: "/users/{user_id}/pats/{token_id}" @@ -1860,8 +1922,7 @@ service ManagementService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Get a Personal-Access-Token (PAT) by ID"; - description: "Returns the PAT for a user, currently only available for machine users/service accounts. PATs are ready-to-use tokens and can be sent directly in the authentication header." + deprecated: true; tags: "Users"; tags: "User Machine"; responses: { @@ -1881,6 +1942,11 @@ service ManagementService { }; } + // List Personal-Access-Tokens (PATs) + // + // Deprecated: use [user service v2 ListPersonalAccessTokens](apis/resources/user_service_v2/user-service-list-personal-access-tokens.api.mdx) instead. + // + // Returns a list of PATs for a user, currently only available for machine users/service accounts. PATs are ready-to-use tokens and can be sent directly in the authentication header. rpc ListPersonalAccessTokens(ListPersonalAccessTokensRequest) returns (ListPersonalAccessTokensResponse) { option (google.api.http) = { post: "/users/{user_id}/pats/_search" @@ -1892,8 +1958,7 @@ service ManagementService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Get a Personal-Access-Token (PAT) by ID"; - description: "Returns a list of PATs for a user, currently only available for machine users/service accounts. PATs are ready-to-use tokens and can be sent directly in the authentication header." + deprecated: true; tags: "Users"; tags: "User Machine"; responses: { @@ -1913,6 +1978,13 @@ service ManagementService { }; } + // Create a Personal-Access-Token (PAT) + // + // Deprecated: use [user service v2 AddPersonalAccessToken](apis/resources/user_service_v2/user-service-add-personal-access-token.api.mdx) instead. + // + // Generates a new PAT for the user. Currently only available for machine users. + // The token will be returned in the response, make sure to store it. + // PATs are ready-to-use tokens and can be sent directly in the authentication header. rpc AddPersonalAccessToken(AddPersonalAccessTokenRequest) returns (AddPersonalAccessTokenResponse) { option (google.api.http) = { post: "/users/{user_id}/pats" @@ -1924,8 +1996,7 @@ service ManagementService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Create a Personal-Access-Token (PAT)"; - description: "Generates a new PAT for the user. Currently only available for machine users. The token will be returned in the response, make sure to store it. PATs are ready-to-use tokens and can be sent directly in the authentication header." + deprecated: true; tags: "Users"; tags: "User Machine"; responses: { @@ -1945,6 +2016,11 @@ service ManagementService { }; } + // Remove a Personal-Access-Token (PAT) by ID + // + // Deprecated: use [user service v2 RemovePersonalAccessToken](apis/resources/user_service_v2/user-service-remove-personal-access-token.api.mdx) instead. + // + // Delete a PAT from a user. Afterward, the user will not be able to authenticate with that token anymore. rpc RemovePersonalAccessToken(RemovePersonalAccessTokenRequest) returns (RemovePersonalAccessTokenResponse) { option (google.api.http) = { delete: "/users/{user_id}/pats/{token_id}" @@ -1955,8 +2031,7 @@ service ManagementService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Get a Personal-Access-Token (PAT) by ID"; - description: "Delete a PAT from a user. Afterward, the user will not be able to authenticate with that token anymore." + deprecated: true; tags: "Users"; tags: "User Machine"; responses: { @@ -2003,7 +2078,7 @@ service ManagementService { }; } - // Deprecated: please use user service v2 RemoveLinkedIDP + // Deprecated: please use [user service v2 RemoveIDPLink](apis/resources/user_service_v2/user-service-remove-idp-link.api.mdx) rpc RemoveHumanLinkedIDP(RemoveHumanLinkedIDPRequest) returns (RemoveHumanLinkedIDPResponse) { option (google.api.http) = { delete: "/users/{user_id}/idps/{idp_id}/{linked_user_id}" diff --git a/proto/zitadel/project/v2beta/query.proto b/proto/zitadel/project/v2beta/query.proto index f328b65189..9bfde662a3 100644 --- a/proto/zitadel/project/v2beta/query.proto +++ b/proto/zitadel/project/v2beta/query.proto @@ -185,10 +185,10 @@ message ProjectSearchFilter { option (validate.required) = true; ProjectNameFilter project_name_filter = 1; - InProjectIDsFilter in_project_ids_filter = 2; - ProjectResourceOwnerFilter project_resource_owner_filter = 3; - ProjectGrantResourceOwnerFilter project_grant_resource_owner_filter = 4; - ProjectOrganizationIDFilter project_organization_id_filter = 5; + zitadel.filter.v2beta.InIDsFilter in_project_ids_filter = 2; + zitadel.filter.v2beta.IDFilter project_resource_owner_filter = 3; + zitadel.filter.v2beta.IDFilter project_grant_resource_owner_filter = 4; + zitadel.filter.v2beta.IDFilter project_organization_id_filter = 5; } } @@ -210,68 +210,18 @@ message ProjectNameFilter { ]; } -message InProjectIDsFilter { - // Defines the ids to query for. - repeated string project_ids = 1 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - description: "the ids of the projects to include" - example: "[\"69629023906488334\",\"69622366012355662\"]"; - } - ]; -} - -message ProjectResourceOwnerFilter { - // Defines the ID of organization the project belongs to query for. - string project_resource_owner = 1 [ - (validate.rules).string = {max_len: 200}, - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"69629023906488334\"" - } - ]; -} - -message ProjectGrantResourceOwnerFilter { - // Defines the ID of organization the project grant belongs to query for. - string project_grant_resource_owner = 1 [ - (validate.rules).string = {max_len: 200}, - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"69629023906488334\"" - } - ]; -} - -message ProjectOrganizationIDFilter { - // Defines the ID of organization the project and granted project belong to query for. - string project_organization_id = 1 [ - (validate.rules).string = {max_len: 200}, - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"69629023906488334\"" - } - ]; -} - message ProjectGrantSearchFilter { oneof filter { option (validate.required) = true; ProjectNameFilter project_name_filter = 1; ProjectRoleKeyFilter role_key_filter = 2; - InProjectIDsFilter in_project_ids_filter = 3; - ProjectResourceOwnerFilter project_resource_owner_filter = 4; - ProjectGrantResourceOwnerFilter project_grant_resource_owner_filter = 5; + zitadel.filter.v2beta.InIDsFilter in_project_ids_filter = 3; + zitadel.filter.v2beta.IDFilter project_resource_owner_filter = 4; + zitadel.filter.v2beta.IDFilter project_grant_resource_owner_filter = 5; } } -message GrantedOrganizationIDFilter { - // Defines the ID of organization the project is granted to query for. - string granted_organization_id = 1 [ - (validate.rules).string = {max_len: 200}, - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"69629023906488334\"" - } - ]; -} - message ProjectRole { // ID of the project. string project_id = 1 [ @@ -344,4 +294,4 @@ message ProjectRoleDisplayNameFilter { zitadel.filter.v2beta.TextFilterMethod method = 2 [ (validate.rules).enum.defined_only = true ]; -} \ No newline at end of file +} diff --git a/proto/zitadel/user/v2/email.proto b/proto/zitadel/user/v2/email.proto index e962707fcf..eb807206a7 100644 --- a/proto/zitadel/user/v2/email.proto +++ b/proto/zitadel/user/v2/email.proto @@ -19,7 +19,7 @@ message SetHumanEmail { example: "\"mini@mouse.com\""; } ]; - // if no verification is specified, an email is sent with the default url + // If no verification is specified, an email is sent with the default url oneof verification { SendEmailVerificationCode send_code = 2; ReturnEmailVerificationCode return_code = 3; diff --git a/proto/zitadel/user/v2/key.proto b/proto/zitadel/user/v2/key.proto new file mode 100644 index 0000000000..ffa83c714e --- /dev/null +++ b/proto/zitadel/user/v2/key.proto @@ -0,0 +1,69 @@ +syntax = "proto3"; + +package zitadel.user.v2; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/user/v2;user"; + +import "protoc-gen-openapiv2/options/annotations.proto"; +import "google/protobuf/timestamp.proto"; +import "validate/validate.proto"; +import "zitadel/filter/v2/filter.proto"; + +message Key { + // The timestamp of the key creation. + google.protobuf.Timestamp creation_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; + // The timestamp of the last change of the key. + google.protobuf.Timestamp change_date = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; + // The unique identifier of the key. + string id = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629012906488334\""; + } + ]; + // The unique identifier of the user the key belongs to. + string user_id = 4 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629012906488334\""; + } + ]; + // The unique identifier of the organization the key belongs to. + string organization_id = 5 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629012906488334\""; + } + ]; + // The keys expiration date. + google.protobuf.Timestamp expiration_date = 6 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; +} + +message KeysSearchFilter { + oneof filter { + option (validate.required) = true; + zitadel.filter.v2.IDFilter key_id_filter = 1; + zitadel.filter.v2.IDFilter user_id_filter = 2; + zitadel.filter.v2.IDFilter organization_id_filter = 3; + zitadel.filter.v2.TimestampFilter created_date_filter = 4; + zitadel.filter.v2.TimestampFilter expiration_date_filter = 5; + } +} + +enum KeyFieldName { + KEY_FIELD_NAME_UNSPECIFIED = 0; + KEY_FIELD_NAME_CREATED_DATE = 1; + KEY_FIELD_NAME_ID = 2; + KEY_FIELD_NAME_USER_ID = 3; + KEY_FIELD_NAME_ORGANIZATION_ID = 4; + KEY_FIELD_NAME_KEY_EXPIRATION_DATE = 5; +} diff --git a/proto/zitadel/user/v2/pat.proto b/proto/zitadel/user/v2/pat.proto new file mode 100644 index 0000000000..1d24c4c496 --- /dev/null +++ b/proto/zitadel/user/v2/pat.proto @@ -0,0 +1,70 @@ +syntax = "proto3"; + +package zitadel.user.v2; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/user/v2;user"; + +import "protoc-gen-openapiv2/options/annotations.proto"; +import "google/protobuf/timestamp.proto"; +import "validate/validate.proto"; +import "zitadel/filter/v2/filter.proto"; + +message PersonalAccessToken { + // The timestamp of the personal access token creation. + google.protobuf.Timestamp creation_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; + // The timestamp of the last change of the personal access token. + google.protobuf.Timestamp change_date = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; + // The unique identifier of the personal access token. + string id = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629012906488334\""; + } + ]; + // The unique identifier of the user the personal access token belongs to. + string user_id = 4 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629012906488334\""; + } + ]; + // The unique identifier of the organization the personal access token belongs to. + string organization_id = 5 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629012906488334\""; + } + ]; + // The personal access tokens expiration date. + google.protobuf.Timestamp expiration_date = 6 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; +} + +message PersonalAccessTokensSearchFilter { + oneof filter { + option (validate.required) = true; + zitadel.filter.v2.IDFilter token_id_filter = 1; + zitadel.filter.v2.IDFilter user_id_filter = 2; + zitadel.filter.v2.IDFilter organization_id_filter = 3; + zitadel.filter.v2.TimestampFilter created_date_filter = 4; + zitadel.filter.v2.TimestampFilter expiration_date_filter = 5; + } +} + +enum PersonalAccessTokenFieldName { + PERSONAL_ACCESS_TOKEN_FIELD_NAME_UNSPECIFIED = 0; + PERSONAL_ACCESS_TOKEN_FIELD_NAME_CREATED_DATE = 1; + PERSONAL_ACCESS_TOKEN_FIELD_NAME_ID = 2; + PERSONAL_ACCESS_TOKEN_FIELD_NAME_USER_ID = 3; + PERSONAL_ACCESS_TOKEN_FIELD_NAME_ORGANIZATION_ID = 4; + PERSONAL_ACCESS_TOKEN_FIELD_NAME_EXPIRATION_DATE = 5; +} + diff --git a/proto/zitadel/user/v2/user_service.proto b/proto/zitadel/user/v2/user_service.proto index 44d25c07b3..3fc81836d6 100644 --- a/proto/zitadel/user/v2/user_service.proto +++ b/proto/zitadel/user/v2/user_service.proto @@ -2,6 +2,14 @@ syntax = "proto3"; package zitadel.user.v2; +import "google/protobuf/timestamp.proto"; +import "google/api/annotations.proto"; +import "google/api/field_behavior.proto"; +import "google/protobuf/duration.proto"; +import "google/protobuf/struct.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; +import "validate/validate.proto"; + import "zitadel/object/v2/object.proto"; import "zitadel/protoc_gen_zitadel/v2/options.proto"; import "zitadel/user/v2/auth.proto"; @@ -10,13 +18,10 @@ import "zitadel/user/v2/phone.proto"; import "zitadel/user/v2/idp.proto"; import "zitadel/user/v2/password.proto"; import "zitadel/user/v2/user.proto"; +import "zitadel/user/v2/key.proto"; +import "zitadel/user/v2/pat.proto"; import "zitadel/user/v2/query.proto"; -import "google/api/annotations.proto"; -import "google/api/field_behavior.proto"; -import "google/protobuf/duration.proto"; -import "google/protobuf/struct.proto"; -import "protoc-gen-openapiv2/options/annotations.proto"; -import "validate/validate.proto"; +import "zitadel/filter/v2/filter.proto"; option go_package = "github.com/zitadel/zitadel/pkg/grpc/user/v2;user"; @@ -85,9 +90,9 @@ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { } } responses: { - key: "403"; + key: "400"; value: { - description: "Returned when the user does not have permission to access the resource."; + description: "The request is malformed."; schema: { json_schema: { ref: "#/definitions/rpcStatus"; @@ -96,9 +101,20 @@ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { } } responses: { - key: "404"; + key: "401"; value: { - description: "Returned when the resource does not exist."; + description: "Returned when the user is not authenticated."; + schema: { + json_schema: { + ref: "#/definitions/rpcStatus"; + } + } + } + } + responses: { + key: "403"; + value: { + description: "Returned when the user does not have permission to access the resource."; schema: { json_schema: { ref: "#/definitions/rpcStatus"; @@ -110,8 +126,51 @@ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { service UserService { + // Create a User + // + // Create a new human or machine user in the specified organization. + // + // Required permission: + // - user.write + rpc CreateUser (CreateUserRequest) returns (CreateUserResponse) { + option (google.api.http) = { + // The /new path segment does not follow Zitadels API design. + // The only reason why it is used here is to avoid a conflict with the ListUsers endpoint, which already handles POST /v2/users. + post: "/v2/users/new" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200" + value: { + description: "OK"; + } + }; + responses: { + key: "409" + value: { + description: "The user already exists."; + schema: { + json_schema: { + ref: "#/definitions/rpcStatus"; + }; + }; + } + }; + }; + } + // Create a new human user // + // Deprecated: Use [CreateUser](apis/resources/user_service_v2/user-service-create-user.api.mdx) to create a new user of type human instead. + // // Create/import a new user with the type human. The newly created user will get a verification email if either the email address is not marked as verified and you did not request the verification to be returned. rpc AddHumanUser (AddHumanUserRequest) returns (AddHumanUserResponse) { option (google.api.http) = { @@ -125,11 +184,12 @@ service UserService { org_field: "organization" } http_response: { - success_code: 201 + success_code: 200 } }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + deprecated: true; responses: { key: "200" value: { @@ -163,6 +223,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -204,6 +270,8 @@ service UserService { // Change the user email // + // Deprecated: [Update the users email field](apis/resources/user_service_v2/user-service-update-user.api.mdx). + // // Change the email address of a user. If the state is set to not verified, a verification code will be generated, which can be either returned or sent to the user by email.. rpc SetEmail (SetEmailRequest) returns (SetEmailResponse) { option (google.api.http) = { @@ -218,18 +286,23 @@ service UserService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + deprecated: true; responses: { key: "200" value: { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } // Resend code to verify user email - // - // Resend code to verify user email. rpc ResendEmailCode (ResendEmailCodeRequest) returns (ResendEmailCodeResponse) { option (google.api.http) = { post: "/v2/users/{user_id}/email/resend" @@ -249,12 +322,16 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } // Send code to verify user email - // - // Send code to verify user email. rpc SendEmailCode (SendEmailCodeRequest) returns (SendEmailCodeResponse) { option (google.api.http) = { post: "/v2/users/{user_id}/email/send" @@ -274,6 +351,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -299,11 +382,19 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } // Set the user phone // + // Deprecated: [Update the users phone field](apis/resources/user_service_v2/user-service-update-user.api.mdx). + // // Set the phone number of a user. If the state is set to not verified, a verification code will be generated, which can be either returned or sent to the user by sms.. rpc SetPhone(SetPhoneRequest) returns (SetPhoneResponse) { option (google.api.http) = { @@ -318,18 +409,27 @@ service UserService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + deprecated: true; responses: { key: "200" value: { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } - // Remove the user phone + // Delete the user phone // - // Remove the user phone + // Deprecated: [Update the users phone field](apis/resources/user_service_v2/user-service-update-user.api.mdx) to remove the phone number. + // + // Delete the phone number of a user. rpc RemovePhone(RemovePhoneRequest) returns (RemovePhoneResponse) { option (google.api.http) = { delete: "/v2/users/{user_id}/phone" @@ -343,20 +443,23 @@ service UserService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Delete the user phone"; - description: "Delete the phone number of a user." + deprecated: true; responses: { key: "200" value: { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } // Resend code to verify user phone - // - // Resend code to verify user phone. rpc ResendPhoneCode (ResendPhoneCodeRequest) returns (ResendPhoneCodeResponse) { option (google.api.http) = { post: "/v2/users/{user_id}/phone/resend" @@ -376,6 +479,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -401,15 +510,27 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } - // Update User + + // Update a User // - // Update all information from a user.. - rpc UpdateHumanUser(UpdateHumanUserRequest) returns (UpdateHumanUserResponse) { + // Partially update an existing user. + // If you change the users email or phone, you can specify how the ownership should be verified. + // If you change the users password, you can specify if the password should be changed again on the users next login. + // + // Required permission: + // - user.write + rpc UpdateUser(UpdateUserRequest) returns (UpdateUserResponse) { option (google.api.http) = { - put: "/v2/users/human/{user_id}" + patch: "/v2/users/{user_id}" body: "*" }; @@ -426,6 +547,62 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + schema: { + json_schema: { + ref: "#/definitions/rpcStatus"; + }; + }; + } + } + responses: { + key: "409" + value: { + description: "The user already exists."; + schema: { + json_schema: { + ref: "#/definitions/rpcStatus"; + }; + }; + } + }; + }; + } + + // Update Human User + // + // Deprecated: Use [UpdateUser](apis/resources/user_service_v2/user-service-update-user.api.mdx) to update a user of type human instead. + // + // Update all information from a user.. + rpc UpdateHumanUser(UpdateHumanUserRequest) returns (UpdateHumanUserResponse) { + option (google.api.http) = { + put: "/v2/users/human/{user_id}" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + deprecated: true; + responses: { + key: "200" + value: { + description: "OK"; + } + }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -451,6 +628,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -476,6 +659,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -501,6 +690,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -526,6 +721,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -550,6 +751,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -574,6 +781,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -598,6 +811,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -622,6 +841,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -670,6 +895,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -694,6 +925,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -718,6 +955,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -743,6 +986,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -767,6 +1016,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -791,6 +1046,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -814,6 +1075,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -838,6 +1105,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -861,6 +1134,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -885,6 +1164,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -908,6 +1193,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -958,6 +1249,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "Intent ID does not exist."; + } + } }; } @@ -983,6 +1280,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -1033,6 +1336,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -1058,11 +1367,19 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } // Change password // + // Deprecated: [Update the users password](apis/resources/user_service_v2/user-service-update-user.api.mdx) instead. + // // Change the password of a user with either a verification code or the current password.. rpc SetPassword (SetPasswordRequest) returns (SetPasswordResponse) { option (google.api.http) = { @@ -1077,12 +1394,19 @@ service UserService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + deprecated: true; responses: { key: "200" value: { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -1155,6 +1479,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -1183,6 +1513,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -1209,6 +1545,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -1234,9 +1576,289 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } + // Add a Users Secret + // + // Generates a client secret for the user. + // The client id is the users username. + // If the user already has a secret, it is overwritten. + // Only users of type machine can have a secret. + // + // Required permission: + // - user.write + rpc AddSecret(AddSecretRequest) returns (AddSecretResponse) { + option (google.api.http) = { + post: "/v2/users/{user_id}/secret" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200" + value: { + description: "The secret was successfully generated."; + } + }; + responses: { + key: "404" + value: { + description: "The user ID does not exist."; + schema: { + json_schema: { + ref: "#/definitions/rpcStatus"; + }; + }; + } + }; + }; + } + + // Remove a Users Secret + // + // Remove the current client ID and client secret from a machine user. + // + // Required permission: + // - user.write + rpc RemoveSecret(RemoveSecretRequest) returns (RemoveSecretResponse) { + option (google.api.http) = { + delete: "/v2/users/{user_id}/secret" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200" + value: { + description: "The secret was either successfully removed or it didn't exist in the first place."; + } + }; + }; + } + + // Add a Key + // + // Add a keys that can be used to securely authenticate at the Zitadel APIs using JWT profile authentication using short-lived tokens. + // Make sure you store the returned key safely, as you won't be able to read it from the Zitadel API anymore. + // Only users of type machine can have keys. + // + // Required permission: + // - user.write + rpc AddKey(AddKeyRequest) returns (AddKeyResponse) { + option (google.api.http) = { + post: "/v2/users/{user_id}/keys" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200" + value: { + description: "The key was successfully created."; + } + }; + responses: { + key: "404" + value: { + description: "The user ID does not exist."; + schema: { + json_schema: { + ref: "#/definitions/rpcStatus"; + }; + }; + } + }; + }; + } + + // Remove a Key + // + // Remove a machine users key by the given key ID and an optionally given user ID. + // + // Required permission: + // - user.write + rpc RemoveKey(RemoveKeyRequest) returns (RemoveKeyResponse) { + option (google.api.http) = { + delete: "/v2/users/{user_id}/keys/{key_id}" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200" + value: { + description: "The key was either successfully removed or it not found in the first place."; + } + }; + }; + } + + // Search Keys + // + // List all matching keys. By default all keys of the instance on which the caller has permission to read the owning users are returned. + // Make sure to include a limit and sorting for pagination. + // + // Required permission: + // - user.read + rpc ListKeys(ListKeysRequest) returns (ListKeysResponse) { + option (google.api.http) = { + post: "/v2/users/keys/search" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "A list of all machine user keys matching the query"; + }; + }; + responses: { + key: "400"; + value: { + description: "invalid list query"; + }; + }; + }; + } + + // Add a Personal Access Token + // + // Personal access tokens (PAT) are the easiest way to authenticate to the Zitadel APIs. + // Make sure you store the returned PAT safely, as you won't be able to read it from the Zitadel API anymore. + // Only users of type machine can have personal access tokens. + // + // Required permission: + // - user.write + rpc AddPersonalAccessToken(AddPersonalAccessTokenRequest) returns (AddPersonalAccessTokenResponse) { + option (google.api.http) = { + post: "/v2/users/{user_id}/pats" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200" + value: { + description: "The personal access token was successfully created."; + } + }; + responses: { + key: "404" + value: { + description: "The user ID does not exist."; + schema: { + json_schema: { + ref: "#/definitions/rpcStatus"; + }; + }; + } + }; + }; + } + + // Remove a Personal Access Token + // + // Removes a machine users personal access token by the given token ID and an optionally given user ID. + // + // Required permission: + // - user.write + rpc RemovePersonalAccessToken(RemovePersonalAccessTokenRequest) returns (RemovePersonalAccessTokenResponse) { + option (google.api.http) = { + delete: "/v2/users/{user_id}/pats/{token_id}" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200" + value: { + description: "The personal access token was either successfully removed or it was not found in the first place."; + } + }; + }; + } + + // Search Personal Access Tokens + // + // List all personal access tokens. By default all personal access tokens of the instance on which the caller has permission to read the owning users are returned. + // Make sure to include a limit and sorting for pagination. + // + // Required permission: + // - user.read + rpc ListPersonalAccessTokens(ListPersonalAccessTokensRequest) returns (ListPersonalAccessTokensResponse) { + option (google.api.http) = { + post: "/v2/users/pats/search" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "A list of all personal access tokens matching the query"; + }; + }; + responses: { + key: "400"; + value: { + description: "invalid list query"; + }; + }; + }; + } } message AddHumanUserRequest{ @@ -1296,6 +1918,149 @@ message AddHumanUserResponse { optional string phone_code = 4; } + +message CreateUserRequest{ + message Human { + // Set the users profile information. + SetHumanProfile profile = 1 [ + (validate.rules).message.required = true, + (google.api.field_behavior) = REQUIRED + ]; + // Set the users email address and optionally send a verification email. + SetHumanEmail email = 2 [ + (validate.rules).message.required = true, + (google.api.field_behavior) = REQUIRED + ]; + // Set the users phone number and optionally send a verification SMS. + optional SetHumanPhone phone = 3; + // Set the users initial password and optionally require the user to set a new password. + oneof password_type { + Password password = 4; + HashedPassword hashed_password = 5; + } + // Create the user with a list of links to identity providers. + // This can be useful in migration-scenarios. + // For example, if a user already has an account in an external identity provider or another Zitadel instance, an IDP link allows the user to authenticate as usual. + // Sessions, second factors, hardware keys registered externally are still available for authentication. + // Use the following endpoints to manage identity provider links: + // - [AddIDPLink](apis/resources/user_service_v2/user-service-add-idp-link.api.mdx) + // - [RemoveIDPLink](apis/resources/user_service_v2/user-service-remove-idp-link.api.mdx) + repeated IDPLink idp_links = 7; + // An Implementation of RFC 6238 is used, with HMAC-SHA-1 and time-step of 30 seconds. + // Currently no other options are supported, and if anything different is used the validation will fail. + optional string totp_secret = 8 [ + (validate.rules).string = {min_len: 1 max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1, + max_length: 200, + example: "\"TJOPWSDYILLHXFV4MLKNNJOWFG7VSDCK\""; + } + ]; + } + message Machine { + // The machine users name is a human readable field that helps identifying the user. + string name = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1, + max_length: 200, + example: "\"Acceptance Test User\""; + } + ]; + // The description is a field that helps to remember the purpose of the user. + optional string description = 2 [ + (validate.rules).string = {min_len: 1, max_len: 500}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1, + max_length: 500, + example: "\"The user calls the session API in the continuous integration pipeline for acceptance tests.\""; + } + ]; + } + // The unique identifier of the organization the user belongs to. + string organization_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1, + max_length: 200, + example: "\"69629026806489455\""; + } + ]; + // The ID is a unique identifier for the user in the instance. + // If not specified, it will be generated. + // You can set your own user id that is unique within the instance. + // This is useful in migration scenarios, for example if the user already has an ID in another Zitadel system. + // If not specified, it will be generated. + // It can't be changed after creation. + optional string user_id = 2 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1, + max_length: 200, + example: "\"163840776835432345\""; + } + ]; + + // The username is a unique identifier for the user in the organization. + // If not specified, Zitadel sets the username to the email for users of type human and to the user_id for users of type machine. + // It is used to identify the user in the organization and can be used for login. + optional string username = 3 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1, + max_length: 200, + example: "\"minnie-mouse\""; + } + ]; + + // The type of the user. + oneof user_type { + option (validate.required) = true; + // Users of type human are users that are meant to be used by a person. + // They can log in interactively using a login UI. + // By default, new users will receive a verification email and, if a phone is configured, a verification SMS. + // To make sure these messages are sent, configure and activate valid SMTP and Twilio configurations. + // Read more about your options for controlling this behaviour in the email and phone field documentations. + Human human = 4; + // Users of type machine are users that are meant to be used by a machine. + // In order to authenticate, [add a secret](apis/resources/user_service_v2/user-service-add-secret.api.mdx), [a key](apis/resources/user_service_v2/user-service-add-key.api.mdx) or [a personal access token](apis/resources/user_service_v2/user-service-add-personal-access-token.api.mdx) to the user. + // Tokens generated for new users of type machine will be of an opaque Bearer type. + // You can change the users token type to JWT by using the [management v1 service method UpdateMachine](apis/resources/mgmt/management-service-update-machine.api.mdx). + Machine machine = 5; + } + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { + example: "{\"organizationId\":\"69629026806489455\",\"userId\":\"163840776835432345\",\"username\":\"minnie-mouse\",\"human\":{\"profile\":{\"givenName\":\"Minnie\",\"familyName\":\"Mouse\",\"nickName\":\"Mini\",\"displayName\":\"Minnie Mouse\",\"preferredLanguage\":\"en\",\"gender\":\"GENDER_FEMALE\"},\"email\":{\"email\":\"mini@mouse.com\",\"sendCode\":{\"urlTemplate\":\"https://example.com/email/verify?userID={{.UserID}}&code={{.Code}}&orgID={{.OrgID}}\"}},\"phone\":{\"phone\":\"+41791234567\",\"isVerified\":true},\"password\":{\"password\":\"Secr3tP4ssw0rd!\",\"changeRequired\":true},\"idpLinks\":[{\"idpId\":\"d654e6ba-70a3-48ef-a95d-37c8d8a7901a\",\"userId\":\"6516849804890468048461403518\",\"userName\":\"user@external.com\"}],\"totpSecret\":\"TJOPWSDYILLHXFV4MLKNNJOWFG7VSDCK\"}}"; + }; +} + +message CreateUserResponse { + // The unique identifier of the newly created user. + string id = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629012906488334\""; + } + ]; + // The timestamp of the user creation. + google.protobuf.Timestamp creation_date = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; + + // The email verification code if it was requested by setting the email verification to return_code. + optional string email_code = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"XTA6BC\""; + } + ]; + // The phone verification code if it was requested by setting the phone verification to return_code. + optional string phone_code = 4 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"XTA6BC\""; + } + ]; +} + message GetUserByIDRequest { reserved 2; reserved "organization"; @@ -1550,6 +2315,142 @@ message DeleteUserResponse { zitadel.object.v2.Details details = 1; } + +message UpdateUserRequest{ + message Human { + message Profile { + // The given name is the first name of the user. + // For example, it can be used to personalize notifications and login UIs. + optional string given_name = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"Minnie\""; + } + ]; + // The family name is the last name of the user. + // For example, it can be used to personalize user interfaces and notifications. + optional string family_name = 2 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"Mouse\""; + } + ]; + // The nick name is the users short name. + // For example, it can be used to personalize user interfaces and notifications. + optional string nick_name = 3 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"Mini\""; + } + ]; + // The display name is how a user should primarily be displayed in lists. + // It can also for example be used to personalize user interfaces and notifications. + optional string display_name = 4 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"Minnie Mouse\""; + } + ]; + // The users preferred language is the language that systems should use to interact with the user. + // It has the format of a [BCP-47 language tag](https://datatracker.ietf.org/doc/html/rfc3066). + // It is used by Zitadel where no higher prioritized preferred language can be used. + // For example, browser settings can overwrite a users preferred_language. + // Notification messages and standard login UIs use the users preferred language if it is supported and allowed on the instance. + // Else, the default language of the instance is used. + optional string preferred_language = 5 [ + (validate.rules).string = {min_len: 1, max_len: 10}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 10; + example: "\"en-US\""; + } + ]; + // The users gender can for example be used to personalize user interfaces and notifications. + optional Gender gender = 6 [ + (validate.rules).enum = {defined_only: true}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"GENDER_FEMALE\""; + } + ]; + } + // Change the users profile information + optional Profile profile = 1; + // Change the users email address and/or trigger a verification email + optional SetHumanEmail email = 2; + // Change the users phone number and/or trigger a verification SMS + // To delete the users phone number, leave the phone field empty and omit the verification field. + optional SetHumanPhone phone = 3; + // Change the users password. + // You can optionally require the current password or the verification code to be correct. + optional SetPassword password = 4; + } + message Machine { + // The machine users name is a human readable field that helps identifying the user. + optional string name = 1 [ + (validate.rules).string = {max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"Acceptance Test User\""; + } + ]; + // The description is a field that helps to remember the purpose of the user. + optional string description = 2 [ + (validate.rules).string = {max_len: 500}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"The user calls the session API in the continuous integration pipeline for acceptance tests.\""; + } + ]; + } + // The user id is the users unique identifier in the instance. + // It can't be changed. + string user_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"163840776835432345\""; + } + ]; + // Set a new username that is unique within the instance. + // Beware that active tokens and sessions are invalidated when the username is changed. + optional string username = 2 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"minnie-mouse\""; + } + ]; + // Change type specific properties of the user. + oneof user_type { + Human human = 3; + Machine machine = 4; + } + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { + example: "{\"username\":\"minnie-mouse\",\"human\":{\"profile\":{\"givenName\":\"Minnie\",\"familyName\":\"Mouse\",\"displayName\":\"Minnie Mouse\"},\"email\":{\"email\":\"mini@mouse.com\",\"returnCode\":{}},\"phone\":{\"phone\":\"+41791234567\",\"isVerified\":true},\"password\":{\"password\":{\"password\":\"Secr3tP4ssw0rd!\",\"changeRequired\":true},\"verificationCode\":\"SKJd342k\"},\"totpSecret\":\"TJOPWSDYILLHXFV4MLKNNJOWFG7VSDCK\"}}"; + }; +} + +message UpdateUserResponse { + // The timestamp of the change of the user. + google.protobuf.Timestamp change_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; + // In case the email verification was set to return_code, the code will be returned + optional string email_code = 2; + // In case the phone verification was set to return_code, the code will be returned + optional string phone_code = 3; +} + message UpdateHumanUserRequest{ string user_id = 1 [ (validate.rules).string = {min_len: 1, max_len: 200}, @@ -1595,7 +2496,6 @@ message DeactivateUserResponse { zitadel.object.v2.Details details = 1; } - message ReactivateUserRequest { string user_id = 1 [ (validate.rules).string = {min_len: 1, max_len: 200}, @@ -2384,3 +3284,237 @@ message HumanMFAInitSkippedRequest { message HumanMFAInitSkippedResponse { zitadel.object.v2.Details details = 1; } + +message AddSecretRequest { + // The users resource ID. + string user_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"163840776835432345\""; + } + ]; +} + +message AddSecretResponse { + // The timestamp of the secret creation. + google.protobuf.Timestamp creation_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; + // The client secret. + // Store this secret in a secure place. + // It is not possible to retrieve it again. + string client_secret = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"WoYLHB23HAZaCSxeMJGEzbu8urHICVdFp2IegVr6Q5U4lZHKAtRvmaalNDWfCuHV\""; + } + ]; +} + +message RemoveSecretRequest { + // The users resource ID. + string user_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"163840776835432345\""; + } + ]; +} + +message RemoveSecretResponse { + // The timestamp of the secret deletion. + google.protobuf.Timestamp deletion_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; +} + +message AddKeyRequest { + // The users resource ID. + string user_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"163840776835432345\""; + } + ]; + // The date the key will expire and no logins will be possible anymore. + google.protobuf.Timestamp expiration_date = 2 [ + (validate.rules).timestamp.required = true, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2519-04-01T08:45:00.000000Z\""; + } + ]; + // Optionally provide a public key of your own generated RSA private key. + bytes public_key = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1...\""; + } + ]; +} + +message AddKeyResponse { + // The timestamp of the key creation. + google.protobuf.Timestamp creation_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; + // The keys ID. + string key_id = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"163840776835432345\""; + } + ]; + // The key which is usable to authenticate against the API. + bytes key_content = 3; +} + + +message RemoveKeyRequest { + // The users resource ID. + string user_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"163840776835432345\""; + } + ]; + // The keys ID. + string key_id = 2 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"163840776835432345\""; + } + ]; +} + +message RemoveKeyResponse { + // The timestamp of the key deletion. + google.protobuf.Timestamp deletion_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; +} + +message ListKeysRequest { + // List limitations and ordering. + optional zitadel.filter.v2.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 KeyFieldName sorting_column = 2 [ + (validate.rules).enum = {defined_only: true}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"KEY_FIELD_NAME_CREATED_DATE\"" + } + ]; + // Define the criteria to query for. + repeated KeysSearchFilter filters = 3; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { + example: "{\"pagination\":{\"offset\":0,\"limit\":10,\"asc\":false},\"sortingColumn\":\"KEY_FIELD_NAME_CREATED_DATE\",\"filters\":[{\"andFilter\":{\"filters\":[{\"organizationIdFilter\":{\"organizationId\":\"163840776835432345\",\"method\":\"TEXT_FILTER_METHOD_EQUALS\"}},{\"notFilter\":{\"filter\":{\"userIdFilter\":{\"userId\":\"163840776835432345\",\"method\":\"TEXT_FILTER_METHOD_EQUALS\"}}}},{\"orFilter\":{\"filters\":[{\"keyIdFilter\":{\"keyId\":\"163840776835432345\",\"method\":\"TEXT_FILTER_METHOD_EQUALS\"}},{\"keyIdFilter\":{\"keyId\":\"163840776835432345\",\"method\":\"TEXT_FILTER_METHOD_EQUALS\"}}]}}]}}]}"; + }; +} + +message ListKeysResponse { + zitadel.filter.v2.PaginationResponse pagination = 1; + repeated Key result = 2; +} + +message AddPersonalAccessTokenRequest { + // The users resource ID. + string user_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"163840776835432345\""; + } + ]; + // The timestamp when the token will expire. + google.protobuf.Timestamp expiration_date = 2 [ + (validate.rules).timestamp.required = true, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2519-04-01T08:45:00.000000Z\""; + } + ]; +} + +message AddPersonalAccessTokenResponse { + // The timestamp of the personal access token creation. + google.protobuf.Timestamp creation_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; + // The tokens ID. + string token_id = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"163840776835432345\""; + } + ]; + // The personal access token that can be used to authenticate against the API + string token = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c\""; + } + ]; +} + +message RemovePersonalAccessTokenRequest { + // The users resource ID. + string user_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"163840776835432345\""; + } + ]; + // The tokens ID. + string token_id = 2 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"163840776835432345\""; + } + ]; +} + +message RemovePersonalAccessTokenResponse { + // The timestamp of the personal access token deletion. + google.protobuf.Timestamp deletion_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; +} + + +message ListPersonalAccessTokensRequest { + // List limitations and ordering. + optional zitadel.filter.v2.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 PersonalAccessTokenFieldName sorting_column = 2 [ + (validate.rules).enum = {defined_only: true}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"PERSONAL_ACCESS_TOKEN_FIELD_NAME_CREATED_DATE\"" + } + ]; + // Define the criteria to query for. + repeated PersonalAccessTokensSearchFilter filters = 3; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { + example: "{\"pagination\":{\"offset\":0,\"limit\":10,\"asc\":false},\"sortingColumn\":\"PERSONAL_ACCESS_TOKEN_FIELD_NAME_CREATED_DATE\",\"filters\":[{\"andFilter\":{\"filters\":[{\"organizationIdFilter\":{\"organizationId\":\"163840776835432345\",\"method\":\"TEXT_FILTER_METHOD_EQUALS\"}},{\"notFilter\":{\"filter\":{\"userIdFilter\":{\"userId\":\"163840776835432345\",\"method\":\"TEXT_FILTER_METHOD_EQUALS\"}}}},{\"orFilter\":{\"filters\":[{\"personalAccessTokenIdFilter\":{\"personalAccessTokenId\":\"163840776835432345\",\"method\":\"TEXT_FILTER_METHOD_EQUALS\"}},{\"personalAccessTokenIdFilter\":{\"personalAccessTokenId\":\"163840776835432345\",\"method\":\"TEXT_FILTER_METHOD_EQUALS\"}}]}}]}}]}"; + }; +} + +message ListPersonalAccessTokensResponse { + zitadel.filter.v2.PaginationResponse pagination = 1; + repeated PersonalAccessToken result = 2; +} From 1a80e265020844a08586691bea135a69796745c4 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Wed, 4 Jun 2025 11:04:52 +0200 Subject: [PATCH 64/76] fix(console): org context for V2 user creation (#9971) # Which Problems Are Solved This PR addresses a bug in Console V2 APIs, specifically when the feature toggle is enabled, which caused incorrect organization context assignment during new user creation. Co-authored-by: Ramon --- .../user-create/user-create-v2/user-create-v2.component.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/console/src/app/pages/users/user-create/user-create-v2/user-create-v2.component.ts b/console/src/app/pages/users/user-create/user-create-v2/user-create-v2.component.ts index 9fd765264d..b92c112357 100644 --- a/console/src/app/pages/users/user-create/user-create-v2/user-create-v2.component.ts +++ b/console/src/app/pages/users/user-create/user-create-v2/user-create-v2.component.ts @@ -32,6 +32,7 @@ import { withLatestFromSynchronousFix } from 'src/app/utils/withLatestFromSynchr import { PasswordComplexityValidatorFactoryService } from 'src/app/services/password-complexity-validator-factory.service'; import { NewFeatureService } from 'src/app/services/new-feature.service'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { GrpcAuthService } from 'src/app/services/grpc-auth.service'; type PwdForm = ReturnType; type AuthenticationFactor = @@ -65,6 +66,7 @@ export class UserCreateV2Component implements OnInit { private readonly destroyRef: DestroyRef, private readonly route: ActivatedRoute, protected readonly location: Location, + private readonly authService: GrpcAuthService, ) { this.userForm = this.buildUserForm(); @@ -180,9 +182,12 @@ export class UserCreateV2Component implements OnInit { private async createUserV2Try(authenticationFactor: AuthenticationFactor) { this.loading.set(true); + const org = await this.authService.getActiveOrg(); + const userValues = this.userForm.getRawValue(); const humanReq: MessageInitShape = { + organization: { org: { case: 'orgId', value: org.id } }, username: userValues.username, profile: { givenName: userValues.givenName, From 839c761357f17f51ede7f0a3f0f4a4c96a3863ab Mon Sep 17 00:00:00 2001 From: AnthonyKot Date: Wed, 4 Jun 2025 11:26:53 +0200 Subject: [PATCH 65/76] fix(FE): allow only enabled factors to be displayed on user page (#9313) # Which Problems Are Solved - Hides for users MFA options are not allowed by org policy. - Fix for "ng test" across "console" # How the Problems Are Solved - Before displaying MFA options we call "listMyMultiFactors" from parent component to filter MFA allowed by org # Additional Changes - Dependency Injection was fixed around ng unit tests # Additional Context admin view Screenshot 2025-02-06 at 00 26 50 user view Screenshot 2025-02-06 at 00 27 16 test Screenshot 2025-02-06 at 00 01 36 The issue: https://github.com/zitadel/zitadel/issues/9176 The bug report: https://discord.com/channels/927474939156643850/1307006457815896094 --------- Co-authored-by: a k Co-authored-by: a k Co-authored-by: a k Co-authored-by: Ramon --- console/package.json | 1 + .../oidc-configuration.component.spec.ts | 10 +- .../modules/domains/domains.component.spec.ts | 10 +- .../filter-project.component.spec.ts | 10 +- .../app/modules/input/input.directive.spec.ts | 45 +++++- .../app/modules/label/label.component.spec.ts | 10 +- .../login-policy/login-policy.component.ts | 7 +- .../message-texts.component.spec.ts | 10 +- .../notification-policy.component.spec.ts | 10 +- ...word-dialog-sms-provider.component.spec.ts | 10 +- .../provider-github-es.component.spec.ts | 10 +- ...vider-gitlab-self-hosted.component.spec.ts | 10 +- .../provider-gitlab.component.spec.ts | 10 +- .../show-token-dialog.component.spec.ts | 10 +- .../smtp-table/smtp-table.component.spec.ts | 10 +- .../add-action-dialog.component.spec.ts | 10 +- .../add-flow-dialog.component.spec.ts | 10 +- .../auth-factor-dialog.component.html | 13 +- .../auth-factor-dialog.component.ts | 14 +- .../auth-user-mfa.component.spec.ts | 133 +++++++++++++++++- .../auth-user-mfa/auth-user-mfa.component.ts | 127 +++++++++-------- .../user-detail/contact/contact.component.ts | 8 +- .../detail-form-machine.component.spec.ts | 10 +- .../passwordless.component.spec.ts | 10 +- console/src/app/services/grpc-auth.service.ts | 45 ------ console/src/app/services/new-auth.service.ts | 37 +++++ console/yarn.lock | 25 +++- 27 files changed, 411 insertions(+), 204 deletions(-) diff --git a/console/package.json b/console/package.json index 77a8a40147..0095360017 100644 --- a/console/package.json +++ b/console/package.json @@ -82,6 +82,7 @@ "jasmine-spec-reporter": "~7.0.0", "karma": "^6.4.4", "karma-chrome-launcher": "^3.2.0", + "karma-coverage": "^2.2.1", "karma-coverage-istanbul-reporter": "^3.0.3", "karma-jasmine": "^5.1.0", "karma-jasmine-html-reporter": "^2.1.0", diff --git a/console/src/app/components/oidc-configuration/oidc-configuration.component.spec.ts b/console/src/app/components/oidc-configuration/oidc-configuration.component.spec.ts index 6155ba5693..e99aee357c 100644 --- a/console/src/app/components/oidc-configuration/oidc-configuration.component.spec.ts +++ b/console/src/app/components/oidc-configuration/oidc-configuration.component.spec.ts @@ -1,16 +1,16 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { QuickstartComponent } from './quickstart.component'; +import { OIDCConfigurationComponent } from './oidc-configuration.component'; describe('QuickstartComponent', () => { - let component: QuickstartComponent; - let fixture: ComponentFixture; + let component: OIDCConfigurationComponent; + let fixture: ComponentFixture; beforeEach(() => { TestBed.configureTestingModule({ - declarations: [QuickstartComponent], + declarations: [OIDCConfigurationComponent], }); - fixture = TestBed.createComponent(QuickstartComponent); + fixture = TestBed.createComponent(OIDCConfigurationComponent); component = fixture.componentInstance; fixture.detectChanges(); }); diff --git a/console/src/app/modules/domains/domains.component.spec.ts b/console/src/app/modules/domains/domains.component.spec.ts index 127bae48b5..f3d75fb12b 100644 --- a/console/src/app/modules/domains/domains.component.spec.ts +++ b/console/src/app/modules/domains/domains.component.spec.ts @@ -1,19 +1,19 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { OrgDomainsComponent } from './org-domains.component'; +import { DomainsComponent } from './domains.component'; describe('OrgDomainsComponent', () => { - let component: OrgDomainsComponent; - let fixture: ComponentFixture; + let component: DomainsComponent; + let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [OrgDomainsComponent], + declarations: [DomainsComponent], }).compileComponents(); }); beforeEach(() => { - fixture = TestBed.createComponent(OrgDomainsComponent); + fixture = TestBed.createComponent(DomainsComponent); component = fixture.componentInstance; fixture.detectChanges(); }); diff --git a/console/src/app/modules/filter-project/filter-project.component.spec.ts b/console/src/app/modules/filter-project/filter-project.component.spec.ts index 0ed0436db8..ff465d8705 100644 --- a/console/src/app/modules/filter-project/filter-project.component.spec.ts +++ b/console/src/app/modules/filter-project/filter-project.component.spec.ts @@ -1,19 +1,19 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { FilterUserComponent } from './filter-user.component'; +import { FilterProjectComponent } from './filter-project.component'; describe('FilterUserComponent', () => { - let component: FilterUserComponent; - let fixture: ComponentFixture; + let component: FilterProjectComponent; + let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [FilterUserComponent], + declarations: [FilterProjectComponent], }).compileComponents(); }); beforeEach(() => { - fixture = TestBed.createComponent(FilterUserComponent); + fixture = TestBed.createComponent(FilterProjectComponent); component = fixture.componentInstance; fixture.detectChanges(); }); diff --git a/console/src/app/modules/input/input.directive.spec.ts b/console/src/app/modules/input/input.directive.spec.ts index 463fed5431..46544ca096 100644 --- a/console/src/app/modules/input/input.directive.spec.ts +++ b/console/src/app/modules/input/input.directive.spec.ts @@ -1,8 +1,49 @@ +import { Component, ElementRef, NgZone } from '@angular/core'; +import { TestBed, ComponentFixture } from '@angular/core/testing'; import { InputDirective } from './input.directive'; +import { Platform } from '@angular/cdk/platform'; +import { NgControl, NgForm, FormGroupDirective } from '@angular/forms'; +import { ErrorStateMatcher } from '@angular/material/core'; +import { AutofillMonitor } from '@angular/cdk/text-field'; +import { MatFormField } from '@angular/material/form-field'; +import { MAT_INPUT_VALUE_ACCESSOR } from '@angular/material/input'; +import { of } from 'rxjs'; +import { By } from '@angular/platform-browser'; + +@Component({ + template: ``, +}) +class TestHostComponent {} describe('InputDirective', () => { + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [InputDirective, TestHostComponent], + providers: [ + { provide: ElementRef, useValue: new ElementRef(document.createElement('input')) }, + Platform, + { provide: NgControl, useValue: null }, + { provide: NgForm, useValue: null }, + { provide: FormGroupDirective, useValue: null }, + ErrorStateMatcher, + { provide: MAT_INPUT_VALUE_ACCESSOR, useValue: null }, + { + provide: AutofillMonitor, + useValue: { monitor: () => of(), stopMonitoring: () => {} }, + }, + NgZone, + { provide: MatFormField, useValue: null }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(TestHostComponent); + fixture.detectChanges(); + }); + it('should create an instance', () => { - const directive = new InputDirective(); - expect(directive).toBeTruthy(); + const directiveEl = fixture.debugElement.query(By.directive(InputDirective)); + expect(directiveEl).toBeTruthy(); }); }); diff --git a/console/src/app/modules/label/label.component.spec.ts b/console/src/app/modules/label/label.component.spec.ts index 2b29b30873..e719aa3775 100644 --- a/console/src/app/modules/label/label.component.spec.ts +++ b/console/src/app/modules/label/label.component.spec.ts @@ -1,19 +1,19 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { AvatarComponent } from './avatar.component'; +import { LabelComponent } from './label.component'; describe('AvatarComponent', () => { - let component: AvatarComponent; - let fixture: ComponentFixture; + let component: LabelComponent; + let fixture: ComponentFixture; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [AvatarComponent], + declarations: [LabelComponent], }).compileComponents(); })); beforeEach(() => { - fixture = TestBed.createComponent(AvatarComponent); + fixture = TestBed.createComponent(LabelComponent); component = fixture.componentInstance; fixture.detectChanges(); }); diff --git a/console/src/app/modules/policies/login-policy/login-policy.component.ts b/console/src/app/modules/policies/login-policy/login-policy.component.ts index b4e4557f00..5dd85b8b38 100644 --- a/console/src/app/modules/policies/login-policy/login-policy.component.ts +++ b/console/src/app/modules/policies/login-policy/login-policy.component.ts @@ -2,14 +2,12 @@ import { Component, Injector, Input, OnDestroy, OnInit, Type } from '@angular/co import { AbstractControl, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; import { MatDialog } from '@angular/material/dialog'; import { Duration } from 'google-protobuf/google/protobuf/duration_pb'; -import { firstValueFrom, forkJoin, from, Observable, of, Subject, take } from 'rxjs'; +import { forkJoin, from, of, Subject, take } from 'rxjs'; import { GetLoginPolicyResponse as AdminGetLoginPolicyResponse, UpdateLoginPolicyRequest, - UpdateLoginPolicyResponse, } from 'src/app/proto/generated/zitadel/admin_pb'; import { - AddCustomLoginPolicyRequest, GetLoginPolicyResponse as MgmtGetLoginPolicyResponse, UpdateCustomLoginPolicyRequest, } from 'src/app/proto/generated/zitadel/management_pb'; @@ -24,8 +22,7 @@ import { InfoSectionType } from '../../info-section/info-section.component'; import { WarnDialogComponent } from '../../warn-dialog/warn-dialog.component'; import { PolicyComponentServiceType } from '../policy-component-types.enum'; import { LoginMethodComponentType } from './factor-table/factor-table.component'; -import { catchError, map, takeUntil } from 'rxjs/operators'; -import { error } from 'console'; +import { map, takeUntil } from 'rxjs/operators'; import { LoginPolicyService } from '../../../services/login-policy.service'; const minValueValidator = (minValue: number) => (control: AbstractControl) => { diff --git a/console/src/app/modules/policies/message-texts/message-texts.component.spec.ts b/console/src/app/modules/policies/message-texts/message-texts.component.spec.ts index 71dd427da5..e5569f9ed3 100644 --- a/console/src/app/modules/policies/message-texts/message-texts.component.spec.ts +++ b/console/src/app/modules/policies/message-texts/message-texts.component.spec.ts @@ -1,19 +1,19 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { LoginPolicyComponent } from './login-policy.component'; +import { MessageTextsComponent } from './message-texts.component'; describe('LoginPolicyComponent', () => { - let component: LoginPolicyComponent; - let fixture: ComponentFixture; + let component: MessageTextsComponent; + let fixture: ComponentFixture; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [LoginPolicyComponent], + declarations: [MessageTextsComponent], }).compileComponents(); })); beforeEach(() => { - fixture = TestBed.createComponent(LoginPolicyComponent); + fixture = TestBed.createComponent(MessageTextsComponent); component = fixture.componentInstance; fixture.detectChanges(); }); diff --git a/console/src/app/modules/policies/notification-policy/notification-policy.component.spec.ts b/console/src/app/modules/policies/notification-policy/notification-policy.component.spec.ts index c323d884f1..f529e143a5 100644 --- a/console/src/app/modules/policies/notification-policy/notification-policy.component.spec.ts +++ b/console/src/app/modules/policies/notification-policy/notification-policy.component.spec.ts @@ -1,19 +1,19 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { PasswordComplexityPolicyComponent } from './password-complexity-policy.component'; +import { NotificationPolicyComponent } from './notification-policy.component'; describe('PasswordComplexityPolicyComponent', () => { - let component: PasswordComplexityPolicyComponent; - let fixture: ComponentFixture; + let component: NotificationPolicyComponent; + let fixture: ComponentFixture; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [PasswordComplexityPolicyComponent], + declarations: [NotificationPolicyComponent], }).compileComponents(); })); beforeEach(() => { - fixture = TestBed.createComponent(PasswordComplexityPolicyComponent); + fixture = TestBed.createComponent(NotificationPolicyComponent); component = fixture.componentInstance; fixture.detectChanges(); }); diff --git a/console/src/app/modules/policies/notification-sms-provider/password-dialog-sms-provider/password-dialog-sms-provider.component.spec.ts b/console/src/app/modules/policies/notification-sms-provider/password-dialog-sms-provider/password-dialog-sms-provider.component.spec.ts index 034bbe8de0..b009b03757 100644 --- a/console/src/app/modules/policies/notification-sms-provider/password-dialog-sms-provider/password-dialog-sms-provider.component.spec.ts +++ b/console/src/app/modules/policies/notification-sms-provider/password-dialog-sms-provider/password-dialog-sms-provider.component.spec.ts @@ -1,19 +1,19 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { PasswordDialogComponent } from './password-dialog-sms-provider.component'; +import { PasswordDialogSMSProviderComponent } from './password-dialog-sms-provider.component'; describe('PasswordDialogComponent', () => { - let component: PasswordDialogComponent; - let fixture: ComponentFixture; + let component: PasswordDialogSMSProviderComponent; + let fixture: ComponentFixture; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [PasswordDialogComponent], + declarations: [PasswordDialogSMSProviderComponent], }).compileComponents(); })); beforeEach(() => { - fixture = TestBed.createComponent(PasswordDialogComponent); + fixture = TestBed.createComponent(PasswordDialogSMSProviderComponent); component = fixture.componentInstance; fixture.detectChanges(); }); diff --git a/console/src/app/modules/providers/provider-github-es/provider-github-es.component.spec.ts b/console/src/app/modules/providers/provider-github-es/provider-github-es.component.spec.ts index 0086bf0ce3..304004d0cf 100644 --- a/console/src/app/modules/providers/provider-github-es/provider-github-es.component.spec.ts +++ b/console/src/app/modules/providers/provider-github-es/provider-github-es.component.spec.ts @@ -1,19 +1,19 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { ProviderOAuthComponent } from './provider-oauth.component'; +import { ProviderGithubESComponent } from './provider-github-es.component'; describe('ProviderOAuthComponent', () => { - let component: ProviderOAuthComponent; - let fixture: ComponentFixture; + let component: ProviderGithubESComponent; + let fixture: ComponentFixture; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ProviderOAuthComponent], + declarations: [ProviderGithubESComponent], }).compileComponents(); })); beforeEach(() => { - fixture = TestBed.createComponent(ProviderOAuthComponent); + fixture = TestBed.createComponent(ProviderGithubESComponent); component = fixture.componentInstance; fixture.detectChanges(); }); diff --git a/console/src/app/modules/providers/provider-gitlab-self-hosted/provider-gitlab-self-hosted.component.spec.ts b/console/src/app/modules/providers/provider-gitlab-self-hosted/provider-gitlab-self-hosted.component.spec.ts index 3b6fdadce3..5a0bbf6d08 100644 --- a/console/src/app/modules/providers/provider-gitlab-self-hosted/provider-gitlab-self-hosted.component.spec.ts +++ b/console/src/app/modules/providers/provider-gitlab-self-hosted/provider-gitlab-self-hosted.component.spec.ts @@ -1,19 +1,19 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { ProviderGoogleComponent } from './provider-google.component'; +import { ProviderGitlabSelfHostedComponent } from './provider-gitlab-self-hosted.component'; describe('ProviderGoogleComponent', () => { - let component: ProviderGoogleComponent; - let fixture: ComponentFixture; + let component: ProviderGitlabSelfHostedComponent; + let fixture: ComponentFixture; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ProviderGoogleComponent], + declarations: [ProviderGitlabSelfHostedComponent], }).compileComponents(); })); beforeEach(() => { - fixture = TestBed.createComponent(ProviderGoogleComponent); + fixture = TestBed.createComponent(ProviderGitlabSelfHostedComponent); component = fixture.componentInstance; fixture.detectChanges(); }); diff --git a/console/src/app/modules/providers/provider-gitlab/provider-gitlab.component.spec.ts b/console/src/app/modules/providers/provider-gitlab/provider-gitlab.component.spec.ts index 3b6fdadce3..7b5becd782 100644 --- a/console/src/app/modules/providers/provider-gitlab/provider-gitlab.component.spec.ts +++ b/console/src/app/modules/providers/provider-gitlab/provider-gitlab.component.spec.ts @@ -1,19 +1,19 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { ProviderGoogleComponent } from './provider-google.component'; +import { ProviderGitlabComponent } from './provider-gitlab.component'; describe('ProviderGoogleComponent', () => { - let component: ProviderGoogleComponent; - let fixture: ComponentFixture; + let component: ProviderGitlabComponent; + let fixture: ComponentFixture; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ProviderGoogleComponent], + declarations: [ProviderGitlabComponent], }).compileComponents(); })); beforeEach(() => { - fixture = TestBed.createComponent(ProviderGoogleComponent); + fixture = TestBed.createComponent(ProviderGitlabComponent); component = fixture.componentInstance; fixture.detectChanges(); }); diff --git a/console/src/app/modules/show-token-dialog/show-token-dialog.component.spec.ts b/console/src/app/modules/show-token-dialog/show-token-dialog.component.spec.ts index de74dc7522..35f4dbcf77 100644 --- a/console/src/app/modules/show-token-dialog/show-token-dialog.component.spec.ts +++ b/console/src/app/modules/show-token-dialog/show-token-dialog.component.spec.ts @@ -1,19 +1,19 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { ShowKeyDialogComponent } from './show-key-dialog.component'; +import { ShowTokenDialogComponent } from './show-token-dialog.component'; describe('ShowKeyDialogComponent', () => { - let component: ShowKeyDialogComponent; - let fixture: ComponentFixture; + let component: ShowTokenDialogComponent; + let fixture: ComponentFixture; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ShowKeyDialogComponent], + declarations: [ShowTokenDialogComponent], }).compileComponents(); })); beforeEach(() => { - fixture = TestBed.createComponent(ShowKeyDialogComponent); + fixture = TestBed.createComponent(ShowTokenDialogComponent); component = fixture.componentInstance; fixture.detectChanges(); }); diff --git a/console/src/app/modules/smtp-table/smtp-table.component.spec.ts b/console/src/app/modules/smtp-table/smtp-table.component.spec.ts index 8095d73255..fe4719482c 100644 --- a/console/src/app/modules/smtp-table/smtp-table.component.spec.ts +++ b/console/src/app/modules/smtp-table/smtp-table.component.spec.ts @@ -1,19 +1,19 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { IdpTableComponent } from './smtp-table.component'; +import { SMTPTableComponent } from './smtp-table.component'; describe('UserTableComponent', () => { - let component: IdpTableComponent; - let fixture: ComponentFixture; + let component: SMTPTableComponent; + let fixture: ComponentFixture; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [IdpTableComponent], + declarations: [SMTPTableComponent], }).compileComponents(); })); beforeEach(() => { - fixture = TestBed.createComponent(IdpTableComponent); + fixture = TestBed.createComponent(SMTPTableComponent); component = fixture.componentInstance; fixture.detectChanges(); }); diff --git a/console/src/app/pages/actions/add-action-dialog/add-action-dialog.component.spec.ts b/console/src/app/pages/actions/add-action-dialog/add-action-dialog.component.spec.ts index f4b79bee55..b7c7069728 100644 --- a/console/src/app/pages/actions/add-action-dialog/add-action-dialog.component.spec.ts +++ b/console/src/app/pages/actions/add-action-dialog/add-action-dialog.component.spec.ts @@ -1,19 +1,19 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { AddKeyDialogComponent } from './add-key-dialog.component'; +import { AddActionDialogComponent } from './add-action-dialog.component'; describe('AddKeyDialogComponent', () => { - let component: AddKeyDialogComponent; - let fixture: ComponentFixture; + let component: AddActionDialogComponent; + let fixture: ComponentFixture; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [AddKeyDialogComponent], + declarations: [AddActionDialogComponent], }).compileComponents(); })); beforeEach(() => { - fixture = TestBed.createComponent(AddKeyDialogComponent); + fixture = TestBed.createComponent(AddActionDialogComponent); component = fixture.componentInstance; fixture.detectChanges(); }); diff --git a/console/src/app/pages/actions/add-flow-dialog/add-flow-dialog.component.spec.ts b/console/src/app/pages/actions/add-flow-dialog/add-flow-dialog.component.spec.ts index f4b79bee55..ca9b7c4507 100644 --- a/console/src/app/pages/actions/add-flow-dialog/add-flow-dialog.component.spec.ts +++ b/console/src/app/pages/actions/add-flow-dialog/add-flow-dialog.component.spec.ts @@ -1,19 +1,19 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { AddKeyDialogComponent } from './add-key-dialog.component'; +import { AddFlowDialogComponent } from './add-flow-dialog.component'; describe('AddKeyDialogComponent', () => { - let component: AddKeyDialogComponent; - let fixture: ComponentFixture; + let component: AddFlowDialogComponent; + let fixture: ComponentFixture; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [AddKeyDialogComponent], + declarations: [AddFlowDialogComponent], }).compileComponents(); })); beforeEach(() => { - fixture = TestBed.createComponent(AddKeyDialogComponent); + fixture = TestBed.createComponent(AddFlowDialogComponent); component = fixture.componentInstance; fixture.detectChanges(); }); diff --git a/console/src/app/pages/users/user-detail/auth-user-detail/auth-factor-dialog/auth-factor-dialog.component.html b/console/src/app/pages/users/user-detail/auth-user-detail/auth-factor-dialog/auth-factor-dialog.component.html index e36ec2bcbb..22a4498090 100644 --- a/console/src/app/pages/users/user-detail/auth-user-detail/auth-factor-dialog/auth-factor-dialog.component.html +++ b/console/src/app/pages/users/user-detail/auth-user-detail/auth-factor-dialog/auth-factor-dialog.component.html @@ -1,5 +1,5 @@

- {{ 'USER.MFA.DIALOG.ADD_MFA_TITLE' | translate }} {{ data?.number }} + {{ 'USER.MFA.DIALOG.ADD_MFA_TITLE' | translate }}

@@ -7,6 +7,7 @@
-
-
@@ -304,22 +317,22 @@

{{ 'IDP.DETAIL.DATECREATED' | translate }}

-

- {{ idp.details.creationDate | timestampToDate | localizedDate: 'dd. MMMM YYYY, HH:mm' }} +

+ {{ creationDate | timestampToDate | localizedDate: 'dd. MMMM YYYY, HH:mm' }}

{{ 'IDP.DETAIL.DATECHANGED' | translate }}

-

- {{ idp.details.changeDate | timestampToDate | localizedDate: 'dd. MMMM YYYY, HH:mm' }} +

+ {{ changeDate | timestampToDate | localizedDate: 'dd. MMMM YYYY, HH:mm' }}

{{ 'IDP.STATE' | translate }}

From 85e3b7449c417a238e12bac2b312ce4d58905804 Mon Sep 17 00:00:00 2001 From: Stefan Benz <46600784+stebenz@users.noreply.github.com> Date: Wed, 4 Jun 2025 13:46:10 +0200 Subject: [PATCH 67/76] fix: correct permissions for projects on v2 api (#9973) # Which Problems Are Solved Permission checks in project v2beta API did not cover projects and granted projects correctly. # How the Problems Are Solved Add permission checks v1 correctly to the list queries, add correct permission checks v2 for projects. # Additional Changes Correct Pre-Checks for project grants that the right resource owner is used. # Additional Context Permission checks v2 for project grants is still outstanding under #9972. --- internal/api/grpc/management/project_grant.go | 4 +- .../v2beta/integration/project_grant_test.go | 65 ++- .../v2beta/integration/project_test.go | 65 +++ .../project/v2beta/integration/query_test.go | 182 ++++++- .../api/grpc/project/v2beta/project_grant.go | 10 +- internal/command/permission_checks.go | 18 +- internal/command/project_grant.go | 116 +++-- internal/command/project_grant_model.go | 62 +-- internal/command/project_grant_test.go | 444 +++++++++++++++++- internal/command/project_old.go | 2 +- internal/domain/roles.go | 1 + internal/integration/client.go | 18 + internal/query/project.go | 48 +- internal/query/project_grant.go | 15 +- internal/query/project_role.go | 2 +- 15 files changed, 950 insertions(+), 102 deletions(-) diff --git a/internal/api/grpc/management/project_grant.go b/internal/api/grpc/management/project_grant.go index e9313c1327..d84375818d 100644 --- a/internal/api/grpc/management/project_grant.go +++ b/internal/api/grpc/management/project_grant.go @@ -109,7 +109,7 @@ func (s *Server) UpdateProjectGrant(ctx context.Context, req *mgmt_pb.UpdateProj } func (s *Server) DeactivateProjectGrant(ctx context.Context, req *mgmt_pb.DeactivateProjectGrantRequest) (*mgmt_pb.DeactivateProjectGrantResponse, error) { - details, err := s.command.DeactivateProjectGrant(ctx, req.ProjectId, req.GrantId, authz.GetCtxData(ctx).OrgID) + details, err := s.command.DeactivateProjectGrant(ctx, req.ProjectId, req.GrantId, "", authz.GetCtxData(ctx).OrgID) if err != nil { return nil, err } @@ -119,7 +119,7 @@ func (s *Server) DeactivateProjectGrant(ctx context.Context, req *mgmt_pb.Deacti } func (s *Server) ReactivateProjectGrant(ctx context.Context, req *mgmt_pb.ReactivateProjectGrantRequest) (*mgmt_pb.ReactivateProjectGrantResponse, error) { - details, err := s.command.ReactivateProjectGrant(ctx, req.ProjectId, req.GrantId, authz.GetCtxData(ctx).OrgID) + details, err := s.command.ReactivateProjectGrant(ctx, req.ProjectId, req.GrantId, "", authz.GetCtxData(ctx).OrgID) if err != nil { return nil, err } diff --git a/internal/api/grpc/project/v2beta/integration/project_grant_test.go b/internal/api/grpc/project/v2beta/integration/project_grant_test.go index 8500f24d56..34fa10e3de 100644 --- a/internal/api/grpc/project/v2beta/integration/project_grant_test.go +++ b/internal/api/grpc/project/v2beta/integration/project_grant_test.go @@ -169,6 +169,12 @@ func TestServer_CreateProjectGrant_Permission(t *testing.T) { iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + userResp := instance.CreateMachineUser(iamOwnerCtx) + patResp := instance.CreatePersonalAccessToken(iamOwnerCtx, userResp.GetUserId()) + projectResp := createProject(iamOwnerCtx, instance, t, instance.DefaultOrg.GetId(), false, false) + instance.CreateProjectMembership(t, iamOwnerCtx, projectResp.GetId(), userResp.GetUserId()) + projectOwnerCtx := integration.WithAuthorizationToken(CTX, patResp.Token) + type want struct { creationDate bool } @@ -206,6 +212,33 @@ func TestServer_CreateProjectGrant_Permission(t *testing.T) { req: &project.CreateProjectGrantRequest{}, wantErr: true, }, + { + name: "project owner, other project", + ctx: projectOwnerCtx, + prepare: func(request *project.CreateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + grantedOrgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + + request.ProjectId = projectResp.GetId() + request.GrantedOrganizationId = grantedOrgResp.GetOrganizationId() + }, + req: &project.CreateProjectGrantRequest{}, + wantErr: true, + }, + { + name: "project owner, ok", + ctx: projectOwnerCtx, + prepare: func(request *project.CreateProjectGrantRequest) { + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + }, + req: &project.CreateProjectGrantRequest{}, + want: want{ + creationDate: true, + }, + }, { name: "organization owner, other org", ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), @@ -405,6 +438,13 @@ func TestServer_UpdateProjectGrant_Permission(t *testing.T) { iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + userResp := instance.CreateMachineUser(iamOwnerCtx) + patResp := instance.CreatePersonalAccessToken(iamOwnerCtx, userResp.GetUserId()) + projectID := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), false, false).GetId() + instance.CreateProjectGrant(iamOwnerCtx, t, projectID, orgResp.GetOrganizationId()) + instance.CreateProjectGrantMembership(t, iamOwnerCtx, projectID, orgResp.GetOrganizationId(), userResp.GetUserId()) + projectGrantOwnerCtx := integration.WithAuthorizationToken(CTX, patResp.Token) + type args struct { ctx context.Context req *project.UpdateProjectGrantRequest @@ -458,6 +498,25 @@ func TestServer_UpdateProjectGrant_Permission(t *testing.T) { }, wantErr: true, }, + { + name: "project grant owner, no permission", + prepare: func(request *project.UpdateProjectGrantRequest) { + roles := []string{gofakeit.Animal(), gofakeit.Animal(), gofakeit.Animal()} + request.ProjectId = projectID + request.GrantedOrganizationId = orgResp.GetOrganizationId() + + for _, role := range roles { + instance.AddProjectRole(iamOwnerCtx, t, projectID, role, role, "") + } + + request.RoleKeys = roles + }, + args: args{ + ctx: projectGrantOwnerCtx, + req: &project.UpdateProjectGrantRequest{}, + }, + wantErr: true, + }, { name: "organization owner, other org", prepare: func(request *project.UpdateProjectGrantRequest) { @@ -598,7 +657,7 @@ func TestServer_DeleteProjectGrant(t *testing.T) { ProjectId: "notexisting", GrantedOrganizationId: "notexisting", }, - wantErr: true, + wantDeletionDate: false, }, { name: "delete", @@ -650,8 +709,8 @@ func TestServer_DeleteProjectGrant(t *testing.T) { instance.DeleteProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) return creationDate, time.Now().UTC() }, - req: &project.DeleteProjectGrantRequest{}, - wantErr: true, + req: &project.DeleteProjectGrantRequest{}, + wantDeletionDate: true, }, } for _, tt := range tests { diff --git a/internal/api/grpc/project/v2beta/integration/project_test.go b/internal/api/grpc/project/v2beta/integration/project_test.go index 6c0a5c96f6..5412f6eb58 100644 --- a/internal/api/grpc/project/v2beta/integration/project_test.go +++ b/internal/api/grpc/project/v2beta/integration/project_test.go @@ -300,6 +300,12 @@ func TestServer_UpdateProject_Permission(t *testing.T) { iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + userResp := instance.CreateMachineUser(iamOwnerCtx) + patResp := instance.CreatePersonalAccessToken(iamOwnerCtx, userResp.GetUserId()) + projectID := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false).GetId() + instance.CreateProjectMembership(t, iamOwnerCtx, projectID, userResp.GetUserId()) + projectOwnerCtx := integration.WithAuthorizationToken(CTX, patResp.Token) + type args struct { ctx context.Context req *project.UpdateProjectRequest @@ -343,6 +349,36 @@ func TestServer_UpdateProject_Permission(t *testing.T) { }, wantErr: true, }, + { + name: "project owner, no permission", + prepare: func(request *project.UpdateProjectRequest) { + projectID := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false).GetId() + request.Id = projectID + }, + args: args{ + ctx: projectOwnerCtx, + req: &project.UpdateProjectRequest{ + Name: gu.Ptr(gofakeit.AppName()), + }, + }, + wantErr: true, + }, + { + name: " roject owner, ok", + prepare: func(request *project.UpdateProjectRequest) { + request.Id = projectID + }, + args: args{ + ctx: projectOwnerCtx, + req: &project.UpdateProjectRequest{ + Name: gu.Ptr(gofakeit.AppName()), + }, + }, + want: want{ + change: true, + changeDate: true, + }, + }, { name: "missing permission, other organization", prepare: func(request *project.UpdateProjectRequest) { @@ -499,6 +535,12 @@ func TestServer_DeleteProject_Permission(t *testing.T) { iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + userResp := instance.CreateMachineUser(iamOwnerCtx) + patResp := instance.CreatePersonalAccessToken(iamOwnerCtx, userResp.GetUserId()) + projectID := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false).GetId() + instance.CreateProjectMembership(t, iamOwnerCtx, projectID, userResp.GetUserId()) + projectOwnerCtx := integration.WithAuthorizationToken(CTX, patResp.Token) + tests := []struct { name string ctx context.Context @@ -531,6 +573,29 @@ func TestServer_DeleteProject_Permission(t *testing.T) { req: &project.DeleteProjectRequest{}, wantErr: true, }, + { + name: "project owner, no permission", + ctx: projectOwnerCtx, + prepare: func(request *project.DeleteProjectRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + projectID := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false).GetId() + request.Id = projectID + return creationDate, time.Time{} + }, + req: &project.DeleteProjectRequest{}, + wantErr: true, + }, + { + name: "project owner, ok", + ctx: projectOwnerCtx, + prepare: func(request *project.DeleteProjectRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + request.Id = projectID + return creationDate, time.Time{} + }, + req: &project.DeleteProjectRequest{}, + wantDeletionDate: true, + }, { name: "organization owner, other org", ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), diff --git a/internal/api/grpc/project/v2beta/integration/query_test.go b/internal/api/grpc/project/v2beta/integration/query_test.go index 517f103628..fc153f0130 100644 --- a/internal/api/grpc/project/v2beta/integration/query_test.go +++ b/internal/api/grpc/project/v2beta/integration/query_test.go @@ -148,6 +148,14 @@ func TestServer_GetProject(t *testing.T) { func TestServer_ListProjects(t *testing.T) { iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + + userResp := instance.CreateMachineUser(iamOwnerCtx) + patResp := instance.CreatePersonalAccessToken(iamOwnerCtx, userResp.GetUserId()) + projectResp := createProject(iamOwnerCtx, instance, t, instance.DefaultOrg.GetId(), false, false) + instance.CreateProjectMembership(t, iamOwnerCtx, projectResp.GetId(), userResp.GetUserId()) + grantedProjectResp := createGrantedProject(iamOwnerCtx, instance, t, projectResp) + projectOwnerCtx := integration.WithAuthorizationToken(CTX, patResp.Token) + type args struct { ctx context.Context dep func(*project.ListProjectsRequest, *project.ListProjectsResponse) @@ -370,6 +378,39 @@ func TestServer_ListProjects(t *testing.T) { }, }, }, + { + name: "list multiple id, limited permissions, project owner", + args: args{ + ctx: projectOwnerCtx, + dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) { + orgID := instance.DefaultOrg.GetId() + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + resp1 := createProject(iamOwnerCtx, instance, t, orgResp.GetOrganizationId(), false, false) + resp2 := createProject(iamOwnerCtx, instance, t, orgID, true, false) + resp3 := createProject(iamOwnerCtx, instance, t, orgResp.GetOrganizationId(), false, true) + request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &project.InProjectIDsFilter{ + ProjectIds: []string{resp1.GetId(), resp2.GetId(), resp3.GetId(), projectResp.GetId()}, + }, + } + response.Projects[0] = grantedProjectResp + response.Projects[1] = projectResp + }, + req: &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{{}}, + }, + }, + want: &project.ListProjectsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 5, + AppliedLimit: 100, + }, + Projects: []*project.Project{ + {}, + {}, + }, + }, + }, { name: "list project and granted projects", args: args{ @@ -462,6 +503,51 @@ func TestServer_ListProjects(t *testing.T) { }, }, }, + { + name: "list granted project, project id", + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) { + orgID := instance.DefaultOrg.GetId() + + orgName := gofakeit.AppName() + projectName := gofakeit.AppName() + orgResp := instance.CreateOrganization(iamOwnerCtx, orgName, gofakeit.Email()) + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), projectName, true, true) + projectGrantResp := instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), orgID) + request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &project.InProjectIDsFilter{ProjectIds: []string{projectResp.GetId()}}, + } + response.Projects[0] = &project.Project{ + Id: projectResp.GetId(), + Name: projectName, + OrganizationId: orgResp.GetOrganizationId(), + CreationDate: projectGrantResp.GetCreationDate(), + ChangeDate: projectGrantResp.GetCreationDate(), + State: 1, + ProjectRoleAssertion: false, + ProjectAccessRequired: true, + AuthorizationRequired: true, + PrivateLabelingSetting: project.PrivateLabelingSetting_PRIVATE_LABELING_SETTING_UNSPECIFIED, + GrantedOrganizationId: gu.Ptr(orgID), + GrantedOrganizationName: gu.Ptr(instance.DefaultOrg.GetName()), + GrantedState: 1, + } + }, + req: &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{{}}, + }, + }, + want: &project.ListProjectsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 2, + AppliedLimit: 100, + }, + Projects: []*project.Project{ + {}, + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -791,6 +877,53 @@ func TestServer_ListProjects_PermissionV2(t *testing.T) { }, }, }, + // TODO: correct when permission check is added for project grants https://github.com/zitadel/zitadel/issues/9972 + { + name: "list granted project, project id", + args: args{ + ctx: instancePermissionV2.WithAuthorization(CTX, integration.UserTypeOrgOwner), + dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) { + orgID := instancePermissionV2.DefaultOrg.GetId() + + orgName := gofakeit.AppName() + projectName := gofakeit.AppName() + orgResp := instancePermissionV2.CreateOrganization(iamOwnerCtx, orgName, gofakeit.Email()) + projectResp := instancePermissionV2.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), projectName, true, true) + // projectGrantResp := + instancePermissionV2.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), orgID) + request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &project.InProjectIDsFilter{ProjectIds: []string{projectResp.GetId()}}, + } + /* + response.Projects[0] = &project.Project{ + Id: projectResp.GetId(), + Name: projectName, + OrganizationId: orgResp.GetOrganizationId(), + CreationDate: projectGrantResp.GetCreationDate(), + ChangeDate: projectGrantResp.GetCreationDate(), + State: 1, + ProjectRoleAssertion: false, + ProjectAccessRequired: true, + AuthorizationRequired: true, + PrivateLabelingSetting: project.PrivateLabelingSetting_PRIVATE_LABELING_SETTING_UNSPECIFIED, + GrantedOrganizationId: gu.Ptr(orgID), + GrantedOrganizationName: gu.Ptr(instancePermissionV2.DefaultOrg.GetName()), + GrantedState: 1, + } + */ + }, + req: &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{{}}, + }, + }, + want: &project.ListProjectsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 0, + AppliedLimit: 100, + }, + Projects: []*project.Project{}, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -865,6 +998,14 @@ func assertPaginationResponse(t *assert.CollectT, expected *filter.PaginationRes func TestServer_ListProjectGrants(t *testing.T) { iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + + userResp := instance.CreateMachineUser(iamOwnerCtx) + patResp := instance.CreatePersonalAccessToken(iamOwnerCtx, userResp.GetUserId()) + projectResp := createProject(iamOwnerCtx, instance, t, instance.DefaultOrg.GetId(), false, false) + projectGrantResp := createProjectGrant(iamOwnerCtx, instance, t, instance.DefaultOrg.GetId(), projectResp.GetId(), projectResp.GetName()) + instance.CreateProjectGrantMembership(t, iamOwnerCtx, projectResp.GetId(), projectGrantResp.GetGrantedOrganizationId(), userResp.GetUserId()) + projectGrantOwnerCtx := integration.WithAuthorizationToken(CTX, patResp.Token) + type args struct { ctx context.Context dep func(*project.ListProjectGrantsRequest, *project.ListProjectGrantsResponse) @@ -1071,7 +1212,46 @@ func TestServer_ListProjectGrants(t *testing.T) { {}, }, }, - }, { + }, + { + name: "list multiple id, limited permissions, project grant owner", + args: args{ + ctx: projectGrantOwnerCtx, + dep: func(request *project.ListProjectGrantsRequest, response *project.ListProjectGrantsResponse) { + name1 := gofakeit.AppName() + name2 := gofakeit.AppName() + name3 := gofakeit.AppName() + orgID := instance.DefaultOrg.GetId() + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + project1Resp := instance.CreateProject(iamOwnerCtx, t, orgID, name1, false, false) + project2Resp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), name2, false, false) + project3Resp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), name3, false, false) + request.Filters[0].Filter = &project.ProjectGrantSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &project.InProjectIDsFilter{ + ProjectIds: []string{project1Resp.GetId(), project2Resp.GetId(), project3Resp.GetId(), projectResp.GetId()}, + }, + } + + createProjectGrant(iamOwnerCtx, instance, t, orgID, project1Resp.GetId(), name1) + createProjectGrant(iamOwnerCtx, instance, t, orgResp.GetOrganizationId(), project2Resp.GetId(), name2) + createProjectGrant(iamOwnerCtx, instance, t, orgResp.GetOrganizationId(), project3Resp.GetId(), name3) + response.ProjectGrants[0] = projectGrantResp + }, + req: &project.ListProjectGrantsRequest{ + Filters: []*project.ProjectGrantSearchFilter{{}}, + }, + }, + want: &project.ListProjectGrantsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 4, + AppliedLimit: 100, + }, + ProjectGrants: []*project.ProjectGrant{ + {}, + }, + }, + }, + { name: "list single id with role", args: args{ ctx: iamOwnerCtx, diff --git a/internal/api/grpc/project/v2beta/project_grant.go b/internal/api/grpc/project/v2beta/project_grant.go index c1c20d9cbc..6c3b195c66 100644 --- a/internal/api/grpc/project/v2beta/project_grant.go +++ b/internal/api/grpc/project/v2beta/project_grant.go @@ -56,13 +56,13 @@ func projectGrantUpdateToCommand(req *project_pb.UpdateProjectGrantRequest) *com ObjectRoot: models.ObjectRoot{ AggregateID: req.ProjectId, }, - GrantID: req.GrantedOrganizationId, - RoleKeys: req.RoleKeys, + GrantedOrgID: req.GrantedOrganizationId, + RoleKeys: req.RoleKeys, } } func (s *Server) DeactivateProjectGrant(ctx context.Context, req *project_pb.DeactivateProjectGrantRequest) (*project_pb.DeactivateProjectGrantResponse, error) { - details, err := s.command.DeactivateProjectGrant(ctx, req.ProjectId, req.GrantedOrganizationId, "") + details, err := s.command.DeactivateProjectGrant(ctx, req.ProjectId, "", req.GrantedOrganizationId, "") if err != nil { return nil, err } @@ -76,7 +76,7 @@ func (s *Server) DeactivateProjectGrant(ctx context.Context, req *project_pb.Dea } func (s *Server) ActivateProjectGrant(ctx context.Context, req *project_pb.ActivateProjectGrantRequest) (*project_pb.ActivateProjectGrantResponse, error) { - details, err := s.command.ReactivateProjectGrant(ctx, req.ProjectId, req.GrantedOrganizationId, "") + details, err := s.command.ReactivateProjectGrant(ctx, req.ProjectId, "", req.GrantedOrganizationId, "") if err != nil { return nil, err } @@ -94,7 +94,7 @@ func (s *Server) DeleteProjectGrant(ctx context.Context, req *project_pb.DeleteP if err != nil { return nil, err } - details, err := s.command.RemoveProjectGrant(ctx, req.ProjectId, req.GrantedOrganizationId, "", userGrantIDs...) + details, err := s.command.DeleteProjectGrant(ctx, req.ProjectId, "", req.GrantedOrganizationId, "", userGrantIDs...) if err != nil { return nil, err } diff --git a/internal/command/permission_checks.go b/internal/command/permission_checks.go index 253b6ee72a..6bfeaae219 100644 --- a/internal/command/permission_checks.go +++ b/internal/command/permission_checks.go @@ -68,6 +68,20 @@ func (c *Commands) checkPermissionUpdateProject(ctx context.Context, resourceOwn return c.newPermissionCheck(ctx, domain.PermissionProjectWrite, project.AggregateType)(resourceOwner, projectID) } -func (c *Commands) checkPermissionWriteProjectGrant(ctx context.Context, resourceOwner, projectGrantID string) error { - return c.newPermissionCheck(ctx, domain.PermissionProjectGrantWrite, project.AggregateType)(resourceOwner, projectGrantID) +func (c *Commands) checkPermissionUpdateProjectGrant(ctx context.Context, resourceOwner, projectID, projectGrantID string) (err error) { + if err := c.newPermissionCheck(ctx, domain.PermissionProjectGrantWrite, project.AggregateType)(resourceOwner, projectGrantID); err != nil { + if err := c.newPermissionCheck(ctx, domain.PermissionProjectGrantWrite, project.AggregateType)(resourceOwner, projectID); err != nil { + return err + } + } + return nil +} + +func (c *Commands) checkPermissionDeleteProjectGrant(ctx context.Context, resourceOwner, projectID, projectGrantID string) (err error) { + if err := c.newPermissionCheck(ctx, domain.PermissionProjectGrantDelete, project.AggregateType)(resourceOwner, projectGrantID); err != nil { + if err := c.newPermissionCheck(ctx, domain.PermissionProjectGrantDelete, project.AggregateType)(resourceOwner, projectID); err != nil { + return err + } + } + return nil } diff --git a/internal/command/project_grant.go b/internal/command/project_grant.go index 763ea7ab67..b613974b7e 100644 --- a/internal/command/project_grant.go +++ b/internal/command/project_grant.go @@ -58,11 +58,11 @@ func (c *Commands) AddProjectGrant(ctx context.Context, grant *AddProjectGrant) if grant.ResourceOwner == "" { grant.ResourceOwner = projectResourceOwner } - if err := c.checkPermissionWriteProjectGrant(ctx, grant.ResourceOwner, grant.GrantID); err != nil { + if err := c.checkPermissionUpdateProjectGrant(ctx, grant.ResourceOwner, grant.AggregateID, grant.GrantID); err != nil { return nil, err } - wm := NewProjectGrantWriteModel(grant.GrantID, grant.AggregateID, grant.ResourceOwner) + wm := NewProjectGrantWriteModel(grant.GrantID, grant.GrantedOrgID, grant.AggregateID, grant.ResourceOwner) // error if provided resourceowner is not equal to the resourceowner of the project or the project grant is for the same organization if projectResourceOwner != wm.ResourceOwner || wm.ResourceOwner == grant.GrantedOrgID { return nil, zerrors.ThrowPreconditionFailed(nil, "PROJECT-ckUpbvboAH", "Errors.Project.Grant.Invalid") @@ -83,19 +83,24 @@ func (c *Commands) AddProjectGrant(ctx context.Context, grant *AddProjectGrant) type ChangeProjectGrant struct { es_models.ObjectRoot - GrantID string - RoleKeys []string + GrantID string + GrantedOrgID string + RoleKeys []string } func (c *Commands) ChangeProjectGrant(ctx context.Context, grant *ChangeProjectGrant, cascadeUserGrantIDs ...string) (_ *domain.ObjectDetails, err error) { - if grant.GrantID == "" { + if grant.GrantID == "" && grant.GrantedOrgID == "" { return nil, zerrors.ThrowInvalidArgument(nil, "PROJECT-1j83s", "Errors.IDMissing") } - existingGrant, err := c.projectGrantWriteModelByID(ctx, grant.GrantID, grant.AggregateID, grant.ResourceOwner) + existingGrant, err := c.projectGrantWriteModelByID(ctx, grant.GrantID, grant.GrantedOrgID, grant.AggregateID, grant.ResourceOwner) if err != nil { return nil, err } - if err := c.checkPermissionWriteProjectGrant(ctx, existingGrant.ResourceOwner, existingGrant.GrantID); err != nil { + if !existingGrant.State.Exists() { + return nil, zerrors.ThrowNotFound(nil, "PROJECT-D8JxR", "Errors.Project.Grant.NotFound") + } + + if err := c.checkPermissionUpdateProjectGrant(ctx, existingGrant.ResourceOwner, existingGrant.AggregateID, existingGrant.GrantID); err != nil { return nil, err } projectResourceOwner, err := c.checkProjectGrantPreCondition(ctx, existingGrant.AggregateID, existingGrant.GrantedOrgID, existingGrant.ResourceOwner, grant.RoleKeys) @@ -152,12 +157,12 @@ func (c *Commands) ChangeProjectGrant(ctx context.Context, grant *ChangeProjectG } func (c *Commands) removeRoleFromProjectGrant(ctx context.Context, projectAgg *eventstore.Aggregate, projectID, projectGrantID, roleKey string, cascade bool) (_ eventstore.Command, _ *ProjectGrantWriteModel, err error) { - existingProjectGrant, err := c.projectGrantWriteModelByID(ctx, projectGrantID, projectID, "") + existingProjectGrant, err := c.projectGrantWriteModelByID(ctx, projectGrantID, "", projectID, "") if err != nil { return nil, nil, err } - if existingProjectGrant.State == domain.ProjectGrantStateUnspecified || existingProjectGrant.State == domain.ProjectGrantStateRemoved { - return nil, nil, zerrors.ThrowNotFound(nil, "COMMAND-3M9sd", "Errors.Project.Grant.NotFound") + if !existingProjectGrant.State.Exists() { + return nil, nil, zerrors.ThrowNotFound(nil, "PROJECT-D8JxR", "Errors.Project.Grant.NotFound") } keyExists := false for i, key := range existingProjectGrant.RoleKeys { @@ -172,7 +177,7 @@ func (c *Commands) removeRoleFromProjectGrant(ctx context.Context, projectAgg *e if !keyExists { return nil, nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-5m8g9", "Errors.Project.Grant.RoleKeyNotFound") } - changedProjectGrant := NewProjectGrantWriteModel(projectGrantID, projectID, existingProjectGrant.ResourceOwner) + changedProjectGrant := NewProjectGrantWriteModel(projectGrantID, projectID, "", existingProjectGrant.ResourceOwner) if cascade { return project.NewGrantCascadeChangedEvent(ctx, projectAgg, projectGrantID, existingProjectGrant.RoleKeys), changedProjectGrant, nil @@ -181,8 +186,8 @@ func (c *Commands) removeRoleFromProjectGrant(ctx context.Context, projectAgg *e return project.NewGrantChangedEvent(ctx, projectAgg, projectGrantID, existingProjectGrant.RoleKeys), changedProjectGrant, nil } -func (c *Commands) DeactivateProjectGrant(ctx context.Context, projectID, grantID, resourceOwner string) (details *domain.ObjectDetails, err error) { - if grantID == "" || projectID == "" { +func (c *Commands) DeactivateProjectGrant(ctx context.Context, projectID, grantID, grantedOrgID, resourceOwner string) (details *domain.ObjectDetails, err error) { + if (grantID == "" && grantedOrgID == "") || projectID == "" { return details, zerrors.ThrowInvalidArgument(nil, "PROJECT-p0s4V", "Errors.IDMissing") } @@ -191,10 +196,13 @@ func (c *Commands) DeactivateProjectGrant(ctx context.Context, projectID, grantI return nil, err } - existingGrant, err := c.projectGrantWriteModelByID(ctx, grantID, projectID, resourceOwner) + existingGrant, err := c.projectGrantWriteModelByID(ctx, grantID, grantedOrgID, projectID, resourceOwner) if err != nil { return details, err } + if !existingGrant.State.Exists() { + return nil, zerrors.ThrowNotFound(nil, "PROJECT-D8JxR", "Errors.Project.Grant.NotFound") + } // error if provided resourceowner is not equal to the resourceowner of the project if projectResourceOwner != existingGrant.ResourceOwner { return nil, zerrors.ThrowPreconditionFailed(nil, "PROJECT-0l10S9OmZV", "Errors.Project.Grant.Invalid") @@ -207,13 +215,13 @@ func (c *Commands) DeactivateProjectGrant(ctx context.Context, projectID, grantI if existingGrant.State != domain.ProjectGrantStateActive { return details, zerrors.ThrowPreconditionFailed(nil, "PROJECT-47fu8", "Errors.Project.Grant.NotActive") } - if err := c.checkPermissionWriteProjectGrant(ctx, existingGrant.ResourceOwner, existingGrant.GrantID); err != nil { + if err := c.checkPermissionUpdateProjectGrant(ctx, existingGrant.ResourceOwner, existingGrant.AggregateID, existingGrant.GrantID); err != nil { return nil, err } pushedEvents, err := c.eventstore.Push(ctx, project.NewGrantDeactivateEvent(ctx, ProjectAggregateFromWriteModelWithCTX(ctx, &existingGrant.WriteModel), - grantID, + existingGrant.GrantID, ), ) if err != nil { @@ -226,8 +234,8 @@ func (c *Commands) DeactivateProjectGrant(ctx context.Context, projectID, grantI return writeModelToObjectDetails(&existingGrant.WriteModel), nil } -func (c *Commands) ReactivateProjectGrant(ctx context.Context, projectID, grantID, resourceOwner string) (details *domain.ObjectDetails, err error) { - if grantID == "" || projectID == "" { +func (c *Commands) ReactivateProjectGrant(ctx context.Context, projectID, grantID, grantedOrgID, resourceOwner string) (details *domain.ObjectDetails, err error) { + if (grantID == "" && grantedOrgID == "") || projectID == "" { return details, zerrors.ThrowInvalidArgument(nil, "PROJECT-p0s4V", "Errors.IDMissing") } @@ -236,10 +244,13 @@ func (c *Commands) ReactivateProjectGrant(ctx context.Context, projectID, grantI return nil, err } - existingGrant, err := c.projectGrantWriteModelByID(ctx, grantID, projectID, resourceOwner) + existingGrant, err := c.projectGrantWriteModelByID(ctx, grantID, grantedOrgID, projectID, resourceOwner) if err != nil { return details, err } + if !existingGrant.State.Exists() { + return nil, zerrors.ThrowNotFound(nil, "PROJECT-D8JxR", "Errors.Project.Grant.NotFound") + } // error if provided resourceowner is not equal to the resourceowner of the project if projectResourceOwner != existingGrant.ResourceOwner { return nil, zerrors.ThrowPreconditionFailed(nil, "PROJECT-byscAarAST", "Errors.Project.Grant.Invalid") @@ -252,13 +263,13 @@ func (c *Commands) ReactivateProjectGrant(ctx context.Context, projectID, grantI if existingGrant.State != domain.ProjectGrantStateInactive { return details, zerrors.ThrowPreconditionFailed(nil, "PROJECT-47fu8", "Errors.Project.Grant.NotInactive") } - if err := c.checkPermissionWriteProjectGrant(ctx, existingGrant.ResourceOwner, existingGrant.GrantID); err != nil { + if err := c.checkPermissionUpdateProjectGrant(ctx, existingGrant.ResourceOwner, existingGrant.AggregateID, existingGrant.GrantID); err != nil { return nil, err } pushedEvents, err := c.eventstore.Push(ctx, project.NewGrantReactivatedEvent(ctx, ProjectAggregateFromWriteModelWithCTX(ctx, &existingGrant.WriteModel), - grantID, + existingGrant.GrantID, ), ) if err != nil { @@ -271,25 +282,25 @@ func (c *Commands) ReactivateProjectGrant(ctx context.Context, projectID, grantI return writeModelToObjectDetails(&existingGrant.WriteModel), nil } +// Deprecated: use commands.DeleteProjectGrant func (c *Commands) RemoveProjectGrant(ctx context.Context, projectID, grantID, resourceOwner string, cascadeUserGrantIDs ...string) (details *domain.ObjectDetails, err error) { if grantID == "" || projectID == "" { return details, zerrors.ThrowInvalidArgument(nil, "PROJECT-1m9fJ", "Errors.IDMissing") } - existingGrant, err := c.projectGrantWriteModelByID(ctx, grantID, projectID, resourceOwner) + existingGrant, err := c.projectGrantWriteModelByID(ctx, grantID, "", projectID, resourceOwner) if err != nil { return details, err } - // return if project grant does not exist, or was removed already if !existingGrant.State.Exists() { - return writeModelToObjectDetails(&existingGrant.WriteModel), nil + return nil, zerrors.ThrowNotFound(nil, "PROJECT-D8JxR", "Errors.Project.Grant.NotFound") } - if err := c.checkPermissionDeleteProjectGrant(ctx, existingGrant.ResourceOwner, existingGrant.GrantID); err != nil { + if err := c.checkPermissionDeleteProjectGrant(ctx, existingGrant.ResourceOwner, existingGrant.AggregateID, existingGrant.GrantID); err != nil { return nil, err } events := make([]eventstore.Command, 0) events = append(events, project.NewGrantRemovedEvent(ctx, ProjectAggregateFromWriteModelWithCTX(ctx, &existingGrant.WriteModel), - grantID, + existingGrant.GrantID, existingGrant.GrantedOrgID, ), ) @@ -297,7 +308,7 @@ func (c *Commands) RemoveProjectGrant(ctx context.Context, projectID, grantID, r for _, userGrantID := range cascadeUserGrantIDs { event, _, err := c.removeUserGrant(ctx, userGrantID, "", true) if err != nil { - logging.LogWithFields("COMMAND-3m8sG", "usergrantid", grantID).WithError(err).Warn("could not cascade remove user grant") + logging.WithFields("id", "COMMAND-3m8sG", "usergrantid", grantID).WithError(err).Warn("could not cascade remove user grant") continue } events = append(events, event) @@ -313,24 +324,57 @@ func (c *Commands) RemoveProjectGrant(ctx context.Context, projectID, grantID, r return writeModelToObjectDetails(&existingGrant.WriteModel), nil } -func (c *Commands) checkPermissionDeleteProjectGrant(ctx context.Context, resourceOwner, projectGrantID string) error { - return c.checkPermission(ctx, domain.PermissionProjectGrantDelete, resourceOwner, projectGrantID) +func (c *Commands) DeleteProjectGrant(ctx context.Context, projectID, grantID, grantedOrgID, resourceOwner string, cascadeUserGrantIDs ...string) (details *domain.ObjectDetails, err error) { + if (grantID == "" && grantedOrgID == "") || projectID == "" { + return details, zerrors.ThrowInvalidArgument(nil, "PROJECT-1m9fJ", "Errors.IDMissing") + } + existingGrant, err := c.projectGrantWriteModelByID(ctx, grantID, grantedOrgID, projectID, resourceOwner) + if err != nil { + return details, err + } + // return if project grant does not exist, or was removed already + if !existingGrant.State.Exists() { + return writeModelToObjectDetails(&existingGrant.WriteModel), nil + } + if err := c.checkPermissionDeleteProjectGrant(ctx, existingGrant.ResourceOwner, existingGrant.AggregateID, existingGrant.GrantID); err != nil { + return nil, err + } + events := make([]eventstore.Command, 0) + events = append(events, project.NewGrantRemovedEvent(ctx, + ProjectAggregateFromWriteModelWithCTX(ctx, &existingGrant.WriteModel), + existingGrant.GrantID, + existingGrant.GrantedOrgID, + ), + ) + + for _, userGrantID := range cascadeUserGrantIDs { + event, _, err := c.removeUserGrant(ctx, userGrantID, "", true) + if err != nil { + logging.WithFields("id", "COMMAND-3m8sG", "usergrantid", grantID).WithError(err).Warn("could not cascade remove user grant") + continue + } + events = append(events, event) + } + pushedEvents, err := c.eventstore.Push(ctx, events...) + if err != nil { + return nil, err + } + err = AppendAndReduce(existingGrant, pushedEvents...) + if err != nil { + return nil, err + } + return writeModelToObjectDetails(&existingGrant.WriteModel), nil } -func (c *Commands) projectGrantWriteModelByID(ctx context.Context, grantID, projectID, resourceOwner string) (member *ProjectGrantWriteModel, err error) { +func (c *Commands) projectGrantWriteModelByID(ctx context.Context, grantID, grantedOrgID, projectID, resourceOwner string) (member *ProjectGrantWriteModel, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - writeModel := NewProjectGrantWriteModel(grantID, projectID, resourceOwner) + writeModel := NewProjectGrantWriteModel(grantID, grantedOrgID, projectID, resourceOwner) err = c.eventstore.FilterToQueryReducer(ctx, writeModel) if err != nil { return nil, err } - - if writeModel.State == domain.ProjectGrantStateUnspecified || writeModel.State == domain.ProjectGrantStateRemoved { - return nil, zerrors.ThrowNotFound(nil, "PROJECT-D8JxR", "Errors.Project.Grant.NotFound") - } - return writeModel, nil } diff --git a/internal/command/project_grant_model.go b/internal/command/project_grant_model.go index a8c1fe2850..15950d4f3d 100644 --- a/internal/command/project_grant_model.go +++ b/internal/command/project_grant_model.go @@ -16,13 +16,14 @@ type ProjectGrantWriteModel struct { State domain.ProjectGrantState } -func NewProjectGrantWriteModel(grantID, projectID, resourceOwner string) *ProjectGrantWriteModel { +func NewProjectGrantWriteModel(grantID, grantedOrgID, projectID, resourceOwner string) *ProjectGrantWriteModel { return &ProjectGrantWriteModel{ WriteModel: eventstore.WriteModel{ AggregateID: projectID, ResourceOwner: resourceOwner, }, - GrantID: grantID, + GrantID: grantID, + GrantedOrgID: grantedOrgID, } } @@ -30,27 +31,28 @@ func (wm *ProjectGrantWriteModel) AppendEvents(events ...eventstore.Event) { for _, event := range events { switch e := event.(type) { case *project.GrantAddedEvent: - if e.GrantID == wm.GrantID { + if (wm.GrantID != "" && e.GrantID == wm.GrantID) || + (wm.GrantedOrgID != "" && e.GrantedOrgID == wm.GrantedOrgID) { wm.WriteModel.AppendEvents(e) } case *project.GrantChangedEvent: - if e.GrantID == wm.GrantID { + if wm.GrantID != "" && e.GrantID == wm.GrantID { wm.WriteModel.AppendEvents(e) } case *project.GrantCascadeChangedEvent: - if e.GrantID == wm.GrantID { + if wm.GrantID != "" && e.GrantID == wm.GrantID { wm.WriteModel.AppendEvents(e) } case *project.GrantDeactivateEvent: - if e.GrantID == wm.GrantID { + if wm.GrantID != "" && e.GrantID == wm.GrantID { wm.WriteModel.AppendEvents(e) } case *project.GrantReactivatedEvent: - if e.GrantID == wm.GrantID { + if wm.GrantID != "" && e.GrantID == wm.GrantID { wm.WriteModel.AppendEvents(e) } case *project.GrantRemovedEvent: - if e.GrantID == wm.GrantID { + if wm.GrantID != "" && e.GrantID == wm.GrantID { wm.WriteModel.AppendEvents(e) } case *project.ProjectRemovedEvent: @@ -114,18 +116,20 @@ func (wm *ProjectGrantWriteModel) Query() *eventstore.SearchQueryBuilder { type ProjectGrantPreConditionReadModel struct { eventstore.WriteModel - ProjectID string - GrantedOrgID string - ProjectExists bool - GrantedOrgExists bool - ExistingRoleKeys []string + ProjectResourceOwner string + ProjectID string + GrantedOrgID string + ProjectExists bool + GrantedOrgExists bool + ExistingRoleKeys []string } func NewProjectGrantPreConditionReadModel(projectID, grantedOrgID, resourceOwner string) *ProjectGrantPreConditionReadModel { return &ProjectGrantPreConditionReadModel{ - WriteModel: eventstore.WriteModel{ResourceOwner: resourceOwner}, - ProjectID: projectID, - GrantedOrgID: grantedOrgID, + WriteModel: eventstore.WriteModel{}, + ProjectResourceOwner: resourceOwner, + ProjectID: projectID, + GrantedOrgID: grantedOrgID, } } @@ -133,26 +137,26 @@ func (wm *ProjectGrantPreConditionReadModel) Reduce() error { for _, event := range wm.Events { switch e := event.(type) { case *project.ProjectAddedEvent: - if wm.ResourceOwner == "" { - wm.ResourceOwner = e.Aggregate().ResourceOwner + if wm.ProjectResourceOwner == "" { + wm.ProjectResourceOwner = e.Aggregate().ResourceOwner } - if wm.ResourceOwner != e.Aggregate().ResourceOwner { + if wm.ProjectResourceOwner != e.Aggregate().ResourceOwner { continue } wm.ProjectExists = true case *project.ProjectRemovedEvent: - if wm.ResourceOwner != e.Aggregate().ResourceOwner { + if wm.ProjectResourceOwner != e.Aggregate().ResourceOwner { continue } - wm.ResourceOwner = "" + wm.ProjectResourceOwner = "" wm.ProjectExists = false case *project.RoleAddedEvent: - if e.Aggregate().ResourceOwner != wm.ResourceOwner { + if e.Aggregate().ResourceOwner != wm.ProjectResourceOwner { continue } wm.ExistingRoleKeys = append(wm.ExistingRoleKeys, e.Key) case *project.RoleRemovedEvent: - if e.Aggregate().ResourceOwner != wm.ResourceOwner { + if e.Aggregate().ResourceOwner != wm.ProjectResourceOwner { continue } for i, key := range wm.ExistingRoleKeys { @@ -175,12 +179,6 @@ func (wm *ProjectGrantPreConditionReadModel) Reduce() error { func (wm *ProjectGrantPreConditionReadModel) Query() *eventstore.SearchQueryBuilder { query := eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). AddQuery(). - AggregateTypes(org.AggregateType). - AggregateIDs(wm.GrantedOrgID). - EventTypes( - org.OrgAddedEventType, - org.OrgRemovedEventType). - Or(). AggregateTypes(project.AggregateType). AggregateIDs(wm.ProjectID). EventTypes( @@ -188,6 +186,12 @@ func (wm *ProjectGrantPreConditionReadModel) Query() *eventstore.SearchQueryBuil project.ProjectRemovedType, project.RoleAddedType, project.RoleRemovedType). + Or(). + AggregateTypes(org.AggregateType). + AggregateIDs(wm.GrantedOrgID). + EventTypes( + org.OrgAddedEventType, + org.OrgRemovedEventType). Builder() return query diff --git a/internal/command/project_grant_test.go b/internal/command/project_grant_test.go index f1befa0de2..7a3bb98e7d 100644 --- a/internal/command/project_grant_test.go +++ b/internal/command/project_grant_test.go @@ -720,6 +720,76 @@ func TestCommandSide_ChangeProjectGrant(t *testing.T) { }, }, }, + { + name: "projectgrant only added roles, grantedOrgID, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher(project.NewGrantAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "grantedorg1", + []string{"key1"}, + )), + ), + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectname1", true, true, true, + domain.PrivateLabelingSettingUnspecified, + ), + ), + eventFromEventPusher( + org.NewOrgAddedEvent(context.Background(), + &org.NewAggregate("grantedorg1").Aggregate, + "granted org", + ), + ), + eventFromEventPusher( + project.NewRoleAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "key1", + "key", + "", + ), + ), + eventFromEventPusher( + project.NewRoleAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "key2", + "key2", + "", + ), + ), + ), + expectPush( + project.NewGrantChangedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + []string{"key1", "key2"}, + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + projectGrant: &ChangeProjectGrant{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + ResourceOwner: "org1", + }, + GrantedOrgID: "grantedorg1", + RoleKeys: []string{"key1", "key2"}, + }, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, { name: "projectgrant remove roles, usergrant not found, ok", fields: fields{ @@ -907,6 +977,7 @@ func TestCommandSide_DeactivateProjectGrant(t *testing.T) { ctx context.Context projectID string grantID string + grantedOrgID string resourceOwner string } type res struct { @@ -1076,6 +1147,48 @@ func TestCommandSide_DeactivateProjectGrant(t *testing.T) { }, }, }, + { + name: "projectgrant deactivate, grantedOrgID, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectname1", true, true, true, + domain.PrivateLabelingSettingUnspecified, + ), + ), + ), + expectFilter( + eventFromEventPusher(project.NewGrantAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "grantedorg1", + []string{"key1"}, + )), + ), + expectPush( + project.NewGrantDeactivateEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + grantedOrgID: "grantedorg1", + resourceOwner: "org1", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -1083,7 +1196,7 @@ func TestCommandSide_DeactivateProjectGrant(t *testing.T) { eventstore: tt.fields.eventstore(t), checkPermission: tt.fields.checkPermission, } - got, err := r.DeactivateProjectGrant(tt.args.ctx, tt.args.projectID, tt.args.grantID, tt.args.resourceOwner) + got, err := r.DeactivateProjectGrant(tt.args.ctx, tt.args.projectID, tt.args.grantID, tt.args.grantedOrgID, tt.args.resourceOwner) if tt.res.err == nil { assert.NoError(t, err) } @@ -1106,6 +1219,7 @@ func TestCommandSide_ReactivateProjectGrant(t *testing.T) { ctx context.Context projectID string grantID string + grantedOrgID string resourceOwner string } type res struct { @@ -1275,6 +1389,52 @@ func TestCommandSide_ReactivateProjectGrant(t *testing.T) { }, }, }, + { + name: "projectgrant reactivate, grantedOrgID, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectname1", true, true, true, + domain.PrivateLabelingSettingUnspecified, + ), + ), + ), + expectFilter( + eventFromEventPusher(project.NewGrantAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "grantedorg1", + []string{"key1"}, + )), + eventFromEventPusher(project.NewGrantDeactivateEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + )), + ), + expectPush( + project.NewGrantReactivatedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + grantedOrgID: "grantedorg1", + resourceOwner: "org1", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -1282,7 +1442,7 @@ func TestCommandSide_ReactivateProjectGrant(t *testing.T) { eventstore: tt.fields.eventstore(t), checkPermission: tt.fields.checkPermission, } - got, err := r.ReactivateProjectGrant(tt.args.ctx, tt.args.projectID, tt.args.grantID, tt.args.resourceOwner) + got, err := r.ReactivateProjectGrant(tt.args.ctx, tt.args.projectID, tt.args.grantID, tt.args.grantedOrgID, tt.args.resourceOwner) if tt.res.err == nil { assert.NoError(t, err) } @@ -1536,3 +1696,283 @@ func TestCommandSide_RemoveProjectGrant(t *testing.T) { }) } } + +func TestCommandSide_DeleteProjectGrant(t *testing.T) { + type fields struct { + eventstore func(t *testing.T) *eventstore.Eventstore + checkPermission domain.PermissionCheck + } + type args struct { + ctx context.Context + projectID string + grantID string + grantedOrgID string + resourceOwner string + cascadeUserGrantIDs []string + } + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "missing projectid, invalid error", + fields: fields{ + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + grantID: "projectgrant1", + resourceOwner: "org1", + }, + res: res{ + err: zerrors.IsErrorInvalidArgument, + }, + }, + { + name: "missing grantid, invalid error", + fields: fields{ + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + resourceOwner: "org1", + }, + res: res{ + err: zerrors.IsErrorInvalidArgument, + }, + }, + { + name: "project already removed, precondition failed error", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher(project.NewGrantAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "grantedorg1", + []string{"key1"}, + )), + eventFromEventPusher( + project.NewProjectRemovedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectname1", + nil, + ), + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + grantID: "projectgrant1", + resourceOwner: "org1", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "projectgrant not existing, precondition error", + fields: fields{ + eventstore: expectEventstore( + expectFilter(), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + grantID: "projectgrant1", + resourceOwner: "org1", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "projectgrant remove, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher(project.NewGrantAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "grantedorg1", + []string{"key1"}, + )), + ), + expectPush( + project.NewGrantRemovedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "grantedorg1", + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + grantID: "projectgrant1", + resourceOwner: "org1", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "projectgrant remove, grantedOrgID, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher(project.NewGrantAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "grantedorg1", + []string{"key1"}, + )), + ), + expectPush( + project.NewGrantRemovedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "grantedorg1", + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + grantedOrgID: "grantedorg1", + resourceOwner: "org1", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "projectgrant remove, cascading usergrant not found, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher(project.NewGrantAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "grantedorg1", + []string{"key1"}, + )), + ), + expectFilter(), + expectPush( + project.NewGrantRemovedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "grantedorg1", + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + grantID: "projectgrant1", + resourceOwner: "org1", + cascadeUserGrantIDs: []string{"usergrant1"}, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "projectgrant remove with cascading usergrants, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher(project.NewGrantAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "grantedorg1", + []string{"key1"}, + )), + ), + expectFilter( + eventFromEventPusher(usergrant.NewUserGrantAddedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org1").Aggregate, + "user1", + "project1", + "projectgrant1", + []string{"key1"}))), + expectPush( + project.NewGrantRemovedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "grantedorg1", + ), + usergrant.NewUserGrantCascadeRemovedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org1").Aggregate, + "user1", + "project1", + "projectgrant1", + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + grantID: "projectgrant1", + resourceOwner: "org1", + cascadeUserGrantIDs: []string{"usergrant1"}, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore(t), + checkPermission: tt.fields.checkPermission, + } + got, err := r.DeleteProjectGrant(tt.args.ctx, tt.args.projectID, tt.args.grantID, tt.args.grantedOrgID, tt.args.resourceOwner, tt.args.cascadeUserGrantIDs...) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assertObjectDetails(t, tt.res.want, got) + } + }) + } +} diff --git a/internal/command/project_old.go b/internal/command/project_old.go index e1b6f02721..99d7dd2e34 100644 --- a/internal/command/project_old.go +++ b/internal/command/project_old.go @@ -94,5 +94,5 @@ func (c *Commands) checkProjectGrantPreConditionOld(ctx context.Context, project if domain.HasInvalidRoles(preConditions.ExistingRoleKeys, roles) { return "", zerrors.ThrowPreconditionFailed(err, "COMMAND-6m9gd", "Errors.Project.Role.NotFound") } - return preConditions.ResourceOwner, nil + return preConditions.ProjectResourceOwner, nil } diff --git a/internal/domain/roles.go b/internal/domain/roles.go index b6bf2ffadd..c40eef6120 100644 --- a/internal/domain/roles.go +++ b/internal/domain/roles.go @@ -16,6 +16,7 @@ const ( RoleIAMOwner = "IAM_OWNER" RoleProjectOwner = "PROJECT_OWNER" RoleProjectOwnerGlobal = "PROJECT_OWNER_GLOBAL" + RoleProjectGrantOwner = "PROJECT_GRANT_OWNER" RoleSelfManagementGlobal = "SELF_MANAGEMENT_GLOBAL" ) diff --git a/internal/integration/client.go b/internal/integration/client.go index 3bf794f5f6..838a728faf 100644 --- a/internal/integration/client.go +++ b/internal/integration/client.go @@ -269,6 +269,14 @@ func (i *Instance) CreateUserTypeMachine(ctx context.Context) *user_v2.CreateUse return resp } +func (i *Instance) CreatePersonalAccessToken(ctx context.Context, userID string) *user_v2.AddPersonalAccessTokenResponse { + resp, err := i.Client.UserV2.AddPersonalAccessToken(ctx, &user_v2.AddPersonalAccessTokenRequest{ + UserId: userID, + }) + logging.OnError(err).Panic("create pat") + return resp +} + // TriggerUserByID makes sure the user projection gets triggered after creation. func (i *Instance) TriggerUserByID(ctx context.Context, users ...string) { var wg sync.WaitGroup @@ -903,6 +911,16 @@ func (i *Instance) CreateProjectMembership(t *testing.T, ctx context.Context, pr require.NoError(t, err) } +func (i *Instance) CreateProjectGrantMembership(t *testing.T, ctx context.Context, projectID, grantID, userID string) { + _, err := i.Client.Mgmt.AddProjectGrantMember(ctx, &mgmt.AddProjectGrantMemberRequest{ + ProjectId: projectID, + GrantId: grantID, + UserId: userID, + Roles: []string{domain.RoleProjectGrantOwner}, + }) + require.NoError(t, err) +} + func (i *Instance) CreateTarget(ctx context.Context, t *testing.T, name, endpoint string, ty domain.TargetType, interrupt bool) *action.CreateTargetResponse { if name == "" { name = gofakeit.Name() diff --git a/internal/query/project.go b/internal/query/project.go index ab58bd11a8..728731f7cb 100644 --- a/internal/query/project.go +++ b/internal/query/project.go @@ -126,6 +126,10 @@ var ( name: "project_grant_resource_owner", table: grantedProjectsAlias, } + grantedProjectColumnGrantID = Column{ + name: projection.ProjectGrantColumnGrantID, + table: grantedProjectsAlias, + } grantedProjectColumnGrantedOrganization = Column{ name: projection.ProjectGrantColumnGrantedOrgID, table: grantedProjectsAlias, @@ -157,20 +161,6 @@ func projectCheckPermission(ctx context.Context, resourceOwner string, projectID return permissionCheck(ctx, domain.PermissionProjectRead, resourceOwner, projectID) } -func projectPermissionCheckV2(ctx context.Context, query sq.SelectBuilder, enabled bool, queries *ProjectAndGrantedProjectSearchQueries) sq.SelectBuilder { - if !enabled { - return query - } - join, args := PermissionClause( - ctx, - grantedProjectColumnResourceOwner, - domain.PermissionProjectRead, - SingleOrgPermissionOption(queries.Queries), - OwnedRowsPermissionOption(GrantedProjectColumnID), - ) - return query.JoinClause(join, args...) -} - type Project struct { ID string CreationDate time.Time @@ -277,6 +267,20 @@ func (q *ProjectAndGrantedProjectSearchQueries) toQuery(query sq.SelectBuilder) return query } +func projectPermissionCheckV2(ctx context.Context, query sq.SelectBuilder, enabled bool, queries *ProjectAndGrantedProjectSearchQueries) sq.SelectBuilder { + if !enabled { + return query + } + join, args := PermissionClause( + ctx, + grantedProjectColumnResourceOwner, + domain.PermissionProjectRead, + SingleOrgPermissionOption(queries.Queries), + WithProjectsPermissionOption(GrantedProjectColumnID), + ) + return query.JoinClause(join, args...) +} + func (q *Queries) SearchGrantedProjects(ctx context.Context, queries *ProjectAndGrantedProjectSearchQueries, permissionCheck domain.PermissionCheck) (*GrantedProjects, error) { permissionCheckV2 := PermissionV2(ctx, permissionCheck) projects, err := q.searchGrantedProjects(ctx, queries, permissionCheckV2) @@ -328,11 +332,11 @@ func NewGrantedProjectIDSearchQuery(ids []string) (SearchQuery, error) { } func NewGrantedProjectOrganizationIDSearchQuery(value string) (SearchQuery, error) { - project, err := NewTextQuery(grantedProjectColumnResourceOwner, value, TextEquals) + project, err := NewGrantedProjectResourceOwnerSearchQuery(value) if err != nil { return nil, err } - grant, err := NewTextQuery(grantedProjectColumnGrantedOrganization, value, TextEquals) + grant, err := NewGrantedProjectGrantedOrganizationIDSearchQuery(value) if err != nil { return nil, err } @@ -494,6 +498,9 @@ type GrantedProjects struct { func grantedProjectsCheckPermission(ctx context.Context, grantedProjects *GrantedProjects, permissionCheck domain.PermissionCheck) { grantedProjects.GrantedProjects = slices.DeleteFunc(grantedProjects.GrantedProjects, func(grantedProject *GrantedProject) bool { + if grantedProject.GrantedOrgID != "" { + return projectGrantCheckPermission(ctx, grantedProject.ResourceOwner, grantedProject.ProjectID, grantedProject.GrantID, grantedProject.GrantedOrgID, permissionCheck) != nil + } return projectCheckPermission(ctx, grantedProject.ResourceOwner, grantedProject.ProjectID, permissionCheck) != nil }, ) @@ -513,6 +520,7 @@ type GrantedProject struct { HasProjectCheck bool PrivateLabelingSetting domain.PrivateLabelingSetting + GrantID string GrantedOrgID string OrgName string ProjectGrantState domain.ProjectGrantState @@ -531,6 +539,7 @@ func prepareGrantedProjectsQuery() (sq.SelectBuilder, func(*sql.Rows) (*GrantedP grantedProjectColumnProjectRoleCheck.identifier(), grantedProjectColumnHasProjectCheck.identifier(), grantedProjectColumnPrivateLabelingSetting.identifier(), + grantedProjectColumnGrantID.identifier(), grantedProjectColumnGrantedOrganization.identifier(), grantedProjectColumnGrantedOrganizationName.identifier(), grantedProjectColumnGrantState.identifier(), @@ -541,6 +550,7 @@ func prepareGrantedProjectsQuery() (sq.SelectBuilder, func(*sql.Rows) (*GrantedP projects := make([]*GrantedProject, 0) var ( count uint64 + grantID = sql.NullString{} orgID = sql.NullString{} orgName = sql.NullString{} projectGrantState = sql.NullInt16{} @@ -559,6 +569,7 @@ func prepareGrantedProjectsQuery() (sq.SelectBuilder, func(*sql.Rows) (*GrantedP &grantedProject.ProjectRoleCheck, &grantedProject.HasProjectCheck, &grantedProject.PrivateLabelingSetting, + &grantID, &orgID, &orgName, &projectGrantState, @@ -567,6 +578,9 @@ func prepareGrantedProjectsQuery() (sq.SelectBuilder, func(*sql.Rows) (*GrantedP if err != nil { return nil, err } + if grantID.Valid { + grantedProject.GrantID = grantID.String + } if orgID.Valid { grantedProject.GrantedOrgID = orgID.String } @@ -614,6 +628,7 @@ func prepareProjects() string { ProjectColumnHasProjectCheck.identifier()+" AS "+grantedProjectColumnHasProjectCheck.name, ProjectColumnPrivateLabelingSetting.identifier()+" AS "+grantedProjectColumnPrivateLabelingSetting.name, "NULL::TEXT AS "+grantedProjectColumnGrantResourceOwner.name, + "NULL::TEXT AS "+grantedProjectColumnGrantID.name, "NULL::TEXT AS "+grantedProjectColumnGrantedOrganization.name, "NULL::TEXT AS "+grantedProjectColumnGrantedOrganizationName.name, "NULL::SMALLINT AS "+grantedProjectColumnGrantState.name, @@ -641,6 +656,7 @@ func prepareGrantedProjects() string { ProjectColumnHasProjectCheck.identifier()+" AS "+grantedProjectColumnHasProjectCheck.name, ProjectColumnPrivateLabelingSetting.identifier()+" AS "+grantedProjectColumnPrivateLabelingSetting.name, ProjectGrantColumnResourceOwner.identifier()+" AS "+grantedProjectColumnGrantResourceOwner.name, + ProjectGrantColumnGrantID.identifier()+" AS "+grantedProjectColumnGrantID.name, ProjectGrantColumnGrantedOrgID.identifier()+" AS "+grantedProjectColumnGrantedOrganization.name, ProjectGrantColumnGrantedOrgName.identifier()+" AS "+grantedProjectColumnGrantedOrganizationName.name, ProjectGrantColumnState.identifier()+" AS "+grantedProjectColumnGrantState.name, diff --git a/internal/query/project_grant.go b/internal/query/project_grant.go index a0dbd7c121..3093d26f30 100644 --- a/internal/query/project_grant.go +++ b/internal/query/project_grant.go @@ -108,15 +108,23 @@ type ProjectGrantSearchQueries struct { func projectGrantsCheckPermission(ctx context.Context, projectGrants *ProjectGrants, permissionCheck domain.PermissionCheck) { projectGrants.ProjectGrants = slices.DeleteFunc(projectGrants.ProjectGrants, func(projectGrant *ProjectGrant) bool { - return projectGrantCheckPermission(ctx, projectGrant.ResourceOwner, projectGrant.GrantID, permissionCheck) != nil + return projectGrantCheckPermission(ctx, projectGrant.ResourceOwner, projectGrant.ProjectID, projectGrant.GrantID, projectGrant.GrantedOrgID, permissionCheck) != nil }, ) } -func projectGrantCheckPermission(ctx context.Context, resourceOwner string, grantID string, permissionCheck domain.PermissionCheck) error { - return permissionCheck(ctx, domain.PermissionProjectGrantRead, resourceOwner, grantID) +func projectGrantCheckPermission(ctx context.Context, resourceOwner, projectID, grantID, grantedOrgID string, permissionCheck domain.PermissionCheck) error { + if err := permissionCheck(ctx, domain.PermissionProjectGrantRead, resourceOwner, grantID); err != nil { + if err := permissionCheck(ctx, domain.PermissionProjectGrantRead, grantedOrgID, grantID); err != nil { + if err := permissionCheck(ctx, domain.PermissionProjectGrantRead, resourceOwner, projectID); err != nil { + return err + } + } + } + return nil } +// TODO: add permission check on project grant level func projectGrantPermissionCheckV2(ctx context.Context, query sq.SelectBuilder, enabled bool, queries *ProjectGrantSearchQueries) sq.SelectBuilder { if !enabled { return query @@ -126,7 +134,6 @@ func projectGrantPermissionCheckV2(ctx context.Context, query sq.SelectBuilder, ProjectGrantColumnResourceOwner, domain.PermissionProjectGrantRead, SingleOrgPermissionOption(queries.Queries), - OwnedRowsPermissionOption(ProjectGrantColumnGrantID), ) return query.JoinClause(join, args...) } diff --git a/internal/query/project_role.go b/internal/query/project_role.go index 15ae806cd4..e70fcf277e 100644 --- a/internal/query/project_role.go +++ b/internal/query/project_role.go @@ -103,7 +103,7 @@ func projectRolePermissionCheckV2(ctx context.Context, query sq.SelectBuilder, e ProjectRoleColumnResourceOwner, domain.PermissionProjectRoleRead, SingleOrgPermissionOption(queries.Queries), - OwnedRowsPermissionOption(ProjectRoleColumnKey), + WithProjectsPermissionOption(ProjectRoleColumnProjectID), ) return query.JoinClause(join, args...) } From 7df4f76f3c6d41320bc4639446afa08d9c631032 Mon Sep 17 00:00:00 2001 From: Iraq <66622793+kkrime@users.noreply.github.com> Date: Thu, 5 Jun 2025 11:05:35 +0200 Subject: [PATCH 68/76] feat(api): reworking AddOrganization() API call to return all admins (#9900) --- internal/api/grpc/admin/org.go | 6 +- internal/api/grpc/org/v2/org.go | 15 ++-- internal/api/grpc/org/v2/org_test.go | 4 +- internal/api/grpc/org/v2beta/helper.go | 33 +++++-- .../org/v2beta/integration_test/org_test.go | 90 +++++++++++++------ internal/api/grpc/org/v2beta/org_test.go | 16 ++-- internal/command/org.go | 25 +++++- internal/command/org_test.go | 16 ++-- proto/zitadel/org/v2beta/org_service.proto | 18 +++- 9 files changed, 160 insertions(+), 63 deletions(-) diff --git a/internal/api/grpc/admin/org.go b/internal/api/grpc/admin/org.go index 293e7c74d7..ef97e47bb0 100644 --- a/internal/api/grpc/admin/org.go +++ b/internal/api/grpc/admin/org.go @@ -78,7 +78,7 @@ func (s *Server) SetUpOrg(ctx context.Context, req *admin_pb.SetUpOrgRequest) (* if err != nil { return nil, err } - human := setUpOrgHumanToCommand(req.User.(*admin_pb.SetUpOrgRequest_Human_).Human) //TODO: handle machine + human := setUpOrgHumanToCommand(req.User.(*admin_pb.SetUpOrgRequest_Human_).Human) // TODO: handle machine createdOrg, err := s.command.SetUpOrg(ctx, &command.OrgSetup{ Name: req.Org.Name, CustomDomain: req.Org.Domain, @@ -93,8 +93,8 @@ func (s *Server) SetUpOrg(ctx context.Context, req *admin_pb.SetUpOrgRequest) (* return nil, err } var userID string - if len(createdOrg.CreatedAdmins) == 1 { - userID = createdOrg.CreatedAdmins[0].ID + if len(createdOrg.OrgAdmins) == 1 { + userID = createdOrg.OrgAdmins[0].GetID() } return &admin_pb.SetUpOrgResponse{ Details: object.DomainToAddDetailsPb(createdOrg.ObjectDetails), diff --git a/internal/api/grpc/org/v2/org.go b/internal/api/grpc/org/v2/org.go index bbc3caca85..b876826365 100644 --- a/internal/api/grpc/org/v2/org.go +++ b/internal/api/grpc/org/v2/org.go @@ -69,12 +69,15 @@ func addOrganizationRequestAdminToCommand(admin *org.AddOrganizationRequest_Admi } func createdOrganizationToPb(createdOrg *command.CreatedOrg) (_ *org.AddOrganizationResponse, err error) { - admins := make([]*org.AddOrganizationResponse_CreatedAdmin, len(createdOrg.CreatedAdmins)) - for i, admin := range createdOrg.CreatedAdmins { - admins[i] = &org.AddOrganizationResponse_CreatedAdmin{ - UserId: admin.ID, - EmailCode: admin.EmailCode, - PhoneCode: admin.PhoneCode, + admins := make([]*org.AddOrganizationResponse_CreatedAdmin, 0, len(createdOrg.OrgAdmins)) + for _, admin := range createdOrg.OrgAdmins { + admin, ok := admin.(*command.CreatedOrgAdmin) + if ok { + admins = append(admins, &org.AddOrganizationResponse_CreatedAdmin{ + UserId: admin.GetID(), + EmailCode: admin.EmailCode, + PhoneCode: admin.PhoneCode, + }) } } return &org.AddOrganizationResponse{ diff --git a/internal/api/grpc/org/v2/org_test.go b/internal/api/grpc/org/v2/org_test.go index 7ae252a209..37a3dca41a 100644 --- a/internal/api/grpc/org/v2/org_test.go +++ b/internal/api/grpc/org/v2/org_test.go @@ -150,8 +150,8 @@ func Test_createdOrganizationToPb(t *testing.T) { EventDate: now, ResourceOwner: "orgID", }, - CreatedAdmins: []*command.CreatedOrgAdmin{ - { + OrgAdmins: []command.OrgAdmin{ + &command.CreatedOrgAdmin{ ID: "id", EmailCode: gu.Ptr("emailCode"), PhoneCode: gu.Ptr("phoneCode"), diff --git a/internal/api/grpc/org/v2beta/helper.go b/internal/api/grpc/org/v2beta/helper.go index 39bad0dae2..6f47819bb4 100644 --- a/internal/api/grpc/org/v2beta/helper.go +++ b/internal/api/grpc/org/v2beta/helper.go @@ -72,18 +72,33 @@ func OrgStateToPb(state domain.OrgState) v2beta_org.OrgState { } func createdOrganizationToPb(createdOrg *command.CreatedOrg) (_ *org.CreateOrganizationResponse, err error) { - admins := make([]*org.CreatedAdmin, len(createdOrg.CreatedAdmins)) - for i, admin := range createdOrg.CreatedAdmins { - admins[i] = &org.CreatedAdmin{ - UserId: admin.ID, - EmailCode: admin.EmailCode, - PhoneCode: admin.PhoneCode, + admins := make([]*org.OrganizationAdmin, len(createdOrg.OrgAdmins)) + for i, admin := range createdOrg.OrgAdmins { + switch admin := admin.(type) { + case *command.CreatedOrgAdmin: + admins[i] = &org.OrganizationAdmin{ + OrganizationAdmin: &org.OrganizationAdmin_CreatedAdmin{ + CreatedAdmin: &org.CreatedAdmin{ + UserId: admin.ID, + EmailCode: admin.EmailCode, + PhoneCode: admin.PhoneCode, + }, + }, + } + case *command.AssignedOrgAdmin: + admins[i] = &org.OrganizationAdmin{ + OrganizationAdmin: &org.OrganizationAdmin_AssignedAdmin{ + AssignedAdmin: &org.AssignedAdmin{ + UserId: admin.ID, + }, + }, + } } } return &org.CreateOrganizationResponse{ - CreationDate: timestamppb.New(createdOrg.ObjectDetails.EventDate), - Id: createdOrg.ObjectDetails.ResourceOwner, - CreatedAdmins: admins, + CreationDate: timestamppb.New(createdOrg.ObjectDetails.EventDate), + Id: createdOrg.ObjectDetails.ResourceOwner, + OrganizationAdmins: admins, }, nil } diff --git a/internal/api/grpc/org/v2beta/integration_test/org_test.go b/internal/api/grpc/org/v2beta/integration_test/org_test.go index 4e0ec26121..0d3b920afe 100644 --- a/internal/api/grpc/org/v2beta/integration_test/org_test.go +++ b/internal/api/grpc/org/v2beta/integration_test/org_test.go @@ -18,7 +18,6 @@ import ( "github.com/zitadel/zitadel/internal/integration" "github.com/zitadel/zitadel/pkg/grpc/admin" v2beta_object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta" - org "github.com/zitadel/zitadel/pkg/grpc/org/v2beta" v2beta_org "github.com/zitadel/zitadel/pkg/grpc/org/v2beta" "github.com/zitadel/zitadel/pkg/grpc/user/v2" user_v2beta "github.com/zitadel/zitadel/pkg/grpc/user/v2beta" @@ -85,6 +84,29 @@ func TestServer_CreateOrganization(t *testing.T) { }, wantErr: true, }, + { + name: "existing user as admin", + ctx: CTX, + req: &v2beta_org.CreateOrganizationRequest{ + Name: gofakeit.AppName(), + Admins: []*v2beta_org.CreateOrganizationRequest_Admin{ + { + UserType: &v2beta_org.CreateOrganizationRequest_Admin_UserId{UserId: User.GetUserId()}, + }, + }, + }, + want: &v2beta_org.CreateOrganizationResponse{ + OrganizationAdmins: []*v2beta_org.OrganizationAdmin{ + { + OrganizationAdmin: &v2beta_org.OrganizationAdmin_AssignedAdmin{ + AssignedAdmin: &v2beta_org.AssignedAdmin{ + UserId: User.GetUserId(), + }, + }, + }, + }, + }, + }, { name: "admin with init", ctx: CTX, @@ -111,11 +133,15 @@ func TestServer_CreateOrganization(t *testing.T) { }, want: &v2beta_org.CreateOrganizationResponse{ Id: integration.NotEmpty, - CreatedAdmins: []*v2beta_org.CreatedAdmin{ + OrganizationAdmins: []*v2beta_org.OrganizationAdmin{ { - UserId: integration.NotEmpty, - EmailCode: gu.Ptr(integration.NotEmpty), - PhoneCode: nil, + OrganizationAdmin: &v2beta_org.OrganizationAdmin_CreatedAdmin{ + CreatedAdmin: &v2beta_org.CreatedAdmin{ + UserId: integration.NotEmpty, + EmailCode: gu.Ptr(integration.NotEmpty), + PhoneCode: nil, + }, + }, }, }, }, @@ -155,10 +181,21 @@ func TestServer_CreateOrganization(t *testing.T) { }, }, want: &v2beta_org.CreateOrganizationResponse{ - CreatedAdmins: []*v2beta_org.CreatedAdmin{ - // a single admin is expected, because the first provided already exists + // OrganizationId: integration.NotEmpty, + OrganizationAdmins: []*v2beta_org.OrganizationAdmin{ { - UserId: integration.NotEmpty, + OrganizationAdmin: &v2beta_org.OrganizationAdmin_AssignedAdmin{ + AssignedAdmin: &v2beta_org.AssignedAdmin{ + UserId: User.GetUserId(), + }, + }, + }, + { + OrganizationAdmin: &v2beta_org.OrganizationAdmin_CreatedAdmin{ + CreatedAdmin: &v2beta_org.CreatedAdmin{ + UserId: integration.NotEmpty, + }, + }, }, }, }, @@ -192,13 +229,16 @@ func TestServer_CreateOrganization(t *testing.T) { now := time.Now() assert.WithinRange(t, gotCD, now.Add(-time.Minute), now.Add(time.Minute)) - // organization id must be the same as the resourceOwner - // check the admins - require.Len(t, got.GetCreatedAdmins(), len(tt.want.GetCreatedAdmins())) - for i, admin := range tt.want.GetCreatedAdmins() { - gotAdmin := got.GetCreatedAdmins()[i] - assertCreatedAdmin(t, admin, gotAdmin) + require.Equal(t, len(tt.want.GetOrganizationAdmins()), len(got.GetOrganizationAdmins())) + for i, admin := range tt.want.GetOrganizationAdmins() { + gotAdmin := got.GetOrganizationAdmins()[i].OrganizationAdmin + switch admin := admin.OrganizationAdmin.(type) { + case *v2beta_org.OrganizationAdmin_CreatedAdmin: + assertCreatedAdmin(t, admin.CreatedAdmin, gotAdmin.(*v2beta_org.OrganizationAdmin_CreatedAdmin).CreatedAdmin) + case *v2beta_org.OrganizationAdmin_AssignedAdmin: + assert.Equal(t, admin.AssignedAdmin.GetUserId(), gotAdmin.(*v2beta_org.OrganizationAdmin_AssignedAdmin).AssignedAdmin.GetUserId()) + } } }) } @@ -472,8 +512,8 @@ func TestServer_ListOrganizations(t *testing.T) { ctx: listOrgIAmOwnerCtx, query: []*v2beta_org.OrganizationSearchFilter{ { - Filter: &org.OrganizationSearchFilter_DomainFilter{ - DomainFilter: &org.OrgDomainFilter{ + Filter: &v2beta_org.OrganizationSearchFilter_DomainFilter{ + DomainFilter: &v2beta_org.OrgDomainFilter{ Domain: func() string { listOrgRes, err := listOrgClient.ListOrganizations(listOrgIAmOwnerCtx, &v2beta_org.ListOrganizationsRequest{ Filter: []*v2beta_org.OrganizationSearchFilter{ @@ -507,8 +547,8 @@ func TestServer_ListOrganizations(t *testing.T) { ctx: listOrgIAmOwnerCtx, query: []*v2beta_org.OrganizationSearchFilter{ { - Filter: &org.OrganizationSearchFilter_DomainFilter{ - DomainFilter: &org.OrgDomainFilter{ + Filter: &v2beta_org.OrganizationSearchFilter_DomainFilter{ + DomainFilter: &v2beta_org.OrgDomainFilter{ Domain: func() string { domain := strings.ToLower(strings.ReplaceAll(orgsName[1][1:len(orgsName[1])-2], " ", "-")) return domain @@ -530,8 +570,8 @@ func TestServer_ListOrganizations(t *testing.T) { ctx: listOrgIAmOwnerCtx, query: []*v2beta_org.OrganizationSearchFilter{ { - Filter: &org.OrganizationSearchFilter_DomainFilter{ - DomainFilter: &org.OrgDomainFilter{ + Filter: &v2beta_org.OrganizationSearchFilter_DomainFilter{ + DomainFilter: &v2beta_org.OrgDomainFilter{ Domain: func() string { domain := strings.ToUpper(strings.ReplaceAll(orgsName[1][1:len(orgsName[1])-2], " ", "-")) return domain @@ -1374,7 +1414,7 @@ func TestServer_ValidateOrganizationDomain(t *testing.T) { req: &v2beta_org.GenerateOrganizationDomainValidationRequest{ OrganizationId: orgId, Domain: domain, - Type: org.DomainValidationType_DOMAIN_VALIDATION_TYPE_HTTP, + Type: v2beta_org.DomainValidationType_DOMAIN_VALIDATION_TYPE_HTTP, }, }, { @@ -1383,7 +1423,7 @@ func TestServer_ValidateOrganizationDomain(t *testing.T) { req: &v2beta_org.GenerateOrganizationDomainValidationRequest{ OrganizationId: "non existent org id", Domain: domain, - Type: org.DomainValidationType_DOMAIN_VALIDATION_TYPE_HTTP, + Type: v2beta_org.DomainValidationType_DOMAIN_VALIDATION_TYPE_HTTP, }, // BUG: this should be 'organization does not exist' err: errors.New("Domain doesn't exist on organization"), @@ -1394,7 +1434,7 @@ func TestServer_ValidateOrganizationDomain(t *testing.T) { req: &v2beta_org.GenerateOrganizationDomainValidationRequest{ OrganizationId: orgId, Domain: domain, - Type: org.DomainValidationType_DOMAIN_VALIDATION_TYPE_DNS, + Type: v2beta_org.DomainValidationType_DOMAIN_VALIDATION_TYPE_DNS, }, }, { @@ -1403,7 +1443,7 @@ func TestServer_ValidateOrganizationDomain(t *testing.T) { req: &v2beta_org.GenerateOrganizationDomainValidationRequest{ OrganizationId: "non existent org id", Domain: domain, - Type: org.DomainValidationType_DOMAIN_VALIDATION_TYPE_DNS, + Type: v2beta_org.DomainValidationType_DOMAIN_VALIDATION_TYPE_DNS, }, // BUG: this should be 'organization does not exist' err: errors.New("Domain doesn't exist on organization"), @@ -1414,7 +1454,7 @@ func TestServer_ValidateOrganizationDomain(t *testing.T) { req: &v2beta_org.GenerateOrganizationDomainValidationRequest{ OrganizationId: orgId, Domain: "non existent domain", - Type: org.DomainValidationType_DOMAIN_VALIDATION_TYPE_HTTP, + Type: v2beta_org.DomainValidationType_DOMAIN_VALIDATION_TYPE_HTTP, }, err: errors.New("Domain doesn't exist on organization"), }, diff --git a/internal/api/grpc/org/v2beta/org_test.go b/internal/api/grpc/org/v2beta/org_test.go index 2047f665a1..346d6b88c1 100644 --- a/internal/api/grpc/org/v2beta/org_test.go +++ b/internal/api/grpc/org/v2beta/org_test.go @@ -150,8 +150,8 @@ func Test_createdOrganizationToPb(t *testing.T) { EventDate: now, ResourceOwner: "orgID", }, - CreatedAdmins: []*command.CreatedOrgAdmin{ - { + OrgAdmins: []command.OrgAdmin{ + &command.CreatedOrgAdmin{ ID: "id", EmailCode: gu.Ptr("emailCode"), PhoneCode: gu.Ptr("phoneCode"), @@ -162,11 +162,15 @@ func Test_createdOrganizationToPb(t *testing.T) { want: &org.CreateOrganizationResponse{ CreationDate: timestamppb.New(now), Id: "orgID", - CreatedAdmins: []*org.CreatedAdmin{ + OrganizationAdmins: []*org.OrganizationAdmin{ { - UserId: "id", - EmailCode: gu.Ptr("emailCode"), - PhoneCode: gu.Ptr("phoneCode"), + OrganizationAdmin: &org.OrganizationAdmin_CreatedAdmin{ + CreatedAdmin: &org.CreatedAdmin{ + UserId: "id", + EmailCode: gu.Ptr("emailCode"), + PhoneCode: gu.Ptr("phoneCode"), + }, + }, }, }, }, diff --git a/internal/command/org.go b/internal/command/org.go index b6650ef7f2..ddf99797e3 100644 --- a/internal/command/org.go +++ b/internal/command/org.go @@ -54,7 +54,11 @@ type orgSetupCommands struct { type CreatedOrg struct { ObjectDetails *domain.ObjectDetails - CreatedAdmins []*CreatedOrgAdmin + OrgAdmins []OrgAdmin +} + +type OrgAdmin interface { + GetID() string } type CreatedOrgAdmin struct { @@ -65,6 +69,18 @@ type CreatedOrgAdmin struct { MachineKey *MachineKey } +func (a *CreatedOrgAdmin) GetID() string { + return a.ID +} + +type AssignedOrgAdmin struct { + ID string +} + +func (a *AssignedOrgAdmin) GetID() string { + return a.ID +} + func (o *OrgSetup) Validate() (err error) { if o.OrgID != "" && strings.TrimSpace(o.OrgID) == "" { return zerrors.ThrowInvalidArgument(nil, "ORG-4ABd3", "Errors.Invalid.Argument") @@ -188,14 +204,15 @@ func (c *orgSetupCommands) push(ctx context.Context) (_ *CreatedOrg, err error) EventDate: events[len(events)-1].CreatedAt(), ResourceOwner: c.aggregate.ID, }, - CreatedAdmins: c.createdAdmins(), + OrgAdmins: c.createdAdmins(), }, nil } -func (c *orgSetupCommands) createdAdmins() []*CreatedOrgAdmin { - users := make([]*CreatedOrgAdmin, 0, len(c.admins)) +func (c *orgSetupCommands) createdAdmins() []OrgAdmin { + users := make([]OrgAdmin, 0, len(c.admins)) for _, admin := range c.admins { if admin.ID != "" && admin.Human == nil { + users = append(users, &AssignedOrgAdmin{ID: admin.ID}) continue } if admin.Human != nil { diff --git a/internal/command/org_test.go b/internal/command/org_test.go index 4b6fd7afe5..4239be760a 100644 --- a/internal/command/org_test.go +++ b/internal/command/org_test.go @@ -1531,8 +1531,8 @@ func TestCommandSide_SetUpOrg(t *testing.T) { ObjectDetails: &domain.ObjectDetails{ ResourceOwner: "orgID", }, - CreatedAdmins: []*CreatedOrgAdmin{ - { + OrgAdmins: []OrgAdmin{ + &CreatedOrgAdmin{ ID: "userID", }, }, @@ -1574,7 +1574,7 @@ func TestCommandSide_SetUpOrg(t *testing.T) { ObjectDetails: &domain.ObjectDetails{ ResourceOwner: "custom-org-ID", }, - CreatedAdmins: []*CreatedOrgAdmin{}, + OrgAdmins: []OrgAdmin{}, }, }, }, @@ -1641,7 +1641,11 @@ func TestCommandSide_SetUpOrg(t *testing.T) { ObjectDetails: &domain.ObjectDetails{ ResourceOwner: "orgID", }, - CreatedAdmins: []*CreatedOrgAdmin{}, + OrgAdmins: []OrgAdmin{ + &AssignedOrgAdmin{ + ID: "userID", + }, + }, }, }, }, @@ -1751,8 +1755,8 @@ func TestCommandSide_SetUpOrg(t *testing.T) { ObjectDetails: &domain.ObjectDetails{ ResourceOwner: "orgID", }, - CreatedAdmins: []*CreatedOrgAdmin{ - { + OrgAdmins: []OrgAdmin{ + &CreatedOrgAdmin{ ID: "userID", PAT: &PersonalAccessToken{ ObjectRoot: models.ObjectRoot{ diff --git a/proto/zitadel/org/v2beta/org_service.proto b/proto/zitadel/org/v2beta/org_service.proto index 28c823a89b..387b2cb825 100644 --- a/proto/zitadel/org/v2beta/org_service.proto +++ b/proto/zitadel/org/v2beta/org_service.proto @@ -558,6 +558,18 @@ message CreatedAdmin { optional string phone_code = 3; } +message AssignedAdmin { + string user_id = 1; +} + +message OrganizationAdmin { + // The admins created/assigned for the Organization. + oneof OrganizationAdmin { + CreatedAdmin created_admin = 1; + AssignedAdmin assigned_admin = 2; + } +} + message CreateOrganizationResponse{ // The timestamp of the organization was created. google.protobuf.Timestamp creation_date = 1 [ @@ -577,8 +589,8 @@ message CreateOrganizationResponse{ } ]; - // The admins created for the Organization - repeated CreatedAdmin created_admins = 3; + // The admins created/assigned for the Organization + repeated OrganizationAdmin organization_admins = 3; } message UpdateOrganizationRequest { @@ -939,3 +951,5 @@ message DeleteOrganizationMetadataResponse{ } ]; } + + From 63c92104baec2d326a3f296cd0572a98b56be199 Mon Sep 17 00:00:00 2001 From: Livio Spring Date: Thu, 5 Jun 2025 12:13:26 +0200 Subject: [PATCH 69/76] chore: service ping api design (#9984) # Which Problems Are Solved Add the possibility to report information and analytical data from (self-hosted) ZITADEL systems to a central endpoint. To be able to do so an API has to be designed to receive the different reports and information. # How the Problems Are Solved - Telemetry service definition added, which currently has two endpoints: - ReportBaseInformation: To gather the zitadel version and instance information such as id and creation date - ReportResourceCounts: Dynamically report (based on #9979) different resources (orgs, users per org, ...) - To be able to paginate and send multiple pages to the endpoint a `report_id` is returned on the first page / request from the server, which needs to be passed by the client on the following pages. - Base error handling is described in the proto and is based on gRPC standards and best practices. # Additional Changes none # Additional Context Public documentation of the behaviour / error handling and what data is collected, resp. how to configure will be provided in https://github.com/zitadel/zitadel/issues/9869. Closes https://github.com/zitadel/zitadel/issues/9872 --- .../zitadel/analytics/v2beta/telemetry.proto | 48 +++++++++++ .../analytics/v2beta/telemetry_service.proto | 79 +++++++++++++++++++ 2 files changed, 127 insertions(+) create mode 100644 proto/zitadel/analytics/v2beta/telemetry.proto create mode 100644 proto/zitadel/analytics/v2beta/telemetry_service.proto diff --git a/proto/zitadel/analytics/v2beta/telemetry.proto b/proto/zitadel/analytics/v2beta/telemetry.proto new file mode 100644 index 0000000000..f0e1537f9a --- /dev/null +++ b/proto/zitadel/analytics/v2beta/telemetry.proto @@ -0,0 +1,48 @@ +syntax = "proto3"; + +package zitadel.analytics.v2beta; + +import "google/protobuf/timestamp.proto"; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/analytics/v2beta;analytics"; + + +message InstanceInformation { + // The unique identifier of the instance. + string id = 1; + // The custom domains (incl. generated ones) of the instance. + repeated string domains = 2; + // The creation date of the instance. + google.protobuf.Timestamp created_at = 3; +} + +message ResourceCount { + // The ID of the instance for which the resource counts are reported. + string instance_id = 3; + // The parent type of the resource counts (e.g. organization or instance). + // For example, reporting the amount of users per organization would use + // `COUNT_PARENT_TYPE_ORGANIZATION` as parent type and the organization ID as parent ID. + CountParentType parent_type = 4; + // The parent ID of the resource counts (e.g. organization or instance ID). + // For example, reporting the amount of users per organization would use + // `COUNT_PARENT_TYPE_ORGANIZATION` as parent type and the organization ID as parent ID. + string parent_id = 5; + // The resource counts to report, e.g. amount of `users`, `organizations`, etc. + string resource_name = 6; + // The name of the table in the database, which was used to calculate the counts. + // This can be used to deduplicate counts in case of multiple reports. + // For example, if the counts were calculated from the `users14` table, + // the table name would be `users14`, where there could also be a `users15` table + // reported at the same time as the system is rolling out a new version. + string table_name = 7; + // The timestamp when the count was last updated. + google.protobuf.Timestamp updated_at = 8; + // The actual amount of the resource. + uint32 amount = 9; +} + +enum CountParentType { + COUNT_PARENT_TYPE_UNSPECIFIED = 0; + COUNT_PARENT_TYPE_INSTANCE = 1; + COUNT_PARENT_TYPE_ORGANIZATION = 2; +} diff --git a/proto/zitadel/analytics/v2beta/telemetry_service.proto b/proto/zitadel/analytics/v2beta/telemetry_service.proto new file mode 100644 index 0000000000..e71536a811 --- /dev/null +++ b/proto/zitadel/analytics/v2beta/telemetry_service.proto @@ -0,0 +1,79 @@ +syntax = "proto3"; + +package zitadel.analytics.v2beta; + +import "google/protobuf/timestamp.proto"; +import "zitadel/analytics/v2beta/telemetry.proto"; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/analytics/v2beta;analytics"; + +// The TelemetryService is used to report telemetry such as usage statistics of the ZITADEL instance(s). +// back to a central storage. +// It is used to collect anonymized data about the usage of ZITADEL features, capabilities, and configurations. +// ZITADEL acts as a client of the TelemetryService. +// +// Reports are sent periodically based on the system's runtime configuration. +// The content of the reports, respectively the data collected, can be configured in the system's runtime configuration. +// +// All endpoints follow the same error and retry handling: +// In case of a failure to report the usage, ZITADEL will retry to report the usage +// based on the configured retry policy and error type: +// - Client side errors will not be retried, as they indicate a misconfiguration or an invalid request: +// - `INVALID_ARGUMENT`: The request was malformed. +// - `NOT_FOUND`: The TelemetryService's endpoint is likely misconfigured. +// - Connection / transfer errors will be retried based on the retry policy configured in the system's runtime configuration: +// - `DEADLINE_EXCEEDED`: The request took too long to complete, it will be retried. +// - `RESOURCE_EXHAUSTED`: The request was rejected due to resource exhaustion, it will be retried after a backoff period. +// - `UNAVAILABLE`: The TelemetryService is currently unavailable, it will be retried after a backoff period. +// Server side errors will also be retried based on the information provided by the server: +// - `FAILED_PRECONDITION`: The request failed due to a precondition, e.g. the report ID does not exists, +// does not correspond to the same system ID or previous reporting is too old, do not retry. +// - `INTERNAL`: An internal error occurred. Check details and logs. +service TelemetryService { + + // ReportBaseInformation is used to report the base information of the ZITADEL system, + // including the version, instances, their creation date and domains. + // The response contains a report ID to link it to the resource counts or other reports. + // The report ID is only valid for the same system ID. + rpc ReportBaseInformation (ReportBaseInformationRequest) returns (ReportBaseInformationResponse) {} + + // ReportResourceCounts is used to report the resource counts such as amount of organizations + // or users per organization and much more. + // Since the resource counts can be reported in multiple batches, + // the response contains a report ID to continue reporting. + // The report ID is only valid for the same system ID. + rpc ReportResourceCounts (ReportResourceCountsRequest) returns (ReportResourceCountsResponse) {} +} + +message ReportBaseInformationRequest { + // The system ID is a unique identifier for the ZITADEL system. + string system_id = 1; + // The current version of the ZITADEL system. + string version = 2; + // A list of instances in the ZITADEL system and their information. + repeated InstanceInformation instances = 3; +} + +message ReportBaseInformationResponse { + // The report ID is a unique identifier for the report. + // It is used to identify the report to be able to link it to the resource counts or other reports. + // Note that the report ID is only valid for the same system ID. + string report_id = 1; +} + +message ReportResourceCountsRequest { + // The system ID is a unique identifier for the ZITADEL system. + string system_id = 1; + // The previously returned report ID from the server to continue reporting. + // Note that the report ID is only valid for the same system ID. + optional string report_id = 2; + // A list of resource counts to report. + repeated ResourceCount resource_counts = 3; +} + +message ReportResourceCountsResponse { + // The report ID is a unique identifier for the report. + // It is used to identify the report in case of additional data / pagination. + // Note that the report ID is only valid for the same system ID. + string report_id = 1; +} \ No newline at end of file From 6c309d65c6d6367959d012a9b97a46a26cc8be6a Mon Sep 17 00:00:00 2001 From: Silvan <27845747+adlerhurst@users.noreply.github.com> Date: Thu, 5 Jun 2025 13:42:59 +0200 Subject: [PATCH 70/76] fix(fields): project by id and resource owner (#10034) # Which Problems Are Solved If the `IMPROVED_PERFORMANCE_PROJECT` feature flag was enabled it was not possible to remove organizations anymore because the project was searched in the `eventstore.fields` table without resource owner. # How the Problems Are Solved Search now includes resource owner. --- internal/command/project.go | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/internal/command/project.go b/internal/command/project.go index bf72306417..40aa79f186 100644 --- a/internal/command/project.go +++ b/internal/command/project.go @@ -138,14 +138,14 @@ func projectWriteModel(ctx context.Context, filter preparation.FilterToQueryRedu return project, nil } -func (c *Commands) projectAggregateByID(ctx context.Context, projectID string) (*eventstore.Aggregate, domain.ProjectState, error) { - result, err := c.projectState(ctx, projectID) +func (c *Commands) projectAggregateByID(ctx context.Context, projectID, resourceOwner string) (*eventstore.Aggregate, domain.ProjectState, error) { + result, err := c.projectState(ctx, projectID, resourceOwner) if err != nil { return nil, domain.ProjectStateUnspecified, zerrors.ThrowNotFound(err, "COMMA-NDQoF", "Errors.Project.NotFound") } if len(result) == 0 { _ = projection.ProjectGrantFields.Trigger(ctx) - result, err = c.projectState(ctx, projectID) + result, err = c.projectState(ctx, projectID, resourceOwner) if err != nil || len(result) == 0 { return nil, domain.ProjectStateUnspecified, zerrors.ThrowNotFound(err, "COMMA-U1nza", "Errors.Project.NotFound") } @@ -159,7 +159,7 @@ func (c *Commands) projectAggregateByID(ctx context.Context, projectID string) ( return &result[0].Aggregate, state, nil } -func (c *Commands) projectState(ctx context.Context, projectID string) ([]*eventstore.SearchResult, error) { +func (c *Commands) projectState(ctx context.Context, projectID, resourceOwner string) ([]*eventstore.SearchResult, error) { return c.eventstore.Search( ctx, map[eventstore.FieldType]any{ @@ -167,6 +167,7 @@ func (c *Commands) projectState(ctx context.Context, projectID string) ([]*event eventstore.FieldTypeObjectID: projectID, eventstore.FieldTypeObjectRevision: project.ProjectObjectRevision, eventstore.FieldTypeFieldName: project.ProjectStateSearchField, + eventstore.FieldTypeResourceOwner: resourceOwner, }, ) } @@ -179,7 +180,7 @@ func (c *Commands) checkProjectExists(ctx context.Context, projectID, resourceOw return c.checkProjectExistsOld(ctx, projectID, resourceOwner) } - agg, state, err := c.projectAggregateByID(ctx, projectID) + agg, state, err := c.projectAggregateByID(ctx, projectID, resourceOwner) if err != nil || !state.Valid() { return "", zerrors.ThrowPreconditionFailed(err, "COMMA-VCnwD", "Errors.Project.NotFound") } @@ -249,7 +250,7 @@ func (c *Commands) DeactivateProject(ctx context.Context, projectID string, reso return c.deactivateProjectOld(ctx, projectID, resourceOwner) } - projectAgg, state, err := c.projectAggregateByID(ctx, projectID) + projectAgg, state, err := c.projectAggregateByID(ctx, projectID, resourceOwner) if err != nil { return nil, err } @@ -285,7 +286,7 @@ func (c *Commands) ReactivateProject(ctx context.Context, projectID string, reso return c.reactivateProjectOld(ctx, projectID, resourceOwner) } - projectAgg, state, err := c.projectAggregateByID(ctx, projectID) + projectAgg, state, err := c.projectAggregateByID(ctx, projectID, resourceOwner) if err != nil { return nil, err } From 647b3b57cffe4c34d3ae9d0d66e635a967f7eea9 Mon Sep 17 00:00:00 2001 From: Stefan Benz <46600784+stebenz@users.noreply.github.com> Date: Thu, 5 Jun 2025 15:50:21 +0200 Subject: [PATCH 71/76] fix: correct id filter for project service (#10035) # Which Problems Are Solved IDs filter definition was changed in another PR and not changed in the Project service. # How the Problems Are Solved Correctly use the IDs filter. # Additional Changes Add timeout to the integration tests. # Additional Context None --- .../grpc/project/v2beta/integration/query_test.go | 12 ++++-------- internal/integration/client.go | 4 +++- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/internal/api/grpc/project/v2beta/integration/query_test.go b/internal/api/grpc/project/v2beta/integration/query_test.go index fc153f0130..b648e8c1d7 100644 --- a/internal/api/grpc/project/v2beta/integration/query_test.go +++ b/internal/api/grpc/project/v2beta/integration/query_test.go @@ -389,9 +389,7 @@ func TestServer_ListProjects(t *testing.T) { resp2 := createProject(iamOwnerCtx, instance, t, orgID, true, false) resp3 := createProject(iamOwnerCtx, instance, t, orgResp.GetOrganizationId(), false, true) request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{ - InProjectIdsFilter: &project.InProjectIDsFilter{ - ProjectIds: []string{resp1.GetId(), resp2.GetId(), resp3.GetId(), projectResp.GetId()}, - }, + InProjectIdsFilter: &filter.InIDsFilter{Ids: []string{resp1.GetId(), resp2.GetId(), resp3.GetId(), projectResp.GetId()}}, } response.Projects[0] = grantedProjectResp response.Projects[1] = projectResp @@ -516,7 +514,7 @@ func TestServer_ListProjects(t *testing.T) { projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), projectName, true, true) projectGrantResp := instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), orgID) request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{ - InProjectIdsFilter: &project.InProjectIDsFilter{ProjectIds: []string{projectResp.GetId()}}, + InProjectIdsFilter: &filter.InIDsFilter{Ids: []string{projectResp.GetId()}}, } response.Projects[0] = &project.Project{ Id: projectResp.GetId(), @@ -892,7 +890,7 @@ func TestServer_ListProjects_PermissionV2(t *testing.T) { // projectGrantResp := instancePermissionV2.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), orgID) request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{ - InProjectIdsFilter: &project.InProjectIDsFilter{ProjectIds: []string{projectResp.GetId()}}, + InProjectIdsFilter: &filter.InIDsFilter{Ids: []string{projectResp.GetId()}}, } /* response.Projects[0] = &project.Project{ @@ -1227,9 +1225,7 @@ func TestServer_ListProjectGrants(t *testing.T) { project2Resp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), name2, false, false) project3Resp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), name3, false, false) request.Filters[0].Filter = &project.ProjectGrantSearchFilter_InProjectIdsFilter{ - InProjectIdsFilter: &project.InProjectIDsFilter{ - ProjectIds: []string{project1Resp.GetId(), project2Resp.GetId(), project3Resp.GetId(), projectResp.GetId()}, - }, + InProjectIdsFilter: &filter.InIDsFilter{Ids: []string{project1Resp.GetId(), project2Resp.GetId(), project3Resp.GetId(), projectResp.GetId()}}, } createProjectGrant(iamOwnerCtx, instance, t, orgID, project1Resp.GetId(), name1) diff --git a/internal/integration/client.go b/internal/integration/client.go index 838a728faf..20c98b5628 100644 --- a/internal/integration/client.go +++ b/internal/integration/client.go @@ -16,6 +16,7 @@ import ( "google.golang.org/grpc/metadata" "google.golang.org/protobuf/types/known/durationpb" "google.golang.org/protobuf/types/known/structpb" + "google.golang.org/protobuf/types/known/timestamppb" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/integration/scim" @@ -271,7 +272,8 @@ func (i *Instance) CreateUserTypeMachine(ctx context.Context) *user_v2.CreateUse func (i *Instance) CreatePersonalAccessToken(ctx context.Context, userID string) *user_v2.AddPersonalAccessTokenResponse { resp, err := i.Client.UserV2.AddPersonalAccessToken(ctx, &user_v2.AddPersonalAccessTokenRequest{ - UserId: userID, + UserId: userID, + ExpirationDate: timestamppb.New(time.Now().Add(30 * time.Minute)), }) logging.OnError(err).Panic("create pat") return resp From 4df138286b673255319b20b685f1cba31c05b47d Mon Sep 17 00:00:00 2001 From: Silvan <27845747+adlerhurst@users.noreply.github.com> Date: Fri, 6 Jun 2025 10:48:29 +0200 Subject: [PATCH 72/76] perf(query): reduce user query duration (#10037) # Which Problems Are Solved The resource usage to query user(s) on the database was high and therefore could have performance impact. # How the Problems Are Solved Database queries involving the users and loginnames table were improved and an index was added for user by email query. # Additional Changes - spellchecks - updated apis on load tests # additional info needs cherry pick to v3 --- cmd/setup/58.go | 49 +++ cmd/setup/58/01_update_login_names3_view.sql | 36 ++ cmd/setup/58/02_create_index.sql | 1 + cmd/setup/config.go | 1 + cmd/setup/setup.go | 2 + internal/query/oidc_settings.go | 2 +- internal/query/org_metadata.go | 4 +- internal/query/project.go | 2 +- internal/query/project_grant.go | 4 +- internal/query/projection/login_name.go | 111 +----- .../query/projection/login_name_query.sql | 35 ++ internal/query/projection/user.go | 1 + internal/query/restrictions.go | 2 +- internal/query/search_query.go | 13 +- internal/query/search_query_test.go | 22 +- internal/query/secret_generators.go | 2 +- internal/query/security_policy.go | 2 +- internal/query/user.go | 158 ++------ internal/query/user_by_id.sql | 52 +-- internal/query/user_metadata.go | 6 +- internal/query/user_notify_by_id.sql | 52 +-- internal/query/user_personal_access_token.go | 2 +- internal/query/user_test.go | 345 +----------------- load-test/src/org.ts | 2 +- load-test/src/use_cases/manipulate_user.ts | 2 +- load-test/src/user.ts | 6 +- 26 files changed, 225 insertions(+), 689 deletions(-) create mode 100644 cmd/setup/58.go create mode 100644 cmd/setup/58/01_update_login_names3_view.sql create mode 100644 cmd/setup/58/02_create_index.sql create mode 100644 internal/query/projection/login_name_query.sql diff --git a/cmd/setup/58.go b/cmd/setup/58.go new file mode 100644 index 0000000000..c46b30f548 --- /dev/null +++ b/cmd/setup/58.go @@ -0,0 +1,49 @@ +package setup + +import ( + "context" + "database/sql" + "embed" + "fmt" + + "github.com/zitadel/logging" + + "github.com/zitadel/zitadel/internal/database" + "github.com/zitadel/zitadel/internal/eventstore" +) + +var ( + //go:embed 58/*.sql + replaceLoginNames3View embed.FS +) + +type ReplaceLoginNames3View struct { + dbClient *database.DB +} + +func (mig *ReplaceLoginNames3View) Execute(ctx context.Context, _ eventstore.Event) error { + var exists bool + err := mig.dbClient.QueryRowContext(ctx, func(r *sql.Row) error { + return r.Scan(&exists) + }, "SELECT exists(SELECT 1 from information_schema.views WHERE table_schema = 'projections' AND table_name = 'login_names3')") + + if err != nil || !exists { + return err + } + + statements, err := readStatements(replaceLoginNames3View, "58") + if err != nil { + return err + } + for _, stmt := range statements { + logging.WithFields("file", stmt.file, "migration", mig.String()).Info("execute statement") + if _, err := mig.dbClient.ExecContext(ctx, stmt.query); err != nil { + return fmt.Errorf("%s %s: %w", mig.String(), stmt.file, err) + } + } + return nil +} + +func (mig *ReplaceLoginNames3View) String() string { + return "58_replace_login_names3_view" +} diff --git a/cmd/setup/58/01_update_login_names3_view.sql b/cmd/setup/58/01_update_login_names3_view.sql new file mode 100644 index 0000000000..4499296152 --- /dev/null +++ b/cmd/setup/58/01_update_login_names3_view.sql @@ -0,0 +1,36 @@ +CREATE OR REPLACE VIEW projections.login_names3 AS + SELECT + u.id AS user_id + , CASE + WHEN p.must_be_domain THEN CONCAT(u.user_name, '@', d.name) + ELSE u.user_name + END AS login_name + , COALESCE(d.is_primary, TRUE) AS is_primary + , u.instance_id + FROM + projections.login_names3_users AS u + LEFT JOIN LATERAL ( + SELECT + must_be_domain + , is_default + FROM + projections.login_names3_policies AS p + WHERE + ( + p.instance_id = u.instance_id + AND NOT p.is_default + AND p.resource_owner = u.resource_owner + ) OR ( + p.instance_id = u.instance_id + AND p.is_default + ) + ORDER BY + p.is_default -- custom first + LIMIT 1 + ) AS p ON TRUE + LEFT JOIN + projections.login_names3_domains d + ON + p.must_be_domain + AND u.resource_owner = d.resource_owner + AND u.instance_id = d.instance_id diff --git a/cmd/setup/58/02_create_index.sql b/cmd/setup/58/02_create_index.sql new file mode 100644 index 0000000000..ed3627b427 --- /dev/null +++ b/cmd/setup/58/02_create_index.sql @@ -0,0 +1 @@ +CREATE INDEX CONCURRENTLY IF NOT EXISTS login_names3_policies_is_default_owner_idx ON projections.login_names3_policies (instance_id, is_default, resource_owner) INCLUDE (must_be_domain) diff --git a/cmd/setup/config.go b/cmd/setup/config.go index dd59ba3f07..0c3f726902 100644 --- a/cmd/setup/config.go +++ b/cmd/setup/config.go @@ -154,6 +154,7 @@ type Steps struct { s55ExecutionHandlerStart *ExecutionHandlerStart s56IDPTemplate6SAMLFederatedLogout *IDPTemplate6SAMLFederatedLogout s57CreateResourceCounts *CreateResourceCounts + s58ReplaceLoginNames3View *ReplaceLoginNames3View } func MustNewSteps(v *viper.Viper) *Steps { diff --git a/cmd/setup/setup.go b/cmd/setup/setup.go index 1465180a6b..8ee8d7fc68 100644 --- a/cmd/setup/setup.go +++ b/cmd/setup/setup.go @@ -216,6 +216,7 @@ func Setup(ctx context.Context, config *Config, steps *Steps, masterKey string) steps.s55ExecutionHandlerStart = &ExecutionHandlerStart{dbClient: dbClient} steps.s56IDPTemplate6SAMLFederatedLogout = &IDPTemplate6SAMLFederatedLogout{dbClient: dbClient} steps.s57CreateResourceCounts = &CreateResourceCounts{dbClient: dbClient} + steps.s58ReplaceLoginNames3View = &ReplaceLoginNames3View{dbClient: dbClient} err = projection.Create(ctx, dbClient, eventstoreClient, config.Projections, nil, nil, nil) logging.OnError(err).Fatal("unable to start projections") @@ -262,6 +263,7 @@ func Setup(ctx context.Context, config *Config, steps *Steps, masterKey string) steps.s55ExecutionHandlerStart, steps.s56IDPTemplate6SAMLFederatedLogout, steps.s57CreateResourceCounts, + steps.s58ReplaceLoginNames3View, } { setupErr = executeMigration(ctx, eventstoreClient, step, "migration failed") if setupErr != nil { diff --git a/internal/query/oidc_settings.go b/internal/query/oidc_settings.go index bdd21cfd15..4ecd6cdad2 100644 --- a/internal/query/oidc_settings.go +++ b/internal/query/oidc_settings.go @@ -84,7 +84,7 @@ func (q *Queries) OIDCSettingsByAggID(ctx context.Context, aggregateID string) ( OIDCSettingsColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), }).ToSql() if err != nil { - return nil, zerrors.ThrowInternal(err, "QUERY-s9nle", "Errors.Query.SQLStatment") + return nil, zerrors.ThrowInternal(err, "QUERY-s9nle", "Errors.Query.SQLStatement") } err = q.client.QueryRowContext(ctx, func(row *sql.Row) error { diff --git a/internal/query/org_metadata.go b/internal/query/org_metadata.go index fe61ad51d9..e67c7222cd 100644 --- a/internal/query/org_metadata.go +++ b/internal/query/org_metadata.go @@ -103,7 +103,7 @@ func (q *Queries) GetOrgMetadataByKey(ctx context.Context, shouldTriggerBulk boo } stmt, args, err := query.Where(eq).ToSql() if err != nil { - return nil, zerrors.ThrowInternal(err, "QUERY-aDaG2", "Errors.Query.SQLStatment") + return nil, zerrors.ThrowInternal(err, "QUERY-aDaG2", "Errors.Query.SQLStatement") } err = q.client.QueryRowContext(ctx, func(row *sql.Row) error { @@ -133,7 +133,7 @@ func (q *Queries) SearchOrgMetadata(ctx context.Context, shouldTriggerBulk bool, query, scan := prepareOrgMetadataListQuery() stmt, args, err := queries.toQuery(query).Where(eq).ToSql() if err != nil { - return nil, zerrors.ThrowInternal(err, "QUERY-Egbld", "Errors.Query.SQLStatment") + return nil, zerrors.ThrowInternal(err, "QUERY-Egbld", "Errors.Query.SQLStatement") } err = q.client.QueryContext(ctx, func(rows *sql.Rows) error { diff --git a/internal/query/project.go b/internal/query/project.go index 728731f7cb..59e2dd95c0 100644 --- a/internal/query/project.go +++ b/internal/query/project.go @@ -211,7 +211,7 @@ func (q *Queries) ProjectByID(ctx context.Context, shouldTriggerBulk bool, id st } query, args, err := stmt.Where(eq).ToSql() if err != nil { - return nil, zerrors.ThrowInternal(err, "QUERY-2m00Q", "Errors.Query.SQLStatment") + return nil, zerrors.ThrowInternal(err, "QUERY-2m00Q", "Errors.Query.SQLStatement") } err = q.client.QueryRowContext(ctx, func(row *sql.Row) error { diff --git a/internal/query/project_grant.go b/internal/query/project_grant.go index 3093d26f30..1931cad0f5 100644 --- a/internal/query/project_grant.go +++ b/internal/query/project_grant.go @@ -167,7 +167,7 @@ func (q *Queries) ProjectGrantByID(ctx context.Context, shouldTriggerBulk bool, } query, args, err := stmt.Where(eq).ToSql() if err != nil { - return nil, zerrors.ThrowInternal(err, "QUERY-Nf93d", "Errors.Query.SQLStatment") + return nil, zerrors.ThrowInternal(err, "QUERY-Nf93d", "Errors.Query.SQLStatement") } err = q.client.QueryRowContext(ctx, func(row *sql.Row) error { @@ -189,7 +189,7 @@ func (q *Queries) ProjectGrantByIDAndGrantedOrg(ctx context.Context, id, granted } query, args, err := stmt.Where(eq).ToSql() if err != nil { - return nil, zerrors.ThrowInternal(err, "QUERY-MO9fs", "Errors.Query.SQLStatment") + return nil, zerrors.ThrowInternal(err, "QUERY-MO9fs", "Errors.Query.SQLStatement") } err = q.client.QueryRowContext(ctx, func(row *sql.Row) error { diff --git a/internal/query/projection/login_name.go b/internal/query/projection/login_name.go index 3c31928af4..e60f725dc7 100644 --- a/internal/query/projection/login_name.go +++ b/internal/query/projection/login_name.go @@ -2,9 +2,7 @@ package projection import ( "context" - "strings" - - sq "github.com/Masterminds/squirrel" + _ "embed" "github.com/zitadel/zitadel/internal/eventstore" old_handler "github.com/zitadel/zitadel/internal/eventstore/handler" @@ -58,105 +56,8 @@ const ( LoginNamePoliciesInstanceIDCol = "instance_id" ) -var ( - policyUsers = sq.Select( - alias( - col(usersAlias, LoginNameUserIDCol), - LoginNameUserCol, - ), - col(usersAlias, LoginNameUserUserNameCol), - col(usersAlias, LoginNameUserInstanceIDCol), - col(usersAlias, LoginNameUserResourceOwnerCol), - alias( - coalesce(col(policyCustomAlias, LoginNamePoliciesMustBeDomainCol), col(policyDefaultAlias, LoginNamePoliciesMustBeDomainCol)), - LoginNamePoliciesMustBeDomainCol, - ), - ).From(alias(LoginNameUserProjectionTable, usersAlias)). - LeftJoin( - leftJoin(LoginNamePolicyProjectionTable, policyCustomAlias, - eq(col(policyCustomAlias, LoginNamePoliciesResourceOwnerCol), col(usersAlias, LoginNameUserResourceOwnerCol)), - eq(col(policyCustomAlias, LoginNamePoliciesInstanceIDCol), col(usersAlias, LoginNameUserInstanceIDCol)), - ), - ). - LeftJoin( - leftJoin(LoginNamePolicyProjectionTable, policyDefaultAlias, - eq(col(policyDefaultAlias, LoginNamePoliciesIsDefaultCol), "true"), - eq(col(policyDefaultAlias, LoginNamePoliciesInstanceIDCol), col(usersAlias, LoginNameUserInstanceIDCol)), - ), - ) - - loginNamesTable = sq.Select( - col(policyUsersAlias, LoginNameUserCol), - col(policyUsersAlias, LoginNameUserUserNameCol), - col(policyUsersAlias, LoginNameUserResourceOwnerCol), - alias(col(policyUsersAlias, LoginNameUserInstanceIDCol), - LoginNameInstanceIDCol), - col(policyUsersAlias, LoginNamePoliciesMustBeDomainCol), - alias(col(domainsAlias, LoginNameDomainNameCol), - domainAlias), - col(domainsAlias, LoginNameDomainIsPrimaryCol), - ).FromSelect(policyUsers, policyUsersAlias). - LeftJoin( - leftJoin(LoginNameDomainProjectionTable, domainsAlias, - col(policyUsersAlias, LoginNamePoliciesMustBeDomainCol), - eq(col(policyUsersAlias, LoginNameUserResourceOwnerCol), col(domainsAlias, LoginNameDomainResourceOwnerCol)), - eq(col(policyUsersAlias, LoginNamePoliciesInstanceIDCol), col(domainsAlias, LoginNameDomainInstanceIDCol)), - ), - ) - - viewStmt, _ = sq.Select( - LoginNameUserCol, - alias( - whenThenElse( - LoginNamePoliciesMustBeDomainCol, - concat(LoginNameUserUserNameCol, "'@'", domainAlias), - LoginNameUserUserNameCol), - LoginNameCol), - alias(coalesce(LoginNameDomainIsPrimaryCol, "true"), - LoginNameIsPrimaryCol), - LoginNameInstanceIDCol, - ).FromSelect(loginNamesTable, LoginNameTableAlias).MustSql() -) - -func col(table, name string) string { - return table + "." + name -} - -func alias(col, alias string) string { - return col + " AS " + alias -} - -func coalesce(values ...string) string { - str := "COALESCE(" - for i, value := range values { - if i > 0 { - str += ", " - } - str += value - } - str += ")" - return str -} - -func eq(first, second string) string { - return first + " = " + second -} - -func leftJoin(table, alias, on string, and ...string) string { - st := table + " " + alias + " ON " + on - for _, a := range and { - st += " AND " + a - } - return st -} - -func concat(strs ...string) string { - return "CONCAT(" + strings.Join(strs, ", ") + ")" -} - -func whenThenElse(when, then, el string) string { - return "(CASE WHEN " + when + " THEN " + then + " ELSE " + el + " END)" -} +//go:embed login_name_query.sql +var loginNameViewStmt string type loginNameProjection struct{} @@ -170,7 +71,7 @@ func (*loginNameProjection) Name() string { func (*loginNameProjection) Init() *old_handler.Check { return handler.NewViewCheck( - viewStmt, + loginNameViewStmt, handler.NewSuffixedTable( []*handler.InitColumn{ handler.NewColumn(LoginNameUserIDCol, handler.ColumnTypeText), @@ -229,7 +130,9 @@ func (*loginNameProjection) Init() *old_handler.Check { }, handler.NewPrimaryKey(LoginNamePoliciesInstanceIDCol, LoginNamePoliciesResourceOwnerCol), loginNamePolicySuffix, - handler.WithIndex(handler.NewIndex("is_default", []string{LoginNamePoliciesResourceOwnerCol, LoginNamePoliciesIsDefaultCol})), + // this index is not used anymore, but kept for understanding why the default exists on existing systems, TODO: remove in login_names4 + // handler.WithIndex(handler.NewIndex("is_default", []string{LoginNamePoliciesResourceOwnerCol, LoginNamePoliciesIsDefaultCol})), + handler.WithIndex(handler.NewIndex("is_default_owner", []string{LoginNamePoliciesInstanceIDCol, LoginNamePoliciesIsDefaultCol, LoginNamePoliciesResourceOwnerCol}, handler.WithInclude(LoginNamePoliciesMustBeDomainCol))), ), ) } diff --git a/internal/query/projection/login_name_query.sql b/internal/query/projection/login_name_query.sql new file mode 100644 index 0000000000..89dc803feb --- /dev/null +++ b/internal/query/projection/login_name_query.sql @@ -0,0 +1,35 @@ +SELECT + u.id AS user_id + , CASE + WHEN p.must_be_domain THEN CONCAT(u.user_name, '@', d.name) + ELSE u.user_name + END AS login_name + , COALESCE(d.is_primary, TRUE) AS is_primary + , u.instance_id +FROM + projections.login_names3_users AS u +LEFT JOIN LATERAL ( + SELECT + must_be_domain + , is_default + FROM + projections.login_names3_policies AS p + WHERE + ( + p.instance_id = u.instance_id + AND NOT p.is_default + AND p.resource_owner = u.resource_owner + ) OR ( + p.instance_id = u.instance_id + AND p.is_default + ) + ORDER BY + p.is_default -- custom first + LIMIT 1 +) AS p ON TRUE +LEFT JOIN + projections.login_names3_domains d + ON + p.must_be_domain + AND u.resource_owner = d.resource_owner + AND u.instance_id = d.instance_id \ No newline at end of file diff --git a/internal/query/projection/user.go b/internal/query/projection/user.go index f1e0613287..d11c4855f7 100644 --- a/internal/query/projection/user.go +++ b/internal/query/projection/user.go @@ -124,6 +124,7 @@ func (*userProjection) Init() *old_handler.Check { handler.NewPrimaryKey(HumanUserInstanceIDCol, HumanUserIDCol), UserHumanSuffix, handler.WithForeignKey(handler.NewForeignKeyOfPublicKeys()), + handler.WithIndex(handler.NewIndex("email", []string{HumanUserInstanceIDCol, "LOWER(" + HumanEmailCol + ")"})), ), handler.NewSuffixedTable([]*handler.InitColumn{ handler.NewColumn(MachineUserIDCol, handler.ColumnTypeText), diff --git a/internal/query/restrictions.go b/internal/query/restrictions.go index 8cff5737f7..93d435278c 100644 --- a/internal/query/restrictions.go +++ b/internal/query/restrictions.go @@ -78,7 +78,7 @@ func (q *Queries) GetInstanceRestrictions(ctx context.Context) (restrictions Res RestrictionsColumnResourceOwner.identifier(): instanceID, }).ToSql() if err != nil { - return restrictions, zitade_errors.ThrowInternal(err, "QUERY-XnLMQ", "Errors.Query.SQLStatment") + return restrictions, zitade_errors.ThrowInternal(err, "QUERY-XnLMQ", "Errors.Query.SQLStatement") } err = q.client.QueryRowContext(ctx, func(row *sql.Row) error { restrictions, err = scan(row) diff --git a/internal/query/search_query.go b/internal/query/search_query.go index d5e09027c4..d6dd710d1e 100644 --- a/internal/query/search_query.go +++ b/internal/query/search_query.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "reflect" + "strings" "time" sq "github.com/Masterminds/squirrel" @@ -334,23 +335,23 @@ func (q *textQuery) comp() sq.Sqlizer { case TextNotEquals: return sq.NotEq{q.Column.identifier(): q.Text} case TextEqualsIgnoreCase: - return sq.ILike{q.Column.identifier(): q.Text} + return sq.Like{"LOWER(" + q.Column.identifier() + ")": strings.ToLower(q.Text)} case TextNotEqualsIgnoreCase: - return sq.NotILike{q.Column.identifier(): q.Text} + return sq.NotLike{"LOWER(" + q.Column.identifier() + ")": strings.ToLower(q.Text)} case TextStartsWith: return sq.Like{q.Column.identifier(): q.Text + "%"} case TextStartsWithIgnoreCase: - return sq.ILike{q.Column.identifier(): q.Text + "%"} + return sq.Like{"LOWER(" + q.Column.identifier() + ")": strings.ToLower(q.Text) + "%"} case TextEndsWith: return sq.Like{q.Column.identifier(): "%" + q.Text} case TextEndsWithIgnoreCase: - return sq.ILike{q.Column.identifier(): "%" + q.Text} + return sq.Like{"LOWER(" + q.Column.identifier() + ")": "%" + strings.ToLower(q.Text)} case TextContains: return sq.Like{q.Column.identifier(): "%" + q.Text + "%"} case TextContainsIgnoreCase: - return sq.ILike{q.Column.identifier(): "%" + q.Text + "%"} + return sq.Like{"LOWER(" + q.Column.identifier() + ")": "%" + strings.ToLower(q.Text) + "%"} case TextListContains: - return &listContains{col: q.Column, args: []interface{}{q.Text}} + return &listContains{col: q.Column, args: []any{q.Text}} case textCompareMax: return nil } diff --git a/internal/query/search_query_test.go b/internal/query/search_query_test.go index 13142a0158..7f6672b279 100644 --- a/internal/query/search_query_test.go +++ b/internal/query/search_query_test.go @@ -1204,7 +1204,7 @@ func TestTextQuery_comp(t *testing.T) { Compare: TextEqualsIgnoreCase, }, want: want{ - query: sq.ILike{"test_table.test_col": "Hurst"}, + query: sq.Like{"LOWER(test_table.test_col)": "hurst"}, }, }, { @@ -1226,7 +1226,7 @@ func TestTextQuery_comp(t *testing.T) { Compare: TextNotEqualsIgnoreCase, }, want: want{ - query: sq.NotILike{"test_table.test_col": "Hurst"}, + query: sq.NotLike{"LOWER(test_table.test_col)": "hurst"}, }, }, { @@ -1237,7 +1237,7 @@ func TestTextQuery_comp(t *testing.T) { Compare: TextEqualsIgnoreCase, }, want: want{ - query: sq.ILike{"test_table.test_col": "Hu\\%\\%rst"}, + query: sq.Like{"LOWER(test_table.test_col)": "hu\\%\\%rst"}, }, }, { @@ -1270,7 +1270,7 @@ func TestTextQuery_comp(t *testing.T) { Compare: TextStartsWithIgnoreCase, }, want: want{ - query: sq.ILike{"test_table.test_col": "Hurst%"}, + query: sq.Like{"LOWER(test_table.test_col)": "hurst%"}, }, }, { @@ -1281,7 +1281,7 @@ func TestTextQuery_comp(t *testing.T) { Compare: TextStartsWithIgnoreCase, }, want: want{ - query: sq.ILike{"test_table.test_col": "Hurst\\%%"}, + query: sq.Like{"LOWER(test_table.test_col)": "hurst\\%%"}, }, }, { @@ -1314,7 +1314,7 @@ func TestTextQuery_comp(t *testing.T) { Compare: TextEndsWithIgnoreCase, }, want: want{ - query: sq.ILike{"test_table.test_col": "%Hurst"}, + query: sq.Like{"LOWER(test_table.test_col)": "%hurst"}, }, }, { @@ -1325,7 +1325,7 @@ func TestTextQuery_comp(t *testing.T) { Compare: TextEndsWithIgnoreCase, }, want: want{ - query: sq.ILike{"test_table.test_col": "%\\%Hurst"}, + query: sq.Like{"LOWER(test_table.test_col)": "%\\%hurst"}, }, }, { @@ -1351,14 +1351,14 @@ func TestTextQuery_comp(t *testing.T) { }, }, { - name: "containts ignore case", + name: "contains ignore case", fields: fields{ Column: testCol, Text: "Hurst", Compare: TextContainsIgnoreCase, }, want: want{ - query: sq.ILike{"test_table.test_col": "%Hurst%"}, + query: sq.Like{"LOWER(test_table.test_col)": "%hurst%"}, }, }, { @@ -1369,11 +1369,11 @@ func TestTextQuery_comp(t *testing.T) { Compare: TextContainsIgnoreCase, }, want: want{ - query: sq.ILike{"test_table.test_col": "%\\%Hurst\\%%"}, + query: sq.Like{"LOWER(test_table.test_col)": "%\\%hurst\\%%"}, }, }, { - name: "list containts", + name: "list contains", fields: fields{ Column: testCol, Text: "Hurst", diff --git a/internal/query/secret_generators.go b/internal/query/secret_generators.go index c267d7b290..ca77bc35b5 100644 --- a/internal/query/secret_generators.go +++ b/internal/query/secret_generators.go @@ -132,7 +132,7 @@ func (q *Queries) SecretGeneratorByType(ctx context.Context, generatorType domai SecretGeneratorColumnInstanceID.identifier(): instanceID, }).ToSql() if err != nil { - return nil, zerrors.ThrowInternal(err, "QUERY-3k99f", "Errors.Query.SQLStatment") + return nil, zerrors.ThrowInternal(err, "QUERY-3k99f", "Errors.Query.SQLStatement") } err = q.client.QueryRowContext(ctx, func(row *sql.Row) error { diff --git a/internal/query/security_policy.go b/internal/query/security_policy.go index 7a3fb3fa89..5a2450258e 100644 --- a/internal/query/security_policy.go +++ b/internal/query/security_policy.go @@ -67,7 +67,7 @@ func (q *Queries) SecurityPolicy(ctx context.Context) (policy *SecurityPolicy, e SecurityPolicyColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), }).ToSql() if err != nil { - return nil, zerrors.ThrowInternal(err, "QUERY-Sf6d1", "Errors.Query.SQLStatment") + return nil, zerrors.ThrowInternal(err, "QUERY-Sf6d1", "Errors.Query.SQLStatement") } err = q.client.QueryRowContext(ctx, func(row *sql.Row) error { diff --git a/internal/query/user.go b/internal/query/user.go index 6844982f07..ac3eb79fc9 100644 --- a/internal/query/user.go +++ b/internal/query/user.go @@ -200,21 +200,15 @@ var ( userLoginNamesTable = loginNameTable.setAlias("login_names") userLoginNamesUserIDCol = LoginNameUserIDCol.setTable(userLoginNamesTable) - userLoginNamesNameCol = LoginNameNameCol.setTable(userLoginNamesTable) userLoginNamesInstanceIDCol = LoginNameInstanceIDCol.setTable(userLoginNamesTable) userLoginNamesListCol = Column{ - name: "loginnames", + name: "login_names", table: userLoginNamesTable, } - userLoginNamesLowerListCol = Column{ - name: "loginnames_lower", + userPreferredLoginNameCol = Column{ + name: "preferred_login_name", table: userLoginNamesTable, } - userPreferredLoginNameTable = loginNameTable.setAlias("preferred_login_name") - userPreferredLoginNameUserIDCol = LoginNameUserIDCol.setTable(userPreferredLoginNameTable) - userPreferredLoginNameCol = LoginNameNameCol.setTable(userPreferredLoginNameTable) - userPreferredLoginNameIsPrimaryCol = LoginNameIsPrimaryCol.setTable(userPreferredLoginNameTable) - userPreferredLoginNameInstanceIDCol = LoginNameInstanceIDCol.setTable(userPreferredLoginNameTable) ) var ( @@ -459,7 +453,7 @@ func (q *Queries) GetHumanProfile(ctx context.Context, userID string, queries .. } stmt, args, err := query.Where(eq).ToSql() if err != nil { - return nil, zerrors.ThrowInternal(err, "QUERY-Dgbg2", "Errors.Query.SQLStatment") + return nil, zerrors.ThrowInternal(err, "QUERY-Dgbg2", "Errors.Query.SQLStatement") } err = q.client.QueryRowContext(ctx, func(row *sql.Row) error { @@ -483,7 +477,7 @@ func (q *Queries) GetHumanEmail(ctx context.Context, userID string, queries ...S } stmt, args, err := query.Where(eq).ToSql() if err != nil { - return nil, zerrors.ThrowInternal(err, "QUERY-BHhj3", "Errors.Query.SQLStatment") + return nil, zerrors.ThrowInternal(err, "QUERY-BHhj3", "Errors.Query.SQLStatement") } err = q.client.QueryRowContext(ctx, func(row *sql.Row) error { @@ -507,7 +501,7 @@ func (q *Queries) GetHumanPhone(ctx context.Context, userID string, queries ...S } stmt, args, err := query.Where(eq).ToSql() if err != nil { - return nil, zerrors.ThrowInternal(err, "QUERY-Dg43g", "Errors.Query.SQLStatment") + return nil, zerrors.ThrowInternal(err, "QUERY-Dg43g", "Errors.Query.SQLStatement") } err = q.client.QueryRowContext(ctx, func(row *sql.Row) error { @@ -593,7 +587,7 @@ func (q *Queries) GetNotifyUser(ctx context.Context, shouldTriggered bool, queri } stmt, args, err := query.Where(eq).ToSql() if err != nil { - return nil, zerrors.ThrowInternal(err, "QUERY-Err3g", "Errors.Query.SQLStatment") + return nil, zerrors.ThrowInternal(err, "QUERY-Err3g", "Errors.Query.SQLStatement") } err = q.client.QueryRowContext(ctx, func(row *sql.Row) error { @@ -611,7 +605,7 @@ func (q *Queries) CountUsers(ctx context.Context, queries *UserSearchQueries) (c eq := sq.Eq{UserInstanceIDCol.identifier(): authz.GetInstance(ctx).InstanceID()} stmt, args, err := queries.toQuery(query).Where(eq).ToSql() if err != nil { - return 0, zerrors.ThrowInternal(err, "QUERY-w3Dx", "Errors.Query.SQLStatment") + return 0, zerrors.ThrowInternal(err, "QUERY-w3Dx", "Errors.Query.SQLStatement") } err = q.client.QueryContext(ctx, func(rows *sql.Rows) error { @@ -646,7 +640,7 @@ func (q *Queries) searchUsers(ctx context.Context, queries *UserSearchQueries, p UserInstanceIDCol.identifier(): authz.GetInstance(ctx).InstanceID(), }).ToSql() if err != nil { - return nil, zerrors.ThrowInternal(err, "QUERY-Dgbg2", "Errors.Query.SQLStatment") + return nil, zerrors.ThrowInternal(err, "QUERY-Dgbg2", "Errors.Query.SQLStatement") } err = q.client.QueryContext(ctx, func(rows *sql.Rows) error { @@ -693,7 +687,7 @@ func (q *Queries) IsUserUnique(ctx context.Context, username, email, resourceOwn eq := sq.Eq{UserInstanceIDCol.identifier(): authz.GetInstance(ctx).InstanceID()} stmt, args, err := query.Where(eq).ToSql() if err != nil { - return false, zerrors.ThrowInternal(err, "QUERY-Dg43g", "Errors.Query.SQLStatment") + return false, zerrors.ThrowInternal(err, "QUERY-Dg43g", "Errors.Query.SQLStatement") } err = q.client.QueryRowContext(ctx, func(row *sql.Row) error { @@ -792,12 +786,8 @@ func NewUserPreferredLoginNameSearchQuery(value string, comparison TextCompariso return NewTextQuery(userPreferredLoginNameCol, value, comparison) } -func NewUserLoginNamesSearchQuery(value string) (SearchQuery, error) { - return NewTextQuery(userLoginNamesLowerListCol, strings.ToLower(value), TextListContains) -} - func NewUserLoginNameExistsQuery(value string, comparison TextComparison) (SearchQuery, error) { - // linking queries for the subselect + // linking queries for the sub select instanceQuery, err := NewColumnComparisonQuery(LoginNameInstanceIDCol, UserInstanceIDCol, ColumnEquals) if err != nil { return nil, err @@ -828,30 +818,16 @@ func triggerUserProjections(ctx context.Context) { triggerBatch(ctx, projection.UserProjection, projection.LoginNameProjection) } -func prepareLoginNamesQuery() (string, []interface{}, error) { - return sq.Select( - userLoginNamesUserIDCol.identifier(), - "ARRAY_AGG("+userLoginNamesNameCol.identifier()+")::TEXT[] AS "+userLoginNamesListCol.name, - "ARRAY_AGG(LOWER("+userLoginNamesNameCol.identifier()+"))::TEXT[] AS "+userLoginNamesLowerListCol.name, - userLoginNamesInstanceIDCol.identifier(), - ).From(userLoginNamesTable.identifier()). - GroupBy( - userLoginNamesUserIDCol.identifier(), - userLoginNamesInstanceIDCol.identifier(), - ).ToSql() -} - -func preparePreferredLoginNamesQuery() (string, []interface{}, error) { - return sq.Select( - userPreferredLoginNameUserIDCol.identifier(), - userPreferredLoginNameCol.identifier(), - userPreferredLoginNameInstanceIDCol.identifier(), - ).From(userPreferredLoginNameTable.identifier()). - Where(sq.Eq{ - userPreferredLoginNameIsPrimaryCol.identifier(): true, - }, - ).ToSql() -} +var joinLoginNames = `LEFT JOIN LATERAL (` + + `SELECT` + + ` ARRAY_AGG(ln.login_name ORDER BY ln.login_name) AS login_names,` + + ` MAX(CASE WHEN ln.is_primary THEN ln.login_name ELSE NULL END) AS preferred_login_name` + + ` FROM` + + ` projections.login_names3 AS ln` + + ` WHERE` + + ` ln.user_id = ` + UserIDCol.identifier() + + ` AND ln.instance_id = ` + UserInstanceIDCol.identifier() + + `) AS login_names ON TRUE` func scanUser(row *sql.Row) (*User, error) { u := new(User) @@ -951,64 +927,6 @@ func scanUser(row *sql.Row) (*User, error) { return u, nil } -func prepareUserQuery() (sq.SelectBuilder, func(*sql.Row) (*User, error)) { - loginNamesQuery, loginNamesArgs, err := prepareLoginNamesQuery() - if err != nil { - return sq.SelectBuilder{}, nil - } - preferredLoginNameQuery, preferredLoginNameArgs, err := preparePreferredLoginNamesQuery() - if err != nil { - return sq.SelectBuilder{}, nil - } - return sq.Select( - UserIDCol.identifier(), - UserCreationDateCol.identifier(), - UserChangeDateCol.identifier(), - UserResourceOwnerCol.identifier(), - UserSequenceCol.identifier(), - UserStateCol.identifier(), - UserTypeCol.identifier(), - UserUsernameCol.identifier(), - userLoginNamesListCol.identifier(), - userPreferredLoginNameCol.identifier(), - HumanUserIDCol.identifier(), - HumanFirstNameCol.identifier(), - HumanLastNameCol.identifier(), - HumanNickNameCol.identifier(), - HumanDisplayNameCol.identifier(), - HumanPreferredLanguageCol.identifier(), - HumanGenderCol.identifier(), - HumanAvatarURLCol.identifier(), - HumanEmailCol.identifier(), - HumanIsEmailVerifiedCol.identifier(), - HumanPhoneCol.identifier(), - HumanIsPhoneVerifiedCol.identifier(), - HumanPasswordChangeRequiredCol.identifier(), - HumanPasswordChangedCol.identifier(), - HumanMFAInitSkippedCol.identifier(), - MachineUserIDCol.identifier(), - MachineNameCol.identifier(), - MachineDescriptionCol.identifier(), - MachineSecretCol.identifier(), - MachineAccessTokenTypeCol.identifier(), - countColumn.identifier(), - ). - From(userTable.identifier()). - LeftJoin(join(HumanUserIDCol, UserIDCol)). - LeftJoin(join(MachineUserIDCol, UserIDCol)). - LeftJoin("("+loginNamesQuery+") AS "+userLoginNamesTable.alias+" ON "+ - userLoginNamesUserIDCol.identifier()+" = "+UserIDCol.identifier()+" AND "+ - userLoginNamesInstanceIDCol.identifier()+" = "+UserInstanceIDCol.identifier(), - loginNamesArgs...). - LeftJoin("("+preferredLoginNameQuery+") AS "+userPreferredLoginNameTable.alias+" ON "+ - userPreferredLoginNameUserIDCol.identifier()+" = "+UserIDCol.identifier()+" AND "+ - userPreferredLoginNameInstanceIDCol.identifier()+" = "+UserInstanceIDCol.identifier(), - preferredLoginNameArgs...). - PlaceholderFormat(sq.Dollar), - - scanUser -} - func prepareProfileQuery() (sq.SelectBuilder, func(*sql.Row) (*Profile, error)) { return sq.Select( UserIDCol.identifier(), @@ -1170,14 +1088,6 @@ func preparePhoneQuery() (sq.SelectBuilder, func(*sql.Row) (*Phone, error)) { } func prepareNotifyUserQuery() (sq.SelectBuilder, func(*sql.Row) (*NotifyUser, error)) { - loginNamesQuery, loginNamesArgs, err := prepareLoginNamesQuery() - if err != nil { - return sq.SelectBuilder{}, nil - } - preferredLoginNameQuery, preferredLoginNameArgs, err := preparePreferredLoginNamesQuery() - if err != nil { - return sq.SelectBuilder{}, nil - } return sq.Select( UserIDCol.identifier(), UserCreationDateCol.identifier(), @@ -1208,14 +1118,7 @@ func prepareNotifyUserQuery() (sq.SelectBuilder, func(*sql.Row) (*NotifyUser, er From(userTable.identifier()). LeftJoin(join(HumanUserIDCol, UserIDCol)). LeftJoin(join(NotifyUserIDCol, UserIDCol)). - LeftJoin("("+loginNamesQuery+") AS "+userLoginNamesTable.alias+" ON "+ - userLoginNamesUserIDCol.identifier()+" = "+UserIDCol.identifier()+" AND "+ - userLoginNamesInstanceIDCol.identifier()+" = "+UserInstanceIDCol.identifier(), - loginNamesArgs...). - LeftJoin("("+preferredLoginNameQuery+") AS "+userPreferredLoginNameTable.alias+" ON "+ - userPreferredLoginNameUserIDCol.identifier()+" = "+UserIDCol.identifier()+" AND "+ - userPreferredLoginNameInstanceIDCol.identifier()+" = "+UserInstanceIDCol.identifier(), - preferredLoginNameArgs...). + JoinClause(joinLoginNames). PlaceholderFormat(sq.Dollar), scanNotifyUser } @@ -1359,14 +1262,6 @@ func prepareUserUniqueQuery() (sq.SelectBuilder, func(*sql.Row) (bool, error)) { } func prepareUsersQuery() (sq.SelectBuilder, func(*sql.Rows) (*Users, error)) { - loginNamesQuery, loginNamesArgs, err := prepareLoginNamesQuery() - if err != nil { - return sq.SelectBuilder{}, nil - } - preferredLoginNameQuery, preferredLoginNameArgs, err := preparePreferredLoginNamesQuery() - if err != nil { - return sq.SelectBuilder{}, nil - } return sq.Select( UserIDCol.identifier(), UserCreationDateCol.identifier(), @@ -1401,14 +1296,7 @@ func prepareUsersQuery() (sq.SelectBuilder, func(*sql.Rows) (*Users, error)) { From(userTable.identifier()). LeftJoin(join(HumanUserIDCol, UserIDCol)). LeftJoin(join(MachineUserIDCol, UserIDCol)). - LeftJoin("("+loginNamesQuery+") AS "+userLoginNamesTable.alias+" ON "+ - userLoginNamesUserIDCol.identifier()+" = "+UserIDCol.identifier()+" AND "+ - userLoginNamesInstanceIDCol.identifier()+" = "+UserInstanceIDCol.identifier(), - loginNamesArgs...). - LeftJoin("("+preferredLoginNameQuery+") AS "+userPreferredLoginNameTable.alias+" ON "+ - userPreferredLoginNameUserIDCol.identifier()+" = "+UserIDCol.identifier()+" AND "+ - userPreferredLoginNameInstanceIDCol.identifier()+" = "+UserInstanceIDCol.identifier(), - preferredLoginNameArgs...). + JoinClause(joinLoginNames). PlaceholderFormat(sq.Dollar), func(rows *sql.Rows) (*Users, error) { users := make([]*User, 0) diff --git a/internal/query/user_by_id.sql b/internal/query/user_by_id.sql index 2ce741f9b7..a89e701698 100644 --- a/internal/query/user_by_id.sql +++ b/internal/query/user_by_id.sql @@ -1,41 +1,3 @@ -WITH login_names AS (SELECT - u.id user_id - , u.instance_id - , u.resource_owner - , u.user_name - , d.name domain_name - , d.is_primary - , p.must_be_domain - , CASE WHEN p.must_be_domain - THEN concat(u.user_name, '@', d.name) - ELSE u.user_name - END login_name - FROM - projections.login_names3_users u - JOIN lateral ( - SELECT - p.must_be_domain - FROM - projections.login_names3_policies p - WHERE - u.instance_id = p.instance_id - AND ( - (p.is_default IS TRUE AND p.instance_id = $3) - OR (p.instance_id = $3 AND p.resource_owner = u.resource_owner) - ) - ORDER BY is_default - LIMIT 1 - ) p ON TRUE - JOIN - projections.login_names3_domains d - ON - u.instance_id = d.instance_id - AND u.resource_owner = d.resource_owner - WHERE - u.id = $1 - AND (u.resource_owner = $2 OR $2 = '') - AND u.instance_id = $3 -) SELECT u.id , u.creation_date @@ -45,8 +7,8 @@ SELECT , u.state , u.type , u.username - , (SELECT array_agg(ln.login_name)::TEXT[] login_names FROM login_names ln GROUP BY ln.user_id, ln.instance_id) login_names - , (SELECT ln.login_name login_names_lower FROM login_names ln WHERE ln.is_primary IS TRUE) preferred_login_name + , login_names.login_names AS login_names + , login_names.preferred_login_name AS preferred_login_name , h.user_id , h.first_name , h.last_name @@ -79,6 +41,16 @@ LEFT JOIN ON u.id = m.user_id AND u.instance_id = m.instance_id +LEFT JOIN LATERAL ( + SELECT + ARRAY_AGG(ln.login_name ORDER BY ln.login_name) AS login_names, + MAX(CASE WHEN ln.is_primary THEN ln.login_name ELSE NULL END) AS preferred_login_name + FROM + projections.login_names3 AS ln + WHERE + ln.user_id = u.id + AND ln.instance_id = u.instance_id +) AS login_names ON TRUE WHERE u.id = $1 AND (u.resource_owner = $2 OR $2 = '') diff --git a/internal/query/user_metadata.go b/internal/query/user_metadata.go index ff612f82c8..534c707593 100644 --- a/internal/query/user_metadata.go +++ b/internal/query/user_metadata.go @@ -97,7 +97,7 @@ func (q *Queries) GetUserMetadataByKey(ctx context.Context, shouldTriggerBulk bo } stmt, args, err := query.Where(eq).ToSql() if err != nil { - return nil, zerrors.ThrowInternal(err, "QUERY-aDGG2", "Errors.Query.SQLStatment") + return nil, zerrors.ThrowInternal(err, "QUERY-aDGG2", "Errors.Query.SQLStatement") } err = q.client.QueryRowContext(ctx, func(row *sql.Row) error { @@ -125,7 +125,7 @@ func (q *Queries) SearchUserMetadataForUsers(ctx context.Context, shouldTriggerB } stmt, args, err := queries.toQuery(query).Where(eq).ToSql() if err != nil { - return nil, zerrors.ThrowInternal(err, "QUERY-Egbgd", "Errors.Query.SQLStatment") + return nil, zerrors.ThrowInternal(err, "QUERY-Egbgd", "Errors.Query.SQLStatement") } err = q.client.QueryContext(ctx, func(rows *sql.Rows) error { @@ -157,7 +157,7 @@ func (q *Queries) SearchUserMetadata(ctx context.Context, shouldTriggerBulk bool } stmt, args, err := queries.toQuery(query).Where(eq).ToSql() if err != nil { - return nil, zerrors.ThrowInternal(err, "QUERY-Egbgd", "Errors.Query.SQLStatment") + return nil, zerrors.ThrowInternal(err, "QUERY-Egbgd", "Errors.Query.SQLStatement") } err = q.client.QueryContext(ctx, func(rows *sql.Rows) error { diff --git a/internal/query/user_notify_by_id.sql b/internal/query/user_notify_by_id.sql index 10aa60ee60..6322229a91 100644 --- a/internal/query/user_notify_by_id.sql +++ b/internal/query/user_notify_by_id.sql @@ -1,41 +1,3 @@ -WITH login_names AS ( - SELECT - u.id user_id - , u.instance_id - , u.resource_owner - , u.user_name - , d.name domain_name - , d.is_primary - , p.must_be_domain - , CASE WHEN p.must_be_domain - THEN concat(u.user_name, '@', d.name) - ELSE u.user_name - END login_name - FROM - projections.login_names3_users u - JOIN lateral ( - SELECT - p.must_be_domain - FROM - projections.login_names3_policies p - WHERE - u.instance_id = p.instance_id - AND ( - (p.is_default IS TRUE AND p.instance_id = $2) - OR (p.instance_id = $2 AND p.resource_owner = u.resource_owner) - ) - ORDER BY is_default - LIMIT 1 - ) p ON TRUE - JOIN - projections.login_names3_domains d - ON - u.instance_id = d.instance_id - AND u.resource_owner = d.resource_owner - WHERE - u.instance_id = $2 - AND u.id = $1 -) SELECT u.id , u.creation_date @@ -45,8 +7,8 @@ SELECT , u.state , u.type , u.username - , (SELECT array_agg(ln.login_name)::TEXT[] login_names FROM login_names ln GROUP BY ln.user_id, ln.instance_id) login_names - , (SELECT ln.login_name login_names_lower FROM login_names ln WHERE ln.is_primary IS TRUE) preferred_login_name + , login_names.login_names AS login_names + , login_names.preferred_login_name AS preferred_login_name , h.user_id , h.first_name , h.last_name @@ -73,6 +35,16 @@ LEFT JOIN ON u.id = n.user_id AND u.instance_id = n.instance_id +LEFT JOIN LATERAL ( + SELECT + ARRAY_AGG(ln.login_name ORDER BY ln.login_name) AS login_names, + MAX(CASE WHEN ln.is_primary THEN ln.login_name ELSE NULL END) AS preferred_login_name + FROM + projections.login_names3 AS ln + WHERE + ln.user_id = u.id + AND ln.instance_id = u.instance_id +) AS login_names ON TRUE WHERE u.id = $1 AND u.instance_id = $2 diff --git a/internal/query/user_personal_access_token.go b/internal/query/user_personal_access_token.go index 61d349961c..49281d9f90 100644 --- a/internal/query/user_personal_access_token.go +++ b/internal/query/user_personal_access_token.go @@ -118,7 +118,7 @@ func (q *Queries) PersonalAccessTokenByID(ctx context.Context, shouldTriggerBulk } stmt, args, err := query.Where(eq).ToSql() if err != nil { - return nil, zerrors.ThrowInternal(err, "QUERY-Dgfb4", "Errors.Query.SQLStatment") + return nil, zerrors.ThrowInternal(err, "QUERY-Dgfb4", "Errors.Query.SQLStatement") } err = q.client.QueryRowContext(ctx, func(row *sql.Row) error { diff --git a/internal/query/user_test.go b/internal/query/user_test.go index 50d65cc1ec..ae5f6be207 100644 --- a/internal/query/user_test.go +++ b/internal/query/user_test.go @@ -222,87 +222,6 @@ func TestUser_userCheckPermission(t *testing.T) { } var ( - loginNamesQuery = `SELECT login_names.user_id, ARRAY_AGG(login_names.login_name)::TEXT[] AS loginnames, ARRAY_AGG(LOWER(login_names.login_name))::TEXT[] AS loginnames_lower, login_names.instance_id` + - ` FROM projections.login_names3 AS login_names` + - ` GROUP BY login_names.user_id, login_names.instance_id` - preferredLoginNameQuery = `SELECT preferred_login_name.user_id, preferred_login_name.login_name, preferred_login_name.instance_id` + - ` FROM projections.login_names3 AS preferred_login_name` + - ` WHERE preferred_login_name.is_primary = $1` - userQuery = `SELECT projections.users14.id,` + - ` projections.users14.creation_date,` + - ` projections.users14.change_date,` + - ` projections.users14.resource_owner,` + - ` projections.users14.sequence,` + - ` projections.users14.state,` + - ` projections.users14.type,` + - ` projections.users14.username,` + - ` login_names.loginnames,` + - ` preferred_login_name.login_name,` + - ` projections.users14_humans.user_id,` + - ` projections.users14_humans.first_name,` + - ` projections.users14_humans.last_name,` + - ` projections.users14_humans.nick_name,` + - ` projections.users14_humans.display_name,` + - ` projections.users14_humans.preferred_language,` + - ` projections.users14_humans.gender,` + - ` projections.users14_humans.avatar_key,` + - ` projections.users14_humans.email,` + - ` projections.users14_humans.is_email_verified,` + - ` projections.users14_humans.phone,` + - ` projections.users14_humans.is_phone_verified,` + - ` projections.users14_humans.password_change_required,` + - ` projections.users14_humans.password_changed,` + - ` projections.users14_humans.mfa_init_skipped,` + - ` projections.users14_machines.user_id,` + - ` projections.users14_machines.name,` + - ` projections.users14_machines.description,` + - ` projections.users14_machines.secret,` + - ` projections.users14_machines.access_token_type,` + - ` COUNT(*) OVER ()` + - ` FROM projections.users14` + - ` LEFT JOIN projections.users14_humans ON projections.users14.id = projections.users14_humans.user_id AND projections.users14.instance_id = projections.users14_humans.instance_id` + - ` LEFT JOIN projections.users14_machines ON projections.users14.id = projections.users14_machines.user_id AND projections.users14.instance_id = projections.users14_machines.instance_id` + - ` LEFT JOIN` + - ` (` + loginNamesQuery + `) AS login_names` + - ` ON login_names.user_id = projections.users14.id AND login_names.instance_id = projections.users14.instance_id` + - ` LEFT JOIN` + - ` (` + preferredLoginNameQuery + `) AS preferred_login_name` + - ` ON preferred_login_name.user_id = projections.users14.id AND preferred_login_name.instance_id = projections.users14.instance_id` - userCols = []string{ - "id", - "creation_date", - "change_date", - "resource_owner", - "sequence", - "state", - "type", - "username", - "loginnames", - "login_name", - // human - "user_id", - "first_name", - "last_name", - "nick_name", - "display_name", - "preferred_language", - "gender", - "avatar_key", - "email", - "is_email_verified", - "phone", - "is_phone_verified", - "password_change_required", - "password_changed", - "mfa_init_skipped", - // machine - "user_id", - "name", - "description", - "secret", - "access_token_type", - "count", - } profileQuery = `SELECT projections.users14.id,` + ` projections.users14.creation_date,` + ` projections.users14.change_date,` + @@ -397,8 +316,8 @@ var ( ` projections.users14.state,` + ` projections.users14.type,` + ` projections.users14.username,` + - ` login_names.loginnames,` + - ` preferred_login_name.login_name,` + + ` login_names.login_names,` + + ` login_names.preferred_login_name,` + ` projections.users14_humans.user_id,` + ` projections.users14_humans.first_name,` + ` projections.users14_humans.last_name,` + @@ -417,12 +336,7 @@ var ( ` FROM projections.users14` + ` LEFT JOIN projections.users14_humans ON projections.users14.id = projections.users14_humans.user_id AND projections.users14.instance_id = projections.users14_humans.instance_id` + ` LEFT JOIN projections.users14_notifications ON projections.users14.id = projections.users14_notifications.user_id AND projections.users14.instance_id = projections.users14_notifications.instance_id` + - ` LEFT JOIN` + - ` (` + loginNamesQuery + `) AS login_names` + - ` ON login_names.user_id = projections.users14.id AND login_names.instance_id = projections.users14.instance_id` + - ` LEFT JOIN` + - ` (` + preferredLoginNameQuery + `) AS preferred_login_name` + - ` ON preferred_login_name.user_id = projections.users14.id AND preferred_login_name.instance_id = projections.users14.instance_id` + ` LEFT JOIN LATERAL (SELECT ARRAY_AGG(ln.login_name ORDER BY ln.login_name) AS login_names, MAX(CASE WHEN ln.is_primary THEN ln.login_name ELSE NULL END) AS preferred_login_name FROM projections.login_names3 AS ln WHERE ln.user_id = projections.users14.id AND ln.instance_id = projections.users14.instance_id) AS login_names ON TRUE` notifyUserCols = []string{ "id", "creation_date", @@ -432,8 +346,8 @@ var ( "state", "type", "username", - "loginnames", - "login_name", + "login_names", + "preferred_login_name", // human "user_id", "first_name", @@ -460,8 +374,8 @@ var ( ` projections.users14.state,` + ` projections.users14.type,` + ` projections.users14.username,` + - ` login_names.loginnames,` + - ` preferred_login_name.login_name,` + + ` login_names.login_names,` + + ` login_names.preferred_login_name,` + ` projections.users14_humans.user_id,` + ` projections.users14_humans.first_name,` + ` projections.users14_humans.last_name,` + @@ -485,12 +399,7 @@ var ( ` FROM projections.users14` + ` LEFT JOIN projections.users14_humans ON projections.users14.id = projections.users14_humans.user_id AND projections.users14.instance_id = projections.users14_humans.instance_id` + ` LEFT JOIN projections.users14_machines ON projections.users14.id = projections.users14_machines.user_id AND projections.users14.instance_id = projections.users14_machines.instance_id` + - ` LEFT JOIN` + - ` (` + loginNamesQuery + `) AS login_names` + - ` ON login_names.user_id = projections.users14.id AND login_names.instance_id = projections.users14.instance_id` + - ` LEFT JOIN` + - ` (` + preferredLoginNameQuery + `) AS preferred_login_name` + - ` ON preferred_login_name.user_id = projections.users14.id AND preferred_login_name.instance_id = projections.users14.instance_id` + ` LEFT JOIN LATERAL (SELECT ARRAY_AGG(ln.login_name ORDER BY ln.login_name) AS login_names, MAX(CASE WHEN ln.is_primary THEN ln.login_name ELSE NULL END) AS preferred_login_name FROM projections.login_names3 AS ln WHERE ln.user_id = projections.users14.id AND ln.instance_id = projections.users14.instance_id) AS login_names ON TRUE` usersCols = []string{ "id", "creation_date", @@ -500,8 +409,8 @@ var ( "state", "type", "username", - "loginnames", - "login_name", + "login_names", + "preferred_login_name", // human "user_id", "first_name", @@ -540,240 +449,6 @@ func Test_UserPrepares(t *testing.T) { want want object interface{} }{ - { - name: "prepareUserQuery no result", - prepare: prepareUserQuery, - want: want{ - sqlExpectations: mockQueryScanErr( - regexp.QuoteMeta(userQuery), - nil, - nil, - ), - err: func(err error) (error, bool) { - if !zerrors.IsNotFound(err) { - return fmt.Errorf("err should be zitadel.NotFoundError got: %w", err), false - } - return nil, true - }, - }, - object: (*User)(nil), - }, - { - name: "prepareUserQuery human found", - prepare: prepareUserQuery, - want: want{ - sqlExpectations: mockQuery( - regexp.QuoteMeta(userQuery), - userCols, - []driver.Value{ - "id", - testNow, - testNow, - "resource_owner", - uint64(20211108), - domain.UserStateActive, - domain.UserTypeHuman, - "username", - database.TextArray[string]{"login_name1", "login_name2"}, - "login_name1", - // human - "id", - "first_name", - "last_name", - "nick_name", - "display_name", - "de", - domain.GenderUnspecified, - "avatar_key", - "email", - true, - "phone", - true, - true, - testNow, - testNow, - // machine - nil, - nil, - nil, - nil, - nil, - 1, - }, - ), - }, - object: &User{ - ID: "id", - CreationDate: testNow, - ChangeDate: testNow, - ResourceOwner: "resource_owner", - Sequence: 20211108, - State: domain.UserStateActive, - Type: domain.UserTypeHuman, - Username: "username", - LoginNames: database.TextArray[string]{"login_name1", "login_name2"}, - PreferredLoginName: "login_name1", - Human: &Human{ - FirstName: "first_name", - LastName: "last_name", - NickName: "nick_name", - DisplayName: "display_name", - AvatarKey: "avatar_key", - PreferredLanguage: language.German, - Gender: domain.GenderUnspecified, - Email: "email", - IsEmailVerified: true, - Phone: "phone", - IsPhoneVerified: true, - PasswordChangeRequired: true, - PasswordChanged: testNow, - MFAInitSkipped: testNow, - }, - }, - }, - { - name: "prepareUserQuery machine found", - prepare: prepareUserQuery, - want: want{ - sqlExpectations: mockQuery( - regexp.QuoteMeta(userQuery), - userCols, - []driver.Value{ - "id", - testNow, - testNow, - "resource_owner", - uint64(20211108), - domain.UserStateActive, - domain.UserTypeMachine, - "username", - database.TextArray[string]{"login_name1", "login_name2"}, - "login_name1", - // human - nil, - nil, - nil, - nil, - nil, - nil, - nil, - nil, - nil, - nil, - nil, - nil, - nil, - nil, - nil, - // machine - "id", - "name", - "description", - nil, - domain.OIDCTokenTypeBearer, - 1, - }, - ), - }, - object: &User{ - ID: "id", - CreationDate: testNow, - ChangeDate: testNow, - ResourceOwner: "resource_owner", - Sequence: 20211108, - State: domain.UserStateActive, - Type: domain.UserTypeMachine, - Username: "username", - LoginNames: database.TextArray[string]{"login_name1", "login_name2"}, - PreferredLoginName: "login_name1", - Machine: &Machine{ - Name: "name", - Description: "description", - EncodedSecret: "", - AccessTokenType: domain.OIDCTokenTypeBearer, - }, - }, - }, - { - name: "prepareUserQuery machine with secret found", - prepare: prepareUserQuery, - want: want{ - sqlExpectations: mockQuery( - regexp.QuoteMeta(userQuery), - userCols, - []driver.Value{ - "id", - testNow, - testNow, - "resource_owner", - uint64(20211108), - domain.UserStateActive, - domain.UserTypeMachine, - "username", - database.TextArray[string]{"login_name1", "login_name2"}, - "login_name1", - // human - nil, - nil, - nil, - nil, - nil, - nil, - nil, - nil, - nil, - nil, - nil, - nil, - nil, - nil, - nil, - // machine - "id", - "name", - "description", - "secret", - domain.OIDCTokenTypeBearer, - 1, - }, - ), - }, - object: &User{ - ID: "id", - CreationDate: testNow, - ChangeDate: testNow, - ResourceOwner: "resource_owner", - Sequence: 20211108, - State: domain.UserStateActive, - Type: domain.UserTypeMachine, - Username: "username", - LoginNames: database.TextArray[string]{"login_name1", "login_name2"}, - PreferredLoginName: "login_name1", - Machine: &Machine{ - Name: "name", - Description: "description", - EncodedSecret: "secret", - AccessTokenType: domain.OIDCTokenTypeBearer, - }, - }, - }, - { - name: "prepareUserQuery sql err", - prepare: prepareUserQuery, - want: want{ - sqlExpectations: mockQueryErr( - regexp.QuoteMeta(userQuery), - sql.ErrConnDone, - ), - err: func(err error) (error, bool) { - if !errors.Is(err, sql.ErrConnDone) { - return fmt.Errorf("err should be sql.ErrConnDone got: %w", err), false - } - return nil, true - }, - }, - object: (*User)(nil), - }, { name: "prepareProfileQuery no result", prepare: prepareProfileQuery, diff --git a/load-test/src/org.ts b/load-test/src/org.ts index f5655432a5..1ed6778d9c 100644 --- a/load-test/src/org.ts +++ b/load-test/src/org.ts @@ -13,7 +13,7 @@ export function createOrg(accessToken: string): Promise { return new Promise((resolve, reject) => { let response = http.asyncRequest( 'POST', - url('/v2beta/organizations'), + url('/v2/organizations'), JSON.stringify({ name: `load-test-${new Date(Date.now()).toISOString()}`, }), diff --git a/load-test/src/use_cases/manipulate_user.ts b/load-test/src/use_cases/manipulate_user.ts index 2ea53bd324..104d81678e 100644 --- a/load-test/src/use_cases/manipulate_user.ts +++ b/load-test/src/use_cases/manipulate_user.ts @@ -15,7 +15,7 @@ export async function setup() { } export default async function (data: any) { - const human = await createHuman(`vu-${__VU}`, data.org, data.tokens.accessToken); + const human = await createHuman(`vu-${__VU}-${new Date(Date.now()).getTime()}`, data.org, data.tokens.accessToken); const updateRes = await updateHuman( { profile: { diff --git a/load-test/src/user.ts b/load-test/src/user.ts index 86ce71fd9b..83a6bba839 100644 --- a/load-test/src/user.ts +++ b/load-test/src/user.ts @@ -30,7 +30,7 @@ export function createHuman(username: string, org: Org, accessToken: string): Pr familyName: 'Zitizen', }, email: { - email: `zitizen-@caos.ch`, + email: `${username}@zitadel.com`, isVerified: true, }, password: { @@ -50,11 +50,11 @@ export function createHuman(username: string, org: Org, accessToken: string): Pr response .then((res) => { check(res, { - 'create user is status ok': (r) => r.status === 201, + 'create user is status ok': (r) => r.status === 200, }) || reject(`unable to create user(username: ${username}) status: ${res.status} body: ${res.body}`); createHumanTrend.add(res.timings.duration); - const user = http.get(url(`/v2beta/users/${res.json('userId')!}`), { + const user = http.get(url(`/v2/users/${res.json('userId')!}`), { headers: { authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json', From c1cda9bfac63ea2a34a7fda555781ec8ba5583bb Mon Sep 17 00:00:00 2001 From: Ramon Date: Tue, 10 Jun 2025 09:48:46 +0200 Subject: [PATCH 73/76] fix: metadata decoding and encoding #9816 (#10024) # Which Problems Are Solved Metadata encoding and decoding on the organization detail page was broken due to use of the old, generated gRPC client. # How the Problems Are Solved The metadata values are now correctly base64 decoded and encoded on the organization detail page. # Additional Changes Refactored parts of the code to remove the dependency on the buffer npm package, replacing it with the browser-native TextEncoder and TextDecoder APIs. # Additional Context - Closes [#9816](https://github.com/zitadel/zitadel/issues/9816) --- .../metadata-dialog/metadata-dialog.component.ts | 4 ++-- .../modules/metadata/metadata/metadata.component.ts | 12 ++++++------ .../pages/orgs/org-detail/org-detail.component.ts | 8 ++++---- .../user-detail/user-detail/user-detail.component.ts | 3 +-- 4 files changed, 13 insertions(+), 14 deletions(-) diff --git a/console/src/app/modules/metadata/metadata-dialog/metadata-dialog.component.ts b/console/src/app/modules/metadata/metadata-dialog/metadata-dialog.component.ts index 8deff09eee..c75e15bf04 100644 --- a/console/src/app/modules/metadata/metadata-dialog/metadata-dialog.component.ts +++ b/console/src/app/modules/metadata/metadata-dialog/metadata-dialog.component.ts @@ -4,7 +4,6 @@ import { Timestamp } from 'google-protobuf/google/protobuf/timestamp_pb'; import { ToastService } from 'src/app/services/toast.service'; import { Metadata as MetadataV2 } from '@zitadel/proto/zitadel/metadata_pb'; import { Metadata } from 'src/app/proto/generated/zitadel/metadata_pb'; -import { Buffer } from 'buffer'; export type MetadataDialogData = { metadata: (Metadata.AsObject | MetadataV2)[]; @@ -26,9 +25,10 @@ export class MetadataDialogComponent { public dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) public data: MetadataDialogData, ) { + const decoder = new TextDecoder(); this.metadata = data.metadata.map(({ key, value }) => ({ key, - value: typeof value === 'string' ? value : Buffer.from(value as unknown as string, 'base64').toString('utf8'), + value: typeof value === 'string' ? value : decoder.decode(value), })); } diff --git a/console/src/app/modules/metadata/metadata/metadata.component.ts b/console/src/app/modules/metadata/metadata/metadata.component.ts index 7f72297c00..bdb2c7734c 100644 --- a/console/src/app/modules/metadata/metadata/metadata.component.ts +++ b/console/src/app/modules/metadata/metadata/metadata.component.ts @@ -5,7 +5,6 @@ import { Observable, ReplaySubject } from 'rxjs'; import { Metadata as MetadataV2 } from '@zitadel/proto/zitadel/metadata_pb'; import { map, startWith } from 'rxjs/operators'; import { Metadata } from 'src/app/proto/generated/zitadel/metadata_pb'; -import { Buffer } from 'buffer'; type StringMetadata = { key: string; @@ -37,12 +36,13 @@ export class MetadataComponent implements OnInit { ngOnInit() { this.dataSource$ = this.metadata$.pipe( - map((metadata) => - metadata.map(({ key, value }) => ({ + map((metadata) => { + const decoder = new TextDecoder(); + return metadata.map(({ key, value }) => ({ key, - value: Buffer.from(value as any as string, 'base64').toString('utf-8'), - })), - ), + value: typeof value === 'string' ? value : decoder.decode(value), + })); + }), startWith([] as StringMetadata[]), map((metadata) => new MatTableDataSource(metadata)), ); diff --git a/console/src/app/pages/orgs/org-detail/org-detail.component.ts b/console/src/app/pages/orgs/org-detail/org-detail.component.ts index 0de6696ac3..39514d33d3 100644 --- a/console/src/app/pages/orgs/org-detail/org-detail.component.ts +++ b/console/src/app/pages/orgs/org-detail/org-detail.component.ts @@ -1,7 +1,6 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; import { Router } from '@angular/router'; -import { Buffer } from 'buffer'; import { BehaviorSubject, from, Observable, of, Subject, takeUntil } from 'rxjs'; import { catchError, finalize, map } from 'rxjs/operators'; import { CreationType, MemberCreateDialogComponent } from 'src/app/modules/add-member-dialog/member-create-dialog.component'; @@ -266,10 +265,11 @@ export class OrgDetailComponent implements OnInit, OnDestroy { .listOrgMetadata() .then((resp) => { this.loadingMetadata = false; - this.metadata = resp.resultList.map((md) => { + const decoder = new TextDecoder(); + this.metadata = resp.resultList.map(({ key, value }) => { return { - key: md.key, - value: Buffer.from(md.value as string, 'base64').toString('utf-8'), + key, + value: atob(typeof value === 'string' ? value : decoder.decode(value)), }; }); }) diff --git a/console/src/app/pages/users/user-detail/user-detail/user-detail.component.ts b/console/src/app/pages/users/user-detail/user-detail/user-detail.component.ts index 9370471ea4..90de097f35 100644 --- a/console/src/app/pages/users/user-detail/user-detail/user-detail.component.ts +++ b/console/src/app/pages/users/user-detail/user-detail/user-detail.component.ts @@ -4,7 +4,6 @@ import { Validators } from '@angular/forms'; import { MatDialog } from '@angular/material/dialog'; import { ActivatedRoute, Params, Router } from '@angular/router'; import { TranslateService } from '@ngx-translate/core'; -import { Buffer } from 'buffer'; import { catchError, filter, map, startWith, take } from 'rxjs/operators'; import { ChangeType } from 'src/app/modules/changes/changes.component'; import { phoneValidator, requiredValidator } from 'src/app/modules/form-field/validators/validators'; @@ -582,7 +581,7 @@ export class UserDetailComponent implements OnInit { const setFcn = (key: string, value: string) => this.newMgmtService.setUserMetadata({ key, - value: Buffer.from(value), + value: new TextEncoder().encode(value), id: user.userId, }); const removeFcn = (key: string): Promise => this.newMgmtService.removeUserMetadata({ key, id: user.userId }); From 0ae3f2a6eab7dfe60f686ed08fca7c4f1c9822e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabienne=20B=C3=BChler?= Date: Wed, 11 Jun 2025 11:23:39 +0200 Subject: [PATCH 74/76] docs: remove token exchange from "GA" list as we have some open issues (#10052) # Which Problems Are Solved Token Exchange will not move from Beta to GA feature, as there are still some unsolved issues # How the Problems Are Solved Remove from roadmap --- docs/docs/product/roadmap.mdx | 9 --------- 1 file changed, 9 deletions(-) diff --git a/docs/docs/product/roadmap.mdx b/docs/docs/product/roadmap.mdx index b83efedb10..b61323fa90 100644 --- a/docs/docs/product/roadmap.mdx +++ b/docs/docs/product/roadmap.mdx @@ -394,15 +394,6 @@ Excitingly, v3 introduces the foundational elements for Actions V2, opening up a - [Manage Users Guide](https://zitadel.com/docs/guides/manage/user/scim2) - -

- Token Exchange (Impersonation) - - The Token Exchange grant implements [RFC 8693, OAuth 2.0 Token Exchange](https://www.rfc-editor.org/rfc/rfc8693) and can be used to exchange tokens to a different scope, audience or subject. - Changing the subject of an authenticated token is called impersonation or delegation. - Read more in our [Impersonation and delegation using Token Exchange](https://zitadel.com/docs/guides/integrate/token-exchange) Guide -
-
Caches From 77f0a10c1e303ac881f3c1357a73596c22ffaf16 Mon Sep 17 00:00:00 2001 From: Iraq <66622793+kkrime@users.noreply.github.com> Date: Wed, 11 Jun 2025 13:50:31 +0200 Subject: [PATCH 75/76] fix(import/export): fix for deactivated user/organization being imported as active (#9992) --- internal/api/grpc/admin/export.go | 12 +- internal/api/grpc/admin/import.go | 15 +- internal/api/grpc/management/user.go | 5 +- internal/api/grpc/user/v2/machine.go | 1 + internal/command/org.go | 13 +- internal/command/user_human.go | 65 +++-- internal/command/user_human_test.go | 367 +++++++++++++++++++++++++- internal/command/user_machine.go | 25 +- internal/command/user_machine_test.go | 109 +++++++- proto/zitadel/admin.proto | 1 + proto/zitadel/v1.proto | 2 + 11 files changed, 574 insertions(+), 41 deletions(-) diff --git a/internal/api/grpc/admin/export.go b/internal/api/grpc/admin/export.go index 2558e5b5fc..b5d36272d4 100644 --- a/internal/api/grpc/admin/export.go +++ b/internal/api/grpc/admin/export.go @@ -8,7 +8,9 @@ import ( "google.golang.org/protobuf/types/known/timestamppb" authn_grpc "github.com/zitadel/zitadel/internal/api/grpc/authn" + "github.com/zitadel/zitadel/internal/api/grpc/org" text_grpc "github.com/zitadel/zitadel/internal/api/grpc/text" + user_converter "github.com/zitadel/zitadel/internal/api/grpc/user" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/query" "github.com/zitadel/zitadel/internal/telemetry/tracing" @@ -65,7 +67,7 @@ func (s *Server) ExportData(ctx context.Context, req *admin_pb.ExportDataRequest /****************************************************************************************************************** Organization ******************************************************************************************************************/ - org := &admin_pb.DataOrg{OrgId: queriedOrg.ID, Org: &management_pb.AddOrgRequest{Name: queriedOrg.Name}} + org := &admin_pb.DataOrg{OrgId: queriedOrg.ID, OrgState: org.OrgStateToPb(queriedOrg.State), Org: &management_pb.AddOrgRequest{Name: queriedOrg.Name}} orgs[i] = org } @@ -567,6 +569,7 @@ func (s *Server) getUsers(ctx context.Context, org string, withPasswords bool, w case domain.UserTypeHuman: dataUser := &v1_pb.DataHumanUser{ UserId: user.ID, + State: user_converter.UserStateToPb(user.State), User: &management_pb.ImportHumanUserRequest{ UserName: user.Username, Profile: &management_pb.ImportHumanUserRequest_Profile{ @@ -620,6 +623,7 @@ func (s *Server) getUsers(ctx context.Context, org string, withPasswords bool, w case domain.UserTypeMachine: machineUsers = append(machineUsers, &v1_pb.DataMachineUser{ UserId: user.ID, + State: user_converter.UserStateToPb(user.State), User: &management_pb.AddMachineUserRequest{ UserName: user.Username, Name: user.Machine.Name, @@ -647,7 +651,6 @@ func (s *Server) getUsers(ctx context.Context, org string, withPasswords bool, w ExpirationDate: timestamppb.New(key.Expiration), PublicKey: key.PublicKey, }) - } } @@ -888,7 +891,6 @@ func (s *Server) getNecessaryProjectGrantMembersForOrg(ctx context.Context, org break } } - } } } @@ -940,7 +942,6 @@ func (s *Server) getNecessaryOrgMembersForOrg(ctx context.Context, org string, p } func (s *Server) getNecessaryProjectGrantsForOrg(ctx context.Context, org string, processedOrgs []string, processedProjects []string) ([]*v1_pb.DataProjectGrant, error) { - projectGrantSearchOrg, err := query.NewProjectGrantResourceOwnerSearchQuery(org) if err != nil { return nil, err @@ -991,7 +992,7 @@ func (s *Server) getNecessaryUserGrantsForOrg(ctx context.Context, org string, p for _, userGrant := range queriedUserGrants.UserGrants { for _, projectID := range processedProjects { if projectID == userGrant.ProjectID { - //if usergrant is on a granted project + // if usergrant is on a granted project if userGrant.GrantID != "" { for _, grantID := range processedGrants { if grantID == userGrant.GrantID { @@ -1024,6 +1025,7 @@ func (s *Server) getNecessaryUserGrantsForOrg(ctx context.Context, org string, p } return userGrants, nil } + func (s *Server) getCustomLoginTexts(ctx context.Context, org string, languages []string) ([]*management_pb.SetCustomLoginTextsRequest, error) { customTexts := make([]*management_pb.SetCustomLoginTextsRequest, 0, len(languages)) for _, lang := range languages { diff --git a/internal/api/grpc/admin/import.go b/internal/api/grpc/admin/import.go index 41a1e39081..84b0215f03 100644 --- a/internal/api/grpc/admin/import.go +++ b/internal/api/grpc/admin/import.go @@ -22,6 +22,7 @@ import ( action_grpc "github.com/zitadel/zitadel/internal/api/grpc/action" "github.com/zitadel/zitadel/internal/api/grpc/authn" "github.com/zitadel/zitadel/internal/api/grpc/management" + org_converter "github.com/zitadel/zitadel/internal/api/grpc/org" "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" @@ -305,7 +306,8 @@ func importOrg1(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataEr ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - _, err = s.command.AddOrgWithID(ctx, org.GetOrg().GetName(), ctxData.UserID, ctxData.ResourceOwner, org.GetOrgId(), []string{}) + setOrgInactive := org_converter.OrgStateToDomain(org.OrgState) == domain.OrgStateInactive + _, err = s.command.AddOrgWithID(ctx, org.GetOrg().GetName(), ctxData.UserID, ctxData.ResourceOwner, org.GetOrgId(), setOrgInactive, []string{}) if err != nil { *errors = append(*errors, &admin_pb.ImportDataError{Type: "org", Id: org.GetOrgId(), Message: err.Error()}) if _, err := s.query.OrgByID(ctx, true, org.OrgId); err != nil { @@ -474,7 +476,10 @@ func importHumanUsers(ctx context.Context, s *Server, errors *[]*admin_pb.Import logging.Debugf("import user: %s", user.GetUserId()) human, passwordless, links := management.ImportHumanUserRequestToDomain(user.User) human.AggregateID = user.UserId - _, _, err := s.command.ImportHuman(ctx, org.GetOrgId(), human, passwordless, links, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator, passwordlessInitCode) + userState := user.State.ToDomain() + + //nolint:staticcheck + _, _, err := s.command.ImportHuman(ctx, org.GetOrgId(), human, passwordless, &userState, links, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator, passwordlessInitCode) if err != nil { *errors = append(*errors, &admin_pb.ImportDataError{Type: "human_user", Id: user.GetUserId(), Message: err.Error()}) if isCtxTimeout(ctx) { @@ -510,7 +515,8 @@ func importMachineUsers(ctx context.Context, s *Server, errors *[]*admin_pb.Impo } for _, user := range org.GetMachineUsers() { logging.Debugf("import user: %s", user.GetUserId()) - _, err := s.command.AddMachine(ctx, management.AddMachineUserRequestToCommand(user.GetUser(), org.GetOrgId()), nil) + userState := user.State.ToDomain() + _, err := s.command.AddMachine(ctx, management.AddMachineUserRequestToCommand(user.GetUser(), org.GetOrgId()), &userState, nil) if err != nil { *errors = append(*errors, &admin_pb.ImportDataError{Type: "machine_user", Id: user.GetUserId(), Message: err.Error()}) if isCtxTimeout(ctx) { @@ -609,7 +615,6 @@ func importUserLinks(ctx context.Context, s *Server, errors *[]*admin_pb.ImportD successOrg.UserLinks = append(successOrg.UserLinks, &admin_pb.ImportDataSuccessUserLinks{UserId: userLinks.GetUserId(), IdpId: userLinks.GetIdpId(), ExternalUserId: userLinks.GetProvidedUserId(), DisplayName: userLinks.GetProvidedUserName()}) } return nil - } func importProjects(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, successOrg *admin_pb.ImportDataSuccessOrg, org *admin_pb.DataOrg, count *counts) (err error) { @@ -750,6 +755,7 @@ func importActions(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDat } return nil } + func importProjectRoles(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, successOrg *admin_pb.ImportDataSuccessOrg, org *admin_pb.DataOrg, count *counts) (err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() @@ -805,6 +811,7 @@ func importResources(ctx context.Context, s *Server, errors *[]*admin_pb.ImportD importDomainClaimedMessageTexts(ctx, s, errors, org) importPasswordlessRegistrationMessageTexts(ctx, s, errors, org) importInviteUserMessageTexts(ctx, s, errors, org) + if err := importHumanUsers(ctx, s, errors, successOrg, org, count, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator, passwordlessInitCode); err != nil { return err } diff --git a/internal/api/grpc/management/user.go b/internal/api/grpc/management/user.go index ae1040cd1e..09b9faa756 100644 --- a/internal/api/grpc/management/user.go +++ b/internal/api/grpc/management/user.go @@ -273,7 +273,8 @@ func (s *Server) ImportHumanUser(ctx context.Context, req *mgmt_pb.ImportHumanUs if err != nil { return nil, err } - addedHuman, code, err := s.command.ImportHuman(ctx, authz.GetCtxData(ctx).OrgID, human, passwordless, links, initCodeGenerator, phoneCodeGenerator, emailCodeGenerator, passwordlessInitCode) + //nolint:staticcheck + addedHuman, code, err := s.command.ImportHuman(ctx, authz.GetCtxData(ctx).OrgID, human, passwordless, nil, links, initCodeGenerator, phoneCodeGenerator, emailCodeGenerator, passwordlessInitCode) if err != nil { return nil, err } @@ -297,7 +298,7 @@ func (s *Server) ImportHumanUser(ctx context.Context, req *mgmt_pb.ImportHumanUs func (s *Server) AddMachineUser(ctx context.Context, req *mgmt_pb.AddMachineUserRequest) (*mgmt_pb.AddMachineUserResponse, error) { machine := AddMachineUserRequestToCommand(req, authz.GetCtxData(ctx).OrgID) - objectDetails, err := s.command.AddMachine(ctx, machine, nil) + objectDetails, err := s.command.AddMachine(ctx, machine, nil, nil) if err != nil { return nil, err } diff --git a/internal/api/grpc/user/v2/machine.go b/internal/api/grpc/user/v2/machine.go index 010ba75678..ad02b2289e 100644 --- a/internal/api/grpc/user/v2/machine.go +++ b/internal/api/grpc/user/v2/machine.go @@ -25,6 +25,7 @@ func (s *Server) createUserTypeMachine(ctx context.Context, machinePb *user.Crea details, err := s.command.AddMachine( ctx, cmd, + nil, s.command.NewPermissionCheckUserWrite(ctx), command.AddMachineWithUsernameToIDFallback(), ) diff --git a/internal/command/org.go b/internal/command/org.go index ddf99797e3..faab882d68 100644 --- a/internal/command/org.go +++ b/internal/command/org.go @@ -317,7 +317,7 @@ func (c *Commands) checkOrgExists(ctx context.Context, orgID string) error { return nil } -func (c *Commands) AddOrgWithID(ctx context.Context, name, userID, resourceOwner, orgID string, claimedUserIDs []string) (_ *domain.Org, err error) { +func (c *Commands) AddOrgWithID(ctx context.Context, name, userID, resourceOwner, orgID string, setOrgInactive bool, claimedUserIDs []string) (_ *domain.Org, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() @@ -329,7 +329,7 @@ func (c *Commands) AddOrgWithID(ctx context.Context, name, userID, resourceOwner return nil, zerrors.ThrowNotFound(nil, "ORG-lapo2m", "Errors.Org.AlreadyExisting") } - return c.addOrgWithIDAndMember(ctx, name, userID, resourceOwner, orgID, claimedUserIDs) + return c.addOrgWithIDAndMember(ctx, name, userID, resourceOwner, orgID, setOrgInactive, claimedUserIDs) } func (c *Commands) AddOrg(ctx context.Context, name, userID, resourceOwner string, claimedUserIDs []string) (*domain.Org, error) { @@ -342,10 +342,10 @@ func (c *Commands) AddOrg(ctx context.Context, name, userID, resourceOwner strin return nil, zerrors.ThrowInternal(err, "COMMA-OwciI", "Errors.Internal") } - return c.addOrgWithIDAndMember(ctx, name, userID, resourceOwner, orgID, claimedUserIDs) + return c.addOrgWithIDAndMember(ctx, name, userID, resourceOwner, orgID, false, claimedUserIDs) } -func (c *Commands) addOrgWithIDAndMember(ctx context.Context, name, userID, resourceOwner, orgID string, claimedUserIDs []string) (_ *domain.Org, err error) { +func (c *Commands) addOrgWithIDAndMember(ctx context.Context, name, userID, resourceOwner, orgID string, setOrgInactive bool, claimedUserIDs []string) (_ *domain.Org, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() @@ -363,10 +363,15 @@ func (c *Commands) addOrgWithIDAndMember(ctx context.Context, name, userID, reso return nil, err } events = append(events, orgMemberEvent) + if setOrgInactive { + deactivateOrgEvent := org.NewOrgDeactivatedEvent(ctx, orgAgg) + events = append(events, deactivateOrgEvent) + } pushedEvents, err := c.eventstore.Push(ctx, events...) if err != nil { return nil, err } + err = AppendAndReduce(addedOrg, pushedEvents...) if err != nil { return nil, err diff --git a/internal/command/user_human.go b/internal/command/user_human.go index 9e6ba43629..07628b9e19 100644 --- a/internal/command/user_human.go +++ b/internal/command/user_human.go @@ -428,7 +428,7 @@ func (h *AddHuman) shouldAddInitCode() bool { } // Deprecated: use commands.AddUserHuman -func (c *Commands) ImportHuman(ctx context.Context, orgID string, human *domain.Human, passwordless bool, links []*domain.UserIDPLink, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator, passwordlessCodeGenerator crypto.Generator) (_ *domain.Human, passwordlessCode *domain.PasswordlessInitCode, err error) { +func (c *Commands) ImportHuman(ctx context.Context, orgID string, human *domain.Human, passwordless bool, state *domain.UserState, links []*domain.UserIDPLink, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator, passwordlessCodeGenerator crypto.Generator) (_ *domain.Human, passwordlessCode *domain.PasswordlessInitCode, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() @@ -455,10 +455,32 @@ func (c *Commands) ImportHuman(ctx context.Context, orgID string, human *domain. } } - events, addedHuman, addedCode, code, err := c.importHuman(ctx, orgID, human, passwordless, links, domainPolicy, pwPolicy, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator, passwordlessCodeGenerator) + events, userAgg, addedHuman, addedCode, code, err := c.importHuman(ctx, orgID, human, passwordless, links, domainPolicy, pwPolicy, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator, passwordlessCodeGenerator) if err != nil { return nil, nil, err } + if state != nil { + var event eventstore.Command + switch *state { + case domain.UserStateInactive: + event = user.NewUserDeactivatedEvent(ctx, userAgg) + case domain.UserStateLocked: + event = user.NewUserLockedEvent(ctx, userAgg) + case domain.UserStateDeleted: + // users are never imported if deleted + case domain.UserStateActive: + // added because of the linter + case domain.UserStateSuspend: + // added because of the linter + case domain.UserStateInitial: + // added because of the linter + case domain.UserStateUnspecified: + // added because of the linter + } + if event != nil { + events = append(events, event) + } + } pushedEvents, err := c.eventstore.Push(ctx, events...) if err != nil { return nil, nil, err @@ -479,48 +501,48 @@ func (c *Commands) ImportHuman(ctx context.Context, orgID string, human *domain. return writeModelToHuman(addedHuman), passwordlessCode, nil } -func (c *Commands) importHuman(ctx context.Context, orgID string, human *domain.Human, passwordless bool, links []*domain.UserIDPLink, domainPolicy *domain.DomainPolicy, pwPolicy *domain.PasswordComplexityPolicy, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator, passwordlessCodeGenerator crypto.Generator) (events []eventstore.Command, humanWriteModel *HumanWriteModel, passwordlessCodeWriteModel *HumanPasswordlessInitCodeWriteModel, code string, err error) { +func (c *Commands) importHuman(ctx context.Context, orgID string, human *domain.Human, passwordless bool, links []*domain.UserIDPLink, domainPolicy *domain.DomainPolicy, pwPolicy *domain.PasswordComplexityPolicy, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator, passwordlessCodeGenerator crypto.Generator) (events []eventstore.Command, userAgg *eventstore.Aggregate, humanWriteModel *HumanWriteModel, passwordlessCodeWriteModel *HumanPasswordlessInitCodeWriteModel, code string, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() if orgID == "" { - return nil, nil, nil, "", zerrors.ThrowInvalidArgument(nil, "COMMAND-00p2b", "Errors.Org.Empty") + return nil, nil, nil, nil, "", zerrors.ThrowInvalidArgument(nil, "COMMAND-00p2b", "Errors.Org.Empty") } if err = human.Normalize(); err != nil { - return nil, nil, nil, "", err + return nil, nil, nil, nil, "", err } - events, humanWriteModel, err = c.createHuman(ctx, orgID, human, links, passwordless, domainPolicy, pwPolicy, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator) + events, userAgg, humanWriteModel, err = c.createHuman(ctx, orgID, human, links, passwordless, domainPolicy, pwPolicy, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator) if err != nil { - return nil, nil, nil, "", err + return nil, nil, nil, nil, "", err } if passwordless { var codeEvent eventstore.Command codeEvent, passwordlessCodeWriteModel, code, err = c.humanAddPasswordlessInitCode(ctx, human.AggregateID, orgID, true, passwordlessCodeGenerator) if err != nil { - return nil, nil, nil, "", err + return nil, nil, nil, nil, "", err } events = append(events, codeEvent) } - return events, humanWriteModel, passwordlessCodeWriteModel, code, nil + return events, userAgg, humanWriteModel, passwordlessCodeWriteModel, code, nil } -func (c *Commands) createHuman(ctx context.Context, orgID string, human *domain.Human, links []*domain.UserIDPLink, passwordless bool, domainPolicy *domain.DomainPolicy, pwPolicy *domain.PasswordComplexityPolicy, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator crypto.Generator) (events []eventstore.Command, addedHuman *HumanWriteModel, err error) { +func (c *Commands) createHuman(ctx context.Context, orgID string, human *domain.Human, links []*domain.UserIDPLink, passwordless bool, domainPolicy *domain.DomainPolicy, pwPolicy *domain.PasswordComplexityPolicy, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator crypto.Generator) (events []eventstore.Command, userAgg *eventstore.Aggregate, addedHuman *HumanWriteModel, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() if err = human.CheckDomainPolicy(domainPolicy); err != nil { - return nil, nil, err + return nil, nil, nil, err } human.Username = strings.TrimSpace(human.Username) human.EmailAddress = human.EmailAddress.Normalize() if err = c.userValidateDomain(ctx, orgID, human.Username, domainPolicy.UserLoginMustBeDomain); err != nil { - return nil, nil, err + return nil, nil, nil, err } if human.AggregateID == "" { userID, err := c.idGenerator.Next() if err != nil { - return nil, nil, err + return nil, nil, nil, err } human.AggregateID = userID } @@ -528,20 +550,21 @@ func (c *Commands) createHuman(ctx context.Context, orgID string, human *domain. human.EnsureDisplayName() if human.Password != nil { if err := human.HashPasswordIfExisting(ctx, pwPolicy, c.userPasswordHasher, human.Password.ChangeRequired); err != nil { - return nil, nil, err + return nil, nil, nil, err } } addedHuman = NewHumanWriteModel(human.AggregateID, orgID) - //TODO: adlerhurst maybe we could simplify the code below - userAgg := UserAggregateFromWriteModel(&addedHuman.WriteModel) + + // TODO: adlerhurst maybe we could simplify the code below + userAgg = UserAggregateFromWriteModelCtx(ctx, &addedHuman.WriteModel) events = append(events, createAddHumanEvent(ctx, userAgg, human, domainPolicy.UserLoginMustBeDomain)) for _, link := range links { event, err := c.addUserIDPLink(ctx, userAgg, link, false) if err != nil { - return nil, nil, err + return nil, nil, nil, err } events = append(events, event) } @@ -549,7 +572,7 @@ func (c *Commands) createHuman(ctx context.Context, orgID string, human *domain. if human.IsInitialState(passwordless, len(links) > 0) { initCode, err := domain.NewInitUserCode(initCodeGenerator) if err != nil { - return nil, nil, err + return nil, nil, nil, err } events = append(events, user.NewHumanInitialCodeAddedEvent(ctx, userAgg, initCode.Code, initCode.Expiry, "")) } else { @@ -558,7 +581,7 @@ func (c *Commands) createHuman(ctx context.Context, orgID string, human *domain. } else { emailCode, _, err := domain.NewEmailCode(emailCodeGenerator) if err != nil { - return nil, nil, err + return nil, nil, nil, err } events = append(events, user.NewHumanEmailCodeAddedEvent(ctx, userAgg, emailCode.Code, emailCode.Expiry, "")) } @@ -567,14 +590,14 @@ func (c *Commands) createHuman(ctx context.Context, orgID string, human *domain. if human.Phone != nil && human.PhoneNumber != "" && !human.IsPhoneVerified { phoneCode, generatorID, err := c.newPhoneCode(ctx, c.eventstore.Filter, domain.SecretGeneratorTypeVerifyPhoneCode, c.userEncryption, c.defaultSecretGenerators.PhoneVerificationCode) //nolint:staticcheck if err != nil { - return nil, nil, err + return nil, nil, nil, err } events = append(events, user.NewHumanPhoneCodeAddedEvent(ctx, userAgg, phoneCode.CryptedCode(), phoneCode.CodeExpiry(), generatorID)) } else if human.Phone != nil && human.PhoneNumber != "" && human.IsPhoneVerified { events = append(events, user.NewHumanPhoneVerifiedEvent(ctx, userAgg)) } - return events, addedHuman, nil + return events, userAgg, addedHuman, nil } func (c *Commands) HumanSkipMFAInit(ctx context.Context, userID, resourceowner string) (err error) { diff --git a/internal/command/user_human_test.go b/internal/command/user_human_test.go index 78d7248516..1ef3e2aab6 100644 --- a/internal/command/user_human_test.go +++ b/internal/command/user_human_test.go @@ -1200,7 +1200,8 @@ func TestCommandSide_AddHuman(t *testing.T) { }, wantID: "user1", }, - }, { + }, + { name: "add human (with return code), ok", fields: fields{ eventstore: expectEventstore( @@ -1432,6 +1433,7 @@ func TestCommandSide_ImportHuman(t *testing.T) { orgID string human *domain.Human passwordless bool + state *domain.UserState links []*domain.UserIDPLink secretGenerator crypto.Generator passwordlessInitCode crypto.Generator @@ -1584,7 +1586,8 @@ func TestCommandSide_ImportHuman(t *testing.T) { res: res{ err: zerrors.IsErrorInvalidArgument, }, - }, { + }, + { name: "add human (with password and initial code), ok", given: func(t *testing.T) (fields, args) { return fields{ @@ -2985,6 +2988,364 @@ func TestCommandSide_ImportHuman(t *testing.T) { }, }, }, + { + name: "add human (with idp, auto creation not allowed) + deactivated state, ok", + given: func(t *testing.T) (fields, args) { + return fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + org.NewDomainPolicyAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + true, + true, + true, + ), + ), + ), + expectFilter( + eventFromEventPusher( + org.NewPasswordComplexityPolicyAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + 1, + false, + false, + false, + false, + ), + ), + ), + expectFilter( + eventFromEventPusher( + org.NewIDPConfigAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "idpID", + "name", + domain.IDPConfigTypeOIDC, + domain.IDPConfigStylingTypeUnspecified, + false, + ), + ), + eventFromEventPusher( + org.NewIDPOIDCConfigAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "clientID", + "idpID", + "issuer", + "authEndpoint", + "tokenEndpoint", + nil, + domain.OIDCMappingFieldUnspecified, + domain.OIDCMappingFieldUnspecified, + ), + ), + eventFromEventPusher( + func() eventstore.Command { + e, _ := org.NewOIDCIDPChangedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "config1", + []idp.OIDCIDPChanges{ + idp.ChangeOIDCOptions(idp.OptionChanges{IsCreationAllowed: gu.Ptr(false)}), + }, + ) + return e + }(), + ), + ), + expectFilter( + eventFromEventPusher( + org.NewIDPConfigAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "idpID", + "name", + domain.IDPConfigTypeOIDC, + domain.IDPConfigStylingTypeUnspecified, + false, + ), + ), + eventFromEventPusher( + org.NewIDPOIDCConfigAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "clientID", + "idpID", + "issuer", + "authEndpoint", + "tokenEndpoint", + nil, + domain.OIDCMappingFieldUnspecified, + domain.OIDCMappingFieldUnspecified, + ), + ), + eventFromEventPusher( + func() eventstore.Command { + e, _ := org.NewOIDCIDPChangedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "config1", + []idp.OIDCIDPChanges{ + idp.ChangeOIDCOptions(idp.OptionChanges{ + IsCreationAllowed: gu.Ptr(true), + IsAutoCreation: gu.Ptr(false), + }), + }, + ) + return e + }(), + ), + eventFromEventPusher( + org.NewIdentityProviderAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "idpID", + domain.IdentityProviderTypeOrg, + ), + ), + ), + expectPush( + newAddHumanEvent("", false, true, "", AllowedLanguage), + user.NewUserIDPLinkAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "idpID", + "name", + "externalID", + ), + user.NewHumanEmailVerifiedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate), + user.NewUserDeactivatedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + ), + ), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), + userPasswordHasher: mockPasswordHasher("x"), + }, + args{ + ctx: context.Background(), + orgID: "org1", + state: func() *domain.UserState { + state := domain.UserStateInactive + return &state + }(), + human: &domain.Human{ + Username: "username", + Profile: &domain.Profile{ + FirstName: "firstname", + LastName: "lastname", + PreferredLanguage: AllowedLanguage, + }, + Email: &domain.Email{ + EmailAddress: "email@test.ch", + IsEmailVerified: true, + }, + }, + links: []*domain.UserIDPLink{ + { + IDPConfigID: "idpID", + ExternalUserID: "externalID", + DisplayName: "name", + }, + }, + secretGenerator: GetMockSecretGenerator(t), + } + }, + res: res{ + wantHuman: &domain.Human{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "user1", + ResourceOwner: "org1", + }, + Username: "username", + Profile: &domain.Profile{ + FirstName: "firstname", + LastName: "lastname", + DisplayName: "firstname lastname", + PreferredLanguage: AllowedLanguage, + }, + Email: &domain.Email{ + EmailAddress: "email@test.ch", + IsEmailVerified: true, + }, + State: domain.UserStateInactive, + }, + }, + }, + { + name: "add human (with idp, auto creation not allowed) + locked state, ok", + given: func(t *testing.T) (fields, args) { + return fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + org.NewDomainPolicyAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + true, + true, + true, + ), + ), + ), + expectFilter( + eventFromEventPusher( + org.NewPasswordComplexityPolicyAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + 1, + false, + false, + false, + false, + ), + ), + ), + expectFilter( + eventFromEventPusher( + org.NewIDPConfigAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "idpID", + "name", + domain.IDPConfigTypeOIDC, + domain.IDPConfigStylingTypeUnspecified, + false, + ), + ), + eventFromEventPusher( + org.NewIDPOIDCConfigAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "clientID", + "idpID", + "issuer", + "authEndpoint", + "tokenEndpoint", + nil, + domain.OIDCMappingFieldUnspecified, + domain.OIDCMappingFieldUnspecified, + ), + ), + eventFromEventPusher( + func() eventstore.Command { + e, _ := org.NewOIDCIDPChangedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "config1", + []idp.OIDCIDPChanges{ + idp.ChangeOIDCOptions(idp.OptionChanges{IsCreationAllowed: gu.Ptr(false)}), + }, + ) + return e + }(), + ), + ), + expectFilter( + eventFromEventPusher( + org.NewIDPConfigAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "idpID", + "name", + domain.IDPConfigTypeOIDC, + domain.IDPConfigStylingTypeUnspecified, + false, + ), + ), + eventFromEventPusher( + org.NewIDPOIDCConfigAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "clientID", + "idpID", + "issuer", + "authEndpoint", + "tokenEndpoint", + nil, + domain.OIDCMappingFieldUnspecified, + domain.OIDCMappingFieldUnspecified, + ), + ), + eventFromEventPusher( + func() eventstore.Command { + e, _ := org.NewOIDCIDPChangedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "config1", + []idp.OIDCIDPChanges{ + idp.ChangeOIDCOptions(idp.OptionChanges{ + IsCreationAllowed: gu.Ptr(true), + IsAutoCreation: gu.Ptr(false), + }), + }, + ) + return e + }(), + ), + eventFromEventPusher( + org.NewIdentityProviderAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "idpID", + domain.IdentityProviderTypeOrg, + ), + ), + ), + expectPush( + newAddHumanEvent("", false, true, "", AllowedLanguage), + user.NewUserIDPLinkAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "idpID", + "name", + "externalID", + ), + user.NewHumanEmailVerifiedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate), + user.NewUserLockedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + ), + ), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), + userPasswordHasher: mockPasswordHasher("x"), + }, + args{ + ctx: context.Background(), + orgID: "org1", + state: func() *domain.UserState { + state := domain.UserStateLocked + return &state + }(), + human: &domain.Human{ + Username: "username", + Profile: &domain.Profile{ + FirstName: "firstname", + LastName: "lastname", + PreferredLanguage: AllowedLanguage, + }, + Email: &domain.Email{ + EmailAddress: "email@test.ch", + IsEmailVerified: true, + }, + }, + links: []*domain.UserIDPLink{ + { + IDPConfigID: "idpID", + ExternalUserID: "externalID", + DisplayName: "name", + }, + }, + secretGenerator: GetMockSecretGenerator(t), + } + }, + res: res{ + wantHuman: &domain.Human{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "user1", + ResourceOwner: "org1", + }, + Username: "username", + Profile: &domain.Profile{ + FirstName: "firstname", + LastName: "lastname", + DisplayName: "firstname lastname", + PreferredLanguage: AllowedLanguage, + }, + Email: &domain.Email{ + EmailAddress: "email@test.ch", + IsEmailVerified: true, + }, + State: domain.UserStateLocked, + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -2996,7 +3357,7 @@ func TestCommandSide_ImportHuman(t *testing.T) { newEncryptedCodeWithDefault: f.newEncryptedCodeWithDefault, defaultSecretGenerators: f.defaultSecretGenerators, } - gotHuman, gotCode, err := r.ImportHuman(a.ctx, a.orgID, a.human, a.passwordless, a.links, a.secretGenerator, a.secretGenerator, a.secretGenerator, a.secretGenerator) + gotHuman, gotCode, err := r.ImportHuman(a.ctx, a.orgID, a.human, a.passwordless, a.state, a.links, a.secretGenerator, a.secretGenerator, a.secretGenerator, a.secretGenerator) if tt.res.err == nil { assert.NoError(t, err) } diff --git a/internal/command/user_machine.go b/internal/command/user_machine.go index 7c8fd89eac..75ed43ee69 100644 --- a/internal/command/user_machine.go +++ b/internal/command/user_machine.go @@ -79,7 +79,7 @@ func AddMachineWithUsernameToIDFallback() addMachineOption { } } -func (c *Commands) AddMachine(ctx context.Context, machine *Machine, check PermissionCheck, options ...addMachineOption) (_ *domain.ObjectDetails, err error) { +func (c *Commands) AddMachine(ctx context.Context, machine *Machine, state *domain.UserState, check PermissionCheck, options ...addMachineOption) (_ *domain.ObjectDetails, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() @@ -107,6 +107,29 @@ func (c *Commands) AddMachine(ctx context.Context, machine *Machine, check Permi return nil, err } + if state != nil { + var cmd eventstore.Command + switch *state { + case domain.UserStateInactive: + cmd = user.NewUserDeactivatedEvent(ctx, &agg.Aggregate) + case domain.UserStateLocked: + cmd = user.NewUserLockedEvent(ctx, &agg.Aggregate) + case domain.UserStateDeleted: + // users are never imported if deleted + case domain.UserStateActive: + // added because of the linter + case domain.UserStateSuspend: + // added because of the linter + case domain.UserStateInitial: + // added because of the linter + case domain.UserStateUnspecified: + // added because of the linter + } + if cmd != nil { + cmds = append(cmds, cmd) + } + } + events, err := c.eventstore.Push(ctx, cmds...) if err != nil { return nil, err diff --git a/internal/command/user_machine_test.go b/internal/command/user_machine_test.go index 19548ae9c6..6d94154a42 100644 --- a/internal/command/user_machine_test.go +++ b/internal/command/user_machine_test.go @@ -24,6 +24,7 @@ func TestCommandSide_AddMachine(t *testing.T) { type args struct { ctx context.Context machine *Machine + state *domain.UserState check PermissionCheck options func(*Commands) []addMachineOption } @@ -419,6 +420,112 @@ func TestCommandSide_AddMachine(t *testing.T) { err: zerrors.IsPermissionDenied, }, }, + { + name: "add machine, ok + deactive state", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + expectFilter( + eventFromEventPusher( + org.NewDomainPolicyAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + true, + true, + true, + ), + ), + ), + expectPush( + user.NewMachineAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "name", + "description", + true, + domain.OIDCTokenTypeBearer, + ), + user.NewUserDeactivatedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + ), + ), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), + }, + args: args{ + ctx: context.Background(), + machine: &Machine{ + ObjectRoot: models.ObjectRoot{ + ResourceOwner: "org1", + }, + Description: "description", + Name: "name", + Username: "username", + }, + state: func() *domain.UserState { + state := domain.UserStateInactive + return &state + }(), + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "add machine, ok + locked state", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + expectFilter( + eventFromEventPusher( + org.NewDomainPolicyAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + true, + true, + true, + ), + ), + ), + expectPush( + user.NewMachineAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "name", + "description", + true, + domain.OIDCTokenTypeBearer, + ), + user.NewUserLockedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + ), + ), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), + }, + args: args{ + ctx: context.Background(), + machine: &Machine{ + ObjectRoot: models.ObjectRoot{ + ResourceOwner: "org1", + }, + Description: "description", + Name: "name", + Username: "username", + }, + state: func() *domain.UserState { + state := domain.UserStateLocked + return &state + }(), + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -431,7 +538,7 @@ func TestCommandSide_AddMachine(t *testing.T) { if tt.args.options != nil { options = tt.args.options(r) } - got, err := r.AddMachine(tt.args.ctx, tt.args.machine, tt.args.check, options...) + got, err := r.AddMachine(tt.args.ctx, tt.args.machine, tt.args.state, tt.args.check, options...) if tt.res.err == nil { assert.NoError(t, err) } diff --git a/proto/zitadel/admin.proto b/proto/zitadel/admin.proto index d8c88d540b..da496b7c7d 100644 --- a/proto/zitadel/admin.proto +++ b/proto/zitadel/admin.proto @@ -9005,6 +9005,7 @@ message DataOrg { repeated zitadel.management.v1.SetCustomVerifySMSOTPMessageTextRequest verify_sms_otp_messages = 37; repeated zitadel.management.v1.SetCustomVerifyEmailOTPMessageTextRequest verify_email_otp_messages = 38; repeated zitadel.management.v1.SetCustomInviteUserMessageTextRequest invite_user_messages = 39; + zitadel.org.v1.OrgState org_state = 40; } message ImportDataResponse{ diff --git a/proto/zitadel/v1.proto b/proto/zitadel/v1.proto index c186ea7d61..beb91116f1 100644 --- a/proto/zitadel/v1.proto +++ b/proto/zitadel/v1.proto @@ -172,10 +172,12 @@ message DataOIDCApplication { message DataHumanUser { string user_id = 1; zitadel.management.v1.ImportHumanUserRequest user = 2; + zitadel.user.v1.UserState state = 3; } message DataMachineUser { string user_id = 1; zitadel.management.v1.AddMachineUserRequest user = 2; + zitadel.user.v1.UserState state = 3; } message DataAction { string action_id = 1; From 83839fc2eff533611abb2b0a81c09ae65eff94b0 Mon Sep 17 00:00:00 2001 From: Abhinav Sethi Date: Thu, 12 Jun 2025 13:03:25 -0400 Subject: [PATCH 76/76] fix: enable opentelemetry metrics for river queue (#10044) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Which Problems Are Solved Right now we have no visibility into river queue's job processing times and queue sizes. This makes it difficult to reliably know if notifications are actually being published in a reasonable time and current queue size. # How the Problems Are Solved Integrates River's OpenTelemetry middleware with Zitadel's metrics system by adding the otelriver middleware to the queue configuration. # Additional Changes - Updated dependencies to include required `otelriver` package # Additional Context Example output from `/debug/metrics`
output # HELP failed_deliveries_json_total Failed JSON message deliveries # TYPE failed_deliveries_json_total counter failed_deliveries_json_total{otel_scope_name="",otel_scope_version="",triggering_event_type="user.human.phone.code.added"} 2 # HELP go_gc_duration_seconds A summary of the wall-time pause (stop-the-world) duration in garbage collection cycles. # TYPE go_gc_duration_seconds summary go_gc_duration_seconds{quantile="0"} 3.8e-05 go_gc_duration_seconds{quantile="0.25"} 6.3916e-05 go_gc_duration_seconds{quantile="0.5"} 7.5584e-05 go_gc_duration_seconds{quantile="0.75"} 9.2584e-05 go_gc_duration_seconds{quantile="1"} 0.000204292 go_gc_duration_seconds_sum 0.003028502 go_gc_duration_seconds_count 34 # HELP go_gc_gogc_percent Heap size target percentage configured by the user, otherwise 100. This value is set by the GOGC environment variable, and the runtime/debug.SetGCPercent function. Sourced from /gc/gogc:percent # TYPE go_gc_gogc_percent gauge go_gc_gogc_percent 100 # HELP go_gc_gomemlimit_bytes Go runtime memory limit configured by the user, otherwise math.MaxInt64. This value is set by the GOMEMLIMIT environment variable, and the runtime/debug.SetMemoryLimit function. Sourced from /gc/gomemlimit:bytes # TYPE go_gc_gomemlimit_bytes gauge go_gc_gomemlimit_bytes 9.223372036854776e+18 # HELP go_goroutines Number of goroutines that currently exist. # TYPE go_goroutines gauge go_goroutines 231 # HELP go_info Information about the Go environment. # TYPE go_info gauge go_info{version="go1.24.3"} 1 # HELP go_memstats_alloc_bytes Number of bytes allocated in heap and currently in use. Equals to /memory/classes/heap/objects:bytes. # TYPE go_memstats_alloc_bytes gauge go_memstats_alloc_bytes 7.7565832e+07 # HELP go_memstats_alloc_bytes_total Total number of bytes allocated in heap until now, even if released already. Equals to /gc/heap/allocs:bytes. # TYPE go_memstats_alloc_bytes_total counter go_memstats_alloc_bytes_total 7.3319844e+08 # HELP go_memstats_buck_hash_sys_bytes Number of bytes used by the profiling bucket hash table. Equals to /memory/classes/profiling/buckets:bytes. # TYPE go_memstats_buck_hash_sys_bytes gauge go_memstats_buck_hash_sys_bytes 1.63816e+06 # HELP go_memstats_frees_total Total number of heap objects frees. Equals to /gc/heap/frees:objects + /gc/heap/tiny/allocs:objects. # TYPE go_memstats_frees_total counter go_memstats_frees_total 1.1496925e+07 # HELP go_memstats_gc_sys_bytes Number of bytes used for garbage collection system metadata. Equals to /memory/classes/metadata/other:bytes. # TYPE go_memstats_gc_sys_bytes gauge go_memstats_gc_sys_bytes 5.182776e+06 # HELP go_memstats_heap_alloc_bytes Number of heap bytes allocated and currently in use, same as go_memstats_alloc_bytes. Equals to /memory/classes/heap/objects:bytes. # TYPE go_memstats_heap_alloc_bytes gauge go_memstats_heap_alloc_bytes 7.7565832e+07 # HELP go_memstats_heap_idle_bytes Number of heap bytes waiting to be used. Equals to /memory/classes/heap/released:bytes + /memory/classes/heap/free:bytes. # TYPE go_memstats_heap_idle_bytes gauge go_memstats_heap_idle_bytes 5.8179584e+07 # HELP go_memstats_heap_inuse_bytes Number of heap bytes that are in use. Equals to /memory/classes/heap/objects:bytes + /memory/classes/heap/unused:bytes # TYPE go_memstats_heap_inuse_bytes gauge go_memstats_heap_inuse_bytes 8.5868544e+07 # HELP go_memstats_heap_objects Number of currently allocated objects. Equals to /gc/heap/objects:objects. # TYPE go_memstats_heap_objects gauge go_memstats_heap_objects 573723 # HELP go_memstats_heap_released_bytes Number of heap bytes released to OS. Equals to /memory/classes/heap/released:bytes. # TYPE go_memstats_heap_released_bytes gauge go_memstats_heap_released_bytes 7.20896e+06 # HELP go_memstats_heap_sys_bytes Number of heap bytes obtained from system. Equals to /memory/classes/heap/objects:bytes + /memory/classes/heap/unused:bytes + /memory/classes/heap/released:bytes + /memory/classes/heap/free:bytes. # TYPE go_memstats_heap_sys_bytes gauge go_memstats_heap_sys_bytes 1.44048128e+08 # HELP go_memstats_last_gc_time_seconds Number of seconds since 1970 of last garbage collection. # TYPE go_memstats_last_gc_time_seconds gauge go_memstats_last_gc_time_seconds 1.749491558214289e+09 # HELP go_memstats_mallocs_total Total number of heap objects allocated, both live and gc-ed. Semantically a counter version for go_memstats_heap_objects gauge. Equals to /gc/heap/allocs:objects + /gc/heap/tiny/allocs:objects. # TYPE go_memstats_mallocs_total counter go_memstats_mallocs_total 1.2070648e+07 # HELP go_memstats_mcache_inuse_bytes Number of bytes in use by mcache structures. Equals to /memory/classes/metadata/mcache/inuse:bytes. # TYPE go_memstats_mcache_inuse_bytes gauge go_memstats_mcache_inuse_bytes 16912 # HELP go_memstats_mcache_sys_bytes Number of bytes used for mcache structures obtained from system. Equals to /memory/classes/metadata/mcache/inuse:bytes + /memory/classes/metadata/mcache/free:bytes. # TYPE go_memstats_mcache_sys_bytes gauge go_memstats_mcache_sys_bytes 31408 # HELP go_memstats_mspan_inuse_bytes Number of bytes in use by mspan structures. Equals to /memory/classes/metadata/mspan/inuse:bytes. # TYPE go_memstats_mspan_inuse_bytes gauge go_memstats_mspan_inuse_bytes 1.3496e+06 # HELP go_memstats_mspan_sys_bytes Number of bytes used for mspan structures obtained from system. Equals to /memory/classes/metadata/mspan/inuse:bytes + /memory/classes/metadata/mspan/free:bytes. # TYPE go_memstats_mspan_sys_bytes gauge go_memstats_mspan_sys_bytes 2.18688e+06 # HELP go_memstats_next_gc_bytes Number of heap bytes when next garbage collection will take place. Equals to /gc/heap/goal:bytes. # TYPE go_memstats_next_gc_bytes gauge go_memstats_next_gc_bytes 1.34730994e+08 # HELP go_memstats_other_sys_bytes Number of bytes used for other system allocations. Equals to /memory/classes/other:bytes. # TYPE go_memstats_other_sys_bytes gauge go_memstats_other_sys_bytes 3.125168e+06 # HELP go_memstats_stack_inuse_bytes Number of bytes obtained from system for stack allocator in non-CGO environments. Equals to /memory/classes/heap/stacks:bytes. # TYPE go_memstats_stack_inuse_bytes gauge go_memstats_stack_inuse_bytes 2.752512e+06 # HELP go_memstats_stack_sys_bytes Number of bytes obtained from system for stack allocator. Equals to /memory/classes/heap/stacks:bytes + /memory/classes/os-stacks:bytes. # TYPE go_memstats_stack_sys_bytes gauge go_memstats_stack_sys_bytes 2.752512e+06 # HELP go_memstats_sys_bytes Number of bytes obtained from system. Equals to /memory/classes/total:byte. # TYPE go_memstats_sys_bytes gauge go_memstats_sys_bytes 1.58965032e+08 # HELP go_sched_gomaxprocs_threads The current runtime.GOMAXPROCS setting, or the number of operating system threads that can execute user-level Go code simultaneously. Sourced from /sched/gomaxprocs:threads # TYPE go_sched_gomaxprocs_threads gauge go_sched_gomaxprocs_threads 14 # HELP go_threads Number of OS threads created. # TYPE go_threads gauge go_threads 25 # HELP grpc_server_grpc_status_code_total Grpc status code counter # TYPE grpc_server_grpc_status_code_total counter grpc_server_grpc_status_code_total{grpc_method="/zitadel.management.v1.ManagementService/ListUserChanges",otel_scope_name="",otel_scope_version="",return_code="200"} 1 grpc_server_grpc_status_code_total{grpc_method="/zitadel.management.v1.ManagementService/ListUserMetadata",otel_scope_name="",otel_scope_version="",return_code="200"} 2 grpc_server_grpc_status_code_total{grpc_method="/zitadel.management.v1.ManagementService/ResendHumanPhoneVerification",otel_scope_name="",otel_scope_version="",return_code="200"} 1 grpc_server_grpc_status_code_total{grpc_method="/zitadel.user.v2.UserService/GetUserByID",otel_scope_name="",otel_scope_version="",return_code="200"} 1 # HELP grpc_server_request_counter_total Grpc request counter # TYPE grpc_server_request_counter_total counter grpc_server_request_counter_total{grpc_method="/zitadel.management.v1.ManagementService/ListUserChanges",otel_scope_name="",otel_scope_version=""} 1 grpc_server_request_counter_total{grpc_method="/zitadel.management.v1.ManagementService/ListUserMetadata",otel_scope_name="",otel_scope_version=""} 2 grpc_server_request_counter_total{grpc_method="/zitadel.management.v1.ManagementService/ResendHumanPhoneVerification",otel_scope_name="",otel_scope_version=""} 1 grpc_server_request_counter_total{grpc_method="/zitadel.user.v2.UserService/GetUserByID",otel_scope_name="",otel_scope_version=""} 1 # HELP grpc_server_total_request_counter_total Total grpc request counter # TYPE grpc_server_total_request_counter_total counter grpc_server_total_request_counter_total{otel_scope_name="",otel_scope_version=""} 5 # HELP otel_scope_info Instrumentation Scope metadata # TYPE otel_scope_info gauge otel_scope_info{otel_scope_name="",otel_scope_version=""} 1 otel_scope_info{otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version=""} 1 # HELP projection_events_processed_total Number of events reduced to process projection updates # TYPE projection_events_processed_total counter projection_events_processed_total{otel_scope_name="",otel_scope_version="",projection="projections.execution_handler",success="true"} 1 projection_events_processed_total{otel_scope_name="",otel_scope_version="",projection="projections.instance_features2",success="true"} 0 projection_events_processed_total{otel_scope_name="",otel_scope_version="",projection="projections.login_names3",success="true"} 0 projection_events_processed_total{otel_scope_name="",otel_scope_version="",projection="projections.notifications",success="true"} 1 projection_events_processed_total{otel_scope_name="",otel_scope_version="",projection="projections.orgs1",success="true"} 0 projection_events_processed_total{otel_scope_name="",otel_scope_version="",projection="projections.user_metadata5",success="true"} 0 projection_events_processed_total{otel_scope_name="",otel_scope_version="",projection="projections.users14",success="true"} 0 # HELP projection_handle_timer_seconds Time taken to process a projection update # TYPE projection_handle_timer_seconds histogram projection_handle_timer_seconds_bucket{otel_scope_name="",otel_scope_version="",projection="projections.execution_handler",le="0.005"} 0 projection_handle_timer_seconds_bucket{otel_scope_name="",otel_scope_version="",projection="projections.execution_handler",le="0.01"} 1 projection_handle_timer_seconds_bucket{otel_scope_name="",otel_scope_version="",projection="projections.execution_handler",le="0.05"} 1 projection_handle_timer_seconds_bucket{otel_scope_name="",otel_scope_version="",projection="projections.execution_handler",le="0.1"} 1 projection_handle_timer_seconds_bucket{otel_scope_name="",otel_scope_version="",projection="projections.execution_handler",le="1"} 1 projection_handle_timer_seconds_bucket{otel_scope_name="",otel_scope_version="",projection="projections.execution_handler",le="5"} 1 projection_handle_timer_seconds_bucket{otel_scope_name="",otel_scope_version="",projection="projections.execution_handler",le="10"} 1 projection_handle_timer_seconds_bucket{otel_scope_name="",otel_scope_version="",projection="projections.execution_handler",le="30"} 1 projection_handle_timer_seconds_bucket{otel_scope_name="",otel_scope_version="",projection="projections.execution_handler",le="60"} 1 projection_handle_timer_seconds_bucket{otel_scope_name="",otel_scope_version="",projection="projections.execution_handler",le="120"} 1 projection_handle_timer_seconds_bucket{otel_scope_name="",otel_scope_version="",projection="projections.execution_handler",le="+Inf"} 1 projection_handle_timer_seconds_sum{otel_scope_name="",otel_scope_version="",projection="projections.execution_handler"} 0.007344541 projection_handle_timer_seconds_count{otel_scope_name="",otel_scope_version="",projection="projections.execution_handler"} 1 projection_handle_timer_seconds_bucket{otel_scope_name="",otel_scope_version="",projection="projections.notifications",le="0.005"} 0 projection_handle_timer_seconds_bucket{otel_scope_name="",otel_scope_version="",projection="projections.notifications",le="0.01"} 0 projection_handle_timer_seconds_bucket{otel_scope_name="",otel_scope_version="",projection="projections.notifications",le="0.05"} 1 projection_handle_timer_seconds_bucket{otel_scope_name="",otel_scope_version="",projection="projections.notifications",le="0.1"} 1 projection_handle_timer_seconds_bucket{otel_scope_name="",otel_scope_version="",projection="projections.notifications",le="1"} 1 projection_handle_timer_seconds_bucket{otel_scope_name="",otel_scope_version="",projection="projections.notifications",le="5"} 1 projection_handle_timer_seconds_bucket{otel_scope_name="",otel_scope_version="",projection="projections.notifications",le="10"} 1 projection_handle_timer_seconds_bucket{otel_scope_name="",otel_scope_version="",projection="projections.notifications",le="30"} 1 projection_handle_timer_seconds_bucket{otel_scope_name="",otel_scope_version="",projection="projections.notifications",le="60"} 1 projection_handle_timer_seconds_bucket{otel_scope_name="",otel_scope_version="",projection="projections.notifications",le="120"} 1 projection_handle_timer_seconds_bucket{otel_scope_name="",otel_scope_version="",projection="projections.notifications",le="+Inf"} 1 projection_handle_timer_seconds_sum{otel_scope_name="",otel_scope_version="",projection="projections.notifications"} 0.014258458 projection_handle_timer_seconds_count{otel_scope_name="",otel_scope_version="",projection="projections.notifications"} 1 # HELP projection_state_latency_seconds When finishing processing a batch of events, this track the age of the last events seen from current time # TYPE projection_state_latency_seconds histogram projection_state_latency_seconds_bucket{otel_scope_name="",otel_scope_version="",projection="projections.execution_handler",le="0.1"} 1 projection_state_latency_seconds_bucket{otel_scope_name="",otel_scope_version="",projection="projections.execution_handler",le="0.5"} 1 projection_state_latency_seconds_bucket{otel_scope_name="",otel_scope_version="",projection="projections.execution_handler",le="1"} 1 projection_state_latency_seconds_bucket{otel_scope_name="",otel_scope_version="",projection="projections.execution_handler",le="5"} 1 projection_state_latency_seconds_bucket{otel_scope_name="",otel_scope_version="",projection="projections.execution_handler",le="10"} 1 projection_state_latency_seconds_bucket{otel_scope_name="",otel_scope_version="",projection="projections.execution_handler",le="30"} 1 projection_state_latency_seconds_bucket{otel_scope_name="",otel_scope_version="",projection="projections.execution_handler",le="60"} 1 projection_state_latency_seconds_bucket{otel_scope_name="",otel_scope_version="",projection="projections.execution_handler",le="300"} 1 projection_state_latency_seconds_bucket{otel_scope_name="",otel_scope_version="",projection="projections.execution_handler",le="600"} 1 projection_state_latency_seconds_bucket{otel_scope_name="",otel_scope_version="",projection="projections.execution_handler",le="1800"} 1 projection_state_latency_seconds_bucket{otel_scope_name="",otel_scope_version="",projection="projections.execution_handler",le="+Inf"} 1 projection_state_latency_seconds_sum{otel_scope_name="",otel_scope_version="",projection="projections.execution_handler"} 0.012979 projection_state_latency_seconds_count{otel_scope_name="",otel_scope_version="",projection="projections.execution_handler"} 1 projection_state_latency_seconds_bucket{otel_scope_name="",otel_scope_version="",projection="projections.notifications",le="0.1"} 1 projection_state_latency_seconds_bucket{otel_scope_name="",otel_scope_version="",projection="projections.notifications",le="0.5"} 1 projection_state_latency_seconds_bucket{otel_scope_name="",otel_scope_version="",projection="projections.notifications",le="1"} 1 projection_state_latency_seconds_bucket{otel_scope_name="",otel_scope_version="",projection="projections.notifications",le="5"} 1 projection_state_latency_seconds_bucket{otel_scope_name="",otel_scope_version="",projection="projections.notifications",le="10"} 1 projection_state_latency_seconds_bucket{otel_scope_name="",otel_scope_version="",projection="projections.notifications",le="30"} 1 projection_state_latency_seconds_bucket{otel_scope_name="",otel_scope_version="",projection="projections.notifications",le="60"} 1 projection_state_latency_seconds_bucket{otel_scope_name="",otel_scope_version="",projection="projections.notifications",le="300"} 1 projection_state_latency_seconds_bucket{otel_scope_name="",otel_scope_version="",projection="projections.notifications",le="600"} 1 projection_state_latency_seconds_bucket{otel_scope_name="",otel_scope_version="",projection="projections.notifications",le="1800"} 1 projection_state_latency_seconds_bucket{otel_scope_name="",otel_scope_version="",projection="projections.notifications",le="+Inf"} 1 projection_state_latency_seconds_sum{otel_scope_name="",otel_scope_version="",projection="projections.notifications"} 0.0199 projection_state_latency_seconds_count{otel_scope_name="",otel_scope_version="",projection="projections.notifications"} 1 # HELP promhttp_metric_handler_requests_in_flight Current number of scrapes being served. # TYPE promhttp_metric_handler_requests_in_flight gauge promhttp_metric_handler_requests_in_flight 1 # HELP promhttp_metric_handler_requests_total Total number of scrapes by HTTP status code. # TYPE promhttp_metric_handler_requests_total counter promhttp_metric_handler_requests_total{code="200"} 1 promhttp_metric_handler_requests_total{code="500"} 0 promhttp_metric_handler_requests_total{code="503"} 0 # HELP river_insert_count_total Number of jobs inserted # TYPE river_insert_count_total counter river_insert_count_total{otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",status="ok"} 1 # HELP river_insert_many_count_total Number of job batches inserted (all jobs are inserted in a batch, but batches may be one job) # TYPE river_insert_many_count_total counter river_insert_many_count_total{otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",status="ok"} 1 # HELP river_insert_many_duration_histogram_seconds Duration of job batch insertion (histogram) # TYPE river_insert_many_duration_histogram_seconds histogram river_insert_many_duration_histogram_seconds_bucket{otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",status="ok",le="0"} 0 river_insert_many_duration_histogram_seconds_bucket{otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",status="ok",le="5"} 1 river_insert_many_duration_histogram_seconds_bucket{otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",status="ok",le="10"} 1 river_insert_many_duration_histogram_seconds_bucket{otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",status="ok",le="25"} 1 river_insert_many_duration_histogram_seconds_bucket{otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",status="ok",le="50"} 1 river_insert_many_duration_histogram_seconds_bucket{otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",status="ok",le="75"} 1 river_insert_many_duration_histogram_seconds_bucket{otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",status="ok",le="100"} 1 river_insert_many_duration_histogram_seconds_bucket{otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",status="ok",le="250"} 1 river_insert_many_duration_histogram_seconds_bucket{otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",status="ok",le="500"} 1 river_insert_many_duration_histogram_seconds_bucket{otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",status="ok",le="750"} 1 river_insert_many_duration_histogram_seconds_bucket{otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",status="ok",le="1000"} 1 river_insert_many_duration_histogram_seconds_bucket{otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",status="ok",le="2500"} 1 river_insert_many_duration_histogram_seconds_bucket{otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",status="ok",le="5000"} 1 river_insert_many_duration_histogram_seconds_bucket{otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",status="ok",le="7500"} 1 river_insert_many_duration_histogram_seconds_bucket{otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",status="ok",le="10000"} 1 river_insert_many_duration_histogram_seconds_bucket{otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",status="ok",le="+Inf"} 1 river_insert_many_duration_histogram_seconds_sum{otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",status="ok"} 0.002905666 river_insert_many_duration_histogram_seconds_count{otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",status="ok"} 1 # HELP river_insert_many_duration_seconds Duration of job batch insertion # TYPE river_insert_many_duration_seconds gauge river_insert_many_duration_seconds{otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",status="ok"} 0.002905666 # HELP river_work_count_total Number of jobs worked # TYPE river_work_count_total counter river_work_count_total{attempt="1",kind="notification_request",otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",priority="1",queue="notification",status="error",tag="[]"} 1 river_work_count_total{attempt="2",kind="notification_request",otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",priority="1",queue="notification",status="error",tag="[]"} 1 # HELP river_work_duration_histogram_seconds Duration of job being worked (histogram) # TYPE river_work_duration_histogram_seconds histogram river_work_duration_histogram_seconds_bucket{attempt="1",kind="notification_request",otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",priority="1",queue="notification",status="error",tag="[]",le="0"} 0 river_work_duration_histogram_seconds_bucket{attempt="1",kind="notification_request",otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",priority="1",queue="notification",status="error",tag="[]",le="5"} 1 river_work_duration_histogram_seconds_bucket{attempt="1",kind="notification_request",otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",priority="1",queue="notification",status="error",tag="[]",le="10"} 1 river_work_duration_histogram_seconds_bucket{attempt="1",kind="notification_request",otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",priority="1",queue="notification",status="error",tag="[]",le="25"} 1 river_work_duration_histogram_seconds_bucket{attempt="1",kind="notification_request",otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",priority="1",queue="notification",status="error",tag="[]",le="50"} 1 river_work_duration_histogram_seconds_bucket{attempt="1",kind="notification_request",otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",priority="1",queue="notification",status="error",tag="[]",le="75"} 1 river_work_duration_histogram_seconds_bucket{attempt="1",kind="notification_request",otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",priority="1",queue="notification",status="error",tag="[]",le="100"} 1 river_work_duration_histogram_seconds_bucket{attempt="1",kind="notification_request",otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",priority="1",queue="notification",status="error",tag="[]",le="250"} 1 river_work_duration_histogram_seconds_bucket{attempt="1",kind="notification_request",otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",priority="1",queue="notification",status="error",tag="[]",le="500"} 1 river_work_duration_histogram_seconds_bucket{attempt="1",kind="notification_request",otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",priority="1",queue="notification",status="error",tag="[]",le="750"} 1 river_work_duration_histogram_seconds_bucket{attempt="1",kind="notification_request",otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",priority="1",queue="notification",status="error",tag="[]",le="1000"} 1 river_work_duration_histogram_seconds_bucket{attempt="1",kind="notification_request",otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",priority="1",queue="notification",status="error",tag="[]",le="2500"} 1 river_work_duration_histogram_seconds_bucket{attempt="1",kind="notification_request",otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",priority="1",queue="notification",status="error",tag="[]",le="5000"} 1 river_work_duration_histogram_seconds_bucket{attempt="1",kind="notification_request",otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",priority="1",queue="notification",status="error",tag="[]",le="7500"} 1 river_work_duration_histogram_seconds_bucket{attempt="1",kind="notification_request",otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",priority="1",queue="notification",status="error",tag="[]",le="10000"} 1 river_work_duration_histogram_seconds_bucket{attempt="1",kind="notification_request",otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",priority="1",queue="notification",status="error",tag="[]",le="+Inf"} 1 river_work_duration_histogram_seconds_sum{attempt="1",kind="notification_request",otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",priority="1",queue="notification",status="error",tag="[]"} 0.029241083 river_work_duration_histogram_seconds_count{attempt="1",kind="notification_request",otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",priority="1",queue="notification",status="error",tag="[]"} 1 river_work_duration_histogram_seconds_bucket{attempt="2",kind="notification_request",otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",priority="1",queue="notification",status="error",tag="[]",le="0"} 0 river_work_duration_histogram_seconds_bucket{attempt="2",kind="notification_request",otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",priority="1",queue="notification",status="error",tag="[]",le="5"} 1 river_work_duration_histogram_seconds_bucket{attempt="2",kind="notification_request",otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",priority="1",queue="notification",status="error",tag="[]",le="10"} 1 river_work_duration_histogram_seconds_bucket{attempt="2",kind="notification_request",otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",priority="1",queue="notification",status="error",tag="[]",le="25"} 1 river_work_duration_histogram_seconds_bucket{attempt="2",kind="notification_request",otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",priority="1",queue="notification",status="error",tag="[]",le="50"} 1 river_work_duration_histogram_seconds_bucket{attempt="2",kind="notification_request",otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",priority="1",queue="notification",status="error",tag="[]",le="75"} 1 river_work_duration_histogram_seconds_bucket{attempt="2",kind="notification_request",otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",priority="1",queue="notification",status="error",tag="[]",le="100"} 1 river_work_duration_histogram_seconds_bucket{attempt="2",kind="notification_request",otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",priority="1",queue="notification",status="error",tag="[]",le="250"} 1 river_work_duration_histogram_seconds_bucket{attempt="2",kind="notification_request",otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",priority="1",queue="notification",status="error",tag="[]",le="500"} 1 river_work_duration_histogram_seconds_bucket{attempt="2",kind="notification_request",otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",priority="1",queue="notification",status="error",tag="[]",le="750"} 1 river_work_duration_histogram_seconds_bucket{attempt="2",kind="notification_request",otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",priority="1",queue="notification",status="error",tag="[]",le="1000"} 1 river_work_duration_histogram_seconds_bucket{attempt="2",kind="notification_request",otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",priority="1",queue="notification",status="error",tag="[]",le="2500"} 1 river_work_duration_histogram_seconds_bucket{attempt="2",kind="notification_request",otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",priority="1",queue="notification",status="error",tag="[]",le="5000"} 1 river_work_duration_histogram_seconds_bucket{attempt="2",kind="notification_request",otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",priority="1",queue="notification",status="error",tag="[]",le="7500"} 1 river_work_duration_histogram_seconds_bucket{attempt="2",kind="notification_request",otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",priority="1",queue="notification",status="error",tag="[]",le="10000"} 1 river_work_duration_histogram_seconds_bucket{attempt="2",kind="notification_request",otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",priority="1",queue="notification",status="error",tag="[]",le="+Inf"} 1 river_work_duration_histogram_seconds_sum{attempt="2",kind="notification_request",otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",priority="1",queue="notification",status="error",tag="[]"} 0.0408745 river_work_duration_histogram_seconds_count{attempt="2",kind="notification_request",otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",priority="1",queue="notification",status="error",tag="[]"} 1 # HELP river_work_duration_seconds Duration of job being worked # TYPE river_work_duration_seconds gauge river_work_duration_seconds{attempt="1",kind="notification_request",otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",priority="1",queue="notification",status="error",tag="[]"} 0.029241083 river_work_duration_seconds{attempt="2",kind="notification_request",otel_scope_name="github.com/riverqueue/rivercontrib/otelriver",otel_scope_version="",priority="1",queue="notification",status="error",tag="[]"} 0.0408745 # HELP target_info Target metadata # TYPE target_info gauge target_info{service_name="ZITADEL",service_version="2025-06-09T13:52:29-04:00",telemetry_sdk_language="go",telemetry_sdk_name="opentelemetry",telemetry_sdk_version="1.35.0"} 1
Example grafana dashboard: ![Screenshot 2025-06-11 at 11 30 06 AM](https://github.com/user-attachments/assets/a2c9b377-8ddd-40b9-a506-7df3b31941da) - Closes #10043 --------- Co-authored-by: Silvan <27845747+adlerhurst@users.noreply.github.com> --- go.mod | 1 + go.sum | 2 ++ internal/queue/queue.go | 7 +++++++ 3 files changed, 10 insertions(+) diff --git a/go.mod b/go.mod index 21a7fe9f16..15b3d2b391 100644 --- a/go.mod +++ b/go.mod @@ -146,6 +146,7 @@ require ( github.com/pkg/errors v0.9.1 // indirect github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/riverqueue/river/rivershared v0.22.0 // indirect + github.com/riverqueue/rivercontrib/otelriver v0.5.0 // indirect github.com/sagikazarmark/locafero v0.7.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spiffe/go-spiffe/v2 v2.5.0 // indirect diff --git a/go.sum b/go.sum index cc3bc35841..272ce655a3 100644 --- a/go.sum +++ b/go.sum @@ -682,6 +682,8 @@ github.com/riverqueue/river/rivershared v0.22.0 h1:hLPHr98d6OEfmUJ4KpIXgoy2tbQ14 github.com/riverqueue/river/rivershared v0.22.0/go.mod h1:BK+hvhECfdDLWNDH3xiGI95m2YoPfVtECZLT+my8XM8= github.com/riverqueue/river/rivertype v0.22.0 h1:rSRhbd5uV/BaFTPxReCxuYTAzx+/riBZJlZdREADvO4= github.com/riverqueue/river/rivertype v0.22.0/go.mod h1:lmdl3vLNDfchDWbYdW2uAocIuwIN+ZaXqAukdSCFqWs= +github.com/riverqueue/rivercontrib/otelriver v0.5.0 h1:dZF4Fy7/3RaIRsyCPdpIJtzEip0pCvoJ44YpSDum8e4= +github.com/riverqueue/rivercontrib/otelriver v0.5.0/go.mod h1:rXANcBrlgRvg+auD3/O6Xfs59AWeWNpa/kim62mkxGo= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= diff --git a/internal/queue/queue.go b/internal/queue/queue.go index d680221753..22df8c2b5c 100644 --- a/internal/queue/queue.go +++ b/internal/queue/queue.go @@ -7,9 +7,12 @@ import ( "github.com/riverqueue/river" "github.com/riverqueue/river/riverdriver" "github.com/riverqueue/river/riverdriver/riverpgxv5" + "github.com/riverqueue/river/rivertype" + "github.com/riverqueue/rivercontrib/otelriver" "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/database" + "github.com/zitadel/zitadel/internal/telemetry/metrics" ) // Queue abstracts the underlying queuing library @@ -27,12 +30,16 @@ type Config struct { } func NewQueue(config *Config) (_ *Queue, err error) { + middleware := []rivertype.Middleware{otelriver.NewMiddleware(&otelriver.MiddlewareConfig{ + MeterProvider: metrics.GetMetricsProvider(), + })} return &Queue{ driver: riverpgxv5.New(config.Client.Pool), config: &river.Config{ Workers: river.NewWorkers(), Queues: make(map[string]river.QueueConfig), JobTimeout: -1, + Middleware: middleware, }, }, nil }

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

wvBeYmA)c51m?GXru@LW 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[Lva9IM$XphWA)+E; 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 $@1U9CSV$;^ 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 literal 0 HcmV?d00001 diff --git a/docs/static/img/go/api-create_service_user.png b/docs/static/img/go/api-create_service_user.png new file mode 100644 index 0000000000000000000000000000000000000000..6af66bcbca0c3feff5e804cfe179733048e7fad9 GIT binary patch literal 43016 zcmeFZcTkgE_bv=J?D#wi2nbjZRGLcfb_`Xi(m{GxAVdfxc2oo;bOMQ}^d_AULQoW> zBP~FHh?E#Y2oNBIkmTHXp5J@s{Qu2&X3osZn8_Uy?(DtS+G}0wy4Lpf9dn~Y`z7`Z z2nZYk-M(QdAh2hLfWWRDd-njp2{xSx2cGslxNRRMAaLL){~;)lm3>@5;G_WP##QS_ z*^85q43Xh!!(V+>ie`-`Z{AUS^(KDw`_m6@UnHQ4cRkc#9&azzeJiZ|ej{E<&R_Aa zm70HPB~!T)vUvKaRA!T&&GtHI}gVhbh+VEepnw|goLSNH=EbF&; zYlvV2*EiHXD4v~_m389Lk0cDM4m;l!0U3u;ogQ{z&MPQ%mDQ7SIKl?^n*+uE`D+Q{ zea4NJnjSb-_Mf03uyHyGOhRc4%v>w*Y}W%@hHPS;%@$jAK(0P|F2zv_3JH` zdoT8E@7>ufxx=)&_1%}TVCs`F+`q%QXRRoDwZ)mKmm%f~3(B8k{Jp39!mis0;FS_& zlJU!_R^*>A7Cjr+b6jxFENhmF4gQf;QlT^0_%cEKV!;u&UnAK3;>IWxu{(R6g1a`d zRDa8sjt$jO9J?eFz3lD+o2xw-v+*K&W2|s&M1S!YXRtJ~Iy24AqNwwSg<+RM#i1@_ z2+pd4G*_-0lrF8`NWe}GTNQ_X`EEBCm_O47QC`mT+qWd*v%r?DR5$nf0w2Fh3 z5U^6D^~Eri`JeJH)8;juLTIJ5v#DF_njS0R!@Yy5yl`~{qIY(C|lWDkl z5u$Ojls#>LEr?-9h^u-BmN#sTXO1xcOsQj+s$!mvug+Ta%~E_&7Kmtg0d1h#h&ePg zH2Wl0x~r?A`BM89b$ zGsmA6OUya#;Y?d!)x;EZ32|6zT=S3Cg5<6fF|+RWFqTUmE8R8bjD_C1X^t1*nGd$s z9Y0>BW!&w{!w&D?@iMU5OJm#@wB4`Q-ak?Gr~-xhRJ^-z45dln-u!R@>g| zH&SY!Z*5+ry7wN&g1$J~A|Izy6X3XihsF{fHsbIx1mTGds_TA#3ffy2%$V;98=;#k z;^0QnYL``-))oS(+bc}@0+aMSgsej8=Rn-k$1YtPic*A>&hzVscWg`q6{M(VZnca8 zetfa(q#IfvkCs`gH|p%_Do}BHh$+Q$8$k#QLyls5&V#5a8H4-zjSQ-)rXgO4zo>8g zEB{FNX8F0dOhg=&-Q^oitr%3%bmi5rksjc*U77-hi)~==wvg4>L0GDH&xLt^_}tvw znEx+t469LLu*f48Gkz<_e}Q?@EH)x2!1;&;g2T+=-25GR*IAz~ylHKgdD&uJY~&Je zOP@<6(G}M4KJNT(oaS|KR5!$7Mq}5BNphyw=12rG~xZzXuDl#m#w=xGp zV!JIhQMqGj;_RLV?O=J(VVz$>s!EQ2FI@Zxb!N8`#dQZ8@!RVQii2U4%(V1$&5o7! z6L+%=$xV3Aug_nrrb1UzGp|?q8Jt*o*sIO#s}2eQo0;JnYcq3RN;4)d1PsWEZLicU zHnl&=)Wd`}2SJSIQnptclF9d$Xb_T$8HxAHLwhMbHht-XOx5HsJ?3wP>QXEXvgeFz zSkD+lNZpgFWHP8eJd9yJ(KbBv`D9tsz<%+no;8g_G6tvNEUHZ;YglstT+}PxH1~M1 zp>>4suf@eBj0MS%-U#Ub=_;N;bVt05{bzN*{av;?cwIQV`F*~e9dBmKDY6GlI?r-J zo!AQO)#m-)UhLmlz5Yunlx4%q3flWl79G9nSEP$fG=gRMpy1~TJaXNMt&g0l_FyY| z-O@l_SR;gr0N&Lh2E}Yq$dN0l1j*>PVDSc4TOLIPzkNYUH{v!B_gi4l`6fdSIDa(+ z$8ed7BIAH{)hv?QHNorm0Phj3R)tM8up*qQ2m3uxVnDp<#Q5Z7@pOMTu z&25H{qT)+6ay@CB$O5R&>tUxguL(O>mbeR;_lk9r^!=S{?U? z)%)Kr5Y65lar^mT3)G(P_q`rtWoCAmMfnMSE>P?IU`1psf*cd-q0d&Y==z@S>`Xm9 zyN9X~_-i7va;#1A)6Flw9&cyBjE3!RZBwfXf$8K|6 zP*n9B_>>Ku&cpC>+eK}wy^AjW8yZA+#|0gE*R6f9VRe8JI+sV@Cnzy?AtuVFM|{f^ zzMLe#I!{z3tvA1qULUs0qexF;m1;Ok%x9CvgNb?@)=L;(lC`Zl?Q6bQN!r*0T&O1CnJe^w&-O#)QS5U`NRXFJiilq@kA0 znIj?5_VzIN);?hW_Cyn5*Ud66UP4E2+55qg<25siy8kw*F=hrkC(9m{H}9rFrqV;& zV9A~fCtHEQ8IrG~8>m^R#u@WL`%E;fFtC(E8qu4}18dS)TE-kyNfkK|_p)tMnjd)q z^s#aE%E0G~SN48hE*tRqKCretedaPIlzV7VM;^QQ+03th8c4tJzNXK2HlV+RnaW_^ z<<||7&d# z6YjS)$&BsGpWU{)dl!Y4j=EEZEs}}crbMepb)TRf9uKWvbIxNb640!6!@h^hzn>sO zEVG(d=7}bQGSQoN4S3PkI*_H5l+?Lc_EHiUT^=ih7#ynknSp#R>FN4lS2(| zU~LS%uQ{*K)gp+TEr-fnD3hs~TYLC-InLdmv_)`+o78PZY(F2yt8PXD79MC^|9h~W zhbC5^Yha*+R6Vg@?7?#(hVous_D~uO6G(%xycabie@(ZC&Djv^ z);|_Lhxocn5LCs9cs5XZoo!!EAI*)aBL&|HZl;yL7`-rFA7GaSqn1I$ei-x`l50#e zFQ4`DG8?Efqt*nDeF!Im5luX9tTu;99u9DH#zue$V{6bUAmxs^q)Y_P9LirZ1P!O0 z$FN4mV;V`JL}%8|Cu~{Jo)B%f+}(moj6xv1aoeT?oqS$VLC5bk-L2rymKn0EPA2BGaEf zjtC;AOxf`rAHai2vXNw*@M@3d41cAH3JZU{q?S2H8M~3C@)K1rdUuZwSc%3xMN6?t zPC(YC$3huq{xKsC^?o^0FAw$}CLZd{Vd}jL>%~4wn5H(t=JPYr|GL;s%U9)35S{?I z?-fbUb|km745p-}&b=||?J*~Y;Ep)9r4*mfqlnXkQD_~o!)5-;q$}qXLSrHchoL%>CHv3bHS1A?7n2{{4MU6X>5k#PNcV2s8^wVKtAKr~`-KjndLEjg z^dieOiKFe?)a1VIbXRkkgBzOdGMRa43u;WsY#LRK&HQO~9j#!-boR;{SPRA;Ja1+} z74?nE@u4be&TL9tM@KeKw^s@P;FP>ZLZc@)47BPwOw?O()w+l$RaJqikKB|CLm+01 z^z5opr4;J5p04r{@R*Aw9zgSvI}i zrwsO6EXBQbsx#M6H@G~Bz2@Yc26kvtMQtrM*=b5^FfGVJI8fm zqX=c(t4O>K+U`{EcU5A2&W<&+JAF1$%n7kH**N=7hL&!o;h^GWmOtNJQa+(e48ht2 zV`rx9gFRdf!c?>VWBjU7A;Ibcf-X^B3%|Yul-goRLGCSI)ql4p8ehHAhte1QVE}vB z{hGCz+&2IqG=kGZ_eKc15ua?Kbpg5r^}W^epnmhI+RS4ND0CxJe%LiP;3EsujEj}w zO}fE&GXvb>LO5kW8$=6!;$5m*27R9|VpiR8i<;-remIW+{{8KBWX!1Jq2R7Um3&CS zAb=CIQc`;^9S6|Vhw#4)LC*|yWfHg5s#y7wJsNv6Tt!p= zC!6)$pH%N(y!M!1g*qObQs+NZnzzSIso;DX-eB)RE}OBPT9YxkjTmByQl6;o6U*7{(IY zgdrK+5FByh@}p+DkGV_qO6tH`6ty%}KZ01K#+wX&k{`JRAH*ITk1U{Fe$*wH$Fg&Y z{FopSwgH6Rf`|%6nTXvV^>&?-vc^!q#^PfC@;wyJuhzs+Y7iuS-si-{Xm{Vm|2;3s zUESw*9dB4KFDfoKw*iCq1S=i%HHA!ZTGrx{yu7q&-#o+~>6)1iI>2647om_vrNrRGCO;CAga zl2<{H$>2w$#7rvZu*}L`zo?0ZePOs9%SK}nyF zAwrH)RW#ekwaS9aYhz;bOD7lHNiq4t8SL@T^JSg#F4VFu0wE>&dz~io07nW zsqq6|rH@&DclGC66Sr-+oJ5nsQu}I$$q7Zp>*$zMRu>9B`F$U%7zG?!wg~I^-jDCe z{4$tddw)OpRqK^b zx7Rci~>}2 zVA{P8Hm#Db!X&AS%q;jeGy&We%`lsaWO4?!xojnu$(>4_?Rrt&qbGPsRex|?^=F*H z_4Ip#dhErx>N3VqKiS(sW04k#eFa+;C#N<30NLA@H!gk+thDE4b_U^wA3JTNhZ)cO zZEb5i&m?dA<%b58*BP5@aerio4pai5X^@|%GdlxSeHo?LrSUu#Tl$eFD#6`FvHO3V zc=-PMjg|?(`j0NZU$F*)eSJYr&LAA>)QL2YAu8dcD<;ha6qpA9sYpVMJ&xX4o8}CS znVj5V7B;!A=@Ta?NpOfDOjb`B#k`f&G=SrN%V@8)v*j~&e3wID>v7wwWWx?0G(1Vx z-oMDac2Af&;(&6aVzQiBmLC+msP{=pv#cQat^K)FeQ3uLbUBGn59O7=mNU=Juyc{oAOg~<(j%u<`9m9uls^}|yYYQA9r%TNs%%KL(&x|gx?He(LM>18iW zjDQMIsc5Mvw}>e}V$sz*_V#Ix!I_Hn6PHy}Iopi8+SA`iJ(V$GjLd*nnEznP?;ir* z*Rsnlxn6thw=ihdKh7D+DA2H~ueg=iAsD-@m+lk+HrRB#0SxaR_KKNV+gouY!9_A{ z5C#c0*P0WqJ}Bt#ea|2=bWV{ONdu4)iY=`SKFW9o5J!1BO79X0ct)RvpB0uGBT%ek4v9q(diTCBzz;<@y?yPwVPv13*`{3ITQ&b$u-MJi3*;vGo zaw9o_3vwsU0&M5P=5D?p1heS+)srK;#axrn+l!+Iq0~v!LH*)HGjm{WmQiH7EJxEOpygGZHT46*6fR5kdd<#!3OTFZWf5}=0P+~*IY#<-9)% zUSH)d{}opFuc{H({9SJE?fjR5iH46Hr=r=Dfta3P2CaN>Rn9Dwltb{z6G^w=_0)^~ zD{tI69%u7E;)PoG{Y&>Q@~0_q5%OQr3gCZb&Ht~GuUqqRt@m&Z z3LfU6xlzk?GJ+U+oBv86L&!`LD|pPcz}#eh`8q5ktEy_7*~?x9sN#$umW)h; zMI(fsm1roPX;QJ-e$9Vw!aiu&U<{m2V|2))=YDsUqdZu{?DA|nT~C&udgy(u!E3(L zU#t6Euz8V;H+ox}gYlEc1TuVpMAWxpB*QQL=laBWN*~)3a)MYIW$8bX40UDAveY`x z!f`rh3^{?PYQw)sHhh;82g(QxH=ihpJr4kKr_~z*2PPz9?0Ny2Bb_T*id-MD4@j8f ztE+s4?BJ9?<3)dCWc7HlLIH0Nb)F4~j~^5D0u$!ykAHr+74kW_?sz}^R88;~_FNwv zp`fCzqM%rB>SqrqUvmR)H(-bU(+gmjljX`QpuoIYoM=x0w`VGFW}C9DI)AU3 za9N*UBlEw)o3i9AcI|*zDW3%3VK!C+E63Kt(rlF+X6x|=7wkCe+qVM6fuyFnOYkE4 zphScHp1{Vt9-1Dx61p2mMO-++DSFzTQW;(!zBF_`-8D2gYJDLSNWq#B3<0<1KaE3{ z+T1^_l34KrcJ}sJnjvX3iCp`afB@ZTnI>6|(~}HpZ~@AacdK&Tcz`ti{#x}>Mq+~Q zLIGvOtEerw?OIsJ9Kn~P=azN5xn#l^*;{ilrhQbX-uj(Y?c1W{yAc3Iu%r%GgH zKi^bik<6WpEo4Z6q8AA9Cf_VjR0vnG|>TmtCeE5$Eo1nC%x=je%N+u_LF{8Ry?|eyV&Hb{u z_U+%iDss85hGhvbc8u3&H&F$JikkZo6R-zqw6QY){_gx-Kh+}znM|KPV}L3Oym>Qn zPtc{40p(+wla1gWf-5Xl%q2>l&;Jz%`1_}FD7iv>T>-QB22ZA5@~8%M22Xa7P@Wm` zY$)$~;v7kHN~66lMsGDaoIav1r5pakPhKi{#zLq1OFJRYgdC{tHd9)+(8;d@Ser8z zp`>St^V999Vw>O`Sb*m*QRIAOJGyhmLO+=cXl-SCg3}^s@4@CF$Z|;E61=ba6pV z%Ow1$dsbhn{=*d;5kwW)b%IZk&#xXBDE(kRVvlu=m~ux>gi~|Y4Vq;*59o`{)Wdo0 z^kmOWlz!EhEvhHg$MwgGJg2;-3D~KgT|Jr|ZGg!%Nm23vrBjftfGT73Vilu~MJpoD z4#f2um)MpWQ==*xqW1)0?9Fu+|&H*k7_3lx(?MomE_dba|0mOlVh-yMmQ7^zU7damR zFOC}M(mw|k-2!B;J`|P?#9Z*5UR`#|nhT6I&^VV)P3_T`x(EIO&e{F>;5yE9{7$53msMU%qY9bTp@b8NIKo7qS0`| zhq+iuO*n62-)52VP9DAG>H?$1TN6xi#0NRGgw7U0j!!Y+!_BzT$|zE_epL8NE*)d( z%E6_(HXqyZR$5gqQIBig24k2ZK7s6$`DLu1rH1|ke$s=%HWRG?Ujjxlx51mr>Ednl zHEj+IVfWiD;jY2Lw!0zBL}^_OAQhP*^mxC!BIjz|K?r^g)TM$iCRFZh`TdKG-!_vo z&q|S{)Q>ET1(8p!7ZU<9rL|Y=jPh3WX1$TWv;ppe;ZNbJEcc(iFp{Mf{(sXVA^ri;&4y<8N>6#V-J6f@QbX0?WSz8 znC*@s`Ms|2nVu##H#Toh2Buc!(tcP#Kv75Uua^7n)R60sfk?g>)?%T`BEnG>P4kW6 zfMzvvlT11R_smK$>uE^ftouby=h>N1`TN`nQ1<}^2Dyl1_p0hAkbu?XTUmQ0rFyju z8KT*c12kp$bHS5lW6(rrfPn_}YBN49*7YT-0u^;2)fo_ssv_{}0M$`J9#G)kb(jL! z`)y8V96}ZghsOdc^^EFp%)G9}0Vhb$cgkq)b%$F2R6g;K+c2!DS=tUW?<$UoD)5RE zwf9)JVE~N_`AwkY6Dxx<@}PWcvkpMH$_9~K{xod7bX;wXiRcsF*oN_UdD_y5x=hc5 zO7~l;21$YfH&uftHn$fveP*)_v$LyG=6*ff=rE~hN^=jv=@O&()J&{&%;_G53#;rf zUN&IK^pVM^RvW*u4>w1gnNaWu2be?XuU!1OVZeg;A99jqXpJ&Q3w?Q=<$}Dh_cDu> zVXy`|3WRWu7}tO|{Th&EM7XH+03=fuROV5QfeO$%`1uupMdDuMn-zH50*xc~;M7$A zGWJpqIR@`+xDZ$2YrhBR}6U&6IcgI001wrD>?^`eeTcpY##z;KiU7Hhf`xL~!5 zFXe0Fb^QQZFdqG`HBl$mmfuzYGbj$1hXbH&#GW~;%DorBWr?|-HzS2YY2~k_A3fMI zTv1p@jpwvPQP)hmM3oS-iTamnj_ojI)Z=-&R>k?Upb^)4(uu^~0&f=P2va}Qc?||2 z%e1QP>9aZ<(U*GG39A^cfIgH>6R|Q%AtnmYgfsCT5S^~+kq!-83*TXF# zLkK!TjhqCw@t}T`qH`81opBm~0a|=loCb8&A36aQsj}MCw?|dCKPF+QOYE>N8f3VU z$OlTsF^ly<*c;`T-xjJtB@BdAvg{-llIb_e?-66S8g++36Lq-t7%n|M-=@UY9_S@H z@tH1-SWB-XA$CAQ_PWWfb;;Bq$!1M$e4G! z8|pTHAr=*$>s$UyW)8|30XAq5QRCySm`g+*3mzRauqIE6AgcEKsq|2N`1@Op@6Xdg zY4=WdG@-=+jsE+oy>3q=0US3F9($NZzSj&8)b(>U&0)y^`6SaTQBvU`A@7z6V9R!) zrDPeY&k$+oecxU^_EUhW_4+Q7rZDVh`%}?5ZNZvs;BT3H$)l~erYWj(5xZPe(UGhW zHuMnRbCb+dHtWpttk91r2N!Tl7u7r;>^UUyE)NSf0f4DV;;QBiFc|FD=>L*JFGqTh zp$6#^fy6qq@wy;Yt4zNc>1fV+#-4g1fj9G`t~pr&6&S4`hkA4RTzX%k@PxdD!FqQf zwUmuhucr21AHXtK{l=_yk(d$%2V!0X8X^UKK453)CW1|;GA5w;j zJRHk`<6L-<~Lg?rVo-&i7m}hO@<<-=s7FM2oLe8(>DHnxCbdow+$;ZDCl|S%(m# z>44z(X5~>dNwARob(#2WSsxUqV6lpPRePRL;0;J)AVhPFojIb3)4QfKn5LeO492PU z>9L~`MUdY?K!*!B4GVNE0OWhDHNl#^0N>WV9xM}(y zz~L=`cC5CyIa|JLe)v0^=BsWf>`|V*Of;MYH1Mpstv7F0!>Ntdh=1B7)p?DTBMm}5 zS<0hA5EOhR)skRn$o<)TAly~pj%@wp!w^*T0=!zOn%l@h04-4)k>)y_Y=@oOBkEU{ zw`=#kF;ESID!#NU7B)jgc6*Xi6&v+c${<3>L(PjG(B0M72>|R!So-?`YV7y zo8Yi>J_rw#f2#;a)pS%qdYXDk;g5V|V1}%?q5CzUbqOIW<`iy^_|(*-2gj~8l{Q3I zSmrbK^0^8f3iQC4c}1msgaQUT}Uz2cV~4WO7y23GlaE z3hwvnftnaV9_s))OEP6C{HlwWJvflR{&(K`GXUblc^t$<#LO^d$ggAfxTwIriR|bt zZPxED6(0<&Go>hoRUbfYB5O z&4zWZ@KX`s$f#4qz*OL&ZZ+nEiaC|;1T!o15?aFBIjB_LFxOtm;Hz66&Eo0iCK2Zj z)j-BldFN7NHY*FN)~i)K0c2#%j-aikAAq?w?sWyQ>4kbzKs)_?hpM(Z*cUpl4ezW- z)c(-BE(728OIw;!kFj$B`qO3xq8UA#;k0pWvo2&lfFf+MV$Z94IbkDCF~i}$B!iL- zN)FEndJ}*rpuzmPya)M>`8t!0;pHOz0xdi`P-d+Tx@Nx%P{zOS6?`0Pz;FUOMs4`b zp~woJqXBO_P7&i7TjM_#y1?pq$~lU^{g3$%pyhYm^W`JE#YRFR(2~nj(3iSs1~vl& z%4i9&oMYPDPoy4e=KJdj-3k@LCV;j05Te|~40qeW{r9(R%5Q~Gq7-xF0(={U?}~&s zs?Fe~8Y%$u0~xpoWOfU-Wp8dkD|O=ZNgB@;OePtYU^jg7s;WYJUo@}xpRt(T(507! zO;+Nyl|D9SjN4oR)^EUr5)9&tN3IEa^WQ=w?X7$!v$7T(7|GPJd~qV6CwOg4F#9KB zNYC+FthKpw6RGgyu_tGgNItXpxl4?2%M{0*pq$qt;G7x-c1fGkMUaEd6V45 zSXHPv3*uWE=;0~w>Jp7))oNC?!(!98(sp(h9H;?}{r~~=$9R>GyV%%ZU9R`>xAgDx z0i;@sNjoujK_KUy?qWJ<%(N%+QxpN~lHQxcA9CbO8RRkmgtXHtfX$q^G&u-B-Ai|6e#~pdPfiXPK9-?5va5w;wm~5n5jpKxnK) z?i5f%{QQXTc$H}6J*rD54_X+hM}&EIu?`5lQ*ngh^TZNa;EaA41a2Oz0a z@skGy-2P%LP($XDqC;`ZUMEl-kda3^pt{US*uc0cMEFcSV$guin_GzlZuVhiM&5JtRpROtyLGU-eE?t<&JGrP z#YHbqx5rP`$o9GlJkC^F0H#sx0T3XC!1Pn43-DAFd-A4`z|bnIP$iV0&y)wy&48|R zW4QmUs5XBTA$RrQf*D7z3LLOTi$tBx+|t6qKL|cuPjZOK;j z2cE)C18HvIXN*6-5twX2lH(4bEX~hRFx7?ln(_y+buR^Yn!jkc$q-AFkb6-fA$)3 z#>&<_Y#YMp`%2_40mTnG5sUBE368oYmEr>wy*cwnj5{H{f z;6_R}F-PbxnajGCp1fZND;jUQj_ z?q>9TxDP`S)Jh67D5DEH8@6Q*Uc)w9vqDf>8nPTON8X+uiVET_D5&(7X}Th7fvk~s z>I%ZzTCJy7H#pr61=lki4I|j6WfDm4NR2Wo?>E5Q>hY^39IZ|&Tb=L+DeE_0bJ_<5 zb~#h;yP+}aHd)HS)goj|)}3pU9IZ~oN^!fKU>z^)Lpu($2w=_mctDMeNEUG8SJ&~A z&sqLg(FEWG$52)UbqDv05DhJ2=X6SUsS1)q^d0yFl7?@VuQ`L~GV3^F@;(r1P%ISY`5S$?`?-U_7l+GF5QkQ2PMM|<8*{vUe5CW1yUOFhu5uUb-} zp+B@a*C*Vi&zwjHn1uYAQs)9MpRpxP7x65hFG>!y)IgnDWIcPf5cv(LKYuCe^oaoU zeRta@P_pV8ywAIx&O0fO4!u4w(Hpi(Q}Z`2)4Xo4hL^9t%ws4tFK8O{tI^QJx-^PJ&kTH0sI zqouQiq@xw-%}lPTS3N1>0iVOBmtms(>?0$S9cVF>RKn`KBYg1B%LRnU?K416w;BP9BR3Y^A~!~3>w0%2NQE(!>d8xN>F&p78pfB`(QM*U z1GJ7-ROp)}6&q-k#e)N90K<>jNVcz#mhyL;I`zE&)Y{)sDyMHBJ@)C<{=Ivj9=m&W z*Uj&TPoIAJ;pDle;eTCqx$)-BZ90x{<(lx>HP7Tlb~^%;7I^#Gj(t>SSOVD;qY8zH zPiL|xs+JS;-n@Nl=~gbFzx68S#+A{c!Na(X>>`IegfzS<$yiiHF7A72ZGfilPlO@( z_l!kZbS zrTB*X!wxTET^~5~-mfg=o~a!!{ONJQ{zJybl5U|*z=;3)0=Q8`Jo7`;Bvo~w;qFeB z7c)tg!)?Dj`Q~j(eSJMDc=8&c(VHae~Zz3+7?Or;s)aV*)#Z zGUP&vUXQGr+kuZd|hcfJxWqGHYL)RT@eF5Pr^!(2fBfCQ~8_g3F*<^Oa;>5?apmFNz z+};ARl4xAM>6Wb$I&i*5$SE+eR8-mFgP2RSePw0k>)sGjljY60T_6MurE1|=PwQWH zm(iLCpf$#4X~fjE!P$8a7nAn%)P@mIvUt0`C6zyMyW)1(7X7>un|NaQ9Yl!IBUoXK zP6dI>koj5XaO5&oSuGne9urGwj>GE7B>Xs}E<) zo5dI`1jI{lf)LCGF`sKp$#|(=6z#eBi7~J4Fumw+zK3|x;U6wr$;Dd{N0bmv?KR?t zCG>!22tD+O&+IDe$V{V;PPk3I7>aPTCrc|OlK#wnYXP|rG$QyoATlp8W`F8RvbUG7 z{*v7`A{&}1@h6_ZUg*(uNd{bEkbz|`T4Bj9{Y8U5z`b$b4l>V_cO>iH5n{HD)BWzpo%d zXO{=@a9+8AJ~j{2%e+6oCZiVmtsT_dZ({j<{}-JaNL*!q=rsd_v6`?h|5|_64Mgj& z9`9<0+URS#8WqM>E2qpW@84V-E0EE_5lXcgp0#Z%X#{$z3j}AE-n(*WKj2P;=`%)j z2ZLNYcJrsmuzb19UGHRtg&~L7oBBN&IzK5aW09@^;(RK9s2xP2qs+lblVtd9(v2%o zE6l!JXGg+>`o5E(95mdV4GBrXLVuIb;m}Dg`i*kNI|Kxlg2z+nYhT{gFKs~rF~2U4 zH+!Xa?oNLgWl(nM`mL&aik%`y+!j?_R?}Nq1w@0nWn8nl*L~IyMRmL`rl8U_H~O=p zXqWzoD6naM+%rA&Ja(XSI6$2x&+cEWD|755Xkum0pZ@)3o>SL>VA$b4Z#e<%iIh{C_qTRq`E}qve%f7fi~{8ZIfk%8BBKBw6-T=tumf^&w`VbH(cZL!X;vi~N#bf~MLhCwp>Fd9-zFpJ#z&LF&bJ!=DpLVOkNK zEpX68jSUg9co*}_lK&S!3ny1QythJU?;JX=_HCLWrjmU|#lj*M8aP&W-QB&wx<0~8 zGsLW-;*fx{*RRKR!^Lw)+nRg4 z2VWm`cfdv9_KRG2efg)qYQCiZZY&g}BF(89&1>@6U*#?U#AOWK zhAE~iNhFjzTfMFtj*lFNXR?v9?2x|*>_-1oSa2)G_OM2`#QTXJ7UGcB}fF*Navg!dp&5`wiCgH)GWRJHu9#@1!q#jV7 zSv$*q(MyYe8HMc0SHVl9KXv^UJg?SX1ZzIQC^OjheBOEq&O0%VhkpFo&x?-rIq|pP zOnbs*2^(2IO4VFc{&l-p=un}HO&7PP!5{vgUVzQvLh`F=nSjokG4q19j&orD;fr;e zA!GCJ_GYR3pcF;p4lj;gvAsO|qZo1zsB0?T8wV`UFA@ZU#=& z+)P(7IvzI8gxA(btu3V&P8`TVYt}IrC(*z6%)AkK8hRuS`HFgOJB^Z|gO)p;1wVK_&9)5boC- zUaC(E3SwK;_Ep1C>4m1Irf((mr;Jr_n@{!h^ujkL>|+_s8F5V1de#N&BIF+ffydvr z)|TA2or9~#hMKK}3oe1GMQJrY?K&T?HhIJrNc}D77cah457{GpyRJJS2RqD*htzmc zH*Sza&u-T97KQ_u*0H(-+r7pYKLAplz@v?&JFXn6OyW{u=~Rg^wreO{%P*z{wXVs18h)0KDa}K@KgLyMO=w+T?T4)@Cc@>=@zd zDubaO)mw1Z`-fs@xcejVNbJ&p%EgNma6YiG zkGtRA5a1qNaym(0$p`gahZkJ-GL|6XdwFOV=-KAGkrSF9)n+HJW~oA+c=u*~(`2(KYyO*mQ{ImWzAyD%=33P4!%lq6@M+@Gq@8>Sf2Whh`?uzw_{;PrAllHUbe#O0>>=3iN_R=7)s