From 693e27b906754d02eb76e98026db70606cb03e91 Mon Sep 17 00:00:00 2001 From: Elio Bischof Date: Thu, 25 Jul 2024 08:39:10 +0200 Subject: [PATCH 01/39] fix: remove default TOS and privacy links (#8122) # Which Problems Are Solved The default terms of service and privacy policy links are applied to all new ZITADEL instances, also for self hosters. However, the links contents don't apply to self-hosters. # How the Problems Are Solved The links are removed from the DefaultInstance section in the *defaults.yaml* file. By default, the links are not shown anymore in the hosted login pages. They can still be configured using the privacy policy. # Additional Context - Found because of a support request --- cmd/defaults.yaml | 4 +- .../settings/external-links-settings.cy.ts | 166 +++++++++--------- .../support/api/external-links-settings.ts | 7 +- ...ions_integration_allowed_languages_test.go | 4 +- 4 files changed, 87 insertions(+), 94 deletions(-) diff --git a/cmd/defaults.yaml b/cmd/defaults.yaml index e75f045b8e..786a829381 100644 --- a/cmd/defaults.yaml +++ b/cmd/defaults.yaml @@ -712,8 +712,8 @@ DefaultInstance: SecondFactorCheckLifetime: 18h # ZITADEL_DEFAULTINSTANCE_LOGINPOLICY_SECONDFACTORCHECKLIFETIME MultiFactorCheckLifetime: 12h # ZITADEL_DEFAULTINSTANCE_LOGINPOLICY_MULTIFACTORCHECKLIFETIME PrivacyPolicy: - TOSLink: https://zitadel.com/docs/legal/terms-of-service # ZITADEL_DEFAULTINSTANCE_PRIVACYPOLICY_TOSLINK - PrivacyLink: https://zitadel.com/docs/legal/privacy-policy # ZITADEL_DEFAULTINSTANCE_PRIVACYPOLICY_PRIVACYLINK + TOSLink: "" # ZITADEL_DEFAULTINSTANCE_PRIVACYPOLICY_TOSLINK + PrivacyLink: "" # ZITADEL_DEFAULTINSTANCE_PRIVACYPOLICY_PRIVACYLINK HelpLink: "" # ZITADEL_DEFAULTINSTANCE_PRIVACYPOLICY_HELPLINK SupportEmail: "" # ZITADEL_DEFAULTINSTANCE_PRIVACYPOLICY_SUPPORTEMAIL DocsLink: https://zitadel.com/docs # ZITADEL_DEFAULTINSTANCE_PRIVACYPOLICY_DOCSLINK diff --git a/e2e/cypress/e2e/settings/external-links-settings.cy.ts b/e2e/cypress/e2e/settings/external-links-settings.cy.ts index 9d8d9fcfff..ee23a2ad65 100644 --- a/e2e/cypress/e2e/settings/external-links-settings.cy.ts +++ b/e2e/cypress/e2e/settings/external-links-settings.cy.ts @@ -1,104 +1,98 @@ import { ensureExternalLinksSettingsSet } from 'support/api/external-links-settings'; import { apiAuth } from '../../support/api/apiauth'; -describe('instance external link settings', () => { - const externalLinkSettingsPath = `/instance?id=privacypolicy`; - - const tosLink = 'https://zitadel.com/docs/legal/terms-of-service'; - const privacyPolicyLink = 'https://zitadel.com/docs/legal/privacy-policy'; +describe('external link settings', () => { + const tosLink = ''; + const privacyPolicyLink = ''; const helpLink = ''; const supportEmail = ''; const customLink = ''; const customLinkText = ''; const docsLink = 'https://zitadel.com/docs'; - beforeEach(`ensure they are set`, () => { + beforeEach(`reset`, () => { apiAuth().then((apiCallProperties) => { ensureExternalLinksSettingsSet(apiCallProperties, tosLink, privacyPolicyLink, docsLink); - cy.visit(externalLinkSettingsPath); }); }); - it(`should have default settings`, () => { - cy.get('[formcontrolname="tosLink"]').should('value', tosLink); - cy.get('[formcontrolname="privacyLink"]').should('value', privacyPolicyLink); - cy.get('[formcontrolname="helpLink"]').should('value', helpLink); - cy.get('[formcontrolname="supportEmail"]').should('value', supportEmail); - cy.get('[formcontrolname="customLink"]').should('value', customLink); - cy.get('[formcontrolname="customLinkText"]').should('value', customLinkText); - cy.get('[formcontrolname="docsLink"]').should('value', docsLink); + describe('instance', () => { + + beforeEach(`visit`, () => { + cy.visit(`/instance?id=privacypolicy`); + }); + + it(`should have default settings`, () => { + cy.get('[formcontrolname="tosLink"]').should('value', tosLink); + cy.get('[formcontrolname="privacyLink"]').should('value', privacyPolicyLink); + cy.get('[formcontrolname="helpLink"]').should('value', helpLink); + cy.get('[formcontrolname="supportEmail"]').should('value', supportEmail); + cy.get('[formcontrolname="customLink"]').should('value', customLink); + cy.get('[formcontrolname="customLinkText"]').should('value', customLinkText); + cy.get('[formcontrolname="docsLink"]').should('value', docsLink); + }); + + it(`should update external links`, () => { + cy.get('[formcontrolname="tosLink"]').clear().type('tosLink2'); + cy.get('[formcontrolname="privacyLink"]').clear().type('privacyLink2'); + cy.get('[formcontrolname="helpLink"]').clear().type('helpLink'); + cy.get('[formcontrolname="supportEmail"]').clear().type('support@example.com'); + cy.get('[formcontrolname="customLink"]').clear().type('customLink'); + cy.get('[formcontrolname="customLinkText"]').clear().type('customLinkText'); + cy.get('[formcontrolname="docsLink"]').clear().type('docsLink'); + cy.get('[data-e2e="save-button"]').click(); + cy.shouldConfirmSuccess(); + }); + + it(`should return to default values`, () => { + cy.get('[formcontrolname="tosLink"]').should('value', tosLink); + cy.get('[formcontrolname="privacyLink"]').should('value', privacyPolicyLink); + cy.get('[formcontrolname="helpLink"]').should('value', helpLink); + cy.get('[formcontrolname="supportEmail"]').should('value', supportEmail); + cy.get('[formcontrolname="customLink"]').should('value', customLink); + cy.get('[formcontrolname="customLinkText"]').should('value', customLinkText); + cy.get('[formcontrolname="docsLink"]').should('value', docsLink); + }); }); - it(`should update external links`, () => { - cy.get('[formcontrolname="tosLink"]').clear().type('tosLink2'); - cy.get('[formcontrolname="privacyLink"]').clear().type('privacyLink2'); - cy.get('[formcontrolname="helpLink"]').clear().type('helpLink'); - cy.get('[formcontrolname="supportEmail"]').clear().type('support@example.com'); - cy.get('[formcontrolname="customLink"]').clear().type('customLink'); - cy.get('[formcontrolname="customLinkText"]').clear().type('customLinkText'); - cy.get('[formcontrolname="docsLink"]').clear().type('docsLink'); - cy.get('[data-e2e="save-button"]').click(); - cy.shouldConfirmSuccess(); + describe('org', () => { + beforeEach(`visit`, () => { + cy.visit(`/org-settings?id=privacypolicy`); + }); + + it(`should have default settings`, () => { + cy.get('[formcontrolname="tosLink"]').should('value', tosLink); + cy.get('[formcontrolname="privacyLink"]').should('value', privacyPolicyLink); + cy.get('[formcontrolname="helpLink"]').should('value', helpLink); + cy.get('[formcontrolname="supportEmail"]').should('value', supportEmail); + cy.get('[formcontrolname="customLink"]').should('value', customLink); + cy.get('[formcontrolname="customLinkText"]').should('value', customLinkText); + cy.get('[formcontrolname="docsLink"]').should('value', docsLink); + }); + + it(`should update external links`, () => { + cy.get('[formcontrolname="tosLink"]').clear().type('tosLink2'); + cy.get('[formcontrolname="privacyLink"]').clear().type('privacyLink2'); + cy.get('[formcontrolname="helpLink"]').clear().type('helpLink'); + cy.get('[formcontrolname="supportEmail"]').clear().type('support@example.com'); + cy.get('[formcontrolname="customLink"]').clear().type('customLink'); + cy.get('[formcontrolname="customLinkText"]').clear().type('customLinkText'); + cy.get('[formcontrolname="docsLink"]').clear().type('docsLink'); + cy.get('[data-e2e="save-button"]').click(); + cy.shouldConfirmSuccess(); + }); + + it(`should return to default values`, () => { + cy.get('[data-e2e="reset-button"]').click(); + cy.get('[data-e2e="confirm-dialog-button"]').click(); + cy.get('[formcontrolname="tosLink"]').should('value', tosLink); + cy.get('[formcontrolname="privacyLink"]').should('value', privacyPolicyLink); + cy.get('[formcontrolname="helpLink"]').should('value', helpLink); + cy.get('[formcontrolname="supportEmail"]').should('value', supportEmail); + cy.get('[formcontrolname="customLink"]').should('value', customLink); + cy.get('[formcontrolname="customLinkText"]').should('value', customLinkText); + cy.get('[formcontrolname="docsLink"]').should('value', docsLink); + }); }); +}) - it(`should return to default values`, () => { - cy.get('[formcontrolname="tosLink"]').should('value', tosLink); - cy.get('[formcontrolname="privacyLink"]').should('value', privacyPolicyLink); - cy.get('[formcontrolname="helpLink"]').should('value', helpLink); - cy.get('[formcontrolname="supportEmail"]').should('value', supportEmail); - cy.get('[formcontrolname="customLink"]').should('value', customLink); - cy.get('[formcontrolname="customLinkText"]').should('value', customLinkText); - cy.get('[formcontrolname="docsLink"]').should('value', docsLink); - }); -}); - -describe('instance external link settings', () => { - const externalLinkSettingsPath = `/org-settings?id=privacypolicy`; - - const tosLink = 'https://zitadel.com/docs/legal/terms-of-service'; - const privacyPolicyLink = 'https://zitadel.com/docs/legal/privacy-policy'; - const helpLink = ''; - const supportEmail = ''; - const customLink = ''; - const customLinkText = ''; - const docsLink = 'https://zitadel.com/docs'; - - beforeEach(() => { - cy.context().as('ctx'); - cy.visit(externalLinkSettingsPath); - }); - - it(`should have default settings`, () => { - cy.get('[formcontrolname="tosLink"]').should('value', tosLink); - cy.get('[formcontrolname="privacyLink"]').should('value', privacyPolicyLink); - cy.get('[formcontrolname="helpLink"]').should('value', helpLink); - cy.get('[formcontrolname="supportEmail"]').should('value', supportEmail); - cy.get('[formcontrolname="customLink"]').should('value', customLink); - cy.get('[formcontrolname="customLinkText"]').should('value', customLinkText); - cy.get('[formcontrolname="docsLink"]').should('value', docsLink); - }); - - it(`should update external links`, () => { - cy.get('[formcontrolname="tosLink"]').clear().type('tosLink2'); - cy.get('[formcontrolname="privacyLink"]').clear().type('privacyLink2'); - cy.get('[formcontrolname="helpLink"]').clear().type('helpLink'); - cy.get('[formcontrolname="supportEmail"]').clear().type('support@example.com'); - cy.get('[formcontrolname="customLink"]').clear().type('customLink'); - cy.get('[formcontrolname="customLinkText"]').clear().type('customLinkText'); - cy.get('[formcontrolname="docsLink"]').clear().type('docsLink'); - cy.get('[data-e2e="save-button"]').click(); - cy.shouldConfirmSuccess(); - }); - - it(`should return to default values`, () => { - cy.get('[data-e2e="reset-button"]').click(); - cy.get('[data-e2e="confirm-dialog-button"]').click(); - cy.get('[formcontrolname="tosLink"]').should('value', tosLink); - cy.get('[formcontrolname="privacyLink"]').should('value', privacyPolicyLink); - cy.get('[formcontrolname="helpLink"]').should('value', helpLink); - cy.get('[formcontrolname="supportEmail"]').should('value', supportEmail); - cy.get('[formcontrolname="customLink"]').should('value', customLink); - cy.get('[formcontrolname="customLinkText"]').should('value', customLinkText); - cy.get('[formcontrolname="docsLink"]').should('value', docsLink); - }); -}); diff --git a/e2e/cypress/support/api/external-links-settings.ts b/e2e/cypress/support/api/external-links-settings.ts index eb4e30a3a2..5426f1233b 100644 --- a/e2e/cypress/support/api/external-links-settings.ts +++ b/e2e/cypress/support/api/external-links-settings.ts @@ -11,12 +11,11 @@ export function ensureExternalLinksSettingsSet(api: API, tosLink: string, privac id: body.policy.id, entity: null, }; - if ( body.policy && - body.policy.tosLink === tosLink && - body.policy.privacyLink === privacyPolicyLink && - body.policy.docsLink === docsLink + (body.policy.tosLink || '') === tosLink && + (body.policy.privacyLink || '') === privacyPolicyLink && + (body.policy.docsLink || '') === docsLink ) { return { ...result, entity: body.policy }; } diff --git a/internal/api/grpc/admin/restrictions_integration_allowed_languages_test.go b/internal/api/grpc/admin/restrictions_integration_allowed_languages_test.go index 3e00978676..8a2f415a8b 100644 --- a/internal/api/grpc/admin/restrictions_integration_allowed_languages_test.go +++ b/internal/api/grpc/admin/restrictions_integration_allowed_languages_test.go @@ -90,7 +90,7 @@ func TestServer_Restrictions_AllowedLanguages(t *testing.T) { awaitDiscoveryEndpoint(tt, domain, []string{defaultAndAllowedLanguage.String()}, []string{disallowedLanguage.String()}) }) t.Run("the login ui is rendered in the default language", func(tt *testing.T) { - awaitLoginUILanguage(tt, domain, disallowedLanguage, defaultAndAllowedLanguage, "Allgemeine Geschäftsbedingungen und Datenschutz") + awaitLoginUILanguage(tt, domain, disallowedLanguage, defaultAndAllowedLanguage, "Passwort") }) t.Run("preferred languages are not restricted by the supported languages", func(tt *testing.T) { tt.Run("change user profile", func(ttt *testing.T) { @@ -151,7 +151,7 @@ func TestServer_Restrictions_AllowedLanguages(t *testing.T) { awaitDiscoveryEndpoint(ttt, domain, []string{disallowedLanguage.String()}, nil) }) tt.Run("the login ui is rendered in the previously disallowed language", func(ttt *testing.T) { - awaitLoginUILanguage(ttt, domain, disallowedLanguage, disallowedLanguage, "Términos y condiciones") + awaitLoginUILanguage(ttt, domain, disallowedLanguage, disallowedLanguage, "Contraseña") }) }) } From 3b59b5cb1afa825c5a8a8605b468f7e7c640859e Mon Sep 17 00:00:00 2001 From: Livio Spring Date: Thu, 25 Jul 2024 09:38:36 +0200 Subject: [PATCH 02/39] fix(login): correctly render logo based on theme mode (#8355) # Which Problems Are Solved The initial load of the login UI with dark mode preference (prefers-color-scheme: dark) first rendered the logo configured for light mode. Also switching from dark to light or vice versa would result in the same behavior. This was due to a mixed logic of server (based on cookie) and client (prefers-color-scheme and cookie) deciding which mode to render. # How the Problems Are Solved - Since the main logic of which mode to use (`prefers-color-scheme`) can only be achieve client side, both logos will be served in the HTML and either will be rendered based on CSS. # Additional Changes None # Additional Context - closes #2085 --- .../themes/scss/styles/header/header.scss | 7 +++++++ .../api/ui/login/static/templates/header.html | 18 +++++++++--------- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/internal/api/ui/login/static/resources/themes/scss/styles/header/header.scss b/internal/api/ui/login/static/resources/themes/scss/styles/header/header.scss index 58b345c43a..ad7901edd8 100644 --- a/internal/api/ui/login/static/resources/themes/scss/styles/header/header.scss +++ b/internal/api/ui/login/static/resources/themes/scss/styles/header/header.scss @@ -18,5 +18,12 @@ $lgn-header-margin: auto; max-width: 250px; max-height: 150px; object-fit: contain; + + .lgn-dark-theme &.lgn-logo-light { + display: none; + } + .lgn-light-theme &.lgn-logo-dark { + display: none; + } } } diff --git a/internal/api/ui/login/static/templates/header.html b/internal/api/ui/login/static/templates/header.html index 34d508fb5e..bad87a39e9 100644 --- a/internal/api/ui/login/static/templates/header.html +++ b/internal/api/ui/login/static/templates/header.html @@ -1,17 +1,17 @@ {{define "header"}}
{{ if hasCustomPolicy .LabelPolicy }} - {{ $logo := customLogoResource .PrivateLabelingOrgID .LabelPolicy .DarkMode }} - {{if $logo}} - + {{ $logoDark := customLogoResource .PrivateLabelingOrgID .LabelPolicy true }} + {{ $logoLight := customLogoResource .PrivateLabelingOrgID .LabelPolicy false }} + {{if $logoDark}} + + {{end}} + {{if $logoLight}} + {{end}} {{ else }} - - {{if .DarkMode }} - - {{else}} - - {{end}} + + {{end}}
{{end}} From 57428a1281b23d309ea3edafe6dc3baeb9f60563 Mon Sep 17 00:00:00 2001 From: RedstonePfalz <55254344+RedstonePfalz@users.noreply.github.com> Date: Fri, 26 Jul 2024 11:33:01 +0200 Subject: [PATCH 03/39] fix: Fixed more spelling and grammar misstakes (#8359) # Which Problems Are Solved I fixed more spelling and grammar misstakes in the German language files. # Additional Context - Follow-up for PR #8240 Co-authored-by: Fabi --- console/src/assets/i18n/de.json | 330 +++++++++++----------- internal/notification/static/i18n/de.yaml | 12 +- 2 files changed, 171 insertions(+), 171 deletions(-) diff --git a/console/src/assets/i18n/de.json b/console/src/assets/i18n/de.json index 9d79b74df4..f17835beba 100644 --- a/console/src/assets/i18n/de.json +++ b/console/src/assets/i18n/de.json @@ -3,13 +3,13 @@ "DESCRIPTIONS": { "METADATA_TITLE": "Metadaten", "HOME": { - "TITLE": "Leg los mit ZITADEL", + "TITLE": "Mit ZITADEL loslegen", "NEXT": { - "TITLE": "Deine nächsten Schritte", - "DESCRIPTION": "Führe die folgenden Schritte durch, um deine Anwendung zu sichern.", + "TITLE": "Die nächsten Schritte", + "DESCRIPTION": "Führe die folgenden Schritte aus, um deine Anwendung zu sichern.", "CREATE_PROJECT": { "TITLE": "Erstelle ein Projekt", - "DESCRIPTION": "Füge ein Projekt hinzu und definiere seine Rollen und Berechtigungen." + "DESCRIPTION": "Füge ein Projekt hinzu und definiere Rollen und Berechtigungen." } }, "MORE_SHORTCUTS": { @@ -29,12 +29,12 @@ }, "ORG": { "TITLE": "Organisation", - "DESCRIPTION": "Eine Organisation beherbergt Benutzer, Projekte mit Apps, Identitätsanbieter und Einstellungen wie Unternehmensbranding. Möchtest du Einstellungen über mehrere Organisationen hinweg teilen? Konfiguriere Standardeinstellungen.", - "METADATA": "Füge der Organisation benutzerdefinierte Attribute hinzu, wie ihren Standort oder einen Identifikator in einem anderen System. Du kannst diese Informationen in deinen Aktionen nutzen." + "DESCRIPTION": "Eine Organisation enthält Benutzer, Projekte mit Apps, Identitätsanbieter und Einstellungen wie Unternehmensbranding. Möchtest du Einstellungen über mehrere Organisationen hinweg teilen? Nutze hierfür die Standardeinstellungen.", + "METADATA": "Füge der Organisation benutzerdefinierte Attribute hinzu, wie ihren Standort oder einen Identifikator in einem anderen System. Du kannst diese Informationen in den Aktionen nutzen." }, "PROJECTS": { "TITLE": "Projekte", - "DESCRIPTION": "Ein Projekt beherbergt eine oder mehrere Anwendungen, die du nutzen kannst, um deine Benutzer zu authentifizieren. Außerdem kannst du deine Benutzer mit Projekten autorisieren. Um Benutzern aus anderen Organisationen das Einloggen in deine Anwendungen zu erlauben, gewähre ihnen Zugriff auf dein Projekt.

Wenn du ein Projekt nicht finden kannst, kontaktiere den Projekteigentümer oder jemanden mit den entsprechenden Rechten, um Zugang zu erhalten.", + "DESCRIPTION": "Ein Projekt enthält eine oder mehrere Anwendungen, die du nutzen kannst, um deine Benutzer zu authentifizieren. Außerdem kannst du deine Benutzer mit Projekten autorisieren. Um Benutzern aus anderen Organisationen das Einloggen in deine Anwendungen zu erlauben, gewähre ihnen Zugriff auf dein Projekt.

Wenn du ein Projekt nicht finden kannst, kontaktiere den Projekteigentümer oder jemanden mit den entsprechenden Rechten, um Zugang zu erhalten.", "OWNED": { "TITLE": "Eigene Projekte", "DESCRIPTION": "Dies sind die Projekte, die du besitzt. Du kannst die Einstellungen, Berechtigungen und Anwendungen dieser Projekte verwalten." @@ -45,17 +45,17 @@ } }, "USERS": { - "TITLE": "Users", - "DESCRIPTION": "Ein Benutzer ist ein Mensch oder eine Maschine, die auf deine Anwendungen zugreifen kann.", + "TITLE": "Benutzer", + "DESCRIPTION": "Ein Benutzer ist ein Mensch oder ein Computer, der auf deine Anwendungen zugreifen kann.", "HUMANS": { - "TITLE": "Users", - "DESCRIPTION": "User authentifizieren sich interaktiv in einer Browsersitzung mit einer Anmeldeaufforderung.", - "METADATA": "Füge dem Benutzer benutzerdefinierte Attribute hinzu, wie die Abteilung. Du kannst diese Informationen in deinen Aktionen nutzen." + "TITLE": "Benutzer", + "DESCRIPTION": "Benutzer authentifizieren sich interaktiv in einer Browsersitzung mit einem Login-Formular.", + "METADATA": "Füge dem Benutzer benutzerdefinierte Attribute hinzu, wie die Abteilung. Du kannst diese Informationen in den Aktionen nutzen." }, "MACHINES": { - "TITLE": "Service users", - "DESCRIPTION": "Maschinen authentifizieren sich nicht-interaktiv mit einem JWT Bearer-Token, das mit einem privaten Schlüssel signiert ist. Sie können auch ein persönliches Zugangstoken verwenden.", - "METADATA": "Füge dem Benutzer benutzerdefinierte Attribute hinzu, wie das authentifizierende System. Du kannst diese Informationen in deinen Aktionen nutzen." + "TITLE": "Service-Benutzer", + "DESCRIPTION": "Service-Benutzer authentifizieren sich nicht-interaktiv mit einem JWT Bearer-Token, welches mit einem privaten Schlüssel signiert ist. Sie können auch ein persönliches Zugangstoken verwenden.", + "METADATA": "Füge dem Benutzer benutzerdefinierte Attribute hinzu, wie das authentifizierende System. Du kannst diese Informationen in den Aktionen nutzen." }, "SELF": { "METADATA": "Füge deinem Benutzer benutzerdefinierte Attribute hinzu, wie deine Abteilung. Du kannst diese Informationen in den Aktionen deiner Organisation nutzen." @@ -63,36 +63,36 @@ }, "AUTHORIZATIONS": { "TITLE": "Berechtigungen", - "DESCRIPTION": "Berechtigungen definieren die Zugriffsrechte eines Benutzers auf ein Projekt. Du kannst einem Benutzer Zugriff auf ein Projekt gewähren und die Rollen des Benutzers innerhalb dieses Projekts definieren." + "DESCRIPTION": "Berechtigungen definieren die Zugriffsrechte eines Benutzers auf ein Projekt. Du kannst einem Benutzer Zugriff auf ein Projekt gewähren und die Rollen des Benutzers innerhalb dieses Projekts einstellen." }, "ACTIONS": { "TITLE": "Aktionen", - "DESCRIPTION": "Führe benutzerdefinierten Code bei Ereignissen aus, die auftreten, während sich deine Benutzer bei ZITADEL authentifizieren. Automatisiere deine Prozesse, bereichere die Metadaten deiner Benutzer und deren Token oder benachrichtige externe Systeme.", + "DESCRIPTION": "Führe eigenen Code bei Ereignissen aus, die auftreten, während sich deine Benutzer bei ZITADEL authentifizieren. Automatisiere deine Prozesse, erweitere die Metadaten deiner Benutzer und deren Token oder benachrichtige externe Systeme.", "SCRIPTS": { "TITLE": "Skripte", - "DESCRIPTION": "Schreibe deinen JavaScript-Code einmal und löse ihn in mehreren Flows aus." + "DESCRIPTION": "Schreibe einmalig deinen JavaScript-Code und löse ihn in mehreren Flows aus." }, "FLOWS": { "TITLE": "Flows", - "DESCRIPTION": "Wähle einen Authentifizierungsflow und löse deine Aktion bei einem spezifischen Ereignis innerhalb dieses Flows aus." + "DESCRIPTION": "Wähle einen Authentifizierungsflow und löse deine Aktionen bei einem spezifischen Ereignis innerhalb dieses Flows aus." } }, "SETTINGS": { "INSTANCE": { "TITLE": "Standardeinstellungen", - "DESCRIPTION": "Standardeinstellungen für alle Organisationen. Mit den richtigen Berechtigungen können einige davon in den Organisationseinstellungen überschrieben werden." + "DESCRIPTION": "Standardeinstellungen gelten für alle Organisationen. Mit den richtigen Berechtigungen können einige davon in den Organisationseinstellungen überschrieben werden." }, "ORG": { "TITLE": "Organisationseinstellungen", - "DESCRIPTION": "Passe die Einstellungen deiner Organisation an." + "DESCRIPTION": "Passe die Einstellungen einer Organisation an." }, "FEATURES": { "TITLE": "Funktionseinstellungen", - "DESCRIPTION": "Schalten Sie Funktionen für Ihre Instanz frei." + "DESCRIPTION": "Schalte Funktionen für deine Instanz frei." }, "IDPS": { "TITLE": "Identitätsanbieter", - "DESCRIPTION": "Erstelle und aktiviere externe Identitätsanbieter. Wähle einen bekannten Anbieter aus oder konfiguriere einen anderen OIDC-, OAuth- oder SAML-kompatiblen Anbieter deiner Wahl. Du kannst sogar deine vorhandenen JWT-Tokens als föderierte Identitäten verwenden, indem du einen JWT-Identitätsanbieter konfigurierst.", + "DESCRIPTION": "Erstelle und aktiviere externe Identitätsanbieter. Wähle einen bekannten Anbieter aus oder konfiguriere einen anderen OIDC-, OAuth- oder SAML-kompatiblen Anbieter deiner Wahl. Du kannst sogar deine vorhandenen JWT-Tokens als föderierte Identitäten verwenden, in dem du einen JWT-Identitätsanbieter konfigurierst.", "NEXT": "Was nun?", "SAML": { "TITLE": "Konfiguriere deinen SAML-Identitätsanbieter", @@ -100,7 +100,7 @@ }, "CALLBACK": { "TITLE": "Konfiguriere deinen {{ provider }}-Identitätsanbieter", - "DESCRIPTION": "Bevor du ZITADEL konfigurieren kannst, gib diese URL an deinen Identitätsanbieter weiter, um die Browserumleitung zurück zu ZITADEL nach der Authentifizierung zu ermöglichen." + "DESCRIPTION": "Bevor du ZITADEL konfigurieren kannst, gib diese URL an deinen Identitätsanbieter weiter, um die Weiterleitung zurück zu ZITADEL nach der Authentifizierung zu ermöglichen." }, "JWT": { "TITLE": "Verwende JWTs als föderierte Identitäten", @@ -112,11 +112,11 @@ }, "AUTOFILL": { "TITLE": "Benutzerdaten automatisch ausfüllen", - "DESCRIPTION": "Verwende eine Aktion, um das Benutzererlebnis zu verbessern. Du kannst das Registrierungsformular von ZITADEL mit Werten vom Identitätsanbieter vorab ausfüllen." + "DESCRIPTION": "Verwende eine Aktion, um das Benutzererlebnis zu verbessern. Du kannst das Registrierungsformular von ZITADEL mit Daten des Identitätsanbieter vorab ausfüllen." }, "ACTIVATE": { "TITLE": "Aktiviere den IdP", - "DESCRIPTION": "Dein IdP ist noch nicht aktiv. Aktiviere ihn, um deinen Benutzern das Einloggen zu ermöglichen." + "DESCRIPTION": "Dein IdP ist noch nicht aktiv. Aktiviere ihn, um Benutzern das Einloggen zu ermöglichen." } }, "PW_COMPLEXITY": { @@ -129,7 +129,7 @@ }, "PRIVACY_POLICY": { "TITLE": "Externe Links", - "DESCRIPTION": "Leiten Sie Ihre Benutzer zu benutzerdefinierten externen Ressourcen, die auf der Anmeldeseite angezeigt werden. Benutzer müssen die Nutzungsbedingungen und Datenschutzrichtlinien akzeptieren, bevor sie sich anmelden können. Ändern Sie den Link zu Ihrer Dokumentation oder legen Sie eine leere Zeichenfolge fest, um die Dokumentationsschaltfläche in der Konsole auszublenden. Fügen Sie in der Konsole einen benutzerdefinierten externen Link und einen benutzerdefinierten Text für diesen Link hinzu oder setzen Sie sie leer, um diese Schaltfläche auszublenden." + "DESCRIPTION": "Leite die Benutzer zu benutzerdefinierten externen Ressourcen, die auf der Anmeldeseite angezeigt werden. Benutzer müssen die Nutzungsbedingungen und Datenschutzrichtlinien akzeptieren, bevor sie sich anmelden können. Ändere den Link zur Dokumentation oder lege eine leere Zeichenfolge fest, um den Dokumentations-Button in der Konsole auszublenden. Füge in der Konsole einen benutzerdefinierten externen Link und einen benutzerdefinierten Text für diesen Link hinzu oder lasse ihn leer, um diesen Button auszublenden." }, "SMTP_PROVIDER": { "TITLE": "SMTP-Einstellungen", @@ -137,7 +137,7 @@ }, "SMS_PROVIDER": { "TITLE": "SMS-Einstellungen", - "DESCRIPTION": "Um alle ZITADEL-Funktionen freizuschalten, konfiguriere Twilio, um SMS-Nachrichten an deine Benutzer zu senden." + "DESCRIPTION": "Um alle ZITADEL-Funktionen zu nutzen, konfiguriere Twilio, um SMS-Nachrichten an deine Benutzer zu senden." }, "IAM_EVENTS": { "TITLE": "Ereignisse", @@ -145,32 +145,32 @@ }, "IAM_FAILED_EVENTS": { "TITLE": "Fehlgeschlagene Ereignisse", - "DESCRIPTION": "Diese Seite zeigt alle fehlgeschlagenen Ereignisse in deiner Instanz. Wenn ZITADEL sich nicht wie erwartet verhält, überprüfe immer zuerst diese Liste." + "DESCRIPTION": "Diese Seite zeigt alle fehlerhaften Ereignisse deiner Instanz. Wenn ZITADEL sich nicht wie erwartet verhält, überprüfe immer zuerst diese Liste." }, "IAM_VIEWS": { - "TITLE": "Ansichten", - "DESCRIPTION": "Diese Seite zeigt alle deine Datenbankansichten und wann sie ihr letztes Ereignis verarbeitet haben. Wenn dir einige Daten fehlen, überprüfe, ob die Ansicht aktuell ist." + "TITLE": "Datenbankansichten", + "DESCRIPTION": "Diese Seite zeigt alle deine Datenbankansichten und wann sie ihr letztes Ereignis verarbeitet haben. Wenn einige Daten fehlen, überprüfe, ob die Ansicht aktuell ist." }, "LANGUAGES": { "TITLE": "Sprachen", - "DESCRIPTION": "Beschränke die Sprachen, in die das Anmeldeformular und die Benachrichtigungsnachrichten übersetzt werden. Wenn du einige der Sprachen deaktivieren möchtest, ziehe sie in den Abschnitt Nicht erlaubte Sprachen. Du kannst eine erlaubte Sprache als Standardsprache festlegen. Wenn die bevorzugte Sprache eines Benutzers nicht erlaubt ist, wird die Standardsprache verwendet." + "DESCRIPTION": "Beschränke die Sprachen, in die das Anmeldeformular und die Benachrichtigungsnachrichten übersetzt werden. Wenn du einige der Sprachen deaktivieren möchtest, ziehe sie in den Abschnitt \"Nicht erlaubte Sprachen\". Du kannst eine erlaubte Sprache als Standardsprache festlegen. Wenn die bevorzugte Sprache eines Benutzers nicht erlaubt ist, wird die Standardsprache verwendet." }, "SECRET_GENERATORS": { - "TITLE": "Secret Generators", - "DESCRIPTION": "Definiere die Komplexität und Lebensdauer deiner Geheimnisse. Eine höhere Komplexität und Lebensdauer verbessert die Sicherheit, eine niedrigere Komplexität und Lebensdauer verbessert die Entschlüsselungsleistung." + "TITLE": "Secret-Generator", + "DESCRIPTION": "Definiere die Komplexität und Lebensdauer deiner Secrets. Eine höhere Komplexität und Lebensdauer verbessert die Sicherheit, eine niedrigere Komplexität und Lebensdauer verschlechtert die Sicherheit." }, "SECURITY": { "TITLE": "Sicherheitseinstellungen", - "DESCRIPTION": "Aktiviere ZITADEL-Funktionen, die Sicherheitsauswirkungen haben können. Du solltest wirklich wissen, was du tust, bevor du diese Einstellungen änderst." + "DESCRIPTION": "Aktiviere ZITADEL-Funktionen, die Auswirkungen auf die Sicherheit haben können. Du solltest wirklich wissen, was du tust, bevor du diese Einstellungen änderst." }, "OIDC": { "TITLE": "OpenID Connect Einstellungen", - "DESCRIPTION": "Konfiguriere die Lebensdauer deiner OIDC-Token. Verwende kürzere Lebensdauern, um die Sicherheit deiner Benutzer zu erhöhen, verwende längere Lebensdauern, um die Bequemlichkeit deiner Benutzer zu erhöhen.", + "DESCRIPTION": "Konfiguriere die Lebensdauer deiner OIDC-Token. Verwende eine kürzere Lebensdauer, um die Sicherheit deiner Benutzer zu erhöhen, verwende längere Lebensdauern, um die Bequemlichkeit deiner Benutzer zu erhöhen.", "LABEL_HOURS": "Maximale Lebensdauer in Stunden", "LABEL_DAYS": "Maximale Lebensdauer in Tagen", "ACCESS_TOKEN": { "TITLE": "Zugangstoken", - "DESCRIPTION": "Das Zugangstoken wird verwendet, um einen Benutzer zu authentifizieren. Es ist ein kurzlebiges Token, das verwendet wird, um auf die Daten des Benutzers zuzugreifen. Verwende eine kurze Lebensdauer, um das Risiko eines unbefugten Zugriffs zu minimieren. Zugangstoken können automatisch mit einem Aktualisierungstoken erneuert werden." + "DESCRIPTION": "Das Zugangstoken wird verwendet, um einen Benutzer zu authentifizieren. Es ist ein kurzlebiges Token, das verwendet wird, um auf die Nutzerdaten zuzugreifen. Verwende eine kurze Lebensdauer, um das Risiko eines unbefugten Zugriffs zu minimieren. Zugangstoken können automatisch mit einem Aktualisierungstoken erneuert werden." }, "ID_TOKEN": { "TITLE": "ID-Token", @@ -178,7 +178,7 @@ }, "REFRESH_TOKEN": { "TITLE": "Aktualisierungstoken", - "DESCRIPTION": "Das Aktualisierungstoken wird verwendet, um ein neues Zugangstoken zu erhalten. Es ist ein langfristiges Token, das verwendet wird, um das Zugangstoken zu erneuern. Ein Benutzer muss sich manuell neu authentifizieren, wenn das Aktualisierungstoken abläuft." + "DESCRIPTION": "Das Aktualisierungstoken wird verwendet, um einen neuen Zugangstoken zu erhalten. Es ist ein langlebiges Token, welches verwendet wird, um das Zugangstoken zu erneuern. Ein Benutzer muss sich manuell neu authentifizieren, wenn das Aktualisierungstoken abläuft." }, "REFRESH_TOKEN_IDLE": { "TITLE": "Inaktives Aktualisierungstoken", @@ -202,22 +202,22 @@ }, "LOGIN_TEXTS": { "TITLE": "Texte der Anmeldeoberfläche", - "DESCRIPTION": "Passe die Texte deines Anmeldeformulars an. Wenn ein Text leer ist, zeigt der Platzhalter den Standardwert an. Wenn du einige der Sprachen deaktivieren möchtest, beschränke sie in den Spracheinstellungen deiner Instanz." + "DESCRIPTION": "Passe die Texte des Anmeldeformulars an. Wenn ein Text leer ist, zeigt der Platzhalter den Standardwert an. Wenn du einige Sprachen deaktivieren möchtest, beschränke sie in den Spracheinstellungen deiner Instanz." }, "DOMAINS": { "TITLE": "Domain-Einstellungen", - "DESCRIPTION": "Definiere Einschränkungen für deine Domains und konfiguriere deine Anmeldenamen-Muster.", + "DESCRIPTION": "Definiere Einschränkungen für deine Domains und konfiguriere ein Anmeldenamen-Muster.", "REQUIRE_VERIFICATION": { - "TITLE": "Verifizierung benutzerdefinierter Domains erforderlich", + "TITLE": "Verifizierung eigener Domains erforderlich", "DESCRIPTION": "Wenn dies aktiviert ist, müssen Organisationsdomänen verifiziert werden, bevor sie für die Domainerkennung oder die Benutzernamenergänzung verwendet werden können." }, "LOGIN_NAME_PATTERN": { "TITLE": "Muster für Anmeldenamen", - "DESCRIPTION": "Steuere das Muster der Anmeldenamen deiner Benutzer. ZITADEL wählt die Organisation deiner Benutzer aus, sobald sie ihren Anmeldenamen eingeben. Daher müssen die Anmeldenamen über alle Organisationen hinweg einzigartig sein. Wenn du Benutzer hast, die Konten in mehreren Domains haben, kannst du die Einzigartigkeit sicherstellen, indem du deine Anmeldenamen mit der Organisationsdomain ergänzt." + "DESCRIPTION": "Erstelle ein Muster für die Anmeldenamen deiner Benutzer. ZITADEL wählt die Organisation deiner Benutzer aus, sobald sie ihren Anmeldenamen eingeben. Daher müssen die Anmeldenamen über alle Organisationen hinweg einzigartig sein. Wenn du Benutzer hast, die Konten in mehreren Domains haben, kannst du die Einzigartigkeit sicherstellen, indem du deine Anmeldenamen mit der Organisationsdomain ergänzt." }, "DOMAIN_VERIFICATION": { "TITLE": "Domain-Verifizierung", - "DESCRIPTION": "Erlaube deiner Organisation nur die Verwendung der Domains, die sie tatsächlich kontrolliert. Wenn aktiviert, werden Organisationsdomänen periodisch durch DNS- oder HTTP-Challenge verifiziert, bevor sie verwendet werden können. Dies ist eine Sicherheitsfunktion, um Domain-Hijacking zu verhindern." + "DESCRIPTION": "Erlaube deiner Organisation nur die Verwendung von Domains, die sie tatsächlich kontrolliert. Wenn aktiviert, werden Organisationsdomänen periodisch durch eine DNS- oder HTTP-Challenge verifiziert, bevor sie verwendet werden können. Dies ist eine Sicherheitsfunktion, um Domain-Hijacking zu verhindern." }, "SMTP_SENDER_ADDRESS": { "TITLE": "SMTP-Absenderadresse", @@ -226,28 +226,28 @@ }, "LOGIN": { "LIFETIMES": { - "TITLE": "Login-Lebensdauern", + "TITLE": "Login-Lebensdauer", "DESCRIPTION": "Verbessere deine Sicherheit, indem du einige maximale Lebensdauern im Zusammenhang mit dem Login reduzierst.", "LABEL": "Maximale Lebensdauer in Stunden", "PW_CHECK": { "TITLE": "Passwortüberprüfung", - "DESCRIPTION": "Nach diesem Zeitraum müssen sich Benutzer erneut mit ihrem Passwort authentifizieren." + "DESCRIPTION": "Nach diesem Zeitraum müssen sich Benutzer erneut mit ihrem Passwort anmelden." }, "EXT_LOGIN_CHECK": { "TITLE": "Externe Login-Überprüfung", - "DESCRIPTION": "Deine Benutzer werden in diesen Zeiträumen zu ihren externen Identitätsanbietern umgeleitet." + "DESCRIPTION": "Deine Benutzer werden in nach diesem Zeitraum zu ihren externen Identitätsanbietern weitergeleitet." }, "MULTI_FACTOR_INIT": { "TITLE": "Multifaktor-Initialisierungsüberprüfung", - "DESCRIPTION": "Deine Benutzer werden aufgefordert, in diesen Zeiträumen einen zweiten Faktor oder einen Multifaktor einzurichten, falls sie dies noch nicht getan haben. Eine Lebensdauer von 0 deaktiviert diese Aufforderung." + "DESCRIPTION": "Deine Benutzer werden aufgefordert, in diesem Zeitraum einen zweiten Faktor oder einen Multifaktor einzurichten, falls sie dies noch nicht getan haben. Eine Lebensdauer von 0 deaktiviert diese Aufforderung." }, "SECOND_FACTOR_CHECK": { "TITLE": "Zweiter-Faktor-Überprüfung", - "DESCRIPTION": "Deine Benutzer müssen ihren zweiten Faktor in diesen Zeiträumen erneut validieren." + "DESCRIPTION": "Deine Benutzer müssen ihren zweiten Faktor nach diesem Zeitraum erneut validieren." }, "MULTI_FACTOR_CHECK": { "TITLE": "Multifaktor-Überprüfung", - "DESCRIPTION": "Deine Benutzer müssen ihren Multifaktor in diesen Zeiträumen erneut validieren." + "DESCRIPTION": "Deine Benutzer müssen ihren Multifaktor nach diesem Zeitraum erneut validieren." } }, "FORM": { @@ -267,7 +267,7 @@ }, "EXTERNAL_LOGIN_ALLOWED": { "TITLE": "Externer Login erlaubt", - "DESCRIPTION": "Erlaube deinen Benutzern, sich mit einem externen Identitätsanbieter einzuloggen, anstatt den ZITADEL-Benutzer zum Einloggen zu verwenden." + "DESCRIPTION": "Erlaube deinen Benutzern, sich mit einem externen Identitätsanbieter einzuloggen, anstatt den ZITADEL-Benutzer zum Anmelden zu verwenden." }, "HIDE_PASSWORD_RESET": { "TITLE": "Passwort-Reset ausgeblendet", @@ -303,7 +303,7 @@ "LINKS": { "CONTACT": "Kontakt", "TOS": "Nutzungsbedingungen", - "PP": "Datenschutz-Bestimmungen" + "PP": "Datenschutzerklärung" }, "THEME": { "DARK": "Dunkel", @@ -311,7 +311,7 @@ } }, "HOME": { - "WELCOME": "Loslegen mit ZITADEL", + "WELCOME": "Mit ZITADEL loslegen", "DISCLAIMER": "ZITADEL behandelt Deine Daten vertraulich und sicher.", "DISCLAIMERLINK": "Mehr Informationen zur Sicherheit", "DOCUMENTATION": { @@ -328,15 +328,15 @@ "SHORTCUTS": "Shortcuts", "SETTINGS": "Verfügbare Shortcuts", "PROJECTS": "Projekte", - "REORDER": "Zum Verschieben Kachel halten un ziehen", + "REORDER": "Zum Verschieben Kachel halten und ziehen", "ADD": "Zum Hinzufügen Kachel halten und ziehen" } }, "ONBOARDING": { - "DESCRIPTION": "Dein Onboarding-prozess", + "DESCRIPTION": "Dein Onboarding-Prozess", "MOREDESCRIPTION": "mehr Shortcuts", - "COMPLETED": "abgeschlossen", - "DISMISS": "schließen", + "COMPLETED": "Abgeschlossen", + "DISMISS": "Schließen", "CARD": { "TITLE": "Bringe deine Instanz zum Laufen", "DESCRIPTION": "Diese Checkliste hilft bei der Einrichtung Ihrer Instanz und führt Sie durch die wichtigsten Schritte" @@ -344,11 +344,11 @@ "MILESTONES": { "instance.policy.label.added": { "title": "Branding anpassen", - "description": "Definiere Farben und Form des Login-UIs und uploade deine Logos und Icons.", + "description": "Definiere Farben und Form des Login-UIs und füge deine Logos und Icons hinzu.", "action": "Branding anpassen" }, "instance.smtp.config.added": { - "title": "SMTP Benachrichtigungseinstellungen", + "title": "SMTP-Einstellungen", "description": "Konfiguriere deinen Mailserver.", "action": "SMTP einrichten" }, @@ -359,7 +359,7 @@ }, "APPLICATION_CREATED": { "title": "Registriere deine App", - "description": "Registriere deine erste Web-, native, API oder SAML-Applikation und konfiguriere den Authentification-flow.", + "description": "Registriere deine erste Web-, Nativ-, API- oder SAML-Applikation und konfiguriere den Authentification-Flow.", "action": "App registrieren" }, "AUTHENTICATION_SUCCEEDED_ON_APPLICATION": { @@ -369,12 +369,12 @@ }, "user.human.added": { "title": "Erfasse Benutzer", - "description": "Erstelle Benutzer die später deine Apps nutzen können.", + "description": "Erstelle Benutzer, die später deine Apps nutzen können.", "action": "Benutzer erfassen" }, "user.grant.added": { "title": "Berechtige Benutzer", - "description": "Erlaube es deinen Nutzern auf deine Apps zuzugreifen und gebe ihnen Rollen.", + "description": "Erlaube es deinen Nutzern, auf deine Apps zuzugreifen und gebe ihnen Rollen.", "action": "Benutzer berechtigen" } } @@ -425,7 +425,7 @@ "DEVMODEWARN": "Der Dev-Modus ist standardmäßig aktiviert. Sie können Werte für die Produktion später aktualisieren.", "GUIDE": "Guide", "BROWSEEXAMPLES": "Beispiele durchsuchen", - "DUPLICATEAPPRENAME": "Es gibt bereits eine App mit demselben Neme. Bitte wählen Sie einen anderen Namen.", + "DUPLICATEAPPRENAME": "Es gibt bereits eine App mit gleichem Namen. Bitte wähle einen anderen Namen.", "DIALOG": { "CHANGE": { "TITLE": "Framework ändern", @@ -454,7 +454,7 @@ "CONTINUE": "Weiter", "CONTINUEWITH": "Mit {{value}} fortfahren", "BACK": "Zurück", - "CLOSE": "Schliessen", + "CLOSE": "Schließen", "CLEAR": "Zurücksetzen", "CANCEL": "Abbrechen", "INFO": "Info", @@ -465,21 +465,21 @@ "DELETE": "Löschen", "REMOVE": "Entfernen", "VERIFY": "Verifizieren", - "FINISH": "Abschliessen", + "FINISH": "Abschließen", "FINISHED": "Fertig", "CHANGE": "Ändern", "REACTIVATE": "Aktivieren", "ACTIVATE": "Aktivieren", "DEACTIVATE": "Deaktivieren", "REFRESH": "Aktualisieren", - "LOGIN": "Einloggen", + "LOGIN": "Anmelden", "EDIT": "Bearbeiten", "PIN": "Anpinnen", "CONFIGURE": "Konfigurieren", "SEND": "Senden", "NEWVALUE": "Neuer Wert", "RESTORE": "Wiederherstellen", - "CONTINUEWITHOUTSAVE": "Ohne speichern fortfahren", + "CONTINUEWITHOUTSAVE": "Ohne Speichern fortfahren", "OF": "von", "PREVIOUS": "Zurück", "NEXT": "Weiter", @@ -490,7 +490,7 @@ "UNSAVEDCHANGES": "Nicht gespeicherte Änderungen", "UNSAVED": { "DIALOG": { - "DESCRIPTION": "Möchten Sie diese neue Aktion wirklich verwerfen? Ihre Aktion geht verloren", + "DESCRIPTION": "Möchtest du diese neue Aktion wirklich verwerfen? Alle Änderungen gehen verloren", "CANCEL": "Abbrechen", "DISCARD": "Verwerfen" } @@ -513,7 +513,7 @@ "ORG_OWNER_VIEWER": "Hat die Leseberechtigung, die gesamte Organisation zu überprüfen", "ORG_USER_PERMISSION_EDITOR": "Verfügt über die Berechtigung zum Verwalten von User grants", "ORG_PROJECT_PERMISSION_EDITOR": "Hat die Berechtigung, Projektberechtigungen für externe Organisationen zu verwalten", - "ORG_PROJECT_CREATOR": "Hat die Berechtigung, seine eigenen Projekte und zugrunde liegenden Einstellungen zu erstellen", + "ORG_PROJECT_CREATOR": "Hat die Berechtigung, seine eigenen Projekte und dessen Einstellungen zu erstellen", "ORG_ADMIN_IMPERSONATOR": "Hat die Berechtigung, sich als Administrator und Endbenutzer der Organisation auszugeben", "ORG_END_USER_IMPERSONATOR": "Hat die Berechtigung, sich als Endbenutzer der Organisation auszugeben", "PROJECT_OWNER": "Hat die Berechtigung für das gesamte Projekt", @@ -525,16 +525,16 @@ }, "OVERLAYS": { "ORGSWITCHER": { - "TEXT": "Alle Organisationseinstellungen und Tabellen basieren auf dieser ausgewählten Organisation. Klicken Sie auf diese Schaltfläche, um die Organisation zu wechseln oder eine neue zu erstellen." + "TEXT": "Alle Organisationseinstellungen und Tabellen basieren auf dieser ausgewählten Organisation. Klicke auf diese Schaltfläche, um die Organisation zu wechseln oder eine neue zu erstellen." }, "INSTANCE": { - "TEXT": "Klicken Sie hier, um zu den Instanceeinstellungen zu gelangen. Beachten Sie, dass Sie nur Zugriff auf diese Schaltfläche haben, wenn Sie über erweiterte Berechtigungen verfügen." + "TEXT": "Klicke hier, um zu den Instanz-Einstellungen zu gelangen. Beachte, dass du nur Zugriff auf diese Schaltfläche hast, wenn du über erweiterte Berechtigungen verfügst." }, "PROFILE": { - "TEXT": "Hier können Sie zwischen Ihren Benutzerkonten wechseln und Ihre Sessions und Ihr Profil verwalten." + "TEXT": "Hier kannst du zwischen Benutzerkonten wechseln und deine Sessions und Profil verwalten." }, "NAV": { - "TEXT": "Diese Navigation ändert sich basierend auf Ihrer Organisation oder Instanz" + "TEXT": "Diese Navigation ändert sich basierend auf deiner Organisation oder Instanz" }, "CONTEXTCHANGED": { "TEXT": "Achtung! Soeben wurde die Organisation gewechselt." @@ -572,8 +572,8 @@ "ME": "Zum eigenen Profil", "PROJECTS": "Zu den Projekten", "USERS": "Zu den Benutzern", - "USERGRANTS": "Zu den Authentisierungen", - "ACTIONS": "Zu den Aktionen und Abläufe", + "USERGRANTS": "Zu den Autorisierungen", + "ACTIONS": "Zu den Aktionen und Flows", "DOMAINS": "Zu den Domains" } }, @@ -585,7 +585,7 @@ }, "ERRORS": { "REQUIRED": "Bitte fülle dieses Feld aus.", - "ATLEASTONE": "Geben Sie mindestens einen Wert an.", + "ATLEASTONE": "Gebe mindestens einen Wert an.", "TOKENINVALID": { "TITLE": "Du bist abgemeldet", "DESCRIPTION": "Klicke auf \"Einloggen\", um Dich erneut anzumelden." @@ -594,11 +594,11 @@ "TITLE": "Deine Instanz ist blockiert.", "DESCRIPTION": "Bitte kontaktiere den Administrator deiner ZITADEL Instanz." }, - "INVALID_FORMAT": "Das Format is ungültig.", + "INVALID_FORMAT": "Das Format ist ungültig.", "NOTANEMAIL": "Der eingegebene Wert ist keine E-Mail Adresse.", "MINLENGTH": "Muss mindestens {{requiredLength}} Zeichen lang sein.", "MAXLENGTH": "Muss weniger als {{requiredLength}} Zeichen enthalten", - "UPPERCASEMISSING": "Muss einen Grossbuchstaben beinhalten.", + "UPPERCASEMISSING": "Muss einen Großbuchstaben beinhalten.", "LOWERCASEMISSING": "Muss einen Kleinbuchstaben beinhalten.", "SYMBOLERROR": "Muss ein Symbol/Satzzeichen beinhalten.", "NUMBERERROR": "Muss eine Ziffer beinhalten.", @@ -658,7 +658,7 @@ }, "SENDEMAILDIALOG": { "TITLE": "Email Benachrichtigung senden", - "DESCRIPTION": "Klicken Sie den untenstehenden Button um ein Verifizierung-E-Mail an die aktuelle Adresse zu versenden oder ändern Sie die Emailadresse in dem Feld.", + "DESCRIPTION": "Klicken Sie den untenstehenden Button um ein Verifizierungs-E-Mail an die aktuelle Adresse zu versenden oder ändern Sie die E-Maila-Adresse in dem Feld.", "NEWEMAIL": "Neue Email" }, "SECRETDIALOG": { @@ -715,10 +715,10 @@ "ADD_DESCRIPTION": "Wählen Sie eine der verfügbaren Optionen für das Erstellen einer passwortlosen Authentifizierungsmethode.", "SEND_DESCRIPTION": "Senden Sie sich einen Registrierungslink an Ihre E-Mail Adresse.", "SEND": "Registrierungslink senden", - "SENT": "Die email wurde erfolgreich zugestellt. Kontrollieren Sie Ihr Postfach um mit dem Setup fortzufahren.", - "QRCODE_DESCRIPTION": "QR-Code zum scannen mit einem anderen Gerät generieren.", + "SENT": "Die E-Mail wurde erfolgreich zugestellt. Kontrollieren Sie Ihr Postfach, um mit dem Setup fortzufahren.", + "QRCODE_DESCRIPTION": "QR-Code zum Scannen mit einem anderen Gerät generieren.", "QRCODE": "QR-Code generieren", - "QRCODE_SCAN": "Scannen Sie diesen QR Code um mit dem Setup auf Ihrem Gerät fortzufahren.", + "QRCODE_SCAN": "Scannen Sie diesen QR-Code, um mit dem Setup auf Ihrem Gerät fortzufahren.", "NEW_DESCRIPTION": "Verwenden Sie dieses Gerät um Passwortlos aufzusetzen.", "NEW": "Hinzufügen" } @@ -744,9 +744,9 @@ "OTPSMS": "OTP (One-Time Password) mit SMS", "OTPEMAIL": "OTP (One-Time Password) mit E-Mail", "SETUPOTPSMSDESCRIPTION": "Möchten Sie diese Telefonnummer als zweiten OTP-Faktor (Einmalpasswort) einrichten?", - "OTPSMSSUCCESS": "OTP Faktor erfolgrech hinzugefügt.", + "OTPSMSSUCCESS": "OTP-Faktor erfolgrech hinzugefügt.", "OTPSMSPHONEMUSTBEVERIFIED": "Um diese Methode nutzen zu können, muss Ihr Telefon verifiziert werden.", - "OTPEMAILSUCCESS": "OTP Faktor erfolgrech hinzugefügt.", + "OTPEMAILSUCCESS": "OTP-Faktor erfolgrech hinzugefügt.", "TYPE": { "0": "Keine MFA definiert", "1": "OTP", @@ -768,13 +768,13 @@ "EXTERNALIDP": { "TITLE": "Externe Identitäts-Provider", "DESC": "", - "IDPCONFIGID": "Idp Konfig ID", - "IDPNAME": "Idp Name", + "IDPCONFIGID": "IDP Konfig ID", + "IDPNAME": "IDP Name", "USERDISPLAYNAME": "Externer Name", "EXTERNALUSERID": "Externe Benutzer ID", "EMPTY": "Kein IDP gefunden", "DIALOG": { - "DELETE_TITLE": "Idp entfernen", + "DELETE_TITLE": "IDP entfernen", "DELETE_DESCRIPTION": "Sie sind im Begriff einen Identitätsanbieter zu entfernen. Wollen Sie dies wirklich tun?" } }, @@ -786,11 +786,11 @@ "PHONESECTION": "Telefonnummer", "PASSWORDSECTION": "Setze ein initiales Passwort.", "ADDRESSANDPHONESECTION": "Telefonnummer", - "INITMAILDESCRIPTION": "Wenn beide Optionen ausgewählt sind, wird keine E-Mail zur Initialisierung gesendet. Wenn nur eine der Optionen ausgewählt ist, wird eine E-Mail zur Aufforderung der Bereitstellung oder Verifikation der Daten gesendet." + "INITMAILDESCRIPTION": "Wenn beide Optionen ausgewählt sind, wird keine E-Mail zur Initialisierung gesendet. Wenn nur eine der Optionen ausgewählt ist, wird eine E-Mail zur Verifikation der Daten gesendet." }, "CODEDIALOG": { "TITLE": "Telefonnummer verifizieren", - "DESCRIPTION": "Gebe Deinen erhaltenen Code ein, um die Telefonnummer zu bestätigen.", + "DESCRIPTION": "Gebe den erhaltenen Code ein, um die Telefonnummer zu bestätigen.", "CODE": "Code" }, "DATA": { @@ -807,7 +807,7 @@ "TITLE": "Profil", "EMAIL": "E-Mail", "PHONE": "Telefonnummer", - "PHONE_HINT": "Verwenden das Symbol + gefolgt von der Landesvorwahl des Anrufers oder wählen Sie das Land aus der Dropdown-Liste aus und geben anschließend die Telefonnummer ein", + "PHONE_HINT": "Verwenden das Symbol + gefolgt von der Landesvorwahl des Anrufers oder wähle das Land aus der Dropdown-Liste aus und gebe anschließend die Telefonnummer ein.", "USERNAME": "Benutzername", "CHANGEUSERNAME": "bearbeiten", "CHANGEUSERNAME_TITLE": "Benutzername ändern", @@ -821,13 +821,13 @@ "GENDER": "Geschlecht", "PASSWORD": "Passwort", "AVATAR": { - "UPLOADTITLE": "Profilfoto uploaden", + "UPLOADTITLE": "Profilfoto hochladen", "UPLOADBTN": "Datei auswählen", "UPLOAD": "Hochladen", "CURRENT": "Aktuelles Bild", "PREVIEW": "Vorschau", "DELETESUCCESS": "Erfolgreich gelöscht!", - "CROPPERERROR": "Ein Fehler beim hochladen Ihrer Datei ist fehlgeschlagen. Versuchen Sie es mit ggf mit einem anderen Format und Grösse." + "CROPPERERROR": "Das Hochladen deiner Datei ist fehlgeschlagen. Versuche es mit ggf. mit einem anderen Format und Größe." }, "COUNTRY": "Land" }, @@ -858,7 +858,7 @@ }, "ADDED": { "TITLE": "Schlüssel wurde erstellt", - "DESCRIPTION": "Speichern Sie den Schlüssen. Der Schlüssel kann später nicht nochmal aufgerufen werden!" + "DESCRIPTION": "Speichern Sie den Schlüssel. Der Schlüssel kann später nicht nochmal aufgerufen werden!" }, "KEYTYPES": { "1": "JSON" @@ -881,8 +881,8 @@ "CONFIRMINITIAL": "Passwort wiederholen", "RESET": "Passwort zurücksetzen", "SET": "Passwort neu setzen", - "RESENDNOTIFICATION": "Email zum Zurücksetzen senden", - "REQUIRED": "Bitte prüfe, dass alle notwendigen Felder ausgefüllt sind.", + "RESENDNOTIFICATION": "E-Mail zum Zurücksetzen senden", + "REQUIRED": "Bitte prüfe, ob alle notwendigen Felder ausgefüllt sind.", "MINLENGTHERROR": "Muss mindestens {{value}} Zeichen lang sein.", "MAXLENGTHERROR": "Muss weniger als {{value}} Zeichen umfassen." }, @@ -901,11 +901,11 @@ "EMAIL": { "TITLE": "E-Mail", "VALID": "Validiert", - "ISVERIFIED": "Email Verifiziert", + "ISVERIFIED": "Email verifiziert", "ISVERIFIEDDESC": "Wenn die Email als verifiziert angegeben wird, wird keine Verifikationsmail an den Benutzer versendet.", "RESEND": "Verifikationsmail erneut senden", "EDITTITLE": "Email ändern", - "EDITDESC": "Geben Sie die neue Email in dem darunterliegenden Feld ein!" + "EDITDESC": "Geben Sie die neue Email in das Feld unten ein!" }, "PHONE": { "TITLE": "Telefon", @@ -913,10 +913,10 @@ "RESEND": "Verifikationsnachricht erneut senden", "EDITTITLE": "Nummer ändern", "EDITVALUE": "Telefonnummer", - "EDITDESC": "Geben Sie die neue Nummer in dem darunterliegenden Feld ein!", + "EDITDESC": "Geben Sie die neue Nummer in das Feld unten ein!", "DELETETITLE": "Telefonnummer löschen", "DELETEDESC": "Wollen Sie die Telefonnummer wirklich löschen?", - "OTPSMSREMOVALWARNING": "Dieses Konto verwendet diese Telefonnummer als zweiten Faktor. Wenn Sie fortfahren können Sie nicht mehr darauf zugreifen." + "OTPSMSREMOVALWARNING": "Dieses Konto verwendet diese Telefonnummer als zweiten Faktor. Wenn Sie fortfahren, können Sie nicht mehr darauf zugreifen." }, "RESENDCODE": "Code erneut senden", "ENTERCODE": "Verifizieren", @@ -924,7 +924,7 @@ }, "GRANTS": { "TITLE": "Benutzerberechtigungen", - "DESCRIPTION": "Erteile diesem Benutzer Verwaltunszugriff auf bestimmte Projekte.", + "DESCRIPTION": "Erteile diesem Benutzer Verwaltungszugriff auf bestimmte Projekte.", "CREATE": { "TITLE": "Benutzerberechtigung erstellen", "DESCRIPTION": "Suche nach der Organisation, dem Projekt und den verfügbaren Rollen." @@ -957,7 +957,7 @@ "EXTERNAL": "Um einen Benutzer Ihrer eigenen Organisation zu berechtigen, ", "CLICKHERE": "klicke hier" }, - "SIGNEDOUT": "Du bist abgemeldet. Klicke auf \"Anmelden\", um Dich erneut anzumelden.", + "SIGNEDOUT": "Du wurdest abgemeldet. Klicke auf \"Anmelden\", um Dich erneut anzumelden.", "SIGNEDOUT_BTN": "Anmelden", "EDITACCOUNT": "Konto bearbeiten", "ADDACCOUNT": "Konto hinzufügen", @@ -966,9 +966,9 @@ "TOAST": { "CREATED": "Benutzer erfolgreich erstellt.", "SAVED": "Profil gespeichert.", - "USERNAMECHANGED": "Username geändert.", + "USERNAMECHANGED": "Benutzername geändert.", "EMAILSAVED": "E-Mail gespeichert.", - "INITEMAILSENT": "Initialisierung Email gesendet.", + "INITEMAILSENT": "Initialisierungs-Email gesendet.", "PHONESAVED": "Telefonnummer gespeichert.", "PHONEREMOVED": "Telefonnummer gelöscht.", "PHONEVERIFIED": "Telefonnummer bestätigt.", @@ -986,10 +986,10 @@ "SELECTEDDEACTIVATED": "Selektierte Benutzer deaktiviert.", "SELECTEDKEYSDELETED": "Selektierte Schlüssel gelöscht.", "KEYADDED": "Schlüssel hinzugefügt!", - "MACHINEADDED": "Service User erstellt!", + "MACHINEADDED": "Service-Benutzer erstellt!", "DELETED": "Benutzer erfolgreich gelöscht!", "UNLOCKED": "Benutzer erfolgreich freigeschaltet!", - "PASSWORDLESSREGISTRATIONSENT": "Link via email versendet.", + "PASSWORDLESSREGISTRATIONSENT": "Link via E-Mail versendet.", "SECRETGENERATED": "Secret erfolgreich generiert!", "SECRETREMOVED": "Secret erfolgreich gelöscht!" }, @@ -998,14 +998,14 @@ "DESCRIPTION": "Dies sind alle Mitgliedschaften des Benutzers. Du kannst die entsprechenden Rechte auch auf der Organisations-, Projekt-, oder IAM-Detailseite aufrufen und modifizieren.", "ORGCONTEXT": "Sie sehen alle Organisationen und Projekte, die mit der aktuell gewählten Organisation in Verbindung stehen.", "USERCONTEXT": "Sie sehen alle Organisationen und Projekte auf denen Sie berechtigt sind inklusive aller zur Auswahl stehenden Organisationen.", - "CREATIONDATE": "Erstelldatum", + "CREATIONDATE": "Erstellungsdatum", "CHANGEDATE": "Letzte Änderung", "DISPLAYNAME": "Anzeigename", "REMOVE": "Entfernen", "TYPE": "Typ", "ORGID": "Organisation ID", - "UPDATED": "Membership wurde geändert.", - "NOPERMISSIONTOEDIT": "Ihnen fehlt die benötigte Berechtigung um Rollen zu ändern!", + "UPDATED": "Mitgliedschaft wurde geändert.", + "NOPERMISSIONTOEDIT": "Ihnen fehlt die benötigte Berechtigung, um Rollen zu ändern!", "TYPES": { "UNKNOWN": "Unbekannt", "ORG": "Organisation", @@ -1028,7 +1028,7 @@ }, "DELETE": { "TITLE": "Token löschen", - "DESCRIPTION": "Sie sind im Begriff das Token unwiderruflich zu löschen. Wollen Sie dies wirklich tun?" + "DESCRIPTION": "Sie sind im Begriff, das Token unwiderruflich zu löschen. Wollen Sie dies wirklich tun?" }, "DELETED": "Personal Access Token gelöscht." } @@ -1060,7 +1060,7 @@ "ALLOWEDTOFAIL": "Scheitern erlaubt", "ALLOWEDTOFAILWARN": { "TITLE": "Warnung", - "DESCRIPTION": "Wenn du diese Einstellung deaktivierst, kann es dazu führen, dass sich Benutzer deiner Organisation nicht mehr anmelden können. Ausserdem kannst du dann nicht mehr auf die Konsole zugreifen, um die Action zu deaktivieren. Wir empfehlen, einen Administratorbenutzer in einer separaten Organisation zu erstellen oder Skripte zuerst in einer Entwicklungsumgebung oder einer Entwicklungsorganisation zu testen." + "DESCRIPTION": "Wenn du diese Einstellung deaktivierst, kann dies dazu führen, dass sich Benutzer deiner Organisation nicht mehr anmelden können. Außerdem kannst du dann nicht mehr auf die Konsole zugreifen, um die Aktion zu deaktivieren. Wir empfehlen, einen Administratorbenutzer in einer separaten Organisation zu erstellen oder Skripte zuerst in einer Entwicklungsumgebung oder einer Entwicklungsorganisation zu testen." }, "SCRIPT": "Script", "FLOWTYPE": "Flow Typ", @@ -1076,18 +1076,18 @@ }, "DELETEACTION": { "TITLE": "Aktion löschen?", - "DESCRIPTION": "Sie sind im Begriff eine Aktion zu löschen. Dieser Vorgang kann nicht zurückgesetzt werden. Sind Sie sicher?", + "DESCRIPTION": "Du bist im Begriff, eine Aktion zu löschen. Dieser Vorgang kann nicht rückgängig gemacht werden. Bist du dir sicher?", "DELETE_SUCCESS": "Aktion erfolgreich gelöscht." }, "CLEAR": { "TITLE": "Flow zurücksetzen?", - "DESCRIPTION": "Sie sind im Begriff den Flow mitsamt seinen Triggern und Aktionen zurückzusetzen. Diese Änderung kann nicht wiederhergestellt werden." + "DESCRIPTION": "Du bist im Begriff, den Flow mitsamt seinen Triggern und Aktionen zurückzusetzen. Diese Änderung kann nicht rückgängig gemacht werden." }, "REMOVEACTIONSLIST": { "TITLE": "Ausgewählte Aktionen löschen?", "DESCRIPTION": "Wollen Sie die gewählten Aktionen wirklich löschen?" }, - "ABOUTNAME": "Der Name der Aktion und der Name der Funktion im Javascript müssen identisch sein" + "ABOUTNAME": "Der Name der Aktion und der Name der Funktion im Javascript müssen identisch sein." }, "TOAST": { "ACTIONSSET": "Aktionen gesetzt", @@ -1102,7 +1102,7 @@ }, "EVENTSTORE": { "TITLE": "IAM Speicher Administration", - "DESCRIPTION": "Verwalte Speicher Einstellungen von ZITADEL." + "DESCRIPTION": "Verwalte Speicher-Einstellungen von ZITADEL." }, "MEMBER": { "TITLE": "Manager", @@ -1130,7 +1130,7 @@ "CLEARED": "View wurde erfolgreich zurückgesetzt!", "DIALOG": { "VIEW_CLEAR_TITLE": "View zurücksetzen?", - "VIEW_CLEAR_DESCRIPTION": "Sie sind im Begriff eine View zu löschen. Durch das Löschen einer View wird ein Prozess gestartet, bei dem Daten für Endbenutzer möglicherweise nicht oder verzögert verfügbar sind. Sind Sie sicher?" + "VIEW_CLEAR_DESCRIPTION": "Du bist im Begriff eine View zu löschen. Durch das Löschen einer View wird ein Prozess gestartet, bei dem Daten für Endbenutzer möglicherweise nicht oder verzögert verfügbar sind. Bist du dir sicher?" } }, "FAILEDEVENTS": { @@ -1212,7 +1212,7 @@ "CREATIONDATE": "Erstelldatum", "DATECHANGED": "Geändert", "FILTER": "Filter", - "FILTERPLACEHOLDER": "Filtern Sie nach dem Namen", + "FILTERPLACEHOLDER": "Nach dem Namen filtern", "LIST": "Organisationen", "LISTDESCRIPTION": "Wähle eine Organisation aus.", "ACTIVE": "Aktiv", @@ -1266,7 +1266,7 @@ "DOMAINS": { "NEW": "Domain hinzufügen", "TITLE": "Verifizierte Domains", - "DESCRIPTION": "Konfiguriere die Domains, die für Domain discovery und als Suffix für die Benutzer verwendet werden können.", + "DESCRIPTION": "Konfiguriere die Domains, die für Domain Discovery und als Suffix für die Benutzer verwendet werden können.", "SETPRIMARY": "Primäre Domain setzen", "DELETE": { "TITLE": "Domain löschen?", @@ -1274,7 +1274,7 @@ }, "ADD": { "TITLE": "Domain hinzufügen", - "DESCRIPTION": "Du bist im Begriff, Deiner Organisation eine Domain hinzuzufügen. Die Domain kann für Domain discovery genutzt werden und als Suffix für deine Benutzernamen." + "DESCRIPTION": "Du bist im Begriff, Deiner Organisation eine Domain hinzuzufügen. Die Domain kann für Domain Discovery genutzt werden und als Suffix für deine Benutzernamen." } }, "STATE": { @@ -1303,15 +1303,15 @@ "DIALOG": { "DEACTIVATE": { "TITLE": "Organisation deaktivieren", - "DESCRIPTION": "Sie sind im Begriff Ihre Organisation zu deaktivieren. User können Sich danach nicht mehr anmelden? Wollen Sie fortfahren?" + "DESCRIPTION": "Sie sind im Begriff, Ihre Organisation zu deaktivieren. Benutzer können Sich danach nicht mehr anmelden? Wollen Sie fortfahren?" }, "REACTIVATE": { "TITLE": "Organisation reaktivieren", - "DESCRIPTION": "Sie sind im Begriff Ihre Organisation zu reaktivieren. User können Sich danach wieder anmelden? Wollen Sie fortfahren?" + "DESCRIPTION": "Sie sind im Begriff, Ihre Organisation zu reaktivieren. Benutzer können Sich danach wieder anmelden? Wollen Sie fortfahren?" }, "DELETE": { "TITLE": "Organisation löschen", - "DESCRIPTION": "Sie sind im Begriff Ihre Organisation endgültig zu löschen. Damit wird ein Prozess eingeleitet, bei dem alle organisationsbezogenen Daten gelöscht werden. Diese Aktion kann vorerst nicht rückgängig gemacht werden!", + "DESCRIPTION": "Sie sind im Begriff, Ihre Organisation endgültig zu löschen. Damit wird ein Prozess eingeleitet, bei dem alle organisationsbezogenen Daten gelöscht werden. Diese Aktion kann vorerst nicht rückgängig gemacht werden!", "TYPENAME": "Wiederholen Sie '{{value}}', um den Benutzer zu löschen.", "ORGNAME": "Loginname", "BTN": "Endgültig löschen" @@ -1385,10 +1385,10 @@ } }, "SMTP": { - "TITLE": "SMTP Einstellungen", + "TITLE": "SMTP-Einstellungen", "DESCRIPTION": "Beschreibung", - "SENDERADDRESS": "Sender Email-Adresse", - "SENDERNAME": "Sender Name", + "SENDERADDRESS": "Absender Email-Adresse", + "SENDERNAME": "Name des Absenders", "REPLYTOADDRESS": "Reply-to-Adresse", "HOSTANDPORT": "Host und Port", "USER": "Benutzer", @@ -1398,7 +1398,7 @@ "TLS": "Transport Layer Security (TLS)", "SAVED": "Erfolgreich gespeichert.", "NOCHANGES": "Keine Änderungen!", - "REQUIREDWARN": "Damit Mails von Ihrer Domain verschickt werden können, müssen Sie Ihre SMTP Einstellungen konfigurieren." + "REQUIREDWARN": "Damit Mails von Ihrer Domain verschickt werden können, müssen Sie Ihre SMTP-Einstellungen konfigurieren." }, "SMS": { "PROVIDERS": "Anbieter", @@ -1415,7 +1415,7 @@ "ACTIVATED": "Anbieter aktiviert.", "DEACTIVATED": "Anbieter deaktiviert.", "TWILIO": { - "SID": "Sid", + "SID": "SID", "TOKEN": "Token", "SENDERNUMBER": "Sender Number", "ADDED": "Twilio erfolgreich hinzugefügt.", @@ -1431,7 +1431,7 @@ "TYPE": { "1": "Email Initialisierungscode", "2": "Email Verifikationscode", - "3": "Telefonnummer Verificationscode", + "3": "Telefonnummer Verifikationscode", "4": "Passwort Zurücksetzen Code", "5": "Passwordless Initialisierungscode", "6": "Applicationssecret", @@ -1448,7 +1448,7 @@ }, "SECURITY": { "IFRAMETITLE": "iFrame", - "IFRAMEDESCRIPTION": "Mit dieser Einstellung wird die CSP so eingestellt, dass Framing von einer Reihe zulässiger Domänen zugelassen wird. Beachten Sie, dass Sie durch die Aktivierung der Verwendung von iFrames das Risiko eingehen, Clickjacking zu ermöglichen.", + "IFRAMEDESCRIPTION": "Mit dieser Einstellung wird die Content-Security-Policy (CSP) so eingestellt, dass Framing von einer Reihe zulässiger Domänen zugelassen wird. Beachten Sie, dass Sie durch die Aktivierung der Verwendung von iFrames das Risiko eingehen, Clickjacking zu ermöglichen.", "IFRAMEENABLED": "iFrame zulassen", "ALLOWEDORIGINS": "Zulässige URLs", "IMPERSONATIONTITLE": "Identitätswechsel", @@ -1522,10 +1522,10 @@ "RELEASEFONT": "Jetzt loslassen", "USEOFLOGO": "Ihr Logo wird im Login sowie emails verwendet, während das Icon für kleinere UI-Elemente wie den Organisationswechsel in der Konsole verwendet wird", "MAXSIZE": "Die maximale Grösse von Uploads ist mit 524kB begrenzt", - "EMAILNOSVG": "Das SVG Dateiformat wird nicht in emails unterstützt. Laden Sie deshalb ihr Logo im PNG oder einem anderen unterstützten Format hoch.", + "EMAILNOSVG": "Das SVG Dateiformat wird nicht in E-Mails unterstützt. Laden Sie deshalb ihr Logo im PNG oder einem anderen unterstützten Format hoch.", "MAXSIZEEXCEEDED": "Maximale Grösse von 524kB überschritten", - "NOSVGSUPPORTED": "SVG werden nicht unterstützt!", - "FONTINLOGINONLY": "Die Schriftart wird momentan nur im Login interface angezeigt.", + "NOSVGSUPPORTED": "SVGs werden nicht unterstützt!", + "FONTINLOGINONLY": "Die Schriftart wird momentan nur im Login Interface angezeigt.", "BACKGROUNDCOLOR": "Hintergrundfarbe", "PRIMARYCOLOR": "Primärfarbe", "WARNCOLOR": "Warnfarbe", @@ -1538,8 +1538,8 @@ "TITLE": "Anmeldung", "SECOND": "mit ZITADEL-Konto anmelden.", "ERROR": "Benutzer konnte nicht gefunden werden!", - "PRIMARYBUTTON": "weiter", - "SECONDARYBUTTON": "registrieren" + "PRIMARYBUTTON": "Weiter", + "SECONDARYBUTTON": "Registrieren" }, "THEMEMODE": { "THEME_MODE_AUTO": "Automatischer Modus", @@ -1579,7 +1579,7 @@ "DOCSLINK": "Link zu Dokumenten (Console)", "CUSTOMLINK": "Benutzerdefinierter Link (Console)", "CUSTOMLINKTEXT": "Benutzerdefinierter Linktext (Console)", - "SAVED": "Saved successfully!", + "SAVED": "Gespeichert!", "RESET_TITLE": "Standardwerte wiederherstellen", "RESET_DESCRIPTION": "Sie sind im Begriff die Standardlinks für die AGBs und Datenschutzrichtlinie wiederherzustellen. Wollen Sie fortfahren?" }, @@ -1706,14 +1706,14 @@ "EXPIREWARNDAYS": "Ablauf Warnung nach Tagen", "MAXAGEDAYS": "Maximale Gültigkeit in Tagen", "USERLOGINMUSTBEDOMAIN": "Organisationsdomain dem Loginname hinzufügen", - "USERLOGINMUSTBEDOMAIN_DESCRIPTION": "If you enable this setting, all loginnames will be suffixed with the organization domain. If this settings is disabled, you have to ensure that usernames are unique over all organizations.", + "USERLOGINMUSTBEDOMAIN_DESCRIPTION": "Wenn Sie diese Einstellung aktivieren, werden alle Loginnamen mit dem Suffix der Organisationsdomäne versehen. Wenn diese Einstellung deaktiviert ist, müssen Sie sicherstellen, dass die Benutzernamen für alle Organisationen eindeutig sind.", "VALIDATEORGDOMAINS": "Verifizierung des Organisations Domain erforderlich (DNS- oder HTTP-Herausforderung)", "SMTPSENDERADDRESSMATCHESINSTANCEDOMAIN": "SMTP Sender Adresse entspricht Instanzdomain", "ALLOWUSERNAMEPASSWORD_DESC": "Der konventionelle Login mit Benutzername und Passwort wird erlaubt.", "ALLOWEXTERNALIDP_DESC": "Der Login wird für die darunter liegenden Identitätsanbieter erlaubt.", "ALLOWREGISTER_DESC": "Ist die Option gewählt, erscheint im Login ein zusätzlicher Schritt zum Registrieren eines Benutzers.", "FORCEMFA": "MFA erzwingen", - "FORCEMFALOCALONLY": "MFA für lokale Users erzwingen", + "FORCEMFALOCALONLY": "MFA für lokale Benutzer erzwingen", "FORCEMFALOCALONLY_DESC": "Ist die Option gewählt, müssen lokal authentifizierte Benutzer einen zweiten Faktor für den Login verwenden.", "HIDEPASSWORDRESET_DESC": "Ist die Option gewählt, ist es nicht möglich im Login das Passwort zurück zusetzen via Passwort vergessen Link.", "HIDELOGINNAMESUFFIX": "Loginname Suffix ausblenden", @@ -2020,7 +2020,7 @@ }, "GITLAB": { "TITLE": "Gitlab Provider", - "DESCRIPTION": "Geben Sie die erforderlichen Daten für Ihren Gitlab Self Hosted Identitätsprovider ein." + "DESCRIPTION": "Geben Sie die erforderlichen Daten für Ihren Gitlab Identitätsprovider ein." }, "GITLABSELFHOSTED": { "TITLE": "Gitlab Self hosted Provider", @@ -2051,9 +2051,9 @@ }, "OPTIONS": { "ISAUTOCREATION": "Automatisches Erstellen", - "ISAUTOCREATION_DESC": "Legt fest ob ein Konto erstellt wird, falls es noch nicht existiert.", + "ISAUTOCREATION_DESC": "Legt fest, ob ein Konto erstellt wird, falls es noch nicht existiert.", "ISAUTOUPDATE": "Automatisches Update", - "ISAUTOUPDATE_DESC": "Legt fest ob Konten bei der erneuten Authentifizierung aktualisiert werden.", + "ISAUTOUPDATE_DESC": "Legt fest, ob Konten bei der erneuten Authentifizierung aktualisiert werden.", "ISCREATIONALLOWED": "Account erstellen erlaubt", "ISCREATIONALLOWED_DESC": "Legt fest, ob Konten erstellt werden können.", "ISLINKINGALLOWED": "Account linking erlaubt", @@ -2170,12 +2170,12 @@ }, "TOAST": { "SAVED": "Erfolgreich gespeichert.", - "REACTIVATED": "Idp reaktiviert.", - "DEACTIVATED": "Idp deaktiviert.", - "SELECTEDREACTIVATED": "Selektierte Idps reaktiviert.", - "SELECTEDDEACTIVATED": "Selektierte Idps deaktiviert.", - "SELECTEDKEYSDELETED": "Selektierte Idps gelöscht.", - "DELETED": "Idp erfolgreich gelöscht!", + "REACTIVATED": "IDP reaktiviert.", + "DEACTIVATED": "IDP deaktiviert.", + "SELECTEDREACTIVATED": "Selektierte IDPs reaktiviert.", + "SELECTEDDEACTIVATED": "Selektierte IDPs deaktiviert.", + "SELECTEDKEYSDELETED": "Selektierte IDPs gelöscht.", + "DELETED": "IDP erfolgreich gelöscht!", "ADDED": "Erfolgreich hinzugefügt.", "REMOVED": "Erfolgreich entfernt." }, @@ -2200,7 +2200,7 @@ "TOAST": { "ADDED": "Erfolgreich hinzugefügt.", "SAVED": "Erfolgreich gespeichert.", - "DELETED": "Mfa erfolgreich gelöscht!" + "DELETED": "MFA erfolgreich gelöscht!" }, "TYPE": "Typ", "MULTIFACTORTYPES": { @@ -2235,12 +2235,12 @@ "SMTP": { "LIST": { "TITLE": "SMTP-Anbieter", - "DESCRIPTION": "Dies sind die SMTP-Anbieter für Ihre ZITADEL-Instanz. Aktivieren Sie diejenige, die Sie zum Senden von Benachrichtigungen an Ihre Benutzer verwenden möchten.", + "DESCRIPTION": "Dies sind die SMTP-Anbieter für diese ZITADEL-Instanz. Aktiviere diejenige, die du zum Senden von Benachrichtigungen an deine Benutzer verwenden möchtest.", "EMPTY": "Kein SMTP-Anbieter verfügbar", "ACTIVATED": "Aktiviert", "ACTIVATE": "Anbieter aktivieren", "DEACTIVATE": "Anbieter deaktivieren", - "TEST": "Testen Sie Ihren Anbieter", + "TEST": "Teste den Anbieter", "TYPE": "Typ", "DIALOG": { "ACTIVATED": "Die SMTP-Konfiguration wurde aktiviert", @@ -2301,7 +2301,7 @@ "DESCRIPTION": "Hier kannst Du Deine Applikationen bearbeiten und deren Konfiguration anpassen.", "CREATE": "Applikation erstellen", "CREATE_SELECT_PROJECT": "Wähle zuerst dein Projekt aus", - "CREATE_NEW_PROJECT": "oder erstelle ein neues hier.", + "CREATE_NEW_PROJECT": "oder erstelle ein neues hier.", "CREATE_DESC_TITLE": "Gebe die Daten der Anwendung Schritt für Schritt ein.", "CREATE_DESC_SUB": "Es wird automatisch eine empfohlene Konfiguration generiert.", "STATE": "Status", @@ -2311,7 +2311,7 @@ "DELETE": "App löschen", "JUMPTOPROJECT": "Um Rollen, Berechtigungen und mehr zu konfigurieren, navigieren Sie zum Projekt.", "DETAIL": { - "TITLE": "Detail", + "TITLE": "Details", "STATE": { "0": "Nicht definiert", "1": "Aktiv", @@ -2320,7 +2320,7 @@ }, "DIALOG": { "CONFIG": { - "TITLE": "OIDC configuration ändern" + "TITLE": "OIDC Konfiguration ändern" }, "DELETE": { "TITLE": "App löschen", @@ -2345,8 +2345,8 @@ }, "NAMEDIALOG": { "TITLE": "App umbenennen", - "DESCRIPTION": "Geben Sie den neuen Namen für Ihre App an!", - "NAME": "Appname" + "DESCRIPTION": "Geben Sie den neuen Namen für Ihre App ein", + "NAME": "App-Name" }, "NAME": "Name", "TYPE": "Anwendungstyp", @@ -2362,7 +2362,7 @@ "TITLEFIRST": "Name der Applikation.", "TYPETITLE": "Art der Anwendung", "OIDC": { - "WELLKNOWN": "Weitere Links können vom Discovery endpoint abgerufen werden.", + "WELLKNOWN": "Weitere Links können vom Discovery Endpoint abgerufen werden.", "INFO": { "ISSUER": "Issuer", "CLIENTID": "Client Id" @@ -2394,7 +2394,7 @@ "POSTLOGOUTREDIRECT": "URIs für Post-Log-out", "RESPONSESECTION": "Antworttypen", "GRANTSECTION": "Berechtigungstypen", - "GRANTTITLE": "Wähle Deine Berechtigungstypen aus. Hinweis: \"Implizit\" ist nur für browser-basierte Anwendungen verfügbar.", + "GRANTTITLE": "Wähle Deine Berechtigungstypen aus. Hinweis: \"Implizit\" ist nur für Browser-basierte Anwendungen verfügbar.", "APPTYPE": { "0": "Web", "1": "User Agent", @@ -2424,7 +2424,7 @@ "TOKENTYPE": "Auth Token Typ", "TOKENTYPE0": "Bearer Token", "TOKENTYPE1": "JWT", - "UNSECUREREDIRECT": "Wir hoffen, Du weisst, was Du tust.", + "UNSECUREREDIRECT": "Wir hoffen, Du weißt, was Du tust.", "OVERVIEWSECTION": "Übersicht", "OVERVIEWTITLE": "Deine Konfiguration ist bereit. Du kannst sie hier nochmals prüfen.", "ACCESSTOKENROLEASSERTION": "Benutzerrollen dem Access Token hinzufügen", @@ -2440,7 +2440,7 @@ "APPTYPE": { "WEB": { "TITLE": "Web", - "DESCRIPTION": "Standard Web applications wie .net, PHP, Node.js, Java, etc." + "DESCRIPTION": "Standard Web applications wie .NET, PHP, Node.js, Java, etc." }, "NATIVE": { "TITLE": "Native", @@ -2448,14 +2448,14 @@ }, "USERAGENT": { "TITLE": "User Agent", - "DESCRIPTION": "Single Page Applications (SPA) und grundsätzlich alle im Browser aufgeführten JS Frameworks" + "DESCRIPTION": "Single Page Applications (SPA) und grundsätzlich alle im Browser aufgeführten JS-Frameworks" } } } }, "API": { "INFO": { - "CLIENTID": "Client Id" + "CLIENTID": "Client ID" }, "REGENERATESECRET": "Client Secret neu generieren", "SELECTION": { @@ -2479,12 +2479,12 @@ "METADATAOPT3": "Option 3: Erstellen Sie spontan eine minimale Metadatendatei mit ENTITYID und ACS-URL", "UPLOAD": "XML-Datei hochladen", "METADATA": "Metadaten", - "METADATAFROMFILE": "Metadata aus Datei", + "METADATAFROMFILE": "Metadaten aus Datei", "CERTIFICATE": "SAML-Zertifikat", - "DOWNLOADCERT": "Laden Sie das SAML-Zertifikat herunter", - "CREATEMETADATA": "Create metadata", + "DOWNLOADCERT": "SAML-Zertifikat herunterladen", + "CREATEMETADATA": "Metadaten erstellen", "ENTITYID": "Entity ID", - "ACSURL": "ACS endpoint URL" + "ACSURL": "ACS Endpoint URL" }, "AUTHMETHODS": { "CODE": { @@ -2509,7 +2509,7 @@ }, "IMPLICIT": { "TITLE": "Implicit", - "DESCRIPTION": "Erhalte die Token direkt vom authorize Endpoint" + "DESCRIPTION": "Erhalte die Token direkt vom Authorize-Endpoint" }, "DEVICECODE": { "TITLE": "Device Code", @@ -2536,7 +2536,7 @@ "0": "Unbekannt", "1": "Weiblich", "2": "Männlich", - "3": "Anderes" + "3": "Divers" }, "LANGUAGES": { "de": "Deutsch", @@ -2565,7 +2565,7 @@ "1": "Berechtigtes Projekt", "4": "Projekt" }, - "EDITROLE": "Rollen editieren", + "EDITROLE": "Rollen bearbeiten", "EDITFOR": "Bearbeiten Sie die Rollen für den Benutzer: {{value}}", "DIALOG": { "DELETE_TITLE": "Manager entfernen", diff --git a/internal/notification/static/i18n/de.yaml b/internal/notification/static/i18n/de.yaml index 676534229c..673b207ef2 100644 --- a/internal/notification/static/i18n/de.yaml +++ b/internal/notification/static/i18n/de.yaml @@ -3,21 +3,21 @@ InitCode: PreHeader: User initialisieren Subject: User initialisieren Greeting: Hallo {{.DisplayName}}, - Text: Dieser Benutzer wurde soeben in ZITADEL erstellt. Mit dem Benutzernamen <br><strong>{{.PreferredLoginName}}</strong><br> kannst du dich anmelden. Nutze den untenstehenden Button, um die Initialisierung abzuschliessen <br>(Code <strong>{{.Code}}</strong>).<br> Falls du dieses Mail nicht angefordert hast, kannst du es einfach ignorieren. + Text: Dieser Benutzer wurde soeben erstellt. Mit dem Benutzernamen <br><strong>{{.PreferredLoginName}}</strong><br> kannst du dich anmelden. Nutze den unten stehenden Button, um die Initialisierung abzuschliessen <br>(Code <strong>{{.Code}}</strong>).<br> Falls du dieses E-Mail nicht angefordert hast, kannst du sie einfach ignorieren. ButtonText: Initialisierung abschliessen PasswordReset: Title: Passwort zurücksetzen PreHeader: Passwort zurücksetzen Subject: Passwort zurücksetzen Greeting: Hallo {{.DisplayName}}, - Text: Wir haben eine Anfrage für das Zurücksetzen deines Passworts bekommen. Du kannst den untenstehenden Button verwenden, um dein Passwort zurückzusetzen <br>(Code <strong>{{.Code}}</strong>).<br> Falls du das Zurücksetzen des Passworts nicht angefordert hast, kannst du diese E-Mail ignorieren. + Text: Wir haben eine Anfrage zum Zurücksetzen deines Passworts bekommen. Du kannst den unten stehenden Button verwenden, um dein Passwort zurückzusetzen <br>(Code <strong>{{.Code}}</strong>).<br> Falls du das Zurücksetzen des Passworts nicht angefordert hast, kannst du diese E-Mail ignorieren. ButtonText: Passwort zurücksetzen VerifyEmail: Title: E-Mail-Adresse verifizieren PreHeader: E-Mail-Adresse verifizieren Subject: E-Mail-Adresse verifizieren Greeting: Hallo {{.DisplayName}}, - Text: Eine neue E-Mail-Adresse wurde hinzugefügt. Bitte verwende den untenstehenden Button, um diese zu verifizieren <br>(Code <strong>{{.Code}}</strong>).<br> Falls du die E-Mail-Adresse nicht selber hinzugefügt hast, kannst du diese E-Mail ignorieren. + Text: Eine neue E-Mail-Adresse wurde hinzugefügt. Bitte verwende den unten stehenden Button, um diese zu verifizieren <br>(Code <strong>{{.Code}}</strong>).<br> Falls du die E-Mail-Adresse nicht selbst hinzugefügt hast, kannst du diese E-Mail ignorieren. ButtonText: E-Mail-Adresse verifizieren VerifyPhone: Title: Telefonnummer verifizieren @@ -31,7 +31,7 @@ VerifyEmailOTP: PreHeader: Einmalpasswort verifizieren Subject: Einmalpasswort verifizieren Greeting: Hallo {{.DisplayName}}, - Text: Bitte nutze den 'Authentifizieren'-Button oder kopiere das Einmalpasswort {{.OTP}} und füge es in den Authentifizierungsbildschirm ein, um dich innerhalb der nächsten fünf Minuten zu authentifizieren. + Text: Bitte nutze den 'Authentifizieren'-Button oder kopiere das Einmalpasswort {{.OTP}} und gib es zur Bestätigung ein, um dich innerhalb der nächsten fünf Minuten zu authentifizieren. ButtonText: Authentifizieren VerifySMSOTP: Text: >- @@ -43,14 +43,14 @@ DomainClaimed: PreHeader: E-Mail / Benutzername ändern Subject: Domain wurde beansprucht Greeting: Hallo {{.DisplayName}}, - Text: Die Domain {{.Domain}} wurde von einer Organisation beansprucht. Dein derzeitiger User {{.Username}} ist nicht Teil dieser Organisation. Daher musst du beim nächsten Login eine neue E-Mail-Adresse hinterlegen. Für diesen Login haben wir dir einen temporären Usernamen ({{.TempUsername}}) erstellt. + Text: Die Domain {{.Domain}} wurde von einer Organisation beansprucht. Dein derzeitiger Benutzer {{.Username}} ist nicht Teil dieser Organisation. Daher musst du beim nächsten Login eine neue E-Mail-Adresse hinterlegen. Für diesen Login haben wir dir einen temporären Usernamen ({{.TempUsername}}) erstellt. ButtonText: Login PasswordlessRegistration: Title: Passwortlosen Login hinzufügen PreHeader: Passwortlosen Login hinzufügen Subject: Passwortlosen Login hinzufügen Greeting: Hallo {{.DisplayName}}, - Text: Wir haben eine Anfrage für das Hinzufügen eines Token für den passwortlosen Login erhalten. Du kannst den untenstehenden Button verwenden, um dein Token oder Gerät hinzuzufügen. + Text: Wir haben eine Anfrage für das Hinzufügen eines Token für den passwortlosen Login erhalten. Du kannst den unten stehenden Button verwenden, um dein Token oder Gerät hinzuzufügen. ButtonText: Passwortlosen Login hinzufügen PasswordChange: Title: Passwort wurde geändert From bc16962aac32170bf111b24b48a7ec7a5cd83002 Mon Sep 17 00:00:00 2001 From: Stefan Benz <46600784+stebenz@users.noreply.github.com> Date: Fri, 26 Jul 2024 12:28:28 +0200 Subject: [PATCH 04/39] feat: api v2beta to api v2 protos (#8343) # Which Problems Are Solved The go linter can't limit the checks to the diff in https://github.com/zitadel/zitadel/pull/8283 because it's too large # How the Problems Are Solved The protos from https://github.com/zitadel/zitadel/pull/8283 are merged separately # Additional Context Contributes to #7236 --------- Co-authored-by: Elio Bischof --- docs/docs/apis/_v3_action_execution.proto | 8 +- docs/docs/apis/_v3_action_search.proto | 2 +- docs/docs/apis/actionsv2/execution-local.md | 8 +- docs/docs/apis/actionsv2/introduction.md | 24 +- docs/docs/apis/introduction.mdx | 6 + docs/docs/apis/v2.mdx | 4 +- .../integrate/login-ui/_list-mfa-options.mdx | 2 +- .../guides/integrate/login-ui/_logout.mdx | 4 +- .../integrate/login-ui/_select-account.mdx | 2 +- .../login-ui/_update_session_webauthn.mdx | 2 +- .../integrate/login-ui/external-login.mdx | 10 +- docs/docs/guides/integrate/login-ui/mfa.mdx | 30 +- .../integrate/login-ui/oidc-standard.mdx | 4 +- .../guides/integrate/login-ui/passkey.mdx | 8 +- .../integrate/login-ui/password-reset.mdx | 6 +- .../integrate/login-ui/username-password.mdx | 6 +- docs/docs/guides/integrate/token-exchange.mdx | 2 +- .../docs/legal/service-description/billing.md | 1 + docs/docusaurus.config.js | 36 +- docs/sidebars.js | 68 +- proto/zitadel/feature/v2/feature.proto | 68 + .../zitadel/feature/v2/feature_service.proto | 395 ++++ proto/zitadel/feature/v2/instance.proto | 132 ++ proto/zitadel/feature/v2/organization.proto | 62 + proto/zitadel/feature/v2/system.proto | 128 + proto/zitadel/feature/v2/user.proto | 62 + proto/zitadel/management.proto | 144 +- proto/zitadel/object/v2/object.proto | 122 + proto/zitadel/oidc/v2/authorization.proto | 117 + proto/zitadel/oidc/v2/oidc_service.proto | 219 ++ proto/zitadel/org/v2/org_service.proto | 174 ++ proto/zitadel/session/v2/challenge.proto | 82 + proto/zitadel/session/v2/session.proto | 178 ++ .../zitadel/session/v2/session_service.proto | 496 ++++ .../settings/v2/branding_settings.proto | 93 + .../zitadel/settings/v2/domain_settings.proto | 33 + .../zitadel/settings/v2/legal_settings.proto | 58 + .../settings/v2/lockout_settings.proto | 29 + .../zitadel/settings/v2/login_settings.proto | 152 ++ .../settings/v2/password_settings.proto | 60 + .../settings/v2/security_settings.proto | 31 + proto/zitadel/settings/v2/settings.proto | 13 + .../settings/v2/settings_service.proto | 479 ++++ proto/zitadel/user/v2/auth.proto | 50 + proto/zitadel/user/v2/email.proto | 54 + proto/zitadel/user/v2/idp.proto | 164 ++ proto/zitadel/user/v2/password.proto | 85 + proto/zitadel/user/v2/phone.proto | 40 + proto/zitadel/user/v2/query.proto | 268 +++ proto/zitadel/user/v2/user.proto | 284 +++ proto/zitadel/user/v2/user_service.proto | 2081 +++++++++++++++++ proto/zitadel/user/v2beta/idp.proto | 8 +- proto/zitadel/user/v2beta/user_service.proto | 277 ++- .../zitadel/user/v3alpha/authenticator.proto | 12 +- proto/zitadel/user/v3alpha/query.proto | 14 +- proto/zitadel/user/v3alpha/user.proto | 4 +- proto/zitadel/user/v3alpha/user_service.proto | 80 +- 57 files changed, 6690 insertions(+), 291 deletions(-) create mode 100644 proto/zitadel/feature/v2/feature.proto create mode 100644 proto/zitadel/feature/v2/feature_service.proto create mode 100644 proto/zitadel/feature/v2/instance.proto create mode 100644 proto/zitadel/feature/v2/organization.proto create mode 100644 proto/zitadel/feature/v2/system.proto create mode 100644 proto/zitadel/feature/v2/user.proto create mode 100644 proto/zitadel/object/v2/object.proto create mode 100644 proto/zitadel/oidc/v2/authorization.proto create mode 100644 proto/zitadel/oidc/v2/oidc_service.proto create mode 100644 proto/zitadel/org/v2/org_service.proto create mode 100644 proto/zitadel/session/v2/challenge.proto create mode 100644 proto/zitadel/session/v2/session.proto create mode 100644 proto/zitadel/session/v2/session_service.proto create mode 100644 proto/zitadel/settings/v2/branding_settings.proto create mode 100644 proto/zitadel/settings/v2/domain_settings.proto create mode 100644 proto/zitadel/settings/v2/legal_settings.proto create mode 100644 proto/zitadel/settings/v2/lockout_settings.proto create mode 100644 proto/zitadel/settings/v2/login_settings.proto create mode 100644 proto/zitadel/settings/v2/password_settings.proto create mode 100644 proto/zitadel/settings/v2/security_settings.proto create mode 100644 proto/zitadel/settings/v2/settings.proto create mode 100644 proto/zitadel/settings/v2/settings_service.proto create mode 100644 proto/zitadel/user/v2/auth.proto create mode 100644 proto/zitadel/user/v2/email.proto create mode 100644 proto/zitadel/user/v2/idp.proto create mode 100644 proto/zitadel/user/v2/password.proto create mode 100644 proto/zitadel/user/v2/phone.proto create mode 100644 proto/zitadel/user/v2/query.proto create mode 100644 proto/zitadel/user/v2/user.proto create mode 100644 proto/zitadel/user/v2/user_service.proto diff --git a/docs/docs/apis/_v3_action_execution.proto b/docs/docs/apis/_v3_action_execution.proto index dc040a2466..80a93b1606 100644 --- a/docs/docs/apis/_v3_action_execution.proto +++ b/docs/docs/apis/_v3_action_execution.proto @@ -52,7 +52,7 @@ message RequestExecution { (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { min_length: 1, max_length: 1000, - example: "\"/zitadel.session.v2beta.SessionService/ListSessions\""; + example: "\"/zitadel.session.v2.SessionService/ListSessions\""; } ]; // GRPC-service as condition. @@ -61,7 +61,7 @@ message RequestExecution { (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { min_length: 1, max_length: 1000, - example: "\"zitadel.session.v2beta.SessionService\""; + example: "\"zitadel.session.v2.SessionService\""; } ]; // All calls to any available services and methods as condition. @@ -78,7 +78,7 @@ message ResponseExecution { (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { min_length: 1, max_length: 1000, - example: "\"/zitadel.session.v2beta.SessionService/ListSessions\""; + example: "\"/zitadel.session.v2.SessionService/ListSessions\""; } ]; // GRPC-service as condition. @@ -87,7 +87,7 @@ message ResponseExecution { (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { min_length: 1, max_length: 1000, - example: "\"zitadel.session.v2beta.SessionService\""; + example: "\"zitadel.session.v2.SessionService\""; } ]; // All calls to any available services and methods as condition. diff --git a/docs/docs/apis/_v3_action_search.proto b/docs/docs/apis/_v3_action_search.proto index bae01c9061..59ead05364 100644 --- a/docs/docs/apis/_v3_action_search.proto +++ b/docs/docs/apis/_v3_action_search.proto @@ -47,7 +47,7 @@ message IncludeFilter { string include = 1 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { description: "the id of the include" - example: "\"request.zitadel.session.v2beta.SessionService\""; + example: "\"request.zitadel.session.v2.SessionService\""; } ]; } diff --git a/docs/docs/apis/actionsv2/execution-local.md b/docs/docs/apis/actionsv2/execution-local.md index 3f0ccc0fe0..26325add57 100644 --- a/docs/docs/apis/actionsv2/execution-local.md +++ b/docs/docs/apis/actionsv2/execution-local.md @@ -85,7 +85,7 @@ curl -L -X PUT 'https://$CUSTOM-DOMAIN/v3alpha/executions' \ --data-raw '{ "condition": { "request": { - "method": "/zitadel.user.v2beta.UserService/AddHumanUser" + "method": "/zitadel.user.v2.UserService/AddHumanUser" } }, "targets": [ @@ -98,10 +98,10 @@ curl -L -X PUT 'https://$CUSTOM-DOMAIN/v3alpha/executions' \ ## Example call -Now on every call on `/zitadel.user.v2beta.UserService/AddHumanUser` the local server prints out the received body of the request: +Now on every call on `/zitadel.user.v2.UserService/AddHumanUser` the local server prints out the received body of the request: ```shell -curl -L -X PUT 'https://$CUSTOM-DOMAIN/v2beta/users/human' \ +curl -L -X PUT 'https://$CUSTOM-DOMAIN/v2/users/human' \ -H 'Content-Type: application/json' \ -H 'Accept: application/json' \ -H 'Authorization: Bearer ' \ @@ -119,7 +119,7 @@ curl -L -X PUT 'https://$CUSTOM-DOMAIN/v2beta/users/human' \ Should print out something like, also described under [Sent information Request](./introduction#sent-information-request): ```shell { - "fullMethod": "/zitadel.user.v2beta.UserService/AddHumanUser", + "fullMethod": "/zitadel.user.v2.UserService/AddHumanUser", "instanceID": "262851882718855632", "orgID": "262851882718921168", "projectID": "262851882719052240", diff --git a/docs/docs/apis/actionsv2/introduction.md b/docs/docs/apis/actionsv2/introduction.md index e6362c1aee..ace2fca321 100644 --- a/docs/docs/apis/actionsv2/introduction.md +++ b/docs/docs/apis/actionsv2/introduction.md @@ -70,16 +70,16 @@ The API documentation to set an Execution can be found [here](/apis/resources/ac ### Condition Best Match As the conditions can be defined on different levels, ZITADEL tries to find out which Execution is the best match. -This means that for example if you have an Execution defined on `all requests`, on the service `zitadel.user.v2beta.UserService` and on `/zitadel.user.v2beta.UserService/AddHumanUser`, -ZITADEL would with a call on the `/zitadel.user.v2beta.UserService/AddHumanUser` use the Executions with the following priority: +This means that for example if you have an Execution defined on `all requests`, on the service `zitadel.user.v2.UserService` and on `/zitadel.user.v2.UserService/AddHumanUser`, +ZITADEL would with a call on the `/zitadel.user.v2.UserService/AddHumanUser` use the Executions with the following priority: -1. `/zitadel.user.v2beta.UserService/AddHumanUser` -2. `zitadel.user.v2beta.UserService` +1. `/zitadel.user.v2.UserService/AddHumanUser` +2. `zitadel.user.v2.UserService` 3. `all` -If you then have a call on `/zitadel.user.v2beta.UserService/UpdateHumanUser` the following priority would be found: +If you then have a call on `/zitadel.user.v2.UserService/UpdateHumanUser` the following priority would be found: -1. `zitadel.user.v2beta.UserService` +1. `zitadel.user.v2.UserService` 2. `all` And if you use a different service, for example `zitadel.session.v2.SessionService`, then the `all` Execution would still be used. @@ -100,7 +100,7 @@ If you define 2 Executions as follows: { "condition": { "request": { - "service": "zitadel.user.v2beta.UserService" + "service": "zitadel.user.v2.UserService" } }, "targets": [ @@ -115,7 +115,7 @@ If you define 2 Executions as follows: { "condition": { "request": { - "method": "/zitadel.user.v2beta.UserService/AddHumanUser" + "method": "/zitadel.user.v2.UserService/AddHumanUser" } }, "targets": [ @@ -125,7 +125,7 @@ If you define 2 Executions as follows: { "include": { "request": { - "service": "zitadel.user.v2beta.UserService" + "service": "zitadel.user.v2.UserService" } } } @@ -133,7 +133,7 @@ If you define 2 Executions as follows: } ``` -The called Targets on "/zitadel.user.v2beta.UserService/AddHumanUser" would be, in order: +The called Targets on "/zitadel.user.v2.UserService/AddHumanUser" would be, in order: 1. `` 2. `` @@ -147,8 +147,8 @@ For Request and Response there are 3 levels the condition can be defined: - `All`, handling any request or response under the ZITADEL API The available conditions can be found under: -- [All available Methods](/apis/resources/action_service_v3/action-service-list-execution-methods), for example `/zitadel.user.v2beta.UserService/AddHumanUser` -- [All available Services](/apis/resources/action_service_v3/action-service-list-execution-services), for example `zitadel.user.v2beta.UserService` +- [All available Methods](/apis/resources/action_service_v3/action-service-list-execution-methods), for example `/zitadel.user.v2.UserService/AddHumanUser` +- [All available Services](/apis/resources/action_service_v3/action-service-list-execution-services), for example `zitadel.user.v2.UserService` ### Condition for Functions diff --git a/docs/docs/apis/introduction.mdx b/docs/docs/apis/introduction.mdx index 081b31cbce..443896d508 100644 --- a/docs/docs/apis/introduction.mdx +++ b/docs/docs/apis/introduction.mdx @@ -313,4 +313,10 @@ For easy copying to your reverse proxy configuration, here is the list of URL pa /zitadel.settings.v2beta.SettingsService/ /zitadel.oidc.v2beta.OIDCService/ /zitadel.org.v2beta.OrganizationService/ +/v2/ +/zitadel.user.v2.UserService/ +/zitadel.session.v2.SessionService/ +/zitadel.settings.v2.SettingsService/ +/zitadel.oidc.v2.OIDCService/ +/zitadel.org.v2.OrganizationService/ ``` diff --git a/docs/docs/apis/v2.mdx b/docs/docs/apis/v2.mdx index 0b69707a19..2f51dba6e9 100644 --- a/docs/docs/apis/v2.mdx +++ b/docs/docs/apis/v2.mdx @@ -1,5 +1,5 @@ --- -title: APIs V2 (Beta) +title: APIs V2 (General Available) --- import DocCardList from '@theme/DocCardList'; @@ -7,6 +7,4 @@ import DocCardList from '@theme/DocCardList'; APIs V2 organize access by resources (users, settings, etc.), unlike context-specific V1 APIs. This simplifies finding the right API, especially for multi-organization resources. -**Note**: V2 is currently in [Beta](/support/software-release-cycles-support#beta) and not yet generally available (breaking changes possible). Check individual services for availability. - \ No newline at end of file diff --git a/docs/docs/guides/integrate/login-ui/_list-mfa-options.mdx b/docs/docs/guides/integrate/login-ui/_list-mfa-options.mdx index dadd773401..0872c1cfce 100644 --- a/docs/docs/guides/integrate/login-ui/_list-mfa-options.mdx +++ b/docs/docs/guides/integrate/login-ui/_list-mfa-options.mdx @@ -8,7 +8,7 @@ Request Example: ```bash curl --request GET \ - --url https://$ZITADEL_DOMAIN/v2beta/settings/login \ + --url https://$ZITADEL_DOMAIN/v2/settings/login \ --header 'Accept: application/json' \ --header 'Authorization: Bearer '"$TOKEN"'' ``` diff --git a/docs/docs/guides/integrate/login-ui/_logout.mdx b/docs/docs/guides/integrate/login-ui/_logout.mdx index 33bc3457f6..73e89b84cc 100644 --- a/docs/docs/guides/integrate/login-ui/_logout.mdx +++ b/docs/docs/guides/integrate/login-ui/_logout.mdx @@ -16,7 +16,7 @@ Make sure that the provided token is from the authenticated user, resp. the mana ```bash curl --request DELETE \ - --url https://$ZITADEL_DOMAIN/v2beta/sessions/218480890961985793 \ + --url https://$ZITADEL_DOMAIN/v2/sessions/218480890961985793 \ --header 'Accept: application/json' \ --header 'Authorization: Bearer '"$TOKEN"''\ --header 'Content-Type: application/json' @@ -28,7 +28,7 @@ Send the session token in the body of the request: ```bash curl --request DELETE \ - --url https://$ZITADEL_DOMAIN/v2beta/sessions/218480890961985793 \ + --url https://$ZITADEL_DOMAIN/v2/sessions/218480890961985793 \ --header 'Accept: application/json' \ --header 'Authorization: Bearer '"$TOKEN"''\ --header 'Content-Type: application/json' \ diff --git a/docs/docs/guides/integrate/login-ui/_select-account.mdx b/docs/docs/guides/integrate/login-ui/_select-account.mdx index 97528186e6..786ecfb713 100644 --- a/docs/docs/guides/integrate/login-ui/_select-account.mdx +++ b/docs/docs/guides/integrate/login-ui/_select-account.mdx @@ -9,7 +9,7 @@ The list of session IDs can be sent in the “search sessions” request to get ```bash curl --request POST \ - --url https://$ZITADEL_DOMAIN/v2beta/sessions/search \ + --url https://$ZITADEL_DOMAIN/v2/sessions/search \ --header 'Accept: application/json' \ --header 'Authorization: Bearer '"$TOKEN"''\ --header 'Content-Type: application/json' \ diff --git a/docs/docs/guides/integrate/login-ui/_update_session_webauthn.mdx b/docs/docs/guides/integrate/login-ui/_update_session_webauthn.mdx index 430a31b33b..9388687bf9 100644 --- a/docs/docs/guides/integrate/login-ui/_update_session_webauthn.mdx +++ b/docs/docs/guides/integrate/login-ui/_update_session_webauthn.mdx @@ -8,7 +8,7 @@ Example Request: ```bash curl --request PATCH \ - --url https://$ZITADEL_DOMAIN/v2beta/sessions/218480890961985793 \ + --url https://$ZITADEL_DOMAIN/v2/sessions/218480890961985793 \ --header 'Accept: application/json' \ --header 'Authorization: Bearer '"$TOKEN"''\ --header 'Content-Type: application/json' \ diff --git a/docs/docs/guides/integrate/login-ui/external-login.mdx b/docs/docs/guides/integrate/login-ui/external-login.mdx index a81340d46a..b9f847935a 100644 --- a/docs/docs/guides/integrate/login-ui/external-login.mdx +++ b/docs/docs/guides/integrate/login-ui/external-login.mdx @@ -26,7 +26,7 @@ In the response, you will get an authentication URL of the provider you like. ```bash curl --request POST \ - --url https://$ZITADEL_DOMAIN/v2beta/idp_intents \ + --url https://$ZITADEL_DOMAIN/v2/idp_intents \ --header 'Accept: application/json' \ --header 'Authorization: Bearer '"$TOKEN"''\ --header 'Content-Type: application/json' \ @@ -73,7 +73,7 @@ To get the information of the provider, make a request to ZITADEL. ### Request ```bash curl --request POST \ - --url https://$ZITADEL_DOMAIN/v2beta/idp_intents/$INTENT_ID \ + --url https://$ZITADEL_DOMAIN/v2/idp_intents/$INTENT_ID \ --header 'Accept: application/json' \ --header 'Authorization: Bearer '"$TOKEN"''\ --header 'Content-Type: application/json' \ @@ -129,7 +129,7 @@ This check requires that the previous step ended on the successful page and didn #### Request ```bash curl --request POST \ - --url https://$ZITADEL_DOMAIN/v2beta/sessions \ + --url https://$ZITADEL_DOMAIN/v2/sessions \ --header 'Accept: application/json' \ --header 'Authorization: Bearer '"$TOKEN"''\ @@ -160,7 +160,7 @@ The display name is used to list the linkings on the users. #### Request ```bash curl --request POST \ - --url https://$ZITADEL_DOMAIN/v2beta/users/human \ + --url https://$ZITADEL_DOMAIN/v2/users/human \ --header 'Accept: application/json' \ --header 'Authorization: Bearer '"$TOKEN"''\ --header 'Content-Type: application/json' \ @@ -198,7 +198,7 @@ If you want to link/connect to an existing account you can perform the add ident #### Request ```bash curl --request POST \ - --url https://$ZITADEL_DOMAIN/v2beta/users/users/218385419895570689/links \ + --url https://$ZITADEL_DOMAIN/v2/users/users/218385419895570689/links \ --header 'Accept: application/json' \ --header 'Authorization: Bearer '"$TOKEN"''\ --header 'Content-Type: application/json' \ diff --git a/docs/docs/guides/integrate/login-ui/mfa.mdx b/docs/docs/guides/integrate/login-ui/mfa.mdx index 8f6fe60e37..accf27398d 100644 --- a/docs/docs/guides/integrate/login-ui/mfa.mdx +++ b/docs/docs/guides/integrate/login-ui/mfa.mdx @@ -41,7 +41,7 @@ Request Example: ```bash curl --request POST \ - --url https://$ZITADEL_DOMAIN/v2beta/users/$USER_ID/totp \ + --url https://$ZITADEL_DOMAIN/v2/users/$USER_ID/totp \ --header 'Accept: application/json' \ --header 'Authorization: Bearer '"$TOKEN"'' --header 'Content-Type: application/json' \ @@ -73,7 +73,7 @@ Request Example: ```bash curl --request POST \ - --url https://$ZITADEL_DOMAIN/v2beta/users/$USER_ID/totp/verify \ + --url https://$ZITADEL_DOMAIN/v2/users/$USER_ID/totp/verify \ --header 'Accept: application/json' \ --header 'Authorization: Bearer '"$TOKEN"'' --header 'Content-Type: application/json' \ @@ -99,7 +99,7 @@ Example Request ```bash curl --request POST \ - --url https://$ZITADEL_DOMAIN/v2beta/sessions \ + --url https://$ZITADEL_DOMAIN/v2/sessions \ --header 'Accept: application/json' \ --header 'Authorization: Bearer '"$TOKEN"'' \ --header 'Content-Type: application/json' \ @@ -136,7 +136,7 @@ More detailed information about the API: [Update session Documentation](/apis/re Example Request ```bash curl --request PATCH \ - --url https://$ZITADEL_DOMAIN/v2beta/sessions/$SESSION-ID \ + --url https://$ZITADEL_DOMAIN/v2/sessions/$SESSION-ID \ --header 'Accept: application/json' \ --header 'Content-Type: application/json' \ --data '{ @@ -175,7 +175,7 @@ Example Request: ```bash curl --request POST \ - --url https://$ZITADEL_DOMAIN/v2beta/users/$USER-ID/phone \ + --url https://$ZITADEL_DOMAIN/v2/users/$USER-ID/phone \ --header 'Accept: application/json' \ --header 'Authorization: Bearer '"$TOKEN"'' \ --header 'Content-Type: application/json' \ @@ -195,7 +195,7 @@ More detailed information about the API: [Verify phone](/apis/resources/user_ser Example Request: ```bash curl --request POST \ - --url https://$ZITADEL_DOMAIN/v2beta/users/$USER-ID/phone/verify \ + --url https://$ZITADEL_DOMAIN/v2/users/$USER-ID/phone/verify \ --header 'Accept: application/json' \ --header 'Authorization: Bearer '"$TOKEN"'' \ --header 'Content-Type: application/json' \ @@ -213,7 +213,7 @@ More detailed information about the API: [Add OTP SMS for a user](/apis/resource Example Request: ```bash curl --request POST \ - --url https://$ZITADEL_DOMAIN/v2beta/users/$USER-ID/otp_sms \ + --url https://$ZITADEL_DOMAIN/v2/users/$USER-ID/otp_sms \ --header 'Accept: application/json' \ --header 'Authorization: Bearer '"$TOKEN"'' \ --header 'Content-Type: application/json' @@ -237,7 +237,7 @@ Example Request ```bash curl --request POST \ - --url https://$ZITADEL_DOMAIN/v2beta/sessions \ + --url https://$ZITADEL_DOMAIN/v2/sessions \ --header 'Accept: application/json' \ --header 'Authorization: Bearer '"$TOKEN"'' \ --header 'Content-Type: application/json' \ @@ -264,7 +264,7 @@ Example Request ```bash curl --request PATCH \ - --url https://$ZITADEL_DOMAIN/v2beta/sessions/225307381909694507 \ + --url https://$ZITADEL_DOMAIN/v2/sessions/225307381909694507 \ --header 'Accept: application/json' \ --header 'Authorization: Bearer '"$TOKEN"'' \ --header 'Content-Type: application/json' \ @@ -301,7 +301,7 @@ More detailed information about the API: [Add OTP Email for a user](/apis/resour Example Request: ```bash curl --request POST \ - --url https://$ZITADEL_DOMAIN/v2beta/users/$USER-ID/otp_email \ + --url https://$ZITADEL_DOMAIN/v2/users/$USER-ID/otp_email \ --header 'Accept: application/json' \ --header 'Authorization: Bearer '"$TOKEN"'' \ --header 'Content-Type: application/json' @@ -325,7 +325,7 @@ Example Request ```bash curl --request POST \ - --url https://$ZITADEL_DOMAIN/v2beta/sessions \ + --url https://$ZITADEL_DOMAIN/v2/sessions \ --header 'Accept: application/json' \ --header 'Authorization: Bearer '"$TOKEN"'' \ --header 'Content-Type: application/json' \ @@ -352,7 +352,7 @@ Example Request ```bash curl --request PATCH \ - --url https://$ZITADEL_DOMAIN/v2beta/sessions/225307381909694507 \ + --url https://$ZITADEL_DOMAIN/v2/sessions/225307381909694507 \ --header 'Accept: application/json' \ --header 'Authorization: Bearer '"$TOKEN"'' \ --header 'Content-Type: application/json' \ @@ -386,7 +386,7 @@ Request Example: ```bash curl --request POST \ - --url https://$ZITADEL_DOMAIN/v2beta/users/$USER_ID/u2f \ + --url https://$ZITADEL_DOMAIN/v2/users/$USER_ID/u2f \ --header 'Accept: application/json' \ --header 'Authorization: Bearer '"$TOKEN"'' --header 'Content-Type: application/json' \ @@ -457,7 +457,7 @@ Example Request: ```bash curl --request POST \ - --url https://$ZITADEL_DOMAIN/v2beta/users/$USER_ID/u2f/$PASSKEY_ID \ + --url https://$ZITADEL_DOMAIN/v2/users/$USER_ID/u2f/$PASSKEY_ID \ --header 'Accept: application/json' \ --header 'Authorization: Bearer '"$TOKEN"''\ --header 'Content-Type: application/json' \ @@ -497,7 +497,7 @@ Example Request ```bash curl --request POST \ - --url https://$ZITADEL_DOMAIN/v2beta/sessions \ + --url https://$ZITADEL_DOMAIN/v2/sessions \ --header 'Accept: application/json' \ --header 'Authorization: Bearer '"$TOKEN"'' \ --header 'Content-Type: application/json' \ diff --git a/docs/docs/guides/integrate/login-ui/oidc-standard.mdx b/docs/docs/guides/integrate/login-ui/oidc-standard.mdx index 72dad88d11..2f5ba563df 100644 --- a/docs/docs/guides/integrate/login-ui/oidc-standard.mdx +++ b/docs/docs/guides/integrate/login-ui/oidc-standard.mdx @@ -55,7 +55,7 @@ With the ID from the redirect before you will now be able to get the information ```bash curl --request GET \ - --url https://$ZITADEL_DOMAIN/v2beta/oidc/auth_requests/V2_224908753244265546 \ + --url https://$ZITADEL_DOMAIN/v2/oidc/auth_requests/V2_224908753244265546 \ --header 'Authorization: Bearer '"$TOKEN"''\ ``` @@ -100,7 +100,7 @@ Read more about the [Finalize Auth Request Documentation](/docs/apis/resources/o Make sure that the authorization header is from the same account that you originally sent in the client id header ```x-zitadel-login-client: ``` on the authorize endpoint. ```bash curl --request POST \ - --url $ZITADEL_DOMAIN/v2beta/oidc/auth_requests/V2_224908753244265546 \ + --url $ZITADEL_DOMAIN/v2/oidc/auth_requests/V2_224908753244265546 \ --header 'Accept: application/json' \ --header 'Authorization: Bearer '"$TOKEN"''\ --header 'Content-Type: application/json' \ diff --git a/docs/docs/guides/integrate/login-ui/passkey.mdx b/docs/docs/guides/integrate/login-ui/passkey.mdx index 06f0049e86..112ba207e8 100644 --- a/docs/docs/guides/integrate/login-ui/passkey.mdx +++ b/docs/docs/guides/integrate/login-ui/passkey.mdx @@ -34,7 +34,7 @@ Send either the sendLink or the returnCode (empty message) in the request body, ```bash curl --request POST \ - --url https://$ZITADEL_DOMAIN/v2beta/users/$USER_ID/passkeys/registration_link \ + --url https://$ZITADEL_DOMAIN/v2/users/$USER_ID/passkeys/registration_link \ --header 'Accept: application/json' \ --header 'Authorization: Bearer '"$TOKEN"''\ --header 'Content-Type: application/json' \ @@ -81,7 +81,7 @@ The code only has to be filled if the user did get a registration code. ```bash curl --request POST \ - --url https://$ZITADEL_DOMAIN/v2beta/users/$USER_ID/passkeys \ + --url https://$ZITADEL_DOMAIN/v2/users/$USER_ID/passkeys \ --header 'Accept: application/json' \ --header 'Authorization: Bearer '"$TOKEN"''\ --header 'Content-Type: application/json' \ @@ -185,7 +185,7 @@ Example Request: ```bash curl --request POST \ - --url https://$ZITADEL_DOMAIN/v2beta/users/$USER_ID/passkeys/$PASSKEY_ID \ + --url https://$ZITADEL_DOMAIN/v2/users/$USER_ID/passkeys/$PASSKEY_ID \ --header 'Accept: application/json' \ --header 'Authorization: Bearer '"$TOKEN"''\ --header 'Content-Type: application/json' \ @@ -223,7 +223,7 @@ More detailed information about the API: [Create Session Documentation](/apis/re Example Request: ```bash curl --request POST \ - --url https://$ZITADEL_DOMAIN/v2beta/sessions \ + --url https://$ZITADEL_DOMAIN/v2/sessions \ --header 'Accept: application/json' \ --header 'Authorization: Bearer '"$TOKEN"''\ --header 'Content-Type: application/json' \ diff --git a/docs/docs/guides/integrate/login-ui/password-reset.mdx b/docs/docs/guides/integrate/login-ui/password-reset.mdx index 9e4ad89cc9..b5caae38b1 100644 --- a/docs/docs/guides/integrate/login-ui/password-reset.mdx +++ b/docs/docs/guides/integrate/login-ui/password-reset.mdx @@ -29,7 +29,7 @@ Make sure to also include the URL Template to customize the reset link in the em ```bash curl --request POST \ - --url https://$ZITADEL_DOMAIN/v2beta/users/$USER_ID/password_reset \ + --url https://$ZITADEL_DOMAIN/v2/users/$USER_ID/password_reset \ --header 'Accept: application/json' \ --header 'Authorization: Bearer '"$TOKEN"'' \ --header 'Content-Type: application/json' \ @@ -48,7 +48,7 @@ Send the request with asking for the return Code in the body of the request. #### Request ```bash curl --request POST \ - --url https://$ZITADEL_DOMAIN/v2beta/users/$USER_ID/password_reset \ + --url https://$ZITADEL_DOMAIN/v2/users/$USER_ID/password_reset \ --header 'Accept: application/json' \ --header 'Authorization: Bearer '"$TOKEN"'' \ --header 'Content-Type: application/json' \ @@ -96,7 +96,7 @@ In this case it requires additionally the current password instead of the verifi ```bash curl --request POST \ - --url https://$ZITADEL_DOMAIN/v2beta/users/$USER_ID/password \ + --url https://$ZITADEL_DOMAIN/v2/users/$USER_ID/password \ --header 'Accept: application/json' \ --header 'Authorization: Bearer '"$TOKEN"'' \ --header 'Content-Type: application/json' \ diff --git a/docs/docs/guides/integrate/login-ui/username-password.mdx b/docs/docs/guides/integrate/login-ui/username-password.mdx index 5c9ab54cdd..d6b913ea77 100644 --- a/docs/docs/guides/integrate/login-ui/username-password.mdx +++ b/docs/docs/guides/integrate/login-ui/username-password.mdx @@ -23,7 +23,7 @@ Read more about the metadata [here](/docs/guides/manage/customize/user-metadata) ```bash curl --request POST \ - --url https://$ZITADEL_DOMAIN/v2beta/users/human \ + --url https://$ZITADEL_DOMAIN/v2/users/human \ --header 'Accept: application/json' \ --header 'Authorization: Bearer '"$TOKEN"'' \ --header 'Content-Type: application/json' \ @@ -111,7 +111,7 @@ Send it to the Get Session Endpoint to find out how the user has authenticated. ```bash curl --request POST \ - --url https://$ZITADEL_DOMAIN/v2beta/sessions \ + --url https://$ZITADEL_DOMAIN/v2/sessions \ --header 'Accept: application/json' \ --header 'Authorization: Bearer '"$TOKEN"'' \ --header 'Content-Type: application/json' \ @@ -176,7 +176,7 @@ To update an existing session, add the session ID you got in the previous step t ```bash curl --request PATCH \ - --url https://$ZITADEL_DOMAIN/v2beta/sessions/$SESSION_ID \ + --url https://$ZITADEL_DOMAIN/v2/sessions/$SESSION_ID \ --header 'Accept: application/json' \ --header 'Authorization: Bearer '"$TOKEN"''\ --header 'Content-Type: application/json' \ diff --git a/docs/docs/guides/integrate/token-exchange.mdx b/docs/docs/guides/integrate/token-exchange.mdx index 0dafd50a9e..ab3ee26f48 100644 --- a/docs/docs/guides/integrate/token-exchange.mdx +++ b/docs/docs/guides/integrate/token-exchange.mdx @@ -188,7 +188,7 @@ These preparation steps are needed for all Token Exchange interaction, including As Token Exchange is still a beta feature, the feature needs to be enabled for your instance by an `IAM_OWNER` first: ```bash -curl -L -X PUT 'https://$CUSTOM-DOMAIN/v2beta/features/instance' \ +curl -L -X PUT 'https://$CUSTOM-DOMAIN/v2/features/instance' \ -H 'Content-Type: application/json' \ -H 'Accept: application/json' \ -H 'Authorization: Bearer ' \ diff --git a/docs/docs/legal/service-description/billing.md b/docs/docs/legal/service-description/billing.md index 8da78e839d..fc3e94587f 100644 --- a/docs/docs/legal/service-description/billing.md +++ b/docs/docs/legal/service-description/billing.md @@ -49,6 +49,7 @@ Management endpoints: - /zitadel.* - /v2alpha* - /v2beta* +- /v2* - /admin* - /management* - /system* diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js index 183ac67b64..c7378c63ec 100644 --- a/docs/docusaurus.config.js +++ b/docs/docusaurus.config.js @@ -284,39 +284,39 @@ module.exports = { categoryLinkSource: "tag", }, }, - user: { - specPath: ".artifacts/openapi/zitadel/user/v2beta/user_service.swagger.json", - outputDir: "docs/apis/resources/user_service", + user_v2: { + specPath: ".artifacts/openapi/zitadel/user/v2/user_service.swagger.json", + outputDir: "docs/apis/resources/user_service_v2", sidebarOptions: { groupPathsBy: "tag", - categoryLinkSource: "auto", + categoryLinkSource: "tag", }, }, - session: { - specPath: ".artifacts/openapi/zitadel/session/v2beta/session_service.swagger.json", - outputDir: "docs/apis/resources/session_service", + session_v2: { + specPath: ".artifacts/openapi/zitadel/session/v2/session_service.swagger.json", + outputDir: "docs/apis/resources/session_service_v2", sidebarOptions: { groupPathsBy: "tag", - categoryLinkSource: "auto", + categoryLinkSource: "tag", }, }, - oidc: { - specPath: ".artifacts/openapi/zitadel/oidc/v2beta/oidc_service.swagger.json", - outputDir: "docs/apis/resources/oidc_service", + oidc_v2: { + specPath: ".artifacts/openapi/zitadel/oidc/v2/oidc_service.swagger.json", + outputDir: "docs/apis/resources/oidc_service_v2", sidebarOptions: { groupPathsBy: "tag", - categoryLinkSource: "auto", + categoryLinkSource: "tag", }, }, - settings: { - specPath: ".artifacts/openapi/zitadel/settings/v2beta/settings_service.swagger.json", - outputDir: "docs/apis/resources/settings_service", + settings_v2: { + specPath: ".artifacts/openapi/zitadel/settings/v2/settings_service.swagger.json", + outputDir: "docs/apis/resources/settings_service_v2", sidebarOptions: { groupPathsBy: "tag", - categoryLinkSource: "auto", + categoryLinkSource: "tag", }, }, - user_schema: { + user_schema_v3: { specPath: ".artifacts/openapi/zitadel/user/schema/v3alpha/user_schema_service.swagger.json", outputDir: "docs/apis/resources/user_schema_service_v3", sidebarOptions: { @@ -341,7 +341,7 @@ module.exports = { }, }, feature_v2: { - specPath: ".artifacts/openapi/zitadel/feature/v2beta/feature_service.swagger.json", + specPath: ".artifacts/openapi/zitadel/feature/v2/feature_service.swagger.json", outputDir: "docs/apis/resources/feature_service_v2", sidebarOptions: { groupPathsBy: "tag", diff --git a/docs/sidebars.js b/docs/sidebars.js index 8dbf546bc6..2c377ea48f 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -5,7 +5,7 @@ module.exports = { type: "category", label: "Get Started", collapsed: false, - link: { type: "doc", id: "guides/start/quickstart" }, + link: {type: "doc", id: "guides/start/quickstart"}, items: [ "guides/start/quickstart", { @@ -52,7 +52,7 @@ module.exports = { { type: "category", label: "Examples & SDKs", - link: { type: "doc", id: "sdk-examples/introduction" }, + link: {type: "doc", id: "sdk-examples/introduction"}, items: [ "sdk-examples/introduction", "sdk-examples/angular", @@ -232,7 +232,7 @@ module.exports = { "guides/integrate/login/oidc/logout", ], }, - "guides/integrate/login/saml", + "guides/integrate/login/saml", ], }, { @@ -548,7 +548,7 @@ module.exports = { items: [ { type: "category", - label: "V1 (General Available)", + label: "V1 (Generally Available)", collapsed: false, link: { type: "generated-index", @@ -612,7 +612,7 @@ module.exports = { }, { type: "category", - label: "V2 (Beta)", + label: "V2 (Generally Available)", collapsed: false, link: { type: "doc", @@ -621,71 +621,61 @@ module.exports = { items: [ { type: "category", - label: "User Lifecycle (Beta)", + label: "User Lifecycle", link: { type: "generated-index", - title: "User Service API (Beta)", - slug: "/apis/resources/user_service", + title: "User Service API", + slug: "/apis/resources/user_service_v2", description: - "This API is intended to manage users in a ZITADEL instance.\n" + - "\n" + - "This project is in beta state. It can AND will continue breaking until the services provide the same functionality as the current login.", + "This API is intended to manage users in a ZITADEL instance.\n" }, - items: require("./docs/apis/resources/user_service/sidebar.ts"), + items: require("./docs/apis/resources/user_service_v2/sidebar.ts"), }, { type: "category", - label: "Session Lifecycle (Beta)", + label: "Session Lifecycle", link: { type: "generated-index", - title: "Session Service API (Beta)", - slug: "/apis/resources/session_service", + title: "Session Service API", + slug: "/apis/resources/session_service_v2", description: - "This API is intended to manage sessions in a ZITADEL instance.\n" + - "\n" + - "This project is in beta state. It can AND will continue breaking until the services provide the same functionality as the current login.", + "This API is intended to manage sessions in a ZITADEL instance.\n" }, - items: require("./docs/apis/resources/session_service/sidebar.ts"), + items: require("./docs/apis/resources/session_service_v2/sidebar.ts"), }, { type: "category", - label: "OIDC Lifecycle (Beta)", + label: "OIDC Lifecycle", link: { type: "generated-index", - title: "OIDC Service API (Beta)", - slug: "/apis/resources/oidc_service", + title: "OIDC Service API", + slug: "/apis/resources/oidc_service_v2", description: - "Get OIDC Auth Request details and create callback URLs.\n" + - "\n" + - "This project is in beta state. It can AND will continue breaking until the services provide the same functionality as the current login.", + "Get OIDC Auth Request details and create callback URLs.\n" }, - items: require("./docs/apis/resources/oidc_service/sidebar.ts"), + items: require("./docs/apis/resources/oidc_service_v2/sidebar.ts"), }, { type: "category", - label: "Settings Lifecycle (Beta)", + label: "Settings Lifecycle", link: { type: "generated-index", - title: "Settings Service API (Beta)", - slug: "/apis/resources/settings_service", + title: "Settings Service API", + slug: "/apis/resources/settings_service_v2", description: - "This API is intended to manage settings in a ZITADEL instance.\n" + - "\n" + - "This project is in beta state. It can AND will continue to break until the services provide the same functionality as the current login.", + "This API is intended to manage settings in a ZITADEL instance.\n" }, - items: require("./docs/apis/resources/settings_service/sidebar.ts"), + items: require("./docs/apis/resources/settings_service_v2/sidebar.ts"), }, { type: "category", - label: "Feature Lifecycle (Beta)", + label: "Feature Lifecycle", link: { type: "generated-index", - title: "Feature Service API (Beta)", - slug: "/apis/resources/feature_service", + title: "Feature Service API", + slug: "/apis/resources/feature_service/v2", description: - 'This API is intended to manage features for ZITADEL. Feature settings that are available on multiple "levels", such as instance and organization. The higher level instance acts as a default for the lower level. When a feature is set on multiple levels, the lower level takes precedence. Features can be experimental where ZITADEL will assume a sane default, such as disabled. When over time confidence in such a feature grows, ZITADEL can default to enabling the feature. As a final step we might choose to always enable a feature and remove the setting from this API, reserving the proto field number. Such removal is not considered a breaking change. Setting a removed field will effectively result in a no-op.\n' + - "\n" + - "This project is in beta state. It can AND will continue breaking until a stable version is released.", + 'This API is intended to manage features for ZITADEL. Feature settings that are available on multiple "levels", such as instance and organization. The higher level instance acts as a default for the lower level. When a feature is set on multiple levels, the lower level takes precedence. Features can be experimental where ZITADEL will assume a sane default, such as disabled. When over time confidence in such a feature grows, ZITADEL can default to enabling the feature. As a final step we might choose to always enable a feature and remove the setting from this API, reserving the proto field number. Such removal is not considered a breaking change. Setting a removed field will effectively result in a no-op.\n' }, items: require("./docs/apis/resources/feature_service_v2/sidebar.ts"), }, diff --git a/proto/zitadel/feature/v2/feature.proto b/proto/zitadel/feature/v2/feature.proto new file mode 100644 index 0000000000..3249248735 --- /dev/null +++ b/proto/zitadel/feature/v2/feature.proto @@ -0,0 +1,68 @@ +syntax = "proto3"; + +package zitadel.feature.v2; + +import "protoc-gen-openapiv2/options/annotations.proto"; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/feature/v2;feature"; + + +enum Source { + SOURCE_UNSPECIFIED = 0; + reserved 1; // in case we want to implement a "DEFAULT" level + SOURCE_SYSTEM = 2; + SOURCE_INSTANCE = 3; + SOURCE_ORGANIZATION = 4; + SOURCE_PROJECT = 5; // reserved for future use + SOURCE_APP = 6; // reserved for future use + SOURCE_USER = 7; +} + +// FeatureFlag is a simple boolean Feature setting, without further payload. +message FeatureFlag { + bool enabled = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "false"; + description: "Whether a feature is enabled."; + } + ]; + + Source source = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "The source where the setting of the feature was defined. The source may be the resource itself or a resource owner through inheritance."; + } + ]; +} + +message ImprovedPerformanceFeatureFlag { + repeated ImprovedPerformance execution_paths = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "[1]"; + description: "Which of the performance improvements is enabled"; + } + ]; + + Source source = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "The source where the setting of the feature was defined. The source may be the resource itself or a resource owner through inheritance."; + } + ]; +} + +enum ImprovedPerformance { + IMPROVED_PERFORMANCE_UNSPECIFIED = 0; + // Uses the eventstore to query the org by id + // instead of the sql table. + IMPROVED_PERFORMANCE_ORG_BY_ID = 1; + // Improves performance on write side by using + // optimized processes to query data to determine + // correctnes of data. + IMPROVED_PERFORMANCE_PROJECT_GRANT = 2; + IMPROVED_PERFORMANCE_PROJECT = 3; + IMPROVED_PERFORMANCE_USER_GRANT = 4; + + // Improve performance on write side when + // users are checked against verified domains + // from other organizations. + IMPROVED_PERFORMANCE_ORG_DOMAIN_VERIFIED = 5; +} \ No newline at end of file diff --git a/proto/zitadel/feature/v2/feature_service.proto b/proto/zitadel/feature/v2/feature_service.proto new file mode 100644 index 0000000000..a89a182632 --- /dev/null +++ b/proto/zitadel/feature/v2/feature_service.proto @@ -0,0 +1,395 @@ +syntax = "proto3"; + +package zitadel.feature.v2; + +import "google/api/annotations.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; + +import "zitadel/feature/v2/system.proto"; +import "zitadel/feature/v2/instance.proto"; +import "zitadel/feature/v2/organization.proto"; +import "zitadel/feature/v2/user.proto"; +import "zitadel/protoc_gen_zitadel/v2/options.proto"; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/feature/v2;feature"; + +option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { + info: { + title: "Feature Service"; + version: "2.0"; + description: "This API is intended to manage features for ZITADEL. Feature settings that are available on multiple \"levels\", such as instance and organization. The higher level instance acts as a default for the lower level. When a feature is set on multiple levels, the lower level takes precedence. Features can be experimental where ZITADEL will assume a sane default, such as disabled. When over time confidence in such a feature grows, ZITADEL can default to enabling the feature. As a final step we might choose to always enable a feature and remove the setting from this API, reserving the proto field number. Such removal is not considered a breaking change. Setting a removed field will effectively result in a no-op."; + contact:{ + name: "ZITADEL" + url: "https://zitadel.com" + email: "hi@zitadel.com" + } + license: { + name: "Apache 2.0", + url: "https://github.com/zitadel/zitadel/blob/main/LICENSE"; + }; + }; + 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 has no feature flag settings and inheritance from the parent is disabled."; + schema: { + json_schema: { + ref: "#/definitions/rpcStatus"; + } + } + } + } +}; + +// FeatureService is intended to manage features for ZITADEL. +// +// Feature settings that are available on multiple "levels", such as instance and organization. +// The higher level (instance) acts as a default for the lower level (organization). +// When a feature is set on multiple levels, the lower level takes precedence. +// +// Features can be experimental where ZITADEL will assume a sane default, such as disabled. +// When over time confidence in such a feature grows, ZITADEL can default to enabling the feature. +// As a final step we might choose to always enable a feature and remove the setting from this API, +// reserving the proto field number. Such removal is not considered a breaking change. +// Setting a removed field will effectively result in a no-op. +service FeatureService { + rpc SetSystemFeatures (SetSystemFeaturesRequest) returns (SetSystemFeaturesResponse) { + option (google.api.http) = { + put: "/v2/features/system" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "system.feature.write" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + summary: "Set system level features"; + description: "Configure and set features that apply to the complete system. Only fields present in the request are set or unset." + responses: { + key: "200" + value: { + description: "OK"; + } + }; + }; + } + + rpc ResetSystemFeatures (ResetSystemFeaturesRequest) returns (ResetSystemFeaturesResponse) { + option (google.api.http) = { + delete: "/v2/features/system" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "system.feature.delete" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + summary: "Reset system level features"; + description: "Deletes ALL configured features for the system, reverting the behaviors to system defaults." + responses: { + key: "200" + value: { + description: "OK"; + } + }; + }; + } + + rpc GetSystemFeatures (GetSystemFeaturesRequest) returns (GetSystemFeaturesResponse) { + option (google.api.http) = { + get: "/v2/features/system" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "system.feature.read" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + summary: "Get system level features"; + description: "Returns all configured features for the system. Unset fields mean the feature is the current system default." + responses: { + key: "200" + value: { + description: "OK"; + } + }; + }; + } + + rpc SetInstanceFeatures (SetInstanceFeaturesRequest) returns (SetInstanceFeaturesResponse) { + option (google.api.http) = { + put: "/v2/features/instance" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "iam.feature.write" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + summary: "Set instance level features"; + description: "Configure and set features that apply to a complete instance. Only fields present in the request are set or unset." + responses: { + key: "200" + value: { + description: "OK"; + } + }; + }; + } + + rpc ResetInstanceFeatures (ResetInstanceFeaturesRequest) returns (ResetInstanceFeaturesResponse) { + option (google.api.http) = { + delete: "/v2/features/instance" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "iam.feature.delete" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + summary: "Reset instance level features"; + description: "Deletes ALL configured features for an instance, reverting the behaviors to system defaults." + responses: { + key: "200" + value: { + description: "OK"; + } + }; + }; + } + + rpc GetInstanceFeatures (GetInstanceFeaturesRequest) returns (GetInstanceFeaturesResponse) { + option (google.api.http) = { + get: "/v2/features/instance" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "iam.feature.read" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + summary: "Get instance level features"; + description: "Returns all configured features for an instance. Unset fields mean the feature is the current system default." + responses: { + key: "200" + value: { + description: "OK"; + } + }; + }; + } + + rpc SetOrganizationFeatures (SetOrganizationFeaturesRequest) returns (SetOrganizationFeaturesResponse) { + option (google.api.http) = { + put: "/v2/features/organization/{organization_id}" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "org.feature.write" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + summary: "Set organization level features"; + description: "Configure and set features that apply to a complete instance. Only fields present in the request are set or unset." + responses: { + key: "200" + value: { + description: "OK"; + } + }; + }; + } + + rpc ResetOrganizationFeatures (ResetOrganizationFeaturesRequest) returns (ResetOrganizationFeaturesResponse) { + option (google.api.http) = { + delete: "/v2/features/organization/{organization_id}" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "org.feature.write" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + summary: "Reset organization level features"; + description: "Deletes ALL configured features for an organization, reverting the behaviors to instance defaults." + responses: { + key: "200" + value: { + description: "OK"; + } + }; + }; + } + + rpc GetOrganizationFeatures(GetOrganizationFeaturesRequest) returns (GetOrganizationFeaturesResponse) { + option (google.api.http) = { + get: "/v2/features/organization/{organization_id}" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "org.feature.read" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + summary: "Get organization level features"; + description: "Returns all configured features for an organization. Unset fields mean the feature is the current instance default." + responses: { + key: "200" + value: { + description: "OK"; + } + }; + }; + } + + rpc SetUserFeatures(SetUserFeatureRequest) returns (SetUserFeaturesResponse) { + option (google.api.http) = { + put: "/v2/features/user/{user_id}" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "user.feature.write" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + summary: "Set user level features"; + description: "Configure and set features that apply to an user. Only fields present in the request are set or unset." + responses: { + key: "200" + value: { + description: "OK"; + } + }; + }; + } + + rpc ResetUserFeatures(ResetUserFeaturesRequest) returns (ResetUserFeaturesResponse) { + option (google.api.http) = { + delete: "/v2/features/user/{user_id}" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "user.feature.write" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + summary: "Reset user level features"; + description: "Deletes ALL configured features for a user, reverting the behaviors to organization defaults." + responses: { + key: "200" + value: { + description: "OK"; + } + }; + }; + } + + rpc GetUserFeatures(GetUserFeaturesRequest) returns (GetUserFeaturesResponse) { + option (google.api.http) = { + get: "/v2/features/user/{user_id}" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "user.feature.read" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + summary: "Get organization level features"; + description: "Returns all configured features for an organization. Unset fields mean the feature is the current instance default." + responses: { + key: "200" + value: { + description: "OK"; + } + }; + }; + } +} diff --git a/proto/zitadel/feature/v2/instance.proto b/proto/zitadel/feature/v2/instance.proto new file mode 100644 index 0000000000..52b28f2101 --- /dev/null +++ b/proto/zitadel/feature/v2/instance.proto @@ -0,0 +1,132 @@ +syntax = "proto3"; + +package zitadel.feature.v2; + +import "protoc-gen-openapiv2/options/annotations.proto"; +import "validate/validate.proto"; + +import "zitadel/object/v2/object.proto"; +import "zitadel/feature/v2/feature.proto"; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/feature/v2;feature"; + +message SetInstanceFeaturesRequest{ + optional bool login_default_org = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "true"; + description: "The login UI will use the settings of the default org (and not from the instance) if no organization context is set"; + } + ]; + optional bool oidc_trigger_introspection_projections = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "true"; + description: "Enable projection triggers during an introspection request. This can act as workaround if there are noticeable consistency issues in the introspection response but can have an impact on performance. We are planning to remove triggers for introspection requests in the future. Please raise an issue if you needed to enable this feature."; + } + ]; + optional bool oidc_legacy_introspection = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "true"; + description: "We have recently refactored the introspection endpoint for performance reasons. This feature can be used to rollback to the legacy implementation if unexpected bugs arise. Please raise an issue if you needed to enable this feature."; + } + ]; + + optional bool user_schema = 4 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "true"; + description: "User Schemas allow to manage data schemas of user. If the flag is enabled, you'll be able to use the new API and its features. Note that it is still in an early stage."; + } + ]; + + optional bool oidc_token_exchange = 5 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "true"; + description: "Enable the experimental `urn:ietf:params:oauth:grant-type:token-exchange` grant type for the OIDC token endpoint. Token exchange can be used to request tokens with a lesser scope or impersonate other users. See the security policy to allow impersonation on an instance."; + } + ]; + optional bool actions = 6 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "true"; + description: "Actions allow to manage data executions and targets. If the flag is enabled, you'll be able to use the new API and its features. Note that it is still in an early stage."; + } + ]; + + repeated ImprovedPerformance improved_performance = 7 [ + (validate.rules).repeated.unique = true, + (validate.rules).repeated.items.enum = {defined_only: true, not_in: [0]}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "[1]"; + description: "Improves performance of specified execution paths."; + } + ]; +} + +message SetInstanceFeaturesResponse { + zitadel.object.v2.Details details = 1; +} + +message ResetInstanceFeaturesRequest {} + +message ResetInstanceFeaturesResponse { + zitadel.object.v2.Details details = 1; +} + +message GetInstanceFeaturesRequest { + bool inheritance = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "true"; + description: "Inherit unset features from the resource owners. This option is recursive: if the flag is set, the resource's ancestors are consulted up to system defaults. If this option is disabled and the feature is not set on the instance, it will be omitted from the response or Not Found is returned when the instance has no features flags at all."; + } + ]; +} + +message GetInstanceFeaturesResponse { + zitadel.object.v2.Details details = 1; + FeatureFlag login_default_org = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "true"; + description: "The login UI will use the settings of the default org (and not from the instance) if no organization context is set"; + } + ]; + + FeatureFlag oidc_trigger_introspection_projections = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "true"; + description: "Enable projection triggers during an introspection request. This can act as workaround if there are noticeable consistency issues in the introspection response but can have an impact on performance. We are planning to remove triggers for introspection requests in the future. Please raise an issue if you needed to enable this feature."; + } + ]; + + FeatureFlag oidc_legacy_introspection = 4 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "true"; + description: "We have recently refactored the introspection endpoint for performance reasons. This feature can be used to rollback to the legacy implementation if unexpected bugs arise. Please raise an issue if you needed to enable this feature."; + } + ]; + + FeatureFlag user_schema = 5 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "true"; + description: "User Schemas allow to manage data schemas of user. If the flag is enabled, you'll be able to use the new API and its features. Note that it is still in an early stage."; + } + ]; + + FeatureFlag oidc_token_exchange = 6 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "true"; + description: "Enable the experimental `urn:ietf:params:oauth:grant-type:token-exchange` grant type for the OIDC token endpoint. Token exchange can be used to request tokens with a lesser scope or impersonate other users. See the security policy to allow impersonation on an instance."; + } + ]; + + FeatureFlag actions = 7 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "true"; + description: "Actions v2 allow to manage data executions and targets. If the flag is enabled, you'll be able to use the new API and its features. Note that it is still in an early stage."; + } + ]; + + ImprovedPerformanceFeatureFlag improved_performance = 8 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "[1]"; + description: "Improves performance of specified execution paths."; + } + ]; +} diff --git a/proto/zitadel/feature/v2/organization.proto b/proto/zitadel/feature/v2/organization.proto new file mode 100644 index 0000000000..919b9114cf --- /dev/null +++ b/proto/zitadel/feature/v2/organization.proto @@ -0,0 +1,62 @@ +syntax = "proto3"; + +package zitadel.feature.v2; + +import "protoc-gen-openapiv2/options/annotations.proto"; +import "validate/validate.proto"; + +import "zitadel/object/v2/object.proto"; +import "zitadel/feature/v2/feature.proto"; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/feature/v2;feature"; + +message SetOrganizationFeaturesRequest { + 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: "\"69629023906488334\""; + } + ]; +} + +message SetOrganizationFeaturesResponse { + zitadel.object.v2.Details details = 1; +} + +message ResetOrganizationFeaturesRequest { + 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: "\"69629023906488334\""; + } + ]; +} + +message ResetOrganizationFeaturesResponse { + zitadel.object.v2.Details details = 1; +} + +message GetOrganizationFeaturesRequest { + 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: "\"69629023906488334\""; + } + ]; + bool inheritance = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "true"; + description: "Inherit unset features from the resource owners. This option is recursive: if the flag is set, the resource's ancestors are consulted up to system defaults. If this option is disabled and the feature is not set on the organization, it will be omitted from the response or Not Found is returned when the organization has no features flags at all."; + } + ]; +} + +message GetOrganizationFeaturesResponse { + zitadel.object.v2.Details details = 1; +} diff --git a/proto/zitadel/feature/v2/system.proto b/proto/zitadel/feature/v2/system.proto new file mode 100644 index 0000000000..48b3ff56d1 --- /dev/null +++ b/proto/zitadel/feature/v2/system.proto @@ -0,0 +1,128 @@ +syntax = "proto3"; + +package zitadel.feature.v2; + +import "protoc-gen-openapiv2/options/annotations.proto"; +import "validate/validate.proto"; + +import "zitadel/object/v2/object.proto"; +import "zitadel/feature/v2/feature.proto"; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/feature/v2;feature"; + +message SetSystemFeaturesRequest{ + optional bool login_default_org = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "true"; + description: "The login UI will use the settings of the default org (and not from the instance) if no organization context is set"; + } + ]; + + optional bool oidc_trigger_introspection_projections = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "true"; + description: "Enable projection triggers during an introspection request. This can act as workaround if there are noticeable consistency issues in the introspection response but can have an impact on performance. We are planning to remove triggers for introspection requests in the future. Please raise an issue if you needed to enable this feature."; + } + ]; + + optional bool oidc_legacy_introspection = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "true"; + description: "We have recently refactored the introspection endpoint for performance reasons. This feature can be used to rollback to the legacy implementation if unexpected bugs arise. Please raise an issue if you needed to enable this feature."; + } + ]; + + optional bool user_schema = 4 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "true"; + description: "User Schemas allow to manage data schemas of user. If the flag is enabled, you'll be able to use the new API and its features. Note that it is still in an early stage."; + } + ]; + + optional bool oidc_token_exchange = 5 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "true"; + description: "Enable the experimental `urn:ietf:params:oauth:grant-type:token-exchange` grant type for the OIDC token endpoint. Token exchange can be used to request tokens with a lesser scope or impersonate other users. See the security policy to allow impersonation on an instance."; + } + ]; + + optional bool actions = 6 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "true"; + description: "Actions allow to manage data executions and targets. If the flag is enabled, you'll be able to use the new API and its features. Note that it is still in an early stage."; + } + ]; + + repeated ImprovedPerformance improved_performance = 7 [ + (validate.rules).repeated.unique = true, + (validate.rules).repeated.items.enum = {defined_only: true, not_in: [0]}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "[1]"; + description: "Improves performance of specified execution paths."; + } + ]; +} + +message SetSystemFeaturesResponse { + zitadel.object.v2.Details details = 1; +} + +message ResetSystemFeaturesRequest {} + +message ResetSystemFeaturesResponse { + zitadel.object.v2.Details details = 1; +} + +message GetSystemFeaturesRequest {} + +message GetSystemFeaturesResponse { + zitadel.object.v2.Details details = 1; + FeatureFlag login_default_org = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "true"; + description: "The login UI will use the settings of the default org (and not from the instance) if no organization context is set"; + } + ]; + + FeatureFlag oidc_trigger_introspection_projections = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "true"; + description: "Enable projection triggers during an introspection request. This can act as workaround if there are noticeable consistency issues in the introspection response but can have an impact on performance. We are planning to remove triggers for introspection requests in the future. Please raise an issue if you needed to enable this feature."; + } + ]; + + FeatureFlag oidc_legacy_introspection = 4 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "true"; + description: "We have recently refactored the introspection endpoint for performance reasons. This feature can be used to rollback to the legacy implementation if unexpected bugs arise. Please raise an issue if you needed to enable this feature."; + } + ]; + + FeatureFlag user_schema = 5 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "true"; + description: "User Schemas allow to manage data schemas of user. If the flag is enabled, you'll be able to use the new API and its features. Note that it is still in an early stage."; + } + ]; + + FeatureFlag oidc_token_exchange = 6 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "true"; + description: "Enable the experimental `urn:ietf:params:oauth:grant-type:token-exchange` grant type for the OIDC token endpoint. Token exchange can be used to request tokens with a lesser scope or impersonate other users. See the security policy to allow impersonation on an instance."; + } + ]; + + FeatureFlag actions = 7 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "true"; + description: "Actions v2 allow to manage data executions and targets. If the flag is enabled, you'll be able to use the new API and its features. Note that it is still in an early stage."; + } + ]; + + ImprovedPerformanceFeatureFlag improved_performance = 8 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "[1]"; + description: "Improves performance of specified execution paths."; + } + ]; +} diff --git a/proto/zitadel/feature/v2/user.proto b/proto/zitadel/feature/v2/user.proto new file mode 100644 index 0000000000..d22a981404 --- /dev/null +++ b/proto/zitadel/feature/v2/user.proto @@ -0,0 +1,62 @@ +syntax = "proto3"; + +package zitadel.feature.v2; + +import "protoc-gen-openapiv2/options/annotations.proto"; +import "validate/validate.proto"; + +import "zitadel/object/v2/object.proto"; +import "zitadel/feature/v2/feature.proto"; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/feature/v2;feature"; + +message SetUserFeatureRequest { + 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: "\"69629023906488334\""; + } + ]; +} + +message SetUserFeaturesResponse { + zitadel.object.v2.Details details = 1; +} + +message ResetUserFeaturesRequest { + 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: "\"69629023906488334\""; + } + ]; +} + +message ResetUserFeaturesResponse { + zitadel.object.v2.Details details = 1; +} + +message GetUserFeaturesRequest { + 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: "\"69629023906488334\""; + } + ]; + bool inheritance = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "true"; + description: "Inherit unset features from the resource owners. This option is recursive: if the flag is set, the resource's ancestors are consulted up to system defaults. If this option is disabled and the feature is not set on the user, it will be ommitted from the response or Not Found is returned when the user has no features flags at all."; + } + ]; +} + +message GetUserFeaturesResponse { + zitadel.object.v2.Details details = 1; +} diff --git a/proto/zitadel/management.proto b/proto/zitadel/management.proto index 24583711ae..64ad68c53a 100644 --- a/proto/zitadel/management.proto +++ b/proto/zitadel/management.proto @@ -280,6 +280,7 @@ service ManagementService { }; } + // Deprecated: please use user service v2 ListUsers, with InUserIDQuery rpc GetUserByID(GetUserByIDRequest) returns (GetUserByIDResponse) { option (google.api.http) = { get: "/users/{id}" @@ -291,8 +292,9 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { summary: "User by ID"; - description: "Returns the full user object (human or machine) including the profile, email, etc." + description: "Returns the full user object (human or machine) including the profile, email, etc.\n\nDeprecated: please use user service v2 GetUserByID" tags: "Users"; + deprecated: true; responses: { key: "200" value: { @@ -310,6 +312,7 @@ service ManagementService { }; } + // Deprecated: please use user service v2 ListUsers, with LoginNameQuery rpc GetUserByLoginNameGlobal(GetUserByLoginNameGlobalRequest) returns (GetUserByLoginNameGlobalResponse) { option (google.api.http) = { get: "/global/users/_by_login_name" @@ -321,9 +324,10 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { summary: "Get User by login name (globally)"; - description: "Get a user by login name searched over all organizations. The request only returns data if the login name matches exactly." + description: "Get a user by login name searched over all organizations. The request only returns data if the login name matches exactly.\n\nDeprecated: please use user service v2 ListUsers, with LoginNameQuery" tags: "Users"; tags: "Global"; + deprecated: true; responses: { key: "200" value: { @@ -333,6 +337,7 @@ service ManagementService { }; } + // Deprecated: please use user service v2 ListUsers rpc ListUsers(ListUsersRequest) returns (ListUsersResponse) { option (google.api.http) = { post: "/users/_search" @@ -345,8 +350,9 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Users"; + deprecated: true; summary: "Search Users"; - description: "Search for users within an organization. By default, we will return users of your organization. Make sure to include a limit and sorting for pagination." + description: "Search for users within an organization. By default, we will return users of your organization. Make sure to include a limit and sorting for pagination.\n\nDeprecated: please use user service v2 ListUsers" parameters: { headers: { name: "x-zitadel-orgid"; @@ -400,6 +406,7 @@ service ManagementService { }; } + // Deprecated: please use user service v2 ListUsers, is unique when no user is returned rpc IsUserUnique(IsUserUniqueRequest) returns (IsUserUniqueResponse) { option (google.api.http) = { get: "/users/_is_unique" @@ -411,8 +418,9 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Users"; + deprecated: true; summary: "Check for existing user"; - description: "Returns if a user with the requested email or username is unique. So you can create the user." + description: "Returns if a user with the requested email or username is unique. So you can create the user. \n\nDeprecated: please use user service v2 ListUsers, is unique when no user is returned" parameters: { headers: { name: "x-zitadel-orgid"; @@ -424,7 +432,7 @@ service ManagementService { }; } - // deprecated: use ImportHumanUser + // Deprecated: use ImportHumanUser rpc AddHumanUser(AddHumanUserRequest) returns (AddHumanUserResponse) { option (google.api.http) = { post: "/users/human" @@ -437,7 +445,7 @@ 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." + 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; parameters: { @@ -451,6 +459,7 @@ service ManagementService { }; } + // Deprecated: please use user service v2 AddHumanUser rpc ImportHumanUser(ImportHumanUserRequest) returns (ImportHumanUserResponse) { option (google.api.http) = { post: "/users/human/_import" @@ -463,9 +472,10 @@ 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." + 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" tags: "Users"; tags: "User Human" + deprecated: true; parameters: { headers: { name: "x-zitadel-orgid"; @@ -509,6 +519,7 @@ service ManagementService { }; } + // Deprecated: please use user service v2 DeactivateUser rpc DeactivateUser(DeactivateUserRequest) returns (DeactivateUserResponse) { option (google.api.http) = { post: "/users/{id}/_deactivate" @@ -521,8 +532,9 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { summary: "Deactivate user"; - description: "The state of the user will be changed to 'deactivated'. The user will not be able to log in anymore. The endpoint returns an error if the user is already in the state 'deactivated'. Use deactivate user when the user should not be able to use the account anymore, but you still need access to the user data." + description: "The state of the user will be changed to 'deactivated'. The user will not be able to log in anymore. The endpoint returns an error if the user is already in the state 'deactivated'. Use deactivate user when the user should not be able to use the account anymore, but you still need access to the user data.\n\nDeprecated: please use user service v2 DeactivateUser" tags: "Users"; + deprecated: true; responses: { key: "200" value: { @@ -540,6 +552,7 @@ service ManagementService { }; } + // Deprecated: please use user service v2 ReactivateUser rpc ReactivateUser(ReactivateUserRequest) returns (ReactivateUserResponse) { option (google.api.http) = { post: "/users/{id}/_reactivate" @@ -552,8 +565,9 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { summary: "Reactivate user"; - description: "Reactivate a user with the state 'deactivated'. The user will be able to log in again afterward. The endpoint returns an error if the user is not in the state 'deactivated'." + description: "Reactivate a user with the state 'deactivated'. The user will be able to log in again afterward. The endpoint returns an error if the user is not in the state 'deactivated'.\n\nDeprecated: please use user service v2 ReactivateUser" tags: "Users"; + deprecated: true; responses: { key: "200" value: { @@ -571,6 +585,7 @@ service ManagementService { }; } + // Deprecated: please use user service v2 LockUser rpc LockUser(LockUserRequest) returns (LockUserResponse) { option (google.api.http) = { post: "/users/{id}/_lock" @@ -583,8 +598,9 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { summary: "Lock user"; - description: "The state of the user will be changed to 'locked'. The user will not be able to log in anymore. The endpoint returns an error if the user is already in the state 'locked'. Use this endpoint if the user should not be able to log in temporarily because of an event that happened (wrong password, etc.)" + description: "The state of the user will be changed to 'locked'. The user will not be able to log in anymore. The endpoint returns an error if the user is already in the state 'locked'. Use this endpoint if the user should not be able to log in temporarily because of an event that happened (wrong password, etc.),\n\nDeprecated: please use user service v2 LockUser" tags: "Users"; + deprecated: true; responses: { key: "200" value: { @@ -602,6 +618,7 @@ service ManagementService { }; } + // Deprecated: please use user service v2 UnlockUser rpc UnlockUser(UnlockUserRequest) returns (UnlockUserResponse) { option (google.api.http) = { post: "/users/{id}/_unlock" @@ -614,8 +631,9 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { summary: "Unlock user"; - description: "Unlock a user with the state 'locked'. The user will be able to log in again afterward. The endpoint returns an error if the user is not in the state 'locked'." + description: "Unlock a user with the state 'locked'. The user will be able to log in again afterward. The endpoint returns an error if the user is not in the state 'locked'.\n\nDeprecated: please use user service v2 UnlockUser" tags: "Users"; + deprecated: true; responses: { key: "200" value: { @@ -633,6 +651,7 @@ service ManagementService { }; } + // Deprecated: please use user service v2 RemoveUser rpc RemoveUser(RemoveUserRequest) returns (RemoveUserResponse) { option (google.api.http) = { delete: "/users/{id}" @@ -644,8 +663,9 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { summary: "Delete user"; - description: "The state of the user will be changed to 'deleted'. The user will not be able to log in anymore. Endpoints requesting this user will return an error 'User not found" + description: "The state of the user will be changed to 'deleted'. The user will not be able to log in anymore. Endpoints requesting this user will return an error 'User not found.\n\nDeprecated: please use user service v2 RemoveUser" tags: "Users"; + deprecated: true; responses: { key: "200" value: { @@ -663,6 +683,7 @@ service ManagementService { }; } + // Deprecated: please use user service v2 UpdateHumanUser rpc UpdateUserName(UpdateUserNameRequest) returns (UpdateUserNameResponse) { option (google.api.http) = { put: "/users/{user_id}/username" @@ -675,8 +696,9 @@ 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." + 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: { key: "200" value: { @@ -848,6 +870,7 @@ service ManagementService { }; } + // Deprecated: please use user service v2 GetUserByID rpc GetHumanProfile(GetHumanProfileRequest) returns (GetHumanProfileResponse) { option (google.api.http) = { get: "/users/{user_id}/profile" @@ -859,9 +882,10 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { summary: "Get User Profile (Human)"; - description: "Get basic information like first_name and last_name of a user." + description: "Get basic information like first_name and last_name of a user.\n\nDeprecated: please use user service v2 GetUserByID" tags: "Users"; tags: "User Human"; + deprecated: true; responses: { key: "200" value: { @@ -879,6 +903,7 @@ service ManagementService { }; } + // Deprecated: please use user service v2 UpdateHumanUser rpc UpdateHumanProfile(UpdateHumanProfileRequest) returns (UpdateHumanProfileResponse) { option (google.api.http) = { put: "/users/{user_id}/profile" @@ -891,9 +916,10 @@ 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." + 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" tags: "Users"; tags: "User Human"; + deprecated: true; responses: { key: "200" value: { @@ -911,6 +937,7 @@ service ManagementService { }; } + // Deprecated: please use user service v2 GetUserByID rpc GetHumanEmail(GetHumanEmailRequest) returns (GetHumanEmailResponse) { option (google.api.http) = { get: "/users/{user_id}/email" @@ -922,9 +949,10 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { summary: "Get User Email (Human)"; - description: "Get the email address and the verification state of the address." + description: "Get the email address and the verification state of the address.\n\nDeprecated: please use user service v2 GetUserByID" tags: "Users"; tags: "User Human"; + deprecated: true; responses: { key: "200" value: { @@ -942,6 +970,7 @@ service ManagementService { }; } + // Deprecated: please use user service v2 SetEmail rpc UpdateHumanEmail(UpdateHumanEmailRequest) returns (UpdateHumanEmailResponse) { option (google.api.http) = { put: "/users/{user_id}/email" @@ -954,9 +983,10 @@ 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." + 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; responses: { key: "200" value: { @@ -974,6 +1004,7 @@ service ManagementService { }; } + // Deprecated: not used anymore in user state rpc ResendHumanInitialization(ResendHumanInitializationRequest) returns (ResendHumanInitializationResponse) { option (google.api.http) = { post: "/users/{user_id}/_resend_initialization" @@ -986,9 +1017,10 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { summary: "Resend User Initialization Email"; - description: "A newly created user will get an initialization email to verify the email address and set a password. Resend the email with this request to the user's email address, or a newly added address." + description: "A newly created user will get an initialization email to verify the email address and set a password. Resend the email with this request to the user's email address, or a newly added address.\n\nDeprecated: not used anymore in user state" tags: "Users"; tags: "User Human"; + deprecated: true; responses: { key: "200" value: { @@ -1006,6 +1038,7 @@ service ManagementService { }; } + // Deprecated: please use user service v2 ResendEmailCode rpc ResendHumanEmailVerification(ResendHumanEmailVerificationRequest) returns (ResendHumanEmailVerificationResponse) { option (google.api.http) = { post: "/users/{user_id}/email/_resend_verification" @@ -1018,9 +1051,10 @@ 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." + 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; responses: { key: "200" value: { @@ -1038,6 +1072,7 @@ service ManagementService { }; } + // Deprecated: please use user service v2 GetUserByID rpc GetHumanPhone(GetHumanPhoneRequest) returns (GetHumanPhoneResponse) { option (google.api.http) = { get: "/users/{user_id}/phone" @@ -1049,9 +1084,10 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { summary: "Get User Phone (Human)"; - description: "Get the phone number and the verification state of the number. The phone number is only for informational purposes and to send messages, not for Authentication (2FA)." + description: "Get the phone number and the verification state of the number. The phone number is only for informational purposes and to send messages, not for Authentication (2FA).\n\nDeprecated: please use user service v2 GetUserByID" tags: "Users"; tags: "User Human"; + deprecated: true; responses: { key: "200" value: { @@ -1069,6 +1105,7 @@ service ManagementService { }; } + // Deprecated: please use user service v2 SetPhone rpc UpdateHumanPhone(UpdateHumanPhoneRequest) returns (UpdateHumanPhoneResponse) { option (google.api.http) = { put: "/users/{user_id}/phone" @@ -1081,9 +1118,10 @@ 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)." + 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; responses: { key: "200" value: { @@ -1101,6 +1139,7 @@ service ManagementService { }; } + // Deprecated: please use user service v2 SetPhone rpc RemoveHumanPhone(RemoveHumanPhoneRequest) returns (RemoveHumanPhoneResponse) { option (google.api.http) = { delete: "/users/{user_id}/phone" @@ -1112,9 +1151,10 @@ 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." + description: "Remove the configured phone number of a user.\n\nDeprecated: please use user service v2 SetPhone" tags: "Users"; tags: "User Human"; + deprecated: true; responses: { key: "200" value: { @@ -1132,6 +1172,7 @@ service ManagementService { }; } + // Deprecated: please use user service v2 ResendPhoneCode rpc ResendHumanPhoneVerification(ResendHumanPhoneVerificationRequest) returns (ResendHumanPhoneVerificationResponse) { option (google.api.http) = { post: "/users/{user_id}/phone/_resend_verification" @@ -1144,9 +1185,10 @@ 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." + 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; responses: { key: "200" value: { @@ -1195,7 +1237,7 @@ service ManagementService { }; } - // deprecated: use SetHumanPassword + // Deprecated: please use user service v2 SetPassword rpc SetHumanInitialPassword(SetHumanInitialPasswordRequest) returns (SetHumanInitialPasswordResponse) { option (google.api.http) = { post: "/users/{user_id}/password/_initialize" @@ -1209,7 +1251,7 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Users"; tags: "User Human"; - summary: "Set Human Initial Password"; + summary: "Set Human Initial Password\n\nDeprecated: please use user service v2 SetPassword"; deprecated: true; parameters: { headers: { @@ -1222,6 +1264,7 @@ service ManagementService { }; } + // Deprecated: please use user service v2 SetPassword rpc SetHumanPassword(SetHumanPasswordRequest) returns (SetHumanPasswordResponse) { option (google.api.http) = { post: "/users/{user_id}/password" @@ -1234,9 +1277,10 @@ 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." + 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; responses: { key: "200" value: { @@ -1254,6 +1298,7 @@ service ManagementService { }; } + // Deprecated: please use user service v2 PasswordReset rpc SendHumanResetPasswordNotification(SendHumanResetPasswordNotificationRequest) returns (SendHumanResetPasswordNotificationResponse) { option (google.api.http) = { post: "/users/{user_id}/password/_reset" @@ -1266,9 +1311,10 @@ 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." + 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; responses: { key: "200" value: { @@ -1286,6 +1332,7 @@ service ManagementService { }; } + // Deprecated: please use user service v2 ListAuthenticationMethodTypes rpc ListHumanAuthFactors(ListHumanAuthFactorsRequest) returns (ListHumanAuthFactorsResponse) { option (google.api.http) = { post: "/users/{user_id}/auth_factors/_search" @@ -1297,9 +1344,10 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { summary: "Get User Authentication Factors (2FA/MFA)"; - description: "Get a list of authentication factors the user has set. Including Second-Factors (2FA) and Multi-Factors (MFA)." + description: "Get a list of authentication factors the user has set. Including Second-Factors (2FA) and Multi-Factors (MFA).\n\nDeprecated: please use user service v2 ListAuthenticationMethodTypes" tags: "Users"; tags: "User Human"; + deprecated: true; responses: { key: "200" value: { @@ -1317,6 +1365,7 @@ service ManagementService { }; } + // Deprecated: please use user service v2 RemoveTOTP rpc RemoveHumanAuthFactorOTP(RemoveHumanAuthFactorOTPRequest) returns (RemoveHumanAuthFactorOTPResponse) { option (google.api.http) = { delete: "/users/{user_id}/auth_factors/otp" @@ -1328,9 +1377,10 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { summary: "Remove Multi-Factor OTP"; - description: "Remove the configured One-Time-Password (OTP) as a factor from the user. OTP is an authentication app, like Authy or Google/Microsoft Authenticator." + description: "Remove the configured One-Time-Password (OTP) as a factor from the user. OTP is an authentication app, like Authy or Google/Microsoft Authenticator.\n\nDeprecated: please use user service v2 RemoveTOTP" tags: "Users"; tags: "User Human"; + deprecated: true; responses: { key: "200" value: { @@ -1348,6 +1398,7 @@ service ManagementService { }; } + // Deprecated: please use user service v2 RemoveU2F rpc RemoveHumanAuthFactorU2F(RemoveHumanAuthFactorU2FRequest) returns (RemoveHumanAuthFactorU2FResponse) { option (google.api.http) = { delete: "/users/{user_id}/auth_factors/u2f/{token_id}" @@ -1359,7 +1410,8 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { summary: "Remove Multi-Factor U2F"; - description: "Remove the configured Universal-Second-Factor (U2F) as a factor from the user. U2F is a device-dependent factor like FingerPrint, Windows-Hello, etc." + deprecated: true; + description: "Remove the configured Universal-Second-Factor (U2F) as a factor from the user. U2F is a device-dependent factor like FingerPrint, Windows-Hello, etc.\n\nDeprecated: please use user service v2 RemoveU2F" tags: "Users"; tags: "User Human"; responses: { @@ -1379,6 +1431,7 @@ service ManagementService { }; } + // Deprecated: please use user service v2 RemoveOTPSMS rpc RemoveHumanAuthFactorOTPSMS(RemoveHumanAuthFactorOTPSMSRequest) returns (RemoveHumanAuthFactorOTPSMSResponse) { option (google.api.http) = { delete: "/users/{user_id}/auth_factors/otp_sms" @@ -1390,9 +1443,10 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { summary: "Remove Multi-Factor OTP SMS"; - description: "Remove the configured One-Time-Password (OTP) SMS as a factor from the user. As only one OTP SMS per user is allowed, the user will not have OTP SMS as a second-factor afterward." + description: "Remove the configured One-Time-Password (OTP) SMS as a factor from the user. As only one OTP SMS per user is allowed, the user will not have OTP SMS as a second-factor afterward.\n\nDeprecated: please use user service v2 RemoveOTPSMS" tags: "Users"; tags: "User Human"; + deprecated: true; responses: { key: "200" value: { @@ -1410,6 +1464,7 @@ service ManagementService { }; } + // Deprecated: please use user service v2 RemoveOTPEmail rpc RemoveHumanAuthFactorOTPEmail(RemoveHumanAuthFactorOTPEmailRequest) returns (RemoveHumanAuthFactorOTPEmailResponse) { option (google.api.http) = { delete: "/users/{user_id}/auth_factors/otp_email" @@ -1421,9 +1476,10 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { summary: "Remove Multi-Factor OTP SMS"; - description: "Remove the configured One-Time-Password (OTP) Email as a factor from the user. As only one OTP Email per user is allowed, the user will not have OTP Email as a second-factor afterward." + description: "Remove the configured One-Time-Password (OTP) Email as a factor from the user. As only one OTP Email per user is allowed, the user will not have OTP Email as a second-factor afterward.\n\nDeprecated: please use user service v2 RemoveOTPEmail" tags: "Users"; tags: "User Human"; + deprecated: true; responses: { key: "200" value: { @@ -1441,6 +1497,7 @@ service ManagementService { }; } + // Deprecated: please use user service v2 ListPasskeys rpc ListHumanPasswordless(ListHumanPasswordlessRequest) returns (ListHumanPasswordlessResponse) { option (google.api.http) = { post: "/users/{user_id}/passwordless/_search" @@ -1452,7 +1509,8 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { summary: "Search Passwordless/Passkey authentication"; - description: "Get a list of configured passwordless/passkey authentication methods from the user. Passwordless/passkey is a device-dependent authentication like FingerScan, WindowsHello or a Hardware Token." + deprecated: true; + description: "Get a list of configured passwordless/passkey authentication methods from the user. Passwordless/passkey is a device-dependent authentication like FingerScan, WindowsHello or a Hardware Token.\n\nDeprecated: please use user service v2 ListPasskeys" tags: "Users"; tags: "User Human"; responses: { @@ -1472,6 +1530,7 @@ service ManagementService { }; } + // Deprecated: please use user service v2 RegisterPasskey rpc AddPasswordlessRegistration(AddPasswordlessRegistrationRequest) returns (AddPasswordlessRegistrationResponse) { option (google.api.http) = { post: "/users/{user_id}/passwordless/_link" @@ -1482,9 +1541,10 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { summary: "Add Passwordless/Passkey Registration Link"; - description: "Adds a new passwordless/passkey authenticator link to the user and returns it in the response. The link enables the user to register a new device if current passwordless/passkey devices are all platform authenticators. e.g. User has already registered Windows Hello and wants to register FaceID on the iPhone" + description: "Adds a new passwordless/passkey authenticator link to the user and returns it in the response. The link enables the user to register a new device if current passwordless/passkey devices are all platform authenticators. e.g. User has already registered Windows Hello and wants to register FaceID on the iPhone\n\nDeprecated: please use user service v2 RegisterPasskey" tags: "Users"; tags: "User Human"; + deprecated: true; responses: { key: "200" value: { @@ -1502,6 +1562,7 @@ service ManagementService { }; } + // Deprecated: please use user service v2 RegisterPasskey rpc SendPasswordlessRegistration(SendPasswordlessRegistrationRequest) returns (SendPasswordlessRegistrationResponse) { option (google.api.http) = { post: "/users/{user_id}/passwordless/_send_link" @@ -1513,9 +1574,10 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { summary: "Send Passwordless/Passkey Registration Link"; - description: "Adds a new passwordless/passkey authenticator link to the user and sends it to the user per email. The link enables the user to register a new device if current passwordless/passkey devices are all platform authenticators. e.g. User has already registered Windows Hello and wants to register FaceID on the iPhone" + description: "Adds a new passwordless/passkey authenticator link to the user and sends it to the user per email. The link enables the user to register a new device if current passwordless/passkey devices are all platform authenticators. e.g. User has already registered Windows Hello and wants to register FaceID on the iPhone.\n\nDeprecated: please use user service v2 RegisterPasskey" tags: "Users"; tags: "User Human"; + deprecated: true; responses: { key: "200" value: { @@ -1533,6 +1595,7 @@ service ManagementService { }; } + // Deprecated: please use user service v2 RemovePasskey rpc RemoveHumanPasswordless(RemoveHumanPasswordlessRequest) returns (RemoveHumanPasswordlessResponse) { option (google.api.http) = { delete: "/users/{user_id}/passwordless/{token_id}" @@ -1544,7 +1607,8 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { summary: "Delete Passwordless/Passkey"; - description: "Remove a configured passwordless/passkey authentication method from the user. (e.g FaceID, FingerScane, WindowsHello, etc.)" + deprecated: true; + description: "Remove a configured passwordless/passkey authentication method from the user. (e.g FaceID, FingerScane, WindowsHello, etc.).\n\nDeprecated: please use user service v2 RemovePasskey" tags: "Users"; tags: "User Human"; responses: { @@ -1911,6 +1975,7 @@ service ManagementService { }; } + // Deprecated: please use user service v2 ListLinkedIDPs rpc ListHumanLinkedIDPs(ListHumanLinkedIDPsRequest) returns (ListHumanLinkedIDPsResponse) { option (google.api.http) = { post: "/users/{user_id}/idps/_search" @@ -1924,7 +1989,8 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Users" summary: "List Social Logins"; - description: "Returns a list of all linked identity providers/social logins of the user. (e. Google, Microsoft, AzureAD, etc.)" + deprecated: true; + description: "Returns a list of all linked identity providers/social logins of the user. (e. Google, Microsoft, AzureAD, etc.).\n\nDeprecated: please use user service v2 ListLinkedIDPs" parameters: { headers: { name: "x-zitadel-orgid"; @@ -1936,6 +2002,7 @@ service ManagementService { }; } + // Deprecated: please use user service v2 RemoveLinkedIDP rpc RemoveHumanLinkedIDP(RemoveHumanLinkedIDPRequest) returns (RemoveHumanLinkedIDPResponse) { option (google.api.http) = { delete: "/users/{user_id}/idps/{idp_id}/{linked_user_id}" @@ -1948,7 +2015,8 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Users" summary: "Remove Social Login"; - description: "Remove a configured social logins/identity providers of the user (e.g. Google, Microsoft, AzureAD, etc.). The user will not be able to log in with the given provider afterward. Make sure the user does have other possibilities to authenticate." + deprecated: true; + description: "Remove a configured social logins/identity providers of the user (e.g. Google, Microsoft, AzureAD, etc.). The user will not be able to log in with the given provider afterward. Make sure the user does have other possibilities to authenticate.\n\nDeprecated: please use user service v2 RemoveLinkedIDP" parameters: { headers: { name: "x-zitadel-orgid"; diff --git a/proto/zitadel/object/v2/object.proto b/proto/zitadel/object/v2/object.proto new file mode 100644 index 0000000000..5a63ece19b --- /dev/null +++ b/proto/zitadel/object/v2/object.proto @@ -0,0 +1,122 @@ +syntax = "proto3"; + +package zitadel.object.v2; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/object/v2;object"; + +import "google/protobuf/timestamp.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; +import "validate/validate.proto"; + +// Deprecated: use Organization +message Organisation { + oneof org { + string org_id = 1; + string org_domain = 2; + } +} + +message Organization { + oneof org { + string org_id = 1; + string org_domain = 2; + } +} + +message RequestContext { + oneof resource_owner { + string org_id = 1; + bool instance = 2 [(validate.rules).bool = {const: true}]; + } +} + +message ListQuery { + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { + json_schema: { + title: "General List Query" + description: "Object unspecific list filters like offset, limit and asc/desc." + } + }; + uint64 offset = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"0\""; + } + ]; + uint32 limit = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "100"; + description: "Maximum amount of events returned. The default is set to 1000 in https://github.com/zitadel/zitadel/blob/new-eventstore/cmd/zitadel/startup.yaml. If the limit exceeds the maximum configured ZITADEL will throw an error. If no limit is present the default is taken."; + } + ]; + bool asc = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "default is descending" + } + ]; +} + +message Details { + //sequence represents the order of events. It's always counting + // + // on read: the sequence of the last event reduced by the projection + // + // on manipulation: the timestamp of the event(s) added by the manipulation + uint64 sequence = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2\""; + } + ]; + //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; + //resource_owner is the organization or instance_id an object belongs to + string resource_owner = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629023906488334\""; + } + ]; +} + +message ListDetails { + uint64 total_result = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2\""; + } + ]; + uint64 processed_sequence = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"267831\""; + } + ]; + google.protobuf.Timestamp timestamp = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "the last time the projection got updated" + } + ]; +} + +enum TextQueryMethod { + TEXT_QUERY_METHOD_EQUALS = 0; + TEXT_QUERY_METHOD_EQUALS_IGNORE_CASE = 1; + TEXT_QUERY_METHOD_STARTS_WITH = 2; + TEXT_QUERY_METHOD_STARTS_WITH_IGNORE_CASE = 3; + TEXT_QUERY_METHOD_CONTAINS = 4; + TEXT_QUERY_METHOD_CONTAINS_IGNORE_CASE = 5; + TEXT_QUERY_METHOD_ENDS_WITH = 6; + TEXT_QUERY_METHOD_ENDS_WITH_IGNORE_CASE = 7; +} + +enum ListQueryMethod { + LIST_QUERY_METHOD_IN = 0; +} + +enum TimestampQueryMethod { + TIMESTAMP_QUERY_METHOD_EQUALS = 0; + TIMESTAMP_QUERY_METHOD_GREATER = 1; + TIMESTAMP_QUERY_METHOD_GREATER_OR_EQUALS = 2; + TIMESTAMP_QUERY_METHOD_LESS = 3; + TIMESTAMP_QUERY_METHOD_LESS_OR_EQUALS = 4; +} \ No newline at end of file diff --git a/proto/zitadel/oidc/v2/authorization.proto b/proto/zitadel/oidc/v2/authorization.proto new file mode 100644 index 0000000000..c0ad751624 --- /dev/null +++ b/proto/zitadel/oidc/v2/authorization.proto @@ -0,0 +1,117 @@ +syntax = "proto3"; + +package zitadel.oidc.v2; + +import "google/protobuf/duration.proto"; +import "google/protobuf/timestamp.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/oidc/v2;oidc"; + +message AuthRequest{ + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { + external_docs: { + url: "https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest"; + description: "Find out more about OIDC Auth Request parameters"; + } + }; + + string id = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "ID of the authorization request"; + } + ]; + + google.protobuf.Timestamp creation_date = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Time when the auth request was created"; + } + ]; + + string client_id = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "OIDC client ID of the application that created the auth request"; + } + ]; + + repeated string scope = 4 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Requested scopes by the application, which the user must consent to."; + } + ]; + + string redirect_uri = 5 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Base URI that points back to the application"; + } + ]; + + repeated Prompt prompt = 6 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Prompts that must be displayed to the user"; + } + ]; + + repeated string ui_locales = 7 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "End-User's preferred languages and scripts for the user interface, represented as a list of BCP47 [RFC5646] language tag values, ordered by preference. For instance, the value [fr-CA, fr, en] represents a preference for French as spoken in Canada, then French (without a region designation), followed by English (without a region designation). An error SHOULD NOT result if some or all of the requested locales are not supported."; + } + ]; + + optional string login_hint = 8 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Login hint can be set by the application with a user identifier such as an email or phone number."; + } + ]; + + optional google.protobuf.Duration max_age = 9 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Specifies the allowable elapsed time in seconds since the last time the End-User was actively authenticated. If the elapsed time is greater than this value, or the field is present with 0 duration, the user must be re-authenticated."; + } + ]; + + optional string hint_user_id = 10 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "User ID taken from a ID Token Hint if it was present and valid."; + } + ]; +} + +enum Prompt { + PROMPT_UNSPECIFIED = 0; + PROMPT_NONE = 1; + PROMPT_LOGIN = 2; + PROMPT_CONSENT = 3; + PROMPT_SELECT_ACCOUNT = 4; + PROMPT_CREATE = 5; +} + +message AuthorizationError { + ErrorReason error = 1; + optional string error_description = 2; + optional string error_uri = 3; +} + +enum ErrorReason { + ERROR_REASON_UNSPECIFIED = 0; + + // Error states from https://datatracker.ietf.org/doc/html/rfc6749#section-4.2.2.1 + ERROR_REASON_INVALID_REQUEST = 1; + ERROR_REASON_UNAUTHORIZED_CLIENT = 2; + ERROR_REASON_ACCESS_DENIED = 3; + ERROR_REASON_UNSUPPORTED_RESPONSE_TYPE = 4; + ERROR_REASON_INVALID_SCOPE = 5; + ERROR_REASON_SERVER_ERROR = 6; + ERROR_REASON_TEMPORARY_UNAVAILABLE = 7; + + // Error states from https://openid.net/specs/openid-connect-core-1_0.html#AuthError + ERROR_REASON_INTERACTION_REQUIRED = 8; + ERROR_REASON_LOGIN_REQUIRED = 9; + ERROR_REASON_ACCOUNT_SELECTION_REQUIRED = 10; + ERROR_REASON_CONSENT_REQUIRED = 11; + ERROR_REASON_INVALID_REQUEST_URI = 12; + ERROR_REASON_INVALID_REQUEST_OBJECT = 13; + ERROR_REASON_REQUEST_NOT_SUPPORTED = 14; + ERROR_REASON_REQUEST_URI_NOT_SUPPORTED = 15; + ERROR_REASON_REGISTRATION_NOT_SUPPORTED = 16; +} \ No newline at end of file diff --git a/proto/zitadel/oidc/v2/oidc_service.proto b/proto/zitadel/oidc/v2/oidc_service.proto new file mode 100644 index 0000000000..85044e9570 --- /dev/null +++ b/proto/zitadel/oidc/v2/oidc_service.proto @@ -0,0 +1,219 @@ +syntax = "proto3"; + +package zitadel.oidc.v2; + +import "zitadel/object/v2/object.proto"; +import "zitadel/protoc_gen_zitadel/v2/options.proto"; +import "zitadel/oidc/v2/authorization.proto"; +import "google/api/annotations.proto"; +import "google/api/field_behavior.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; +import "validate/validate.proto"; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/oidc/v2;oidc"; + +option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { + info: { + title: "OIDC Service"; + version: "2.0"; + description: "Get OIDC Auth Request details and create callback URLs."; + contact:{ + name: "ZITADEL" + url: "https://zitadel.com" + email: "hi@zitadel.com" + } + license: { + name: "Apache 2.0", + url: "https://github.com/zitadel/zitadel/blob/main/LICENSE"; + }; + }; + 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 OIDCService { + rpc GetAuthRequest (GetAuthRequestRequest) returns (GetAuthRequestResponse) { + option (google.api.http) = { + get: "/v2/oidc/auth_requests/{auth_request_id}" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + summary: "Get OIDC Auth Request details"; + description: "Get OIDC Auth Request details by ID, obtained from the redirect URL. Returns details that are parsed from the application's Auth Request." + responses: { + key: "200" + value: { + description: "OK"; + } + }; + }; + } + + rpc CreateCallback (CreateCallbackRequest) returns (CreateCallbackResponse) { + option (google.api.http) = { + post: "/v2/oidc/auth_requests/{auth_request_id}" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + summary: "Finalize an Auth Request and get the callback URL."; + description: "Finalize an Auth Request and get the callback URL for success or failure. The user must be redirected to the URL in order to inform the application about the success or failure. On success, the URL contains details for the application to obtain the tokens. This method can only be called once for an Auth request." + responses: { + key: "200" + value: { + description: "OK"; + } + }; + }; + } +} + +message GetAuthRequestRequest { + string auth_request_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; + description: "ID of the Auth Request, as obtained from the redirect URL."; + example: "\"163840776835432705\""; + } + ]; +} + +message GetAuthRequestResponse { + AuthRequest auth_request = 1; +} + +message CreateCallbackRequest { + string auth_request_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Set this field when the authorization flow failed. It creates a callback URL to the application, with the error details set."; + ref: "https://openid.net/specs/openid-connect-core-1_0.html#AuthError"; + } + ]; + + oneof callback_kind { + option (validate.required) = true; + Session session = 2; + AuthorizationError error = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Set this field when the authorization flow failed. It creates a callback URL to the application, with the error details set."; + ref: "https://openid.net/specs/openid-connect-core-1_0.html#AuthError"; + } + ]; + } +} + +message Session { + string session_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; + description: "ID of the session, used to login the user. Connects the session to the Auth Request."; + example: "\"163840776835432705\""; + } + ]; + + string session_token = 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: "Token to verify the session is valid"; + } + ]; +} + +message CreateCallbackResponse { + zitadel.object.v2.Details details = 1; + string callback_url = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Callback URL where the user should be redirected, using a \"302 FOUND\" status. Contains details for the application to obtain the tokens on success, or error details on failure. Note that this field must be treated as credentials, as the contained code can be used to obtain tokens on behalve of the user."; + example: "\"https://client.example.org/cb?code=SplxlOBeZQQYbYS6WxSbIA&state=af0ifjsldkj\"" + } + ]; +} + diff --git a/proto/zitadel/org/v2/org_service.proto b/proto/zitadel/org/v2/org_service.proto new file mode 100644 index 0000000000..9b0006c46f --- /dev/null +++ b/proto/zitadel/org/v2/org_service.proto @@ -0,0 +1,174 @@ +syntax = "proto3"; + + +package zitadel.org.v2; + +import "zitadel/object/v2/object.proto"; +import "zitadel/protoc_gen_zitadel/v2/options.proto"; +import "zitadel/user/v2/auth.proto"; +import "zitadel/user/v2/email.proto"; +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/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"; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/org/v2;org"; + +option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { + info: { + title: "User Service"; + version: "2.0"; + description: "This API is intended to manage organizations in a ZITADEL instance."; + contact:{ + name: "ZITADEL" + url: "https://zitadel.com" + email: "hi@zitadel.com" + } + license: { + name: "Apache 2.0", + url: "https://github.com/zitadel/zitadel/blob/main/LICENSE"; + }; + }; + 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 OrganizationService { + + // Create a new organization and grant the user(s) permission to manage it + rpc AddOrganization(AddOrganizationRequest) returns (AddOrganizationResponse) { + option (google.api.http) = { + post: "/v2/organizations" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + 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." + responses: { + key: "200" + value: { + description: "OK"; + } + }; + }; + } +} + +message AddOrganizationRequest{ + message Admin { + oneof user_type{ + string user_id = 1; + zitadel.user.v2.AddHumanUserRequest human = 2; + } + // specify Org Member Roles for the provided user (default is ORG_OWNER if roles are empty) + repeated string roles = 3; + } + + string name = 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: "\"ZITADEL\""; + } + ]; + repeated Admin admins = 2; +} + +message AddOrganizationResponse{ + message CreatedAdmin { + string user_id = 1; + optional string email_code = 2; + optional string phone_code = 3; + } + zitadel.object.v2.Details details = 1; + string organization_id = 2; + repeated CreatedAdmin created_admins = 3; +} diff --git a/proto/zitadel/session/v2/challenge.proto b/proto/zitadel/session/v2/challenge.proto new file mode 100644 index 0000000000..77a73e8e68 --- /dev/null +++ b/proto/zitadel/session/v2/challenge.proto @@ -0,0 +1,82 @@ +syntax = "proto3"; + +package zitadel.session.v2; + +import "google/api/field_behavior.proto"; +import "google/protobuf/struct.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; +import "validate/validate.proto"; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/session/v2;session"; + +enum UserVerificationRequirement { + USER_VERIFICATION_REQUIREMENT_UNSPECIFIED = 0; + USER_VERIFICATION_REQUIREMENT_REQUIRED = 1; + USER_VERIFICATION_REQUIREMENT_PREFERRED = 2; + USER_VERIFICATION_REQUIREMENT_DISCOURAGED = 3; +} + +message RequestChallenges { + message WebAuthN { + string domain = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "\"Domain on which the session was created. Will be used in the WebAuthN challenge.\""; + } + ]; + UserVerificationRequirement user_verification_requirement = 2 [ + (validate.rules).enum = { + defined_only: true, + not_in: [0] + }, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "\"User verification that is required during validation. When set to `USER_VERIFICATION_REQUIREMENT_REQUIRED` the behaviour is for passkey authentication. Other values will mean U2F\""; + ref: "https://www.w3.org/TR/webauthn/#enum-userVerificationRequirement"; + } + ]; + } + message OTPSMS { + bool return_code = 1; + } + message OTPEmail { + message SendCode { + optional string url_template = 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: "\"https://example.com/otp/verify?userID={{.UserID}}&code={{.Code}}\""; + description: "\"Optionally set a url_template, which will be used in the mail sent by ZITADEL to guide the user to your verification page. If no template is set, the default ZITADEL url will be used.\"" + } + ]; + } + message ReturnCode {} + + // if no delivery_type is specified, an email is sent with the default url + oneof delivery_type { + SendCode send_code = 2; + ReturnCode return_code = 3; + } + } + + optional WebAuthN web_auth_n = 1; + optional OTPSMS otp_sms = 2; + optional OTPEmail otp_email = 3; +} + +message Challenges { + message WebAuthN { + google.protobuf.Struct public_key_credential_request_options = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Options for Assertion Generaration (dictionary PublicKeyCredentialRequestOptions). Generated helper methods transform the field to JSON, for use in a WebauthN client. See also: https://www.w3.org/TR/webauthn/#dictdef-publickeycredentialrequestoptions" + example: "{\"publicKey\":{\"allowCredentials\":[{\"id\":\"ATmqBg-99qyOZk2zloPdJQyS2R7IkFT7v9Hoos_B_nM\",\"type\":\"public-key\"}],\"challenge\":\"GAOHYz2jE69kJMYo6Laij8yWw9-dKKgbViNhfuy0StA\",\"rpId\":\"localhost\",\"timeout\":300000,\"userVerification\":\"required\"}}" + } + ]; + } + + optional WebAuthN web_auth_n = 1; + optional string otp_sms = 2; + optional string otp_email = 3; +} diff --git a/proto/zitadel/session/v2/session.proto b/proto/zitadel/session/v2/session.proto new file mode 100644 index 0000000000..2c17d81f99 --- /dev/null +++ b/proto/zitadel/session/v2/session.proto @@ -0,0 +1,178 @@ +syntax = "proto3"; + +package zitadel.session.v2; + +import "zitadel/object.proto"; +import "google/protobuf/timestamp.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; +import "validate/validate.proto"; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/session/v2;session"; + +message Session { + string id = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "\"id of the session\""; + } + ]; + google.protobuf.Timestamp creation_date = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "\"time when the session was created\""; + } + ]; + google.protobuf.Timestamp change_date = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "\"time when the session was last updated\""; + } + ]; + uint64 sequence = 4 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "\"sequence of the session\""; + } + ]; + Factors factors = 5 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "\"checked factors of the session, e.g. the user, password and more\""; + } + ]; + map metadata = 6 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "\"custom key value list\""; + } + ]; + UserAgent user_agent = 7; + optional google.protobuf.Timestamp expiration_date = 8 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "\"time the session will be automatically invalidated\""; + } + ]; +} + +message Factors { + UserFactor user = 1; + PasswordFactor password = 2; + WebAuthNFactor web_auth_n = 3; + IntentFactor intent = 4; + TOTPFactor totp = 5; + OTPFactor otp_sms = 6; + OTPFactor otp_email = 7; +} + +message UserFactor { + reserved 5; + reserved "organisation_id"; + google.protobuf.Timestamp verified_at = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "\"time when the user was last checked\""; + } + ]; + string id = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "\"id of the checked user\""; + } + ]; + string login_name = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "\"login name of the checked user\""; + } + ]; + string display_name = 4 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "\"display name of the checked user\""; + } + ]; + string organization_id = 6 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "\"organization id of the checked user\""; + } + ]; +} + +message PasswordFactor { + google.protobuf.Timestamp verified_at = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "\"time when the password was last checked\""; + } + ]; +} + +message IntentFactor { + google.protobuf.Timestamp verified_at = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "\"time when an intent was last checked\""; + } + ]; +} + +message WebAuthNFactor { + google.protobuf.Timestamp verified_at = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "\"time when the passkey challenge was last checked\""; + } + ]; + bool user_verified = 2; +} + +message TOTPFactor { + google.protobuf.Timestamp verified_at = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "\"time when the Time-based One-Time Password was last checked\""; + } + ]; +} + +message OTPFactor { + google.protobuf.Timestamp verified_at = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "\"time when the One-Time Password was last checked\""; + } + ]; +} + +message SearchQuery { + oneof query { + option (validate.required) = true; + + IDsQuery ids_query = 1; + UserIDQuery user_id_query = 2; + CreationDateQuery creation_date_query = 3; + } +} + +message IDsQuery { + repeated string ids = 1; +} + +message UserIDQuery { + string id = 1; +} + +message CreationDateQuery { + google.protobuf.Timestamp creation_date = 1; + zitadel.v1.TimestampQueryMethod method = 2 [ + (validate.rules).enum.defined_only = true, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "defines which timestamp comparison method is used"; + } + ]; +} + +message UserAgent { + optional string fingerprint_id = 1; + optional string ip = 2; + optional string description = 3; + + // A header may have multiple values. + // In Go, headers are defined + // as map[string][]string, but protobuf + // doesn't allow this scheme. + message HeaderValues { + repeated string values = 1; + } + map header = 4; +} + +enum SessionFieldName { + SESSION_FIELD_NAME_UNSPECIFIED = 0; + SESSION_FIELD_NAME_CREATION_DATE = 1; +} diff --git a/proto/zitadel/session/v2/session_service.proto b/proto/zitadel/session/v2/session_service.proto new file mode 100644 index 0000000000..f531c0a593 --- /dev/null +++ b/proto/zitadel/session/v2/session_service.proto @@ -0,0 +1,496 @@ +syntax = "proto3"; + +package zitadel.session.v2; + + +import "zitadel/object/v2/object.proto"; +import "zitadel/protoc_gen_zitadel/v2/options.proto"; +import "zitadel/session/v2/challenge.proto"; +import "zitadel/session/v2/session.proto"; +import "google/api/annotations.proto"; +import "google/api/field_behavior.proto"; +import "google/protobuf/struct.proto"; +import "google/protobuf/duration.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; +import "validate/validate.proto"; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/session/v2;session"; + +option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { + info: { + title: "Session Service"; + version: "2.0"; + description: "This API is intended to manage sessions in a ZITADEL instance. Follow the guides on how to [build your own Login UI](/docs/guides/integrate/login-ui) and learn how to use the Session API."; + contact:{ + name: "ZITADEL" + url: "https://zitadel.com" + email: "hi@zitadel.com" + } + license: { + name: "Apache 2.0", + url: "https://github.com/zitadel/zitadel/blob/main/LICENSE"; + }; + }; + 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 SessionService { + + // Search sessions + rpc ListSessions (ListSessionsRequest) returns (ListSessionsResponse) { + option (google.api.http) = { + post: "/v2/sessions/search" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + summary: "Search sessions"; + description: "Search for sessions" + responses: { + key: "200" + value: { + description: "OK"; + } + }; + responses: { + key: "400"; + value: { + description: "invalid list query"; + schema: { + json_schema: { + ref: "#/definitions/rpcStatus"; + }; + }; + }; + }; + }; + } + + // GetSession a session + rpc GetSession (GetSessionRequest) returns (GetSessionResponse) { + option (google.api.http) = { + get: "/v2/sessions/{session_id}" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + summary: "Get a session"; + description: "Get a session and all its information like the time of the user or password verification" + responses: { + key: "200" + value: { + description: "OK"; + } + }; + }; + } + + // Create a new session + rpc CreateSession (CreateSessionRequest) returns (CreateSessionResponse) { + option (google.api.http) = { + post: "/v2/sessions" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + http_response: { + success_code: 201 + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + summary: "Create a new session"; + description: "Create a new session. A token will be returned, which is required for further updates of the session." + responses: { + key: "200" + value: { + description: "OK"; + } + }; + }; + } + + // Update a session + rpc SetSession (SetSessionRequest) returns (SetSessionResponse) { + option (google.api.http) = { + patch: "/v2/sessions/{session_id}" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + summary: "Update an existing session"; + description: "Update an existing session with new information." + responses: { + key: "200" + value: { + description: "OK"; + } + }; + }; + } + + // Terminate a session + rpc DeleteSession (DeleteSessionRequest) returns (DeleteSessionResponse) { + option (google.api.http) = { + delete: "/v2/sessions/{session_id}" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + summary: "Terminate an existing session"; + description: "Terminate your own session or if granted any other session." + responses: { + key: "200" + value: { + description: "OK"; + } + }; + }; + } +} + +message ListSessionsRequest{ + zitadel.object.v2.ListQuery query = 1; + repeated SearchQuery queries = 2; + zitadel.session.v2.SessionFieldName sorting_column = 3; +} + +message ListSessionsResponse{ + zitadel.object.v2.ListDetails details = 1; + repeated Session sessions = 2; +} + +message GetSessionRequest{ + string session_id = 1; + optional string session_token = 2; +} +message GetSessionResponse{ + Session session = 1; +} + +message CreateSessionRequest{ + Checks checks = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "\"Check for user and password. Successful checks will be stated as factors on the session.\""; + } + ]; + map metadata = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "\"custom key value list to be stored on the session\""; + } + ]; + RequestChallenges challenges = 3; + UserAgent user_agent = 4; + optional google.protobuf.Duration lifetime = 5 [ + (validate.rules).duration = {gt: {seconds: 0}}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "\"duration (in seconds) after which the session will be automatically invalidated\""; + example:"\"18000s\"" + } + ]; +} + +message CreateSessionResponse{ + zitadel.object.v2.Details details = 1; + string session_id = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "\"id of the session\""; + example: "\"222430354126975533\""; + } + ]; + string session_token = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "\"The current token of the session, which is required for delete session, get session or the request of other resources.\""; + } + ]; + Challenges challenges = 4; +} + +message SetSessionRequest{ + string session_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; + description: "\"id of the session to update\""; + example: "\"222430354126975533\""; + } + ]; + string session_token = 2 [ + (validate.rules).string = {min_len: 0, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + description: "\"DEPRECATED: this field is ignored.\""; + } + ]; + Checks checks = 3[ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "\"Check for user and password. Successful checks will be stated as factors on the session.\""; + } + ]; + map metadata = 4 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "\"custom key value list to be stored on the session\""; + } + ]; + RequestChallenges challenges = 5; + optional google.protobuf.Duration lifetime = 6 [ + (validate.rules).duration = {gt: {seconds: 0}}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "\"duration (in seconds) after which the session will be automatically invalidated\""; + example:"\"18000s\"" + } + ]; +} + +message SetSessionResponse{ + zitadel.object.v2.Details details = 1; + string session_token = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "\"The current token of the session, which is required for delete session, get session or the request of other resources.\""; + } + ]; + Challenges challenges = 3; +} + +message DeleteSessionRequest{ + string session_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; + description: "\"id of the session to terminate\""; + example: "\"222430354126975533\""; + } + ]; + optional string session_token = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "\"The current token of the session, previously returned on the create / update request. The token is required unless the authenticated user terminates the own session or is granted the `session.delete` permission.\""; + } + ]; +} + +message DeleteSessionResponse{ + zitadel.object.v2.Details details = 1; +} + +message Checks { + optional CheckUser user = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "\"checks the user and updates the session on success\""; + } + ]; + optional CheckPassword password = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "\"Checks the password and updates the session on success. Requires that the user is already checked, either in the previous or the same request.\""; + } + ]; + optional CheckWebAuthN web_auth_n = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "\"Checks the public key credential issued by the WebAuthN client. Requires that the user is already checked and a WebAuthN challenge to be requested, in any previous request.\""; + } + ]; + optional CheckIDPIntent idp_intent = 4 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "\"Checks the IDP intent. Requires that the userlink is already checked and a successful idp intent.\""; + } + ]; + optional CheckTOTP totp = 5 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "\"Checks the Time-based One-Time Password and updates the session on success. Requires that the user is already checked, either in the previous or the same request.\""; + } + ]; + optional CheckOTP otp_sms = 6 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "\"Checks the One-Time Password sent over SMS and updates the session on success. Requires that the user is already checked, either in the previous or the same request.\""; + } + ]; + optional CheckOTP otp_email = 7 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "\"Checks the One-Time Password sent over Email and updates the session on success. Requires that the user is already checked, either in the previous or the same request.\""; + } + ]; +} + +message CheckUser { + oneof search { + 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: "\"d654e6ba-70a3-48ef-a95d-37c8d8a7901a\""; + } + ]; + string login_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: "\"mini@mouse.com\""; + } + ]; + } +} + +message CheckPassword { + string password = 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: "\"V3ryS3cure!\""; + } + ]; +} + +message CheckWebAuthN { + google.protobuf.Struct credential_assertion_data = 1 [ + (validate.rules).message.required = true, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "JSON representation of public key credential issued by the webAuthN client"; + min_length: 55; + max_length: 1048576; //1 MB + } + ]; +} + +message CheckIDPIntent { + string idp_intent_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "ID of the idp intent, previously returned on the success response of the IDP callback" + min_length: 1; + max_length: 200; + example: "\"d654e6ba-70a3-48ef-a95d-37c8d8a7901a\""; + } + ]; + string idp_intent_token = 2 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "token of the idp intent, previously returned on the success response of the IDP callback" + min_length: 1; + max_length: 200; + example: "\"SJKL3ioIDpo342ioqw98fjp3sdf32wahb=\""; + } + ]; +} + +message CheckTOTP { + string code = 1 [ + (validate.rules).string = {min_len: 6, max_len: 6}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 6; + max_length: 6; + example: "\"323764\""; + } + ]; +} + +message CheckOTP { + string code = 1 [ + (validate.rules).string = {min_len: 1}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + example: "\"3237642\""; + } + ]; +} \ No newline at end of file diff --git a/proto/zitadel/settings/v2/branding_settings.proto b/proto/zitadel/settings/v2/branding_settings.proto new file mode 100644 index 0000000000..84c4ecd755 --- /dev/null +++ b/proto/zitadel/settings/v2/branding_settings.proto @@ -0,0 +1,93 @@ +syntax = "proto3"; + +package zitadel.settings.v2; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/settings/v2;settings"; + +import "protoc-gen-openapiv2/options/annotations.proto"; +import "zitadel/settings/v2/settings.proto"; + +message BrandingSettings { + Theme light_theme = 1; + Theme dark_theme = 2; + string font_url = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "url to the font used"; + example: "\"https://acme.com/assets/v1/165617850692654601/policy/label/font-180950243237405441\""; + } + ]; + // hides the org suffix on the login form if the scope \"urn:zitadel:iam:org:domain:primary:{domainname}\" is set + bool hide_login_name_suffix = 4 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "hides the org suffix on the login form if the scope \"urn:zitadel:iam:org:domain:primary:{domainname}\" is set"; + } + ]; + bool disable_watermark = 5 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "boolean to disable the watermark"; + } + ]; + // resource_owner_type returns if the setting is managed on the organization or on the instance + ResourceOwnerType resource_owner_type = 6 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "resource_owner_type returns if the setting is managed on the organization or on the instance"; + } + ]; + ThemeMode theme_mode = 7 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "states whether both or only dark or light theme will be used"; + } + ]; +} + +message Theme { + // hex value for primary color + string primary_color = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "hex value for primary color"; + example: "\"#5469d4\""; + } + ]; + // hex value for background color + string background_color = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "hex value for background color"; + example: "\"#FAFAFA\""; + } + ]; + // hex value for warning color + string warn_color = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "hex value for warn color"; + example: "\"#CD3D56\""; + } + ]; + // hex value for font color + string font_color = 4 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "hex value for font color"; + example: "\"#000000\""; + } + ]; + // url where the logo is served + string logo_url = 5 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "url to the logo"; + example: "\"https://acme.com/assets/v1/165617850692654601/policy/label/logo-180950416321494657\""; + } + ]; + // url where the icon is served + string icon_url = 6 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "url to the icon"; + example: "\"https://acme.com/assets/v1/165617850692654601/policy/label/icon-180950498874178817\""; + } + ]; +} + +enum ThemeMode { + THEME_MODE_UNSPECIFIED = 0; + THEME_MODE_AUTO = 1; + THEME_MODE_LIGHT = 2; + THEME_MODE_DARK = 3; +} \ No newline at end of file diff --git a/proto/zitadel/settings/v2/domain_settings.proto b/proto/zitadel/settings/v2/domain_settings.proto new file mode 100644 index 0000000000..0649e65ba1 --- /dev/null +++ b/proto/zitadel/settings/v2/domain_settings.proto @@ -0,0 +1,33 @@ +syntax = "proto3"; + +package zitadel.settings.v2; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/settings/v2;settings"; + +import "protoc-gen-openapiv2/options/annotations.proto"; +import "zitadel/settings/v2/settings.proto"; + +message DomainSettings { + bool login_name_includes_domain = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "the username has to end with the domain of its organization" + } + ]; + bool require_org_domain_verification = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "defines if organization domains should be verified upon creation, otherwise will be created already verified" + } + ]; + bool smtp_sender_address_matches_instance_domain = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "defines if the SMTP sender address domain should match an existing domain on the instance" + } + ]; + // resource_owner_type returns if the setting is managed on the organization or on the instance + ResourceOwnerType resource_owner_type = 6 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "resource_owner_type returns if the setting is managed on the organization or on the instance"; + } + ]; +} + diff --git a/proto/zitadel/settings/v2/legal_settings.proto b/proto/zitadel/settings/v2/legal_settings.proto new file mode 100644 index 0000000000..acd34e6ccb --- /dev/null +++ b/proto/zitadel/settings/v2/legal_settings.proto @@ -0,0 +1,58 @@ +syntax = "proto3"; + +package zitadel.settings.v2; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/settings/v2;settings"; + +import "protoc-gen-openapiv2/options/annotations.proto"; +import "zitadel/settings/v2/settings.proto"; +import "validate/validate.proto"; + +message LegalAndSupportSettings { + string tos_link = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"https://zitadel.com/docs/legal/terms-of-service\""; + } + ]; + string privacy_policy_link = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"https://zitadel.com/docs/legal/privacy-policy\""; + } + ]; + string help_link = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"https://zitadel.com/docs/manuals/introduction\""; + } + ]; + string support_email = 4 [ + (validate.rules).string = {ignore_empty: true, max_len: 320, email: true}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"support-email@test.com\""; + description: "help / support email address." + } + ]; + // resource_owner_type returns if the setting is managed on the organization or on the instance + ResourceOwnerType resource_owner_type = 5 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "resource_owner_type returns if the setting is managed on the organization or on the instance"; + } + ]; + string docs_link = 6 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Link to documentation to be shown in the console."; + example: "\"https://zitadel.com/docs\""; + } +]; +string custom_link = 7 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Link to an external resource that will be available to users in the console."; + example: "\"https://external.link\""; + } +]; +string custom_link_text = 8 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "The button text that would be shown in console pointing to custom link."; + example: "\"External\""; + } +]; +} diff --git a/proto/zitadel/settings/v2/lockout_settings.proto b/proto/zitadel/settings/v2/lockout_settings.proto new file mode 100644 index 0000000000..f4fefc2709 --- /dev/null +++ b/proto/zitadel/settings/v2/lockout_settings.proto @@ -0,0 +1,29 @@ +syntax = "proto3"; + +package zitadel.settings.v2; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/settings/v2;settings"; + +import "protoc-gen-openapiv2/options/annotations.proto"; +import "zitadel/settings/v2/settings.proto"; + +message LockoutSettings { + uint64 max_password_attempts = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Maximum password check attempts before the account gets locked. Attempts are reset as soon as the password is entered correctly or the password is reset. If set to 0 the account will never be locked." + example: "\"10\"" + } + ]; + // resource_owner_type returns if the settings is managed on the organization or on the instance + ResourceOwnerType resource_owner_type = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "resource_owner_type returns if the settings is managed on the organization or on the instance"; + } + ]; + uint64 max_otp_attempts = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Maximum failed attempts for a single OTP type (TOTP, SMS, Email) before the account gets locked. Attempts are reset as soon as the OTP is entered correctly. If set to 0 the account will never be locked." + example: "\"10\"" + } + ]; +} diff --git a/proto/zitadel/settings/v2/login_settings.proto b/proto/zitadel/settings/v2/login_settings.proto new file mode 100644 index 0000000000..d7d41a8a90 --- /dev/null +++ b/proto/zitadel/settings/v2/login_settings.proto @@ -0,0 +1,152 @@ +syntax = "proto3"; + +package zitadel.settings.v2; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/settings/v2;settings"; + +import "protoc-gen-openapiv2/options/annotations.proto"; +import "zitadel/settings/v2/settings.proto"; +import "google/protobuf/duration.proto"; + +message LoginSettings { + bool allow_username_password = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "defines if a user is allowed to log in with username and password"; + } + ]; + bool allow_register = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "defines if a person is allowed to register a user on this organization"; + } + ]; + bool allow_external_idp = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "defines if a user is allowed to add a defined identity provider. E.g. Google auth"; + } + ]; + bool force_mfa = 4 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "defines if a user MUST use a multi-factor to log in"; + } + ]; + PasskeysType passkeys_type = 5 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "defines if passkeys are allowed for users" + } + ]; + bool hide_password_reset = 6 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "defines if password reset link should be shown in the login screen" + } + ]; + bool ignore_unknown_usernames = 7 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "defines if unknown username on login screen directly returns an error or always displays the password screen" + } + ]; + string default_redirect_uri = 8 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "defines where the user will be redirected to if the login is started without app context (e.g. from mail)"; + example: "\"https://acme.com/ui/console\""; + } + ]; + google.protobuf.Duration password_check_lifetime = 9 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Defines after how much time the user has to re-authenticate with the password."; + example: "\"864000s\""; + } + ]; + google.protobuf.Duration external_login_check_lifetime = 10 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Defines after how much time the user has to re-authenticate with an external provider."; + example: "\"864000s\""; + } + ]; + google.protobuf.Duration mfa_init_skip_lifetime = 11 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Defines after how much time the mfa prompt will be shown again."; + example: "\"2592000s\""; + } + ]; + google.protobuf.Duration second_factor_check_lifetime = 12 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Defines after how long the second-factor check is valid."; + example: "\"64800s\""; + } + ]; + google.protobuf.Duration multi_factor_check_lifetime = 13 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Defines how long the multi-factor check is valid."; + example: "\"43200s\""; + } + ]; + repeated SecondFactorType second_factors = 14; + repeated MultiFactorType multi_factors = 15; + // If set to true, the suffix (@domain.com) of an unknown username input on the login screen will be matched against the org domains and will redirect to the registration of that organization on success. + bool allow_domain_discovery = 16 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "If set to true, the suffix (@domain.com) of an unknown username input on the login screen will be matched against the org domains and will redirect to the registration of that organization on success." + } + ]; + bool disable_login_with_email = 17 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "defines if the user can additionally (to the login name) be identified by their verified email address" + } + ]; + bool disable_login_with_phone = 18 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "defines if the user can additionally (to the login name) be identified by their verified phone number" + } + ]; + // resource_owner_type returns if the settings is managed on the organization or on the instance + ResourceOwnerType resource_owner_type = 19 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "resource_owner_type returns if the settings is managed on the organization or on the instance"; + } + ]; + bool force_mfa_local_only = 22 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "if activated, only local authenticated users are forced to use MFA. Authentication through IDPs won't prompt a MFA step in the login." + } + ]; +} + +enum SecondFactorType { + SECOND_FACTOR_TYPE_UNSPECIFIED = 0; + // This is the type for TOTP + SECOND_FACTOR_TYPE_OTP = 1; + SECOND_FACTOR_TYPE_U2F = 2; + SECOND_FACTOR_TYPE_OTP_EMAIL = 3; + SECOND_FACTOR_TYPE_OTP_SMS = 4; +} + +enum MultiFactorType { + MULTI_FACTOR_TYPE_UNSPECIFIED = 0; + MULTI_FACTOR_TYPE_U2F_WITH_VERIFICATION = 1; +} + +enum PasskeysType { + PASSKEYS_TYPE_NOT_ALLOWED = 0; + PASSKEYS_TYPE_ALLOWED = 1; +} + +message IdentityProvider { + string id = 1; + string name = 2; + IdentityProviderType type = 3; +} + +enum IdentityProviderType { + IDENTITY_PROVIDER_TYPE_UNSPECIFIED = 0; + IDENTITY_PROVIDER_TYPE_OIDC = 1; + IDENTITY_PROVIDER_TYPE_JWT = 2; + IDENTITY_PROVIDER_TYPE_LDAP = 3; + IDENTITY_PROVIDER_TYPE_OAUTH = 4; + IDENTITY_PROVIDER_TYPE_AZURE_AD = 5; + IDENTITY_PROVIDER_TYPE_GITHUB = 6; + IDENTITY_PROVIDER_TYPE_GITHUB_ES = 7; + IDENTITY_PROVIDER_TYPE_GITLAB = 8; + IDENTITY_PROVIDER_TYPE_GITLAB_SELF_HOSTED = 9; + IDENTITY_PROVIDER_TYPE_GOOGLE = 10; + IDENTITY_PROVIDER_TYPE_SAML=11; +} diff --git a/proto/zitadel/settings/v2/password_settings.proto b/proto/zitadel/settings/v2/password_settings.proto new file mode 100644 index 0000000000..d75ccec39e --- /dev/null +++ b/proto/zitadel/settings/v2/password_settings.proto @@ -0,0 +1,60 @@ +syntax = "proto3"; + +package zitadel.settings.v2; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/settings/v2;settings"; + +import "protoc-gen-openapiv2/options/annotations.proto"; +import "zitadel/settings/v2/settings.proto"; + +message PasswordComplexitySettings { + uint64 min_length = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Defines the minimum length of a password."; + example: "\"8\"" + } + ]; + bool requires_uppercase = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "defines if the password MUST contain an upper case letter" + } + ]; + bool requires_lowercase = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "defines if the password MUST contain a lowercase letter" + } + ]; + bool requires_number = 4 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "defines if the password MUST contain a number" + } + ]; + bool requires_symbol = 5 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "defines if the password MUST contain a symbol. E.g. \"$\"" + } + ]; + // resource_owner_type returns if the settings is managed on the organization or on the instance + ResourceOwnerType resource_owner_type = 6 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "resource_owner_type returns if the settings is managed on the organization or on the instance"; + } + ]; +} + +message PasswordExpirySettings { + // Amount of days after which a password will expire. The user will be forced to change the password on the following authentication. + uint64 max_age_days = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"365\"" + } + ]; + // Amount of days after which the user should be notified of the upcoming expiry. ZITADEL will not notify the user. + uint64 expire_warn_days = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"10\"" + } + ]; + // resource_owner_type returns if the settings is managed on the organization or on the instance + ResourceOwnerType resource_owner_type = 3; +} diff --git a/proto/zitadel/settings/v2/security_settings.proto b/proto/zitadel/settings/v2/security_settings.proto new file mode 100644 index 0000000000..1045022bab --- /dev/null +++ b/proto/zitadel/settings/v2/security_settings.proto @@ -0,0 +1,31 @@ +syntax = "proto3"; + +package zitadel.settings.v2; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/settings/v2;settings"; + +import "protoc-gen-openapiv2/options/annotations.proto"; + +message SecuritySettings { + EmbeddedIframeSettings embedded_iframe = 1; + bool enable_impersonation = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "default language for the current context" + example: "\"en\"" + } + ]; +} + +message EmbeddedIframeSettings{ + bool enabled = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "states if iframe embedding is enabled or disabled" + } + ]; + repeated string allowed_origins = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "origins allowed loading ZITADEL in an iframe if enabled." + example: "[\"foo.bar.com\", \"localhost:8080\"]" + } + ]; +} diff --git a/proto/zitadel/settings/v2/settings.proto b/proto/zitadel/settings/v2/settings.proto new file mode 100644 index 0000000000..b3ca5b5ca5 --- /dev/null +++ b/proto/zitadel/settings/v2/settings.proto @@ -0,0 +1,13 @@ +syntax = "proto3"; + +package zitadel.settings.v2; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/settings/v2;settings"; + +import "protoc-gen-openapiv2/options/annotations.proto"; + +enum ResourceOwnerType { + RESOURCE_OWNER_TYPE_UNSPECIFIED = 0; + RESOURCE_OWNER_TYPE_INSTANCE = 1; + RESOURCE_OWNER_TYPE_ORG = 2; +} diff --git a/proto/zitadel/settings/v2/settings_service.proto b/proto/zitadel/settings/v2/settings_service.proto new file mode 100644 index 0000000000..cc8e5d05cc --- /dev/null +++ b/proto/zitadel/settings/v2/settings_service.proto @@ -0,0 +1,479 @@ +syntax = "proto3"; + +package zitadel.settings.v2; + +import "zitadel/protoc_gen_zitadel/v2/options.proto"; +import "zitadel/object/v2/object.proto"; +import "zitadel/settings/v2/branding_settings.proto"; +import "zitadel/settings/v2/domain_settings.proto"; +import "zitadel/settings/v2/legal_settings.proto"; +import "zitadel/settings/v2/lockout_settings.proto"; +import "zitadel/settings/v2/login_settings.proto"; +import "zitadel/settings/v2/password_settings.proto"; +import "zitadel/settings/v2/security_settings.proto"; +import "google/api/annotations.proto"; +import "google/api/field_behavior.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; +import "validate/validate.proto"; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/settings/v2;settings"; + +option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { + info: { + title: "Settings Service"; + version: "2.0"; + description: "This API is intended to manage settings in a ZITADEL instance."; + contact:{ + name: "ZITADEL" + url: "https://zitadel.com" + email: "hi@zitadel.com" + } + license: { + name: "Apache 2.0", + url: "https://github.com/zitadel/zitadel/blob/main/LICENSE"; + }; + }; + 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 SettingsService { + + // Get basic information over the instance + rpc GetGeneralSettings (GetGeneralSettingsRequest) returns (GetGeneralSettingsResponse) { + option (google.api.http) = { + get: "/v2/settings" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "policy.read" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + summary: "Get basic information over the instance"; + description: "Return the basic information of the instance for the requested context" + responses: { + key: "200" + value: { + description: "OK"; + } + }; + }; + } + + // Get the login settings + rpc GetLoginSettings (GetLoginSettingsRequest) returns (GetLoginSettingsResponse) { + option (google.api.http) = { + get: "/v2/settings/login" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "policy.read" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + summary: "Get the login settings"; + description: "Return the settings for the requested context" + responses: { + key: "200" + value: { + description: "OK"; + } + }; + }; + } + + // Get the current active identity providers + rpc GetActiveIdentityProviders (GetActiveIdentityProvidersRequest) returns (GetActiveIdentityProvidersResponse) { + option (google.api.http) = { + get: "/v2/settings/login/idps" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "policy.read" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + summary: "Get the current active identity providers"; + description: "Return the current active identity providers for the requested context" + responses: { + key: "200" + value: { + description: "OK"; + } + }; + }; + } + + // Get the password complexity settings + rpc GetPasswordComplexitySettings (GetPasswordComplexitySettingsRequest) returns (GetPasswordComplexitySettingsResponse) { + option (google.api.http) = { + get: "/v2/settings/password/complexity" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "policy.read" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + summary: "Get the password complexity settings"; + description: "Return the password complexity settings for the requested context" + responses: { + key: "200" + value: { + description: "OK"; + } + }; + }; + } + + // Get the password expiry settings + rpc GetPasswordExpirySettings (GetPasswordExpirySettingsRequest) returns (GetPasswordExpirySettingsResponse) { + option (google.api.http) = { + get: "/v2/settings/password/expiry" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "policy.read" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + summary: "Get the password expiry settings"; + description: "Return the password expiry settings for the requested context" + responses: { + key: "200" + value: { + description: "OK"; + } + }; + }; + } + + // Get the current active branding settings + rpc GetBrandingSettings (GetBrandingSettingsRequest) returns (GetBrandingSettingsResponse) { + option (google.api.http) = { + get: "/v2/settings/branding" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "policy.read" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + summary: "Get the current active branding settings"; + description: "Return the current active branding settings for the requested context" + responses: { + key: "200" + value: { + description: "OK"; + } + }; + }; + } + + // Get the domain settings + rpc GetDomainSettings (GetDomainSettingsRequest) returns (GetDomainSettingsResponse) { + option (google.api.http) = { + get: "/v2/settings/domain" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "policy.read" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + summary: "Get the domain settings"; + description: "Return the domain settings for the requested context" + responses: { + key: "200" + value: { + description: "OK"; + } + }; + }; + } + + // Get the legal and support settings + rpc GetLegalAndSupportSettings (GetLegalAndSupportSettingsRequest) returns (GetLegalAndSupportSettingsResponse) { + option (google.api.http) = { + get: "/v2/settings/legal_support" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "policy.read" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + summary: "Get the legal and support settings"; + description: "Return the legal settings for the requested context" + responses: { + key: "200" + value: { + description: "OK"; + } + }; + }; + } + + // Get the lockout settings + rpc GetLockoutSettings (GetLockoutSettingsRequest) returns (GetLockoutSettingsResponse) { + option (google.api.http) = { + get: "/v2/settings/lockout" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "policy.read" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + summary: "Get the lockout settings"; + description: "Return the lockout settings for the requested context, which define when a user will be locked" + responses: { + key: "200" + value: { + description: "OK"; + } + }; + }; + } + +// Get the security settings + rpc GetSecuritySettings(GetSecuritySettingsRequest) returns (GetSecuritySettingsResponse) { + option (google.api.http) = { + get: "/v2/settings/security"; + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "iam.policy.read" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + tags: "Settings"; + summary: "Get Security Settings"; + description: "Returns the security settings of the ZITADEL instance." + }; + } + +// Set the security settings + rpc SetSecuritySettings(SetSecuritySettingsRequest) returns (SetSecuritySettingsResponse) { + option (google.api.http) = { + put: "/v2/policies/security"; + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "iam.policy.write" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + tags: "Settings"; + summary: "Set Security Settings"; + description: "Set the security settings of the ZITADEL instance." + }; + } +} + +message GetLoginSettingsRequest { + zitadel.object.v2.RequestContext ctx = 1; +} + +message GetLoginSettingsResponse { + zitadel.object.v2.Details details = 1; + zitadel.settings.v2.LoginSettings settings = 2; +} + +message GetPasswordComplexitySettingsRequest { + zitadel.object.v2.RequestContext ctx = 1; +} + +message GetPasswordComplexitySettingsResponse { + zitadel.object.v2.Details details = 1; + zitadel.settings.v2.PasswordComplexitySettings settings = 2; +} + +message GetPasswordExpirySettingsRequest { + zitadel.object.v2.RequestContext ctx = 1; +} + +message GetPasswordExpirySettingsResponse { + zitadel.object.v2.Details details = 1; + zitadel.settings.v2.PasswordExpirySettings settings = 2; +} + +message GetBrandingSettingsRequest { + zitadel.object.v2.RequestContext ctx = 1; +} + +message GetBrandingSettingsResponse { + zitadel.object.v2.Details details = 1; + zitadel.settings.v2.BrandingSettings settings = 2; +} + +message GetDomainSettingsRequest { + zitadel.object.v2.RequestContext ctx = 1; +} + +message GetDomainSettingsResponse { + zitadel.object.v2.Details details = 1; + zitadel.settings.v2.DomainSettings settings = 2; +} + +message GetLegalAndSupportSettingsRequest { + zitadel.object.v2.RequestContext ctx = 1; +} + +message GetLegalAndSupportSettingsResponse { + zitadel.object.v2.Details details = 1; + zitadel.settings.v2.LegalAndSupportSettings settings = 2; +} + +message GetLockoutSettingsRequest { + zitadel.object.v2.RequestContext ctx = 1; +} + +message GetLockoutSettingsResponse { + zitadel.object.v2.Details details = 1; + zitadel.settings.v2.LockoutSettings settings = 2; +} + +message GetActiveIdentityProvidersRequest { + zitadel.object.v2.RequestContext ctx = 1; +} + +message GetActiveIdentityProvidersResponse { + zitadel.object.v2.ListDetails details = 1; + repeated zitadel.settings.v2.IdentityProvider identity_providers = 2; +} + +message GetGeneralSettingsRequest {} + +message GetGeneralSettingsResponse { + string default_org_id = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "default organization for the current context" + } + ]; + string default_language = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "default language for the current context" + example: "\"en\"" + } + ]; + repeated string supported_languages = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "[\"en\", \"de\", \"it\"]" + } + ]; +} + +// This is an empty request +message GetSecuritySettingsRequest{} + +message GetSecuritySettingsResponse{ + zitadel.object.v2.Details details = 1; + SecuritySettings settings = 2; +} + +message SetSecuritySettingsRequest{ + EmbeddedIframeSettings embedded_iframe = 1; + bool enable_impersonation = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "allows users to impersonate other users. The impersonator needs the appropriate `*_IMPERSONATOR` roles assigned as well" + } + ]; +} + +message SetSecuritySettingsResponse{ + zitadel.object.v2.Details details = 1; +} \ No newline at end of file diff --git a/proto/zitadel/user/v2/auth.proto b/proto/zitadel/user/v2/auth.proto new file mode 100644 index 0000000000..ff72ecbba8 --- /dev/null +++ b/proto/zitadel/user/v2/auth.proto @@ -0,0 +1,50 @@ +syntax = "proto3"; + +package zitadel.user.v2; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/user/v2;user"; + +import "google/api/field_behavior.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; +import "validate/validate.proto"; + +enum PasskeyAuthenticator { + PASSKEY_AUTHENTICATOR_UNSPECIFIED = 0; + PASSKEY_AUTHENTICATOR_PLATFORM = 1; + PASSKEY_AUTHENTICATOR_CROSS_PLATFORM = 2; +} + +message SendPasskeyRegistrationLink { + optional string url_template = 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: "\"https://example.com/passkey/register?userID={{.UserID}}&orgID={{.OrgID}}&codeID={{.CodeID}}&code={{.Code}}\""; + description: "\"Optionally set a url_template, which will be used in the mail sent by ZITADEL to guide the user to your passkey registration page. If no template is set, the default ZITADEL url will be used.\"" + } + ]; +} + +message ReturnPasskeyRegistrationCode {} + +message PasskeyRegistrationCode { + 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) = { + description: "\"id to the one time code generated by ZITADEL\""; + example: "\"e2a48d6a-362b-4db6-a1fb-34feab84dc62\""; + max_length: 200; + } + ]; + string code = 2 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "\"one time code generated by ZITADEL\""; + example: "\"SomeSpecialCode\""; + max_length: 200; + } + ]; +} diff --git a/proto/zitadel/user/v2/email.proto b/proto/zitadel/user/v2/email.proto new file mode 100644 index 0000000000..2af5369fe6 --- /dev/null +++ b/proto/zitadel/user/v2/email.proto @@ -0,0 +1,54 @@ +syntax = "proto3"; + +package zitadel.user.v2; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/user/v2;user"; + +import "google/api/annotations.proto"; +import "google/api/field_behavior.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; +import "validate/validate.proto"; + +message SetHumanEmail { + string email = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200, email: true}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"mini@mouse.com\""; + } + ]; + // if no verification is specified, an email is sent with the default url + oneof verification { + SendEmailVerificationCode send_code = 2; + ReturnEmailVerificationCode return_code = 3; + bool is_verified = 4 [(validate.rules).bool.const = true]; + } +} + +message HumanEmail { + string email = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"mini@mouse.com\""; + } + ]; + bool is_verified = 2; +} + +message SendEmailVerificationCode { + optional string url_template = 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: "\"https://example.com/email/verify?userID={{.UserID}}&code={{.Code}}&orgID={{.OrgID}}\""; + description: "\"Optionally set a url_template, which will be used in the verification mail sent by ZITADEL to guide the user to your verification page. If no template is set, the default ZITADEL url will be used.\"" + } + ]; +} + +message ReturnEmailVerificationCode {} + diff --git a/proto/zitadel/user/v2/idp.proto b/proto/zitadel/user/v2/idp.proto new file mode 100644 index 0000000000..528242d9fe --- /dev/null +++ b/proto/zitadel/user/v2/idp.proto @@ -0,0 +1,164 @@ +syntax = "proto3"; + +package zitadel.user.v2; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/user/v2;user"; + +import "google/api/field_behavior.proto"; +import "google/protobuf/struct.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; +import "validate/validate.proto"; + +message LDAPCredentials { + string username = 1[ + (validate.rules).string = {min_len: 1, max_len: 200, uri_ref: true}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Username used to login through LDAP" + min_length: 1; + max_length: 200; + example: "\"username\""; + } + ]; + string password = 2[ + (validate.rules).string = {min_len: 1, max_len: 200, uri_ref: true}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Password used to login through LDAP" + min_length: 1; + max_length: 200; + example: "\"Password1!\""; + } + ]; +} + +message RedirectURLs { + string success_url = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200, uri_ref: true}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "URL on which the user will be redirected after a successful login" + min_length: 1; + max_length: 200; + example: "\"https://custom.com/login/idp/success\""; + } + ]; + string failure_url = 2 [ + (validate.rules).string = {min_len: 1, max_len: 200, uri_ref: true}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "URL on which the user will be redirected after a failed login" + min_length: 1; + max_length: 200; + example: "\"https://custom.com/login/idp/fail\""; + } + ]; +} + +message IDPIntent { + string idp_intent_id = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "ID of the IDP intent" + min_length: 1; + max_length: 200; + example: "\"163840776835432705\""; + } + ]; + string idp_intent_token = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "token of the IDP intent" + min_length: 1; + max_length: 200; + example: "\"SJKL3ioIDpo342ioqw98fjp3sdf32wahb=\""; + } + ]; + string user_id = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "ID of the ZITADEL user if external user already linked" + max_length: 200; + example: "\"163840776835432345\""; + } + ]; +} + +message IDPInformation{ + oneof access{ + IDPOAuthAccessInformation oauth = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "OAuth/OIDC access (and id_token) returned by the identity provider" + } + ]; + IDPLDAPAccessInformation ldap = 6 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "LDAP entity attributes returned by the identity provider" + } + ]; + IDPSAMLAccessInformation saml = 7 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "SAMLResponse returned by the identity provider" + } + ]; + } + string idp_id = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "ID of the identity provider" + example: "\"d654e6ba-70a3-48ef-a95d-37c8d8a7901a\""; + } + ]; + string user_id = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "ID of the user of the identity provider" + example: "\"6516849804890468048461403518\""; + } + ]; + string user_name = 4 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "username of the user of the identity provider" + example: "\"user@external.com\""; + } + ]; + google.protobuf.Struct raw_information = 5 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "complete information returned by the identity provider" + } + ]; +} + +message IDPOAuthAccessInformation{ + string access_token = 1; + optional string id_token = 2; +} + +message IDPLDAPAccessInformation{ + google.protobuf.Struct attributes = 1; +} + +message IDPSAMLAccessInformation{ + bytes assertion = 1; +} + +message IDPLink { + string idp_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "ID of the identity provider" + min_length: 1; + max_length: 200; + example: "\"d654e6ba-70a3-48ef-a95d-37c8d8a7901a\""; + } + ]; + string user_id = 2 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "ID of the user of the identity provider" + min_length: 1; + max_length: 200; + example: "\"6516849804890468048461403518\""; + } + ]; + string user_name = 3 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "username of the user of the identity provider" + min_length: 1; + max_length: 200; + example: "\"user@external.com\""; + } + ]; +} diff --git a/proto/zitadel/user/v2/password.proto b/proto/zitadel/user/v2/password.proto new file mode 100644 index 0000000000..e306788f4c --- /dev/null +++ b/proto/zitadel/user/v2/password.proto @@ -0,0 +1,85 @@ +syntax = "proto3"; + +package zitadel.user.v2; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/user/v2;user"; + +import "google/api/field_behavior.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; +import "validate/validate.proto"; + +message Password { + string password = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"Secr3tP4ssw0rd!\""; + min_length: 1, + max_length: 200; + } + ]; + bool change_required = 2; +} + +message HashedPassword { + string hash = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"$2a$12$lJ08fqVr8bFJilRVnDT9QeULI7YW.nT3iwUv6dyg0aCrfm3UY8XR2\""; + description: "\"Encoded hash of a password in Modular Crypt Format: https://zitadel.com/docs/concepts/architecture/secrets#hashed-secrets\""; + min_length: 1, + max_length: 200; + } + ]; + bool change_required = 2; +} + +message SendPasswordResetLink { + NotificationType notification_type = 1; + optional string url_template = 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: "\"https://example.com/password/changey?userID={{.UserID}}&code={{.Code}}&orgID={{.OrgID}}\""; + description: "\"Optionally set a url_template, which will be used in the password reset mail sent by ZITADEL to guide the user to your password change page. If no template is set, the default ZITADEL url will be used.\"" + } + ]; +} + +message ReturnPasswordResetCode {} + +enum NotificationType { + NOTIFICATION_TYPE_Unspecified = 0; + NOTIFICATION_TYPE_Email = 1; + NOTIFICATION_TYPE_SMS = 2; +} + +message SetPassword { + oneof password_type { + Password password = 1; + HashedPassword hashed_password = 2; + } + oneof verification { + string current_password = 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: "\"Secr3tP4ssw0rd!\""; + } + ]; + string verification_code = 4 [ + (validate.rules).string = {min_len: 1, max_len: 20}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 20; + example: "\"SKJd342k\""; + description: "\"the verification code generated during password reset request\""; + } + ]; + } +} \ No newline at end of file diff --git a/proto/zitadel/user/v2/phone.proto b/proto/zitadel/user/v2/phone.proto new file mode 100644 index 0000000000..115cfdbdc0 --- /dev/null +++ b/proto/zitadel/user/v2/phone.proto @@ -0,0 +1,40 @@ +syntax = "proto3"; + +package zitadel.user.v2; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/user/v2;user"; + +import "google/api/annotations.proto"; +import "google/api/field_behavior.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; +import "validate/validate.proto"; + +message SetHumanPhone { + string phone = 1 [ + (validate.rules).string = {min_len: 0, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + max_length: 200; + example: "\"+41791234567\""; + } + ]; + oneof verification { + SendPhoneVerificationCode send_code = 2; + ReturnPhoneVerificationCode return_code = 3; + bool is_verified = 4 [(validate.rules).bool.const = true]; + } +} + +message HumanPhone { + string phone = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + max_length: 200; + example: "\"+41791234567\""; + } + ]; + bool is_verified = 2; +} + +message SendPhoneVerificationCode {} + +message ReturnPhoneVerificationCode {} + diff --git a/proto/zitadel/user/v2/query.proto b/proto/zitadel/user/v2/query.proto new file mode 100644 index 0000000000..53f3446bca --- /dev/null +++ b/proto/zitadel/user/v2/query.proto @@ -0,0 +1,268 @@ +syntax = "proto3"; + +package zitadel.user.v2; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/user/v2;user"; + +import "google/api/annotations.proto"; +import "google/api/field_behavior.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; +import "validate/validate.proto"; +import "zitadel/user/v2/user.proto"; +import "zitadel/object/v2/object.proto"; + +message SearchQuery { + oneof query { + option (validate.required) = true; + + UserNameQuery user_name_query = 1; + FirstNameQuery first_name_query = 2; + LastNameQuery last_name_query = 3; + NickNameQuery nick_name_query = 4; + DisplayNameQuery display_name_query = 5; + EmailQuery email_query = 6; + StateQuery state_query = 7; + TypeQuery type_query = 8; + LoginNameQuery login_name_query = 9; + InUserIDQuery in_user_ids_query = 10; + OrQuery or_query = 11; + AndQuery and_query = 12; + NotQuery not_query = 13; + InUserEmailsQuery in_user_emails_query = 14; + OrganizationIdQuery organization_id_query = 15; + } +} + +// Connect multiple sub-condition with and OR operator. +message OrQuery { + repeated SearchQuery queries = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "the sub queries to 'OR'" + } + ]; +} + +// Connect multiple sub-condition with and AND operator. +message AndQuery { + repeated SearchQuery queries = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "the sub queries to 'AND'" + } + ]; +} + +// Negate the sub-condition. +message NotQuery { + SearchQuery query = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "the sub query to negate (NOT)" + } + ]; +} + +// Query for users with ID in list of IDs. +message InUserIDQuery { + repeated string user_ids = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "the ids of the users to include" + example: "[\"69629023906488334\",\"69622366012355662\"]"; + } + ]; +} + +// Query for users with a specific user name. +message UserNameQuery { + string user_name = 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: "\"gigi-giraffe\""; + } + ]; + 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"; + } + ]; +} + +// Query for users with a specific first name. +message FirstNameQuery { + string first_name = 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: "\"Gigi\""; + } + ]; + 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"; + } + ]; +} + +// Query for users with a specific last name. +message LastNameQuery { + string last_name = 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: "\"Giraffe\""; + } + ]; + 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"; + } + ]; +} + +// Query for users with a specific nickname. +message NickNameQuery { + string nick_name = 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: "\"Gigi\""; + } + ]; + 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"; + } + ]; +} + +// Query for users with a specific display name. +message DisplayNameQuery { + string display_name = 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: "\"Gigi Giraffe\""; + } + ]; + 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"; + } + ]; +} + +// Query for users with a specific email. +message EmailQuery { + string email_address = 1 [ + (validate.rules).string = {max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "email address of the user" + max_length: 200; + example: "\"gigi@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"; + } + ]; +} + +// Query for users with a specific state. +message LoginNameQuery { + string login_name = 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: "\"gigi@zitadel.cloud\""; + } + ]; + 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"; + } + ]; +} + +// Query for users with a specific state. +message StateQuery { + UserState state = 1 [ + (validate.rules).enum.defined_only = true, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "current state of the user"; + } + ]; +} + +// Query for users with a specific type. +message TypeQuery { + Type type = 1 [ + (validate.rules).enum.defined_only = true, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "the type of the user"; + } + ]; +} + +// Query for users with email in list of emails. +message InUserEmailsQuery { + repeated string user_emails = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "the emails of the users to include" + example: "[\"test@example.com\",\"test@example.org\"]"; + } + ]; +} + +// Query for users under a specific organization as resource owner. +message OrganizationIdQuery { + 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: "\"69629023906488334\"" + } + ]; +} + +enum Type { + TYPE_UNSPECIFIED = 0; + TYPE_HUMAN = 1; + TYPE_MACHINE = 2; +} + +enum UserFieldName { + USER_FIELD_NAME_UNSPECIFIED = 0; + USER_FIELD_NAME_USER_NAME = 1; + USER_FIELD_NAME_FIRST_NAME = 2; + USER_FIELD_NAME_LAST_NAME = 3; + USER_FIELD_NAME_NICK_NAME = 4; + USER_FIELD_NAME_DISPLAY_NAME = 5; + USER_FIELD_NAME_EMAIL = 6; + USER_FIELD_NAME_STATE = 7; + USER_FIELD_NAME_TYPE = 8; + USER_FIELD_NAME_CREATION_DATE = 9; +} diff --git a/proto/zitadel/user/v2/user.proto b/proto/zitadel/user/v2/user.proto new file mode 100644 index 0000000000..9a0794197e --- /dev/null +++ b/proto/zitadel/user/v2/user.proto @@ -0,0 +1,284 @@ +syntax = "proto3"; + +package zitadel.user.v2; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/user/v2;user"; + +import "google/api/field_behavior.proto"; +import "google/protobuf/timestamp.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; +import "validate/validate.proto"; +import "zitadel/object/v2/object.proto"; +import "zitadel/user/v2/email.proto"; +import "zitadel/user/v2/phone.proto"; + +enum Gender { + GENDER_UNSPECIFIED = 0; + GENDER_FEMALE = 1; + GENDER_MALE = 2; + GENDER_DIVERSE = 3; +} + +message SetHumanProfile { + string given_name = 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: "\"Minnie\""; + } + ]; + string family_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: "\"Mouse\""; + } + ]; + optional string nick_name = 3 [ + (validate.rules).string = {max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + max_length: 200; + example: "\"Mini\""; + } + ]; + optional string display_name = 4 [ + (validate.rules).string = {max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + max_length: 200; + example: "\"Minnie Mouse\""; + } + ]; + optional string preferred_language = 5 [ + (validate.rules).string = {max_len: 10}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + max_length: 10; + example: "\"en\""; + } + ]; + optional Gender gender = 6 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"GENDER_FEMALE\""; + } + ]; +} + +message HumanProfile { + string given_name = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"Minnie\""; + } + ]; + string family_name = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"Mouse\""; + } + ]; + optional string nick_name = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + max_length: 200; + example: "\"Mini\""; + } + ]; + optional string display_name = 4 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + max_length: 200; + example: "\"Minnie Mouse\""; + } + ]; + optional string preferred_language = 5 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + max_length: 10; + example: "\"en\""; + } + ]; + optional Gender gender = 6 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"GENDER_FEMALE\""; + } + ]; + string avatar_url = 7 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "avatar URL of the user" + example: "\"https://api.zitadel.ch/assets/v1/avatar-32432jkh4kj32\""; + } + ]; +} + +message SetMetadataEntry { + string key = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"my-key\""; + min_length: 1, + max_length: 200; + } + ]; + bytes value = 2 [ + (validate.rules).bytes = {min_len: 1, max_len: 500000}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "The value has to be base64 encoded."; + example: "\"VGhpcyBpcyBteSB0ZXN0IHZhbHVl\""; + min_length: 1, + max_length: 500000; + } + ]; +} + +message HumanUser { + // Unique identifier of the user. + string user_id = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"d654e6ba-70a3-48ef-a95d-37c8d8a7901a\""; + } + ]; + // State of the user, for example active, inactive, locked, deleted, initial. + UserState state = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "current state of the user"; + } + ]; + // Username of the user, which can be globally unique or unique on organization level. + string username = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"minnie-mouse\""; + } + ]; + // Possible usable login names for the user. + repeated string login_names = 4 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "[\"gigi@zitadel.com\", \"gigi@zitadel.zitadel.ch\"]"; + } + ]; + // Preferred login name of the user. + string preferred_login_name = 5 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"gigi@zitadel.com\""; + } + ]; + // Profile information of the user. + HumanProfile profile = 6; + // Email of the user, if defined. + HumanEmail email = 7; + // Phone of the user, if defined. + HumanPhone phone = 8; + // User is required to change the used password on the next login. + bool password_change_required = 9; + // The time the user last changed their password. + google.protobuf.Timestamp password_changed = 10; +} + +message User { + string user_id = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"d654e6ba-70a3-48ef-a95d-37c8d8a7901a\""; + } + ]; + zitadel.object.v2.Details details = 8; + UserState state = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "current state of the user"; + } + ]; + string username = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"minnie-mouse\""; + } + ]; + repeated string login_names = 4 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "[\"gigi@zitadel.com\", \"gigi@zitadel.zitadel.ch\"]"; + } + ]; + string preferred_login_name = 5 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"gigi@zitadel.com\""; + } + ]; + oneof type { + HumanUser human = 6 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "one of type use human or machine" + } + ]; + MachineUser machine = 7 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "one of type use human or machine" + } + ]; + } +} + +message MachineUser { + string name = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"zitadel\""; + } + ]; + string description = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"The one and only IAM\""; + } + ]; + bool has_secret = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"true\""; + } + ]; + AccessTokenType access_token_type = 4 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Type of access token to receive"; + } + ]; +} + +enum AccessTokenType { + ACCESS_TOKEN_TYPE_BEARER = 0; + ACCESS_TOKEN_TYPE_JWT = 1; +} + +enum UserState { + USER_STATE_UNSPECIFIED = 0; + USER_STATE_ACTIVE = 1; + USER_STATE_INACTIVE = 2; + USER_STATE_DELETED = 3; + USER_STATE_LOCKED = 4; + USER_STATE_INITIAL = 5; +} + + +message Passkey { + string id = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629023906488334\"" + } + ]; + AuthFactorState state = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "current state of the passkey"; + } + ]; + string name = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"fido key\"" + } + ]; +} + +enum AuthFactorState { + AUTH_FACTOR_STATE_UNSPECIFIED = 0; + AUTH_FACTOR_STATE_NOT_READY = 1; + AUTH_FACTOR_STATE_READY = 2; + AUTH_FACTOR_STATE_REMOVED = 3; +} diff --git a/proto/zitadel/user/v2/user_service.proto b/proto/zitadel/user/v2/user_service.proto new file mode 100644 index 0000000000..11230594dc --- /dev/null +++ b/proto/zitadel/user/v2/user_service.proto @@ -0,0 +1,2081 @@ +syntax = "proto3"; + +package zitadel.user.v2; + +import "zitadel/object/v2/object.proto"; +import "zitadel/protoc_gen_zitadel/v2/options.proto"; +import "zitadel/user/v2/auth.proto"; +import "zitadel/user/v2/email.proto"; +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/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"; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/user/v2;user"; + +option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { + info: { + title: "User Service"; + version: "2.0"; + description: "This API is intended to manage users in a ZITADEL instance."; + contact:{ + name: "ZITADEL" + url: "https://zitadel.com" + email: "hi@zitadel.com" + } + license: { + name: "Apache 2.0", + url: "https://github.com/zitadel/zitadel/blob/main/LICENSE"; + }; + }; + 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 UserService { + + // Create a new human user + // + // 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) = { + post: "/v2/users/human" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "user.write" + org_field: "organization" + } + http_response: { + success_code: 201 + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200" + value: { + description: "OK"; + } + }; + }; + } + + // User by ID + // + // Returns the full user object (human or machine) including the profile, email, etc.. + rpc GetUserByID(GetUserByIDRequest) returns (GetUserByIDResponse) { + option (google.api.http) = { + get: "/v2/users/{user_id}" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + http_response: { + success_code: 200 + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200" + value: { + description: "OK"; + } + }; + }; + } + + // Search Users + // + // Search for users. By default, we will return users of your organization. Make sure to include a limit and sorting for pagination.. + rpc ListUsers(ListUsersRequest) returns (ListUsersResponse) { + option (google.api.http) = { + post: "/v2/users" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + http_response: { + success_code: 200 + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "A list of all users matching the query"; + }; + }; + responses: { + key: "400"; + value: { + description: "invalid list query"; + schema: { + json_schema: { + ref: "#/definitions/rpcStatus"; + }; + }; + }; + }; + }; + } + + // Change the user email + // + // 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) = { + post: "/v2/users/{user_id}/email" + 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"; + } + }; + }; + } + + // 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" + 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"; + } + }; + }; + } + + // Verify the email + // + // Verify the email with the generated code.. + rpc VerifyEmail (VerifyEmailRequest) returns (VerifyEmailResponse) { + option (google.api.http) = { + post: "/v2/users/{user_id}/email/verify" + 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"; + } + }; + }; + } + + // Set the user phone + // + // 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) = { + post: "/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) = { + responses: { + key: "200" + value: { + description: "OK"; + } + }; + }; + } + + // Remove the user phone + // + // Remove the user phone + 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) = { + summary: "Delete the user phone"; + description: "Delete the phone number of a user." + responses: { + key: "200" + value: { + description: "OK"; + } + }; + }; + } + + // 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" + 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"; + } + }; + }; + } + + // Verify the phone + // + // Verify the phone with the generated code.. + rpc VerifyPhone (VerifyPhoneRequest) returns (VerifyPhoneResponse) { + option (google.api.http) = { + post: "/v2/users/{user_id}/phone/verify" + 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"; + } + }; + }; + } + + // Update User + // + // Update all information from a user.. + rpc UpdateHumanUser(UpdateHumanUserRequest) returns (UpdateHumanUserResponse) { + option (google.api.http) = { + put: "/v2/users/human/{user_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: "OK"; + } + }; + }; + } + + // Deactivate user + // + // The state of the user will be changed to 'deactivated'. The user will not be able to log in anymore. The endpoint returns an error if the user is already in the state 'deactivated'. Use deactivate user when the user should not be able to use the account anymore, but you still need access to the user data.. + rpc DeactivateUser(DeactivateUserRequest) returns (DeactivateUserResponse) { + option (google.api.http) = { + post: "/v2/users/{user_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: "OK"; + } + }; + }; + } + + // Reactivate user + // + // Reactivate a user with the state 'deactivated'. The user will be able to log in again afterward. The endpoint returns an error if the user is not in the state 'deactivated'.. + rpc ReactivateUser(ReactivateUserRequest) returns (ReactivateUserResponse) { + option (google.api.http) = { + post: "/v2/users/{user_id}/reactivate" + 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"; + } + }; + }; + } + + // Lock user + // + // The state of the user will be changed to 'locked'. The user will not be able to log in anymore. The endpoint returns an error if the user is already in the state 'locked'. Use this endpoint if the user should not be able to log in temporarily because of an event that happened (wrong password, etc.).. + rpc LockUser(LockUserRequest) returns (LockUserResponse) { + option (google.api.http) = { + post: "/v2/users/{user_id}/lock" + 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"; + } + }; + }; + } + + // Unlock user + // + // The state of the user will be changed to 'locked'. The user will not be able to log in anymore. The endpoint returns an error if the user is already in the state 'locked'. Use this endpoint if the user should not be able to log in temporarily because of an event that happened (wrong password, etc.).. + rpc UnlockUser(UnlockUserRequest) returns (UnlockUserResponse) { + option (google.api.http) = { + post: "/v2/users/{user_id}/unlock" + 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"; + } + }; + }; + } + + // Delete user + // + // The state of the user will be changed to 'deleted'. The user will not be able to log in anymore. Endpoints requesting this user will return an error 'User not found.. + rpc DeleteUser(DeleteUserRequest) returns (DeleteUserResponse) { + option (google.api.http) = { + delete: "/v2/users/{user_id}" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "user.delete" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200" + value: { + description: "OK"; + } + }; + }; + } + + // Start the registration of passkey for a user + // + // Start the registration of a passkey for a user, as a response the public key credential creation options are returned, which are used to verify the passkey.. + rpc RegisterPasskey (RegisterPasskeyRequest) returns (RegisterPasskeyResponse) { + option (google.api.http) = { + post: "/v2/users/{user_id}/passkeys" + 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"; + } + }; + }; + } + + // Verify a passkey for a user + // + // Verify the passkey registration with the public key credential.. + rpc VerifyPasskeyRegistration (VerifyPasskeyRegistrationRequest) returns (VerifyPasskeyRegistrationResponse) { + option (google.api.http) = { + post: "/v2/users/{user_id}/passkeys/{passkey_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: "OK"; + } + }; + }; + } + + // Create a passkey registration link for a user + // + // Create a passkey registration link which includes a code and either return it or send it to the user.. + rpc CreatePasskeyRegistrationLink (CreatePasskeyRegistrationLinkRequest) returns (CreatePasskeyRegistrationLinkResponse) { + option (google.api.http) = { + post: "/v2/users/{user_id}/passkeys/registration_link" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "user.passkey.write" + } + }; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200" + value: { + description: "OK"; + } + }; + }; + } + + // List passkeys of an user + // + // List passkeys of an user + rpc ListPasskeys (ListPasskeysRequest) returns (ListPasskeysResponse) { + option (google.api.http) = { + post: "/v2/users/{user_id}/passkeys/_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: "OK"; + } + }; + }; + } + + // Remove passkey from a user + // + // Remove passkey from a user. + rpc RemovePasskey (RemovePasskeyRequest) returns (RemovePasskeyResponse) { + option (google.api.http) = { + delete: "/v2/users/{user_id}/passkeys/{passkey_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: "OK"; + } + }; + }; + } + + // Start the registration of a u2f token for a user + // + // Start the registration of a u2f token for a user, as a response the public key credential creation options are returned, which are used to verify the u2f token.. + rpc RegisterU2F (RegisterU2FRequest) returns (RegisterU2FResponse) { + option (google.api.http) = { + post: "/v2/users/{user_id}/u2f" + 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"; + } + }; + }; + } + + // Verify a u2f token for a user + // + // Verify the u2f token registration with the public key credential.. + rpc VerifyU2FRegistration (VerifyU2FRegistrationRequest) returns (VerifyU2FRegistrationResponse) { + option (google.api.http) = { + post: "/v2/users/{user_id}/u2f/{u2f_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: "OK"; + } + }; + }; + } + + // Remove u2f token from a user + // + // Remove u2f token from a user. + rpc RemoveU2F (RemoveU2FRequest) returns (RemoveU2FResponse) { + option (google.api.http) = { + delete: "/v2/users/{user_id}/u2f/{u2f_id}" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + summary: "Remove u2f token from a user"; + description: "Remove u2f token from a user" + responses: { + key: "200" + value: { + description: "OK"; + } + }; + }; + } + + // Start the registration of a TOTP generator for a user + // + // Start the registration of a TOTP generator for a user, as a response a secret returned, which is used to initialize a TOTP app or device.. + rpc RegisterTOTP (RegisterTOTPRequest) returns (RegisterTOTPResponse) { + option (google.api.http) = { + post: "/v2/users/{user_id}/totp" + 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"; + } + }; + }; + } + + // Verify a TOTP generator for a user + // + // Verify the TOTP registration with a generated code.. + rpc VerifyTOTPRegistration (VerifyTOTPRegistrationRequest) returns (VerifyTOTPRegistrationResponse) { + option (google.api.http) = { + post: "/v2/users/{user_id}/totp/verify" + 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"; + } + }; + }; + } + + // Remove TOTP generator from a user + // + // Remove the configured TOTP generator of a user. As only one TOTP generator per user is allowed, the user will not have TOTP as a second-factor afterward.. + rpc RemoveTOTP (RemoveTOTPRequest) returns (RemoveTOTPResponse) { + option (google.api.http) = { + delete: "/v2/users/{user_id}/totp" + }; + + 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"; + } + }; + }; + } + + // Add OTP SMS for a user + // + // Add a new One-Time-Password (OTP) SMS factor to the authenticated user. OTP SMS will enable the user to verify a OTP with the latest verified phone number. The phone number has to be verified to add the second factor.. + rpc AddOTPSMS (AddOTPSMSRequest) returns (AddOTPSMSResponse) { + option (google.api.http) = { + post: "/v2/users/{user_id}/otp_sms" + 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"; + } + }; + }; + } + + // Remove One-Time-Password (OTP) SMS from a user + // + // Remove the configured One-Time-Password (OTP) SMS factor of a user. As only one OTP SMS per user is allowed, the user will not have OTP SMS as a second-factor afterward.. + rpc RemoveOTPSMS (RemoveOTPSMSRequest) returns (RemoveOTPSMSResponse) { + option (google.api.http) = { + delete: "/v2/users/{user_id}/otp_sms" + }; + + 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"; + } + }; + }; + } + + // Add OTP Email for a user + // + // Add a new One-Time-Password (OTP) Email factor to the authenticated user. OTP Email will enable the user to verify a OTP with the latest verified email. The email has to be verified to add the second factor.. + rpc AddOTPEmail (AddOTPEmailRequest) returns (AddOTPEmailResponse) { + option (google.api.http) = { + post: "/v2/users/{user_id}/otp_email" + 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"; + } + }; + }; + } + + // Remove One-Time-Password (OTP) Email from a user + // + // Remove the configured One-Time-Password (OTP) Email factor of a user. As only one OTP Email per user is allowed, the user will not have OTP Email as a second-factor afterward.. + rpc RemoveOTPEmail (RemoveOTPEmailRequest) returns (RemoveOTPEmailResponse) { + option (google.api.http) = { + delete: "/v2/users/{user_id}/otp_email" + }; + + 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"; + } + }; + }; + } + + // Start flow with an identity provider + // + // Start a flow with an identity provider, for external login, registration or linking.. + rpc StartIdentityProviderIntent (StartIdentityProviderIntentRequest) returns (StartIdentityProviderIntentResponse) { + option (google.api.http) = { + post: "/v2/idp_intents" + 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"; + } + }; + }; + } + + // Retrieve the information returned by the identity provider + // + // Retrieve the information returned by the identity provider for registration or updating an existing user with new information.. + rpc RetrieveIdentityProviderIntent (RetrieveIdentityProviderIntentRequest) returns (RetrieveIdentityProviderIntentResponse) { + option (google.api.http) = { + post: "/v2/idp_intents/{idp_intent_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: "OK"; + } + }; + }; + } + + // Add link to an identity provider to an user + // + // Add link to an identity provider to an user.. + rpc AddIDPLink (AddIDPLinkRequest) returns (AddIDPLinkResponse) { + option (google.api.http) = { + post: "/v2/users/{user_id}/links" + 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"; + } + }; + }; + } + + // List links to an identity provider of an user + // + // List links to an identity provider of an user. + rpc ListIDPLinks (ListIDPLinksRequest) returns (ListIDPLinksResponse) { + option (google.api.http) = { + post: "/v2/users/{user_id}/links/_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: "OK"; + } + }; + }; + } + + // Remove link of an identity provider to an user + // + // Remove link of an identity provider to an user. + rpc RemoveIDPLink (RemoveIDPLinkRequest) returns (RemoveIDPLinkResponse) { + option (google.api.http) = { + delete: "/v2/users/{user_id}/links/{idp_id}/{linked_user_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: "OK"; + } + }; + }; + } + + // Request a code to reset a password + // + // Request a code to reset a password.. + rpc PasswordReset (PasswordResetRequest) returns (PasswordResetResponse) { + option (google.api.http) = { + post: "/v2/users/{user_id}/password_reset" + 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"; + } + }; + }; + } + + // Change password + // + // Change the password of a user with either a verification code or the current password.. + rpc SetPassword (SetPasswordRequest) returns (SetPasswordResponse) { + option (google.api.http) = { + post: "/v2/users/{user_id}/password" + 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"; + } + }; + }; + } + + // List all possible authentication methods of a user + // + // List all possible authentication methods of a user like password, passwordless, (T)OTP and more.. + rpc ListAuthenticationMethodTypes (ListAuthenticationMethodTypesRequest) returns (ListAuthenticationMethodTypesResponse) { + option (google.api.http) = { + get: "/v2/users/{user_id}/authentication_methods" + }; + + 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"; + } + }; + }; + } +} + +message AddHumanUserRequest{ + reserved 3; + reserved "organisation"; + // optionally set your own id unique for the user. + optional 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: "\"d654e6ba-70a3-48ef-a95d-37c8d8a7901a\""; + } + ]; + // optionally set a unique username, if none is provided the email will be used. + 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\""; + } + ]; + zitadel.object.v2.Organization organization = 11; + SetHumanProfile profile = 4 [ + (validate.rules).message.required = true, + (google.api.field_behavior) = REQUIRED + ]; + SetHumanEmail email = 5 [ + (validate.rules).message.required = true, + (google.api.field_behavior) = REQUIRED + ]; + SetHumanPhone phone = 10; + repeated SetMetadataEntry metadata = 6; + oneof password_type { + Password password = 7; + HashedPassword hashed_password = 8; + } + repeated IDPLink idp_links = 9; + + // 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 = 12 [ + (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 AddHumanUserResponse { + string user_id = 1; + zitadel.object.v2.Details details = 2; + optional string email_code = 3; + optional string phone_code = 4; +} + +message GetUserByIDRequest { + reserved 2; + reserved "organization"; + string user_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\""; + description: "User ID of the user you like to get." + } + ]; +} + +message GetUserByIDResponse { + //deprecated: details is moved into user + zitadel.object.v2.Details details = 1; + zitadel.user.v2.User user = 2; +} + +message ListUsersRequest { + //list limitations and ordering + zitadel.object.v2.ListQuery query = 1; + // the field the result is sorted + zitadel.user.v2.UserFieldName sorting_column = 2; + //criteria the client is looking for + repeated zitadel.user.v2.SearchQuery queries = 3; +} + +message ListUsersResponse { + zitadel.object.v2.ListDetails details = 1; + zitadel.user.v2.UserFieldName sorting_column = 2; + repeated zitadel.user.v2.User result = 3; +} + +message SetEmailRequest{ + string user_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\""; + } + ]; + string email = 2 [ + (validate.rules).string = {min_len: 1, max_len: 200, email: true}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"mini@mouse.com\""; + } + ]; + // if no verification is specified, an email is sent with the default url + oneof verification { + SendEmailVerificationCode send_code = 3; + ReturnEmailVerificationCode return_code = 4; + bool is_verified = 5 [(validate.rules).bool.const = true]; + } +} + +message SetEmailResponse{ + zitadel.object.v2.Details details = 1; + // in case the verification was set to return_code, the code will be returned + optional string verification_code = 2; +} + +message ResendEmailCodeRequest{ + string user_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\""; + } + ]; + // if no verification is specified, an email is sent with the default url + oneof verification { + SendEmailVerificationCode send_code = 2; + ReturnEmailVerificationCode return_code = 3; + } +} + +message ResendEmailCodeResponse{ + zitadel.object.v2.Details details = 1; + // in case the verification was set to return_code, the code will be returned + optional string verification_code = 2; +} + +message VerifyEmailRequest{ + string user_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\""; + } + ]; + string verification_code = 2 [ + (validate.rules).string = {min_len: 1, max_len: 20}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 20; + example: "\"SKJd342k\""; + description: "\"the verification code generated during the set email request\""; + } + ]; +} + +message VerifyEmailResponse{ + zitadel.object.v2.Details details = 1; +} + +message SetPhoneRequest{ + string user_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\""; + } + ]; + string phone = 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: "\"+41791234567\""; + } + ]; + // if no verification is specified, an sms is sent + oneof verification { + SendPhoneVerificationCode send_code = 3; + ReturnPhoneVerificationCode return_code = 4; + bool is_verified = 5 [(validate.rules).bool.const = true]; + } +} + +message SetPhoneResponse{ + zitadel.object.v2.Details details = 1; + // in case the verification was set to return_code, the code will be returned + optional string verification_code = 2; +} + +message RemovePhoneRequest{ + string user_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 RemovePhoneResponse{ + zitadel.object.v2.Details details = 1; +} + +message ResendPhoneCodeRequest{ + string user_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\""; + } + ]; + // if no verification is specified, an sms is sent + oneof verification { + SendPhoneVerificationCode send_code = 3; + ReturnPhoneVerificationCode return_code = 4; + } +} + +message ResendPhoneCodeResponse{ + zitadel.object.v2.Details details = 1; + // in case the verification was set to return_code, the code will be returned + optional string verification_code = 2; +} + +message VerifyPhoneRequest{ + string user_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\""; + } + ]; + string verification_code = 2 [ + (validate.rules).string = {min_len: 1, max_len: 20}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 20; + example: "\"SKJd342k\""; + description: "\"the verification code generated during the set phone request\""; + } + ]; +} + +message VerifyPhoneResponse{ + zitadel.object.v2.Details details = 1; +} + +message DeleteUserRequest { + string user_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\""; + }]; +} + +message DeleteUserResponse { + zitadel.object.v2.Details details = 1; +} + +message UpdateHumanUserRequest{ + 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: "\"d654e6ba-70a3-48ef-a95d-37c8d8a7901a\""; + } + ]; + 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\""; + } + ]; + optional SetHumanProfile profile = 3; + optional SetHumanEmail email = 4; + optional SetHumanPhone phone = 5; + optional SetPassword password = 6; +} + +message UpdateHumanUserResponse { + zitadel.object.v2.Details details = 1; + optional string email_code = 2; + optional string phone_code = 3; +} + +message DeactivateUserRequest { + string user_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\""; + } + ]; +} + +message DeactivateUserResponse { + zitadel.object.v2.Details details = 1; +} + + +message ReactivateUserRequest { + string user_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\""; + } + ]; +} + +message ReactivateUserResponse { + zitadel.object.v2.Details details = 1; +} + +message LockUserRequest { + string user_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\""; + } + ]; +} + +message LockUserResponse { + zitadel.object.v2.Details details = 1; +} + +message UnlockUserRequest { + string user_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\""; + } + ]; +} + +message UnlockUserResponse { + zitadel.object.v2.Details details = 1; +} + +message RegisterPasskeyRequest{ + string user_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: "\"163840776835432705\""; + } + ]; + optional PasskeyRegistrationCode code = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "\"one time code generated by ZITADEL; required to start the passkey registration without user authentication\""; + } + ]; + PasskeyAuthenticator authenticator = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "\"Optionally specify the authenticator type of the passkey device (platform or cross-platform). If none is provided, both values are allowed.\""; + } + ]; + string domain = 4 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "\"Domain on which the user is authenticated.\""; + } + ]; +} + +message RegisterPasskeyResponse{ + zitadel.object.v2.Details details = 1; + string passkey_id = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"163840776835432705\"" + } + ]; + google.protobuf.Struct public_key_credential_creation_options = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Options for Credential Creation (dictionary PublicKeyCredentialCreationOptions). Generated helper methods transform the field to JSON, for use in a WebauthN client. See also: https://www.w3.org/TR/webauthn/#dictdef-publickeycredentialcreationoptions" + example: "{\"publicKey\":{\"attestation\":\"none\",\"authenticatorSelection\":{\"userVerification\":\"required\"},\"challenge\":\"XaMYwWOZ5hj6pwtwJJlpcI-ExkO5TxevBMG4R8DoKQQ\",\"excludeCredentials\":[{\"id\":\"tVp1QfYhT8DkyEHVrv7blnpAo2YJzbZgZNBf7zPs6CI\",\"type\":\"public-key\"}],\"pubKeyCredParams\":[{\"alg\":-7,\"type\":\"public-key\"}],\"rp\":{\"id\":\"localhost\",\"name\":\"ZITADEL\"},\"timeout\":300000,\"user\":{\"displayName\":\"Tim Mohlmann\",\"id\":\"MjE1NTk4MDAwNDY0OTk4OTQw\",\"name\":\"tim\"}}}" + } + ]; +} + +message VerifyPasskeyRegistrationRequest{ + string user_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: "\"163840776835432705\""; + } + ]; + string passkey_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: "\"163840776835432705\""; + } + ]; + google.protobuf.Struct public_key_credential = 3 [ + (validate.rules).message.required = true, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "PublicKeyCredential Interface. Generated helper methods populate the field from JSON created by a WebauthN client. See also: https://www.w3.org/TR/webauthn/#publickeycredential"; + example: "{\"type\":\"public-key\",\"id\":\"pawVarF4xPxLFmfCnRkwXWeTrKGzabcAi92LEI1WC00\",\"rawId\":\"pawVarF4xPxLFmfCnRkwXWeTrKGzabcAi92LEI1WC00\",\"response\":{\"attestationObject\":\"o2NmbXRmcGFja2VkZ2F0dFN0bXSiY2FsZyZjc2lnWEcwRQIgRKS3VpeE9tfExXRzkoUKnG4rQWPvtSSt4YtDGgTx32oCIQDPey-2YJ4uIg-QCM4jj6aE2U3tgMFM_RP7Efx6xRu3JGhhdXRoRGF0YVikSZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NFAAAAADju76085Yhmlt1CEOHkwLQAIKWsFWqxeMT8SxZnwp0ZMF1nk6yhs2m3AIvdixCNVgtNpQECAyYgASFYIMGUDSP2FAQn2MIfPMy7cyB_Y30VqixVgGULTBtFjfRiIlggjUGfQo3_-CrMmH3S-ZQkFKWKnNBQEAMkFtG-9A4zqW0\",\"clientDataJSON\":\"eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiQlhXdHh0WGxJeFZZa0pHT1dVaUVmM25zby02aXZKdWw2YmNmWHdMVlFIayIsIm9yaWdpbiI6Imh0dHBzOi8vbG9jYWxob3N0OjgwODAifQ\"}}"; + min_length: 55; + max_length: 1048576; //1 MB + } + ]; + string passkey_name = 4 [ + (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: "\"fido key\"" + } + ]; +} + +message VerifyPasskeyRegistrationResponse{ + zitadel.object.v2.Details details = 1; +} + +message RegisterU2FRequest{ + string user_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: "\"163840776835432705\""; + } + ]; + string domain = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "\"Domain on which the user is authenticated.\""; + } + ]; +} + +message RegisterU2FResponse{ + zitadel.object.v2.Details details = 1; + string u2f_id = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"163840776835432705\"" + } + ]; + google.protobuf.Struct public_key_credential_creation_options = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Options for Credential Creation (dictionary PublicKeyCredentialCreationOptions). Generated helper methods transform the field to JSON, for use in a WebauthN client. See also: https://www.w3.org/TR/webauthn/#dictdef-publickeycredentialcreationoptions" + example: "{\"publicKey\":{\"attestation\":\"none\",\"authenticatorSelection\":{\"userVerification\":\"required\"},\"challenge\":\"XaMYwWOZ5hj6pwtwJJlpcI-ExkO5TxevBMG4R8DoKQQ\",\"excludeCredentials\":[{\"id\":\"tVp1QfYhT8DkyEHVrv7blnpAo2YJzbZgZNBf7zPs6CI\",\"type\":\"public-key\"}],\"pubKeyCredParams\":[{\"alg\":-7,\"type\":\"public-key\"}],\"rp\":{\"id\":\"localhost\",\"name\":\"ZITADEL\"},\"timeout\":300000,\"user\":{\"displayName\":\"Tim Mohlmann\",\"id\":\"MjE1NTk4MDAwNDY0OTk4OTQw\",\"name\":\"tim\"}}}" + } + ]; +} + +message VerifyU2FRegistrationRequest{ + string user_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: "\"163840776835432705\""; + } + ]; + string u2f_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: "\"163840776835432705\""; + } + ]; + google.protobuf.Struct public_key_credential = 3 [ + (validate.rules).message.required = true, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "PublicKeyCredential Interface. Generated helper methods populate the field from JSON created by a WebauthN client. See also: https://www.w3.org/TR/webauthn/#publickeycredential"; + example: "{\"type\":\"public-key\",\"id\":\"pawVarF4xPxLFmfCnRkwXWeTrKGzabcAi92LEI1WC00\",\"rawId\":\"pawVarF4xPxLFmfCnRkwXWeTrKGzabcAi92LEI1WC00\",\"response\":{\"attestationObject\":\"o2NmbXRmcGFja2VkZ2F0dFN0bXSiY2FsZyZjc2lnWEcwRQIgRKS3VpeE9tfExXRzkoUKnG4rQWPvtSSt4YtDGgTx32oCIQDPey-2YJ4uIg-QCM4jj6aE2U3tgMFM_RP7Efx6xRu3JGhhdXRoRGF0YVikSZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NFAAAAADju76085Yhmlt1CEOHkwLQAIKWsFWqxeMT8SxZnwp0ZMF1nk6yhs2m3AIvdixCNVgtNpQECAyYgASFYIMGUDSP2FAQn2MIfPMy7cyB_Y30VqixVgGULTBtFjfRiIlggjUGfQo3_-CrMmH3S-ZQkFKWKnNBQEAMkFtG-9A4zqW0\",\"clientDataJSON\":\"eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiQlhXdHh0WGxJeFZZa0pHT1dVaUVmM25zby02aXZKdWw2YmNmWHdMVlFIayIsIm9yaWdpbiI6Imh0dHBzOi8vbG9jYWxob3N0OjgwODAifQ\"}}"; + min_length: 55; + max_length: 1048576; //1 MB + } + ]; + string token_name = 4 [ + (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: "\"fido key\"" + } + ]; +} + +message VerifyU2FRegistrationResponse{ + zitadel.object.v2.Details details = 1; +} + +message RemoveU2FRequest { + string user_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\""; + } + ]; + string u2f_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: "\"69629026806489455\""; + } + ]; +} + +message RemoveU2FResponse { + zitadel.object.v2.Details details = 1; +} + +message RegisterTOTPRequest { + string user_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: "\"163840776835432705\""; + } + ]; +} + +message RegisterTOTPResponse { + zitadel.object.v2.Details details = 1; + string uri = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"otpauth://totp/ZITADEL:gigi@acme.zitadel.cloud?algorithm=SHA1&digits=6&issuer=ZITADEL&period=30&secret=TJOPWSDYILLHXFV4MLKNNJOWFG7VSDCK\""; + } + ]; + string secret = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"TJOPWSDYILLHXFV4MLKNNJOWFG7VSDCK\""; + } + ]; +} + +message VerifyTOTPRegistrationRequest { + string user_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: "\"163840776835432705\""; + } + ]; + string code = 2 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Code generated by TOTP app or device" + example: "\"123456\""; + } + ]; +} + +message VerifyTOTPRegistrationResponse { + zitadel.object.v2.Details details = 1; +} + +message RemoveTOTPRequest { + string user_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: "\"163840776835432705\""; + } + ]; +} + +message RemoveTOTPResponse { + zitadel.object.v2.Details details = 1; +} + +message AddOTPSMSRequest { + string user_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: "\"163840776835432705\""; + } + ]; +} + +message AddOTPSMSResponse { + zitadel.object.v2.Details details = 1; +} + +message RemoveOTPSMSRequest { + string user_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: "\"163840776835432705\""; + } + ]; +} + +message RemoveOTPSMSResponse { + zitadel.object.v2.Details details = 1; +} + +message AddOTPEmailRequest { + string user_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: "\"163840776835432705\""; + } + ]; +} + +message AddOTPEmailResponse { + zitadel.object.v2.Details details = 1; +} + +message RemoveOTPEmailRequest { + string user_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: "\"163840776835432705\""; + } + ]; +} + +message RemoveOTPEmailResponse { + zitadel.object.v2.Details details = 1; +} + +message CreatePasskeyRegistrationLinkRequest{ + string user_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: "\"163840776835432705\""; + } + ]; + // if no medium is specified, an email is sent with the default url + oneof medium { + SendPasskeyRegistrationLink send_link = 2; + ReturnPasskeyRegistrationCode return_code = 3; + } +} + +message CreatePasskeyRegistrationLinkResponse{ + zitadel.object.v2.Details details = 1; + // in case the medium was set to return_code, the code will be returned + optional PasskeyRegistrationCode code = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "\"one time code generated by ZITADEL; required to start the passkey registration without user authentication\""; + } + ]; +} + +message ListPasskeysRequest { + string user_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 ListPasskeysResponse { + zitadel.object.v2.ListDetails details = 1; + repeated Passkey result = 2; +} + +message RemovePasskeyRequest { + string user_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\""; + } + ]; + string passkey_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: "\"69629026806489455\""; + } + ]; +} + +message RemovePasskeyResponse { + zitadel.object.v2.Details details = 1; +} + +message StartIdentityProviderIntentRequest{ + string idp_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "ID for existing identity provider" + min_length: 1; + max_length: 200; + example: "\"163840776835432705\""; + } + ]; + + oneof content { + RedirectURLs urls = 2; + LDAPCredentials ldap = 3; + } +} + +message StartIdentityProviderIntentResponse{ + zitadel.object.v2.Details details = 1; + oneof next_step { + string auth_url = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "URL to which the client should redirect" + example: "\"https://accounts.google.com/o/oauth2/v2/auth?client_id=clientID&callback=https%3A%2F%2Fzitadel.cloud%2Fidps%2Fcallback\""; + } + ]; + IDPIntent idp_intent = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "IDP Intent information" + } + ]; + bytes post_form = 4 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "POST call information" + } + ]; + } +} + +message RetrieveIdentityProviderIntentRequest{ + string idp_intent_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "ID of the idp intent, previously returned on the success response of the IDP callback" + min_length: 1; + max_length: 200; + example: "\"163840776835432705\""; + } + ]; + string idp_intent_token = 2 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "token of the idp intent, previously returned on the success response of the IDP callback" + min_length: 1; + max_length: 200; + example: "\"SJKL3ioIDpo342ioqw98fjp3sdf32wahb=\""; + } + ]; +} + +message RetrieveIdentityProviderIntentResponse{ + zitadel.object.v2.Details details = 1; + IDPInformation idp_information = 2; + string user_id = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "ID of the user in ZITADEL if external user is linked" + example: "\"163840776835432345\""; + } + ]; +} + +message AddIDPLinkRequest{ + string user_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\""; + } + ]; + IDPLink idp_link = 2; +} + +message AddIDPLinkResponse { + zitadel.object.v2.Details details = 1; +} + +message ListIDPLinksRequest{ + string user_id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; + //list limitations and ordering + zitadel.object.v2.ListQuery query = 2; +} + +message ListIDPLinksResponse{ + zitadel.object.v2.ListDetails details = 1; + repeated IDPLink result = 2; +} + +message RemoveIDPLinkRequest { + string user_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\""; + } + ]; + string idp_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: "\"69629026806489455\""; + } + ]; + string linked_user_id = 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: "\"69629026806489455\""; + } + ]; +} + +message RemoveIDPLinkResponse { + zitadel.object.v2.Details details = 1; +} + +message PasswordResetRequest{ + string user_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\""; + } + ]; + // if no medium is specified, an email is sent with the default url + oneof medium { + SendPasswordResetLink send_link = 2; + ReturnPasswordResetCode return_code = 3; + } +} + +message PasswordResetResponse{ + zitadel.object.v2.Details details = 1; + // in case the medium was set to return_code, the code will be returned + optional string verification_code = 2; +} + +message SetPasswordRequest{ + string user_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\""; + } + ]; + Password new_password = 2; + // if neither, the current password must be provided nor a verification code generated by the PasswordReset is provided, + // the user must be granted permission to set a password + oneof verification { + string current_password = 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: "\"Secr3tP4ssw0rd!\""; + } + ]; + string verification_code = 4 [ + (validate.rules).string = {min_len: 1, max_len: 20}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 20; + example: "\"SKJd342k\""; + description: "\"the verification code generated during password reset request\""; + } + ]; + } +} + +message SetPasswordResponse{ + zitadel.object.v2.Details details = 1; +} + +message ListAuthenticationMethodTypesRequest{ + string user_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 ListAuthenticationMethodTypesResponse{ + zitadel.object.v2.ListDetails details = 1; + repeated AuthenticationMethodType auth_method_types = 2; +} + +enum AuthenticationMethodType { + AUTHENTICATION_METHOD_TYPE_UNSPECIFIED = 0; + AUTHENTICATION_METHOD_TYPE_PASSWORD = 1; + AUTHENTICATION_METHOD_TYPE_PASSKEY = 2; + AUTHENTICATION_METHOD_TYPE_IDP = 3; + AUTHENTICATION_METHOD_TYPE_TOTP = 4; + AUTHENTICATION_METHOD_TYPE_U2F = 5; + AUTHENTICATION_METHOD_TYPE_OTP_SMS = 6; + AUTHENTICATION_METHOD_TYPE_OTP_EMAIL = 7; +} diff --git a/proto/zitadel/user/v2beta/idp.proto b/proto/zitadel/user/v2beta/idp.proto index 7d58ec5363..dc6e07a5e2 100644 --- a/proto/zitadel/user/v2beta/idp.proto +++ b/proto/zitadel/user/v2beta/idp.proto @@ -32,20 +32,20 @@ message LDAPCredentials { message RedirectURLs { string success_url = 1 [ - (validate.rules).string = {min_len: 1, max_len: 2048, uri_ref: true}, + (validate.rules).string = {min_len: 1, max_len: 200, uri_ref: true}, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { description: "URL on which the user will be redirected after a successful login" min_length: 1; - max_length: 2048; + max_length: 200; example: "\"https://custom.com/login/idp/success\""; } ]; string failure_url = 2 [ - (validate.rules).string = {min_len: 1, max_len: 2048, uri_ref: true}, + (validate.rules).string = {min_len: 1, max_len: 200, uri_ref: true}, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { description: "URL on which the user will be redirected after a failed login" min_length: 1; - max_length: 2048; + max_length: 200; example: "\"https://custom.com/login/idp/fail\""; } ]; diff --git a/proto/zitadel/user/v2beta/user_service.proto b/proto/zitadel/user/v2beta/user_service.proto index 08085c231f..f4ee9e5f3c 100644 --- a/proto/zitadel/user/v2beta/user_service.proto +++ b/proto/zitadel/user/v2beta/user_service.proto @@ -111,6 +111,10 @@ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { service UserService { // Create a new human user + // + // 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. + // + // Deprecated: please move to the corresponding endpoint under user service v2 (GA) rpc AddHumanUser (AddHumanUserRequest) returns (AddHumanUserResponse) { option (google.api.http) = { post: "/v2beta/users/human" @@ -128,8 +132,7 @@ service UserService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Create a user (Human)"; - description: "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." + deprecated: true; responses: { key: "200" value: { @@ -139,7 +142,11 @@ service UserService { }; } - + // User by ID + // + // Returns the full user object (human or machine) including the profile, email, etc. + // + // Deprecated: please move to the corresponding endpoint under user service v2 (GA). rpc GetUserByID(GetUserByIDRequest) returns (GetUserByIDResponse) { option (google.api.http) = { get: "/v2beta/users/{user_id}" @@ -155,8 +162,7 @@ service UserService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "User by ID"; - description: "Returns the full user object (human or machine) including the profile, email, etc." + deprecated: true; responses: { key: "200" value: { @@ -166,6 +172,11 @@ service UserService { }; } + // Search Users + // + // Search for users. By default, we will return users of your organization. Make sure to include a limit and sorting for pagination. + // + // Deprecated: please move to the corresponding endpoint under user service v2 (GA). rpc ListUsers(ListUsersRequest) returns (ListUsersResponse) { option (google.api.http) = { post: "/v2beta/users" @@ -182,8 +193,7 @@ service UserService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Search Users"; - description: "Search for users. By default, we will return users of your organization. Make sure to include a limit and sorting for pagination." + deprecated: true; responses: { key: "200"; value: { @@ -204,7 +214,11 @@ service UserService { }; } - // Change the email of a user + // Change the user email + // + // 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. + // + // Deprecated: please move to the corresponding endpoint under user service v2 (GA). rpc SetEmail (SetEmailRequest) returns (SetEmailResponse) { option (google.api.http) = { post: "/v2beta/users/{user_id}/email" @@ -218,8 +232,7 @@ service UserService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Change the user email"; - description: "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." + deprecated: true; responses: { key: "200" value: { @@ -229,8 +242,11 @@ service UserService { }; } - // Resend code to verify user email + // + // Resend code to verify user email + // + // Deprecated: please move to the corresponding endpoint under user service v2 (GA). rpc ResendEmailCode (ResendEmailCodeRequest) returns (ResendEmailCodeResponse) { option (google.api.http) = { post: "/v2beta/users/{user_id}/email/resend" @@ -244,8 +260,7 @@ service UserService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Resend code to verify user email"; - description: "Resend code to verify user email." + deprecated: true; responses: { key: "200" value: { @@ -255,7 +270,11 @@ service UserService { }; } - // Verify the email with the provided code + // Verify the email + // + // Verify the email with the generated code. + // + // Deprecated: please move to the corresponding endpoint under user service v2 (GA). rpc VerifyEmail (VerifyEmailRequest) returns (VerifyEmailResponse) { option (google.api.http) = { post: "/v2beta/users/{user_id}/email/verify" @@ -269,8 +288,7 @@ service UserService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Verify the email"; - description: "Verify the email with the generated code." + deprecated: true; responses: { key: "200" value: { @@ -280,7 +298,11 @@ service UserService { }; } - // Change the phone of a user + // Set the user phone + // + // 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. + // + // Deprecated: please move to the corresponding endpoint under user service v2 (GA). rpc SetPhone(SetPhoneRequest) returns (SetPhoneResponse) { option (google.api.http) = { post: "/v2beta/users/{user_id}/phone" @@ -294,8 +316,7 @@ service UserService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Set the user phone"; - description: "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." + deprecated: true; responses: { key: "200" value: { @@ -305,6 +326,11 @@ service UserService { }; } + // Remove the user phone + // + // Remove the user phone + // + // Deprecated: please move to the corresponding endpoint under user service v2 (GA). rpc RemovePhone(RemovePhoneRequest) returns (RemovePhoneResponse) { option (google.api.http) = { delete: "/v2beta/users/{user_id}/phone" @@ -318,6 +344,7 @@ service UserService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + deprecated: true; summary: "Delete the user phone"; description: "Delete the phone number of a user." responses: { @@ -329,6 +356,11 @@ service UserService { }; } + // Resend code to verify user phone + // + // Resend code to verify user phone + // + // Deprecated: please move to the corresponding endpoint under user service v2 (GA). rpc ResendPhoneCode (ResendPhoneCodeRequest) returns (ResendPhoneCodeResponse) { option (google.api.http) = { post: "/v2beta/users/{user_id}/phone/resend" @@ -342,8 +374,7 @@ service UserService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Resend code to verify user phone"; - description: "Resend code to verify user phone." + deprecated: true; responses: { key: "200" value: { @@ -353,7 +384,11 @@ service UserService { }; } - // Verify the phone with the provided code + // Verify the phone + // + // Verify the phone with the generated code. + // + // Deprecated: please move to the corresponding endpoint under user service v2 (GA). rpc VerifyPhone (VerifyPhoneRequest) returns (VerifyPhoneResponse) { option (google.api.http) = { post: "/v2beta/users/{user_id}/phone/verify" @@ -367,8 +402,7 @@ service UserService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Verify the phone"; - description: "Verify the phone with the generated code." + deprecated: true; responses: { key: "200" value: { @@ -378,6 +412,11 @@ service UserService { }; } + // Update User + // + // Update all information from a user. + // + // Deprecated: please move to the corresponding endpoint under user service v2 (GA). rpc UpdateHumanUser(UpdateHumanUserRequest) returns (UpdateHumanUserResponse) { option (google.api.http) = { put: "/v2beta/users/{user_id}" @@ -390,8 +429,7 @@ service UserService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Update User"; - description: "Update all information from a user." + deprecated: true; responses: { key: "200" value: { @@ -401,6 +439,11 @@ service UserService { }; } + // Deactivate user + // + // The state of the user will be changed to 'deactivated'. The user will not be able to log in anymore. The endpoint returns an error if the user is already in the state 'deactivated'. Use deactivate user when the user should not be able to use the account anymore, but you still need access to the user data. + // + // Deprecated: please move to the corresponding endpoint under user service v2 (GA). rpc DeactivateUser(DeactivateUserRequest) returns (DeactivateUserResponse) { option (google.api.http) = { post: "/v2beta/users/{user_id}/deactivate" @@ -414,8 +457,7 @@ service UserService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Deactivate user"; - description: "The state of the user will be changed to 'deactivated'. The user will not be able to log in anymore. The endpoint returns an error if the user is already in the state 'deactivated'. Use deactivate user when the user should not be able to use the account anymore, but you still need access to the user data." + deprecated: true; responses: { key: "200" value: { @@ -425,6 +467,11 @@ service UserService { }; } + // Reactivate user + // + // Reactivate a user with the state 'deactivated'. The user will be able to log in again afterward. The endpoint returns an error if the user is not in the state 'deactivated'. + // + // Deprecated: please move to the corresponding endpoint under user service v2 (GA). rpc ReactivateUser(ReactivateUserRequest) returns (ReactivateUserResponse) { option (google.api.http) = { post: "/v2beta/users/{user_id}/reactivate" @@ -438,8 +485,7 @@ service UserService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Reactivate user"; - description: "Reactivate a user with the state 'deactivated'. The user will be able to log in again afterward. The endpoint returns an error if the user is not in the state 'deactivated'." + deprecated: true; responses: { key: "200" value: { @@ -449,6 +495,11 @@ service UserService { }; } + // Lock user + // + // The state of the user will be changed to 'locked'. The user will not be able to log in anymore. The endpoint returns an error if the user is already in the state 'locked'. Use this endpoint if the user should not be able to log in temporarily because of an event that happened (wrong password, etc.). + // + // Deprecated: please move to the corresponding endpoint under user service v2 (GA). rpc LockUser(LockUserRequest) returns (LockUserResponse) { option (google.api.http) = { post: "/v2beta/users/{user_id}/lock" @@ -462,8 +513,7 @@ service UserService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Lock user"; - description: "The state of the user will be changed to 'locked'. The user will not be able to log in anymore. The endpoint returns an error if the user is already in the state 'locked'. Use this endpoint if the user should not be able to log in temporarily because of an event that happened (wrong password, etc.)" + deprecated: true; responses: { key: "200" value: { @@ -473,6 +523,11 @@ service UserService { }; } + // Unlock user + // + // The state of the user will be changed to 'locked'. The user will not be able to log in anymore. The endpoint returns an error if the user is already in the state 'locked'. Use this endpoint if the user should not be able to log in temporarily because of an event that happened (wrong password, etc.). + // + // Deprecated: please move to the corresponding endpoint under user service v2 (GA). rpc UnlockUser(UnlockUserRequest) returns (UnlockUserResponse) { option (google.api.http) = { post: "/v2beta/users/{user_id}/unlock" @@ -486,8 +541,7 @@ service UserService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Unlock user"; - description: "Unlock a user with the state 'locked'. The user will be able to log in again afterward. The endpoint returns an error if the user is not in the state 'locked'." + deprecated: true; responses: { key: "200" value: { @@ -497,6 +551,11 @@ service UserService { }; } + // Delete user + // + // The state of the user will be changed to 'deleted'. The user will not be able to log in anymore. Endpoints requesting this user will return an error 'User not found. + // + // Deprecated: please move to the corresponding endpoint under user service v2 (GA). rpc DeleteUser(DeleteUserRequest) returns (DeleteUserResponse) { option (google.api.http) = { delete: "/v2beta/users/{user_id}" @@ -509,8 +568,7 @@ service UserService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Delete user"; - description: "The state of the user will be changed to 'deleted'. The user will not be able to log in anymore. Endpoints requesting this user will return an error 'User not found" + deprecated: true; responses: { key: "200" value: { @@ -520,6 +578,11 @@ service UserService { }; } + // Start the registration of passkey for a user + // + // Start the registration of a passkey for a user, as a response the public key credential creation options are returned, which are used to verify the passkey. + // + // Deprecated: please move to the corresponding endpoint under user service v2 (GA). rpc RegisterPasskey (RegisterPasskeyRequest) returns (RegisterPasskeyResponse) { option (google.api.http) = { post: "/v2beta/users/{user_id}/passkeys" @@ -532,8 +595,7 @@ service UserService { } }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Start the registration of passkey for a user"; - description: "Start the registration of a passkey for a user, as a response the public key credential creation options are returned, which are used to verify the passkey." + deprecated: true; responses: { key: "200" value: { @@ -542,6 +604,12 @@ service UserService { }; }; } + + // Verify a passkey for a user + // + // Verify the passkey registration with the public key credential. + // + // Deprecated: please move to the corresponding endpoint under user service v2 (GA). rpc VerifyPasskeyRegistration (VerifyPasskeyRegistrationRequest) returns (VerifyPasskeyRegistrationResponse) { option (google.api.http) = { post: "/v2beta/users/{user_id}/passkeys/{passkey_id}" @@ -554,8 +622,7 @@ service UserService { } }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Verify a passkey for a user"; - description: "Verify the passkey registration with the public key credential." + deprecated: true; responses: { key: "200" value: { @@ -564,6 +631,12 @@ service UserService { }; }; } + + // Create a passkey registration link for a user + // + // Create a passkey registration link which includes a code and either return it or send it to the user. + // + // Deprecated: please move to the corresponding endpoint under user service v2 (GA). rpc CreatePasskeyRegistrationLink (CreatePasskeyRegistrationLinkRequest) returns (CreatePasskeyRegistrationLinkResponse) { option (google.api.http) = { post: "/v2beta/users/{user_id}/passkeys/registration_link" @@ -576,8 +649,7 @@ service UserService { } }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Create a passkey registration link for a user"; - description: "Create a passkey registration link which includes a code and either return it or send it to the user." + deprecated: true; responses: { key: "200" value: { @@ -587,6 +659,11 @@ service UserService { }; } + // Start the registration of a u2f token for a user + // + // Start the registration of a u2f token for a user, as a response the public key credential creation options are returned, which are used to verify the u2f token. + // + // Deprecated: please move to the corresponding endpoint under user service v2 (GA). rpc RegisterU2F (RegisterU2FRequest) returns (RegisterU2FResponse) { option (google.api.http) = { post: "/v2beta/users/{user_id}/u2f" @@ -599,8 +676,7 @@ service UserService { } }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Start the registration of a u2f token for a user"; - description: "Start the registration of a u2f token for a user, as a response the public key credential creation options are returned, which are used to verify the u2f token." + deprecated: true; responses: { key: "200" value: { @@ -610,6 +686,11 @@ service UserService { }; } + // Verify a u2f token for a user + // + // Verify the u2f token registration with the public key credential. + // + // Deprecated: please move to the corresponding endpoint under user service v2 (GA). rpc VerifyU2FRegistration (VerifyU2FRegistrationRequest) returns (VerifyU2FRegistrationResponse) { option (google.api.http) = { post: "/v2beta/users/{user_id}/u2f/{u2f_id}" @@ -622,8 +703,7 @@ service UserService { } }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Verify a u2f token for a user"; - description: "Verify the u2f token registration with the public key credential." + deprecated: true; responses: { key: "200" value: { @@ -633,6 +713,11 @@ service UserService { }; } + // Start the registration of a TOTP generator for a user + // + // Start the registration of a TOTP generator for a user, as a response a secret returned, which is used to initialize a TOTP app or device. + // + // Deprecated: please move to the corresponding endpoint under user service v2 (GA). rpc RegisterTOTP (RegisterTOTPRequest) returns (RegisterTOTPResponse) { option (google.api.http) = { post: "/v2beta/users/{user_id}/totp" @@ -645,8 +730,7 @@ service UserService { } }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Start the registration of a TOTP generator for a user"; - description: "Start the registration of a TOTP generator for a user, as a response a secret returned, which is used to initialize a TOTP app or device." + deprecated: true; responses: { key: "200" value: { @@ -656,6 +740,11 @@ service UserService { }; } + // Verify a TOTP generator for a user + // + // Verify the TOTP registration with a generated code. + // + // Deprecated: please move to the corresponding endpoint under user service v2 (GA). rpc VerifyTOTPRegistration (VerifyTOTPRegistrationRequest) returns (VerifyTOTPRegistrationResponse) { option (google.api.http) = { post: "/v2beta/users/{user_id}/totp/verify" @@ -668,8 +757,7 @@ service UserService { } }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Verify a TOTP generator for a user"; - description: "Verify the TOTP registration with a generated code." + deprecated: true; responses: { key: "200" value: { @@ -679,6 +767,11 @@ service UserService { }; } + // Remove TOTP generator from a user + // + // Remove the configured TOTP generator of a user. As only one TOTP generator per user is allowed, the user will not have TOTP as a second-factor afterward. + // + // Deprecated: please move to the corresponding endpoint under user service v2 (GA). rpc RemoveTOTP (RemoveTOTPRequest) returns (RemoveTOTPResponse) { option (google.api.http) = { delete: "/v2beta/users/{user_id}/totp" @@ -690,8 +783,7 @@ service UserService { } }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Remove TOTP generator from a user"; - description: "Remove the configured TOTP generator of a user. As only one TOTP generator per user is allowed, the user will not have TOTP as a second-factor afterward." + deprecated: true; responses: { key: "200" value: { @@ -701,6 +793,11 @@ service UserService { }; } + // Add OTP SMS for a user + // + // Add a new One-Time-Password (OTP) SMS factor to the authenticated user. OTP SMS will enable the user to verify a OTP with the latest verified phone number. The phone number has to be verified to add the second factor. + // + // Deprecated: please move to the corresponding endpoint under user service v2 (GA). rpc AddOTPSMS (AddOTPSMSRequest) returns (AddOTPSMSResponse) { option (google.api.http) = { post: "/v2beta/users/{user_id}/otp_sms" @@ -713,8 +810,7 @@ service UserService { } }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Add OTP SMS for a user"; - description: "Add a new One-Time-Password (OTP) SMS factor to the authenticated user. OTP SMS will enable the user to verify a OTP with the latest verified phone number. The phone number has to be verified to add the second factor." + deprecated: true; responses: { key: "200" value: { @@ -724,6 +820,11 @@ service UserService { }; } + // Remove One-Time-Password (OTP) SMS from a user + // + // Remove the configured One-Time-Password (OTP) SMS factor of a user. As only one OTP SMS per user is allowed, the user will not have OTP SMS as a second-factor afterward. + // + // Deprecated: please move to the corresponding endpoint under user service v2 (GA). rpc RemoveOTPSMS (RemoveOTPSMSRequest) returns (RemoveOTPSMSResponse) { option (google.api.http) = { delete: "/v2beta/users/{user_id}/otp_sms" @@ -735,8 +836,7 @@ service UserService { } }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Remove One-Time-Password (OTP) SMS from a user"; - description: "Remove the configured One-Time-Password (OTP) SMS factor of a user. As only one OTP SMS per user is allowed, the user will not have OTP SMS as a second-factor afterward." + deprecated: true; responses: { key: "200" value: { @@ -746,6 +846,11 @@ service UserService { }; } + // Add OTP Email for a user + // + // Add a new One-Time-Password (OTP) Email factor to the authenticated user. OTP Email will enable the user to verify a OTP with the latest verified email. The email has to be verified to add the second factor. + // + // Deprecated: please move to the corresponding endpoint under user service v2 (GA). rpc AddOTPEmail (AddOTPEmailRequest) returns (AddOTPEmailResponse) { option (google.api.http) = { post: "/v2beta/users/{user_id}/otp_email" @@ -758,8 +863,7 @@ service UserService { } }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Add OTP Email for a user"; - description: "Add a new One-Time-Password (OTP) Email factor to the authenticated user. OTP Email will enable the user to verify a OTP with the latest verified email. The email has to be verified to add the second factor." + deprecated: true; responses: { key: "200" value: { @@ -769,6 +873,11 @@ service UserService { }; } + // Remove One-Time-Password (OTP) Email from a user + // + // Remove the configured One-Time-Password (OTP) Email factor of a user. As only one OTP Email per user is allowed, the user will not have OTP Email as a second-factor afterward. + // + // Deprecated: please move to the corresponding endpoint under user service v2 (GA). rpc RemoveOTPEmail (RemoveOTPEmailRequest) returns (RemoveOTPEmailResponse) { option (google.api.http) = { delete: "/v2beta/users/{user_id}/otp_email" @@ -780,8 +889,7 @@ service UserService { } }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Remove One-Time-Password (OTP) Email from a user"; - description: "Remove the configured One-Time-Password (OTP) Email factor of a user. As only one OTP Email per user is allowed, the user will not have OTP Email as a second-factor afterward." + deprecated: true; responses: { key: "200" value: { @@ -791,7 +899,11 @@ service UserService { }; } - // Start an IDP authentication (for external login, registration or linking) + // Start flow with an identity provider + // + // Start a flow with an identity provider, for external login, registration or linking. + // + // Deprecated: please move to the corresponding endpoint under user service v2 (GA). rpc StartIdentityProviderIntent (StartIdentityProviderIntentRequest) returns (StartIdentityProviderIntentResponse) { option (google.api.http) = { post: "/v2beta/idp_intents" @@ -805,8 +917,7 @@ service UserService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Start flow with an identity provider"; - description: "Start a flow with an identity provider, for external login, registration or linking"; + deprecated: true; responses: { key: "200" value: { @@ -816,6 +927,11 @@ service UserService { }; } + // Retrieve the information returned by the identity provider + // + // Retrieve the information returned by the identity provider for registration or updating an existing user with new information. + // + // Deprecated: please move to the corresponding endpoint under user service v2 (GA). rpc RetrieveIdentityProviderIntent (RetrieveIdentityProviderIntentRequest) returns (RetrieveIdentityProviderIntentResponse) { option (google.api.http) = { post: "/v2beta/idp_intents/{idp_intent_id}" @@ -829,8 +945,7 @@ service UserService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Retrieve the information returned by the identity provider"; - description: "Retrieve the information returned by the identity provider for registration or updating an existing user with new information"; + deprecated: true; responses: { key: "200" value: { @@ -840,7 +955,11 @@ service UserService { }; } - // Link an IDP to an existing user + // Add link to an identity provider to an user + // + // Add link to an identity provider to an user. + // + // Deprecated: please move to the corresponding endpoint under user service v2 (GA). rpc AddIDPLink (AddIDPLinkRequest) returns (AddIDPLinkResponse) { option (google.api.http) = { post: "/v2beta/users/{user_id}/links" @@ -854,8 +973,7 @@ service UserService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Add link to an identity provider to an user"; - description: "Add link to an identity provider to an user"; + deprecated: true; responses: { key: "200" value: { @@ -865,7 +983,11 @@ service UserService { }; } - // Request password reset + // Request a code to reset a password + // + // Request a code to reset a password. + // + // Deprecated: please move to the corresponding endpoint under user service v2 (GA). rpc PasswordReset (PasswordResetRequest) returns (PasswordResetResponse) { option (google.api.http) = { post: "/v2beta/users/{user_id}/password_reset" @@ -879,8 +1001,7 @@ service UserService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Request a code to reset a password"; - description: "Request a code to reset a password"; + deprecated: true; responses: { key: "200" value: { @@ -891,6 +1012,10 @@ service UserService { } // Change password + // + // Change the password of a user with either a verification code or the current password. + // + // Deprecated: please move to the corresponding endpoint under user service v2 (GA). rpc SetPassword (SetPasswordRequest) returns (SetPasswordResponse) { option (google.api.http) = { post: "/v2beta/users/{user_id}/password" @@ -904,8 +1029,7 @@ service UserService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Change password"; - description: "Change the password of a user with either a verification code or the current password."; + deprecated: true; responses: { key: "200" value: { @@ -916,6 +1040,10 @@ service UserService { } // List all possible authentication methods of a user + // + // List all possible authentication methods of a user like password, passwordless, (T)OTP and more. + // + // Deprecated: please move to the corresponding endpoint under user service v2 (GA). rpc ListAuthenticationMethodTypes (ListAuthenticationMethodTypesRequest) returns (ListAuthenticationMethodTypesResponse) { option (google.api.http) = { get: "/v2beta/users/{user_id}/authentication_methods" @@ -928,8 +1056,7 @@ service UserService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "List all possible authentication methods of a user"; - description: "List all possible authentication methods of a user like password, passwordless, (T)OTP and more"; + deprecated: true; responses: { key: "200" value: { diff --git a/proto/zitadel/user/v3alpha/authenticator.proto b/proto/zitadel/user/v3alpha/authenticator.proto index bab19bdcb6..7527bcdb69 100644 --- a/proto/zitadel/user/v3alpha/authenticator.proto +++ b/proto/zitadel/user/v3alpha/authenticator.proto @@ -7,7 +7,7 @@ import "google/protobuf/struct.proto"; import "google/protobuf/timestamp.proto"; import "protoc-gen-openapiv2/options/annotations.proto"; import "validate/validate.proto"; -import "zitadel/object/v2beta/object.proto"; +import "zitadel/object/v2/object.proto"; option go_package = "github.com/zitadel/zitadel/pkg/grpc/user/v3alpha"; @@ -167,7 +167,7 @@ message AuthenticationKey { example: "\"69629023906488334\""; } ]; - zitadel.object.v2beta.Details details = 2; + zitadel.object.v2.Details details = 2; // the file type of the key AuthNKeyType type = 3 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -315,21 +315,21 @@ message ReturnWebAuthNRegistrationCode {} message RedirectURLs { // URL to which the user will be redirected after a successful login. string success_url = 1 [ - (validate.rules).string = {min_len: 1, max_len: 2048, uri_ref: true}, + (validate.rules).string = {min_len: 1, max_len: 200, uri_ref: true}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { min_length: 1; - max_length: 2048; + max_length: 200; example: "\"https://custom.com/login/idp/success\""; } ]; // URL to which the user will be redirected after a failed login. string failure_url = 2 [ - (validate.rules).string = {min_len: 1, max_len: 2048, uri_ref: true}, + (validate.rules).string = {min_len: 1, max_len: 200, uri_ref: true}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { min_length: 1; - max_length: 2048; + max_length: 200; example: "\"https://custom.com/login/idp/fail\""; } ]; diff --git a/proto/zitadel/user/v3alpha/query.proto b/proto/zitadel/user/v3alpha/query.proto index 6be060b0b1..4e2bf062b1 100644 --- a/proto/zitadel/user/v3alpha/query.proto +++ b/proto/zitadel/user/v3alpha/query.proto @@ -8,7 +8,7 @@ import "google/api/field_behavior.proto"; import "protoc-gen-openapiv2/options/annotations.proto"; import "validate/validate.proto"; import "zitadel/user/v3alpha/user.proto"; -import "zitadel/object/v2beta/object.proto"; +import "zitadel/object/v2/object.proto"; message SearchQuery { oneof query { @@ -78,7 +78,7 @@ message UserIDQuery { } ]; // Defines which text comparison method used for the id query. - zitadel.object.v2beta.TextQueryMethod method = 2 [ + zitadel.object.v2.TextQueryMethod method = 2 [ (validate.rules).enum.defined_only = true ]; } @@ -95,7 +95,7 @@ message OrganizationIDQuery { } ]; // Defines which text comparison method used for the id query. - zitadel.object.v2beta.TextQueryMethod method = 2 [ + zitadel.object.v2.TextQueryMethod method = 2 [ (validate.rules).enum.defined_only = true ]; } @@ -112,7 +112,7 @@ message UsernameQuery { } ]; // Defines which text comparison method used for the username query. - zitadel.object.v2beta.TextQueryMethod method = 2 [ + zitadel.object.v2.TextQueryMethod method = 2 [ (validate.rules).enum.defined_only = true ]; // Defines that the username must only be unique in the organisation. @@ -131,7 +131,7 @@ message EmailQuery { } ]; // Defines which text comparison method used for the email query. - zitadel.object.v2beta.TextQueryMethod method = 2 [ + zitadel.object.v2.TextQueryMethod method = 2 [ (validate.rules).enum.defined_only = true ]; } @@ -148,7 +148,7 @@ message PhoneQuery { } ]; // Defines which text comparison method used for the phone query. - zitadel.object.v2beta.TextQueryMethod method = 2 [ + zitadel.object.v2.TextQueryMethod method = 2 [ (validate.rules).enum.defined_only = true ]; } @@ -189,7 +189,7 @@ message SchemaTypeQuery { } ]; // Defines which text comparison method used for the type query. - zitadel.object.v2beta.TextQueryMethod method = 2 [ + zitadel.object.v2.TextQueryMethod method = 2 [ (validate.rules).enum.defined_only = true ]; } diff --git a/proto/zitadel/user/v3alpha/user.proto b/proto/zitadel/user/v3alpha/user.proto index 244c9a87e9..47b9d7ee98 100644 --- a/proto/zitadel/user/v3alpha/user.proto +++ b/proto/zitadel/user/v3alpha/user.proto @@ -7,7 +7,7 @@ import "google/protobuf/struct.proto"; import "google/protobuf/timestamp.proto"; import "protoc-gen-openapiv2/options/annotations.proto"; import "validate/validate.proto"; -import "zitadel/object/v2beta/object.proto"; +import "zitadel/object/v2/object.proto"; import "zitadel/user/v3alpha/authenticator.proto"; import "zitadel/user/v3alpha/communication.proto"; @@ -22,7 +22,7 @@ message User { } ]; // Details provide some base information (such as the last change date) of the user. - zitadel.object.v2beta.Details details = 2; + zitadel.object.v2.Details details = 2; // The user's authenticators. They are used to identify and authenticate the user // during the authentication process. Authenticators authenticators = 3; diff --git a/proto/zitadel/user/v3alpha/user_service.proto b/proto/zitadel/user/v3alpha/user_service.proto index cb6989be05..b193cc52cb 100644 --- a/proto/zitadel/user/v3alpha/user_service.proto +++ b/proto/zitadel/user/v3alpha/user_service.proto @@ -8,7 +8,7 @@ import "google/protobuf/duration.proto"; import "google/protobuf/struct.proto"; import "protoc-gen-openapiv2/options/annotations.proto"; import "validate/validate.proto"; -import "zitadel/object/v2beta/object.proto"; +import "zitadel/object/v2/object.proto"; import "zitadel/protoc_gen_zitadel/v2/options.proto"; import "zitadel/user/v3alpha/authenticator.proto"; import "zitadel/user/v3alpha/communication.proto"; @@ -573,7 +573,7 @@ service UserService { // Add, update or reset a user's password with either a verification code or the current password. rpc SetPassword (SetPasswordRequest) returns (SetPasswordResponse) { option (google.api.http) = { - post: "/v2beta/users/{user_id}/password" + post: "/v2/users/{user_id}/password" body: "*" }; @@ -598,7 +598,7 @@ service UserService { // Request a code to be able to set a new password. rpc RequestPasswordReset (RequestPasswordResetRequest) returns (RequestPasswordResetResponse) { option (google.api.http) = { - post: "/v2beta/users/{user_id}/password/reset" + post: "/v2/users/{user_id}/password/reset" body: "*" }; @@ -1039,7 +1039,7 @@ service UserService { message ListUsersRequest { // list limitations and ordering. - zitadel.object.v2beta.ListQuery query = 1; + zitadel.object.v2.ListQuery query = 1; // the field the result is sorted. zitadel.user.v3alpha.FieldName sorting_column = 2 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1052,7 +1052,7 @@ message ListUsersRequest { message ListUsersResponse { // Details provides information about the returned result including total amount found. - zitadel.object.v2beta.ListDetails details = 1; + zitadel.object.v2.ListDetails details = 1; // States by which field the results are sorted. zitadel.user.v3alpha.FieldName sorting_column = 2; // The result contains the user schemas, which matched the queries. @@ -1087,7 +1087,7 @@ message CreateUserRequest { } ]; // Set the organization the user belongs to. - zitadel.object.v2beta.Organization organization = 2 [ + zitadel.object.v2.Organization organization = 2 [ (validate.rules).message = {required: true}, (google.api.field_behavior) = REQUIRED ]; @@ -1115,7 +1115,7 @@ message CreateUserRequest { message CreateUserResponse { string user_id = 1; - zitadel.object.v2beta.Details details = 2; + zitadel.object.v2.Details details = 2; // The email code will be set if a contact email was set with a return_code verification option. optional string email_code = 3 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1157,7 +1157,7 @@ message UpdateUserRequest { } message UpdateUserResponse { - zitadel.object.v2beta.Details details = 1; + zitadel.object.v2.Details details = 1; // The email code will be set if a contact email was set with a return_code verification option. optional string email_code = 3 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1186,7 +1186,7 @@ message DeactivateUserRequest { } message DeactivateUserResponse { - zitadel.object.v2beta.Details details = 1; + zitadel.object.v2.Details details = 1; } @@ -1204,7 +1204,7 @@ message ReactivateUserRequest { } message ReactivateUserResponse { - zitadel.object.v2beta.Details details = 1; + zitadel.object.v2.Details details = 1; } message LockUserRequest { @@ -1221,7 +1221,7 @@ message LockUserRequest { } message LockUserResponse { - zitadel.object.v2beta.Details details = 1; + zitadel.object.v2.Details details = 1; } message UnlockUserRequest { @@ -1238,7 +1238,7 @@ message UnlockUserRequest { } message UnlockUserResponse { - zitadel.object.v2beta.Details details = 1; + zitadel.object.v2.Details details = 1; } message DeleteUserRequest { @@ -1255,7 +1255,7 @@ message DeleteUserRequest { } message DeleteUserResponse { - zitadel.object.v2beta.Details details = 1; + zitadel.object.v2.Details details = 1; } message SetContactEmailRequest { @@ -1274,7 +1274,7 @@ message SetContactEmailRequest { } message SetContactEmailResponse { - zitadel.object.v2beta.Details details = 1; + zitadel.object.v2.Details details = 1; // The verification code will be set if a contact email was set with a return_code verification option. optional string verification_code = 3 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1307,7 +1307,7 @@ message VerifyContactEmailRequest { } message VerifyContactEmailResponse { - zitadel.object.v2beta.Details details = 1; + zitadel.object.v2.Details details = 1; } message ResendContactEmailCodeRequest { @@ -1331,7 +1331,7 @@ message ResendContactEmailCodeRequest { } message ResendContactEmailCodeResponse { - zitadel.object.v2beta.Details details = 1; + zitadel.object.v2.Details details = 1; // in case the verification was set to return_code, the code will be returned. optional string verification_code = 2; } @@ -1352,7 +1352,7 @@ message SetContactPhoneRequest { } message SetContactPhoneResponse { - zitadel.object.v2beta.Details details = 1; + zitadel.object.v2.Details details = 1; // The phone verification code will be set if a contact phone was set with a return_code verification option. optional string email_code = 3 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1385,7 +1385,7 @@ message VerifyContactPhoneRequest { } message VerifyContactPhoneResponse { - zitadel.object.v2beta.Details details = 1; + zitadel.object.v2.Details details = 1; } message ResendContactPhoneCodeRequest { @@ -1409,7 +1409,7 @@ message ResendContactPhoneCodeRequest { } message ResendContactPhoneCodeResponse { - zitadel.object.v2beta.Details details = 1; + zitadel.object.v2.Details details = 1; // in case the verification was set to return_code, the code will be returned. optional string verification_code = 2; } @@ -1430,7 +1430,7 @@ message AddUsernameRequest { } message AddUsernameResponse { - zitadel.object.v2beta.Details details = 1; + zitadel.object.v2.Details details = 1; // unique identifier of the username. string username_id = 2 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1463,7 +1463,7 @@ message RemoveUsernameRequest { } message RemoveUsernameResponse { - zitadel.object.v2beta.Details details = 1; + zitadel.object.v2.Details details = 1; } message SetPasswordRequest { @@ -1506,7 +1506,7 @@ message SetPasswordRequest { } message SetPasswordResponse { - zitadel.object.v2beta.Details details = 1; + zitadel.object.v2.Details details = 1; } message RequestPasswordResetRequest { @@ -1532,7 +1532,7 @@ message RequestPasswordResetRequest { } message RequestPasswordResetResponse { - zitadel.object.v2beta.Details details = 1; + zitadel.object.v2.Details details = 1; // In case the medium was set to return_code, the code will be returned. optional string verification_code = 2 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1571,7 +1571,7 @@ message StartWebAuthNRegistrationRequest { } message StartWebAuthNRegistrationResponse { - zitadel.object.v2beta.Details details = 1; + zitadel.object.v2.Details details = 1; // unique identifier of the WebAuthN registration. string web_auth_n_id = 2 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1634,7 +1634,7 @@ message VerifyWebAuthNRegistrationRequest { } message VerifyWebAuthNRegistrationResponse { - zitadel.object.v2beta.Details details = 1; + zitadel.object.v2.Details details = 1; } message CreateWebAuthNRegistrationLinkRequest { @@ -1658,7 +1658,7 @@ message CreateWebAuthNRegistrationLinkRequest { } message CreateWebAuthNRegistrationLinkResponse { - zitadel.object.v2beta.Details details = 1; + zitadel.object.v2.Details details = 1; // In case the medium was set to return_code, the code will be returned. optional AuthenticatorRegistrationCode code = 2; } @@ -1687,7 +1687,7 @@ message RemoveWebAuthNAuthenticatorRequest { } message RemoveWebAuthNAuthenticatorResponse { - zitadel.object.v2beta.Details details = 1; + zitadel.object.v2.Details details = 1; } message StartTOTPRegistrationRequest { @@ -1704,7 +1704,7 @@ message StartTOTPRegistrationRequest { } message StartTOTPRegistrationResponse { - zitadel.object.v2beta.Details details = 1; + zitadel.object.v2.Details details = 1; // unique identifier of the TOTP registration. string totp_id = 2 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1759,7 +1759,7 @@ message VerifyTOTPRegistrationRequest { } message VerifyTOTPRegistrationResponse { - zitadel.object.v2beta.Details details = 1; + zitadel.object.v2.Details details = 1; } message RemoveTOTPAuthenticatorRequest { @@ -1786,7 +1786,7 @@ message RemoveTOTPAuthenticatorRequest { } message RemoveTOTPAuthenticatorResponse { - zitadel.object.v2beta.Details details = 1; + zitadel.object.v2.Details details = 1; } message AddOTPSMSAuthenticatorRequest { @@ -1805,7 +1805,7 @@ message AddOTPSMSAuthenticatorRequest { } message AddOTPSMSAuthenticatorResponse { - zitadel.object.v2beta.Details details = 1; + zitadel.object.v2.Details details = 1; // unique identifier of the OTP SMS registration. string otp_sms_id = 2 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1855,7 +1855,7 @@ message VerifyOTPSMSRegistrationRequest { } message VerifyOTPSMSRegistrationResponse { - zitadel.object.v2beta.Details details = 1; + zitadel.object.v2.Details details = 1; } message RemoveOTPSMSAuthenticatorRequest { @@ -1882,7 +1882,7 @@ message RemoveOTPSMSAuthenticatorRequest { } message RemoveOTPSMSAuthenticatorResponse { - zitadel.object.v2beta.Details details = 1; + zitadel.object.v2.Details details = 1; } message AddOTPEmailAuthenticatorRequest { @@ -1901,7 +1901,7 @@ message AddOTPEmailAuthenticatorRequest { } message AddOTPEmailAuthenticatorResponse { - zitadel.object.v2beta.Details details = 1; + zitadel.object.v2.Details details = 1; // unique identifier of the OTP Email registration. string otp_email_id = 2 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1950,7 +1950,7 @@ message VerifyOTPEmailRegistrationRequest { } message VerifyOTPEmailRegistrationResponse { - zitadel.object.v2beta.Details details = 1; + zitadel.object.v2.Details details = 1; } message RemoveOTPEmailAuthenticatorRequest { @@ -1977,7 +1977,7 @@ message RemoveOTPEmailAuthenticatorRequest { } message RemoveOTPEmailAuthenticatorResponse { - zitadel.object.v2beta.Details details = 1; + zitadel.object.v2.Details details = 1; } message StartIdentityProviderIntentRequest { @@ -1999,7 +1999,7 @@ message StartIdentityProviderIntentRequest { } message StartIdentityProviderIntentResponse { - zitadel.object.v2beta.Details details = 1; + zitadel.object.v2.Details details = 1; // the next step to take in the idp intent flow. oneof next_step { // The authentication URL to which the client should redirect. @@ -2040,7 +2040,7 @@ message RetrieveIdentityProviderIntentRequest { } message RetrieveIdentityProviderIntentResponse { - zitadel.object.v2beta.Details details = 1; + zitadel.object.v2.Details details = 1; // Information returned by the identity provider (IDP) such as the identification of the user // and detailed / profile information. IDPInformation idp_information = 2; @@ -2067,7 +2067,7 @@ message AddIDPAuthenticatorRequest { } message AddIDPAuthenticatorResponse { - zitadel.object.v2beta.Details details = 1; + zitadel.object.v2.Details details = 1; } message RemoveIDPAuthenticatorRequest { @@ -2094,6 +2094,6 @@ message RemoveIDPAuthenticatorRequest { } message RemoveIDPAuthenticatorResponse { - zitadel.object.v2beta.Details details = 1; + zitadel.object.v2.Details details = 1; } From 7d2d85f57cccae4e22efab040b6fb5961d6441fe Mon Sep 17 00:00:00 2001 From: Stefan Benz <46600784+stebenz@users.noreply.github.com> Date: Fri, 26 Jul 2024 22:39:55 +0200 Subject: [PATCH 05/39] feat: api v2beta to api v2 (#8283) # Which Problems Are Solved The v2beta services are stable but not GA. # How the Problems Are Solved The v2beta services are copied to v2. The corresponding v1 and v2beta services are deprecated. # Additional Context Closes #7236 --------- Co-authored-by: Elio Bischof --- cmd/initialise/verify_database.go | 2 +- cmd/initialise/verify_zitadel.go | 5 +- cmd/key/masterkey.go | 3 +- cmd/start/start.go | 41 +- docs/docs/apis/v2.mdx | 5 +- docs/package.json | 2 +- .../v3alpha/execution_integration_test.go | 34 +- .../action/v3alpha/query_integration_test.go | 20 +- .../action/v3alpha/server_integration_test.go | 2 +- .../action/v3alpha/target_integration_test.go | 2 +- internal/api/grpc/feature/v2/converter.go | 2 +- .../api/grpc/feature/v2/converter_test.go | 4 +- internal/api/grpc/feature/v2/feature.go | 2 +- .../feature/v2/feature_integration_test.go | 4 +- internal/api/grpc/feature/v2/server.go | 2 +- internal/api/grpc/feature/v2beta/converter.go | 155 + .../api/grpc/feature/v2beta/converter_test.go | 268 ++ internal/api/grpc/feature/v2beta/feature.go | 86 + .../v2beta/feature_integration_test.go | 499 ++++ internal/api/grpc/feature/v2beta/server.go | 47 + internal/api/grpc/object/v2/converter.go | 2 +- internal/api/grpc/object/v2beta/converter.go | 72 + internal/api/grpc/oidc/v2/oidc.go | 2 +- .../api/grpc/oidc/v2/oidc_integration_test.go | 8 +- internal/api/grpc/oidc/v2/oidc_test.go | 2 +- internal/api/grpc/oidc/v2/server.go | 2 +- internal/api/grpc/oidc/v2beta/oidc.go | 204 ++ .../grpc/oidc/v2beta/oidc_integration_test.go | 258 ++ internal/api/grpc/oidc/v2beta/oidc_test.go | 150 + internal/api/grpc/oidc/v2beta/server.go | 59 + internal/api/grpc/org/v2/org.go | 2 +- .../api/grpc/org/v2/org_integration_test.go | 4 +- internal/api/grpc/org/v2/org_test.go | 6 +- internal/api/grpc/org/v2/server.go | 2 +- internal/api/grpc/org/v2beta/org.go | 83 + .../grpc/org/v2beta/org_integration_test.go | 207 ++ internal/api/grpc/org/v2beta/org_test.go | 172 ++ internal/api/grpc/org/v2beta/server.go | 55 + .../server/middleware/activity_interceptor.go | 2 + .../middleware/execution_interceptor_test.go | 38 +- internal/api/grpc/session/v2/server.go | 2 +- internal/api/grpc/session/v2/session.go | 2 +- .../session/v2/session_integration_test.go | 22 +- internal/api/grpc/session/v2/session_test.go | 4 +- internal/api/grpc/session/v2beta/server.go | 51 + internal/api/grpc/session/v2beta/session.go | 500 ++++ .../v2beta/session_integration_test.go | 996 +++++++ .../api/grpc/session/v2beta/session_test.go | 739 +++++ internal/api/grpc/settings/v2/server.go | 2 +- .../settings/v2/server_integration_test.go | 2 +- internal/api/grpc/settings/v2/settings.go | 4 +- .../grpc/settings/v2/settings_converter.go | 2 +- .../settings/v2/settings_converter_test.go | 2 +- .../settings/v2/settings_integration_test.go | 4 +- internal/api/grpc/settings/v2beta/server.go | 57 + .../v2beta/server_integration_test.go | 34 + internal/api/grpc/settings/v2beta/settings.go | 161 ++ .../settings/v2beta/settings_converter.go | 245 ++ .../v2beta/settings_converter_test.go | 517 ++++ .../v2beta/settings_integration_test.go | 174 ++ internal/api/grpc/user/converter.go | 17 - .../schema/v3alpha/schema_integration_test.go | 4 +- internal/api/grpc/user/v2/email.go | 4 +- .../grpc/user/v2/email_integration_test.go | 5 +- internal/api/grpc/user/v2/idp_link.go | 94 + .../grpc/user/v2/idp_link_integration_test.go | 360 +++ internal/api/grpc/user/v2/otp.go | 2 +- .../api/grpc/user/v2/otp_integration_test.go | 5 +- internal/api/grpc/user/v2/passkey.go | 70 +- .../grpc/user/v2/passkey_integration_test.go | 308 +- internal/api/grpc/user/v2/passkey_test.go | 4 +- internal/api/grpc/user/v2/password.go | 2 +- .../grpc/user/v2/password_integration_test.go | 5 +- internal/api/grpc/user/v2/password_test.go | 2 +- internal/api/grpc/user/v2/phone.go | 4 +- .../grpc/user/v2/phone_integration_test.go | 5 +- internal/api/grpc/user/v2/query.go | 2 +- .../grpc/user/v2/query_integration_test.go | 5 +- internal/api/grpc/user/v2/server.go | 2 +- internal/api/grpc/user/v2/totp.go | 2 +- .../api/grpc/user/v2/totp_integration_test.go | 5 +- internal/api/grpc/user/v2/totp_test.go | 4 +- internal/api/grpc/user/v2/u2f.go | 12 +- .../api/grpc/user/v2/u2f_integration_test.go | 151 +- internal/api/grpc/user/v2/u2f_test.go | 4 +- internal/api/grpc/user/v2/user.go | 19 +- .../api/grpc/user/v2/user_integration_test.go | 85 +- internal/api/grpc/user/v2/user_test.go | 4 +- internal/api/grpc/user/v2beta/email.go | 86 + .../user/v2beta/email_integration_test.go | 297 ++ internal/api/grpc/user/v2beta/otp.go | 42 + .../grpc/user/v2beta/otp_integration_test.go | 362 +++ internal/api/grpc/user/v2beta/passkey.go | 118 + .../user/v2beta/passkey_integration_test.go | 319 +++ internal/api/grpc/user/v2beta/passkey_test.go | 235 ++ internal/api/grpc/user/v2beta/password.go | 69 + .../user/v2beta/password_integration_test.go | 232 ++ .../api/grpc/user/v2beta/password_test.go | 39 + internal/api/grpc/user/v2beta/phone.go | 102 + .../user/v2beta/phone_integration_test.go | 344 +++ internal/api/grpc/user/v2beta/query.go | 338 +++ .../user/v2beta/query_integration_test.go | 982 +++++++ internal/api/grpc/user/v2beta/server.go | 75 + internal/api/grpc/user/v2beta/totp.go | 44 + .../grpc/user/v2beta/totp_integration_test.go | 284 ++ internal/api/grpc/user/v2beta/totp_test.go | 71 + internal/api/grpc/user/v2beta/u2f.go | 42 + .../grpc/user/v2beta/u2f_integration_test.go | 190 ++ internal/api/grpc/user/v2beta/u2f_test.go | 97 + internal/api/grpc/user/v2beta/user.go | 633 +++++ .../grpc/user/v2beta/user_integration_test.go | 2521 +++++++++++++++++ internal/api/grpc/user/v2beta/user_test.go | 410 +++ internal/api/http/domain_check.go | 4 +- internal/api/http/marshal.go | 2 +- internal/api/idp/idp_integration_test.go | 2 +- .../api/oidc/auth_request_integration_test.go | 4 +- internal/api/oidc/client_integration_test.go | 2 +- internal/api/oidc/oidc_integration_test.go | 6 +- .../oidc/token_exchange_integration_test.go | 2 +- .../api/oidc/userinfo_integration_test.go | 4 +- internal/command/org_idp_config_test.go | 86 +- internal/command/user_human_webauthn.go | 5 + internal/command/user_idp_link.go | 5 + internal/command/user_idp_link_test.go | 43 +- internal/config/config.go | 3 +- internal/integration/assert.go | 18 +- internal/integration/client.go | 86 +- internal/notification/channels/log/channel.go | 4 +- .../telemetry_pusher_integration_test.go | 2 +- internal/query/idp_user_link.go | 23 + internal/query/user_auth_method.go | 23 + internal/repository/user/machine_key.go | 3 +- internal/telemetry/tracing/caller.go | 4 +- pkg/grpc/user/v2/user.go | 3 + .../action/v3alpha/action_service.proto | 20 +- proto/zitadel/action/v3alpha/execution.proto | 12 +- proto/zitadel/action/v3alpha/query.proto | 6 +- proto/zitadel/action/v3alpha/target.proto | 4 +- proto/zitadel/management.proto | 1 + .../user/schema/v3alpha/user_schema.proto | 8 +- .../schema/v3alpha/user_schema_service.proto | 16 +- statik/doc.go | 2 +- 142 files changed, 15170 insertions(+), 386 deletions(-) create mode 100644 internal/api/grpc/feature/v2beta/converter.go create mode 100644 internal/api/grpc/feature/v2beta/converter_test.go create mode 100644 internal/api/grpc/feature/v2beta/feature.go create mode 100644 internal/api/grpc/feature/v2beta/feature_integration_test.go create mode 100644 internal/api/grpc/feature/v2beta/server.go create mode 100644 internal/api/grpc/object/v2beta/converter.go create mode 100644 internal/api/grpc/oidc/v2beta/oidc.go create mode 100644 internal/api/grpc/oidc/v2beta/oidc_integration_test.go create mode 100644 internal/api/grpc/oidc/v2beta/oidc_test.go create mode 100644 internal/api/grpc/oidc/v2beta/server.go create mode 100644 internal/api/grpc/org/v2beta/org.go create mode 100644 internal/api/grpc/org/v2beta/org_integration_test.go create mode 100644 internal/api/grpc/org/v2beta/org_test.go create mode 100644 internal/api/grpc/org/v2beta/server.go create mode 100644 internal/api/grpc/session/v2beta/server.go create mode 100644 internal/api/grpc/session/v2beta/session.go create mode 100644 internal/api/grpc/session/v2beta/session_integration_test.go create mode 100644 internal/api/grpc/session/v2beta/session_test.go create mode 100644 internal/api/grpc/settings/v2beta/server.go create mode 100644 internal/api/grpc/settings/v2beta/server_integration_test.go create mode 100644 internal/api/grpc/settings/v2beta/settings.go create mode 100644 internal/api/grpc/settings/v2beta/settings_converter.go create mode 100644 internal/api/grpc/settings/v2beta/settings_converter_test.go create mode 100644 internal/api/grpc/settings/v2beta/settings_integration_test.go create mode 100644 internal/api/grpc/user/v2/idp_link.go create mode 100644 internal/api/grpc/user/v2/idp_link_integration_test.go create mode 100644 internal/api/grpc/user/v2beta/email.go create mode 100644 internal/api/grpc/user/v2beta/email_integration_test.go create mode 100644 internal/api/grpc/user/v2beta/otp.go create mode 100644 internal/api/grpc/user/v2beta/otp_integration_test.go create mode 100644 internal/api/grpc/user/v2beta/passkey.go create mode 100644 internal/api/grpc/user/v2beta/passkey_integration_test.go create mode 100644 internal/api/grpc/user/v2beta/passkey_test.go create mode 100644 internal/api/grpc/user/v2beta/password.go create mode 100644 internal/api/grpc/user/v2beta/password_integration_test.go create mode 100644 internal/api/grpc/user/v2beta/password_test.go create mode 100644 internal/api/grpc/user/v2beta/phone.go create mode 100644 internal/api/grpc/user/v2beta/phone_integration_test.go create mode 100644 internal/api/grpc/user/v2beta/query.go create mode 100644 internal/api/grpc/user/v2beta/query_integration_test.go create mode 100644 internal/api/grpc/user/v2beta/server.go create mode 100644 internal/api/grpc/user/v2beta/totp.go create mode 100644 internal/api/grpc/user/v2beta/totp_integration_test.go create mode 100644 internal/api/grpc/user/v2beta/totp_test.go create mode 100644 internal/api/grpc/user/v2beta/u2f.go create mode 100644 internal/api/grpc/user/v2beta/u2f_integration_test.go create mode 100644 internal/api/grpc/user/v2beta/u2f_test.go create mode 100644 internal/api/grpc/user/v2beta/user.go create mode 100644 internal/api/grpc/user/v2beta/user_integration_test.go create mode 100644 internal/api/grpc/user/v2beta/user_test.go create mode 100644 pkg/grpc/user/v2/user.go diff --git a/cmd/initialise/verify_database.go b/cmd/initialise/verify_database.go index ac7d39253a..be3ec19bab 100644 --- a/cmd/initialise/verify_database.go +++ b/cmd/initialise/verify_database.go @@ -38,6 +38,6 @@ func VerifyDatabase(databaseName string) func(*database.DB) error { return func(db *database.DB) error { logging.WithFields("database", databaseName).Info("verify database") - return exec(db, fmt.Sprintf(string(databaseStmt), databaseName), []string{dbAlreadyExistsCode}) + return exec(db, fmt.Sprintf(databaseStmt, databaseName), []string{dbAlreadyExistsCode}) } } diff --git a/cmd/initialise/verify_zitadel.go b/cmd/initialise/verify_zitadel.go index 53203eb4f6..7c16fdaadf 100644 --- a/cmd/initialise/verify_zitadel.go +++ b/cmd/initialise/verify_zitadel.go @@ -95,7 +95,8 @@ func createEncryptionKeys(ctx context.Context, db *database.DB) error { return err } if _, err = tx.Exec(createEncryptionKeysStmt); err != nil { - tx.Rollback() + rollbackErr := tx.Rollback() + logging.OnError(rollbackErr).Error("rollback failed") return err } @@ -110,7 +111,7 @@ func createEvents(ctx context.Context, db *database.DB) (err error) { defer func() { if err != nil { rollbackErr := tx.Rollback() - logging.OnError(rollbackErr).Debug("rollback failed") + logging.OnError(rollbackErr).Error("rollback failed") return } err = tx.Commit() diff --git a/cmd/key/masterkey.go b/cmd/key/masterkey.go index e1d3792b8a..9c14eb020e 100644 --- a/cmd/key/masterkey.go +++ b/cmd/key/masterkey.go @@ -2,7 +2,6 @@ package key import ( "errors" - "io/ioutil" "os" "github.com/spf13/cobra" @@ -42,7 +41,7 @@ func MasterKey(cmd *cobra.Command) (string, error) { if masterKeyFromEnv { return os.Getenv(envMasterKey), nil } - data, err := ioutil.ReadFile(masterKeyFile) + data, err := os.ReadFile(masterKeyFile) if err != nil { return "", err } diff --git a/cmd/start/start.go b/cmd/start/start.go index e424a3d903..0969c5388a 100644 --- a/cmd/start/start.go +++ b/cmd/start/start.go @@ -37,15 +37,21 @@ import ( action_v3_alpha "github.com/zitadel/zitadel/internal/api/grpc/action/v3alpha" "github.com/zitadel/zitadel/internal/api/grpc/admin" "github.com/zitadel/zitadel/internal/api/grpc/auth" - "github.com/zitadel/zitadel/internal/api/grpc/feature/v2" + feature_v2 "github.com/zitadel/zitadel/internal/api/grpc/feature/v2" + feature_v2beta "github.com/zitadel/zitadel/internal/api/grpc/feature/v2beta" "github.com/zitadel/zitadel/internal/api/grpc/management" oidc_v2 "github.com/zitadel/zitadel/internal/api/grpc/oidc/v2" - "github.com/zitadel/zitadel/internal/api/grpc/org/v2" - "github.com/zitadel/zitadel/internal/api/grpc/session/v2" - "github.com/zitadel/zitadel/internal/api/grpc/settings/v2" + 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" + session_v2 "github.com/zitadel/zitadel/internal/api/grpc/session/v2" + session_v2beta "github.com/zitadel/zitadel/internal/api/grpc/session/v2beta" + settings_v2 "github.com/zitadel/zitadel/internal/api/grpc/settings/v2" + settings_v2beta "github.com/zitadel/zitadel/internal/api/grpc/settings/v2beta" "github.com/zitadel/zitadel/internal/api/grpc/system" user_schema_v3_alpha "github.com/zitadel/zitadel/internal/api/grpc/user/schema/v3alpha" user_v2 "github.com/zitadel/zitadel/internal/api/grpc/user/v2" + user_v2beta "github.com/zitadel/zitadel/internal/api/grpc/user/v2beta" http_util "github.com/zitadel/zitadel/internal/api/http" "github.com/zitadel/zitadel/internal/api/http/middleware" "github.com/zitadel/zitadel/internal/api/idp" @@ -399,20 +405,34 @@ func startAPIs( if err := apis.RegisterServer(ctx, auth.CreateServer(commands, queries, authRepo, config.SystemDefaults, keys.User, config.ExternalSecure), tlsConfig); err != nil { return nil, err } + if err := apis.RegisterService(ctx, user_v2beta.CreateServer(commands, queries, keys.User, keys.IDPConfig, idp.CallbackURL(config.ExternalSecure), idp.SAMLRootURL(config.ExternalSecure), assets.AssetAPI(config.ExternalSecure), permissionCheck)); err != nil { + return nil, err + } if err := apis.RegisterService(ctx, user_v2.CreateServer(commands, queries, keys.User, keys.IDPConfig, idp.CallbackURL(config.ExternalSecure), idp.SAMLRootURL(config.ExternalSecure), assets.AssetAPI(config.ExternalSecure), permissionCheck)); err != nil { return nil, err } - if err := apis.RegisterService(ctx, session.CreateServer(commands, queries)); err != nil { + if err := apis.RegisterService(ctx, session_v2beta.CreateServer(commands, queries)); err != nil { return nil, err } - - if err := apis.RegisterService(ctx, settings.CreateServer(commands, queries, config.ExternalSecure)); err != nil { + if err := apis.RegisterService(ctx, settings_v2beta.CreateServer(commands, queries, config.ExternalSecure)); err != nil { return nil, err } - if err := apis.RegisterService(ctx, org.CreateServer(commands, queries, permissionCheck)); err != nil { + if err := apis.RegisterService(ctx, org_v2beta.CreateServer(commands, queries, permissionCheck)); err != nil { return nil, err } - if err := apis.RegisterService(ctx, feature.CreateServer(commands, queries)); err != nil { + if err := apis.RegisterService(ctx, feature_v2beta.CreateServer(commands, queries)); err != nil { + return nil, err + } + if err := apis.RegisterService(ctx, session_v2.CreateServer(commands, queries)); err != nil { + return nil, err + } + if err := apis.RegisterService(ctx, settings_v2.CreateServer(commands, queries, config.ExternalSecure)); err != nil { + return nil, err + } + if err := apis.RegisterService(ctx, org_v2.CreateServer(commands, queries, permissionCheck)); err != nil { + return nil, err + } + if err := apis.RegisterService(ctx, feature_v2.CreateServer(commands, queries)); err != nil { return nil, err } if err := apis.RegisterService(ctx, action_v3_alpha.CreateServer(commands, queries, domain.AllFunctions, apis.ListGrpcMethods, apis.ListGrpcServices)); err != nil { @@ -491,6 +511,9 @@ func startAPIs( apis.HandleFunc(login.EndpointDeviceAuth, login.RedirectDeviceAuthToPrefix) // After OIDC provider so that the callback endpoint can be used + if err := apis.RegisterService(ctx, oidc_v2beta.CreateServer(commands, queries, oidcServer, config.ExternalSecure)); err != nil { + return nil, err + } if err := apis.RegisterService(ctx, oidc_v2.CreateServer(commands, queries, oidcServer, config.ExternalSecure)); err != nil { return nil, err } diff --git a/docs/docs/apis/v2.mdx b/docs/docs/apis/v2.mdx index 2f51dba6e9..5f5f582a4e 100644 --- a/docs/docs/apis/v2.mdx +++ b/docs/docs/apis/v2.mdx @@ -1,5 +1,5 @@ --- -title: APIs V2 (General Available) +title: APIs V2 (Generally Available) --- import DocCardList from '@theme/DocCardList'; @@ -7,4 +7,7 @@ import DocCardList from '@theme/DocCardList'; APIs V2 organize access by resources (users, settings, etc.), unlike context-specific V1 APIs. This simplifies finding the right API, especially for multi-organization resources. +Users created with the V2 API have no initial state anymore, so new users are immediately active. +Requesting ZITADEL to send a verification email on user creation is still possible. + \ No newline at end of file diff --git a/docs/package.json b/docs/package.json index edaf6e72ca..c7e4d36796 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 && docusaurus build", + "build": "yarn run generate && NODE_OPTIONS=--max-old-space-size=6144 docusaurus build", "swizzle": "docusaurus swizzle", "deploy": "docusaurus deploy", "clear": "docusaurus clear", diff --git a/internal/api/grpc/action/v3alpha/execution_integration_test.go b/internal/api/grpc/action/v3alpha/execution_integration_test.go index fcbdb2b576..2a01e40383 100644 --- a/internal/api/grpc/action/v3alpha/execution_integration_test.go +++ b/internal/api/grpc/action/v3alpha/execution_integration_test.go @@ -12,7 +12,7 @@ import ( "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/integration" action "github.com/zitadel/zitadel/pkg/grpc/action/v3alpha" - object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta" + "github.com/zitadel/zitadel/pkg/grpc/object/v2" ) func executionTargetsSingleTarget(id string) []*action.ExecutionTargetType { @@ -69,7 +69,7 @@ func TestServer_SetExecution_Request(t *testing.T) { ConditionType: &action.Condition_Request{ Request: &action.RequestExecution{ Condition: &action.RequestExecution_Method{ - Method: "/zitadel.session.v2beta.NotExistingService/List", + Method: "/zitadel.session.v2.NotExistingService/List", }, }, }, @@ -86,7 +86,7 @@ func TestServer_SetExecution_Request(t *testing.T) { ConditionType: &action.Condition_Request{ Request: &action.RequestExecution{ Condition: &action.RequestExecution_Method{ - Method: "/zitadel.session.v2beta.SessionService/ListSessions", + Method: "/zitadel.session.v2.SessionService/ListSessions", }, }, }, @@ -125,7 +125,7 @@ func TestServer_SetExecution_Request(t *testing.T) { ConditionType: &action.Condition_Request{ Request: &action.RequestExecution{ Condition: &action.RequestExecution_Service{ - Service: "zitadel.session.v2beta.SessionService", + Service: "zitadel.session.v2.SessionService", }, }, }, @@ -200,7 +200,7 @@ func TestServer_SetExecution_Request_Include(t *testing.T) { ConditionType: &action.Condition_Request{ Request: &action.RequestExecution{ Condition: &action.RequestExecution_Service{ - Service: "zitadel.session.v2beta.SessionService", + Service: "zitadel.session.v2.SessionService", }, }, }, @@ -213,7 +213,7 @@ func TestServer_SetExecution_Request_Include(t *testing.T) { ConditionType: &action.Condition_Request{ Request: &action.RequestExecution{ Condition: &action.RequestExecution_Method{ - Method: "/zitadel.session.v2beta.SessionService/ListSessions", + Method: "/zitadel.session.v2.SessionService/ListSessions", }, }, }, @@ -247,7 +247,7 @@ func TestServer_SetExecution_Request_Include(t *testing.T) { ConditionType: &action.Condition_Request{ Request: &action.RequestExecution{ Condition: &action.RequestExecution_Method{ - Method: "/zitadel.session.v2beta.SessionService/ListSessions", + Method: "/zitadel.session.v2.SessionService/ListSessions", }, }, }, @@ -269,7 +269,7 @@ func TestServer_SetExecution_Request_Include(t *testing.T) { ConditionType: &action.Condition_Request{ Request: &action.RequestExecution{ Condition: &action.RequestExecution_Service{ - Service: "zitadel.session.v2beta.SessionService", + Service: "zitadel.session.v2.SessionService", }, }, }, @@ -347,7 +347,7 @@ func TestServer_DeleteExecution_Request(t *testing.T) { ConditionType: &action.Condition_Request{ Request: &action.RequestExecution{ Condition: &action.RequestExecution_Method{ - Method: "/zitadel.session.v2beta.SessionService/NotExisting", + Method: "/zitadel.session.v2.SessionService/NotExisting", }, }, }, @@ -367,7 +367,7 @@ func TestServer_DeleteExecution_Request(t *testing.T) { ConditionType: &action.Condition_Request{ Request: &action.RequestExecution{ Condition: &action.RequestExecution_Method{ - Method: "/zitadel.session.v2beta.SessionService/GetSession", + Method: "/zitadel.session.v2.SessionService/GetSession", }, }, }, @@ -408,7 +408,7 @@ func TestServer_DeleteExecution_Request(t *testing.T) { ConditionType: &action.Condition_Request{ Request: &action.RequestExecution{ Condition: &action.RequestExecution_Service{ - Service: "zitadel.user.v2beta.UserService", + Service: "zitadel.user.v2.UserService", }, }, }, @@ -512,7 +512,7 @@ func TestServer_SetExecution_Response(t *testing.T) { ConditionType: &action.Condition_Response{ Response: &action.ResponseExecution{ Condition: &action.ResponseExecution_Method{ - Method: "/zitadel.session.v2beta.NotExistingService/List", + Method: "/zitadel.session.v2.NotExistingService/List", }, }, }, @@ -529,7 +529,7 @@ func TestServer_SetExecution_Response(t *testing.T) { ConditionType: &action.Condition_Response{ Response: &action.ResponseExecution{ Condition: &action.ResponseExecution_Method{ - Method: "/zitadel.session.v2beta.SessionService/ListSessions", + Method: "/zitadel.session.v2.SessionService/ListSessions", }, }, }, @@ -568,7 +568,7 @@ func TestServer_SetExecution_Response(t *testing.T) { ConditionType: &action.Condition_Response{ Response: &action.ResponseExecution{ Condition: &action.ResponseExecution_Service{ - Service: "zitadel.session.v2beta.SessionService", + Service: "zitadel.session.v2.SessionService", }, }, }, @@ -670,7 +670,7 @@ func TestServer_DeleteExecution_Response(t *testing.T) { ConditionType: &action.Condition_Response{ Response: &action.ResponseExecution{ Condition: &action.ResponseExecution_Method{ - Method: "/zitadel.session.v2beta.SessionService/NotExisting", + Method: "/zitadel.session.v2.SessionService/NotExisting", }, }, }, @@ -690,7 +690,7 @@ func TestServer_DeleteExecution_Response(t *testing.T) { ConditionType: &action.Condition_Response{ Response: &action.ResponseExecution{ Condition: &action.ResponseExecution_Method{ - Method: "/zitadel.session.v2beta.SessionService/GetSession", + Method: "/zitadel.session.v2.SessionService/GetSession", }, }, }, @@ -731,7 +731,7 @@ func TestServer_DeleteExecution_Response(t *testing.T) { ConditionType: &action.Condition_Response{ Response: &action.ResponseExecution{ Condition: &action.ResponseExecution_Service{ - Service: "zitadel.user.v2beta.UserService", + Service: "zitadel.user.v2.UserService", }, }, }, diff --git a/internal/api/grpc/action/v3alpha/query_integration_test.go b/internal/api/grpc/action/v3alpha/query_integration_test.go index b083eda5a4..279109ef78 100644 --- a/internal/api/grpc/action/v3alpha/query_integration_test.go +++ b/internal/api/grpc/action/v3alpha/query_integration_test.go @@ -16,7 +16,7 @@ import ( "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/integration" action "github.com/zitadel/zitadel/pkg/grpc/action/v3alpha" - object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta" + "github.com/zitadel/zitadel/pkg/grpc/object/v2" ) func TestServer_GetTargetByID(t *testing.T) { @@ -532,7 +532,7 @@ func TestServer_ListExecutions(t *testing.T) { ConditionType: &action.Condition_Request{ Request: &action.RequestExecution{ Condition: &action.RequestExecution_Method{ - Method: "/zitadel.session.v2beta.SessionService/GetSession", + Method: "/zitadel.session.v2.SessionService/GetSession", }, }, }, @@ -555,7 +555,7 @@ func TestServer_ListExecutions(t *testing.T) { ConditionType: &action.Condition_Request{ Request: &action.RequestExecution{ Condition: &action.RequestExecution_Method{ - Method: "/zitadel.session.v2beta.SessionService/GetSession", + Method: "/zitadel.session.v2.SessionService/GetSession", }, }, }, @@ -720,7 +720,7 @@ func TestServer_ListExecutions(t *testing.T) { ConditionType: &action.Condition_Request{ Request: &action.RequestExecution{ Condition: &action.RequestExecution_Method{ - Method: "/zitadel.session.v2beta.SessionService/GetSession", + Method: "/zitadel.session.v2.SessionService/GetSession", }, }, }, @@ -729,7 +729,7 @@ func TestServer_ListExecutions(t *testing.T) { ConditionType: &action.Condition_Request{ Request: &action.RequestExecution{ Condition: &action.RequestExecution_Method{ - Method: "/zitadel.session.v2beta.SessionService/CreateSession", + Method: "/zitadel.session.v2.SessionService/CreateSession", }, }, }, @@ -738,7 +738,7 @@ func TestServer_ListExecutions(t *testing.T) { ConditionType: &action.Condition_Request{ Request: &action.RequestExecution{ Condition: &action.RequestExecution_Method{ - Method: "/zitadel.session.v2beta.SessionService/SetSession", + Method: "/zitadel.session.v2.SessionService/SetSession", }, }, }, @@ -795,11 +795,11 @@ func TestServer_ListExecutions(t *testing.T) { Query: &action.SearchQuery_InConditionsQuery{ InConditionsQuery: &action.InConditionsQuery{ Conditions: []*action.Condition{ - {ConditionType: &action.Condition_Request{Request: &action.RequestExecution{Condition: &action.RequestExecution_Method{Method: "/zitadel.session.v2beta.SessionService/GetSession"}}}}, - {ConditionType: &action.Condition_Request{Request: &action.RequestExecution{Condition: &action.RequestExecution_Service{Service: "zitadel.session.v2beta.SessionService"}}}}, + {ConditionType: &action.Condition_Request{Request: &action.RequestExecution{Condition: &action.RequestExecution_Method{Method: "/zitadel.session.v2.SessionService/GetSession"}}}}, + {ConditionType: &action.Condition_Request{Request: &action.RequestExecution{Condition: &action.RequestExecution_Service{Service: "zitadel.session.v2.SessionService"}}}}, {ConditionType: &action.Condition_Request{Request: &action.RequestExecution{Condition: &action.RequestExecution_All{All: true}}}}, - {ConditionType: &action.Condition_Response{Response: &action.ResponseExecution{Condition: &action.ResponseExecution_Method{Method: "/zitadel.session.v2beta.SessionService/GetSession"}}}}, - {ConditionType: &action.Condition_Response{Response: &action.ResponseExecution{Condition: &action.ResponseExecution_Service{Service: "zitadel.session.v2beta.SessionService"}}}}, + {ConditionType: &action.Condition_Response{Response: &action.ResponseExecution{Condition: &action.ResponseExecution_Method{Method: "/zitadel.session.v2.SessionService/GetSession"}}}}, + {ConditionType: &action.Condition_Response{Response: &action.ResponseExecution{Condition: &action.ResponseExecution_Service{Service: "zitadel.session.v2.SessionService"}}}}, {ConditionType: &action.Condition_Response{Response: &action.ResponseExecution{Condition: &action.ResponseExecution_All{All: true}}}}, {ConditionType: &action.Condition_Event{Event: &action.EventExecution{Condition: &action.EventExecution_Event{Event: "user.added"}}}}, {ConditionType: &action.Condition_Event{Event: &action.EventExecution{Condition: &action.EventExecution_Group{Group: "user"}}}}, diff --git a/internal/api/grpc/action/v3alpha/server_integration_test.go b/internal/api/grpc/action/v3alpha/server_integration_test.go index d27105fc48..e97605e1f0 100644 --- a/internal/api/grpc/action/v3alpha/server_integration_test.go +++ b/internal/api/grpc/action/v3alpha/server_integration_test.go @@ -14,7 +14,7 @@ import ( "github.com/zitadel/zitadel/internal/integration" action "github.com/zitadel/zitadel/pkg/grpc/action/v3alpha" - feature "github.com/zitadel/zitadel/pkg/grpc/feature/v2beta" + "github.com/zitadel/zitadel/pkg/grpc/feature/v2" ) var ( diff --git a/internal/api/grpc/action/v3alpha/target_integration_test.go b/internal/api/grpc/action/v3alpha/target_integration_test.go index 8b143fddb8..539f3c6d35 100644 --- a/internal/api/grpc/action/v3alpha/target_integration_test.go +++ b/internal/api/grpc/action/v3alpha/target_integration_test.go @@ -17,7 +17,7 @@ import ( "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/integration" action "github.com/zitadel/zitadel/pkg/grpc/action/v3alpha" - object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta" + "github.com/zitadel/zitadel/pkg/grpc/object/v2" ) func TestServer_CreateTarget(t *testing.T) { diff --git a/internal/api/grpc/feature/v2/converter.go b/internal/api/grpc/feature/v2/converter.go index e65a1f26b1..4d0698feaf 100644 --- a/internal/api/grpc/feature/v2/converter.go +++ b/internal/api/grpc/feature/v2/converter.go @@ -5,7 +5,7 @@ import ( "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/feature" "github.com/zitadel/zitadel/internal/query" - feature_pb "github.com/zitadel/zitadel/pkg/grpc/feature/v2beta" + feature_pb "github.com/zitadel/zitadel/pkg/grpc/feature/v2" ) func systemFeaturesToCommand(req *feature_pb.SetSystemFeaturesRequest) *command.SystemFeatures { diff --git a/internal/api/grpc/feature/v2/converter_test.go b/internal/api/grpc/feature/v2/converter_test.go index 35dbf98014..7c2cf5fc39 100644 --- a/internal/api/grpc/feature/v2/converter_test.go +++ b/internal/api/grpc/feature/v2/converter_test.go @@ -12,8 +12,8 @@ import ( "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/feature" "github.com/zitadel/zitadel/internal/query" - feature_pb "github.com/zitadel/zitadel/pkg/grpc/feature/v2beta" - object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta" + feature_pb "github.com/zitadel/zitadel/pkg/grpc/feature/v2" + "github.com/zitadel/zitadel/pkg/grpc/object/v2" ) func Test_systemFeaturesToCommand(t *testing.T) { diff --git a/internal/api/grpc/feature/v2/feature.go b/internal/api/grpc/feature/v2/feature.go index 1e4200ccf1..9125dea518 100644 --- a/internal/api/grpc/feature/v2/feature.go +++ b/internal/api/grpc/feature/v2/feature.go @@ -7,7 +7,7 @@ import ( "google.golang.org/grpc/status" "github.com/zitadel/zitadel/internal/api/grpc/object/v2" - feature "github.com/zitadel/zitadel/pkg/grpc/feature/v2beta" + "github.com/zitadel/zitadel/pkg/grpc/feature/v2" ) func (s *Server) SetSystemFeatures(ctx context.Context, req *feature.SetSystemFeaturesRequest) (_ *feature.SetSystemFeaturesResponse, err error) { diff --git a/internal/api/grpc/feature/v2/feature_integration_test.go b/internal/api/grpc/feature/v2/feature_integration_test.go index 5dcd0c37f4..3936b4bcd5 100644 --- a/internal/api/grpc/feature/v2/feature_integration_test.go +++ b/internal/api/grpc/feature/v2/feature_integration_test.go @@ -14,8 +14,8 @@ import ( "google.golang.org/protobuf/types/known/timestamppb" "github.com/zitadel/zitadel/internal/integration" - feature "github.com/zitadel/zitadel/pkg/grpc/feature/v2beta" - object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta" + "github.com/zitadel/zitadel/pkg/grpc/feature/v2" + "github.com/zitadel/zitadel/pkg/grpc/object/v2" ) var ( diff --git a/internal/api/grpc/feature/v2/server.go b/internal/api/grpc/feature/v2/server.go index 4208c4acfc..ab92df5822 100644 --- a/internal/api/grpc/feature/v2/server.go +++ b/internal/api/grpc/feature/v2/server.go @@ -7,7 +7,7 @@ import ( "github.com/zitadel/zitadel/internal/api/grpc/server" "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/query" - feature "github.com/zitadel/zitadel/pkg/grpc/feature/v2beta" + "github.com/zitadel/zitadel/pkg/grpc/feature/v2" ) type Server struct { diff --git a/internal/api/grpc/feature/v2beta/converter.go b/internal/api/grpc/feature/v2beta/converter.go new file mode 100644 index 0000000000..c866cc017d --- /dev/null +++ b/internal/api/grpc/feature/v2beta/converter.go @@ -0,0 +1,155 @@ +package feature + +import ( + object "github.com/zitadel/zitadel/internal/api/grpc/object/v2beta" + "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/feature" + "github.com/zitadel/zitadel/internal/query" + feature_pb "github.com/zitadel/zitadel/pkg/grpc/feature/v2beta" +) + +func systemFeaturesToCommand(req *feature_pb.SetSystemFeaturesRequest) *command.SystemFeatures { + return &command.SystemFeatures{ + LoginDefaultOrg: req.LoginDefaultOrg, + TriggerIntrospectionProjections: req.OidcTriggerIntrospectionProjections, + LegacyIntrospection: req.OidcLegacyIntrospection, + UserSchema: req.UserSchema, + Actions: req.Actions, + TokenExchange: req.OidcTokenExchange, + ImprovedPerformance: improvedPerformanceListToDomain(req.ImprovedPerformance), + } +} + +func systemFeaturesToPb(f *query.SystemFeatures) *feature_pb.GetSystemFeaturesResponse { + return &feature_pb.GetSystemFeaturesResponse{ + Details: object.DomainToDetailsPb(f.Details), + LoginDefaultOrg: featureSourceToFlagPb(&f.LoginDefaultOrg), + OidcTriggerIntrospectionProjections: featureSourceToFlagPb(&f.TriggerIntrospectionProjections), + OidcLegacyIntrospection: featureSourceToFlagPb(&f.LegacyIntrospection), + UserSchema: featureSourceToFlagPb(&f.UserSchema), + OidcTokenExchange: featureSourceToFlagPb(&f.TokenExchange), + Actions: featureSourceToFlagPb(&f.Actions), + ImprovedPerformance: featureSourceToImprovedPerformanceFlagPb(&f.ImprovedPerformance), + } +} + +func instanceFeaturesToCommand(req *feature_pb.SetInstanceFeaturesRequest) *command.InstanceFeatures { + return &command.InstanceFeatures{ + LoginDefaultOrg: req.LoginDefaultOrg, + TriggerIntrospectionProjections: req.OidcTriggerIntrospectionProjections, + LegacyIntrospection: req.OidcLegacyIntrospection, + UserSchema: req.UserSchema, + TokenExchange: req.OidcTokenExchange, + Actions: req.Actions, + ImprovedPerformance: improvedPerformanceListToDomain(req.ImprovedPerformance), + } +} + +func instanceFeaturesToPb(f *query.InstanceFeatures) *feature_pb.GetInstanceFeaturesResponse { + return &feature_pb.GetInstanceFeaturesResponse{ + Details: object.DomainToDetailsPb(f.Details), + LoginDefaultOrg: featureSourceToFlagPb(&f.LoginDefaultOrg), + OidcTriggerIntrospectionProjections: featureSourceToFlagPb(&f.TriggerIntrospectionProjections), + OidcLegacyIntrospection: featureSourceToFlagPb(&f.LegacyIntrospection), + UserSchema: featureSourceToFlagPb(&f.UserSchema), + OidcTokenExchange: featureSourceToFlagPb(&f.TokenExchange), + Actions: featureSourceToFlagPb(&f.Actions), + ImprovedPerformance: featureSourceToImprovedPerformanceFlagPb(&f.ImprovedPerformance), + } +} + +func featureSourceToImprovedPerformanceFlagPb(fs *query.FeatureSource[[]feature.ImprovedPerformanceType]) *feature_pb.ImprovedPerformanceFeatureFlag { + return &feature_pb.ImprovedPerformanceFeatureFlag{ + ExecutionPaths: improvedPerformanceTypesToPb(fs.Value), + Source: featureLevelToSourcePb(fs.Level), + } +} + +func featureSourceToFlagPb(fs *query.FeatureSource[bool]) *feature_pb.FeatureFlag { + return &feature_pb.FeatureFlag{ + Enabled: fs.Value, + Source: featureLevelToSourcePb(fs.Level), + } +} + +func featureLevelToSourcePb(level feature.Level) feature_pb.Source { + switch level { + case feature.LevelUnspecified: + return feature_pb.Source_SOURCE_UNSPECIFIED + case feature.LevelSystem: + return feature_pb.Source_SOURCE_SYSTEM + case feature.LevelInstance: + return feature_pb.Source_SOURCE_INSTANCE + case feature.LevelOrg: + return feature_pb.Source_SOURCE_ORGANIZATION + case feature.LevelProject: + return feature_pb.Source_SOURCE_PROJECT + case feature.LevelApp: + return feature_pb.Source_SOURCE_APP + case feature.LevelUser: + return feature_pb.Source_SOURCE_USER + default: + return feature_pb.Source(level) + } +} + +func improvedPerformanceTypesToPb(types []feature.ImprovedPerformanceType) []feature_pb.ImprovedPerformance { + res := make([]feature_pb.ImprovedPerformance, len(types)) + + for i, typ := range types { + res[i] = improvedPerformanceTypeToPb(typ) + } + + return res +} + +func improvedPerformanceTypeToPb(typ feature.ImprovedPerformanceType) feature_pb.ImprovedPerformance { + switch typ { + case feature.ImprovedPerformanceTypeUnknown: + return feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_UNSPECIFIED + case feature.ImprovedPerformanceTypeOrgByID: + return feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_ORG_BY_ID + case feature.ImprovedPerformanceTypeProjectGrant: + return feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_PROJECT_GRANT + case feature.ImprovedPerformanceTypeProject: + return feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_PROJECT + case feature.ImprovedPerformanceTypeUserGrant: + return feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_USER_GRANT + case feature.ImprovedPerformanceTypeOrgDomainVerified: + return feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_ORG_DOMAIN_VERIFIED + default: + return feature_pb.ImprovedPerformance(typ) + } +} + +func improvedPerformanceListToDomain(list []feature_pb.ImprovedPerformance) []feature.ImprovedPerformanceType { + if list == nil { + return nil + } + res := make([]feature.ImprovedPerformanceType, len(list)) + + for i, typ := range list { + res[i] = improvedPerformanceToDomain(typ) + } + + return res +} + +func improvedPerformanceToDomain(typ feature_pb.ImprovedPerformance) feature.ImprovedPerformanceType { + switch typ { + case feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_UNSPECIFIED: + return feature.ImprovedPerformanceTypeUnknown + case feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_ORG_BY_ID: + return feature.ImprovedPerformanceTypeOrgByID + case feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_PROJECT_GRANT: + return feature.ImprovedPerformanceTypeProjectGrant + case feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_PROJECT: + return feature.ImprovedPerformanceTypeProject + case feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_USER_GRANT: + return feature.ImprovedPerformanceTypeUserGrant + case feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_ORG_DOMAIN_VERIFIED: + return feature.ImprovedPerformanceTypeOrgDomainVerified + default: + return feature.ImprovedPerformanceTypeUnknown + } +} diff --git a/internal/api/grpc/feature/v2beta/converter_test.go b/internal/api/grpc/feature/v2beta/converter_test.go new file mode 100644 index 0000000000..35dbf98014 --- /dev/null +++ b/internal/api/grpc/feature/v2beta/converter_test.go @@ -0,0 +1,268 @@ +package feature + +import ( + "testing" + "time" + + "github.com/muhlemmer/gu" + "github.com/stretchr/testify/assert" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/feature" + "github.com/zitadel/zitadel/internal/query" + feature_pb "github.com/zitadel/zitadel/pkg/grpc/feature/v2beta" + object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta" +) + +func Test_systemFeaturesToCommand(t *testing.T) { + arg := &feature_pb.SetSystemFeaturesRequest{ + LoginDefaultOrg: gu.Ptr(true), + OidcTriggerIntrospectionProjections: gu.Ptr(false), + OidcLegacyIntrospection: nil, + UserSchema: gu.Ptr(true), + Actions: gu.Ptr(true), + OidcTokenExchange: gu.Ptr(true), + ImprovedPerformance: nil, + } + want := &command.SystemFeatures{ + LoginDefaultOrg: gu.Ptr(true), + TriggerIntrospectionProjections: gu.Ptr(false), + LegacyIntrospection: nil, + UserSchema: gu.Ptr(true), + Actions: gu.Ptr(true), + TokenExchange: gu.Ptr(true), + ImprovedPerformance: nil, + } + got := systemFeaturesToCommand(arg) + assert.Equal(t, want, got) +} + +func Test_systemFeaturesToPb(t *testing.T) { + arg := &query.SystemFeatures{ + Details: &domain.ObjectDetails{ + Sequence: 22, + EventDate: time.Unix(123, 0), + ResourceOwner: "SYSTEM", + }, + LoginDefaultOrg: query.FeatureSource[bool]{ + Level: feature.LevelSystem, + Value: true, + }, + TriggerIntrospectionProjections: query.FeatureSource[bool]{ + Level: feature.LevelUnspecified, + Value: false, + }, + LegacyIntrospection: query.FeatureSource[bool]{ + Level: feature.LevelSystem, + Value: true, + }, + UserSchema: query.FeatureSource[bool]{ + Level: feature.LevelSystem, + Value: true, + }, + Actions: query.FeatureSource[bool]{ + Level: feature.LevelSystem, + Value: true, + }, + TokenExchange: query.FeatureSource[bool]{ + Level: feature.LevelSystem, + Value: false, + }, + ImprovedPerformance: query.FeatureSource[[]feature.ImprovedPerformanceType]{ + Level: feature.LevelSystem, + Value: []feature.ImprovedPerformanceType{feature.ImprovedPerformanceTypeOrgByID}, + }, + } + want := &feature_pb.GetSystemFeaturesResponse{ + Details: &object.Details{ + Sequence: 22, + ChangeDate: ×tamppb.Timestamp{Seconds: 123}, + ResourceOwner: "SYSTEM", + }, + LoginDefaultOrg: &feature_pb.FeatureFlag{ + Enabled: true, + Source: feature_pb.Source_SOURCE_SYSTEM, + }, + OidcTriggerIntrospectionProjections: &feature_pb.FeatureFlag{ + Enabled: false, + Source: feature_pb.Source_SOURCE_UNSPECIFIED, + }, + OidcLegacyIntrospection: &feature_pb.FeatureFlag{ + Enabled: true, + Source: feature_pb.Source_SOURCE_SYSTEM, + }, + UserSchema: &feature_pb.FeatureFlag{ + Enabled: true, + Source: feature_pb.Source_SOURCE_SYSTEM, + }, + OidcTokenExchange: &feature_pb.FeatureFlag{ + Enabled: false, + Source: feature_pb.Source_SOURCE_SYSTEM, + }, + Actions: &feature_pb.FeatureFlag{ + Enabled: true, + Source: feature_pb.Source_SOURCE_SYSTEM, + }, + ImprovedPerformance: &feature_pb.ImprovedPerformanceFeatureFlag{ + ExecutionPaths: []feature_pb.ImprovedPerformance{feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_ORG_BY_ID}, + Source: feature_pb.Source_SOURCE_SYSTEM, + }, + } + got := systemFeaturesToPb(arg) + assert.Equal(t, want, got) +} + +func Test_instanceFeaturesToCommand(t *testing.T) { + arg := &feature_pb.SetInstanceFeaturesRequest{ + LoginDefaultOrg: gu.Ptr(true), + OidcTriggerIntrospectionProjections: gu.Ptr(false), + OidcLegacyIntrospection: nil, + UserSchema: gu.Ptr(true), + OidcTokenExchange: gu.Ptr(true), + Actions: gu.Ptr(true), + ImprovedPerformance: nil, + } + want := &command.InstanceFeatures{ + LoginDefaultOrg: gu.Ptr(true), + TriggerIntrospectionProjections: gu.Ptr(false), + LegacyIntrospection: nil, + UserSchema: gu.Ptr(true), + TokenExchange: gu.Ptr(true), + Actions: gu.Ptr(true), + ImprovedPerformance: nil, + } + got := instanceFeaturesToCommand(arg) + assert.Equal(t, want, got) +} + +func Test_instanceFeaturesToPb(t *testing.T) { + arg := &query.InstanceFeatures{ + Details: &domain.ObjectDetails{ + Sequence: 22, + EventDate: time.Unix(123, 0), + ResourceOwner: "instance1", + }, + LoginDefaultOrg: query.FeatureSource[bool]{ + Level: feature.LevelSystem, + Value: true, + }, + TriggerIntrospectionProjections: query.FeatureSource[bool]{ + Level: feature.LevelUnspecified, + Value: false, + }, + LegacyIntrospection: query.FeatureSource[bool]{ + Level: feature.LevelInstance, + Value: true, + }, + UserSchema: query.FeatureSource[bool]{ + Level: feature.LevelInstance, + Value: true, + }, + Actions: query.FeatureSource[bool]{ + Level: feature.LevelInstance, + Value: true, + }, + TokenExchange: query.FeatureSource[bool]{ + Level: feature.LevelSystem, + Value: false, + }, + ImprovedPerformance: query.FeatureSource[[]feature.ImprovedPerformanceType]{ + Level: feature.LevelSystem, + Value: []feature.ImprovedPerformanceType{feature.ImprovedPerformanceTypeOrgByID}, + }, + } + want := &feature_pb.GetInstanceFeaturesResponse{ + Details: &object.Details{ + Sequence: 22, + ChangeDate: ×tamppb.Timestamp{Seconds: 123}, + ResourceOwner: "instance1", + }, + LoginDefaultOrg: &feature_pb.FeatureFlag{ + Enabled: true, + Source: feature_pb.Source_SOURCE_SYSTEM, + }, + OidcTriggerIntrospectionProjections: &feature_pb.FeatureFlag{ + Enabled: false, + Source: feature_pb.Source_SOURCE_UNSPECIFIED, + }, + OidcLegacyIntrospection: &feature_pb.FeatureFlag{ + Enabled: true, + Source: feature_pb.Source_SOURCE_INSTANCE, + }, + UserSchema: &feature_pb.FeatureFlag{ + Enabled: true, + Source: feature_pb.Source_SOURCE_INSTANCE, + }, + Actions: &feature_pb.FeatureFlag{ + Enabled: true, + Source: feature_pb.Source_SOURCE_INSTANCE, + }, + OidcTokenExchange: &feature_pb.FeatureFlag{ + Enabled: false, + Source: feature_pb.Source_SOURCE_SYSTEM, + }, + ImprovedPerformance: &feature_pb.ImprovedPerformanceFeatureFlag{ + ExecutionPaths: []feature_pb.ImprovedPerformance{feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_ORG_BY_ID}, + Source: feature_pb.Source_SOURCE_SYSTEM, + }, + } + got := instanceFeaturesToPb(arg) + assert.Equal(t, want, got) +} + +func Test_featureLevelToSourcePb(t *testing.T) { + tests := []struct { + name string + level feature.Level + want feature_pb.Source + }{ + { + name: "unspecified", + level: feature.LevelUnspecified, + want: feature_pb.Source_SOURCE_UNSPECIFIED, + }, + { + name: "system", + level: feature.LevelSystem, + want: feature_pb.Source_SOURCE_SYSTEM, + }, + { + name: "instance", + level: feature.LevelInstance, + want: feature_pb.Source_SOURCE_INSTANCE, + }, + { + name: "org", + level: feature.LevelOrg, + want: feature_pb.Source_SOURCE_ORGANIZATION, + }, + { + name: "project", + level: feature.LevelProject, + want: feature_pb.Source_SOURCE_PROJECT, + }, + { + name: "app", + level: feature.LevelApp, + want: feature_pb.Source_SOURCE_APP, + }, + { + name: "user", + level: feature.LevelUser, + want: feature_pb.Source_SOURCE_USER, + }, + { + name: "unknown", + level: 99, + want: 99, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := featureLevelToSourcePb(tt.level) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/internal/api/grpc/feature/v2beta/feature.go b/internal/api/grpc/feature/v2beta/feature.go new file mode 100644 index 0000000000..b94f8e7de2 --- /dev/null +++ b/internal/api/grpc/feature/v2beta/feature.go @@ -0,0 +1,86 @@ +package feature + +import ( + "context" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + object "github.com/zitadel/zitadel/internal/api/grpc/object/v2beta" + feature "github.com/zitadel/zitadel/pkg/grpc/feature/v2beta" +) + +func (s *Server) SetSystemFeatures(ctx context.Context, req *feature.SetSystemFeaturesRequest) (_ *feature.SetSystemFeaturesResponse, err error) { + details, err := s.command.SetSystemFeatures(ctx, systemFeaturesToCommand(req)) + if err != nil { + return nil, err + } + return &feature.SetSystemFeaturesResponse{ + Details: object.DomainToDetailsPb(details), + }, nil +} + +func (s *Server) ResetSystemFeatures(ctx context.Context, req *feature.ResetSystemFeaturesRequest) (_ *feature.ResetSystemFeaturesResponse, err error) { + details, err := s.command.ResetSystemFeatures(ctx) + if err != nil { + return nil, err + } + return &feature.ResetSystemFeaturesResponse{ + Details: object.DomainToDetailsPb(details), + }, nil +} + +func (s *Server) GetSystemFeatures(ctx context.Context, req *feature.GetSystemFeaturesRequest) (_ *feature.GetSystemFeaturesResponse, err error) { + f, err := s.query.GetSystemFeatures(ctx) + if err != nil { + return nil, err + } + return systemFeaturesToPb(f), nil +} + +func (s *Server) SetInstanceFeatures(ctx context.Context, req *feature.SetInstanceFeaturesRequest) (_ *feature.SetInstanceFeaturesResponse, err error) { + details, err := s.command.SetInstanceFeatures(ctx, instanceFeaturesToCommand(req)) + if err != nil { + return nil, err + } + return &feature.SetInstanceFeaturesResponse{ + Details: object.DomainToDetailsPb(details), + }, nil +} + +func (s *Server) ResetInstanceFeatures(ctx context.Context, req *feature.ResetInstanceFeaturesRequest) (_ *feature.ResetInstanceFeaturesResponse, err error) { + details, err := s.command.ResetInstanceFeatures(ctx) + if err != nil { + return nil, err + } + return &feature.ResetInstanceFeaturesResponse{ + Details: object.DomainToDetailsPb(details), + }, nil +} + +func (s *Server) GetInstanceFeatures(ctx context.Context, req *feature.GetInstanceFeaturesRequest) (_ *feature.GetInstanceFeaturesResponse, err error) { + f, err := s.query.GetInstanceFeatures(ctx, req.GetInheritance()) + if err != nil { + return nil, err + } + return instanceFeaturesToPb(f), nil +} + +func (s *Server) SetOrganizationFeatures(ctx context.Context, req *feature.SetOrganizationFeaturesRequest) (_ *feature.SetOrganizationFeaturesResponse, err error) { + return nil, status.Errorf(codes.Unimplemented, "method SetOrganizationFeatures not implemented") +} +func (s *Server) ResetOrganizationFeatures(ctx context.Context, req *feature.ResetOrganizationFeaturesRequest) (_ *feature.ResetOrganizationFeaturesResponse, err error) { + return nil, status.Errorf(codes.Unimplemented, "method ResetOrganizationFeatures not implemented") +} +func (s *Server) GetOrganizationFeatures(ctx context.Context, req *feature.GetOrganizationFeaturesRequest) (_ *feature.GetOrganizationFeaturesResponse, err error) { + return nil, status.Errorf(codes.Unimplemented, "method GetOrganizationFeatures not implemented") +} +func (s *Server) SetUserFeatures(ctx context.Context, req *feature.SetUserFeatureRequest) (_ *feature.SetUserFeaturesResponse, err error) { + return nil, status.Errorf(codes.Unimplemented, "method SetUserFeatures not implemented") +} +func (s *Server) ResetUserFeatures(ctx context.Context, req *feature.ResetUserFeaturesRequest) (_ *feature.ResetUserFeaturesResponse, err error) { + return nil, status.Errorf(codes.Unimplemented, "method ResetUserFeatures not implemented") +} +func (s *Server) GetUserFeatures(ctx context.Context, req *feature.GetUserFeaturesRequest) (_ *feature.GetUserFeaturesResponse, err error) { + return nil, status.Errorf(codes.Unimplemented, "method GetUserFeatures not implemented") +} diff --git a/internal/api/grpc/feature/v2beta/feature_integration_test.go b/internal/api/grpc/feature/v2beta/feature_integration_test.go new file mode 100644 index 0000000000..794a080202 --- /dev/null +++ b/internal/api/grpc/feature/v2beta/feature_integration_test.go @@ -0,0 +1,499 @@ +//go:build integration + +package feature_test + +import ( + "context" + "os" + "testing" + "time" + + "github.com/muhlemmer/gu" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/integration" + feature "github.com/zitadel/zitadel/pkg/grpc/feature/v2beta" + object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta" +) + +var ( + SystemCTX context.Context + IamCTX context.Context + OrgCTX context.Context + Tester *integration.Tester + Client feature.FeatureServiceClient +) + +func TestMain(m *testing.M) { + os.Exit(func() int { + ctx, _, cancel := integration.Contexts(5 * time.Minute) + defer cancel() + Tester = integration.NewTester(ctx) + SystemCTX = Tester.WithAuthorization(ctx, integration.SystemUser) + IamCTX = Tester.WithAuthorization(ctx, integration.IAMOwner) + OrgCTX = Tester.WithAuthorization(ctx, integration.OrgOwner) + + defer Tester.Done() + Client = Tester.Client.FeatureV2beta + + return m.Run() + }()) +} + +func TestServer_SetSystemFeatures(t *testing.T) { + type args struct { + ctx context.Context + req *feature.SetSystemFeaturesRequest + } + tests := []struct { + name string + args args + want *feature.SetSystemFeaturesResponse + wantErr bool + }{ + { + name: "permission error", + args: args{ + ctx: IamCTX, + req: &feature.SetSystemFeaturesRequest{ + OidcTriggerIntrospectionProjections: gu.Ptr(true), + }, + }, + wantErr: true, + }, + { + name: "no changes error", + args: args{ + ctx: SystemCTX, + req: &feature.SetSystemFeaturesRequest{}, + }, + wantErr: true, + }, + { + name: "success", + args: args{ + ctx: SystemCTX, + req: &feature.SetSystemFeaturesRequest{ + OidcTriggerIntrospectionProjections: gu.Ptr(true), + }, + }, + want: &feature.SetSystemFeaturesResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: "SYSTEM", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Cleanup(func() { + // make sure we have a clean state after each test + _, err := Client.ResetSystemFeatures(SystemCTX, &feature.ResetSystemFeaturesRequest{}) + require.NoError(t, err) + }) + got, err := Client.SetSystemFeatures(tt.args.ctx, tt.args.req) + if tt.wantErr { + assert.Error(t, err) + return + } + require.NoError(t, err) + integration.AssertDetails(t, tt.want, got) + }) + } +} + +func TestServer_ResetSystemFeatures(t *testing.T) { + _, err := Client.SetSystemFeatures(SystemCTX, &feature.SetSystemFeaturesRequest{ + LoginDefaultOrg: gu.Ptr(true), + }) + require.NoError(t, err) + + tests := []struct { + name string + ctx context.Context + want *feature.ResetSystemFeaturesResponse + wantErr bool + }{ + { + name: "permission error", + ctx: IamCTX, + wantErr: true, + }, + { + name: "success", + ctx: SystemCTX, + want: &feature.ResetSystemFeaturesResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: "SYSTEM", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Client.ResetSystemFeatures(tt.ctx, &feature.ResetSystemFeaturesRequest{}) + if tt.wantErr { + assert.Error(t, err) + return + } + require.NoError(t, err) + integration.AssertDetails(t, tt.want, got) + }) + } +} + +func TestServer_GetSystemFeatures(t *testing.T) { + type args struct { + ctx context.Context + req *feature.GetSystemFeaturesRequest + } + tests := []struct { + name string + prepare func(t *testing.T) + args args + want *feature.GetSystemFeaturesResponse + wantErr bool + }{ + { + name: "permission error", + args: args{ + ctx: IamCTX, + req: &feature.GetSystemFeaturesRequest{}, + }, + wantErr: true, + }, + { + name: "nothing set", + args: args{ + ctx: SystemCTX, + req: &feature.GetSystemFeaturesRequest{}, + }, + want: &feature.GetSystemFeaturesResponse{}, + }, + { + name: "some features", + prepare: func(t *testing.T) { + _, err := Client.SetSystemFeatures(SystemCTX, &feature.SetSystemFeaturesRequest{ + LoginDefaultOrg: gu.Ptr(true), + OidcTriggerIntrospectionProjections: gu.Ptr(false), + }) + require.NoError(t, err) + }, + args: args{ + ctx: SystemCTX, + req: &feature.GetSystemFeaturesRequest{}, + }, + want: &feature.GetSystemFeaturesResponse{ + LoginDefaultOrg: &feature.FeatureFlag{ + Enabled: true, + Source: feature.Source_SOURCE_SYSTEM, + }, + OidcTriggerIntrospectionProjections: &feature.FeatureFlag{ + Enabled: false, + Source: feature.Source_SOURCE_SYSTEM, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Cleanup(func() { + // make sure we have a clean state after each test + _, err := Client.ResetSystemFeatures(SystemCTX, &feature.ResetSystemFeaturesRequest{}) + require.NoError(t, err) + }) + if tt.prepare != nil { + tt.prepare(t) + } + got, err := Client.GetSystemFeatures(tt.args.ctx, tt.args.req) + if tt.wantErr { + assert.Error(t, err) + return + } + require.NoError(t, err) + assertFeatureFlag(t, tt.want.LoginDefaultOrg, got.LoginDefaultOrg) + assertFeatureFlag(t, tt.want.OidcTriggerIntrospectionProjections, got.OidcTriggerIntrospectionProjections) + assertFeatureFlag(t, tt.want.OidcLegacyIntrospection, got.OidcLegacyIntrospection) + assertFeatureFlag(t, tt.want.UserSchema, got.UserSchema) + assertFeatureFlag(t, tt.want.Actions, got.Actions) + }) + } +} + +func TestServer_SetInstanceFeatures(t *testing.T) { + type args struct { + ctx context.Context + req *feature.SetInstanceFeaturesRequest + } + tests := []struct { + name string + args args + want *feature.SetInstanceFeaturesResponse + wantErr bool + }{ + { + name: "permission error", + args: args{ + ctx: OrgCTX, + req: &feature.SetInstanceFeaturesRequest{ + OidcTriggerIntrospectionProjections: gu.Ptr(true), + }, + }, + wantErr: true, + }, + { + name: "no changes error", + args: args{ + ctx: IamCTX, + req: &feature.SetInstanceFeaturesRequest{}, + }, + wantErr: true, + }, + { + name: "success", + args: args{ + ctx: IamCTX, + req: &feature.SetInstanceFeaturesRequest{ + OidcTriggerIntrospectionProjections: gu.Ptr(true), + }, + }, + want: &feature.SetInstanceFeaturesResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Instance.InstanceID(), + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Cleanup(func() { + // make sure we have a clean state after each test + _, err := Client.ResetInstanceFeatures(IamCTX, &feature.ResetInstanceFeaturesRequest{}) + require.NoError(t, err) + }) + got, err := Client.SetInstanceFeatures(tt.args.ctx, tt.args.req) + if tt.wantErr { + assert.Error(t, err) + return + } + require.NoError(t, err) + integration.AssertDetails(t, tt.want, got) + }) + } +} + +func TestServer_ResetInstanceFeatures(t *testing.T) { + _, err := Client.SetInstanceFeatures(IamCTX, &feature.SetInstanceFeaturesRequest{ + LoginDefaultOrg: gu.Ptr(true), + }) + require.NoError(t, err) + + tests := []struct { + name string + ctx context.Context + want *feature.ResetInstanceFeaturesResponse + wantErr bool + }{ + { + name: "permission error", + ctx: OrgCTX, + wantErr: true, + }, + { + name: "success", + ctx: IamCTX, + want: &feature.ResetInstanceFeaturesResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Instance.InstanceID(), + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Client.ResetInstanceFeatures(tt.ctx, &feature.ResetInstanceFeaturesRequest{}) + if tt.wantErr { + assert.Error(t, err) + return + } + require.NoError(t, err) + integration.AssertDetails(t, tt.want, got) + }) + } +} + +func TestServer_GetInstanceFeatures(t *testing.T) { + _, err := Client.SetSystemFeatures(SystemCTX, &feature.SetSystemFeaturesRequest{ + OidcLegacyIntrospection: gu.Ptr(true), + }) + require.NoError(t, err) + t.Cleanup(func() { + _, err := Client.ResetSystemFeatures(SystemCTX, &feature.ResetSystemFeaturesRequest{}) + require.NoError(t, err) + }) + + type args struct { + ctx context.Context + req *feature.GetInstanceFeaturesRequest + } + tests := []struct { + name string + prepare func(t *testing.T) + args args + want *feature.GetInstanceFeaturesResponse + wantErr bool + }{ + { + name: "permission error", + args: args{ + ctx: OrgCTX, + req: &feature.GetInstanceFeaturesRequest{}, + }, + wantErr: true, + }, + { + name: "defaults, no inheritance", + args: args{ + ctx: IamCTX, + req: &feature.GetInstanceFeaturesRequest{}, + }, + want: &feature.GetInstanceFeaturesResponse{}, + }, + { + name: "defaults, inheritance", + args: args{ + ctx: IamCTX, + req: &feature.GetInstanceFeaturesRequest{ + Inheritance: true, + }, + }, + want: &feature.GetInstanceFeaturesResponse{ + LoginDefaultOrg: &feature.FeatureFlag{ + Enabled: false, + Source: feature.Source_SOURCE_UNSPECIFIED, + }, + OidcTriggerIntrospectionProjections: &feature.FeatureFlag{ + Enabled: false, + Source: feature.Source_SOURCE_UNSPECIFIED, + }, + OidcLegacyIntrospection: &feature.FeatureFlag{ + Enabled: true, + Source: feature.Source_SOURCE_SYSTEM, + }, + UserSchema: &feature.FeatureFlag{ + Enabled: false, + Source: feature.Source_SOURCE_UNSPECIFIED, + }, + Actions: &feature.FeatureFlag{ + Enabled: false, + Source: feature.Source_SOURCE_UNSPECIFIED, + }, + }, + }, + { + name: "some features, no inheritance", + prepare: func(t *testing.T) { + _, err := Client.SetInstanceFeatures(IamCTX, &feature.SetInstanceFeaturesRequest{ + LoginDefaultOrg: gu.Ptr(true), + OidcTriggerIntrospectionProjections: gu.Ptr(false), + UserSchema: gu.Ptr(true), + Actions: gu.Ptr(true), + }) + require.NoError(t, err) + }, + args: args{ + ctx: IamCTX, + req: &feature.GetInstanceFeaturesRequest{}, + }, + want: &feature.GetInstanceFeaturesResponse{ + LoginDefaultOrg: &feature.FeatureFlag{ + Enabled: true, + Source: feature.Source_SOURCE_INSTANCE, + }, + OidcTriggerIntrospectionProjections: &feature.FeatureFlag{ + Enabled: false, + Source: feature.Source_SOURCE_INSTANCE, + }, + UserSchema: &feature.FeatureFlag{ + Enabled: true, + Source: feature.Source_SOURCE_INSTANCE, + }, + Actions: &feature.FeatureFlag{ + Enabled: true, + Source: feature.Source_SOURCE_INSTANCE, + }, + }, + }, + { + name: "one feature, inheritance", + prepare: func(t *testing.T) { + _, err := Client.SetInstanceFeatures(IamCTX, &feature.SetInstanceFeaturesRequest{ + LoginDefaultOrg: gu.Ptr(true), + }) + require.NoError(t, err) + }, + args: args{ + ctx: IamCTX, + req: &feature.GetInstanceFeaturesRequest{ + Inheritance: true, + }, + }, + want: &feature.GetInstanceFeaturesResponse{ + LoginDefaultOrg: &feature.FeatureFlag{ + Enabled: true, + Source: feature.Source_SOURCE_INSTANCE, + }, + OidcTriggerIntrospectionProjections: &feature.FeatureFlag{ + Enabled: false, + Source: feature.Source_SOURCE_UNSPECIFIED, + }, + OidcLegacyIntrospection: &feature.FeatureFlag{ + Enabled: true, + Source: feature.Source_SOURCE_SYSTEM, + }, + UserSchema: &feature.FeatureFlag{ + Enabled: false, + Source: feature.Source_SOURCE_UNSPECIFIED, + }, + Actions: &feature.FeatureFlag{ + Enabled: false, + Source: feature.Source_SOURCE_UNSPECIFIED, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Cleanup(func() { + // make sure we have a clean state after each test + _, err := Client.ResetInstanceFeatures(IamCTX, &feature.ResetInstanceFeaturesRequest{}) + require.NoError(t, err) + }) + if tt.prepare != nil { + tt.prepare(t) + } + got, err := Client.GetInstanceFeatures(tt.args.ctx, tt.args.req) + if tt.wantErr { + assert.Error(t, err) + return + } + require.NoError(t, err) + assertFeatureFlag(t, tt.want.LoginDefaultOrg, got.LoginDefaultOrg) + assertFeatureFlag(t, tt.want.OidcTriggerIntrospectionProjections, got.OidcTriggerIntrospectionProjections) + assertFeatureFlag(t, tt.want.OidcLegacyIntrospection, got.OidcLegacyIntrospection) + assertFeatureFlag(t, tt.want.UserSchema, got.UserSchema) + }) + } +} + +func assertFeatureFlag(t *testing.T, expected, actual *feature.FeatureFlag) { + t.Helper() + assert.Equal(t, expected.GetEnabled(), actual.GetEnabled(), "enabled") + assert.Equal(t, expected.GetSource(), actual.GetSource(), "source") +} diff --git a/internal/api/grpc/feature/v2beta/server.go b/internal/api/grpc/feature/v2beta/server.go new file mode 100644 index 0000000000..4208c4acfc --- /dev/null +++ b/internal/api/grpc/feature/v2beta/server.go @@ -0,0 +1,47 @@ +package feature + +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/query" + feature "github.com/zitadel/zitadel/pkg/grpc/feature/v2beta" +) + +type Server struct { + feature.UnimplementedFeatureServiceServer + command *command.Commands + query *query.Queries +} + +func CreateServer( + command *command.Commands, + query *query.Queries, +) *Server { + return &Server{ + command: command, + query: query, + } +} + +func (s *Server) RegisterServer(grpcServer *grpc.Server) { + feature.RegisterFeatureServiceServer(grpcServer, s) +} + +func (s *Server) AppName() string { + return feature.FeatureService_ServiceDesc.ServiceName +} + +func (s *Server) MethodPrefix() string { + return feature.FeatureService_ServiceDesc.ServiceName +} + +func (s *Server) AuthMethods() authz.MethodMapping { + return feature.FeatureService_AuthMethods +} + +func (s *Server) RegisterGateway() server.RegisterGatewayFunc { + return feature.RegisterFeatureServiceHandler +} diff --git a/internal/api/grpc/object/v2/converter.go b/internal/api/grpc/object/v2/converter.go index cc7e02c7fe..fe8aba5d6e 100644 --- a/internal/api/grpc/object/v2/converter.go +++ b/internal/api/grpc/object/v2/converter.go @@ -8,7 +8,7 @@ import ( "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/query" - object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta" + "github.com/zitadel/zitadel/pkg/grpc/object/v2" ) func DomainToDetailsPb(objectDetail *domain.ObjectDetails) *object.Details { diff --git a/internal/api/grpc/object/v2beta/converter.go b/internal/api/grpc/object/v2beta/converter.go new file mode 100644 index 0000000000..cc7e02c7fe --- /dev/null +++ b/internal/api/grpc/object/v2beta/converter.go @@ -0,0 +1,72 @@ +package object + +import ( + "context" + + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/query" + object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta" +) + +func DomainToDetailsPb(objectDetail *domain.ObjectDetails) *object.Details { + details := &object.Details{ + Sequence: objectDetail.Sequence, + ResourceOwner: objectDetail.ResourceOwner, + } + if !objectDetail.EventDate.IsZero() { + details.ChangeDate = timestamppb.New(objectDetail.EventDate) + } + return details +} + +func ToListDetails(response query.SearchResponse) *object.ListDetails { + details := &object.ListDetails{ + TotalResult: response.Count, + ProcessedSequence: response.Sequence, + Timestamp: timestamppb.New(response.EventCreatedAt), + } + + return details +} +func ListQueryToQuery(query *object.ListQuery) (offset, limit uint64, asc bool) { + if query == nil { + return 0, 0, false + } + return query.Offset, uint64(query.Limit), query.Asc +} + +func ResourceOwnerFromReq(ctx context.Context, req *object.RequestContext) string { + if req.GetInstance() { + return authz.GetInstance(ctx).InstanceID() + } + if req.GetOrgId() != "" { + return req.GetOrgId() + } + return authz.GetCtxData(ctx).OrgID +} + +func TextMethodToQuery(method object.TextQueryMethod) query.TextComparison { + switch method { + case object.TextQueryMethod_TEXT_QUERY_METHOD_EQUALS: + return query.TextEquals + case object.TextQueryMethod_TEXT_QUERY_METHOD_EQUALS_IGNORE_CASE: + return query.TextEqualsIgnoreCase + case object.TextQueryMethod_TEXT_QUERY_METHOD_STARTS_WITH: + return query.TextStartsWith + case object.TextQueryMethod_TEXT_QUERY_METHOD_STARTS_WITH_IGNORE_CASE: + return query.TextStartsWithIgnoreCase + case object.TextQueryMethod_TEXT_QUERY_METHOD_CONTAINS: + return query.TextContains + case object.TextQueryMethod_TEXT_QUERY_METHOD_CONTAINS_IGNORE_CASE: + return query.TextContainsIgnoreCase + case object.TextQueryMethod_TEXT_QUERY_METHOD_ENDS_WITH: + return query.TextEndsWith + case object.TextQueryMethod_TEXT_QUERY_METHOD_ENDS_WITH_IGNORE_CASE: + return query.TextEndsWithIgnoreCase + default: + return -1 + } +} diff --git a/internal/api/grpc/oidc/v2/oidc.go b/internal/api/grpc/oidc/v2/oidc.go index 7a13c7ea99..d84edd1c2f 100644 --- a/internal/api/grpc/oidc/v2/oidc.go +++ b/internal/api/grpc/oidc/v2/oidc.go @@ -15,7 +15,7 @@ import ( "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/query" "github.com/zitadel/zitadel/internal/zerrors" - oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2beta" + oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2" ) func (s *Server) GetAuthRequest(ctx context.Context, req *oidc_pb.GetAuthRequestRequest) (*oidc_pb.GetAuthRequestResponse, error) { diff --git a/internal/api/grpc/oidc/v2/oidc_integration_test.go b/internal/api/grpc/oidc/v2/oidc_integration_test.go index 27884e80a5..901e667b34 100644 --- a/internal/api/grpc/oidc/v2/oidc_integration_test.go +++ b/internal/api/grpc/oidc/v2/oidc_integration_test.go @@ -16,10 +16,10 @@ import ( "google.golang.org/protobuf/types/known/timestamppb" "github.com/zitadel/zitadel/internal/integration" - object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta" - oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2beta" - session "github.com/zitadel/zitadel/pkg/grpc/session/v2beta" - user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta" + "github.com/zitadel/zitadel/pkg/grpc/object/v2" + oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2" + "github.com/zitadel/zitadel/pkg/grpc/session/v2" + "github.com/zitadel/zitadel/pkg/grpc/user/v2" ) var ( diff --git a/internal/api/grpc/oidc/v2/oidc_test.go b/internal/api/grpc/oidc/v2/oidc_test.go index 27dcdf7fb7..c7d06c4a61 100644 --- a/internal/api/grpc/oidc/v2/oidc_test.go +++ b/internal/api/grpc/oidc/v2/oidc_test.go @@ -12,7 +12,7 @@ import ( "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/query" - oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2beta" + oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2" ) func Test_authRequestToPb(t *testing.T) { diff --git a/internal/api/grpc/oidc/v2/server.go b/internal/api/grpc/oidc/v2/server.go index 7595ae927e..28c7134904 100644 --- a/internal/api/grpc/oidc/v2/server.go +++ b/internal/api/grpc/oidc/v2/server.go @@ -8,7 +8,7 @@ import ( "github.com/zitadel/zitadel/internal/api/oidc" "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/query" - oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2beta" + oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2" ) var _ oidc_pb.OIDCServiceServer = (*Server)(nil) diff --git a/internal/api/grpc/oidc/v2beta/oidc.go b/internal/api/grpc/oidc/v2beta/oidc.go new file mode 100644 index 0000000000..d504e411f0 --- /dev/null +++ b/internal/api/grpc/oidc/v2beta/oidc.go @@ -0,0 +1,204 @@ +package oidc + +import ( + "context" + + "github.com/zitadel/logging" + "github.com/zitadel/oidc/v3/pkg/op" + "google.golang.org/protobuf/types/known/durationpb" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/api/authz" + object "github.com/zitadel/zitadel/internal/api/grpc/object/v2beta" + "github.com/zitadel/zitadel/internal/api/http" + "github.com/zitadel/zitadel/internal/api/oidc" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/query" + "github.com/zitadel/zitadel/internal/zerrors" + oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2beta" +) + +func (s *Server) GetAuthRequest(ctx context.Context, req *oidc_pb.GetAuthRequestRequest) (*oidc_pb.GetAuthRequestResponse, error) { + authRequest, err := s.query.AuthRequestByID(ctx, true, req.GetAuthRequestId(), true) + if err != nil { + logging.WithError(err).Error("query authRequest by ID") + return nil, err + } + return &oidc_pb.GetAuthRequestResponse{ + AuthRequest: authRequestToPb(authRequest), + }, nil +} + +func authRequestToPb(a *query.AuthRequest) *oidc_pb.AuthRequest { + pba := &oidc_pb.AuthRequest{ + Id: a.ID, + CreationDate: timestamppb.New(a.CreationDate), + ClientId: a.ClientID, + Scope: a.Scope, + RedirectUri: a.RedirectURI, + Prompt: promptsToPb(a.Prompt), + UiLocales: a.UiLocales, + LoginHint: a.LoginHint, + HintUserId: a.HintUserID, + } + if a.MaxAge != nil { + pba.MaxAge = durationpb.New(*a.MaxAge) + } + return pba +} + +func promptsToPb(promps []domain.Prompt) []oidc_pb.Prompt { + out := make([]oidc_pb.Prompt, len(promps)) + for i, p := range promps { + out[i] = promptToPb(p) + } + return out +} + +func promptToPb(p domain.Prompt) oidc_pb.Prompt { + switch p { + case domain.PromptUnspecified: + return oidc_pb.Prompt_PROMPT_UNSPECIFIED + case domain.PromptNone: + return oidc_pb.Prompt_PROMPT_NONE + case domain.PromptLogin: + return oidc_pb.Prompt_PROMPT_LOGIN + case domain.PromptConsent: + return oidc_pb.Prompt_PROMPT_CONSENT + case domain.PromptSelectAccount: + return oidc_pb.Prompt_PROMPT_SELECT_ACCOUNT + case domain.PromptCreate: + return oidc_pb.Prompt_PROMPT_CREATE + default: + return oidc_pb.Prompt_PROMPT_UNSPECIFIED + } +} + +func (s *Server) CreateCallback(ctx context.Context, req *oidc_pb.CreateCallbackRequest) (*oidc_pb.CreateCallbackResponse, error) { + switch v := req.GetCallbackKind().(type) { + case *oidc_pb.CreateCallbackRequest_Error: + return s.failAuthRequest(ctx, req.GetAuthRequestId(), v.Error) + case *oidc_pb.CreateCallbackRequest_Session: + return s.linkSessionToAuthRequest(ctx, req.GetAuthRequestId(), v.Session) + default: + return nil, zerrors.ThrowUnimplementedf(nil, "OIDCv2-zee7A", "verification oneOf %T in method CreateCallback not implemented", v) + } +} + +func (s *Server) failAuthRequest(ctx context.Context, authRequestID string, ae *oidc_pb.AuthorizationError) (*oidc_pb.CreateCallbackResponse, error) { + details, aar, err := s.command.FailAuthRequest(ctx, authRequestID, errorReasonToDomain(ae.GetError())) + if err != nil { + return nil, err + } + authReq := &oidc.AuthRequestV2{CurrentAuthRequest: aar} + callback, err := oidc.CreateErrorCallbackURL(authReq, errorReasonToOIDC(ae.GetError()), ae.GetErrorDescription(), ae.GetErrorUri(), s.op.Provider()) + if err != nil { + return nil, err + } + return &oidc_pb.CreateCallbackResponse{ + Details: object.DomainToDetailsPb(details), + CallbackUrl: callback, + }, nil +} + +func (s *Server) linkSessionToAuthRequest(ctx context.Context, authRequestID string, session *oidc_pb.Session) (*oidc_pb.CreateCallbackResponse, error) { + details, aar, err := s.command.LinkSessionToAuthRequest(ctx, authRequestID, session.GetSessionId(), session.GetSessionToken(), true) + if err != nil { + return nil, err + } + authReq := &oidc.AuthRequestV2{CurrentAuthRequest: aar} + ctx = op.ContextWithIssuer(ctx, http.BuildOrigin(authz.GetInstance(ctx).RequestedHost(), s.externalSecure)) + var callback string + if aar.ResponseType == domain.OIDCResponseTypeCode { + callback, err = oidc.CreateCodeCallbackURL(ctx, authReq, s.op.Provider()) + } else { + callback, err = s.op.CreateTokenCallbackURL(ctx, authReq) + } + if err != nil { + return nil, err + } + return &oidc_pb.CreateCallbackResponse{ + Details: object.DomainToDetailsPb(details), + CallbackUrl: callback, + }, nil +} + +func errorReasonToDomain(errorReason oidc_pb.ErrorReason) domain.OIDCErrorReason { + switch errorReason { + case oidc_pb.ErrorReason_ERROR_REASON_UNSPECIFIED: + return domain.OIDCErrorReasonUnspecified + case oidc_pb.ErrorReason_ERROR_REASON_INVALID_REQUEST: + return domain.OIDCErrorReasonInvalidRequest + case oidc_pb.ErrorReason_ERROR_REASON_UNAUTHORIZED_CLIENT: + return domain.OIDCErrorReasonUnauthorizedClient + case oidc_pb.ErrorReason_ERROR_REASON_ACCESS_DENIED: + return domain.OIDCErrorReasonAccessDenied + case oidc_pb.ErrorReason_ERROR_REASON_UNSUPPORTED_RESPONSE_TYPE: + return domain.OIDCErrorReasonUnsupportedResponseType + case oidc_pb.ErrorReason_ERROR_REASON_INVALID_SCOPE: + return domain.OIDCErrorReasonInvalidScope + case oidc_pb.ErrorReason_ERROR_REASON_SERVER_ERROR: + return domain.OIDCErrorReasonServerError + case oidc_pb.ErrorReason_ERROR_REASON_TEMPORARY_UNAVAILABLE: + return domain.OIDCErrorReasonTemporaryUnavailable + case oidc_pb.ErrorReason_ERROR_REASON_INTERACTION_REQUIRED: + return domain.OIDCErrorReasonInteractionRequired + case oidc_pb.ErrorReason_ERROR_REASON_LOGIN_REQUIRED: + return domain.OIDCErrorReasonLoginRequired + case oidc_pb.ErrorReason_ERROR_REASON_ACCOUNT_SELECTION_REQUIRED: + return domain.OIDCErrorReasonAccountSelectionRequired + case oidc_pb.ErrorReason_ERROR_REASON_CONSENT_REQUIRED: + return domain.OIDCErrorReasonConsentRequired + case oidc_pb.ErrorReason_ERROR_REASON_INVALID_REQUEST_URI: + return domain.OIDCErrorReasonInvalidRequestURI + case oidc_pb.ErrorReason_ERROR_REASON_INVALID_REQUEST_OBJECT: + return domain.OIDCErrorReasonInvalidRequestObject + case oidc_pb.ErrorReason_ERROR_REASON_REQUEST_NOT_SUPPORTED: + return domain.OIDCErrorReasonRequestNotSupported + case oidc_pb.ErrorReason_ERROR_REASON_REQUEST_URI_NOT_SUPPORTED: + return domain.OIDCErrorReasonRequestURINotSupported + case oidc_pb.ErrorReason_ERROR_REASON_REGISTRATION_NOT_SUPPORTED: + return domain.OIDCErrorReasonRegistrationNotSupported + default: + return domain.OIDCErrorReasonUnspecified + } +} + +func errorReasonToOIDC(reason oidc_pb.ErrorReason) string { + switch reason { + case oidc_pb.ErrorReason_ERROR_REASON_INVALID_REQUEST: + return "invalid_request" + case oidc_pb.ErrorReason_ERROR_REASON_UNAUTHORIZED_CLIENT: + return "unauthorized_client" + case oidc_pb.ErrorReason_ERROR_REASON_ACCESS_DENIED: + return "access_denied" + case oidc_pb.ErrorReason_ERROR_REASON_UNSUPPORTED_RESPONSE_TYPE: + return "unsupported_response_type" + case oidc_pb.ErrorReason_ERROR_REASON_INVALID_SCOPE: + return "invalid_scope" + case oidc_pb.ErrorReason_ERROR_REASON_TEMPORARY_UNAVAILABLE: + return "temporarily_unavailable" + case oidc_pb.ErrorReason_ERROR_REASON_INTERACTION_REQUIRED: + return "interaction_required" + case oidc_pb.ErrorReason_ERROR_REASON_LOGIN_REQUIRED: + return "login_required" + case oidc_pb.ErrorReason_ERROR_REASON_ACCOUNT_SELECTION_REQUIRED: + return "account_selection_required" + case oidc_pb.ErrorReason_ERROR_REASON_CONSENT_REQUIRED: + return "consent_required" + case oidc_pb.ErrorReason_ERROR_REASON_INVALID_REQUEST_URI: + return "invalid_request_uri" + case oidc_pb.ErrorReason_ERROR_REASON_INVALID_REQUEST_OBJECT: + return "invalid_request_object" + case oidc_pb.ErrorReason_ERROR_REASON_REQUEST_NOT_SUPPORTED: + return "request_not_supported" + case oidc_pb.ErrorReason_ERROR_REASON_REQUEST_URI_NOT_SUPPORTED: + return "request_uri_not_supported" + case oidc_pb.ErrorReason_ERROR_REASON_REGISTRATION_NOT_SUPPORTED: + return "registration_not_supported" + case oidc_pb.ErrorReason_ERROR_REASON_UNSPECIFIED, oidc_pb.ErrorReason_ERROR_REASON_SERVER_ERROR: + fallthrough + default: + return "server_error" + } +} diff --git a/internal/api/grpc/oidc/v2beta/oidc_integration_test.go b/internal/api/grpc/oidc/v2beta/oidc_integration_test.go new file mode 100644 index 0000000000..574718212a --- /dev/null +++ b/internal/api/grpc/oidc/v2beta/oidc_integration_test.go @@ -0,0 +1,258 @@ +//go:build integration + +package oidc_test + +import ( + "context" + "net/url" + "os" + "regexp" + "testing" + "time" + + "github.com/muhlemmer/gu" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/integration" + object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta" + oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2beta" + session "github.com/zitadel/zitadel/pkg/grpc/session/v2beta" + "github.com/zitadel/zitadel/pkg/grpc/user/v2" +) + +var ( + CTX context.Context + Tester *integration.Tester + Client oidc_pb.OIDCServiceClient + User *user.AddHumanUserResponse +) + +const ( + redirectURI = "oidcintegrationtest://callback" + redirectURIImplicit = "http://localhost:9999/callback" + logoutRedirectURI = "oidcintegrationtest://logged-out" +) + +func TestMain(m *testing.M) { + os.Exit(func() int { + ctx, _, cancel := integration.Contexts(5 * time.Minute) + defer cancel() + + Tester = integration.NewTester(ctx) + defer Tester.Done() + Client = Tester.Client.OIDCv2beta + + CTX = Tester.WithAuthorization(ctx, integration.OrgOwner) + User = Tester.CreateHumanUser(CTX) + return m.Run() + }()) +} + +func TestServer_GetAuthRequest(t *testing.T) { + project, err := Tester.CreateProject(CTX) + require.NoError(t, err) + client, err := Tester.CreateOIDCNativeClient(CTX, redirectURI, logoutRedirectURI, project.GetId(), false) + require.NoError(t, err) + authRequestID, err := Tester.CreateOIDCAuthRequest(CTX, client.GetClientId(), Tester.Users[integration.FirstInstanceUsersKey][integration.OrgOwner].ID, redirectURI) + require.NoError(t, err) + now := time.Now() + + tests := []struct { + name string + AuthRequestID string + want *oidc_pb.GetAuthRequestResponse + wantErr bool + }{ + { + name: "Not found", + AuthRequestID: "123", + wantErr: true, + }, + { + name: "success", + AuthRequestID: authRequestID, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Client.GetAuthRequest(CTX, &oidc_pb.GetAuthRequestRequest{ + AuthRequestId: tt.AuthRequestID, + }) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + authRequest := got.GetAuthRequest() + assert.NotNil(t, authRequest) + assert.Equal(t, authRequestID, authRequest.GetId()) + assert.WithinRange(t, authRequest.GetCreationDate().AsTime(), now.Add(-time.Second), now.Add(time.Second)) + assert.Contains(t, authRequest.GetScope(), "openid") + }) + } +} + +func TestServer_CreateCallback(t *testing.T) { + project, err := Tester.CreateProject(CTX) + require.NoError(t, err) + client, err := Tester.CreateOIDCNativeClient(CTX, redirectURI, logoutRedirectURI, project.GetId(), false) + require.NoError(t, err) + sessionResp, err := Tester.Client.SessionV2beta.CreateSession(CTX, &session.CreateSessionRequest{ + Checks: &session.Checks{ + User: &session.CheckUser{ + Search: &session.CheckUser_UserId{ + UserId: Tester.Users[integration.FirstInstanceUsersKey][integration.OrgOwner].ID, + }, + }, + }, + }) + require.NoError(t, err) + + tests := []struct { + name string + req *oidc_pb.CreateCallbackRequest + AuthError string + want *oidc_pb.CreateCallbackResponse + wantURL *url.URL + wantErr bool + }{ + { + name: "Not found", + req: &oidc_pb.CreateCallbackRequest{ + AuthRequestId: "123", + CallbackKind: &oidc_pb.CreateCallbackRequest_Session{ + Session: &oidc_pb.Session{ + SessionId: sessionResp.GetSessionId(), + SessionToken: sessionResp.GetSessionToken(), + }, + }, + }, + wantErr: true, + }, + { + name: "session not found", + req: &oidc_pb.CreateCallbackRequest{ + AuthRequestId: func() string { + authRequestID, err := Tester.CreateOIDCAuthRequest(CTX, client.GetClientId(), Tester.Users[integration.FirstInstanceUsersKey][integration.OrgOwner].ID, redirectURI) + require.NoError(t, err) + return authRequestID + }(), + CallbackKind: &oidc_pb.CreateCallbackRequest_Session{ + Session: &oidc_pb.Session{ + SessionId: "foo", + SessionToken: "bar", + }, + }, + }, + wantErr: true, + }, + { + name: "session token invalid", + req: &oidc_pb.CreateCallbackRequest{ + AuthRequestId: func() string { + authRequestID, err := Tester.CreateOIDCAuthRequest(CTX, client.GetClientId(), Tester.Users[integration.FirstInstanceUsersKey][integration.OrgOwner].ID, redirectURI) + require.NoError(t, err) + return authRequestID + }(), + CallbackKind: &oidc_pb.CreateCallbackRequest_Session{ + Session: &oidc_pb.Session{ + SessionId: sessionResp.GetSessionId(), + SessionToken: "bar", + }, + }, + }, + wantErr: true, + }, + { + name: "fail callback", + req: &oidc_pb.CreateCallbackRequest{ + AuthRequestId: func() string { + authRequestID, err := Tester.CreateOIDCAuthRequest(CTX, client.GetClientId(), Tester.Users[integration.FirstInstanceUsersKey][integration.OrgOwner].ID, redirectURI) + require.NoError(t, err) + return authRequestID + }(), + CallbackKind: &oidc_pb.CreateCallbackRequest_Error{ + Error: &oidc_pb.AuthorizationError{ + Error: oidc_pb.ErrorReason_ERROR_REASON_ACCESS_DENIED, + ErrorDescription: gu.Ptr("nope"), + ErrorUri: gu.Ptr("https://example.com/docs"), + }, + }, + }, + want: &oidc_pb.CreateCallbackResponse{ + CallbackUrl: regexp.QuoteMeta(`oidcintegrationtest://callback?error=access_denied&error_description=nope&error_uri=https%3A%2F%2Fexample.com%2Fdocs&state=state`), + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Instance.InstanceID(), + }, + }, + wantErr: false, + }, + { + name: "code callback", + req: &oidc_pb.CreateCallbackRequest{ + AuthRequestId: func() string { + authRequestID, err := Tester.CreateOIDCAuthRequest(CTX, client.GetClientId(), Tester.Users[integration.FirstInstanceUsersKey][integration.OrgOwner].ID, redirectURI) + require.NoError(t, err) + return authRequestID + }(), + CallbackKind: &oidc_pb.CreateCallbackRequest_Session{ + Session: &oidc_pb.Session{ + SessionId: sessionResp.GetSessionId(), + SessionToken: sessionResp.GetSessionToken(), + }, + }, + }, + want: &oidc_pb.CreateCallbackResponse{ + CallbackUrl: `oidcintegrationtest:\/\/callback\?code=(.*)&state=state`, + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Instance.InstanceID(), + }, + }, + wantErr: false, + }, + { + name: "implicit", + req: &oidc_pb.CreateCallbackRequest{ + AuthRequestId: func() string { + client, err := Tester.CreateOIDCImplicitFlowClient(CTX, redirectURIImplicit) + require.NoError(t, err) + authRequestID, err := Tester.CreateOIDCAuthRequestImplicit(CTX, client.GetClientId(), Tester.Users[integration.FirstInstanceUsersKey][integration.OrgOwner].ID, redirectURIImplicit) + require.NoError(t, err) + return authRequestID + }(), + CallbackKind: &oidc_pb.CreateCallbackRequest_Session{ + Session: &oidc_pb.Session{ + SessionId: sessionResp.GetSessionId(), + SessionToken: sessionResp.GetSessionToken(), + }, + }, + }, + want: &oidc_pb.CreateCallbackResponse{ + CallbackUrl: `http:\/\/localhost:9999\/callback#access_token=(.*)&expires_in=(.*)&id_token=(.*)&state=state&token_type=Bearer`, + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Instance.InstanceID(), + }, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Client.CreateCallback(CTX, tt.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + integration.AssertDetails(t, tt.want, got) + if tt.want != nil { + assert.Regexp(t, regexp.MustCompile(tt.want.CallbackUrl), got.GetCallbackUrl()) + } + }) + } +} diff --git a/internal/api/grpc/oidc/v2beta/oidc_test.go b/internal/api/grpc/oidc/v2beta/oidc_test.go new file mode 100644 index 0000000000..27dcdf7fb7 --- /dev/null +++ b/internal/api/grpc/oidc/v2beta/oidc_test.go @@ -0,0 +1,150 @@ +package oidc + +import ( + "testing" + "time" + + "github.com/muhlemmer/gu" + "github.com/stretchr/testify/assert" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/durationpb" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/query" + oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2beta" +) + +func Test_authRequestToPb(t *testing.T) { + now := time.Now() + arg := &query.AuthRequest{ + ID: "authID", + CreationDate: now, + ClientID: "clientID", + Scope: []string{"a", "b", "c"}, + RedirectURI: "callbackURI", + Prompt: []domain.Prompt{ + domain.PromptUnspecified, + domain.PromptNone, + domain.PromptLogin, + domain.PromptConsent, + domain.PromptSelectAccount, + domain.PromptCreate, + 999, + }, + UiLocales: []string{"en", "fi"}, + LoginHint: gu.Ptr("foo@bar.com"), + MaxAge: gu.Ptr(time.Minute), + HintUserID: gu.Ptr("userID"), + } + want := &oidc_pb.AuthRequest{ + Id: "authID", + CreationDate: timestamppb.New(now), + ClientId: "clientID", + RedirectUri: "callbackURI", + Prompt: []oidc_pb.Prompt{ + oidc_pb.Prompt_PROMPT_UNSPECIFIED, + oidc_pb.Prompt_PROMPT_NONE, + oidc_pb.Prompt_PROMPT_LOGIN, + oidc_pb.Prompt_PROMPT_CONSENT, + oidc_pb.Prompt_PROMPT_SELECT_ACCOUNT, + oidc_pb.Prompt_PROMPT_CREATE, + oidc_pb.Prompt_PROMPT_UNSPECIFIED, + }, + UiLocales: []string{"en", "fi"}, + Scope: []string{"a", "b", "c"}, + LoginHint: gu.Ptr("foo@bar.com"), + MaxAge: durationpb.New(time.Minute), + HintUserId: gu.Ptr("userID"), + } + got := authRequestToPb(arg) + if !proto.Equal(want, got) { + t.Errorf("authRequestToPb() =\n%v\nwant\n%v\n", got, want) + } +} + +func Test_errorReasonToOIDC(t *testing.T) { + tests := []struct { + reason oidc_pb.ErrorReason + want string + }{ + { + reason: oidc_pb.ErrorReason_ERROR_REASON_UNSPECIFIED, + want: "server_error", + }, + { + reason: oidc_pb.ErrorReason_ERROR_REASON_INVALID_REQUEST, + want: "invalid_request", + }, + { + reason: oidc_pb.ErrorReason_ERROR_REASON_UNAUTHORIZED_CLIENT, + want: "unauthorized_client", + }, + { + reason: oidc_pb.ErrorReason_ERROR_REASON_ACCESS_DENIED, + want: "access_denied", + }, + { + reason: oidc_pb.ErrorReason_ERROR_REASON_UNSUPPORTED_RESPONSE_TYPE, + want: "unsupported_response_type", + }, + { + reason: oidc_pb.ErrorReason_ERROR_REASON_INVALID_SCOPE, + want: "invalid_scope", + }, + { + reason: oidc_pb.ErrorReason_ERROR_REASON_SERVER_ERROR, + want: "server_error", + }, + { + reason: oidc_pb.ErrorReason_ERROR_REASON_TEMPORARY_UNAVAILABLE, + want: "temporarily_unavailable", + }, + { + reason: oidc_pb.ErrorReason_ERROR_REASON_INTERACTION_REQUIRED, + want: "interaction_required", + }, + { + reason: oidc_pb.ErrorReason_ERROR_REASON_LOGIN_REQUIRED, + want: "login_required", + }, + { + reason: oidc_pb.ErrorReason_ERROR_REASON_ACCOUNT_SELECTION_REQUIRED, + want: "account_selection_required", + }, + { + reason: oidc_pb.ErrorReason_ERROR_REASON_CONSENT_REQUIRED, + want: "consent_required", + }, + { + reason: oidc_pb.ErrorReason_ERROR_REASON_INVALID_REQUEST_URI, + want: "invalid_request_uri", + }, + { + reason: oidc_pb.ErrorReason_ERROR_REASON_INVALID_REQUEST_OBJECT, + want: "invalid_request_object", + }, + { + reason: oidc_pb.ErrorReason_ERROR_REASON_REQUEST_NOT_SUPPORTED, + want: "request_not_supported", + }, + { + reason: oidc_pb.ErrorReason_ERROR_REASON_REQUEST_URI_NOT_SUPPORTED, + want: "request_uri_not_supported", + }, + { + reason: oidc_pb.ErrorReason_ERROR_REASON_REGISTRATION_NOT_SUPPORTED, + want: "registration_not_supported", + }, + { + reason: 99999, + want: "server_error", + }, + } + for _, tt := range tests { + t.Run(tt.reason.String(), func(t *testing.T) { + got := errorReasonToOIDC(tt.reason) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/internal/api/grpc/oidc/v2beta/server.go b/internal/api/grpc/oidc/v2beta/server.go new file mode 100644 index 0000000000..7595ae927e --- /dev/null +++ b/internal/api/grpc/oidc/v2beta/server.go @@ -0,0 +1,59 @@ +package oidc + +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/api/oidc" + "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/query" + oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2beta" +) + +var _ oidc_pb.OIDCServiceServer = (*Server)(nil) + +type Server struct { + oidc_pb.UnimplementedOIDCServiceServer + command *command.Commands + query *query.Queries + + op *oidc.Server + externalSecure bool +} + +type Config struct{} + +func CreateServer( + command *command.Commands, + query *query.Queries, + op *oidc.Server, + externalSecure bool, +) *Server { + return &Server{ + command: command, + query: query, + op: op, + externalSecure: externalSecure, + } +} + +func (s *Server) RegisterServer(grpcServer *grpc.Server) { + oidc_pb.RegisterOIDCServiceServer(grpcServer, s) +} + +func (s *Server) AppName() string { + return oidc_pb.OIDCService_ServiceDesc.ServiceName +} + +func (s *Server) MethodPrefix() string { + return oidc_pb.OIDCService_ServiceDesc.ServiceName +} + +func (s *Server) AuthMethods() authz.MethodMapping { + return oidc_pb.OIDCService_AuthMethods +} + +func (s *Server) RegisterGateway() server.RegisterGatewayFunc { + return oidc_pb.RegisterOIDCServiceHandler +} diff --git a/internal/api/grpc/org/v2/org.go b/internal/api/grpc/org/v2/org.go index 1fc0ca8aad..be830bc7b5 100644 --- a/internal/api/grpc/org/v2/org.go +++ b/internal/api/grpc/org/v2/org.go @@ -7,7 +7,7 @@ import ( "github.com/zitadel/zitadel/internal/api/grpc/user/v2" "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/zerrors" - org "github.com/zitadel/zitadel/pkg/grpc/org/v2beta" + "github.com/zitadel/zitadel/pkg/grpc/org/v2" ) func (s *Server) AddOrganization(ctx context.Context, request *org.AddOrganizationRequest) (*org.AddOrganizationResponse, error) { diff --git a/internal/api/grpc/org/v2/org_integration_test.go b/internal/api/grpc/org/v2/org_integration_test.go index 7276e8d5eb..9f3f9fa64b 100644 --- a/internal/api/grpc/org/v2/org_integration_test.go +++ b/internal/api/grpc/org/v2/org_integration_test.go @@ -14,8 +14,8 @@ import ( "github.com/stretchr/testify/require" "github.com/zitadel/zitadel/internal/integration" - org "github.com/zitadel/zitadel/pkg/grpc/org/v2beta" - user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta" + "github.com/zitadel/zitadel/pkg/grpc/org/v2" + "github.com/zitadel/zitadel/pkg/grpc/user/v2" ) var ( diff --git a/internal/api/grpc/org/v2/org_test.go b/internal/api/grpc/org/v2/org_test.go index 5024b59c1d..451c4006b3 100644 --- a/internal/api/grpc/org/v2/org_test.go +++ b/internal/api/grpc/org/v2/org_test.go @@ -12,9 +12,9 @@ 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" + "github.com/zitadel/zitadel/pkg/grpc/object/v2" + "github.com/zitadel/zitadel/pkg/grpc/org/v2" + "github.com/zitadel/zitadel/pkg/grpc/user/v2" ) func Test_addOrganizationRequestToCommand(t *testing.T) { diff --git a/internal/api/grpc/org/v2/server.go b/internal/api/grpc/org/v2/server.go index 89dba81702..36588f3eb7 100644 --- a/internal/api/grpc/org/v2/server.go +++ b/internal/api/grpc/org/v2/server.go @@ -8,7 +8,7 @@ import ( "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/query" - org "github.com/zitadel/zitadel/pkg/grpc/org/v2beta" + "github.com/zitadel/zitadel/pkg/grpc/org/v2" ) var _ org.OrganizationServiceServer = (*Server)(nil) diff --git a/internal/api/grpc/org/v2beta/org.go b/internal/api/grpc/org/v2beta/org.go new file mode 100644 index 0000000000..ab2da2b766 --- /dev/null +++ b/internal/api/grpc/org/v2beta/org.go @@ -0,0 +1,83 @@ +package org + +import ( + "context" + + 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/zerrors" + 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) + if err != nil { + return nil, err + } + createdOrg, err := s.command.SetUpOrg(ctx, orgSetup, false) + if err != nil { + return nil, err + } + return createdOrganizationToPb(createdOrg) +} + +func addOrganizationRequestToCommand(request *org.AddOrganizationRequest) (*command.OrgSetup, error) { + admins, err := addOrganizationRequestAdminsToCommand(request.GetAdmins()) + if err != nil { + return nil, err + } + return &command.OrgSetup{ + Name: request.GetName(), + CustomDomain: "", + Admins: admins, + }, nil +} + +func addOrganizationRequestAdminsToCommand(requestAdmins []*org.AddOrganizationRequest_Admin) (admins []*command.OrgSetupAdmin, err error) { + admins = make([]*command.OrgSetupAdmin, len(requestAdmins)) + for i, admin := range requestAdmins { + admins[i], err = addOrganizationRequestAdminToCommand(admin) + if err != nil { + return nil, err + } + } + return admins, nil +} + +func addOrganizationRequestAdminToCommand(admin *org.AddOrganizationRequest_Admin) (*command.OrgSetupAdmin, error) { + switch a := admin.GetUserType().(type) { + case *org.AddOrganizationRequest_Admin_UserId: + return &command.OrgSetupAdmin{ + ID: a.UserId, + Roles: admin.GetRoles(), + }, nil + case *org.AddOrganizationRequest_Admin_Human: + human, err := user.AddUserRequestToAddHuman(a.Human) + if err != nil { + return nil, err + } + return &command.OrgSetupAdmin{ + Human: human, + Roles: admin.GetRoles(), + }, nil + default: + return nil, zerrors.ThrowUnimplementedf(nil, "ORGv2-SD2r1", "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, + } + } + return &org.AddOrganizationResponse{ + Details: object.DomainToDetailsPb(createdOrg.ObjectDetails), + OrganizationId: createdOrg.ObjectDetails.ResourceOwner, + CreatedAdmins: admins, + }, nil +} diff --git a/internal/api/grpc/org/v2beta/org_integration_test.go b/internal/api/grpc/org/v2beta/org_integration_test.go new file mode 100644 index 0000000000..97f7e0a719 --- /dev/null +++ b/internal/api/grpc/org/v2beta/org_integration_test.go @@ -0,0 +1,207 @@ +//go:build integration + +package org_test + +import ( + "context" + "fmt" + "os" + "testing" + "time" + + "github.com/muhlemmer/gu" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/zitadel/zitadel/internal/integration" + 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" +) + +var ( + CTX context.Context + Tester *integration.Tester + Client org.OrganizationServiceClient + User *user.AddHumanUserResponse +) + +func TestMain(m *testing.M) { + os.Exit(func() int { + ctx, errCtx, cancel := integration.Contexts(5 * time.Minute) + defer cancel() + + Tester = integration.NewTester(ctx) + defer Tester.Done() + Client = Tester.Client.OrgV2beta + + CTX, _ = Tester.WithAuthorization(ctx, integration.IAMOwner), errCtx + User = Tester.CreateHumanUser(CTX) + return m.Run() + }()) +} + +func TestServer_AddOrganization(t *testing.T) { + idpID := Tester.AddGenericOAuthProvider(t, CTX) + + tests := []struct { + name string + ctx context.Context + req *org.AddOrganizationRequest + want *org.AddOrganizationResponse + wantErr bool + }{ + { + name: "missing permission", + ctx: Tester.WithAuthorization(context.Background(), integration.OrgOwner), + req: &org.AddOrganizationRequest{ + Name: "name", + Admins: nil, + }, + wantErr: true, + }, + { + name: "empty name", + ctx: CTX, + req: &org.AddOrganizationRequest{ + Name: "", + Admins: nil, + }, + wantErr: true, + }, + { + name: "invalid admin type", + ctx: CTX, + req: &org.AddOrganizationRequest{ + Name: fmt.Sprintf("%d", time.Now().UnixNano()), + Admins: []*org.AddOrganizationRequest_Admin{ + {}, + }, + }, + wantErr: true, + }, + { + name: "admin with init", + ctx: CTX, + req: &org.AddOrganizationRequest{ + Name: fmt.Sprintf("%d", time.Now().UnixNano()), + Admins: []*org.AddOrganizationRequest_Admin{ + { + UserType: &org.AddOrganizationRequest_Admin_Human{ + Human: &user_v2beta.AddHumanUserRequest{ + Profile: &user_v2beta.SetHumanProfile{ + GivenName: "firstname", + FamilyName: "lastname", + }, + Email: &user_v2beta.SetHumanEmail{ + Email: fmt.Sprintf("%d@mouse.com", time.Now().UnixNano()), + Verification: &user_v2beta.SetHumanEmail_ReturnCode{ + ReturnCode: &user_v2beta.ReturnEmailVerificationCode{}, + }, + }, + }, + }, + }, + }, + }, + want: &org.AddOrganizationResponse{ + OrganizationId: integration.NotEmpty, + CreatedAdmins: []*org.AddOrganizationResponse_CreatedAdmin{ + { + UserId: integration.NotEmpty, + EmailCode: gu.Ptr(integration.NotEmpty), + PhoneCode: nil, + }, + }, + }, + }, + { + name: "existing user and new human with idp", + ctx: CTX, + req: &org.AddOrganizationRequest{ + Name: fmt.Sprintf("%d", time.Now().UnixNano()), + Admins: []*org.AddOrganizationRequest_Admin{ + { + UserType: &org.AddOrganizationRequest_Admin_UserId{UserId: User.GetUserId()}, + }, + { + UserType: &org.AddOrganizationRequest_Admin_Human{ + Human: &user_v2beta.AddHumanUserRequest{ + Profile: &user_v2beta.SetHumanProfile{ + GivenName: "firstname", + FamilyName: "lastname", + }, + Email: &user_v2beta.SetHumanEmail{ + Email: fmt.Sprintf("%d@mouse.com", time.Now().UnixNano()), + Verification: &user_v2beta.SetHumanEmail_IsVerified{ + IsVerified: true, + }, + }, + IdpLinks: []*user_v2beta.IDPLink{ + { + IdpId: idpID, + UserId: "userID", + UserName: "username", + }, + }, + }, + }, + }, + }, + }, + want: &org.AddOrganizationResponse{ + CreatedAdmins: []*org.AddOrganizationResponse_CreatedAdmin{ + // a single admin is expected, because the first provided already exists + { + UserId: integration.NotEmpty, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Client.AddOrganization(tt.ctx, tt.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + + // check details + assert.NotZero(t, got.GetDetails().GetSequence()) + gotCD := got.GetDetails().GetChangeDate().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())) + for i, admin := range tt.want.GetCreatedAdmins() { + gotAdmin := got.GetCreatedAdmins()[i] + assertCreatedAdmin(t, admin, gotAdmin) + } + }) + } +} + +func assertCreatedAdmin(t *testing.T, expected, got *org.AddOrganizationResponse_CreatedAdmin) { + if expected.GetUserId() != "" { + assert.NotEmpty(t, got.GetUserId()) + } else { + assert.Empty(t, got.GetUserId()) + } + if expected.GetEmailCode() != "" { + assert.NotEmpty(t, got.GetEmailCode()) + } else { + assert.Empty(t, got.GetEmailCode()) + } + if expected.GetPhoneCode() != "" { + assert.NotEmpty(t, got.GetPhoneCode()) + } else { + assert.Empty(t, got.GetPhoneCode()) + } +} diff --git a/internal/api/grpc/org/v2beta/org_test.go b/internal/api/grpc/org/v2beta/org_test.go new file mode 100644 index 0000000000..5024b59c1d --- /dev/null +++ b/internal/api/grpc/org/v2beta/org_test.go @@ -0,0 +1,172 @@ +package org + +import ( + "testing" + "time" + + "github.com/muhlemmer/gu" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "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" + 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) { + type args struct { + request *org.AddOrganizationRequest + } + tests := []struct { + name string + args args + want *command.OrgSetup + wantErr error + }{ + { + name: "nil user", + args: args{ + request: &org.AddOrganizationRequest{ + Name: "name", + Admins: []*org.AddOrganizationRequest_Admin{ + {}, + }, + }, + }, + wantErr: zerrors.ThrowUnimplementedf(nil, "ORGv2-SD2r1", "userType oneOf %T in method AddOrganization not implemented", nil), + }, + { + name: "user ID", + args: args{ + request: &org.AddOrganizationRequest{ + Name: "name", + Admins: []*org.AddOrganizationRequest_Admin{ + { + UserType: &org.AddOrganizationRequest_Admin_UserId{ + UserId: "userID", + }, + Roles: nil, + }, + }, + }, + }, + want: &command.OrgSetup{ + Name: "name", + CustomDomain: "", + Admins: []*command.OrgSetupAdmin{ + { + ID: "userID", + }, + }, + }, + }, + { + name: "human user", + args: args{ + request: &org.AddOrganizationRequest{ + Name: "name", + Admins: []*org.AddOrganizationRequest_Admin{ + { + UserType: &org.AddOrganizationRequest_Admin_Human{ + Human: &user.AddHumanUserRequest{ + Profile: &user.SetHumanProfile{ + GivenName: "firstname", + FamilyName: "lastname", + }, + Email: &user.SetHumanEmail{ + Email: "email@test.com", + }, + }, + }, + Roles: nil, + }, + }, + }, + }, + want: &command.OrgSetup{ + Name: "name", + CustomDomain: "", + Admins: []*command.OrgSetupAdmin{ + { + Human: &command.AddHuman{ + Username: "email@test.com", + FirstName: "firstname", + LastName: "lastname", + Email: command.Email{ + Address: "email@test.com", + }, + Metadata: make([]*command.AddMetadataEntry, 0), + Links: make([]*command.AddLink, 0), + }, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := addOrganizationRequestToCommand(tt.args.request) + require.ErrorIs(t, err, tt.wantErr) + assert.Equal(t, tt.want, got) + }) + } +} + +func Test_createdOrganizationToPb(t *testing.T) { + now := time.Now() + type args struct { + createdOrg *command.CreatedOrg + } + tests := []struct { + name string + args args + want *org.AddOrganizationResponse + wantErr error + }{ + { + name: "human user with phone and email code", + args: args{ + createdOrg: &command.CreatedOrg{ + ObjectDetails: &domain.ObjectDetails{ + Sequence: 1, + EventDate: now, + ResourceOwner: "orgID", + }, + CreatedAdmins: []*command.CreatedOrgAdmin{ + { + ID: "id", + EmailCode: gu.Ptr("emailCode"), + PhoneCode: gu.Ptr("phoneCode"), + }, + }, + }, + }, + want: &org.AddOrganizationResponse{ + Details: &object.Details{ + Sequence: 1, + ChangeDate: timestamppb.New(now), + ResourceOwner: "orgID", + }, + OrganizationId: "orgID", + CreatedAdmins: []*org.AddOrganizationResponse_CreatedAdmin{ + { + UserId: "id", + EmailCode: gu.Ptr("emailCode"), + PhoneCode: gu.Ptr("phoneCode"), + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := createdOrganizationToPb(tt.args.createdOrg) + require.ErrorIs(t, err, tt.wantErr) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/internal/api/grpc/org/v2beta/server.go b/internal/api/grpc/org/v2beta/server.go new file mode 100644 index 0000000000..89dba81702 --- /dev/null +++ b/internal/api/grpc/org/v2beta/server.go @@ -0,0 +1,55 @@ +package org + +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/domain" + "github.com/zitadel/zitadel/internal/query" + org "github.com/zitadel/zitadel/pkg/grpc/org/v2beta" +) + +var _ org.OrganizationServiceServer = (*Server)(nil) + +type Server struct { + org.UnimplementedOrganizationServiceServer + command *command.Commands + query *query.Queries + checkPermission domain.PermissionCheck +} + +type Config struct{} + +func CreateServer( + command *command.Commands, + query *query.Queries, + checkPermission domain.PermissionCheck, +) *Server { + return &Server{ + command: command, + query: query, + checkPermission: checkPermission, + } +} + +func (s *Server) RegisterServer(grpcServer *grpc.Server) { + org.RegisterOrganizationServiceServer(grpcServer, s) +} + +func (s *Server) AppName() string { + return org.OrganizationService_ServiceDesc.ServiceName +} + +func (s *Server) MethodPrefix() string { + return org.OrganizationService_ServiceDesc.ServiceName +} + +func (s *Server) AuthMethods() authz.MethodMapping { + return org.OrganizationService_AuthMethods +} + +func (s *Server) RegisterGateway() server.RegisterGatewayFunc { + return org.RegisterOrganizationServiceHandler +} diff --git a/internal/api/grpc/server/middleware/activity_interceptor.go b/internal/api/grpc/server/middleware/activity_interceptor.go index 7b8b164e99..29b7612eef 100644 --- a/internal/api/grpc/server/middleware/activity_interceptor.go +++ b/internal/api/grpc/server/middleware/activity_interceptor.go @@ -29,6 +29,8 @@ func ActivityInterceptor() grpc.UnaryServerInterceptor { var resourcePrefixes = []string{ "/zitadel.management.v1.ManagementService/", "/zitadel.admin.v1.AdminService/", + "/zitadel.user.v2.UserService/", + "/zitadel.settings.v2.SettingsService/", "/zitadel.user.v2beta.UserService/", "/zitadel.settings.v2beta.SettingsService/", "/zitadel.auth.v1.AuthService/", diff --git a/internal/api/grpc/server/middleware/execution_interceptor_test.go b/internal/api/grpc/server/middleware/execution_interceptor_test.go index bbc87c374f..f59fd00441 100644 --- a/internal/api/grpc/server/middleware/execution_interceptor_test.go +++ b/internal/api/grpc/server/middleware/execution_interceptor_test.go @@ -131,7 +131,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) { executionTargets: []execution.Target{ &mockExecutionTarget{ InstanceID: "instance", - ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession", + ExecutionID: "request./zitadel.session.v2.SessionService/SetSession", TargetID: "target", TargetType: domain.TargetTypeCall, Timeout: time.Minute, @@ -153,7 +153,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) { executionTargets: []execution.Target{ &mockExecutionTarget{ InstanceID: "instance", - ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession", + ExecutionID: "request./zitadel.session.v2.SessionService/SetSession", TargetID: "target", TargetType: domain.TargetTypeCall, Timeout: time.Minute, @@ -181,7 +181,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) { executionTargets: []execution.Target{ &mockExecutionTarget{ InstanceID: "instance", - ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession", + ExecutionID: "request./zitadel.session.v2.SessionService/SetSession", TargetID: "target", TargetType: domain.TargetTypeCall, Timeout: time.Minute, @@ -211,7 +211,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) { executionTargets: []execution.Target{ &mockExecutionTarget{ InstanceID: "instance", - ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession", + ExecutionID: "request./zitadel.session.v2.SessionService/SetSession", TargetID: "target", TargetType: domain.TargetTypeCall, Timeout: time.Second, @@ -240,7 +240,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) { executionTargets: []execution.Target{ &mockExecutionTarget{ InstanceID: "instance", - ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession", + ExecutionID: "request./zitadel.session.v2.SessionService/SetSession", TargetID: "target", TargetType: domain.TargetTypeCall, Timeout: time.Second, @@ -264,7 +264,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) { executionTargets: []execution.Target{ &mockExecutionTarget{ InstanceID: "instance", - ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession", + ExecutionID: "request./zitadel.session.v2.SessionService/SetSession", TargetID: "target", TargetType: domain.TargetTypeCall, Timeout: time.Minute, @@ -293,7 +293,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) { executionTargets: []execution.Target{ &mockExecutionTarget{ InstanceID: "instance", - ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession", + ExecutionID: "request./zitadel.session.v2.SessionService/SetSession", TargetID: "target", TargetType: domain.TargetTypeAsync, Timeout: time.Second, @@ -321,7 +321,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) { executionTargets: []execution.Target{ &mockExecutionTarget{ InstanceID: "instance", - ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession", + ExecutionID: "request./zitadel.session.v2.SessionService/SetSession", TargetID: "target", TargetType: domain.TargetTypeAsync, Timeout: time.Minute, @@ -349,7 +349,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) { executionTargets: []execution.Target{ &mockExecutionTarget{ InstanceID: "instance", - ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession", + ExecutionID: "request./zitadel.session.v2.SessionService/SetSession", TargetID: "target", TargetType: domain.TargetTypeWebhook, Timeout: time.Minute, @@ -377,7 +377,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) { executionTargets: []execution.Target{ &mockExecutionTarget{ InstanceID: "instance", - ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession", + ExecutionID: "request./zitadel.session.v2.SessionService/SetSession", TargetID: "target", TargetType: domain.TargetTypeWebhook, Timeout: time.Second, @@ -406,7 +406,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) { executionTargets: []execution.Target{ &mockExecutionTarget{ InstanceID: "instance", - ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession", + ExecutionID: "request./zitadel.session.v2.SessionService/SetSession", TargetID: "target", TargetType: domain.TargetTypeWebhook, Timeout: time.Minute, @@ -435,7 +435,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) { executionTargets: []execution.Target{ &mockExecutionTarget{ InstanceID: "instance", - ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession", + ExecutionID: "request./zitadel.session.v2.SessionService/SetSession", TargetID: "target1", TargetType: domain.TargetTypeCall, Timeout: time.Minute, @@ -443,7 +443,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) { }, &mockExecutionTarget{ InstanceID: "instance", - ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession", + ExecutionID: "request./zitadel.session.v2.SessionService/SetSession", TargetID: "target2", TargetType: domain.TargetTypeCall, Timeout: time.Minute, @@ -451,7 +451,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) { }, &mockExecutionTarget{ InstanceID: "instance", - ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession", + ExecutionID: "request./zitadel.session.v2.SessionService/SetSession", TargetID: "target3", TargetType: domain.TargetTypeCall, Timeout: time.Minute, @@ -493,7 +493,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) { executionTargets: []execution.Target{ &mockExecutionTarget{ InstanceID: "instance", - ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession", + ExecutionID: "request./zitadel.session.v2.SessionService/SetSession", TargetID: "target1", TargetType: domain.TargetTypeCall, Timeout: time.Minute, @@ -501,7 +501,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) { }, &mockExecutionTarget{ InstanceID: "instance", - ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession", + ExecutionID: "request./zitadel.session.v2.SessionService/SetSession", TargetID: "target2", TargetType: domain.TargetTypeCall, Timeout: time.Second, @@ -509,7 +509,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) { }, &mockExecutionTarget{ InstanceID: "instance", - ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession", + ExecutionID: "request./zitadel.session.v2.SessionService/SetSession", TargetID: "target3", TargetType: domain.TargetTypeCall, Timeout: time.Second, @@ -687,7 +687,7 @@ func Test_executeTargetsForGRPCFullMethod_response(t *testing.T) { executionTargets: []execution.Target{ &mockExecutionTarget{ InstanceID: "instance", - ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession", + ExecutionID: "request./zitadel.session.v2.SessionService/SetSession", TargetID: "target", TargetType: domain.TargetTypeCall, Timeout: time.Minute, @@ -716,7 +716,7 @@ func Test_executeTargetsForGRPCFullMethod_response(t *testing.T) { executionTargets: []execution.Target{ &mockExecutionTarget{ InstanceID: "instance", - ExecutionID: "response./zitadel.session.v2beta.SessionService/SetSession", + ExecutionID: "response./zitadel.session.v2.SessionService/SetSession", TargetID: "target", TargetType: domain.TargetTypeCall, Timeout: time.Minute, diff --git a/internal/api/grpc/session/v2/server.go b/internal/api/grpc/session/v2/server.go index 550d013ad5..e94336bf47 100644 --- a/internal/api/grpc/session/v2/server.go +++ b/internal/api/grpc/session/v2/server.go @@ -7,7 +7,7 @@ import ( "github.com/zitadel/zitadel/internal/api/grpc/server" "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/query" - session "github.com/zitadel/zitadel/pkg/grpc/session/v2beta" + "github.com/zitadel/zitadel/pkg/grpc/session/v2" ) var _ session.SessionServiceServer = (*Server)(nil) diff --git a/internal/api/grpc/session/v2/session.go b/internal/api/grpc/session/v2/session.go index 7af87798e6..aa25fa0ae3 100644 --- a/internal/api/grpc/session/v2/session.go +++ b/internal/api/grpc/session/v2/session.go @@ -18,7 +18,7 @@ import ( "github.com/zitadel/zitadel/internal/query" "github.com/zitadel/zitadel/internal/zerrors" objpb "github.com/zitadel/zitadel/pkg/grpc/object" - session "github.com/zitadel/zitadel/pkg/grpc/session/v2beta" + "github.com/zitadel/zitadel/pkg/grpc/session/v2" ) var ( diff --git a/internal/api/grpc/session/v2/session_integration_test.go b/internal/api/grpc/session/v2/session_integration_test.go index 92d3b0baf7..0871f92994 100644 --- a/internal/api/grpc/session/v2/session_integration_test.go +++ b/internal/api/grpc/session/v2/session_integration_test.go @@ -23,9 +23,9 @@ import ( "github.com/zitadel/zitadel/internal/integration" mgmt "github.com/zitadel/zitadel/pkg/grpc/management" - object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta" - session "github.com/zitadel/zitadel/pkg/grpc/session/v2beta" - user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta" + "github.com/zitadel/zitadel/pkg/grpc/object/v2" + "github.com/zitadel/zitadel/pkg/grpc/session/v2" + "github.com/zitadel/zitadel/pkg/grpc/user/v2" ) var ( @@ -860,7 +860,7 @@ func TestServer_SetSession_expired(t *testing.T) { // ensure session expires and does not work anymore time.Sleep(20 * time.Second) - _, err = Tester.Client.SessionV2.SetSession(CTX, &session.SetSessionRequest{ + _, err = Client.SetSession(CTX, &session.SetSessionRequest{ SessionId: createResp.GetSessionId(), Lifetime: durationpb.New(20 * time.Second), }) @@ -944,7 +944,7 @@ func Test_ZITADEL_API_missing_authentication(t *testing.T) { require.NoError(t, err) ctx := metadata.AppendToOutgoingContext(context.Background(), "Authorization", fmt.Sprintf("Bearer %s", createResp.GetSessionToken())) - sessionResp, err := Tester.Client.SessionV2.GetSession(ctx, &session.GetSessionRequest{SessionId: createResp.GetSessionId()}) + sessionResp, err := Client.GetSession(ctx, &session.GetSessionRequest{SessionId: createResp.GetSessionId()}) require.Error(t, err) require.Nil(t, sessionResp) } @@ -953,7 +953,7 @@ func Test_ZITADEL_API_success(t *testing.T) { id, token, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, User.GetUserId()) ctx := Tester.WithAuthorizationToken(context.Background(), token) - sessionResp, err := Tester.Client.SessionV2.GetSession(ctx, &session.GetSessionRequest{SessionId: id}) + sessionResp, err := Client.GetSession(ctx, &session.GetSessionRequest{SessionId: id}) require.NoError(t, err) webAuthN := sessionResp.GetSession().GetFactors().GetWebAuthN() @@ -966,17 +966,17 @@ func Test_ZITADEL_API_session_not_found(t *testing.T) { // test session token works ctx := Tester.WithAuthorizationToken(context.Background(), token) - _, err := Tester.Client.SessionV2.GetSession(ctx, &session.GetSessionRequest{SessionId: id}) + _, err := Client.GetSession(ctx, &session.GetSessionRequest{SessionId: id}) require.NoError(t, err) //terminate the session and test it does not work anymore - _, err = Tester.Client.SessionV2.DeleteSession(CTX, &session.DeleteSessionRequest{ + _, err = Client.DeleteSession(CTX, &session.DeleteSessionRequest{ SessionId: id, SessionToken: gu.Ptr(token), }) require.NoError(t, err) ctx = Tester.WithAuthorizationToken(context.Background(), token) - _, err = Tester.Client.SessionV2.GetSession(ctx, &session.GetSessionRequest{SessionId: id}) + _, err = Client.GetSession(ctx, &session.GetSessionRequest{SessionId: id}) require.Error(t, err) } @@ -985,12 +985,12 @@ func Test_ZITADEL_API_session_expired(t *testing.T) { // test session token works ctx := Tester.WithAuthorizationToken(context.Background(), token) - _, err := Tester.Client.SessionV2.GetSession(ctx, &session.GetSessionRequest{SessionId: id}) + _, err := Client.GetSession(ctx, &session.GetSessionRequest{SessionId: id}) require.NoError(t, err) // ensure session expires and does not work anymore time.Sleep(20 * time.Second) - sessionResp, err := Tester.Client.SessionV2.GetSession(ctx, &session.GetSessionRequest{SessionId: id}) + sessionResp, err := Client.GetSession(ctx, &session.GetSessionRequest{SessionId: id}) require.Error(t, err) require.Nil(t, sessionResp) } diff --git a/internal/api/grpc/session/v2/session_test.go b/internal/api/grpc/session/v2/session_test.go index c088b5b886..917be882f8 100644 --- a/internal/api/grpc/session/v2/session_test.go +++ b/internal/api/grpc/session/v2/session_test.go @@ -18,8 +18,8 @@ import ( "github.com/zitadel/zitadel/internal/query" "github.com/zitadel/zitadel/internal/zerrors" objpb "github.com/zitadel/zitadel/pkg/grpc/object" - object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta" - session "github.com/zitadel/zitadel/pkg/grpc/session/v2beta" + "github.com/zitadel/zitadel/pkg/grpc/object/v2" + "github.com/zitadel/zitadel/pkg/grpc/session/v2" ) var ( diff --git a/internal/api/grpc/session/v2beta/server.go b/internal/api/grpc/session/v2beta/server.go new file mode 100644 index 0000000000..550d013ad5 --- /dev/null +++ b/internal/api/grpc/session/v2beta/server.go @@ -0,0 +1,51 @@ +package session + +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/query" + session "github.com/zitadel/zitadel/pkg/grpc/session/v2beta" +) + +var _ session.SessionServiceServer = (*Server)(nil) + +type Server struct { + session.UnimplementedSessionServiceServer + command *command.Commands + query *query.Queries +} + +type Config struct{} + +func CreateServer( + command *command.Commands, + query *query.Queries, +) *Server { + return &Server{ + command: command, + query: query, + } +} + +func (s *Server) RegisterServer(grpcServer *grpc.Server) { + session.RegisterSessionServiceServer(grpcServer, s) +} + +func (s *Server) AppName() string { + return session.SessionService_ServiceDesc.ServiceName +} + +func (s *Server) MethodPrefix() string { + return session.SessionService_ServiceDesc.ServiceName +} + +func (s *Server) AuthMethods() authz.MethodMapping { + return session.SessionService_AuthMethods +} + +func (s *Server) RegisterGateway() server.RegisterGatewayFunc { + return session.RegisterSessionServiceHandler +} diff --git a/internal/api/grpc/session/v2beta/session.go b/internal/api/grpc/session/v2beta/session.go new file mode 100644 index 0000000000..7e67a4b3ff --- /dev/null +++ b/internal/api/grpc/session/v2beta/session.go @@ -0,0 +1,500 @@ +package session + +import ( + "context" + "net" + "net/http" + "time" + + "github.com/muhlemmer/gu" + "golang.org/x/text/language" + "google.golang.org/protobuf/types/known/structpb" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/api/authz" + object "github.com/zitadel/zitadel/internal/api/grpc/object/v2beta" + "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/query" + "github.com/zitadel/zitadel/internal/zerrors" + objpb "github.com/zitadel/zitadel/pkg/grpc/object" + session "github.com/zitadel/zitadel/pkg/grpc/session/v2beta" +) + +var ( + timestampComparisons = map[objpb.TimestampQueryMethod]query.TimestampComparison{ + objpb.TimestampQueryMethod_TIMESTAMP_QUERY_METHOD_EQUALS: query.TimestampEquals, + objpb.TimestampQueryMethod_TIMESTAMP_QUERY_METHOD_GREATER: query.TimestampGreater, + objpb.TimestampQueryMethod_TIMESTAMP_QUERY_METHOD_GREATER_OR_EQUALS: query.TimestampGreaterOrEquals, + objpb.TimestampQueryMethod_TIMESTAMP_QUERY_METHOD_LESS: query.TimestampLess, + objpb.TimestampQueryMethod_TIMESTAMP_QUERY_METHOD_LESS_OR_EQUALS: query.TimestampLessOrEquals, + } +) + +func (s *Server) GetSession(ctx context.Context, req *session.GetSessionRequest) (*session.GetSessionResponse, error) { + res, err := s.query.SessionByID(ctx, true, req.GetSessionId(), req.GetSessionToken()) + if err != nil { + return nil, err + } + return &session.GetSessionResponse{ + Session: sessionToPb(res), + }, nil +} + +func (s *Server) ListSessions(ctx context.Context, req *session.ListSessionsRequest) (*session.ListSessionsResponse, error) { + queries, err := listSessionsRequestToQuery(ctx, req) + if err != nil { + return nil, err + } + sessions, err := s.query.SearchSessions(ctx, queries) + if err != nil { + return nil, err + } + return &session.ListSessionsResponse{ + Details: object.ToListDetails(sessions.SearchResponse), + Sessions: sessionsToPb(sessions.Sessions), + }, nil +} + +func (s *Server) CreateSession(ctx context.Context, req *session.CreateSessionRequest) (*session.CreateSessionResponse, error) { + checks, metadata, userAgent, lifetime, err := s.createSessionRequestToCommand(ctx, req) + if err != nil { + return nil, err + } + challengeResponse, cmds, err := s.challengesToCommand(req.GetChallenges(), checks) + if err != nil { + return nil, err + } + + set, err := s.command.CreateSession(ctx, cmds, metadata, userAgent, lifetime) + if err != nil { + return nil, err + } + + return &session.CreateSessionResponse{ + Details: object.DomainToDetailsPb(set.ObjectDetails), + SessionId: set.ID, + SessionToken: set.NewToken, + Challenges: challengeResponse, + }, nil +} + +func (s *Server) SetSession(ctx context.Context, req *session.SetSessionRequest) (*session.SetSessionResponse, error) { + checks, err := s.setSessionRequestToCommand(ctx, req) + if err != nil { + return nil, err + } + challengeResponse, cmds, err := s.challengesToCommand(req.GetChallenges(), checks) + if err != nil { + return nil, err + } + + set, err := s.command.UpdateSession(ctx, req.GetSessionId(), cmds, req.GetMetadata(), req.GetLifetime().AsDuration()) + if err != nil { + return nil, err + } + return &session.SetSessionResponse{ + Details: object.DomainToDetailsPb(set.ObjectDetails), + SessionToken: set.NewToken, + Challenges: challengeResponse, + }, nil +} + +func (s *Server) DeleteSession(ctx context.Context, req *session.DeleteSessionRequest) (*session.DeleteSessionResponse, error) { + details, err := s.command.TerminateSession(ctx, req.GetSessionId(), req.GetSessionToken()) + if err != nil { + return nil, err + } + return &session.DeleteSessionResponse{ + Details: object.DomainToDetailsPb(details), + }, nil +} + +func sessionsToPb(sessions []*query.Session) []*session.Session { + s := make([]*session.Session, len(sessions)) + for i, session := range sessions { + s[i] = sessionToPb(session) + } + return s +} + +func sessionToPb(s *query.Session) *session.Session { + return &session.Session{ + Id: s.ID, + CreationDate: timestamppb.New(s.CreationDate), + ChangeDate: timestamppb.New(s.ChangeDate), + Sequence: s.Sequence, + Factors: factorsToPb(s), + Metadata: s.Metadata, + UserAgent: userAgentToPb(s.UserAgent), + ExpirationDate: expirationToPb(s.Expiration), + } +} + +func userAgentToPb(ua domain.UserAgent) *session.UserAgent { + if ua.IsEmpty() { + return nil + } + + out := &session.UserAgent{ + FingerprintId: ua.FingerprintID, + Description: ua.Description, + } + if ua.IP != nil { + out.Ip = gu.Ptr(ua.IP.String()) + } + if ua.Header == nil { + return out + } + out.Header = make(map[string]*session.UserAgent_HeaderValues, len(ua.Header)) + for k, v := range ua.Header { + out.Header[k] = &session.UserAgent_HeaderValues{ + Values: v, + } + } + return out +} + +func expirationToPb(expiration time.Time) *timestamppb.Timestamp { + if expiration.IsZero() { + return nil + } + return timestamppb.New(expiration) +} + +func factorsToPb(s *query.Session) *session.Factors { + user := userFactorToPb(s.UserFactor) + if user == nil { + return nil + } + return &session.Factors{ + User: user, + Password: passwordFactorToPb(s.PasswordFactor), + WebAuthN: webAuthNFactorToPb(s.WebAuthNFactor), + Intent: intentFactorToPb(s.IntentFactor), + Totp: totpFactorToPb(s.TOTPFactor), + OtpSms: otpFactorToPb(s.OTPSMSFactor), + OtpEmail: otpFactorToPb(s.OTPEmailFactor), + } +} + +func passwordFactorToPb(factor query.SessionPasswordFactor) *session.PasswordFactor { + if factor.PasswordCheckedAt.IsZero() { + return nil + } + return &session.PasswordFactor{ + VerifiedAt: timestamppb.New(factor.PasswordCheckedAt), + } +} + +func intentFactorToPb(factor query.SessionIntentFactor) *session.IntentFactor { + if factor.IntentCheckedAt.IsZero() { + return nil + } + return &session.IntentFactor{ + VerifiedAt: timestamppb.New(factor.IntentCheckedAt), + } +} + +func webAuthNFactorToPb(factor query.SessionWebAuthNFactor) *session.WebAuthNFactor { + if factor.WebAuthNCheckedAt.IsZero() { + return nil + } + return &session.WebAuthNFactor{ + VerifiedAt: timestamppb.New(factor.WebAuthNCheckedAt), + UserVerified: factor.UserVerified, + } +} + +func totpFactorToPb(factor query.SessionTOTPFactor) *session.TOTPFactor { + if factor.TOTPCheckedAt.IsZero() { + return nil + } + return &session.TOTPFactor{ + VerifiedAt: timestamppb.New(factor.TOTPCheckedAt), + } +} + +func otpFactorToPb(factor query.SessionOTPFactor) *session.OTPFactor { + if factor.OTPCheckedAt.IsZero() { + return nil + } + return &session.OTPFactor{ + VerifiedAt: timestamppb.New(factor.OTPCheckedAt), + } +} + +func userFactorToPb(factor query.SessionUserFactor) *session.UserFactor { + if factor.UserID == "" || factor.UserCheckedAt.IsZero() { + return nil + } + return &session.UserFactor{ + VerifiedAt: timestamppb.New(factor.UserCheckedAt), + Id: factor.UserID, + LoginName: factor.LoginName, + DisplayName: factor.DisplayName, + OrganizationId: factor.ResourceOwner, + } +} + +func listSessionsRequestToQuery(ctx context.Context, req *session.ListSessionsRequest) (*query.SessionsSearchQueries, error) { + offset, limit, asc := object.ListQueryToQuery(req.Query) + queries, err := sessionQueriesToQuery(ctx, req.GetQueries()) + if err != nil { + return nil, err + } + return &query.SessionsSearchQueries{ + SearchRequest: query.SearchRequest{ + Offset: offset, + Limit: limit, + Asc: asc, + SortingColumn: fieldNameToSessionColumn(req.GetSortingColumn()), + }, + Queries: queries, + }, nil +} + +func sessionQueriesToQuery(ctx context.Context, queries []*session.SearchQuery) (_ []query.SearchQuery, err error) { + q := make([]query.SearchQuery, len(queries)+1) + for i, v := range queries { + q[i], err = sessionQueryToQuery(v) + if err != nil { + return nil, err + } + } + creatorQuery, err := query.NewSessionCreatorSearchQuery(authz.GetCtxData(ctx).UserID) + if err != nil { + return nil, err + } + q[len(queries)] = creatorQuery + return q, nil +} + +func sessionQueryToQuery(sq *session.SearchQuery) (query.SearchQuery, error) { + switch q := sq.Query.(type) { + case *session.SearchQuery_IdsQuery: + return idsQueryToQuery(q.IdsQuery) + case *session.SearchQuery_UserIdQuery: + return query.NewUserIDSearchQuery(q.UserIdQuery.GetId()) + case *session.SearchQuery_CreationDateQuery: + return creationDateQueryToQuery(q.CreationDateQuery) + default: + return nil, zerrors.ThrowInvalidArgument(nil, "GRPC-Sfefs", "List.Query.Invalid") + } +} + +func idsQueryToQuery(q *session.IDsQuery) (query.SearchQuery, error) { + return query.NewSessionIDsSearchQuery(q.Ids) +} + +func creationDateQueryToQuery(q *session.CreationDateQuery) (query.SearchQuery, error) { + comparison := timestampComparisons[q.GetMethod()] + return query.NewCreationDateQuery(q.GetCreationDate().AsTime(), comparison) +} + +func fieldNameToSessionColumn(field session.SessionFieldName) query.Column { + switch field { + case session.SessionFieldName_SESSION_FIELD_NAME_CREATION_DATE: + return query.SessionColumnCreationDate + case session.SessionFieldName_SESSION_FIELD_NAME_UNSPECIFIED: + // Handle all remaining cases so the linter succeeds + return query.Column{} + default: + return query.Column{} + } +} + +func (s *Server) createSessionRequestToCommand(ctx context.Context, req *session.CreateSessionRequest) ([]command.SessionCommand, map[string][]byte, *domain.UserAgent, time.Duration, error) { + checks, err := s.checksToCommand(ctx, req.Checks) + if err != nil { + return nil, nil, nil, 0, err + } + return checks, req.GetMetadata(), userAgentToCommand(req.GetUserAgent()), req.GetLifetime().AsDuration(), nil +} + +func userAgentToCommand(userAgent *session.UserAgent) *domain.UserAgent { + if userAgent == nil { + return nil + } + out := &domain.UserAgent{ + FingerprintID: userAgent.FingerprintId, + IP: net.ParseIP(userAgent.GetIp()), + Description: userAgent.Description, + } + if len(userAgent.Header) > 0 { + out.Header = make(http.Header, len(userAgent.Header)) + for k, values := range userAgent.Header { + out.Header[k] = values.GetValues() + } + } + return out +} + +func (s *Server) setSessionRequestToCommand(ctx context.Context, req *session.SetSessionRequest) ([]command.SessionCommand, error) { + checks, err := s.checksToCommand(ctx, req.Checks) + if err != nil { + return nil, err + } + return checks, nil +} + +func (s *Server) checksToCommand(ctx context.Context, checks *session.Checks) ([]command.SessionCommand, error) { + checkUser, err := userCheck(checks.GetUser()) + if err != nil { + return nil, err + } + sessionChecks := make([]command.SessionCommand, 0, 7) + if checkUser != nil { + user, err := checkUser.search(ctx, s.query) + if err != nil { + return nil, err + } + if !user.State.IsEnabled() { + return nil, zerrors.ThrowPreconditionFailed(nil, "SESSION-Gj4ko", "Errors.User.NotActive") + } + + var preferredLanguage *language.Tag + if user.Human != nil && !user.Human.PreferredLanguage.IsRoot() { + preferredLanguage = &user.Human.PreferredLanguage + } + sessionChecks = append(sessionChecks, command.CheckUser(user.ID, user.ResourceOwner, preferredLanguage)) + } + if password := checks.GetPassword(); password != nil { + sessionChecks = append(sessionChecks, command.CheckPassword(password.GetPassword())) + } + if intent := checks.GetIdpIntent(); intent != nil { + sessionChecks = append(sessionChecks, command.CheckIntent(intent.GetIdpIntentId(), intent.GetIdpIntentToken())) + } + if passkey := checks.GetWebAuthN(); passkey != nil { + sessionChecks = append(sessionChecks, s.command.CheckWebAuthN(passkey.GetCredentialAssertionData())) + } + if totp := checks.GetTotp(); totp != nil { + sessionChecks = append(sessionChecks, command.CheckTOTP(totp.GetCode())) + } + if otp := checks.GetOtpSms(); otp != nil { + sessionChecks = append(sessionChecks, command.CheckOTPSMS(otp.GetCode())) + } + if otp := checks.GetOtpEmail(); otp != nil { + sessionChecks = append(sessionChecks, command.CheckOTPEmail(otp.GetCode())) + } + return sessionChecks, nil +} + +func (s *Server) challengesToCommand(challenges *session.RequestChallenges, cmds []command.SessionCommand) (*session.Challenges, []command.SessionCommand, error) { + if challenges == nil { + return nil, cmds, nil + } + resp := new(session.Challenges) + if req := challenges.GetWebAuthN(); req != nil { + challenge, cmd := s.createWebAuthNChallengeCommand(req) + resp.WebAuthN = challenge + cmds = append(cmds, cmd) + } + if req := challenges.GetOtpSms(); req != nil { + challenge, cmd := s.createOTPSMSChallengeCommand(req) + resp.OtpSms = challenge + cmds = append(cmds, cmd) + } + if req := challenges.GetOtpEmail(); req != nil { + challenge, cmd, err := s.createOTPEmailChallengeCommand(req) + if err != nil { + return nil, nil, err + } + resp.OtpEmail = challenge + cmds = append(cmds, cmd) + } + return resp, cmds, nil +} + +func (s *Server) createWebAuthNChallengeCommand(req *session.RequestChallenges_WebAuthN) (*session.Challenges_WebAuthN, command.SessionCommand) { + challenge := &session.Challenges_WebAuthN{ + PublicKeyCredentialRequestOptions: new(structpb.Struct), + } + userVerification := userVerificationRequirementToDomain(req.GetUserVerificationRequirement()) + return challenge, s.command.CreateWebAuthNChallenge(userVerification, req.GetDomain(), challenge.PublicKeyCredentialRequestOptions) +} + +func userVerificationRequirementToDomain(req session.UserVerificationRequirement) domain.UserVerificationRequirement { + switch req { + case session.UserVerificationRequirement_USER_VERIFICATION_REQUIREMENT_UNSPECIFIED: + return domain.UserVerificationRequirementUnspecified + case session.UserVerificationRequirement_USER_VERIFICATION_REQUIREMENT_REQUIRED: + return domain.UserVerificationRequirementRequired + case session.UserVerificationRequirement_USER_VERIFICATION_REQUIREMENT_PREFERRED: + return domain.UserVerificationRequirementPreferred + case session.UserVerificationRequirement_USER_VERIFICATION_REQUIREMENT_DISCOURAGED: + return domain.UserVerificationRequirementDiscouraged + default: + return domain.UserVerificationRequirementUnspecified + } +} + +func (s *Server) createOTPSMSChallengeCommand(req *session.RequestChallenges_OTPSMS) (*string, command.SessionCommand) { + if req.GetReturnCode() { + challenge := new(string) + return challenge, s.command.CreateOTPSMSChallengeReturnCode(challenge) + } + + return nil, s.command.CreateOTPSMSChallenge() + +} + +func (s *Server) createOTPEmailChallengeCommand(req *session.RequestChallenges_OTPEmail) (*string, command.SessionCommand, error) { + switch t := req.GetDeliveryType().(type) { + case *session.RequestChallenges_OTPEmail_SendCode_: + cmd, err := s.command.CreateOTPEmailChallengeURLTemplate(t.SendCode.GetUrlTemplate()) + if err != nil { + return nil, nil, err + } + return nil, cmd, nil + case *session.RequestChallenges_OTPEmail_ReturnCode_: + challenge := new(string) + return challenge, s.command.CreateOTPEmailChallengeReturnCode(challenge), nil + case nil: + return nil, s.command.CreateOTPEmailChallenge(), nil + default: + return nil, nil, zerrors.ThrowUnimplementedf(nil, "SESSION-k3ng0", "delivery_type oneOf %T in OTPEmailChallenge not implemented", t) + } +} + +func userCheck(user *session.CheckUser) (userSearch, error) { + if user == nil { + return nil, nil + } + switch s := user.GetSearch().(type) { + case *session.CheckUser_UserId: + return userByID(s.UserId), nil + case *session.CheckUser_LoginName: + return userByLoginName(s.LoginName) + default: + return nil, zerrors.ThrowUnimplementedf(nil, "SESSION-d3b4g0", "user search %T not implemented", s) + } +} + +type userSearch interface { + search(ctx context.Context, q *query.Queries) (*query.User, error) +} + +func userByID(userID string) userSearch { + return userSearchByID{userID} +} + +func userByLoginName(loginName string) (userSearch, error) { + return userSearchByLoginName{loginName}, nil +} + +type userSearchByID struct { + id string +} + +func (u userSearchByID) search(ctx context.Context, q *query.Queries) (*query.User, error) { + return q.GetUserByID(ctx, true, u.id) +} + +type userSearchByLoginName struct { + loginName string +} + +func (u userSearchByLoginName) search(ctx context.Context, q *query.Queries) (*query.User, error) { + return q.GetUserByLoginName(ctx, true, u.loginName) +} diff --git a/internal/api/grpc/session/v2beta/session_integration_test.go b/internal/api/grpc/session/v2beta/session_integration_test.go new file mode 100644 index 0000000000..94ecdf5410 --- /dev/null +++ b/internal/api/grpc/session/v2beta/session_integration_test.go @@ -0,0 +1,996 @@ +//go:build integration + +package session_test + +import ( + "context" + "fmt" + "os" + "testing" + "time" + + "github.com/muhlemmer/gu" + "github.com/pquerna/otp/totp" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/zitadel/logging" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/durationpb" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/integration" + mgmt "github.com/zitadel/zitadel/pkg/grpc/management" + object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta" + session "github.com/zitadel/zitadel/pkg/grpc/session/v2beta" + "github.com/zitadel/zitadel/pkg/grpc/user/v2" +) + +var ( + CTX context.Context + IAMOwnerCTX context.Context + Tester *integration.Tester + Client session.SessionServiceClient + User *user.AddHumanUserResponse + DeactivatedUser *user.AddHumanUserResponse + LockedUser *user.AddHumanUserResponse +) + +func TestMain(m *testing.M) { + os.Exit(func() int { + ctx, errCtx, cancel := integration.Contexts(5 * time.Minute) + defer cancel() + + Tester = integration.NewTester(ctx) + defer Tester.Done() + Client = Tester.Client.SessionV2beta + + CTX, _ = Tester.WithAuthorization(ctx, integration.OrgOwner), errCtx + IAMOwnerCTX = Tester.WithAuthorization(ctx, integration.IAMOwner) + User = createFullUser(CTX) + DeactivatedUser = createDeactivatedUser(CTX) + LockedUser = createLockedUser(CTX) + return m.Run() + }()) +} + +func createFullUser(ctx context.Context) *user.AddHumanUserResponse { + userResp := Tester.CreateHumanUser(ctx) + Tester.Client.UserV2.VerifyEmail(ctx, &user.VerifyEmailRequest{ + UserId: userResp.GetUserId(), + VerificationCode: userResp.GetEmailCode(), + }) + Tester.Client.UserV2.VerifyPhone(ctx, &user.VerifyPhoneRequest{ + UserId: userResp.GetUserId(), + VerificationCode: userResp.GetPhoneCode(), + }) + Tester.SetUserPassword(ctx, userResp.GetUserId(), integration.UserPassword, false) + Tester.RegisterUserPasskey(ctx, userResp.GetUserId()) + return userResp +} + +func createDeactivatedUser(ctx context.Context) *user.AddHumanUserResponse { + userResp := Tester.CreateHumanUser(ctx) + _, err := Tester.Client.UserV2.DeactivateUser(ctx, &user.DeactivateUserRequest{UserId: userResp.GetUserId()}) + logging.OnError(err).Fatal("deactivate human user") + return userResp +} + +func createLockedUser(ctx context.Context) *user.AddHumanUserResponse { + userResp := Tester.CreateHumanUser(ctx) + _, err := Tester.Client.UserV2.LockUser(ctx, &user.LockUserRequest{UserId: userResp.GetUserId()}) + logging.OnError(err).Fatal("lock human user") + return userResp +} + +func verifyCurrentSession(t testing.TB, id, token string, sequence uint64, window time.Duration, metadata map[string][]byte, userAgent *session.UserAgent, expirationWindow time.Duration, userID string, factors ...wantFactor) *session.Session { + t.Helper() + require.NotEmpty(t, id) + require.NotEmpty(t, token) + + resp, err := Client.GetSession(CTX, &session.GetSessionRequest{ + SessionId: id, + SessionToken: &token, + }) + require.NoError(t, err) + s := resp.GetSession() + + assert.Equal(t, id, s.GetId()) + assert.WithinRange(t, s.GetCreationDate().AsTime(), time.Now().Add(-window), time.Now().Add(window)) + assert.WithinRange(t, s.GetChangeDate().AsTime(), time.Now().Add(-window), time.Now().Add(window)) + assert.Equal(t, sequence, s.GetSequence()) + assert.Equal(t, metadata, s.GetMetadata()) + + if !proto.Equal(userAgent, s.GetUserAgent()) { + t.Errorf("user agent =\n%v\nwant\n%v", s.GetUserAgent(), userAgent) + } + if expirationWindow == 0 { + assert.Nil(t, s.GetExpirationDate()) + } else { + assert.WithinRange(t, s.GetExpirationDate().AsTime(), time.Now().Add(-expirationWindow), time.Now().Add(expirationWindow)) + } + + verifyFactors(t, s.GetFactors(), window, userID, factors) + return s +} + +type wantFactor int + +const ( + wantUserFactor wantFactor = iota + wantPasswordFactor + wantWebAuthNFactor + wantWebAuthNFactorUserVerified + wantTOTPFactor + wantIntentFactor + wantOTPSMSFactor + wantOTPEmailFactor +) + +func verifyFactors(t testing.TB, factors *session.Factors, window time.Duration, userID string, want []wantFactor) { + for _, w := range want { + switch w { + case wantUserFactor: + uf := factors.GetUser() + assert.NotNil(t, uf) + assert.WithinRange(t, uf.GetVerifiedAt().AsTime(), time.Now().Add(-window), time.Now().Add(window)) + assert.Equal(t, userID, uf.GetId()) + case wantPasswordFactor: + pf := factors.GetPassword() + assert.NotNil(t, pf) + assert.WithinRange(t, pf.GetVerifiedAt().AsTime(), time.Now().Add(-window), time.Now().Add(window)) + case wantWebAuthNFactor: + pf := factors.GetWebAuthN() + assert.NotNil(t, pf) + assert.WithinRange(t, pf.GetVerifiedAt().AsTime(), time.Now().Add(-window), time.Now().Add(window)) + assert.False(t, pf.GetUserVerified()) + case wantWebAuthNFactorUserVerified: + pf := factors.GetWebAuthN() + assert.NotNil(t, pf) + assert.WithinRange(t, pf.GetVerifiedAt().AsTime(), time.Now().Add(-window), time.Now().Add(window)) + assert.True(t, pf.GetUserVerified()) + case wantTOTPFactor: + pf := factors.GetTotp() + assert.NotNil(t, pf) + assert.WithinRange(t, pf.GetVerifiedAt().AsTime(), time.Now().Add(-window), time.Now().Add(window)) + case wantIntentFactor: + pf := factors.GetIntent() + assert.NotNil(t, pf) + assert.WithinRange(t, pf.GetVerifiedAt().AsTime(), time.Now().Add(-window), time.Now().Add(window)) + case wantOTPSMSFactor: + pf := factors.GetOtpSms() + assert.NotNil(t, pf) + assert.WithinRange(t, pf.GetVerifiedAt().AsTime(), time.Now().Add(-window), time.Now().Add(window)) + case wantOTPEmailFactor: + pf := factors.GetOtpEmail() + assert.NotNil(t, pf) + assert.WithinRange(t, pf.GetVerifiedAt().AsTime(), time.Now().Add(-window), time.Now().Add(window)) + } + } +} + +func TestServer_CreateSession(t *testing.T) { + tests := []struct { + name string + req *session.CreateSessionRequest + want *session.CreateSessionResponse + wantErr bool + wantFactors []wantFactor + wantUserAgent *session.UserAgent + wantExpirationWindow time.Duration + }{ + { + name: "empty session", + req: &session.CreateSessionRequest{ + Metadata: map[string][]byte{"foo": []byte("bar")}, + }, + want: &session.CreateSessionResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Instance.InstanceID(), + }, + }, + }, + { + name: "user agent", + req: &session.CreateSessionRequest{ + Metadata: map[string][]byte{"foo": []byte("bar")}, + UserAgent: &session.UserAgent{ + FingerprintId: gu.Ptr("fingerPrintID"), + Ip: gu.Ptr("1.2.3.4"), + Description: gu.Ptr("Description"), + Header: map[string]*session.UserAgent_HeaderValues{ + "foo": {Values: []string{"foo", "bar"}}, + }, + }, + }, + want: &session.CreateSessionResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Instance.InstanceID(), + }, + }, + wantUserAgent: &session.UserAgent{ + FingerprintId: gu.Ptr("fingerPrintID"), + Ip: gu.Ptr("1.2.3.4"), + Description: gu.Ptr("Description"), + Header: map[string]*session.UserAgent_HeaderValues{ + "foo": {Values: []string{"foo", "bar"}}, + }, + }, + }, + { + name: "negative lifetime", + req: &session.CreateSessionRequest{ + Metadata: map[string][]byte{"foo": []byte("bar")}, + Lifetime: durationpb.New(-5 * time.Minute), + }, + wantErr: true, + }, + { + name: "lifetime", + req: &session.CreateSessionRequest{ + Metadata: map[string][]byte{"foo": []byte("bar")}, + Lifetime: durationpb.New(5 * time.Minute), + }, + want: &session.CreateSessionResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Instance.InstanceID(), + }, + }, + wantExpirationWindow: 5 * time.Minute, + }, + { + name: "with user", + req: &session.CreateSessionRequest{ + Checks: &session.Checks{ + User: &session.CheckUser{ + Search: &session.CheckUser_UserId{ + UserId: User.GetUserId(), + }, + }, + }, + Metadata: map[string][]byte{"foo": []byte("bar")}, + }, + want: &session.CreateSessionResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Instance.InstanceID(), + }, + }, + wantFactors: []wantFactor{wantUserFactor}, + }, + { + name: "deactivated user", + req: &session.CreateSessionRequest{ + Checks: &session.Checks{ + User: &session.CheckUser{ + Search: &session.CheckUser_UserId{ + UserId: DeactivatedUser.GetUserId(), + }, + }, + }, + }, + wantErr: true, + }, + { + name: "locked user", + req: &session.CreateSessionRequest{ + Checks: &session.Checks{ + User: &session.CheckUser{ + Search: &session.CheckUser_UserId{ + UserId: LockedUser.GetUserId(), + }, + }, + }, + }, + wantErr: true, + }, + { + name: "password without user error", + req: &session.CreateSessionRequest{ + Checks: &session.Checks{ + Password: &session.CheckPassword{ + Password: "Difficult", + }, + }, + }, + wantErr: true, + }, + { + name: "passkey without user error", + req: &session.CreateSessionRequest{ + Challenges: &session.RequestChallenges{ + WebAuthN: &session.RequestChallenges_WebAuthN{ + Domain: Tester.Config.ExternalDomain, + UserVerificationRequirement: session.UserVerificationRequirement_USER_VERIFICATION_REQUIREMENT_REQUIRED, + }, + }, + }, + wantErr: true, + }, + { + name: "passkey without domain (not registered) error", + req: &session.CreateSessionRequest{ + Checks: &session.Checks{ + User: &session.CheckUser{ + Search: &session.CheckUser_UserId{ + UserId: User.GetUserId(), + }, + }, + }, + Challenges: &session.RequestChallenges{ + WebAuthN: &session.RequestChallenges_WebAuthN{ + UserVerificationRequirement: session.UserVerificationRequirement_USER_VERIFICATION_REQUIREMENT_REQUIRED, + }, + }, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Client.CreateSession(CTX, tt.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + integration.AssertDetails(t, tt.want, got) + + verifyCurrentSession(t, got.GetSessionId(), got.GetSessionToken(), got.GetDetails().GetSequence(), time.Minute, tt.req.GetMetadata(), tt.wantUserAgent, tt.wantExpirationWindow, User.GetUserId(), tt.wantFactors...) + }) + } +} + +func TestServer_CreateSession_lock_user(t *testing.T) { + // create a separate org so we don't interfere with any other test + org := Tester.CreateOrganization(IAMOwnerCTX, + fmt.Sprintf("TestServer_CreateSession_lock_user_%d", time.Now().UnixNano()), + fmt.Sprintf("%d@mouse.com", time.Now().UnixNano()), + ) + userID := org.CreatedAdmins[0].GetUserId() + Tester.SetUserPassword(IAMOwnerCTX, userID, integration.UserPassword, false) + + // enable password lockout + maxAttempts := 2 + ctxOrg := metadata.AppendToOutgoingContext(IAMOwnerCTX, "x-zitadel-orgid", org.GetOrganizationId()) + _, err := Tester.Client.Mgmt.AddCustomLockoutPolicy(ctxOrg, &mgmt.AddCustomLockoutPolicyRequest{ + MaxPasswordAttempts: uint32(maxAttempts), + }) + require.NoError(t, err) + + for i := 0; i <= maxAttempts; i++ { + _, err := Client.CreateSession(CTX, &session.CreateSessionRequest{ + Checks: &session.Checks{ + User: &session.CheckUser{ + Search: &session.CheckUser_UserId{ + UserId: userID, + }, + }, + Password: &session.CheckPassword{ + Password: "invalid", + }, + }, + }) + assert.Error(t, err) + statusCode := status.Code(err) + expectedCode := codes.InvalidArgument + // as soon as we hit the limit the user is locked and following request will + // already deny any check with a precondition failed since the user is locked + if i >= maxAttempts { + expectedCode = codes.FailedPrecondition + } + assert.Equal(t, expectedCode, statusCode) + } +} + +func TestServer_CreateSession_webauthn(t *testing.T) { + // create new session with user and request the webauthn challenge + createResp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{ + Checks: &session.Checks{ + User: &session.CheckUser{ + Search: &session.CheckUser_UserId{ + UserId: User.GetUserId(), + }, + }, + }, + Challenges: &session.RequestChallenges{ + WebAuthN: &session.RequestChallenges_WebAuthN{ + Domain: Tester.Config.ExternalDomain, + UserVerificationRequirement: session.UserVerificationRequirement_USER_VERIFICATION_REQUIREMENT_REQUIRED, + }, + }, + }) + require.NoError(t, err) + verifyCurrentSession(t, createResp.GetSessionId(), createResp.GetSessionToken(), createResp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, User.GetUserId()) + + assertionData, err := Tester.WebAuthN.CreateAssertionResponse(createResp.GetChallenges().GetWebAuthN().GetPublicKeyCredentialRequestOptions(), true) + require.NoError(t, err) + + // update the session with webauthn assertion data + updateResp, err := Client.SetSession(CTX, &session.SetSessionRequest{ + SessionId: createResp.GetSessionId(), + Checks: &session.Checks{ + WebAuthN: &session.CheckWebAuthN{ + CredentialAssertionData: assertionData, + }, + }, + }) + require.NoError(t, err) + verifyCurrentSession(t, createResp.GetSessionId(), updateResp.GetSessionToken(), updateResp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, User.GetUserId(), wantUserFactor, wantWebAuthNFactorUserVerified) +} + +func TestServer_CreateSession_successfulIntent(t *testing.T) { + idpID := Tester.AddGenericOAuthProvider(t, CTX) + createResp, err := Client.CreateSession(CTX, &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, _, _ := Tester.CreateSuccessfulOAuthIntent(t, CTX, idpID, User.GetUserId(), "id") + updateResp, err := Client.SetSession(CTX, &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) +} + +func TestServer_CreateSession_successfulIntent_instant(t *testing.T) { + idpID := Tester.AddGenericOAuthProvider(t, CTX) + + intentID, token, _, _ := Tester.CreateSuccessfulOAuthIntent(t, CTX, idpID, User.GetUserId(), "id") + createResp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{ + Checks: &session.Checks{ + User: &session.CheckUser{ + Search: &session.CheckUser_UserId{ + UserId: User.GetUserId(), + }, + }, + IdpIntent: &session.CheckIDPIntent{ + IdpIntentId: intentID, + IdpIntentToken: token, + }, + }, + }) + require.NoError(t, err) + verifyCurrentSession(t, createResp.GetSessionId(), createResp.GetSessionToken(), createResp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, User.GetUserId(), wantUserFactor, wantIntentFactor) +} + +func TestServer_CreateSession_successfulIntentUnknownUserID(t *testing.T) { + idpID := Tester.AddGenericOAuthProvider(t, CTX) + + // successful intent without known / linked user + idpUserID := "id" + intentID, token, _, _ := Tester.CreateSuccessfulOAuthIntent(t, CTX, idpID, "", idpUserID) + + // link the user (with info from intent) + Tester.CreateUserIDPlink(CTX, User.GetUserId(), idpUserID, idpID, User.GetUserId()) + + // session with intent check must now succeed + createResp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{ + Checks: &session.Checks{ + User: &session.CheckUser{ + Search: &session.CheckUser_UserId{ + UserId: User.GetUserId(), + }, + }, + IdpIntent: &session.CheckIDPIntent{ + IdpIntentId: intentID, + IdpIntentToken: token, + }, + }, + }) + require.NoError(t, err) + verifyCurrentSession(t, createResp.GetSessionId(), createResp.GetSessionToken(), createResp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, User.GetUserId(), wantUserFactor, wantIntentFactor) +} + +func TestServer_CreateSession_startedIntentFalseToken(t *testing.T) { + idpID := Tester.AddGenericOAuthProvider(t, CTX) + + createResp, err := Client.CreateSession(CTX, &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 := Tester.CreateIntent(t, CTX, idpID) + _, err = Client.SetSession(CTX, &session.SetSessionRequest{ + SessionId: createResp.GetSessionId(), + Checks: &session.Checks{ + IdpIntent: &session.CheckIDPIntent{ + IdpIntentId: intentID, + IdpIntentToken: "false", + }, + }, + }) + require.Error(t, err) +} + +func registerTOTP(ctx context.Context, t *testing.T, userID string) (secret string) { + resp, err := Tester.Client.UserV2.RegisterTOTP(ctx, &user.RegisterTOTPRequest{ + UserId: userID, + }) + require.NoError(t, err) + secret = resp.GetSecret() + code, err := totp.GenerateCode(secret, time.Now()) + require.NoError(t, err) + + _, err = Tester.Client.UserV2.VerifyTOTPRegistration(ctx, &user.VerifyTOTPRegistrationRequest{ + UserId: userID, + Code: code, + }) + require.NoError(t, err) + return secret +} + +func registerOTPSMS(ctx context.Context, t *testing.T, userID string) { + _, err := Tester.Client.UserV2.AddOTPSMS(ctx, &user.AddOTPSMSRequest{ + UserId: userID, + }) + require.NoError(t, err) +} + +func registerOTPEmail(ctx context.Context, t *testing.T, userID string) { + _, err := Tester.Client.UserV2.AddOTPEmail(ctx, &user.AddOTPEmailRequest{ + UserId: userID, + }) + require.NoError(t, err) +} + +func TestServer_SetSession_flow_totp(t *testing.T) { + userExisting := createFullUser(CTX) + + // create new, empty session + createResp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{}) + require.NoError(t, err) + sessionToken := createResp.GetSessionToken() + verifyCurrentSession(t, createResp.GetSessionId(), sessionToken, createResp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, "") + + t.Run("check user", func(t *testing.T) { + resp, err := Client.SetSession(CTX, &session.SetSessionRequest{ + SessionId: createResp.GetSessionId(), + Checks: &session.Checks{ + User: &session.CheckUser{ + Search: &session.CheckUser_UserId{ + UserId: userExisting.GetUserId(), + }, + }, + }, + }) + require.NoError(t, err) + sessionToken = resp.GetSessionToken() + verifyCurrentSession(t, createResp.GetSessionId(), sessionToken, resp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, userExisting.GetUserId(), wantUserFactor) + }) + + t.Run("check webauthn, user verified (passkey)", func(t *testing.T) { + resp, err := Client.SetSession(CTX, &session.SetSessionRequest{ + SessionId: createResp.GetSessionId(), + Challenges: &session.RequestChallenges{ + WebAuthN: &session.RequestChallenges_WebAuthN{ + Domain: Tester.Config.ExternalDomain, + UserVerificationRequirement: session.UserVerificationRequirement_USER_VERIFICATION_REQUIREMENT_REQUIRED, + }, + }, + }) + require.NoError(t, err) + verifyCurrentSession(t, createResp.GetSessionId(), resp.GetSessionToken(), resp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, userExisting.GetUserId()) + sessionToken = resp.GetSessionToken() + + assertionData, err := Tester.WebAuthN.CreateAssertionResponse(resp.GetChallenges().GetWebAuthN().GetPublicKeyCredentialRequestOptions(), true) + require.NoError(t, err) + + resp, err = Client.SetSession(CTX, &session.SetSessionRequest{ + SessionId: createResp.GetSessionId(), + Checks: &session.Checks{ + WebAuthN: &session.CheckWebAuthN{ + CredentialAssertionData: assertionData, + }, + }, + }) + require.NoError(t, err) + sessionToken = resp.GetSessionToken() + verifyCurrentSession(t, createResp.GetSessionId(), sessionToken, resp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, userExisting.GetUserId(), wantUserFactor, wantWebAuthNFactorUserVerified) + }) + + userAuthCtx := Tester.WithAuthorizationToken(CTX, sessionToken) + Tester.RegisterUserU2F(userAuthCtx, userExisting.GetUserId()) + totpSecret := registerTOTP(userAuthCtx, t, userExisting.GetUserId()) + registerOTPSMS(userAuthCtx, t, userExisting.GetUserId()) + registerOTPEmail(userAuthCtx, t, userExisting.GetUserId()) + + t.Run("check TOTP", func(t *testing.T) { + code, err := totp.GenerateCode(totpSecret, time.Now()) + require.NoError(t, err) + resp, err := Client.SetSession(CTX, &session.SetSessionRequest{ + SessionId: createResp.GetSessionId(), + Checks: &session.Checks{ + Totp: &session.CheckTOTP{ + Code: code, + }, + }, + }) + require.NoError(t, err) + sessionToken = resp.GetSessionToken() + verifyCurrentSession(t, createResp.GetSessionId(), sessionToken, resp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, userExisting.GetUserId(), wantUserFactor, wantTOTPFactor) + }) + + userImport := Tester.CreateHumanUserWithTOTP(CTX, totpSecret) + createRespImport, err := Client.CreateSession(CTX, &session.CreateSessionRequest{}) + require.NoError(t, err) + sessionTokenImport := createRespImport.GetSessionToken() + verifyCurrentSession(t, createRespImport.GetSessionId(), sessionTokenImport, createRespImport.GetDetails().GetSequence(), time.Minute, nil, nil, 0, "") + + t.Run("check user", func(t *testing.T) { + resp, err := Client.SetSession(CTX, &session.SetSessionRequest{ + SessionId: createRespImport.GetSessionId(), + Checks: &session.Checks{ + User: &session.CheckUser{ + Search: &session.CheckUser_UserId{ + UserId: userImport.GetUserId(), + }, + }, + }, + }) + require.NoError(t, err) + sessionTokenImport = resp.GetSessionToken() + verifyCurrentSession(t, createRespImport.GetSessionId(), sessionTokenImport, resp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, userImport.GetUserId(), wantUserFactor) + }) + t.Run("check TOTP", func(t *testing.T) { + code, err := totp.GenerateCode(totpSecret, time.Now()) + require.NoError(t, err) + resp, err := Client.SetSession(CTX, &session.SetSessionRequest{ + SessionId: createRespImport.GetSessionId(), + Checks: &session.Checks{ + Totp: &session.CheckTOTP{ + Code: code, + }, + }, + }) + require.NoError(t, err) + sessionTokenImport = resp.GetSessionToken() + verifyCurrentSession(t, createRespImport.GetSessionId(), sessionTokenImport, resp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, userImport.GetUserId(), wantUserFactor, wantTOTPFactor) + }) +} + +func TestServer_SetSession_flow(t *testing.T) { + // create new, empty session + createResp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{}) + require.NoError(t, err) + sessionToken := createResp.GetSessionToken() + verifyCurrentSession(t, createResp.GetSessionId(), sessionToken, createResp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, User.GetUserId()) + + t.Run("check user", func(t *testing.T) { + resp, err := Client.SetSession(CTX, &session.SetSessionRequest{ + SessionId: createResp.GetSessionId(), + Checks: &session.Checks{ + User: &session.CheckUser{ + Search: &session.CheckUser_UserId{ + UserId: User.GetUserId(), + }, + }, + }, + }) + require.NoError(t, err) + sessionToken = resp.GetSessionToken() + verifyCurrentSession(t, createResp.GetSessionId(), sessionToken, resp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, User.GetUserId(), wantUserFactor) + }) + + t.Run("check webauthn, user verified (passkey)", func(t *testing.T) { + resp, err := Client.SetSession(CTX, &session.SetSessionRequest{ + SessionId: createResp.GetSessionId(), + Challenges: &session.RequestChallenges{ + WebAuthN: &session.RequestChallenges_WebAuthN{ + Domain: Tester.Config.ExternalDomain, + UserVerificationRequirement: session.UserVerificationRequirement_USER_VERIFICATION_REQUIREMENT_REQUIRED, + }, + }, + }) + require.NoError(t, err) + verifyCurrentSession(t, createResp.GetSessionId(), resp.GetSessionToken(), resp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, User.GetUserId()) + sessionToken = resp.GetSessionToken() + + assertionData, err := Tester.WebAuthN.CreateAssertionResponse(resp.GetChallenges().GetWebAuthN().GetPublicKeyCredentialRequestOptions(), true) + require.NoError(t, err) + + resp, err = Client.SetSession(CTX, &session.SetSessionRequest{ + SessionId: createResp.GetSessionId(), + Checks: &session.Checks{ + WebAuthN: &session.CheckWebAuthN{ + CredentialAssertionData: assertionData, + }, + }, + }) + require.NoError(t, err) + sessionToken = resp.GetSessionToken() + verifyCurrentSession(t, createResp.GetSessionId(), sessionToken, resp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, User.GetUserId(), wantUserFactor, wantWebAuthNFactorUserVerified) + }) + + userAuthCtx := Tester.WithAuthorizationToken(CTX, sessionToken) + Tester.RegisterUserU2F(userAuthCtx, User.GetUserId()) + totpSecret := registerTOTP(userAuthCtx, t, User.GetUserId()) + registerOTPSMS(userAuthCtx, t, User.GetUserId()) + registerOTPEmail(userAuthCtx, t, User.GetUserId()) + + t.Run("check webauthn, user not verified (U2F)", func(t *testing.T) { + + for _, userVerificationRequirement := range []session.UserVerificationRequirement{ + session.UserVerificationRequirement_USER_VERIFICATION_REQUIREMENT_PREFERRED, + session.UserVerificationRequirement_USER_VERIFICATION_REQUIREMENT_DISCOURAGED, + } { + t.Run(userVerificationRequirement.String(), func(t *testing.T) { + resp, err := Client.SetSession(CTX, &session.SetSessionRequest{ + SessionId: createResp.GetSessionId(), + Challenges: &session.RequestChallenges{ + WebAuthN: &session.RequestChallenges_WebAuthN{ + Domain: Tester.Config.ExternalDomain, + UserVerificationRequirement: userVerificationRequirement, + }, + }, + }) + require.NoError(t, err) + verifyCurrentSession(t, createResp.GetSessionId(), resp.GetSessionToken(), resp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, User.GetUserId()) + sessionToken = resp.GetSessionToken() + + assertionData, err := Tester.WebAuthN.CreateAssertionResponse(resp.GetChallenges().GetWebAuthN().GetPublicKeyCredentialRequestOptions(), false) + require.NoError(t, err) + + resp, err = Client.SetSession(CTX, &session.SetSessionRequest{ + SessionId: createResp.GetSessionId(), + Checks: &session.Checks{ + WebAuthN: &session.CheckWebAuthN{ + CredentialAssertionData: assertionData, + }, + }, + }) + require.NoError(t, err) + sessionToken = resp.GetSessionToken() + verifyCurrentSession(t, createResp.GetSessionId(), sessionToken, resp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, User.GetUserId(), wantUserFactor, wantWebAuthNFactor) + }) + } + }) + + t.Run("check TOTP", func(t *testing.T) { + code, err := totp.GenerateCode(totpSecret, time.Now()) + require.NoError(t, err) + resp, err := Client.SetSession(CTX, &session.SetSessionRequest{ + SessionId: createResp.GetSessionId(), + Checks: &session.Checks{ + Totp: &session.CheckTOTP{ + Code: code, + }, + }, + }) + require.NoError(t, err) + sessionToken = resp.GetSessionToken() + verifyCurrentSession(t, createResp.GetSessionId(), sessionToken, resp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, User.GetUserId(), wantUserFactor, wantWebAuthNFactor, wantTOTPFactor) + }) + + t.Run("check OTP SMS", func(t *testing.T) { + resp, err := Client.SetSession(CTX, &session.SetSessionRequest{ + SessionId: createResp.GetSessionId(), + Challenges: &session.RequestChallenges{ + OtpSms: &session.RequestChallenges_OTPSMS{ReturnCode: true}, + }, + }) + require.NoError(t, err) + verifyCurrentSession(t, createResp.GetSessionId(), resp.GetSessionToken(), resp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, User.GetUserId()) + sessionToken = resp.GetSessionToken() + + otp := resp.GetChallenges().GetOtpSms() + require.NotEmpty(t, otp) + + resp, err = Client.SetSession(CTX, &session.SetSessionRequest{ + SessionId: createResp.GetSessionId(), + Checks: &session.Checks{ + OtpSms: &session.CheckOTP{ + Code: otp, + }, + }, + }) + require.NoError(t, err) + sessionToken = resp.GetSessionToken() + verifyCurrentSession(t, createResp.GetSessionId(), sessionToken, resp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, User.GetUserId(), wantUserFactor, wantWebAuthNFactor, wantOTPSMSFactor) + }) + + t.Run("check OTP Email", func(t *testing.T) { + resp, err := Client.SetSession(CTX, &session.SetSessionRequest{ + SessionId: createResp.GetSessionId(), + Challenges: &session.RequestChallenges{ + OtpEmail: &session.RequestChallenges_OTPEmail{ + DeliveryType: &session.RequestChallenges_OTPEmail_ReturnCode_{}, + }, + }, + }) + require.NoError(t, err) + verifyCurrentSession(t, createResp.GetSessionId(), resp.GetSessionToken(), resp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, User.GetUserId()) + sessionToken = resp.GetSessionToken() + + otp := resp.GetChallenges().GetOtpEmail() + require.NotEmpty(t, otp) + + resp, err = Client.SetSession(CTX, &session.SetSessionRequest{ + SessionId: createResp.GetSessionId(), + Checks: &session.Checks{ + OtpEmail: &session.CheckOTP{ + Code: otp, + }, + }, + }) + require.NoError(t, err) + sessionToken = resp.GetSessionToken() + verifyCurrentSession(t, createResp.GetSessionId(), sessionToken, resp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, User.GetUserId(), wantUserFactor, wantWebAuthNFactor, wantOTPEmailFactor) + }) +} + +func TestServer_SetSession_expired(t *testing.T) { + createResp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{ + Lifetime: durationpb.New(20 * time.Second), + }) + require.NoError(t, err) + + // test session token works + _, err = Client.SetSession(CTX, &session.SetSessionRequest{ + SessionId: createResp.GetSessionId(), + Lifetime: durationpb.New(20 * time.Second), + }) + require.NoError(t, err) + + // ensure session expires and does not work anymore + time.Sleep(20 * time.Second) + _, err = Client.SetSession(CTX, &session.SetSessionRequest{ + SessionId: createResp.GetSessionId(), + Lifetime: durationpb.New(20 * time.Second), + }) + require.Error(t, err) +} + +func TestServer_DeleteSession_token(t *testing.T) { + createResp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{}) + require.NoError(t, err) + + _, err = Client.DeleteSession(CTX, &session.DeleteSessionRequest{ + SessionId: createResp.GetSessionId(), + SessionToken: gu.Ptr("invalid"), + }) + require.Error(t, err) + + _, err = Client.DeleteSession(CTX, &session.DeleteSessionRequest{ + SessionId: createResp.GetSessionId(), + SessionToken: gu.Ptr(createResp.GetSessionToken()), + }) + require.NoError(t, err) +} + +func TestServer_DeleteSession_own_session(t *testing.T) { + // create two users for the test and a session each to get tokens for authorization + user1 := Tester.CreateHumanUser(CTX) + Tester.SetUserPassword(CTX, user1.GetUserId(), integration.UserPassword, false) + _, token1, _, _ := Tester.CreatePasswordSession(t, CTX, user1.GetUserId(), integration.UserPassword) + + user2 := Tester.CreateHumanUser(CTX) + Tester.SetUserPassword(CTX, user2.GetUserId(), integration.UserPassword, false) + _, token2, _, _ := Tester.CreatePasswordSession(t, CTX, user2.GetUserId(), integration.UserPassword) + + // create a new session for the first user + createResp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{ + Checks: &session.Checks{ + User: &session.CheckUser{ + Search: &session.CheckUser_UserId{ + UserId: user1.GetUserId(), + }, + }, + }, + }) + require.NoError(t, err) + + // delete the new (user1) session must not be possible with user (has no permission) + _, err = Client.DeleteSession(Tester.WithAuthorizationToken(context.Background(), token2), &session.DeleteSessionRequest{ + SessionId: createResp.GetSessionId(), + }) + require.Error(t, err) + + // delete the new (user1) session by themselves + _, err = Client.DeleteSession(Tester.WithAuthorizationToken(context.Background(), token1), &session.DeleteSessionRequest{ + SessionId: createResp.GetSessionId(), + }) + require.NoError(t, err) +} + +func TestServer_DeleteSession_with_permission(t *testing.T) { + createResp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{ + Checks: &session.Checks{ + User: &session.CheckUser{ + Search: &session.CheckUser_UserId{ + UserId: User.GetUserId(), + }, + }, + }, + }) + require.NoError(t, err) + + // delete the new session by ORG_OWNER + _, err = Client.DeleteSession(Tester.WithAuthorization(context.Background(), integration.OrgOwner), &session.DeleteSessionRequest{ + SessionId: createResp.GetSessionId(), + }) + require.NoError(t, err) +} + +func Test_ZITADEL_API_missing_authentication(t *testing.T) { + // create new, empty session + createResp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{}) + require.NoError(t, err) + + ctx := metadata.AppendToOutgoingContext(context.Background(), "Authorization", fmt.Sprintf("Bearer %s", createResp.GetSessionToken())) + sessionResp, err := Client.GetSession(ctx, &session.GetSessionRequest{SessionId: createResp.GetSessionId()}) + require.Error(t, err) + require.Nil(t, sessionResp) +} + +func Test_ZITADEL_API_success(t *testing.T) { + id, token, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, User.GetUserId()) + + ctx := Tester.WithAuthorizationToken(context.Background(), token) + sessionResp, err := Client.GetSession(ctx, &session.GetSessionRequest{SessionId: id}) + require.NoError(t, err) + + webAuthN := sessionResp.GetSession().GetFactors().GetWebAuthN() + require.NotNil(t, id, webAuthN.GetVerifiedAt().AsTime()) + require.True(t, webAuthN.GetUserVerified()) +} + +func Test_ZITADEL_API_session_not_found(t *testing.T) { + id, token, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, User.GetUserId()) + + // test session token works + ctx := Tester.WithAuthorizationToken(context.Background(), token) + _, err := Client.GetSession(ctx, &session.GetSessionRequest{SessionId: id}) + require.NoError(t, err) + + //terminate the session and test it does not work anymore + _, err = Client.DeleteSession(CTX, &session.DeleteSessionRequest{ + SessionId: id, + SessionToken: gu.Ptr(token), + }) + require.NoError(t, err) + ctx = Tester.WithAuthorizationToken(context.Background(), token) + _, err = Client.GetSession(ctx, &session.GetSessionRequest{SessionId: id}) + require.Error(t, err) +} + +func Test_ZITADEL_API_session_expired(t *testing.T) { + id, token, _, _ := Tester.CreateVerifiedWebAuthNSessionWithLifetime(t, CTX, User.GetUserId(), 20*time.Second) + + // test session token works + ctx := Tester.WithAuthorizationToken(context.Background(), token) + _, err := Client.GetSession(ctx, &session.GetSessionRequest{SessionId: id}) + require.NoError(t, err) + + // ensure session expires and does not work anymore + time.Sleep(20 * time.Second) + sessionResp, err := Client.GetSession(ctx, &session.GetSessionRequest{SessionId: id}) + require.Error(t, err) + require.Nil(t, sessionResp) +} diff --git a/internal/api/grpc/session/v2beta/session_test.go b/internal/api/grpc/session/v2beta/session_test.go new file mode 100644 index 0000000000..c088b5b886 --- /dev/null +++ b/internal/api/grpc/session/v2beta/session_test.go @@ -0,0 +1,739 @@ +package session + +import ( + "context" + "net" + "net/http" + "testing" + "time" + + "github.com/muhlemmer/gu" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/query" + "github.com/zitadel/zitadel/internal/zerrors" + objpb "github.com/zitadel/zitadel/pkg/grpc/object" + object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta" + session "github.com/zitadel/zitadel/pkg/grpc/session/v2beta" +) + +var ( + creationDate = time.Date(2023, 10, 10, 14, 15, 0, 0, time.UTC) +) + +func Test_sessionsToPb(t *testing.T) { + now := time.Now() + past := now.Add(-time.Hour) + + sessions := []*query.Session{ + { // no factor, with user agent and expiration + ID: "999", + CreationDate: now, + ChangeDate: now, + Sequence: 123, + State: domain.SessionStateActive, + ResourceOwner: "me", + Creator: "he", + Metadata: map[string][]byte{"hello": []byte("world")}, + UserAgent: domain.UserAgent{ + FingerprintID: gu.Ptr("fingerprintID"), + Description: gu.Ptr("description"), + IP: net.IPv4(1, 2, 3, 4), + Header: http.Header{"foo": []string{"foo", "bar"}}, + }, + Expiration: now, + }, + { // user factor + ID: "999", + CreationDate: now, + ChangeDate: now, + Sequence: 123, + State: domain.SessionStateActive, + ResourceOwner: "me", + Creator: "he", + UserFactor: query.SessionUserFactor{ + UserID: "345", + UserCheckedAt: past, + LoginName: "donald", + DisplayName: "donald duck", + ResourceOwner: "org1", + }, + Metadata: map[string][]byte{"hello": []byte("world")}, + }, + { // password factor + ID: "999", + CreationDate: now, + ChangeDate: now, + Sequence: 123, + State: domain.SessionStateActive, + ResourceOwner: "me", + Creator: "he", + UserFactor: query.SessionUserFactor{ + UserID: "345", + UserCheckedAt: past, + LoginName: "donald", + DisplayName: "donald duck", + ResourceOwner: "org1", + }, + PasswordFactor: query.SessionPasswordFactor{ + PasswordCheckedAt: past, + }, + Metadata: map[string][]byte{"hello": []byte("world")}, + }, + { // webAuthN factor + ID: "999", + CreationDate: now, + ChangeDate: now, + Sequence: 123, + State: domain.SessionStateActive, + ResourceOwner: "me", + Creator: "he", + UserFactor: query.SessionUserFactor{ + UserID: "345", + UserCheckedAt: past, + LoginName: "donald", + DisplayName: "donald duck", + ResourceOwner: "org1", + }, + WebAuthNFactor: query.SessionWebAuthNFactor{ + WebAuthNCheckedAt: past, + UserVerified: true, + }, + Metadata: map[string][]byte{"hello": []byte("world")}, + }, + { // totp factor + ID: "999", + CreationDate: now, + ChangeDate: now, + Sequence: 123, + State: domain.SessionStateActive, + ResourceOwner: "me", + Creator: "he", + UserFactor: query.SessionUserFactor{ + UserID: "345", + UserCheckedAt: past, + LoginName: "donald", + DisplayName: "donald duck", + ResourceOwner: "org1", + }, + TOTPFactor: query.SessionTOTPFactor{ + TOTPCheckedAt: past, + }, + Metadata: map[string][]byte{"hello": []byte("world")}, + }, + } + + want := []*session.Session{ + { // no factor, with user agent and expiration + Id: "999", + CreationDate: timestamppb.New(now), + ChangeDate: timestamppb.New(now), + Sequence: 123, + Factors: nil, + Metadata: map[string][]byte{"hello": []byte("world")}, + UserAgent: &session.UserAgent{ + FingerprintId: gu.Ptr("fingerprintID"), + Description: gu.Ptr("description"), + Ip: gu.Ptr("1.2.3.4"), + Header: map[string]*session.UserAgent_HeaderValues{ + "foo": {Values: []string{"foo", "bar"}}, + }, + }, + ExpirationDate: timestamppb.New(now), + }, + { // user factor + Id: "999", + CreationDate: timestamppb.New(now), + ChangeDate: timestamppb.New(now), + Sequence: 123, + Factors: &session.Factors{ + User: &session.UserFactor{ + VerifiedAt: timestamppb.New(past), + Id: "345", + LoginName: "donald", + DisplayName: "donald duck", + OrganizationId: "org1", + }, + }, + Metadata: map[string][]byte{"hello": []byte("world")}, + }, + { // password factor + Id: "999", + CreationDate: timestamppb.New(now), + ChangeDate: timestamppb.New(now), + Sequence: 123, + Factors: &session.Factors{ + User: &session.UserFactor{ + VerifiedAt: timestamppb.New(past), + Id: "345", + LoginName: "donald", + DisplayName: "donald duck", + OrganizationId: "org1", + }, + Password: &session.PasswordFactor{ + VerifiedAt: timestamppb.New(past), + }, + }, + Metadata: map[string][]byte{"hello": []byte("world")}, + }, + { // webAuthN factor + Id: "999", + CreationDate: timestamppb.New(now), + ChangeDate: timestamppb.New(now), + Sequence: 123, + Factors: &session.Factors{ + User: &session.UserFactor{ + VerifiedAt: timestamppb.New(past), + Id: "345", + LoginName: "donald", + DisplayName: "donald duck", + OrganizationId: "org1", + }, + WebAuthN: &session.WebAuthNFactor{ + VerifiedAt: timestamppb.New(past), + UserVerified: true, + }, + }, + Metadata: map[string][]byte{"hello": []byte("world")}, + }, + { // totp factor + Id: "999", + CreationDate: timestamppb.New(now), + ChangeDate: timestamppb.New(now), + Sequence: 123, + Factors: &session.Factors{ + User: &session.UserFactor{ + VerifiedAt: timestamppb.New(past), + Id: "345", + LoginName: "donald", + DisplayName: "donald duck", + OrganizationId: "org1", + }, + Totp: &session.TOTPFactor{ + VerifiedAt: timestamppb.New(past), + }, + }, + Metadata: map[string][]byte{"hello": []byte("world")}, + }, + } + + out := sessionsToPb(sessions) + require.Len(t, out, len(want)) + + for i, got := range out { + if !proto.Equal(got, want[i]) { + t.Errorf("session %d got:\n%v\nwant:\n%v", i, got, want[i]) + } + } +} + +func Test_userAgentToPb(t *testing.T) { + type args struct { + ua domain.UserAgent + } + tests := []struct { + name string + args args + want *session.UserAgent + }{ + { + name: "empty", + args: args{domain.UserAgent{}}, + }, + { + name: "fingerprint id and description", + args: args{domain.UserAgent{ + FingerprintID: gu.Ptr("fingerPrintID"), + Description: gu.Ptr("description"), + }}, + want: &session.UserAgent{ + FingerprintId: gu.Ptr("fingerPrintID"), + Description: gu.Ptr("description"), + }, + }, + { + name: "with ip", + args: args{domain.UserAgent{ + FingerprintID: gu.Ptr("fingerPrintID"), + Description: gu.Ptr("description"), + IP: net.IPv4(1, 2, 3, 4), + }}, + want: &session.UserAgent{ + FingerprintId: gu.Ptr("fingerPrintID"), + Description: gu.Ptr("description"), + Ip: gu.Ptr("1.2.3.4"), + }, + }, + { + name: "with header", + args: args{domain.UserAgent{ + FingerprintID: gu.Ptr("fingerPrintID"), + Description: gu.Ptr("description"), + Header: http.Header{ + "foo": []string{"foo", "bar"}, + "hello": []string{"world"}, + }, + }}, + want: &session.UserAgent{ + FingerprintId: gu.Ptr("fingerPrintID"), + Description: gu.Ptr("description"), + Header: map[string]*session.UserAgent_HeaderValues{ + "foo": {Values: []string{"foo", "bar"}}, + "hello": {Values: []string{"world"}}, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := userAgentToPb(tt.args.ua) + assert.Equal(t, tt.want, got) + }) + } +} + +func mustNewTextQuery(t testing.TB, column query.Column, value string, compare query.TextComparison) query.SearchQuery { + q, err := query.NewTextQuery(column, value, compare) + require.NoError(t, err) + return q +} + +func mustNewListQuery(t testing.TB, column query.Column, list []any, compare query.ListComparison) query.SearchQuery { + q, err := query.NewListQuery(query.SessionColumnID, list, compare) + require.NoError(t, err) + return q +} + +func mustNewTimestampQuery(t testing.TB, column query.Column, ts time.Time, compare query.TimestampComparison) query.SearchQuery { + q, err := query.NewTimestampQuery(column, ts, compare) + require.NoError(t, err) + return q +} + +func Test_listSessionsRequestToQuery(t *testing.T) { + type args struct { + ctx context.Context + req *session.ListSessionsRequest + } + + tests := []struct { + name string + args args + want *query.SessionsSearchQueries + wantErr error + }{ + { + name: "default request", + args: args{ + ctx: authz.NewMockContext("123", "456", "789"), + req: &session.ListSessionsRequest{}, + }, + want: &query.SessionsSearchQueries{ + SearchRequest: query.SearchRequest{ + Offset: 0, + Limit: 0, + Asc: false, + }, + Queries: []query.SearchQuery{ + mustNewTextQuery(t, query.SessionColumnCreator, "789", query.TextEquals), + }, + }, + }, + { + name: "default request with sorting column", + args: args{ + ctx: authz.NewMockContext("123", "456", "789"), + req: &session.ListSessionsRequest{ + SortingColumn: session.SessionFieldName_SESSION_FIELD_NAME_CREATION_DATE, + }, + }, + want: &query.SessionsSearchQueries{ + SearchRequest: query.SearchRequest{ + Offset: 0, + Limit: 0, + SortingColumn: query.SessionColumnCreationDate, + Asc: false, + }, + Queries: []query.SearchQuery{ + mustNewTextQuery(t, query.SessionColumnCreator, "789", query.TextEquals), + }, + }, + }, + { + name: "with list query and sessions", + args: args{ + ctx: authz.NewMockContext("123", "456", "789"), + req: &session.ListSessionsRequest{ + Query: &object.ListQuery{ + Offset: 10, + Limit: 20, + Asc: true, + }, + Queries: []*session.SearchQuery{ + {Query: &session.SearchQuery_IdsQuery{ + IdsQuery: &session.IDsQuery{ + Ids: []string{"1", "2", "3"}, + }, + }}, + {Query: &session.SearchQuery_IdsQuery{ + IdsQuery: &session.IDsQuery{ + Ids: []string{"4", "5", "6"}, + }, + }}, + {Query: &session.SearchQuery_UserIdQuery{ + UserIdQuery: &session.UserIDQuery{ + Id: "10", + }, + }}, + {Query: &session.SearchQuery_CreationDateQuery{ + CreationDateQuery: &session.CreationDateQuery{ + CreationDate: timestamppb.New(creationDate), + Method: objpb.TimestampQueryMethod_TIMESTAMP_QUERY_METHOD_GREATER, + }, + }}, + }, + }, + }, + want: &query.SessionsSearchQueries{ + SearchRequest: query.SearchRequest{ + Offset: 10, + Limit: 20, + Asc: true, + }, + Queries: []query.SearchQuery{ + mustNewListQuery(t, query.SessionColumnID, []interface{}{"1", "2", "3"}, query.ListIn), + mustNewListQuery(t, query.SessionColumnID, []interface{}{"4", "5", "6"}, query.ListIn), + mustNewTextQuery(t, query.SessionColumnUserID, "10", query.TextEquals), + mustNewTimestampQuery(t, query.SessionColumnCreationDate, creationDate, query.TimestampGreater), + mustNewTextQuery(t, query.SessionColumnCreator, "789", query.TextEquals), + }, + }, + }, + { + name: "invalid argument error", + args: args{ + ctx: authz.NewMockContext("123", "456", "789"), + req: &session.ListSessionsRequest{ + Query: &object.ListQuery{ + Offset: 10, + Limit: 20, + Asc: true, + }, + Queries: []*session.SearchQuery{ + {Query: &session.SearchQuery_IdsQuery{ + IdsQuery: &session.IDsQuery{ + Ids: []string{"1", "2", "3"}, + }, + }}, + {Query: nil}, + }, + }, + }, + wantErr: zerrors.ThrowInvalidArgument(nil, "GRPC-Sfefs", "List.Query.Invalid"), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := listSessionsRequestToQuery(tt.args.ctx, tt.args.req) + require.ErrorIs(t, err, tt.wantErr) + assert.Equal(t, tt.want, got) + }) + } +} + +func Test_sessionQueriesToQuery(t *testing.T) { + type args struct { + ctx context.Context + queries []*session.SearchQuery + } + tests := []struct { + name string + args args + want []query.SearchQuery + wantErr error + }{ + { + name: "creator only", + args: args{ + ctx: authz.NewMockContext("123", "456", "789"), + }, + want: []query.SearchQuery{ + mustNewTextQuery(t, query.SessionColumnCreator, "789", query.TextEquals), + }, + }, + { + name: "invalid argument", + args: args{ + ctx: authz.NewMockContext("123", "456", "789"), + queries: []*session.SearchQuery{ + {Query: nil}, + }, + }, + wantErr: zerrors.ThrowInvalidArgument(nil, "GRPC-Sfefs", "List.Query.Invalid"), + }, + { + name: "creator and sessions", + args: args{ + ctx: authz.NewMockContext("123", "456", "789"), + queries: []*session.SearchQuery{ + {Query: &session.SearchQuery_IdsQuery{ + IdsQuery: &session.IDsQuery{ + Ids: []string{"1", "2", "3"}, + }, + }}, + {Query: &session.SearchQuery_IdsQuery{ + IdsQuery: &session.IDsQuery{ + Ids: []string{"4", "5", "6"}, + }, + }}, + }, + }, + want: []query.SearchQuery{ + mustNewListQuery(t, query.SessionColumnID, []interface{}{"1", "2", "3"}, query.ListIn), + mustNewListQuery(t, query.SessionColumnID, []interface{}{"4", "5", "6"}, query.ListIn), + mustNewTextQuery(t, query.SessionColumnCreator, "789", query.TextEquals), + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := sessionQueriesToQuery(tt.args.ctx, tt.args.queries) + require.ErrorIs(t, err, tt.wantErr) + assert.Equal(t, tt.want, got) + }) + } +} + +func Test_sessionQueryToQuery(t *testing.T) { + type args struct { + query *session.SearchQuery + } + tests := []struct { + name string + args args + want query.SearchQuery + wantErr error + }{ + { + name: "invalid argument", + args: args{&session.SearchQuery{ + Query: nil, + }}, + wantErr: zerrors.ThrowInvalidArgument(nil, "GRPC-Sfefs", "List.Query.Invalid"), + }, + { + name: "ids query", + args: args{&session.SearchQuery{ + Query: &session.SearchQuery_IdsQuery{ + IdsQuery: &session.IDsQuery{ + Ids: []string{"1", "2", "3"}, + }, + }, + }}, + want: mustNewListQuery(t, query.SessionColumnID, []interface{}{"1", "2", "3"}, query.ListIn), + }, + { + name: "user id query", + args: args{&session.SearchQuery{ + Query: &session.SearchQuery_UserIdQuery{ + UserIdQuery: &session.UserIDQuery{ + Id: "10", + }, + }, + }}, + want: mustNewTextQuery(t, query.SessionColumnUserID, "10", query.TextEquals), + }, + { + name: "creation date query", + args: args{&session.SearchQuery{ + Query: &session.SearchQuery_CreationDateQuery{ + CreationDateQuery: &session.CreationDateQuery{ + CreationDate: timestamppb.New(creationDate), + Method: objpb.TimestampQueryMethod_TIMESTAMP_QUERY_METHOD_LESS, + }, + }, + }}, + want: mustNewTimestampQuery(t, query.SessionColumnCreationDate, creationDate, query.TimestampLess), + }, + { + name: "creation date query with default method", + args: args{&session.SearchQuery{ + Query: &session.SearchQuery_CreationDateQuery{ + CreationDateQuery: &session.CreationDateQuery{ + CreationDate: timestamppb.New(creationDate), + }, + }, + }}, + want: mustNewTimestampQuery(t, query.SessionColumnCreationDate, creationDate, query.TimestampEquals), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := sessionQueryToQuery(tt.args.query) + require.ErrorIs(t, err, tt.wantErr) + assert.Equal(t, tt.want, got) + }) + } +} + +func Test_userCheck(t *testing.T) { + type args struct { + user *session.CheckUser + } + tests := []struct { + name string + args args + want userSearch + wantErr error + }{ + { + name: "nil user", + args: args{nil}, + want: nil, + }, + { + name: "by user id", + args: args{&session.CheckUser{ + Search: &session.CheckUser_UserId{ + UserId: "foo", + }, + }}, + want: userSearchByID{"foo"}, + }, + { + name: "by user id", + args: args{&session.CheckUser{ + Search: &session.CheckUser_LoginName{ + LoginName: "bar", + }, + }}, + want: userSearchByLoginName{"bar"}, + }, + { + name: "unimplemented error", + args: args{&session.CheckUser{ + Search: nil, + }}, + wantErr: zerrors.ThrowUnimplementedf(nil, "SESSION-d3b4g0", "user search %T not implemented", nil), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := userCheck(tt.args.user) + require.ErrorIs(t, err, tt.wantErr) + assert.Equal(t, tt.want, got) + }) + } +} + +func Test_userVerificationRequirementToDomain(t *testing.T) { + type args struct { + req session.UserVerificationRequirement + } + tests := []struct { + args args + want domain.UserVerificationRequirement + }{ + { + args: args{session.UserVerificationRequirement_USER_VERIFICATION_REQUIREMENT_UNSPECIFIED}, + want: domain.UserVerificationRequirementUnspecified, + }, + { + args: args{session.UserVerificationRequirement_USER_VERIFICATION_REQUIREMENT_REQUIRED}, + want: domain.UserVerificationRequirementRequired, + }, + { + args: args{session.UserVerificationRequirement_USER_VERIFICATION_REQUIREMENT_PREFERRED}, + want: domain.UserVerificationRequirementPreferred, + }, + { + args: args{session.UserVerificationRequirement_USER_VERIFICATION_REQUIREMENT_DISCOURAGED}, + want: domain.UserVerificationRequirementDiscouraged, + }, + { + args: args{999}, + want: domain.UserVerificationRequirementUnspecified, + }, + } + for _, tt := range tests { + t.Run(tt.args.req.String(), func(t *testing.T) { + got := userVerificationRequirementToDomain(tt.args.req) + assert.Equal(t, tt.want, got) + }) + } +} + +func Test_userAgentToCommand(t *testing.T) { + type args struct { + userAgent *session.UserAgent + } + tests := []struct { + name string + args args + want *domain.UserAgent + }{ + { + name: "nil", + args: args{nil}, + want: nil, + }, + { + name: "all fields", + args: args{&session.UserAgent{ + FingerprintId: gu.Ptr("fp1"), + Ip: gu.Ptr("1.2.3.4"), + Description: gu.Ptr("firefox"), + Header: map[string]*session.UserAgent_HeaderValues{ + "hello": { + Values: []string{"foo", "bar"}, + }, + }, + }}, + want: &domain.UserAgent{ + FingerprintID: gu.Ptr("fp1"), + IP: net.ParseIP("1.2.3.4"), + Description: gu.Ptr("firefox"), + Header: http.Header{ + "hello": []string{"foo", "bar"}, + }, + }, + }, + { + name: "invalid ip", + args: args{&session.UserAgent{ + FingerprintId: gu.Ptr("fp1"), + Ip: gu.Ptr("oops"), + Description: gu.Ptr("firefox"), + Header: map[string]*session.UserAgent_HeaderValues{ + "hello": { + Values: []string{"foo", "bar"}, + }, + }, + }}, + want: &domain.UserAgent{ + FingerprintID: gu.Ptr("fp1"), + IP: nil, + Description: gu.Ptr("firefox"), + Header: http.Header{ + "hello": []string{"foo", "bar"}, + }, + }, + }, + { + name: "nil fields", + args: args{&session.UserAgent{}}, + want: &domain.UserAgent{}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := userAgentToCommand(tt.args.userAgent) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/internal/api/grpc/settings/v2/server.go b/internal/api/grpc/settings/v2/server.go index f001549595..0391d01188 100644 --- a/internal/api/grpc/settings/v2/server.go +++ b/internal/api/grpc/settings/v2/server.go @@ -10,7 +10,7 @@ import ( "github.com/zitadel/zitadel/internal/api/grpc/server" "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/query" - settings "github.com/zitadel/zitadel/pkg/grpc/settings/v2beta" + "github.com/zitadel/zitadel/pkg/grpc/settings/v2" ) var _ settings.SettingsServiceServer = (*Server)(nil) diff --git a/internal/api/grpc/settings/v2/server_integration_test.go b/internal/api/grpc/settings/v2/server_integration_test.go index 703e8cb971..611194a5ce 100644 --- a/internal/api/grpc/settings/v2/server_integration_test.go +++ b/internal/api/grpc/settings/v2/server_integration_test.go @@ -9,7 +9,7 @@ import ( "time" "github.com/zitadel/zitadel/internal/integration" - settings "github.com/zitadel/zitadel/pkg/grpc/settings/v2beta" + "github.com/zitadel/zitadel/pkg/grpc/settings/v2" ) var ( diff --git a/internal/api/grpc/settings/v2/settings.go b/internal/api/grpc/settings/v2/settings.go index 9b6546645a..3e48ab0c04 100644 --- a/internal/api/grpc/settings/v2/settings.go +++ b/internal/api/grpc/settings/v2/settings.go @@ -10,8 +10,8 @@ import ( "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/i18n" "github.com/zitadel/zitadel/internal/query" - object_pb "github.com/zitadel/zitadel/pkg/grpc/object/v2beta" - settings "github.com/zitadel/zitadel/pkg/grpc/settings/v2beta" + object_pb "github.com/zitadel/zitadel/pkg/grpc/object/v2" + "github.com/zitadel/zitadel/pkg/grpc/settings/v2" ) func (s *Server) GetLoginSettings(ctx context.Context, req *settings.GetLoginSettingsRequest) (*settings.GetLoginSettingsResponse, error) { diff --git a/internal/api/grpc/settings/v2/settings_converter.go b/internal/api/grpc/settings/v2/settings_converter.go index 912df689aa..848ea3e14a 100644 --- a/internal/api/grpc/settings/v2/settings_converter.go +++ b/internal/api/grpc/settings/v2/settings_converter.go @@ -8,7 +8,7 @@ import ( "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/query" - settings "github.com/zitadel/zitadel/pkg/grpc/settings/v2beta" + "github.com/zitadel/zitadel/pkg/grpc/settings/v2" ) func loginSettingsToPb(current *query.LoginPolicy) *settings.LoginSettings { diff --git a/internal/api/grpc/settings/v2/settings_converter_test.go b/internal/api/grpc/settings/v2/settings_converter_test.go index 99c60f2628..75785c47b8 100644 --- a/internal/api/grpc/settings/v2/settings_converter_test.go +++ b/internal/api/grpc/settings/v2/settings_converter_test.go @@ -16,7 +16,7 @@ import ( "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/query" - settings "github.com/zitadel/zitadel/pkg/grpc/settings/v2beta" + "github.com/zitadel/zitadel/pkg/grpc/settings/v2" ) var ignoreTypes = []protoreflect.FullName{"google.protobuf.Duration"} diff --git a/internal/api/grpc/settings/v2/settings_integration_test.go b/internal/api/grpc/settings/v2/settings_integration_test.go index 3accc0d63f..c9f5f7bd8f 100644 --- a/internal/api/grpc/settings/v2/settings_integration_test.go +++ b/internal/api/grpc/settings/v2/settings_integration_test.go @@ -11,8 +11,8 @@ import ( "google.golang.org/protobuf/types/known/timestamppb" "github.com/zitadel/zitadel/internal/integration" - object_pb "github.com/zitadel/zitadel/pkg/grpc/object/v2beta" - settings "github.com/zitadel/zitadel/pkg/grpc/settings/v2beta" + object_pb "github.com/zitadel/zitadel/pkg/grpc/object/v2" + "github.com/zitadel/zitadel/pkg/grpc/settings/v2" ) func TestServer_GetSecuritySettings(t *testing.T) { diff --git a/internal/api/grpc/settings/v2beta/server.go b/internal/api/grpc/settings/v2beta/server.go new file mode 100644 index 0000000000..f001549595 --- /dev/null +++ b/internal/api/grpc/settings/v2beta/server.go @@ -0,0 +1,57 @@ +package settings + +import ( + "context" + + "google.golang.org/grpc" + + "github.com/zitadel/zitadel/internal/api/assets" + "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/query" + settings "github.com/zitadel/zitadel/pkg/grpc/settings/v2beta" +) + +var _ settings.SettingsServiceServer = (*Server)(nil) + +type Server struct { + settings.UnimplementedSettingsServiceServer + command *command.Commands + query *query.Queries + assetsAPIDomain func(context.Context) string +} + +type Config struct{} + +func CreateServer( + command *command.Commands, + query *query.Queries, + externalSecure bool, +) *Server { + return &Server{ + command: command, + query: query, + assetsAPIDomain: assets.AssetAPI(externalSecure), + } +} + +func (s *Server) RegisterServer(grpcServer *grpc.Server) { + settings.RegisterSettingsServiceServer(grpcServer, s) +} + +func (s *Server) AppName() string { + return settings.SettingsService_ServiceDesc.ServiceName +} + +func (s *Server) MethodPrefix() string { + return settings.SettingsService_ServiceDesc.ServiceName +} + +func (s *Server) AuthMethods() authz.MethodMapping { + return settings.SettingsService_AuthMethods +} + +func (s *Server) RegisterGateway() server.RegisterGatewayFunc { + return settings.RegisterSettingsServiceHandler +} diff --git a/internal/api/grpc/settings/v2beta/server_integration_test.go b/internal/api/grpc/settings/v2beta/server_integration_test.go new file mode 100644 index 0000000000..34afe5733d --- /dev/null +++ b/internal/api/grpc/settings/v2beta/server_integration_test.go @@ -0,0 +1,34 @@ +//go:build integration + +package settings_test + +import ( + "context" + "os" + "testing" + "time" + + "github.com/zitadel/zitadel/internal/integration" + settings "github.com/zitadel/zitadel/pkg/grpc/settings/v2beta" +) + +var ( + CTX, AdminCTX context.Context + Tester *integration.Tester + Client settings.SettingsServiceClient +) + +func TestMain(m *testing.M) { + os.Exit(func() int { + ctx, _, cancel := integration.Contexts(3 * time.Minute) + defer cancel() + + Tester = integration.NewTester(ctx) + defer Tester.Done() + + CTX = ctx + AdminCTX = Tester.WithAuthorization(ctx, integration.IAMOwner) + Client = Tester.Client.SettingsV2beta + return m.Run() + }()) +} diff --git a/internal/api/grpc/settings/v2beta/settings.go b/internal/api/grpc/settings/v2beta/settings.go new file mode 100644 index 0000000000..677d8f1c15 --- /dev/null +++ b/internal/api/grpc/settings/v2beta/settings.go @@ -0,0 +1,161 @@ +package settings + +import ( + "context" + + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/api/authz" + object "github.com/zitadel/zitadel/internal/api/grpc/object/v2beta" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/i18n" + "github.com/zitadel/zitadel/internal/query" + object_pb "github.com/zitadel/zitadel/pkg/grpc/object/v2beta" + settings "github.com/zitadel/zitadel/pkg/grpc/settings/v2beta" +) + +func (s *Server) GetLoginSettings(ctx context.Context, req *settings.GetLoginSettingsRequest) (*settings.GetLoginSettingsResponse, error) { + current, err := s.query.LoginPolicyByID(ctx, true, object.ResourceOwnerFromReq(ctx, req.GetCtx()), false) + if err != nil { + return nil, err + } + return &settings.GetLoginSettingsResponse{ + Settings: loginSettingsToPb(current), + Details: &object_pb.Details{ + Sequence: current.Sequence, + ChangeDate: timestamppb.New(current.ChangeDate), + ResourceOwner: current.OrgID, + }, + }, nil +} + +func (s *Server) GetPasswordComplexitySettings(ctx context.Context, req *settings.GetPasswordComplexitySettingsRequest) (*settings.GetPasswordComplexitySettingsResponse, error) { + current, err := s.query.PasswordComplexityPolicyByOrg(ctx, true, object.ResourceOwnerFromReq(ctx, req.GetCtx()), false) + if err != nil { + return nil, err + } + return &settings.GetPasswordComplexitySettingsResponse{ + Settings: passwordComplexitySettingsToPb(current), + Details: &object_pb.Details{ + Sequence: current.Sequence, + ChangeDate: timestamppb.New(current.ChangeDate), + ResourceOwner: current.ResourceOwner, + }, + }, nil +} + +func (s *Server) GetPasswordExpirySettings(ctx context.Context, req *settings.GetPasswordExpirySettingsRequest) (*settings.GetPasswordExpirySettingsResponse, error) { + current, err := s.query.PasswordAgePolicyByOrg(ctx, true, object.ResourceOwnerFromReq(ctx, req.GetCtx()), false) + if err != nil { + return nil, err + } + return &settings.GetPasswordExpirySettingsResponse{ + Settings: passwordExpirySettingsToPb(current), + Details: &object_pb.Details{ + Sequence: current.Sequence, + ChangeDate: timestamppb.New(current.ChangeDate), + ResourceOwner: current.ResourceOwner, + }, + }, nil +} + +func (s *Server) GetBrandingSettings(ctx context.Context, req *settings.GetBrandingSettingsRequest) (*settings.GetBrandingSettingsResponse, error) { + current, err := s.query.ActiveLabelPolicyByOrg(ctx, object.ResourceOwnerFromReq(ctx, req.GetCtx()), false) + if err != nil { + return nil, err + } + return &settings.GetBrandingSettingsResponse{ + Settings: brandingSettingsToPb(current, s.assetsAPIDomain(ctx)), + Details: &object_pb.Details{ + Sequence: current.Sequence, + ChangeDate: timestamppb.New(current.ChangeDate), + ResourceOwner: current.ResourceOwner, + }, + }, nil +} + +func (s *Server) GetDomainSettings(ctx context.Context, req *settings.GetDomainSettingsRequest) (*settings.GetDomainSettingsResponse, error) { + current, err := s.query.DomainPolicyByOrg(ctx, true, object.ResourceOwnerFromReq(ctx, req.GetCtx()), false) + if err != nil { + return nil, err + } + return &settings.GetDomainSettingsResponse{ + Settings: domainSettingsToPb(current), + Details: &object_pb.Details{ + Sequence: current.Sequence, + ChangeDate: timestamppb.New(current.ChangeDate), + ResourceOwner: current.ResourceOwner, + }, + }, nil +} + +func (s *Server) GetLegalAndSupportSettings(ctx context.Context, req *settings.GetLegalAndSupportSettingsRequest) (*settings.GetLegalAndSupportSettingsResponse, error) { + current, err := s.query.PrivacyPolicyByOrg(ctx, true, object.ResourceOwnerFromReq(ctx, req.GetCtx()), false) + if err != nil { + return nil, err + } + return &settings.GetLegalAndSupportSettingsResponse{ + Settings: legalAndSupportSettingsToPb(current), + Details: &object_pb.Details{ + Sequence: current.Sequence, + ChangeDate: timestamppb.New(current.ChangeDate), + ResourceOwner: current.ResourceOwner, + }, + }, nil +} + +func (s *Server) GetLockoutSettings(ctx context.Context, req *settings.GetLockoutSettingsRequest) (*settings.GetLockoutSettingsResponse, error) { + current, err := s.query.LockoutPolicyByOrg(ctx, true, object.ResourceOwnerFromReq(ctx, req.GetCtx())) + if err != nil { + return nil, err + } + return &settings.GetLockoutSettingsResponse{ + Settings: lockoutSettingsToPb(current), + Details: &object_pb.Details{ + Sequence: current.Sequence, + ChangeDate: timestamppb.New(current.ChangeDate), + ResourceOwner: current.ResourceOwner, + }, + }, nil +} + +func (s *Server) GetActiveIdentityProviders(ctx context.Context, req *settings.GetActiveIdentityProvidersRequest) (*settings.GetActiveIdentityProvidersResponse, error) { + links, err := s.query.IDPLoginPolicyLinks(ctx, object.ResourceOwnerFromReq(ctx, req.GetCtx()), &query.IDPLoginPolicyLinksSearchQuery{}, false) + if err != nil { + return nil, err + } + + return &settings.GetActiveIdentityProvidersResponse{ + Details: object.ToListDetails(links.SearchResponse), + IdentityProviders: identityProvidersToPb(links.Links), + }, nil +} + +func (s *Server) GetGeneralSettings(ctx context.Context, _ *settings.GetGeneralSettingsRequest) (*settings.GetGeneralSettingsResponse, error) { + instance := authz.GetInstance(ctx) + return &settings.GetGeneralSettingsResponse{ + SupportedLanguages: domain.LanguagesToStrings(i18n.SupportedLanguages()), + DefaultOrgId: instance.DefaultOrganisationID(), + DefaultLanguage: instance.DefaultLanguage().String(), + }, nil +} + +func (s *Server) GetSecuritySettings(ctx context.Context, req *settings.GetSecuritySettingsRequest) (*settings.GetSecuritySettingsResponse, error) { + policy, err := s.query.SecurityPolicy(ctx) + if err != nil { + return nil, err + } + return &settings.GetSecuritySettingsResponse{ + Settings: securityPolicyToSettingsPb(policy), + }, nil +} + +func (s *Server) SetSecuritySettings(ctx context.Context, req *settings.SetSecuritySettingsRequest) (*settings.SetSecuritySettingsResponse, error) { + details, err := s.command.SetSecurityPolicy(ctx, securitySettingsToCommand(req)) + if err != nil { + return nil, err + } + return &settings.SetSecuritySettingsResponse{ + Details: object.DomainToDetailsPb(details), + }, nil +} diff --git a/internal/api/grpc/settings/v2beta/settings_converter.go b/internal/api/grpc/settings/v2beta/settings_converter.go new file mode 100644 index 0000000000..2b20e738e1 --- /dev/null +++ b/internal/api/grpc/settings/v2beta/settings_converter.go @@ -0,0 +1,245 @@ +package settings + +import ( + "time" + + "google.golang.org/protobuf/types/known/durationpb" + + "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/query" + settings "github.com/zitadel/zitadel/pkg/grpc/settings/v2beta" +) + +func loginSettingsToPb(current *query.LoginPolicy) *settings.LoginSettings { + multi := make([]settings.MultiFactorType, len(current.MultiFactors)) + for i, typ := range current.MultiFactors { + multi[i] = multiFactorTypeToPb(typ) + } + second := make([]settings.SecondFactorType, len(current.SecondFactors)) + for i, typ := range current.SecondFactors { + second[i] = secondFactorTypeToPb(typ) + } + + return &settings.LoginSettings{ + AllowUsernamePassword: current.AllowUsernamePassword, + AllowRegister: current.AllowRegister, + AllowExternalIdp: current.AllowExternalIDPs, + ForceMfa: current.ForceMFA, + ForceMfaLocalOnly: current.ForceMFALocalOnly, + PasskeysType: passkeysTypeToPb(current.PasswordlessType), + HidePasswordReset: current.HidePasswordReset, + IgnoreUnknownUsernames: current.IgnoreUnknownUsernames, + AllowDomainDiscovery: current.AllowDomainDiscovery, + DisableLoginWithEmail: current.DisableLoginWithEmail, + DisableLoginWithPhone: current.DisableLoginWithPhone, + DefaultRedirectUri: current.DefaultRedirectURI, + PasswordCheckLifetime: durationpb.New(time.Duration(current.PasswordCheckLifetime)), + ExternalLoginCheckLifetime: durationpb.New(time.Duration(current.ExternalLoginCheckLifetime)), + MfaInitSkipLifetime: durationpb.New(time.Duration(current.MFAInitSkipLifetime)), + SecondFactorCheckLifetime: durationpb.New(time.Duration(current.SecondFactorCheckLifetime)), + MultiFactorCheckLifetime: durationpb.New(time.Duration(current.MultiFactorCheckLifetime)), + SecondFactors: second, + MultiFactors: multi, + ResourceOwnerType: isDefaultToResourceOwnerTypePb(current.IsDefault), + } +} + +func isDefaultToResourceOwnerTypePb(isDefault bool) settings.ResourceOwnerType { + if isDefault { + return settings.ResourceOwnerType_RESOURCE_OWNER_TYPE_INSTANCE + } + return settings.ResourceOwnerType_RESOURCE_OWNER_TYPE_ORG +} + +func passkeysTypeToPb(passwordlessType domain.PasswordlessType) settings.PasskeysType { + switch passwordlessType { + case domain.PasswordlessTypeAllowed: + return settings.PasskeysType_PASSKEYS_TYPE_ALLOWED + case domain.PasswordlessTypeNotAllowed: + return settings.PasskeysType_PASSKEYS_TYPE_NOT_ALLOWED + default: + return settings.PasskeysType_PASSKEYS_TYPE_NOT_ALLOWED + } +} + +func secondFactorTypeToPb(secondFactorType domain.SecondFactorType) settings.SecondFactorType { + switch secondFactorType { + case domain.SecondFactorTypeTOTP: + return settings.SecondFactorType_SECOND_FACTOR_TYPE_OTP + case domain.SecondFactorTypeU2F: + return settings.SecondFactorType_SECOND_FACTOR_TYPE_U2F + case domain.SecondFactorTypeOTPEmail: + return settings.SecondFactorType_SECOND_FACTOR_TYPE_OTP_EMAIL + case domain.SecondFactorTypeOTPSMS: + return settings.SecondFactorType_SECOND_FACTOR_TYPE_OTP_SMS + case domain.SecondFactorTypeUnspecified: + return settings.SecondFactorType_SECOND_FACTOR_TYPE_UNSPECIFIED + default: + return settings.SecondFactorType_SECOND_FACTOR_TYPE_UNSPECIFIED + } +} + +func multiFactorTypeToPb(typ domain.MultiFactorType) settings.MultiFactorType { + switch typ { + case domain.MultiFactorTypeU2FWithPIN: + return settings.MultiFactorType_MULTI_FACTOR_TYPE_U2F_WITH_VERIFICATION + case domain.MultiFactorTypeUnspecified: + return settings.MultiFactorType_MULTI_FACTOR_TYPE_UNSPECIFIED + default: + return settings.MultiFactorType_MULTI_FACTOR_TYPE_UNSPECIFIED + } +} + +func passwordComplexitySettingsToPb(current *query.PasswordComplexityPolicy) *settings.PasswordComplexitySettings { + return &settings.PasswordComplexitySettings{ + MinLength: current.MinLength, + RequiresUppercase: current.HasUppercase, + RequiresLowercase: current.HasLowercase, + RequiresNumber: current.HasNumber, + RequiresSymbol: current.HasSymbol, + ResourceOwnerType: isDefaultToResourceOwnerTypePb(current.IsDefault), + } +} + +func passwordExpirySettingsToPb(current *query.PasswordAgePolicy) *settings.PasswordExpirySettings { + return &settings.PasswordExpirySettings{ + MaxAgeDays: current.MaxAgeDays, + ExpireWarnDays: current.ExpireWarnDays, + ResourceOwnerType: isDefaultToResourceOwnerTypePb(current.IsDefault), + } +} + +func brandingSettingsToPb(current *query.LabelPolicy, assetPrefix string) *settings.BrandingSettings { + return &settings.BrandingSettings{ + LightTheme: themeToPb(current.Light, assetPrefix, current.ResourceOwner), + DarkTheme: themeToPb(current.Dark, assetPrefix, current.ResourceOwner), + FontUrl: domain.AssetURL(assetPrefix, current.ResourceOwner, current.FontURL), + DisableWatermark: current.WatermarkDisabled, + HideLoginNameSuffix: current.HideLoginNameSuffix, + ResourceOwnerType: isDefaultToResourceOwnerTypePb(current.IsDefault), + ThemeMode: themeModeToPb(current.ThemeMode), + } +} + +func themeModeToPb(themeMode domain.LabelPolicyThemeMode) settings.ThemeMode { + switch themeMode { + case domain.LabelPolicyThemeAuto: + return settings.ThemeMode_THEME_MODE_AUTO + case domain.LabelPolicyThemeLight: + return settings.ThemeMode_THEME_MODE_LIGHT + case domain.LabelPolicyThemeDark: + return settings.ThemeMode_THEME_MODE_DARK + default: + return settings.ThemeMode_THEME_MODE_AUTO + } +} + +func themeToPb(theme query.Theme, assetPrefix, resourceOwner string) *settings.Theme { + return &settings.Theme{ + PrimaryColor: theme.PrimaryColor, + BackgroundColor: theme.BackgroundColor, + FontColor: theme.FontColor, + WarnColor: theme.WarnColor, + LogoUrl: domain.AssetURL(assetPrefix, resourceOwner, theme.LogoURL), + IconUrl: domain.AssetURL(assetPrefix, resourceOwner, theme.IconURL), + } +} + +func domainSettingsToPb(current *query.DomainPolicy) *settings.DomainSettings { + return &settings.DomainSettings{ + LoginNameIncludesDomain: current.UserLoginMustBeDomain, + RequireOrgDomainVerification: current.ValidateOrgDomains, + SmtpSenderAddressMatchesInstanceDomain: current.SMTPSenderAddressMatchesInstanceDomain, + ResourceOwnerType: isDefaultToResourceOwnerTypePb(current.IsDefault), + } +} + +func legalAndSupportSettingsToPb(current *query.PrivacyPolicy) *settings.LegalAndSupportSettings { + return &settings.LegalAndSupportSettings{ + TosLink: current.TOSLink, + PrivacyPolicyLink: current.PrivacyLink, + HelpLink: current.HelpLink, + SupportEmail: string(current.SupportEmail), + ResourceOwnerType: isDefaultToResourceOwnerTypePb(current.IsDefault), + DocsLink: current.DocsLink, + CustomLink: current.CustomLink, + CustomLinkText: current.CustomLinkText, + } +} + +func lockoutSettingsToPb(current *query.LockoutPolicy) *settings.LockoutSettings { + return &settings.LockoutSettings{ + MaxPasswordAttempts: current.MaxPasswordAttempts, + MaxOtpAttempts: current.MaxOTPAttempts, + ResourceOwnerType: isDefaultToResourceOwnerTypePb(current.IsDefault), + } +} + +func identityProvidersToPb(idps []*query.IDPLoginPolicyLink) []*settings.IdentityProvider { + providers := make([]*settings.IdentityProvider, len(idps)) + for i, idp := range idps { + providers[i] = identityProviderToPb(idp) + } + return providers +} + +func identityProviderToPb(idp *query.IDPLoginPolicyLink) *settings.IdentityProvider { + return &settings.IdentityProvider{ + Id: idp.IDPID, + Name: domain.IDPName(idp.IDPName, idp.IDPType), + Type: idpTypeToPb(idp.IDPType), + } +} + +func idpTypeToPb(idpType domain.IDPType) settings.IdentityProviderType { + switch idpType { + case domain.IDPTypeUnspecified: + return settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_UNSPECIFIED + case domain.IDPTypeOIDC: + return settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_OIDC + case domain.IDPTypeJWT: + return settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_JWT + case domain.IDPTypeOAuth: + return settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_OAUTH + case domain.IDPTypeLDAP: + return settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_LDAP + case domain.IDPTypeAzureAD: + return settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_AZURE_AD + case domain.IDPTypeGitHub: + return settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_GITHUB + case domain.IDPTypeGitHubEnterprise: + return settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_GITHUB_ES + case domain.IDPTypeGitLab: + return settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_GITLAB + case domain.IDPTypeGitLabSelfHosted: + return settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_GITLAB_SELF_HOSTED + case domain.IDPTypeGoogle: + return settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_GOOGLE + case domain.IDPTypeSAML: + return settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_SAML + case domain.IDPTypeApple: + // Handle all remaining cases so the linter succeeds + return settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_UNSPECIFIED + default: + return settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_UNSPECIFIED + } +} + +func securityPolicyToSettingsPb(policy *query.SecurityPolicy) *settings.SecuritySettings { + return &settings.SecuritySettings{ + EmbeddedIframe: &settings.EmbeddedIframeSettings{ + Enabled: policy.EnableIframeEmbedding, + AllowedOrigins: policy.AllowedOrigins, + }, + EnableImpersonation: policy.EnableImpersonation, + } +} + +func securitySettingsToCommand(req *settings.SetSecuritySettingsRequest) *command.SecurityPolicy { + return &command.SecurityPolicy{ + EnableIframeEmbedding: req.GetEmbeddedIframe().GetEnabled(), + AllowedOrigins: req.GetEmbeddedIframe().GetAllowedOrigins(), + EnableImpersonation: req.GetEnableImpersonation(), + } +} diff --git a/internal/api/grpc/settings/v2beta/settings_converter_test.go b/internal/api/grpc/settings/v2beta/settings_converter_test.go new file mode 100644 index 0000000000..99c60f2628 --- /dev/null +++ b/internal/api/grpc/settings/v2beta/settings_converter_test.go @@ -0,0 +1,517 @@ +package settings + +import ( + "reflect" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/reflect/protoreflect" + "google.golang.org/protobuf/types/known/durationpb" + + "github.com/zitadel/zitadel/internal/api/grpc" + "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/database" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/query" + settings "github.com/zitadel/zitadel/pkg/grpc/settings/v2beta" +) + +var ignoreTypes = []protoreflect.FullName{"google.protobuf.Duration"} + +func Test_loginSettingsToPb(t *testing.T) { + arg := &query.LoginPolicy{ + AllowUsernamePassword: true, + AllowRegister: true, + AllowExternalIDPs: true, + ForceMFA: true, + ForceMFALocalOnly: true, + PasswordlessType: domain.PasswordlessTypeAllowed, + HidePasswordReset: true, + IgnoreUnknownUsernames: true, + AllowDomainDiscovery: true, + DisableLoginWithEmail: true, + DisableLoginWithPhone: true, + DefaultRedirectURI: "example.com", + PasswordCheckLifetime: database.Duration(time.Hour), + ExternalLoginCheckLifetime: database.Duration(time.Minute), + MFAInitSkipLifetime: database.Duration(time.Millisecond), + SecondFactorCheckLifetime: database.Duration(time.Microsecond), + MultiFactorCheckLifetime: database.Duration(time.Nanosecond), + SecondFactors: []domain.SecondFactorType{ + domain.SecondFactorTypeTOTP, + domain.SecondFactorTypeU2F, + domain.SecondFactorTypeOTPEmail, + domain.SecondFactorTypeOTPSMS, + }, + MultiFactors: []domain.MultiFactorType{ + domain.MultiFactorTypeU2FWithPIN, + }, + IsDefault: true, + } + + want := &settings.LoginSettings{ + AllowUsernamePassword: true, + AllowRegister: true, + AllowExternalIdp: true, + ForceMfa: true, + ForceMfaLocalOnly: true, + PasskeysType: settings.PasskeysType_PASSKEYS_TYPE_ALLOWED, + HidePasswordReset: true, + IgnoreUnknownUsernames: true, + AllowDomainDiscovery: true, + DisableLoginWithEmail: true, + DisableLoginWithPhone: true, + DefaultRedirectUri: "example.com", + PasswordCheckLifetime: durationpb.New(time.Hour), + ExternalLoginCheckLifetime: durationpb.New(time.Minute), + MfaInitSkipLifetime: durationpb.New(time.Millisecond), + SecondFactorCheckLifetime: durationpb.New(time.Microsecond), + MultiFactorCheckLifetime: durationpb.New(time.Nanosecond), + SecondFactors: []settings.SecondFactorType{ + settings.SecondFactorType_SECOND_FACTOR_TYPE_OTP, + settings.SecondFactorType_SECOND_FACTOR_TYPE_U2F, + settings.SecondFactorType_SECOND_FACTOR_TYPE_OTP_EMAIL, + settings.SecondFactorType_SECOND_FACTOR_TYPE_OTP_SMS, + }, + MultiFactors: []settings.MultiFactorType{ + settings.MultiFactorType_MULTI_FACTOR_TYPE_U2F_WITH_VERIFICATION, + }, + ResourceOwnerType: settings.ResourceOwnerType_RESOURCE_OWNER_TYPE_INSTANCE, + } + + got := loginSettingsToPb(arg) + grpc.AllFieldsSet(t, got.ProtoReflect(), ignoreTypes...) + if !proto.Equal(got, want) { + t.Errorf("loginSettingsToPb() =\n%v\nwant\n%v", got, want) + } +} + +func Test_isDefaultToResourceOwnerTypePb(t *testing.T) { + type args struct { + isDefault bool + } + tests := []struct { + args args + want settings.ResourceOwnerType + }{ + { + args: args{false}, + want: settings.ResourceOwnerType_RESOURCE_OWNER_TYPE_ORG, + }, + { + args: args{true}, + want: settings.ResourceOwnerType_RESOURCE_OWNER_TYPE_INSTANCE, + }, + } + for _, tt := range tests { + t.Run(tt.want.String(), func(t *testing.T) { + got := isDefaultToResourceOwnerTypePb(tt.args.isDefault) + assert.Equal(t, tt.want, got) + }) + } +} + +func Test_passkeysTypeToPb(t *testing.T) { + type args struct { + passwordlessType domain.PasswordlessType + } + tests := []struct { + args args + want settings.PasskeysType + }{ + { + args: args{domain.PasswordlessTypeNotAllowed}, + want: settings.PasskeysType_PASSKEYS_TYPE_NOT_ALLOWED, + }, + { + args: args{domain.PasswordlessTypeAllowed}, + want: settings.PasskeysType_PASSKEYS_TYPE_ALLOWED, + }, + { + args: args{99}, + want: settings.PasskeysType_PASSKEYS_TYPE_NOT_ALLOWED, + }, + } + for _, tt := range tests { + t.Run(tt.want.String(), func(t *testing.T) { + got := passkeysTypeToPb(tt.args.passwordlessType) + assert.Equal(t, tt.want, got) + }) + } +} + +func Test_secondFactorTypeToPb(t *testing.T) { + type args struct { + secondFactorType domain.SecondFactorType + } + tests := []struct { + args args + want settings.SecondFactorType + }{ + { + args: args{domain.SecondFactorTypeTOTP}, + want: settings.SecondFactorType_SECOND_FACTOR_TYPE_OTP, + }, + { + args: args{domain.SecondFactorTypeU2F}, + want: settings.SecondFactorType_SECOND_FACTOR_TYPE_U2F, + }, + { + args: args{domain.SecondFactorTypeOTPSMS}, + want: settings.SecondFactorType_SECOND_FACTOR_TYPE_OTP_SMS, + }, + { + args: args{domain.SecondFactorTypeOTPEmail}, + want: settings.SecondFactorType_SECOND_FACTOR_TYPE_OTP_EMAIL, + }, + { + args: args{domain.SecondFactorTypeUnspecified}, + want: settings.SecondFactorType_SECOND_FACTOR_TYPE_UNSPECIFIED, + }, + { + args: args{99}, + want: settings.SecondFactorType_SECOND_FACTOR_TYPE_UNSPECIFIED, + }, + } + for _, tt := range tests { + t.Run(tt.want.String(), func(t *testing.T) { + got := secondFactorTypeToPb(tt.args.secondFactorType) + assert.Equal(t, tt.want, got) + }) + } +} + +func Test_multiFactorTypeToPb(t *testing.T) { + type args struct { + typ domain.MultiFactorType + } + tests := []struct { + args args + want settings.MultiFactorType + }{ + { + args: args{domain.MultiFactorTypeU2FWithPIN}, + want: settings.MultiFactorType_MULTI_FACTOR_TYPE_U2F_WITH_VERIFICATION, + }, + { + args: args{domain.MultiFactorTypeUnspecified}, + want: settings.MultiFactorType_MULTI_FACTOR_TYPE_UNSPECIFIED, + }, + { + args: args{99}, + want: settings.MultiFactorType_MULTI_FACTOR_TYPE_UNSPECIFIED, + }, + } + for _, tt := range tests { + t.Run(tt.want.String(), func(t *testing.T) { + got := multiFactorTypeToPb(tt.args.typ) + assert.Equal(t, tt.want, got) + }) + } +} + +func Test_passwordComplexitySettingsToPb(t *testing.T) { + arg := &query.PasswordComplexityPolicy{ + MinLength: 12, + HasUppercase: true, + HasLowercase: true, + HasNumber: true, + HasSymbol: true, + IsDefault: true, + } + want := &settings.PasswordComplexitySettings{ + MinLength: 12, + RequiresUppercase: true, + RequiresLowercase: true, + RequiresNumber: true, + RequiresSymbol: true, + ResourceOwnerType: settings.ResourceOwnerType_RESOURCE_OWNER_TYPE_INSTANCE, + } + + got := passwordComplexitySettingsToPb(arg) + grpc.AllFieldsSet(t, got.ProtoReflect(), ignoreTypes...) + if !proto.Equal(got, want) { + t.Errorf("passwordComplexitySettingsToPb() =\n%v\nwant\n%v", got, want) + } +} + +func Test_passwordExpirySettingsToPb(t *testing.T) { + arg := &query.PasswordAgePolicy{ + ExpireWarnDays: 80, + MaxAgeDays: 90, + IsDefault: true, + } + want := &settings.PasswordExpirySettings{ + ExpireWarnDays: 80, + MaxAgeDays: 90, + ResourceOwnerType: settings.ResourceOwnerType_RESOURCE_OWNER_TYPE_INSTANCE, + } + + got := passwordExpirySettingsToPb(arg) + grpc.AllFieldsSet(t, got.ProtoReflect(), ignoreTypes...) + if !proto.Equal(got, want) { + t.Errorf("passwordExpirySettingsToPb() =\n%v\nwant\n%v", got, want) + } +} + +func Test_brandingSettingsToPb(t *testing.T) { + arg := &query.LabelPolicy{ + Light: query.Theme{ + PrimaryColor: "red", + WarnColor: "white", + BackgroundColor: "blue", + FontColor: "orange", + LogoURL: "light-logo", + IconURL: "light-icon", + }, + Dark: query.Theme{ + PrimaryColor: "magenta", + WarnColor: "pink", + BackgroundColor: "black", + FontColor: "white", + LogoURL: "dark-logo", + IconURL: "dark-icon", + }, + ResourceOwner: "me", + FontURL: "fonts", + WatermarkDisabled: true, + HideLoginNameSuffix: true, + ThemeMode: domain.LabelPolicyThemeDark, + IsDefault: true, + } + want := &settings.BrandingSettings{ + LightTheme: &settings.Theme{ + PrimaryColor: "red", + WarnColor: "white", + BackgroundColor: "blue", + FontColor: "orange", + LogoUrl: "http://example.com/me/light-logo", + IconUrl: "http://example.com/me/light-icon", + }, + DarkTheme: &settings.Theme{ + PrimaryColor: "magenta", + WarnColor: "pink", + BackgroundColor: "black", + FontColor: "white", + LogoUrl: "http://example.com/me/dark-logo", + IconUrl: "http://example.com/me/dark-icon", + }, + FontUrl: "http://example.com/me/fonts", + DisableWatermark: true, + HideLoginNameSuffix: true, + ResourceOwnerType: settings.ResourceOwnerType_RESOURCE_OWNER_TYPE_INSTANCE, + ThemeMode: settings.ThemeMode_THEME_MODE_DARK, + } + + got := brandingSettingsToPb(arg, "http://example.com") + grpc.AllFieldsSet(t, got.ProtoReflect(), ignoreTypes...) + if !proto.Equal(got, want) { + t.Errorf("brandingSettingsToPb() =\n%v\nwant\n%v", got, want) + } +} + +func Test_domainSettingsToPb(t *testing.T) { + arg := &query.DomainPolicy{ + UserLoginMustBeDomain: true, + ValidateOrgDomains: true, + SMTPSenderAddressMatchesInstanceDomain: true, + IsDefault: true, + } + want := &settings.DomainSettings{ + LoginNameIncludesDomain: true, + RequireOrgDomainVerification: true, + SmtpSenderAddressMatchesInstanceDomain: true, + ResourceOwnerType: settings.ResourceOwnerType_RESOURCE_OWNER_TYPE_INSTANCE, + } + got := domainSettingsToPb(arg) + grpc.AllFieldsSet(t, got.ProtoReflect(), ignoreTypes...) + if !proto.Equal(got, want) { + t.Errorf("domainSettingsToPb() =\n%v\nwant\n%v", got, want) + } +} + +func Test_legalSettingsToPb(t *testing.T) { + arg := &query.PrivacyPolicy{ + TOSLink: "http://example.com/tos", + PrivacyLink: "http://example.com/pricacy", + HelpLink: "http://example.com/help", + SupportEmail: "support@zitadel.com", + IsDefault: true, + DocsLink: "http://example.com/docs", + CustomLink: "http://example.com/custom", + CustomLinkText: "Custom", + } + want := &settings.LegalAndSupportSettings{ + TosLink: "http://example.com/tos", + PrivacyPolicyLink: "http://example.com/pricacy", + HelpLink: "http://example.com/help", + SupportEmail: "support@zitadel.com", + DocsLink: "http://example.com/docs", + CustomLink: "http://example.com/custom", + CustomLinkText: "Custom", + ResourceOwnerType: settings.ResourceOwnerType_RESOURCE_OWNER_TYPE_INSTANCE, + } + got := legalAndSupportSettingsToPb(arg) + grpc.AllFieldsSet(t, got.ProtoReflect(), ignoreTypes...) + if !proto.Equal(got, want) { + t.Errorf("legalSettingsToPb() =\n%v\nwant\n%v", got, want) + } +} + +func Test_lockoutSettingsToPb(t *testing.T) { + arg := &query.LockoutPolicy{ + MaxPasswordAttempts: 22, + MaxOTPAttempts: 22, + IsDefault: true, + } + want := &settings.LockoutSettings{ + MaxPasswordAttempts: 22, + MaxOtpAttempts: 22, + ResourceOwnerType: settings.ResourceOwnerType_RESOURCE_OWNER_TYPE_INSTANCE, + } + got := lockoutSettingsToPb(arg) + grpc.AllFieldsSet(t, got.ProtoReflect(), ignoreTypes...) + if !proto.Equal(got, want) { + t.Errorf("lockoutSettingsToPb() =\n%v\nwant\n%v", got, want) + } +} + +func Test_identityProvidersToPb(t *testing.T) { + arg := []*query.IDPLoginPolicyLink{ + { + IDPID: "1", + IDPName: "foo", + IDPType: domain.IDPTypeOIDC, + }, + { + IDPID: "2", + IDPName: "bar", + IDPType: domain.IDPTypeGitHub, + }, + } + want := []*settings.IdentityProvider{ + { + Id: "1", + Name: "foo", + Type: settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_OIDC, + }, + { + Id: "2", + Name: "bar", + Type: settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_GITHUB, + }, + } + got := identityProvidersToPb(arg) + require.Len(t, got, len(got)) + for i, v := range got { + grpc.AllFieldsSet(t, v.ProtoReflect(), ignoreTypes...) + if !proto.Equal(v, want[i]) { + t.Errorf("identityProvidersToPb() =\n%v\nwant\n%v", got, want) + } + } +} + +func Test_idpTypeToPb(t *testing.T) { + type args struct { + idpType domain.IDPType + } + tests := []struct { + args args + want settings.IdentityProviderType + }{ + { + args: args{domain.IDPTypeUnspecified}, + want: settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_UNSPECIFIED, + }, + { + args: args{domain.IDPTypeOIDC}, + want: settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_OIDC, + }, + { + args: args{domain.IDPTypeJWT}, + want: settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_JWT, + }, + { + args: args{domain.IDPTypeOAuth}, + want: settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_OAUTH, + }, + { + args: args{domain.IDPTypeLDAP}, + want: settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_LDAP, + }, + { + args: args{domain.IDPTypeAzureAD}, + want: settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_AZURE_AD, + }, + { + args: args{domain.IDPTypeGitHub}, + want: settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_GITHUB, + }, + { + args: args{domain.IDPTypeGitHubEnterprise}, + want: settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_GITHUB_ES, + }, + { + args: args{domain.IDPTypeGitLab}, + want: settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_GITLAB, + }, + { + args: args{domain.IDPTypeGitLabSelfHosted}, + want: settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_GITLAB_SELF_HOSTED, + }, + { + args: args{domain.IDPTypeGoogle}, + want: settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_GOOGLE, + }, + { + args: args{domain.IDPTypeSAML}, + want: settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_SAML, + }, + { + args: args{99}, + want: settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_UNSPECIFIED, + }, + } + for _, tt := range tests { + t.Run(tt.want.String(), func(t *testing.T) { + if got := idpTypeToPb(tt.args.idpType); !reflect.DeepEqual(got, tt.want) { + t.Errorf("idpTypeToPb() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_securityPolicyToSettingsPb(t *testing.T) { + want := &settings.SecuritySettings{ + EmbeddedIframe: &settings.EmbeddedIframeSettings{ + Enabled: true, + AllowedOrigins: []string{"foo", "bar"}, + }, + EnableImpersonation: true, + } + got := securityPolicyToSettingsPb(&query.SecurityPolicy{ + EnableIframeEmbedding: true, + AllowedOrigins: []string{"foo", "bar"}, + EnableImpersonation: true, + }) + assert.Equal(t, want, got) +} + +func Test_securitySettingsToCommand(t *testing.T) { + want := &command.SecurityPolicy{ + EnableIframeEmbedding: true, + AllowedOrigins: []string{"foo", "bar"}, + EnableImpersonation: true, + } + got := securitySettingsToCommand(&settings.SetSecuritySettingsRequest{ + EmbeddedIframe: &settings.EmbeddedIframeSettings{ + Enabled: true, + AllowedOrigins: []string{"foo", "bar"}, + }, + EnableImpersonation: true, + }) + assert.Equal(t, want, got) +} diff --git a/internal/api/grpc/settings/v2beta/settings_integration_test.go b/internal/api/grpc/settings/v2beta/settings_integration_test.go new file mode 100644 index 0000000000..3accc0d63f --- /dev/null +++ b/internal/api/grpc/settings/v2beta/settings_integration_test.go @@ -0,0 +1,174 @@ +//go:build integration + +package settings_test + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/integration" + object_pb "github.com/zitadel/zitadel/pkg/grpc/object/v2beta" + settings "github.com/zitadel/zitadel/pkg/grpc/settings/v2beta" +) + +func TestServer_GetSecuritySettings(t *testing.T) { + _, err := Client.SetSecuritySettings(AdminCTX, &settings.SetSecuritySettingsRequest{ + EmbeddedIframe: &settings.EmbeddedIframeSettings{ + Enabled: true, + AllowedOrigins: []string{"foo", "bar"}, + }, + EnableImpersonation: true, + }) + require.NoError(t, err) + + tests := []struct { + name string + ctx context.Context + want *settings.GetSecuritySettingsResponse + wantErr bool + }{ + { + name: "permission error", + ctx: Tester.WithAuthorization(CTX, integration.OrgOwner), + wantErr: true, + }, + { + name: "success", + ctx: AdminCTX, + want: &settings.GetSecuritySettingsResponse{ + Settings: &settings.SecuritySettings{ + EmbeddedIframe: &settings.EmbeddedIframeSettings{ + Enabled: true, + AllowedOrigins: []string{"foo", "bar"}, + }, + EnableImpersonation: true, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + resp, err := Client.GetSecuritySettings(tt.ctx, &settings.GetSecuritySettingsRequest{}) + if tt.wantErr { + assert.Error(t, err) + return + } + require.NoError(t, err) + got, want := resp.GetSettings(), tt.want.GetSettings() + assert.Equal(t, want.GetEmbeddedIframe().GetEnabled(), got.GetEmbeddedIframe().GetEnabled(), "enable iframe embedding") + assert.Equal(t, want.GetEmbeddedIframe().GetAllowedOrigins(), got.GetEmbeddedIframe().GetAllowedOrigins(), "allowed origins") + assert.Equal(t, want.GetEnableImpersonation(), got.GetEnableImpersonation(), "enable impersonation") + }) + } +} + +func TestServer_SetSecuritySettings(t *testing.T) { + type args struct { + ctx context.Context + req *settings.SetSecuritySettingsRequest + } + tests := []struct { + name string + args args + want *settings.SetSecuritySettingsResponse + wantErr bool + }{ + { + name: "permission error", + args: args{ + ctx: Tester.WithAuthorization(CTX, integration.OrgOwner), + req: &settings.SetSecuritySettingsRequest{ + EmbeddedIframe: &settings.EmbeddedIframeSettings{ + Enabled: true, + AllowedOrigins: []string{"foo.com", "bar.com"}, + }, + EnableImpersonation: true, + }, + }, + wantErr: true, + }, + { + name: "success allowed origins", + args: args{ + ctx: AdminCTX, + req: &settings.SetSecuritySettingsRequest{ + EmbeddedIframe: &settings.EmbeddedIframeSettings{ + AllowedOrigins: []string{"foo.com", "bar.com"}, + }, + }, + }, + want: &settings.SetSecuritySettingsResponse{ + Details: &object_pb.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Instance.InstanceID(), + }, + }, + }, + { + name: "success enable iframe embedding", + args: args{ + ctx: AdminCTX, + req: &settings.SetSecuritySettingsRequest{ + EmbeddedIframe: &settings.EmbeddedIframeSettings{ + Enabled: true, + }, + }, + }, + want: &settings.SetSecuritySettingsResponse{ + Details: &object_pb.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Instance.InstanceID(), + }, + }, + }, + { + name: "success impersonation", + args: args{ + ctx: AdminCTX, + req: &settings.SetSecuritySettingsRequest{ + EnableImpersonation: true, + }, + }, + want: &settings.SetSecuritySettingsResponse{ + Details: &object_pb.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Instance.InstanceID(), + }, + }, + }, + { + name: "success all", + args: args{ + ctx: AdminCTX, + req: &settings.SetSecuritySettingsRequest{ + EmbeddedIframe: &settings.EmbeddedIframeSettings{ + Enabled: true, + AllowedOrigins: []string{"foo.com", "bar.com"}, + }, + EnableImpersonation: true, + }, + }, + want: &settings.SetSecuritySettingsResponse{ + Details: &object_pb.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Instance.InstanceID(), + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Client.SetSecuritySettings(tt.args.ctx, tt.args.req) + if tt.wantErr { + assert.Error(t, err) + return + } + require.NoError(t, err) + integration.AssertDetails(t, tt.want, got) + }) + } +} diff --git a/internal/api/grpc/user/converter.go b/internal/api/grpc/user/converter.go index 621d445672..85592b0f12 100644 --- a/internal/api/grpc/user/converter.go +++ b/internal/api/grpc/user/converter.go @@ -5,7 +5,6 @@ import ( "github.com/zitadel/zitadel/internal/api/grpc/object" "github.com/zitadel/zitadel/internal/domain" - "github.com/zitadel/zitadel/internal/eventstore/v1/models" "github.com/zitadel/zitadel/internal/query" user_pb "github.com/zitadel/zitadel/pkg/grpc/user" ) @@ -255,22 +254,6 @@ func UserAuthMethodToWebAuthNTokenPb(token *query.AuthMethod) *user_pb.WebAuthNT } } -func ExternalIDPViewsToExternalIDPs(externalIDPs []*query.IDPUserLink) []*domain.UserIDPLink { - idps := make([]*domain.UserIDPLink, len(externalIDPs)) - for i, idp := range externalIDPs { - idps[i] = &domain.UserIDPLink{ - ObjectRoot: models.ObjectRoot{ - AggregateID: idp.UserID, - ResourceOwner: idp.ResourceOwner, - }, - IDPConfigID: idp.IDPID, - ExternalUserID: idp.ProvidedUserID, - DisplayName: idp.ProvidedUsername, - } - } - return idps -} - func TypeToPb(userType domain.UserType) user_pb.Type { switch userType { case domain.UserTypeHuman: diff --git a/internal/api/grpc/user/schema/v3alpha/schema_integration_test.go b/internal/api/grpc/user/schema/v3alpha/schema_integration_test.go index f84c338ab4..5cf279144d 100644 --- a/internal/api/grpc/user/schema/v3alpha/schema_integration_test.go +++ b/internal/api/grpc/user/schema/v3alpha/schema_integration_test.go @@ -17,8 +17,8 @@ import ( "github.com/zitadel/zitadel/internal/api/grpc" "github.com/zitadel/zitadel/internal/integration" - feature "github.com/zitadel/zitadel/pkg/grpc/feature/v2beta" - object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta" + "github.com/zitadel/zitadel/pkg/grpc/feature/v2" + "github.com/zitadel/zitadel/pkg/grpc/object/v2" schema "github.com/zitadel/zitadel/pkg/grpc/user/schema/v3alpha" ) diff --git a/internal/api/grpc/user/v2/email.go b/internal/api/grpc/user/v2/email.go index 38cc73c75c..6d0871b26e 100644 --- a/internal/api/grpc/user/v2/email.go +++ b/internal/api/grpc/user/v2/email.go @@ -7,8 +7,8 @@ import ( "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/zerrors" - object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta" - user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta" + "github.com/zitadel/zitadel/pkg/grpc/object/v2" + "github.com/zitadel/zitadel/pkg/grpc/user/v2" ) func (s *Server) SetEmail(ctx context.Context, req *user.SetEmailRequest) (resp *user.SetEmailResponse, err error) { diff --git a/internal/api/grpc/user/v2/email_integration_test.go b/internal/api/grpc/user/v2/email_integration_test.go index 4034a5e7da..2264934f25 100644 --- a/internal/api/grpc/user/v2/email_integration_test.go +++ b/internal/api/grpc/user/v2/email_integration_test.go @@ -12,9 +12,10 @@ import ( "github.com/stretchr/testify/require" "google.golang.org/protobuf/types/known/timestamppb" + object "github.com/zitadel/zitadel/pkg/grpc/object/v2" + user "github.com/zitadel/zitadel/pkg/grpc/user/v2" + "github.com/zitadel/zitadel/internal/integration" - object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta" - user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta" ) func TestServer_SetEmail(t *testing.T) { diff --git a/internal/api/grpc/user/v2/idp_link.go b/internal/api/grpc/user/v2/idp_link.go new file mode 100644 index 0000000000..5567ab24a2 --- /dev/null +++ b/internal/api/grpc/user/v2/idp_link.go @@ -0,0 +1,94 @@ +package user + +import ( + "context" + + "github.com/zitadel/zitadel/internal/api/grpc/object/v2" + "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" + "github.com/zitadel/zitadel/pkg/grpc/user/v2" +) + +func (s *Server) AddIDPLink(ctx context.Context, req *user.AddIDPLinkRequest) (_ *user.AddIDPLinkResponse, err error) { + details, err := s.command.AddUserIDPLink(ctx, req.UserId, "", &command.AddLink{ + IDPID: req.GetIdpLink().GetIdpId(), + DisplayName: req.GetIdpLink().GetUserName(), + IDPExternalID: req.GetIdpLink().GetUserId(), + }) + if err != nil { + return nil, err + } + return &user.AddIDPLinkResponse{ + Details: object.DomainToDetailsPb(details), + }, nil +} + +func (s *Server) ListIDPLinks(ctx context.Context, req *user.ListIDPLinksRequest) (_ *user.ListIDPLinksResponse, err error) { + queries, err := ListLinkedIDPsRequestToQuery(req) + if err != nil { + return nil, err + } + res, err := s.query.IDPUserLinks(ctx, queries, false) + if err != nil { + return nil, err + } + res.RemoveNoPermission(ctx, s.checkPermission) + return &user.ListIDPLinksResponse{ + Result: IDPLinksToPb(res.Links), + Details: object.ToListDetails(res.SearchResponse), + }, nil +} + +func ListLinkedIDPsRequestToQuery(req *user.ListIDPLinksRequest) (*query.IDPUserLinksSearchQuery, error) { + offset, limit, asc := object.ListQueryToQuery(req.Query) + userQuery, err := query.NewIDPUserLinksUserIDSearchQuery(req.UserId) + if err != nil { + return nil, err + } + return &query.IDPUserLinksSearchQuery{ + SearchRequest: query.SearchRequest{ + Offset: offset, + Limit: limit, + Asc: asc, + }, + Queries: []query.SearchQuery{userQuery}, + }, nil +} + +func IDPLinksToPb(res []*query.IDPUserLink) []*user.IDPLink { + links := make([]*user.IDPLink, len(res)) + for i, link := range res { + links[i] = IDPLinkToPb(link) + } + return links +} + +func IDPLinkToPb(link *query.IDPUserLink) *user.IDPLink { + return &user.IDPLink{ + IdpId: link.IDPID, + UserId: link.ProvidedUserID, + UserName: link.ProvidedUsername, + } +} + +func (s *Server) RemoveIDPLink(ctx context.Context, req *user.RemoveIDPLinkRequest) (*user.RemoveIDPLinkResponse, error) { + objectDetails, err := s.command.RemoveUserIDPLink(ctx, RemoveIDPLinkRequestToDomain(ctx, req)) + if err != nil { + return nil, err + } + return &user.RemoveIDPLinkResponse{ + Details: object.DomainToDetailsPb(objectDetails), + }, nil +} + +func RemoveIDPLinkRequestToDomain(ctx context.Context, req *user.RemoveIDPLinkRequest) *domain.UserIDPLink { + return &domain.UserIDPLink{ + ObjectRoot: models.ObjectRoot{ + AggregateID: req.UserId, + }, + IDPConfigID: req.IdpId, + ExternalUserID: req.LinkedUserId, + } +} diff --git a/internal/api/grpc/user/v2/idp_link_integration_test.go b/internal/api/grpc/user/v2/idp_link_integration_test.go new file mode 100644 index 0000000000..6b85c80f98 --- /dev/null +++ b/internal/api/grpc/user/v2/idp_link_integration_test.go @@ -0,0 +1,360 @@ +//go:build integration + +package user_test + +import ( + "context" + "fmt" + "testing" + "time" + + "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_AddIDPLink(t *testing.T) { + idpID := Tester.AddGenericOAuthProvider(t, CTX) + type args struct { + ctx context.Context + req *user.AddIDPLinkRequest + } + tests := []struct { + name string + args args + want *user.AddIDPLinkResponse + wantErr bool + }{ + { + name: "user does not exist", + args: args{ + CTX, + &user.AddIDPLinkRequest{ + UserId: "userID", + IdpLink: &user.IDPLink{ + IdpId: idpID, + UserId: "userID", + UserName: "username", + }, + }, + }, + want: nil, + wantErr: true, + }, + { + name: "idp does not exist", + args: args{ + CTX, + &user.AddIDPLinkRequest{ + UserId: Tester.Users[integration.FirstInstanceUsersKey][integration.OrgOwner].ID, + IdpLink: &user.IDPLink{ + IdpId: "idpID", + UserId: "userID", + UserName: "username", + }, + }, + }, + want: nil, + wantErr: true, + }, + { + name: "add link", + args: args{ + CTX, + &user.AddIDPLinkRequest{ + UserId: Tester.Users[integration.FirstInstanceUsersKey][integration.OrgOwner].ID, + IdpLink: &user.IDPLink{ + IdpId: idpID, + UserId: "userID", + UserName: "username", + }, + }, + }, + want: &user.AddIDPLinkResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Organisation.ID, + }, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Client.AddIDPLink(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + + integration.AssertDetails(t, tt.want, got) + }) + } +} + +func TestServer_ListIDPLinks(t *testing.T) { + orgResp := Tester.CreateOrganization(IamCTX, fmt.Sprintf("ListIDPLinks%d", time.Now().UnixNano()), fmt.Sprintf("%d@mouse.com", time.Now().UnixNano())) + + instanceIdpID := Tester.AddGenericOAuthProvider(t, IamCTX) + userInstanceResp := Tester.CreateHumanUserVerified(IamCTX, orgResp.OrganizationId, fmt.Sprintf("%d@listidplinks.com", time.Now().UnixNano())) + Tester.CreateUserIDPlink(IamCTX, userInstanceResp.GetUserId(), "external_instance", instanceIdpID, "externalUsername_instance") + + orgIdpID := Tester.AddOrgGenericOAuthProvider(t, IamCTX, orgResp.OrganizationId) + userOrgResp := Tester.CreateHumanUserVerified(IamCTX, orgResp.OrganizationId, fmt.Sprintf("%d@listidplinks.com", time.Now().UnixNano())) + Tester.CreateUserIDPlink(IamCTX, userOrgResp.GetUserId(), "external_org", orgIdpID, "externalUsername_org") + + userMultipleResp := Tester.CreateHumanUserVerified(IamCTX, orgResp.OrganizationId, fmt.Sprintf("%d@listidplinks.com", time.Now().UnixNano())) + Tester.CreateUserIDPlink(IamCTX, userMultipleResp.GetUserId(), "external_multi", instanceIdpID, "externalUsername_multi") + Tester.CreateUserIDPlink(IamCTX, userMultipleResp.GetUserId(), "external_multi", orgIdpID, "externalUsername_multi") + + type args struct { + ctx context.Context + req *user.ListIDPLinksRequest + } + tests := []struct { + name string + args args + want *user.ListIDPLinksResponse + wantErr bool + }{ + { + name: "list links, no permission", + args: args{ + UserCTX, + &user.ListIDPLinksRequest{ + UserId: userOrgResp.GetUserId(), + }, + }, + want: &user.ListIDPLinksResponse{ + Details: &object.ListDetails{ + TotalResult: 0, + Timestamp: timestamppb.Now(), + }, + Result: []*user.IDPLink{}, + }, + }, + { + name: "list links, no permission, org", + args: args{ + CTX, + &user.ListIDPLinksRequest{ + UserId: userOrgResp.GetUserId(), + }, + }, + want: &user.ListIDPLinksResponse{ + Details: &object.ListDetails{ + TotalResult: 0, + Timestamp: timestamppb.Now(), + }, + Result: []*user.IDPLink{}, + }, + }, + { + name: "list idp links, org, ok", + args: args{ + IamCTX, + &user.ListIDPLinksRequest{ + UserId: userOrgResp.GetUserId(), + }, + }, + want: &user.ListIDPLinksResponse{ + Details: &object.ListDetails{ + TotalResult: 1, + Timestamp: timestamppb.Now(), + }, + Result: []*user.IDPLink{ + { + IdpId: orgIdpID, + UserId: "external_org", + UserName: "externalUsername_org", + }, + }, + }, + }, + { + name: "list idp links, instance, ok", + args: args{ + IamCTX, + &user.ListIDPLinksRequest{ + UserId: userInstanceResp.GetUserId(), + }, + }, + want: &user.ListIDPLinksResponse{ + Details: &object.ListDetails{ + TotalResult: 1, + Timestamp: timestamppb.Now(), + }, + Result: []*user.IDPLink{ + { + IdpId: instanceIdpID, + UserId: "external_instance", + UserName: "externalUsername_instance", + }, + }, + }, + }, + { + name: "list idp links, multi, ok", + args: args{ + IamCTX, + &user.ListIDPLinksRequest{ + UserId: userMultipleResp.GetUserId(), + }, + }, + want: &user.ListIDPLinksResponse{ + Details: &object.ListDetails{ + TotalResult: 2, + Timestamp: timestamppb.Now(), + }, + Result: []*user.IDPLink{ + { + IdpId: instanceIdpID, + UserId: "external_multi", + UserName: "externalUsername_multi", + }, + { + IdpId: orgIdpID, + UserId: "external_multi", + UserName: "externalUsername_multi", + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + retryDuration := time.Minute + if ctxDeadline, ok := CTX.Deadline(); ok { + retryDuration = time.Until(ctxDeadline) + } + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + got, listErr := Client.ListIDPLinks(tt.args.ctx, tt.args.req) + assertErr := assert.NoError + if tt.wantErr { + assertErr = assert.Error + } + assertErr(ttt, listErr) + if listErr != nil { + return + } + // always first check length, otherwise its failed anyway + assert.Len(ttt, got.Result, len(tt.want.Result)) + for i := range tt.want.Result { + assert.Contains(ttt, got.Result, tt.want.Result[i]) + } + integration.AssertListDetails(t, tt.want, got) + }, retryDuration, time.Millisecond*100, "timeout waiting for expected idplinks result") + }) + } +} + +func TestServer_RemoveIDPLink(t *testing.T) { + orgResp := Tester.CreateOrganization(IamCTX, fmt.Sprintf("ListIDPLinks%d", time.Now().UnixNano()), fmt.Sprintf("%d@mouse.com", time.Now().UnixNano())) + + instanceIdpID := Tester.AddGenericOAuthProvider(t, IamCTX) + userInstanceResp := Tester.CreateHumanUserVerified(IamCTX, orgResp.OrganizationId, fmt.Sprintf("%d@listidplinks.com", time.Now().UnixNano())) + Tester.CreateUserIDPlink(IamCTX, userInstanceResp.GetUserId(), "external_instance", instanceIdpID, "externalUsername_instance") + + orgIdpID := Tester.AddOrgGenericOAuthProvider(t, IamCTX, orgResp.OrganizationId) + userOrgResp := Tester.CreateHumanUserVerified(IamCTX, orgResp.OrganizationId, fmt.Sprintf("%d@listidplinks.com", time.Now().UnixNano())) + Tester.CreateUserIDPlink(IamCTX, userOrgResp.GetUserId(), "external_org", orgIdpID, "externalUsername_org") + + userNoLinkResp := Tester.CreateHumanUserVerified(IamCTX, orgResp.OrganizationId, fmt.Sprintf("%d@listidplinks.com", time.Now().UnixNano())) + + type args struct { + ctx context.Context + req *user.RemoveIDPLinkRequest + } + tests := []struct { + name string + args args + want *user.RemoveIDPLinkResponse + wantErr bool + }{ + { + name: "remove link, no permission", + args: args{ + UserCTX, + &user.RemoveIDPLinkRequest{ + UserId: userOrgResp.GetUserId(), + IdpId: orgIdpID, + LinkedUserId: "external_org", + }, + }, + wantErr: true, + }, + { + name: "remove link, no permission, org", + args: args{ + CTX, + &user.RemoveIDPLinkRequest{ + UserId: userOrgResp.GetUserId(), + IdpId: orgIdpID, + LinkedUserId: "external_org", + }, + }, + wantErr: true, + }, + { + name: "remove link, org, ok", + args: args{ + IamCTX, + &user.RemoveIDPLinkRequest{ + UserId: userOrgResp.GetUserId(), + IdpId: orgIdpID, + LinkedUserId: "external_org", + }, + }, + want: &user.RemoveIDPLinkResponse{ + Details: &object.Details{ + ResourceOwner: orgResp.GetOrganizationId(), + ChangeDate: timestamppb.Now(), + }, + }, + }, + { + name: "remove link, instance, ok", + args: args{ + IamCTX, + &user.RemoveIDPLinkRequest{ + UserId: userInstanceResp.GetUserId(), + IdpId: instanceIdpID, + LinkedUserId: "external_instance", + }, + }, + want: &user.RemoveIDPLinkResponse{ + Details: &object.Details{ + ResourceOwner: orgResp.GetOrganizationId(), + ChangeDate: timestamppb.Now(), + }, + }, + }, + { + name: "remove link, no link, error", + args: args{ + IamCTX, + &user.RemoveIDPLinkRequest{ + UserId: userNoLinkResp.GetUserId(), + }, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Client.RemoveIDPLink(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + + integration.AssertDetails(t, tt.want, got) + }) + } +} diff --git a/internal/api/grpc/user/v2/otp.go b/internal/api/grpc/user/v2/otp.go index 0eae8c6bdd..e2fe6b794d 100644 --- a/internal/api/grpc/user/v2/otp.go +++ b/internal/api/grpc/user/v2/otp.go @@ -4,7 +4,7 @@ import ( "context" "github.com/zitadel/zitadel/internal/api/grpc/object/v2" - user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta" + "github.com/zitadel/zitadel/pkg/grpc/user/v2" ) func (s *Server) AddOTPSMS(ctx context.Context, req *user.AddOTPSMSRequest) (*user.AddOTPSMSResponse, error) { diff --git a/internal/api/grpc/user/v2/otp_integration_test.go b/internal/api/grpc/user/v2/otp_integration_test.go index 7f4c4a0f43..52b30fbd38 100644 --- a/internal/api/grpc/user/v2/otp_integration_test.go +++ b/internal/api/grpc/user/v2/otp_integration_test.go @@ -9,9 +9,10 @@ import ( "github.com/stretchr/testify/require" "google.golang.org/protobuf/types/known/timestamppb" + "github.com/zitadel/zitadel/pkg/grpc/object/v2" + "github.com/zitadel/zitadel/pkg/grpc/user/v2" + "github.com/zitadel/zitadel/internal/integration" - object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta" - user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta" ) func TestServer_AddOTPSMS(t *testing.T) { diff --git a/internal/api/grpc/user/v2/passkey.go b/internal/api/grpc/user/v2/passkey.go index 58c89ae22e..bf539e252b 100644 --- a/internal/api/grpc/user/v2/passkey.go +++ b/internal/api/grpc/user/v2/passkey.go @@ -7,9 +7,10 @@ import ( "github.com/zitadel/zitadel/internal/api/grpc/object/v2" "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/query" "github.com/zitadel/zitadel/internal/zerrors" - object_pb "github.com/zitadel/zitadel/pkg/grpc/object/v2beta" - user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta" + object_pb "github.com/zitadel/zitadel/pkg/grpc/object/v2" + "github.com/zitadel/zitadel/pkg/grpc/user/v2" ) func (s *Server) RegisterPasskey(ctx context.Context, req *user.RegisterPasskeyRequest) (resp *user.RegisterPasskeyResponse, err error) { @@ -116,3 +117,68 @@ func passkeyCodeDetailsToPb(details *domain.PasskeyCodeDetails, err error) (*use }, }, nil } + +func (s *Server) RemovePasskey(ctx context.Context, req *user.RemovePasskeyRequest) (*user.RemovePasskeyResponse, error) { + objectDetails, err := s.command.HumanRemovePasswordless(ctx, req.GetUserId(), req.GetPasskeyId(), "") + if err != nil { + return nil, err + } + return &user.RemovePasskeyResponse{ + Details: object.DomainToDetailsPb(objectDetails), + }, nil +} + +func (s *Server) ListPasskeys(ctx context.Context, req *user.ListPasskeysRequest) (*user.ListPasskeysResponse, error) { + query := new(query.UserAuthMethodSearchQueries) + err := query.AppendUserIDQuery(req.UserId) + if err != nil { + return nil, err + } + err = query.AppendAuthMethodQuery(domain.UserAuthMethodTypePasswordless) + if err != nil { + return nil, err + } + err = query.AppendStateQuery(domain.MFAStateReady) + if err != nil { + return nil, err + } + authMethods, err := s.query.SearchUserAuthMethods(ctx, query, false) + authMethods.RemoveNoPermission(ctx, s.checkPermission) + if err != nil { + return nil, err + } + return &user.ListPasskeysResponse{ + Details: object.ToListDetails(authMethods.SearchResponse), + Result: authMethodsToPasskeyPb(authMethods), + }, nil +} + +func authMethodsToPasskeyPb(methods *query.AuthMethods) []*user.Passkey { + t := make([]*user.Passkey, len(methods.AuthMethods)) + for i, token := range methods.AuthMethods { + t[i] = authMethodToPasskeyPb(token) + } + return t +} + +func authMethodToPasskeyPb(token *query.AuthMethod) *user.Passkey { + return &user.Passkey{ + Id: token.TokenID, + State: mfaStateToPb(token.State), + Name: token.Name, + } +} + +func mfaStateToPb(state domain.MFAState) user.AuthFactorState { + switch state { + case domain.MFAStateNotReady: + return user.AuthFactorState_AUTH_FACTOR_STATE_NOT_READY + case domain.MFAStateReady: + return user.AuthFactorState_AUTH_FACTOR_STATE_READY + case domain.MFAStateUnspecified, domain.MFAStateRemoved: + // Handle all remaining cases so the linter succeeds + return user.AuthFactorState_AUTH_FACTOR_STATE_UNSPECIFIED + default: + return user.AuthFactorState_AUTH_FACTOR_STATE_UNSPECIFIED + } +} diff --git a/internal/api/grpc/user/v2/passkey_integration_test.go b/internal/api/grpc/user/v2/passkey_integration_test.go index 230a744a64..027005c438 100644 --- a/internal/api/grpc/user/v2/passkey_integration_test.go +++ b/internal/api/grpc/user/v2/passkey_integration_test.go @@ -5,6 +5,7 @@ package user_test import ( "context" "testing" + "time" "github.com/muhlemmer/gu" "github.com/stretchr/testify/assert" @@ -12,9 +13,10 @@ import ( "google.golang.org/protobuf/types/known/structpb" "google.golang.org/protobuf/types/known/timestamppb" + "github.com/zitadel/zitadel/pkg/grpc/object/v2" + "github.com/zitadel/zitadel/pkg/grpc/user/v2" + "github.com/zitadel/zitadel/internal/integration" - object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta" - user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta" ) func TestServer_RegisterPasskey(t *testing.T) { @@ -138,19 +140,7 @@ func TestServer_RegisterPasskey(t *testing.T) { } func TestServer_VerifyPasskeyRegistration(t *testing.T) { - userID := Tester.CreateHumanUser(CTX).GetUserId() - reg, err := Client.CreatePasskeyRegistrationLink(CTX, &user.CreatePasskeyRegistrationLinkRequest{ - UserId: userID, - Medium: &user.CreatePasskeyRegistrationLinkRequest_ReturnCode{}, - }) - require.NoError(t, err) - pkr, err := Client.RegisterPasskey(CTX, &user.RegisterPasskeyRequest{ - UserId: userID, - Code: reg.GetCode(), - }) - require.NoError(t, err) - require.NotEmpty(t, pkr.GetPasskeyId()) - require.NotEmpty(t, pkr.GetPublicKeyCredentialCreationOptions()) + userID, pkr := userWithPasskeyRegistered(t) attestationResponse, err := Tester.WebAuthN.CreateAttestationResponse(pkr.GetPublicKeyCredentialCreationOptions()) require.NoError(t, err) @@ -317,3 +307,291 @@ func TestServer_CreatePasskeyRegistrationLink(t *testing.T) { }) } } + +func userWithPasskeyRegistered(t *testing.T) (string, *user.RegisterPasskeyResponse) { + userID := Tester.CreateHumanUser(CTX).GetUserId() + return userID, passkeyRegister(t, userID) +} + +func userWithPasskeyVerified(t *testing.T) (string, string) { + userID, pkr := userWithPasskeyRegistered(t) + return userID, passkeyVerify(t, userID, pkr) +} + +func passkeyRegister(t *testing.T, userID string) *user.RegisterPasskeyResponse { + reg, err := Client.CreatePasskeyRegistrationLink(CTX, &user.CreatePasskeyRegistrationLinkRequest{ + UserId: userID, + Medium: &user.CreatePasskeyRegistrationLinkRequest_ReturnCode{}, + }) + require.NoError(t, err) + pkr, err := Client.RegisterPasskey(CTX, &user.RegisterPasskeyRequest{ + UserId: userID, + Code: reg.GetCode(), + }) + require.NoError(t, err) + require.NotEmpty(t, pkr.GetPasskeyId()) + require.NotEmpty(t, pkr.GetPublicKeyCredentialCreationOptions()) + return pkr +} + +func passkeyVerify(t *testing.T, userID string, pkr *user.RegisterPasskeyResponse) string { + attestationResponse, err := Tester.WebAuthN.CreateAttestationResponse(pkr.GetPublicKeyCredentialCreationOptions()) + require.NoError(t, err) + + _, err = Client.VerifyPasskeyRegistration(CTX, &user.VerifyPasskeyRegistrationRequest{ + UserId: userID, + PasskeyId: pkr.GetPasskeyId(), + PublicKeyCredential: attestationResponse, + PasskeyName: "nice name", + }) + require.NoError(t, err) + return pkr.GetPasskeyId() +} + +func TestServer_RemovePasskey(t *testing.T) { + userIDWithout := Tester.CreateHumanUser(CTX).GetUserId() + userIDRegistered, pkrRegistered := userWithPasskeyRegistered(t) + userIDVerified, passkeyIDVerified := userWithPasskeyVerified(t) + userIDVerifiedPermission, passkeyIDVerifiedPermission := userWithPasskeyVerified(t) + + type args struct { + ctx context.Context + req *user.RemovePasskeyRequest + } + tests := []struct { + name string + args args + want *user.RemovePasskeyResponse + wantErr bool + }{ + { + name: "missing user id", + args: args{ + ctx: IamCTX, + req: &user.RemovePasskeyRequest{ + PasskeyId: "123", + }, + }, + wantErr: true, + }, + { + name: "missing passkey id", + args: args{ + ctx: IamCTX, + req: &user.RemovePasskeyRequest{ + UserId: "123", + }, + }, + wantErr: true, + }, + { + name: "success, registered", + args: args{ + ctx: IamCTX, + req: &user.RemovePasskeyRequest{ + UserId: userIDRegistered, + PasskeyId: pkrRegistered.GetPasskeyId(), + }, + }, + want: &user.RemovePasskeyResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Organisation.ID, + }, + }, + }, + { + name: "no passkey, error", + args: args{ + ctx: IamCTX, + req: &user.RemovePasskeyRequest{ + UserId: userIDWithout, + PasskeyId: pkrRegistered.GetPasskeyId(), + }, + }, + wantErr: true, + }, + { + name: "success, verified", + args: args{ + ctx: IamCTX, + req: &user.RemovePasskeyRequest{ + UserId: userIDVerified, + PasskeyId: passkeyIDVerified, + }, + }, + want: &user.RemovePasskeyResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Organisation.ID, + }, + }, + }, + { + name: "verified, permission error", + args: args{ + ctx: UserCTX, + req: &user.RemovePasskeyRequest{ + UserId: userIDVerifiedPermission, + PasskeyId: passkeyIDVerifiedPermission, + }, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Client.RemovePasskey(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + require.NotNil(t, got) + integration.AssertDetails(t, tt.want, got) + }) + } +} + +func TestServer_ListPasskeys(t *testing.T) { + userIDWithout := Tester.CreateHumanUser(CTX).GetUserId() + userIDRegistered, _ := userWithPasskeyRegistered(t) + userIDVerified, passkeyIDVerified := userWithPasskeyVerified(t) + + userIDMulti, passkeyIDMulti1 := userWithPasskeyVerified(t) + passkeyIDMulti2 := passkeyVerify(t, userIDMulti, passkeyRegister(t, userIDMulti)) + + type args struct { + ctx context.Context + req *user.ListPasskeysRequest + } + tests := []struct { + name string + args args + want *user.ListPasskeysResponse + wantErr bool + }{ + { + name: "list passkeys, no permission", + args: args{ + UserCTX, + &user.ListPasskeysRequest{ + UserId: userIDVerified, + }, + }, + want: &user.ListPasskeysResponse{ + Details: &object.ListDetails{ + TotalResult: 0, + Timestamp: timestamppb.Now(), + }, + Result: []*user.Passkey{}, + }, + }, + { + name: "list passkeys, none", + args: args{ + UserCTX, + &user.ListPasskeysRequest{ + UserId: userIDWithout, + }, + }, + want: &user.ListPasskeysResponse{ + Details: &object.ListDetails{ + TotalResult: 0, + Timestamp: timestamppb.Now(), + }, + Result: []*user.Passkey{}, + }, + }, + { + name: "list passkeys, registered", + args: args{ + UserCTX, + &user.ListPasskeysRequest{ + UserId: userIDRegistered, + }, + }, + want: &user.ListPasskeysResponse{ + Details: &object.ListDetails{ + TotalResult: 0, + Timestamp: timestamppb.Now(), + }, + Result: []*user.Passkey{}, + }, + }, + { + name: "list passkeys, ok", + args: args{ + IamCTX, + &user.ListPasskeysRequest{ + UserId: userIDVerified, + }, + }, + want: &user.ListPasskeysResponse{ + Details: &object.ListDetails{ + TotalResult: 1, + Timestamp: timestamppb.Now(), + }, + Result: []*user.Passkey{ + { + Id: passkeyIDVerified, + State: user.AuthFactorState_AUTH_FACTOR_STATE_READY, + Name: "nice name", + }, + }, + }, + }, + { + name: "list idp links, multi, ok", + args: args{ + IamCTX, + &user.ListPasskeysRequest{ + UserId: userIDMulti, + }, + }, + want: &user.ListPasskeysResponse{ + Details: &object.ListDetails{ + TotalResult: 2, + Timestamp: timestamppb.Now(), + }, + Result: []*user.Passkey{ + { + Id: passkeyIDMulti1, + State: user.AuthFactorState_AUTH_FACTOR_STATE_READY, + Name: "nice name", + }, + { + Id: passkeyIDMulti2, + State: user.AuthFactorState_AUTH_FACTOR_STATE_READY, + Name: "nice name", + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + retryDuration := time.Minute + if ctxDeadline, ok := CTX.Deadline(); ok { + retryDuration = time.Until(ctxDeadline) + } + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + got, listErr := Client.ListPasskeys(tt.args.ctx, tt.args.req) + assertErr := assert.NoError + if tt.wantErr { + assertErr = assert.Error + } + assertErr(ttt, listErr) + if listErr != nil { + return + } + // always first check length, otherwise its failed anyway + assert.Len(ttt, got.Result, len(tt.want.Result)) + for i := range tt.want.Result { + assert.Contains(ttt, got.Result, tt.want.Result[i]) + } + integration.AssertListDetails(t, tt.want, got) + }, retryDuration, time.Millisecond*100, "timeout waiting for expected idplinks result") + }) + } +} diff --git a/internal/api/grpc/user/v2/passkey_test.go b/internal/api/grpc/user/v2/passkey_test.go index 7d45c41756..7facfc74e0 100644 --- a/internal/api/grpc/user/v2/passkey_test.go +++ b/internal/api/grpc/user/v2/passkey_test.go @@ -14,8 +14,8 @@ import ( "github.com/zitadel/zitadel/internal/api/grpc" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/zerrors" - object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta" - user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta" + "github.com/zitadel/zitadel/pkg/grpc/object/v2" + "github.com/zitadel/zitadel/pkg/grpc/user/v2" ) func Test_passkeyAuthenticatorToDomain(t *testing.T) { diff --git a/internal/api/grpc/user/v2/password.go b/internal/api/grpc/user/v2/password.go index a7d6a55f2e..55cf225c4b 100644 --- a/internal/api/grpc/user/v2/password.go +++ b/internal/api/grpc/user/v2/password.go @@ -6,7 +6,7 @@ import ( "github.com/zitadel/zitadel/internal/api/grpc/object/v2" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/zerrors" - user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta" + "github.com/zitadel/zitadel/pkg/grpc/user/v2" ) func (s *Server) PasswordReset(ctx context.Context, req *user.PasswordResetRequest) (_ *user.PasswordResetResponse, err error) { diff --git a/internal/api/grpc/user/v2/password_integration_test.go b/internal/api/grpc/user/v2/password_integration_test.go index 03b18a5fa7..f97d0467d7 100644 --- a/internal/api/grpc/user/v2/password_integration_test.go +++ b/internal/api/grpc/user/v2/password_integration_test.go @@ -11,9 +11,10 @@ import ( "github.com/stretchr/testify/require" "google.golang.org/protobuf/types/known/timestamppb" + object "github.com/zitadel/zitadel/pkg/grpc/object/v2" + user "github.com/zitadel/zitadel/pkg/grpc/user/v2" + "github.com/zitadel/zitadel/internal/integration" - object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta" - user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta" ) func TestServer_RequestPasswordReset(t *testing.T) { diff --git a/internal/api/grpc/user/v2/password_test.go b/internal/api/grpc/user/v2/password_test.go index 5ce9930b39..f3c35b090b 100644 --- a/internal/api/grpc/user/v2/password_test.go +++ b/internal/api/grpc/user/v2/password_test.go @@ -6,7 +6,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/zitadel/zitadel/internal/domain" - user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta" + "github.com/zitadel/zitadel/pkg/grpc/user/v2" ) func Test_notificationTypeToDomain(t *testing.T) { diff --git a/internal/api/grpc/user/v2/phone.go b/internal/api/grpc/user/v2/phone.go index eac7eb4e31..fdd5a140c1 100644 --- a/internal/api/grpc/user/v2/phone.go +++ b/internal/api/grpc/user/v2/phone.go @@ -7,8 +7,8 @@ import ( "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/zerrors" - object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta" - user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta" + "github.com/zitadel/zitadel/pkg/grpc/object/v2" + "github.com/zitadel/zitadel/pkg/grpc/user/v2" ) func (s *Server) SetPhone(ctx context.Context, req *user.SetPhoneRequest) (resp *user.SetPhoneResponse, err error) { diff --git a/internal/api/grpc/user/v2/phone_integration_test.go b/internal/api/grpc/user/v2/phone_integration_test.go index e2c670c6bd..d67b59d0b4 100644 --- a/internal/api/grpc/user/v2/phone_integration_test.go +++ b/internal/api/grpc/user/v2/phone_integration_test.go @@ -13,9 +13,10 @@ import ( "github.com/stretchr/testify/require" "google.golang.org/protobuf/types/known/timestamppb" + object "github.com/zitadel/zitadel/pkg/grpc/object/v2" + user "github.com/zitadel/zitadel/pkg/grpc/user/v2" + "github.com/zitadel/zitadel/internal/integration" - object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta" - user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta" ) func TestServer_SetPhone(t *testing.T) { diff --git a/internal/api/grpc/user/v2/query.go b/internal/api/grpc/user/v2/query.go index 2aaf0ad8ef..95262a66ae 100644 --- a/internal/api/grpc/user/v2/query.go +++ b/internal/api/grpc/user/v2/query.go @@ -11,7 +11,7 @@ import ( "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/query" "github.com/zitadel/zitadel/internal/zerrors" - user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta" + "github.com/zitadel/zitadel/pkg/grpc/user/v2" ) func (s *Server) GetUserByID(ctx context.Context, req *user.GetUserByIDRequest) (_ *user.GetUserByIDResponse, err error) { diff --git a/internal/api/grpc/user/v2/query_integration_test.go b/internal/api/grpc/user/v2/query_integration_test.go index b4b770481b..f7fcf8fbe5 100644 --- a/internal/api/grpc/user/v2/query_integration_test.go +++ b/internal/api/grpc/user/v2/query_integration_test.go @@ -13,9 +13,10 @@ import ( "github.com/stretchr/testify/require" "google.golang.org/protobuf/types/known/timestamppb" + object "github.com/zitadel/zitadel/pkg/grpc/object/v2" + user "github.com/zitadel/zitadel/pkg/grpc/user/v2" + "github.com/zitadel/zitadel/internal/integration" - object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta" - user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta" ) func TestServer_GetUserByID(t *testing.T) { diff --git a/internal/api/grpc/user/v2/server.go b/internal/api/grpc/user/v2/server.go index 93af47f58b..9272ea27ee 100644 --- a/internal/api/grpc/user/v2/server.go +++ b/internal/api/grpc/user/v2/server.go @@ -11,7 +11,7 @@ import ( "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/query" - user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta" + "github.com/zitadel/zitadel/pkg/grpc/user/v2" ) var _ user.UserServiceServer = (*Server)(nil) diff --git a/internal/api/grpc/user/v2/totp.go b/internal/api/grpc/user/v2/totp.go index e426f5788d..9e2d028d72 100644 --- a/internal/api/grpc/user/v2/totp.go +++ b/internal/api/grpc/user/v2/totp.go @@ -5,7 +5,7 @@ import ( "github.com/zitadel/zitadel/internal/api/grpc/object/v2" "github.com/zitadel/zitadel/internal/domain" - user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta" + "github.com/zitadel/zitadel/pkg/grpc/user/v2" ) func (s *Server) RegisterTOTP(ctx context.Context, req *user.RegisterTOTPRequest) (*user.RegisterTOTPResponse, error) { diff --git a/internal/api/grpc/user/v2/totp_integration_test.go b/internal/api/grpc/user/v2/totp_integration_test.go index 489c189b03..474aed95b8 100644 --- a/internal/api/grpc/user/v2/totp_integration_test.go +++ b/internal/api/grpc/user/v2/totp_integration_test.go @@ -12,9 +12,10 @@ import ( "github.com/stretchr/testify/require" "google.golang.org/protobuf/types/known/timestamppb" + "github.com/zitadel/zitadel/pkg/grpc/object/v2" + "github.com/zitadel/zitadel/pkg/grpc/user/v2" + "github.com/zitadel/zitadel/internal/integration" - object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta" - user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta" ) func TestServer_RegisterTOTP(t *testing.T) { diff --git a/internal/api/grpc/user/v2/totp_test.go b/internal/api/grpc/user/v2/totp_test.go index 81a54675f2..27ce6fb469 100644 --- a/internal/api/grpc/user/v2/totp_test.go +++ b/internal/api/grpc/user/v2/totp_test.go @@ -10,8 +10,8 @@ import ( "google.golang.org/protobuf/types/known/timestamppb" "github.com/zitadel/zitadel/internal/domain" - object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta" - user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta" + "github.com/zitadel/zitadel/pkg/grpc/object/v2" + "github.com/zitadel/zitadel/pkg/grpc/user/v2" ) func Test_totpDetailsToPb(t *testing.T) { diff --git a/internal/api/grpc/user/v2/u2f.go b/internal/api/grpc/user/v2/u2f.go index f13d21736e..60c0f5ab07 100644 --- a/internal/api/grpc/user/v2/u2f.go +++ b/internal/api/grpc/user/v2/u2f.go @@ -6,7 +6,7 @@ import ( "github.com/zitadel/zitadel/internal/api/grpc/object/v2" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/zerrors" - user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta" + "github.com/zitadel/zitadel/pkg/grpc/user/v2" ) func (s *Server) RegisterU2F(ctx context.Context, req *user.RegisterU2FRequest) (*user.RegisterU2FResponse, error) { @@ -40,3 +40,13 @@ func (s *Server) VerifyU2FRegistration(ctx context.Context, req *user.VerifyU2FR Details: object.DomainToDetailsPb(objectDetails), }, nil } + +func (s *Server) RemoveU2F(ctx context.Context, req *user.RemoveU2FRequest) (*user.RemoveU2FResponse, error) { + objectDetails, err := s.command.HumanRemoveU2F(ctx, req.GetUserId(), req.GetU2FId(), "") + if err != nil { + return nil, err + } + return &user.RemoveU2FResponse{ + Details: object.DomainToDetailsPb(objectDetails), + }, nil +} diff --git a/internal/api/grpc/user/v2/u2f_integration_test.go b/internal/api/grpc/user/v2/u2f_integration_test.go index 3b7fbd293c..c4d4c33071 100644 --- a/internal/api/grpc/user/v2/u2f_integration_test.go +++ b/internal/api/grpc/user/v2/u2f_integration_test.go @@ -11,9 +11,10 @@ import ( "google.golang.org/protobuf/types/known/structpb" "google.golang.org/protobuf/types/known/timestamppb" + "github.com/zitadel/zitadel/pkg/grpc/object/v2" + "github.com/zitadel/zitadel/pkg/grpc/user/v2" + "github.com/zitadel/zitadel/internal/integration" - object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta" - user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta" ) func TestServer_RegisterU2F(t *testing.T) { @@ -106,16 +107,7 @@ func TestServer_RegisterU2F(t *testing.T) { } func TestServer_VerifyU2FRegistration(t *testing.T) { - userID := Tester.CreateHumanUser(CTX).GetUserId() - Tester.RegisterUserPasskey(CTX, userID) - _, sessionToken, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, userID) - ctx := Tester.WithAuthorizationToken(CTX, sessionToken) - - pkr, err := Client.RegisterU2F(ctx, &user.RegisterU2FRequest{ - UserId: userID, - }) - require.NoError(t, err) - require.NotEmpty(t, pkr.GetPublicKeyCredentialCreationOptions()) + ctx, userID, pkr := ctxFromNewUserWithRegisteredU2F(t) attestationResponse, err := Tester.WebAuthN.CreateAttestationResponse(pkr.GetPublicKeyCredentialCreationOptions()) require.NoError(t, err) @@ -188,3 +180,138 @@ func TestServer_VerifyU2FRegistration(t *testing.T) { }) } } + +func ctxFromNewUserWithRegisteredU2F(t *testing.T) (context.Context, string, *user.RegisterU2FResponse) { + userID := Tester.CreateHumanUser(CTX).GetUserId() + Tester.RegisterUserPasskey(CTX, userID) + _, sessionToken, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, userID) + ctx := Tester.WithAuthorizationToken(CTX, sessionToken) + + pkr, err := Client.RegisterU2F(ctx, &user.RegisterU2FRequest{ + UserId: userID, + }) + require.NoError(t, err) + require.NotEmpty(t, pkr.GetPublicKeyCredentialCreationOptions()) + return ctx, userID, pkr +} + +func ctxFromNewUserWithVerifiedU2F(t *testing.T) (context.Context, string, string) { + ctx, userID, pkr := ctxFromNewUserWithRegisteredU2F(t) + + attestationResponse, err := Tester.WebAuthN.CreateAttestationResponse(pkr.GetPublicKeyCredentialCreationOptions()) + require.NoError(t, err) + + _, err = Client.VerifyU2FRegistration(ctx, &user.VerifyU2FRegistrationRequest{ + UserId: userID, + U2FId: pkr.GetU2FId(), + PublicKeyCredential: attestationResponse, + TokenName: "nice name", + }) + require.NoError(t, err) + return ctx, userID, pkr.GetU2FId() +} + +func TestServer_RemoveU2F(t *testing.T) { + userIDWithout := Tester.CreateHumanUser(CTX).GetUserId() + ctxRegistered, userIDRegistered, pkrRegistered := ctxFromNewUserWithRegisteredU2F(t) + _, userIDVerified, u2fVerified := ctxFromNewUserWithVerifiedU2F(t) + _, userIDVerifiedPermission, u2fVerifiedPermission := ctxFromNewUserWithVerifiedU2F(t) + + type args struct { + ctx context.Context + req *user.RemoveU2FRequest + } + tests := []struct { + name string + args args + want *user.RemoveU2FResponse + wantErr bool + }{ + { + name: "missing user id", + args: args{ + ctx: ctxRegistered, + req: &user.RemoveU2FRequest{ + U2FId: "123", + }, + }, + wantErr: true, + }, + { + name: "missing u2f id", + args: args{ + ctx: ctxRegistered, + req: &user.RemoveU2FRequest{ + UserId: "123", + }, + }, + wantErr: true, + }, + { + name: "success, registered", + args: args{ + ctx: ctxRegistered, + req: &user.RemoveU2FRequest{ + UserId: userIDRegistered, + U2FId: pkrRegistered.GetU2FId(), + }, + }, + want: &user.RemoveU2FResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Organisation.ID, + }, + }, + }, + { + name: "no u2f, error", + args: args{ + ctx: IamCTX, + req: &user.RemoveU2FRequest{ + UserId: userIDWithout, + U2FId: pkrRegistered.GetU2FId(), + }, + }, + wantErr: true, + }, + { + name: "success, IAMOwner permission, verified", + args: args{ + ctx: IamCTX, + req: &user.RemoveU2FRequest{ + UserId: userIDVerified, + U2FId: u2fVerified, + }, + }, + want: &user.RemoveU2FResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Organisation.ID, + }, + }, + }, + { + name: "verified, permission error", + args: args{ + ctx: UserCTX, + req: &user.RemoveU2FRequest{ + UserId: userIDVerifiedPermission, + U2FId: u2fVerifiedPermission, + }, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Client.RemoveU2F(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + require.NotNil(t, got) + integration.AssertDetails(t, tt.want, got) + }) + } +} diff --git a/internal/api/grpc/user/v2/u2f_test.go b/internal/api/grpc/user/v2/u2f_test.go index 087837ce3c..73366ab29b 100644 --- a/internal/api/grpc/user/v2/u2f_test.go +++ b/internal/api/grpc/user/v2/u2f_test.go @@ -13,8 +13,8 @@ import ( "github.com/zitadel/zitadel/internal/api/grpc" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/zerrors" - object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta" - user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta" + "github.com/zitadel/zitadel/pkg/grpc/object/v2" + "github.com/zitadel/zitadel/pkg/grpc/user/v2" ) func Test_u2fRegistrationDetailsToPb(t *testing.T) { diff --git a/internal/api/grpc/user/v2/user.go b/internal/api/grpc/user/v2/user.go index dd96f3107a..9ad83dfea3 100644 --- a/internal/api/grpc/user/v2/user.go +++ b/internal/api/grpc/user/v2/user.go @@ -18,11 +18,12 @@ import ( "github.com/zitadel/zitadel/internal/idp/providers/ldap" "github.com/zitadel/zitadel/internal/query" "github.com/zitadel/zitadel/internal/zerrors" - object_pb "github.com/zitadel/zitadel/pkg/grpc/object/v2beta" - user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta" + object_pb "github.com/zitadel/zitadel/pkg/grpc/object/v2" + "github.com/zitadel/zitadel/pkg/grpc/user/v2" ) func (s *Server) AddHumanUser(ctx context.Context, req *user.AddHumanUserRequest) (_ *user.AddHumanUserResponse, err error) { + human, err := AddUserRequestToAddHuman(req) if err != nil { return nil, err @@ -259,20 +260,6 @@ func SetHumanPasswordToPassword(password *user.SetPassword) *command.Password { } } -func (s *Server) AddIDPLink(ctx context.Context, req *user.AddIDPLinkRequest) (_ *user.AddIDPLinkResponse, err error) { - details, err := s.command.AddUserIDPLink(ctx, req.UserId, "", &command.AddLink{ - IDPID: req.GetIdpLink().GetIdpId(), - DisplayName: req.GetIdpLink().GetUserName(), - IDPExternalID: req.GetIdpLink().GetUserId(), - }) - if err != nil { - return nil, err - } - return &user.AddIDPLinkResponse{ - Details: object.DomainToDetailsPb(details), - }, nil -} - 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 { diff --git a/internal/api/grpc/user/v2/user_integration_test.go b/internal/api/grpc/user/v2/user_integration_test.go index 824029724e..e762c10181 100644 --- a/internal/api/grpc/user/v2/user_integration_test.go +++ b/internal/api/grpc/user/v2/user_integration_test.go @@ -18,12 +18,13 @@ import ( "google.golang.org/protobuf/types/known/structpb" "google.golang.org/protobuf/types/known/timestamppb" + "github.com/zitadel/zitadel/pkg/grpc/object/v2" + "github.com/zitadel/zitadel/pkg/grpc/user/v2" + "github.com/zitadel/zitadel/internal/api/grpc" "github.com/zitadel/zitadel/internal/integration" "github.com/zitadel/zitadel/pkg/grpc/idp" mgmt "github.com/zitadel/zitadel/pkg/grpc/management" - object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta" - user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta" ) var ( @@ -1797,86 +1798,6 @@ func TestServer_DeleteUser(t *testing.T) { } } -func TestServer_AddIDPLink(t *testing.T) { - idpID := Tester.AddGenericOAuthProvider(t, CTX) - type args struct { - ctx context.Context - req *user.AddIDPLinkRequest - } - tests := []struct { - name string - args args - want *user.AddIDPLinkResponse - wantErr bool - }{ - { - name: "user does not exist", - args: args{ - CTX, - &user.AddIDPLinkRequest{ - UserId: "userID", - IdpLink: &user.IDPLink{ - IdpId: idpID, - UserId: "userID", - UserName: "username", - }, - }, - }, - want: nil, - wantErr: true, - }, - { - name: "idp does not exist", - args: args{ - CTX, - &user.AddIDPLinkRequest{ - UserId: Tester.Users[integration.FirstInstanceUsersKey][integration.OrgOwner].ID, - IdpLink: &user.IDPLink{ - IdpId: "idpID", - UserId: "userID", - UserName: "username", - }, - }, - }, - want: nil, - wantErr: true, - }, - { - name: "add link", - args: args{ - CTX, - &user.AddIDPLinkRequest{ - UserId: Tester.Users[integration.FirstInstanceUsersKey][integration.OrgOwner].ID, - IdpLink: &user.IDPLink{ - IdpId: idpID, - UserId: "userID", - UserName: "username", - }, - }, - }, - want: &user.AddIDPLinkResponse{ - Details: &object.Details{ - ChangeDate: timestamppb.Now(), - ResourceOwner: Tester.Organisation.ID, - }, - }, - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := Client.AddIDPLink(tt.args.ctx, tt.args.req) - if tt.wantErr { - require.Error(t, err) - } else { - require.NoError(t, err) - } - - integration.AssertDetails(t, tt.want, got) - }) - } -} - func TestServer_StartIdentityProviderIntent(t *testing.T) { idpID := Tester.AddGenericOAuthProvider(t, CTX) orgIdpID := Tester.AddOrgGenericOAuthProvider(t, CTX, Tester.Organisation.ID) diff --git a/internal/api/grpc/user/v2/user_test.go b/internal/api/grpc/user/v2/user_test.go index 9e398e83ff..9e7a5a5ab0 100644 --- a/internal/api/grpc/user/v2/user_test.go +++ b/internal/api/grpc/user/v2/user_test.go @@ -17,8 +17,8 @@ import ( "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/zerrors" - object_pb "github.com/zitadel/zitadel/pkg/grpc/object/v2beta" - user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta" + object_pb "github.com/zitadel/zitadel/pkg/grpc/object/v2" + "github.com/zitadel/zitadel/pkg/grpc/user/v2" ) func Test_idpIntentToIDPIntentPb(t *testing.T) { diff --git a/internal/api/grpc/user/v2beta/email.go b/internal/api/grpc/user/v2beta/email.go new file mode 100644 index 0000000000..38cc73c75c --- /dev/null +++ b/internal/api/grpc/user/v2beta/email.go @@ -0,0 +1,86 @@ +package user + +import ( + "context" + + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/zerrors" + object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta" + user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta" +) + +func (s *Server) SetEmail(ctx context.Context, req *user.SetEmailRequest) (resp *user.SetEmailResponse, err error) { + var email *domain.Email + + switch v := req.GetVerification().(type) { + case *user.SetEmailRequest_SendCode: + email, err = s.command.ChangeUserEmailURLTemplate(ctx, req.GetUserId(), req.GetEmail(), s.userCodeAlg, v.SendCode.GetUrlTemplate()) + case *user.SetEmailRequest_ReturnCode: + email, err = s.command.ChangeUserEmailReturnCode(ctx, req.GetUserId(), req.GetEmail(), s.userCodeAlg) + case *user.SetEmailRequest_IsVerified: + email, err = s.command.ChangeUserEmailVerified(ctx, req.GetUserId(), req.GetEmail()) + case nil: + email, err = s.command.ChangeUserEmail(ctx, req.GetUserId(), req.GetEmail(), s.userCodeAlg) + default: + err = zerrors.ThrowUnimplementedf(nil, "USERv2-Ahng0", "verification oneOf %T in method SetEmail not implemented", v) + } + if err != nil { + return nil, err + } + + return &user.SetEmailResponse{ + Details: &object.Details{ + Sequence: email.Sequence, + ChangeDate: timestamppb.New(email.ChangeDate), + ResourceOwner: email.ResourceOwner, + }, + VerificationCode: email.PlainCode, + }, nil +} + +func (s *Server) ResendEmailCode(ctx context.Context, req *user.ResendEmailCodeRequest) (resp *user.ResendEmailCodeResponse, err error) { + var email *domain.Email + + switch v := req.GetVerification().(type) { + case *user.ResendEmailCodeRequest_SendCode: + email, err = s.command.ResendUserEmailCodeURLTemplate(ctx, req.GetUserId(), s.userCodeAlg, v.SendCode.GetUrlTemplate()) + case *user.ResendEmailCodeRequest_ReturnCode: + email, err = s.command.ResendUserEmailReturnCode(ctx, req.GetUserId(), s.userCodeAlg) + case nil: + email, err = s.command.ResendUserEmailCode(ctx, req.GetUserId(), s.userCodeAlg) + default: + err = zerrors.ThrowUnimplementedf(nil, "USERv2-faj0l0nj5x", "verification oneOf %T in method ResendEmailCode not implemented", v) + } + if err != nil { + return nil, err + } + + return &user.ResendEmailCodeResponse{ + Details: &object.Details{ + Sequence: email.Sequence, + ChangeDate: timestamppb.New(email.ChangeDate), + ResourceOwner: email.ResourceOwner, + }, + VerificationCode: email.PlainCode, + }, nil +} + +func (s *Server) VerifyEmail(ctx context.Context, req *user.VerifyEmailRequest) (*user.VerifyEmailResponse, error) { + details, err := s.command.VerifyUserEmail(ctx, + req.GetUserId(), + req.GetVerificationCode(), + s.userCodeAlg, + ) + if err != nil { + return nil, err + } + return &user.VerifyEmailResponse{ + Details: &object.Details{ + Sequence: details.Sequence, + ChangeDate: timestamppb.New(details.EventDate), + ResourceOwner: details.ResourceOwner, + }, + }, nil +} diff --git a/internal/api/grpc/user/v2beta/email_integration_test.go b/internal/api/grpc/user/v2beta/email_integration_test.go new file mode 100644 index 0000000000..4034a5e7da --- /dev/null +++ b/internal/api/grpc/user/v2beta/email_integration_test.go @@ -0,0 +1,297 @@ +//go:build integration + +package user_test + +import ( + "fmt" + "testing" + "time" + + "github.com/muhlemmer/gu" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/integration" + object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta" + user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta" +) + +func TestServer_SetEmail(t *testing.T) { + userID := Tester.CreateHumanUser(CTX).GetUserId() + + tests := []struct { + name string + req *user.SetEmailRequest + want *user.SetEmailResponse + wantErr bool + }{ + { + name: "user not existing", + req: &user.SetEmailRequest{ + UserId: "xxx", + Email: "default-verifier@mouse.com", + }, + wantErr: true, + }, + { + name: "default verfication", + req: &user.SetEmailRequest{ + UserId: userID, + Email: "default-verifier@mouse.com", + }, + want: &user.SetEmailResponse{ + Details: &object.Details{ + Sequence: 1, + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Organisation.ID, + }, + }, + }, + { + name: "custom url template", + req: &user.SetEmailRequest{ + UserId: userID, + Email: "custom-url@mouse.com", + Verification: &user.SetEmailRequest_SendCode{ + SendCode: &user.SendEmailVerificationCode{ + UrlTemplate: gu.Ptr("https://example.com/email/verify?userID={{.UserID}}&code={{.Code}}&orgID={{.OrgID}}"), + }, + }, + }, + want: &user.SetEmailResponse{ + Details: &object.Details{ + Sequence: 1, + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Organisation.ID, + }, + }, + }, + { + name: "template error", + req: &user.SetEmailRequest{ + UserId: userID, + Email: "custom-url@mouse.com", + Verification: &user.SetEmailRequest_SendCode{ + SendCode: &user.SendEmailVerificationCode{ + UrlTemplate: gu.Ptr("{{"), + }, + }, + }, + wantErr: true, + }, + { + name: "return code", + req: &user.SetEmailRequest{ + UserId: userID, + Email: "return-code@mouse.com", + Verification: &user.SetEmailRequest_ReturnCode{ + ReturnCode: &user.ReturnEmailVerificationCode{}, + }, + }, + want: &user.SetEmailResponse{ + Details: &object.Details{ + Sequence: 1, + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Organisation.ID, + }, + VerificationCode: gu.Ptr("xxx"), + }, + }, + { + name: "is verified true", + req: &user.SetEmailRequest{ + UserId: userID, + Email: "verified-true@mouse.com", + Verification: &user.SetEmailRequest_IsVerified{ + IsVerified: true, + }, + }, + want: &user.SetEmailResponse{ + Details: &object.Details{ + Sequence: 1, + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Organisation.ID, + }, + }, + }, + { + name: "is verified false", + req: &user.SetEmailRequest{ + UserId: userID, + Email: "verified-false@mouse.com", + Verification: &user.SetEmailRequest_IsVerified{ + IsVerified: false, + }, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Client.SetEmail(CTX, tt.req) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + integration.AssertDetails(t, tt.want, got) + if tt.want.GetVerificationCode() != "" { + assert.NotEmpty(t, got.GetVerificationCode()) + } + }) + } +} + +func TestServer_ResendEmailCode(t *testing.T) { + userID := Tester.CreateHumanUser(CTX).GetUserId() + verifiedUserID := Tester.CreateHumanUserVerified(CTX, Tester.Organisation.ID, fmt.Sprintf("%d@mouse.com", time.Now().UnixNano())).GetUserId() + + tests := []struct { + name string + req *user.ResendEmailCodeRequest + want *user.ResendEmailCodeResponse + wantErr bool + }{ + { + name: "user not existing", + req: &user.ResendEmailCodeRequest{ + UserId: "xxx", + }, + wantErr: true, + }, + { + name: "user no code", + req: &user.ResendEmailCodeRequest{ + UserId: verifiedUserID, + }, + wantErr: true, + }, + { + name: "resend", + req: &user.ResendEmailCodeRequest{ + UserId: userID, + }, + want: &user.ResendEmailCodeResponse{ + Details: &object.Details{ + Sequence: 1, + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Organisation.ID, + }, + }, + }, + { + name: "custom url template", + req: &user.ResendEmailCodeRequest{ + UserId: userID, + Verification: &user.ResendEmailCodeRequest_SendCode{ + SendCode: &user.SendEmailVerificationCode{ + UrlTemplate: gu.Ptr("https://example.com/email/verify?userID={{.UserID}}&code={{.Code}}&orgID={{.OrgID}}"), + }, + }, + }, + want: &user.ResendEmailCodeResponse{ + Details: &object.Details{ + Sequence: 1, + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Organisation.ID, + }, + }, + }, + { + name: "template error", + req: &user.ResendEmailCodeRequest{ + UserId: userID, + Verification: &user.ResendEmailCodeRequest_SendCode{ + SendCode: &user.SendEmailVerificationCode{ + UrlTemplate: gu.Ptr("{{"), + }, + }, + }, + wantErr: true, + }, + { + name: "return code", + req: &user.ResendEmailCodeRequest{ + UserId: userID, + Verification: &user.ResendEmailCodeRequest_ReturnCode{ + ReturnCode: &user.ReturnEmailVerificationCode{}, + }, + }, + want: &user.ResendEmailCodeResponse{ + Details: &object.Details{ + Sequence: 1, + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Organisation.ID, + }, + VerificationCode: gu.Ptr("xxx"), + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Client.ResendEmailCode(CTX, tt.req) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + integration.AssertDetails(t, tt.want, got) + if tt.want.GetVerificationCode() != "" { + assert.NotEmpty(t, got.GetVerificationCode()) + } + }) + } +} + +func TestServer_VerifyEmail(t *testing.T) { + userResp := Tester.CreateHumanUser(CTX) + tests := []struct { + name string + req *user.VerifyEmailRequest + want *user.VerifyEmailResponse + wantErr bool + }{ + { + name: "wrong code", + req: &user.VerifyEmailRequest{ + UserId: userResp.GetUserId(), + VerificationCode: "xxx", + }, + wantErr: true, + }, + { + name: "wrong user", + req: &user.VerifyEmailRequest{ + UserId: "xxx", + VerificationCode: userResp.GetEmailCode(), + }, + wantErr: true, + }, + { + name: "verify user", + req: &user.VerifyEmailRequest{ + UserId: userResp.GetUserId(), + VerificationCode: userResp.GetEmailCode(), + }, + want: &user.VerifyEmailResponse{ + Details: &object.Details{ + Sequence: 1, + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Organisation.ID, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Client.VerifyEmail(CTX, tt.req) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + integration.AssertDetails(t, tt.want, got) + }) + } +} diff --git a/internal/api/grpc/user/v2beta/otp.go b/internal/api/grpc/user/v2beta/otp.go new file mode 100644 index 0000000000..c11aa4c1a4 --- /dev/null +++ b/internal/api/grpc/user/v2beta/otp.go @@ -0,0 +1,42 @@ +package user + +import ( + "context" + + object "github.com/zitadel/zitadel/internal/api/grpc/object/v2beta" + user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta" +) + +func (s *Server) AddOTPSMS(ctx context.Context, req *user.AddOTPSMSRequest) (*user.AddOTPSMSResponse, error) { + details, err := s.command.AddHumanOTPSMS(ctx, req.GetUserId(), "") + if err != nil { + return nil, err + } + return &user.AddOTPSMSResponse{Details: object.DomainToDetailsPb(details)}, nil + +} + +func (s *Server) RemoveOTPSMS(ctx context.Context, req *user.RemoveOTPSMSRequest) (*user.RemoveOTPSMSResponse, error) { + objectDetails, err := s.command.RemoveHumanOTPSMS(ctx, req.GetUserId(), "") + if err != nil { + return nil, err + } + return &user.RemoveOTPSMSResponse{Details: object.DomainToDetailsPb(objectDetails)}, nil +} + +func (s *Server) AddOTPEmail(ctx context.Context, req *user.AddOTPEmailRequest) (*user.AddOTPEmailResponse, error) { + details, err := s.command.AddHumanOTPEmail(ctx, req.GetUserId(), "") + if err != nil { + return nil, err + } + return &user.AddOTPEmailResponse{Details: object.DomainToDetailsPb(details)}, nil + +} + +func (s *Server) RemoveOTPEmail(ctx context.Context, req *user.RemoveOTPEmailRequest) (*user.RemoveOTPEmailResponse, error) { + objectDetails, err := s.command.RemoveHumanOTPEmail(ctx, req.GetUserId(), "") + if err != nil { + return nil, err + } + return &user.RemoveOTPEmailResponse{Details: object.DomainToDetailsPb(objectDetails)}, nil +} diff --git a/internal/api/grpc/user/v2beta/otp_integration_test.go b/internal/api/grpc/user/v2beta/otp_integration_test.go new file mode 100644 index 0000000000..a6d671c645 --- /dev/null +++ b/internal/api/grpc/user/v2beta/otp_integration_test.go @@ -0,0 +1,362 @@ +//go:build integration + +package user_test + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/integration" + object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta" + user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta" +) + +func TestServer_AddOTPSMS(t *testing.T) { + userID := Tester.CreateHumanUser(CTX).GetUserId() + Tester.RegisterUserPasskey(CTX, userID) + _, sessionToken, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, userID) + + otherUser := Tester.CreateHumanUser(CTX).GetUserId() + Tester.RegisterUserPasskey(CTX, otherUser) + _, sessionTokenOtherUser, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, otherUser) + + userVerified := Tester.CreateHumanUser(CTX) + _, err := Client.VerifyPhone(CTX, &user.VerifyPhoneRequest{ + UserId: userVerified.GetUserId(), + VerificationCode: userVerified.GetPhoneCode(), + }) + require.NoError(t, err) + Tester.RegisterUserPasskey(CTX, userVerified.GetUserId()) + _, sessionTokenVerified, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, userVerified.GetUserId()) + + userVerified2 := Tester.CreateHumanUser(CTX) + _, err = Client.VerifyPhone(CTX, &user.VerifyPhoneRequest{ + UserId: userVerified2.GetUserId(), + VerificationCode: userVerified2.GetPhoneCode(), + }) + require.NoError(t, err) + + type args struct { + ctx context.Context + req *user.AddOTPSMSRequest + } + tests := []struct { + name string + args args + want *user.AddOTPSMSResponse + wantErr bool + }{ + { + name: "missing user id", + args: args{ + ctx: CTX, + req: &user.AddOTPSMSRequest{}, + }, + wantErr: true, + }, + { + name: "user mismatch", + args: args{ + ctx: Tester.WithAuthorizationToken(context.Background(), sessionTokenOtherUser), + req: &user.AddOTPSMSRequest{ + UserId: userID, + }, + }, + wantErr: true, + }, + { + name: "phone not verified", + args: args{ + ctx: Tester.WithAuthorizationToken(context.Background(), sessionToken), + req: &user.AddOTPSMSRequest{ + UserId: userID, + }, + }, + wantErr: true, + }, + { + name: "add success", + args: args{ + ctx: Tester.WithAuthorizationToken(context.Background(), sessionTokenVerified), + req: &user.AddOTPSMSRequest{ + UserId: userVerified.GetUserId(), + }, + }, + want: &user.AddOTPSMSResponse{ + Details: &object.Details{ + ResourceOwner: Tester.Organisation.ID, + }, + }, + }, + { + name: "add success, admin", + args: args{ + ctx: CTX, + req: &user.AddOTPSMSRequest{ + UserId: userVerified2.GetUserId(), + }, + }, + want: &user.AddOTPSMSResponse{ + Details: &object.Details{ + ResourceOwner: Tester.Organisation.ID, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Client.AddOTPSMS(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + require.NotNil(t, got) + integration.AssertDetails(t, tt.want, got) + }) + } +} + +func TestServer_RemoveOTPSMS(t *testing.T) { + userID := Tester.CreateHumanUser(CTX).GetUserId() + Tester.RegisterUserPasskey(CTX, userID) + _, sessionToken, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, userID) + + userVerified := Tester.CreateHumanUser(CTX) + Tester.RegisterUserPasskey(CTX, userVerified.GetUserId()) + _, sessionTokenVerified, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, userVerified.GetUserId()) + userVerifiedCtx := Tester.WithAuthorizationToken(context.Background(), sessionTokenVerified) + _, err := Client.VerifyPhone(userVerifiedCtx, &user.VerifyPhoneRequest{ + UserId: userVerified.GetUserId(), + VerificationCode: userVerified.GetPhoneCode(), + }) + require.NoError(t, err) + _, err = Client.AddOTPSMS(userVerifiedCtx, &user.AddOTPSMSRequest{UserId: userVerified.GetUserId()}) + require.NoError(t, err) + + type args struct { + ctx context.Context + req *user.RemoveOTPSMSRequest + } + tests := []struct { + name string + args args + want *user.RemoveOTPSMSResponse + wantErr bool + }{ + { + name: "not added", + args: args{ + ctx: Tester.WithAuthorizationToken(context.Background(), sessionToken), + req: &user.RemoveOTPSMSRequest{ + UserId: userID, + }, + }, + wantErr: true, + }, + { + name: "success", + args: args{ + ctx: userVerifiedCtx, + req: &user.RemoveOTPSMSRequest{ + UserId: userVerified.GetUserId(), + }, + }, + want: &user.RemoveOTPSMSResponse{ + Details: &object.Details{ + ResourceOwner: Tester.Organisation.ResourceOwner, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Client.RemoveOTPSMS(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + require.NotNil(t, got) + integration.AssertDetails(t, tt.want, got) + }) + } +} + +func TestServer_AddOTPEmail(t *testing.T) { + userID := Tester.CreateHumanUser(CTX).GetUserId() + Tester.RegisterUserPasskey(CTX, userID) + _, sessionToken, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, userID) + + otherUser := Tester.CreateHumanUser(CTX).GetUserId() + Tester.RegisterUserPasskey(CTX, otherUser) + _, sessionTokenOtherUser, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, otherUser) + + userVerified := Tester.CreateHumanUser(CTX) + _, err := Client.VerifyEmail(CTX, &user.VerifyEmailRequest{ + UserId: userVerified.GetUserId(), + VerificationCode: userVerified.GetEmailCode(), + }) + require.NoError(t, err) + Tester.RegisterUserPasskey(CTX, userVerified.GetUserId()) + _, sessionTokenVerified, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, userVerified.GetUserId()) + + userVerified2 := Tester.CreateHumanUser(CTX) + _, err = Client.VerifyEmail(CTX, &user.VerifyEmailRequest{ + UserId: userVerified2.GetUserId(), + VerificationCode: userVerified2.GetEmailCode(), + }) + require.NoError(t, err) + + type args struct { + ctx context.Context + req *user.AddOTPEmailRequest + } + tests := []struct { + name string + args args + want *user.AddOTPEmailResponse + wantErr bool + }{ + { + name: "missing user id", + args: args{ + ctx: CTX, + req: &user.AddOTPEmailRequest{}, + }, + wantErr: true, + }, + { + name: "user mismatch", + args: args{ + ctx: Tester.WithAuthorizationToken(context.Background(), sessionTokenOtherUser), + req: &user.AddOTPEmailRequest{ + UserId: userID, + }, + }, + wantErr: true, + }, + { + name: "email not verified", + args: args{ + ctx: Tester.WithAuthorizationToken(context.Background(), sessionToken), + req: &user.AddOTPEmailRequest{ + UserId: userID, + }, + }, + wantErr: true, + }, + { + name: "add success", + args: args{ + ctx: Tester.WithAuthorizationToken(context.Background(), sessionTokenVerified), + req: &user.AddOTPEmailRequest{ + UserId: userVerified.GetUserId(), + }, + }, + want: &user.AddOTPEmailResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Organisation.ID, + }, + }, + }, + { + name: "add success, admin", + args: args{ + ctx: CTX, + req: &user.AddOTPEmailRequest{ + UserId: userVerified2.GetUserId(), + }, + }, + want: &user.AddOTPEmailResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Organisation.ID, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Client.AddOTPEmail(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + require.NotNil(t, got) + integration.AssertDetails(t, tt.want, got) + }) + } +} + +func TestServer_RemoveOTPEmail(t *testing.T) { + userID := Tester.CreateHumanUser(CTX).GetUserId() + Tester.RegisterUserPasskey(CTX, userID) + _, sessionToken, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, userID) + + userVerified := Tester.CreateHumanUser(CTX) + Tester.RegisterUserPasskey(CTX, userVerified.GetUserId()) + _, sessionTokenVerified, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, userVerified.GetUserId()) + userVerifiedCtx := Tester.WithAuthorizationToken(context.Background(), sessionTokenVerified) + _, err := Client.VerifyEmail(userVerifiedCtx, &user.VerifyEmailRequest{ + UserId: userVerified.GetUserId(), + VerificationCode: userVerified.GetEmailCode(), + }) + require.NoError(t, err) + _, err = Client.AddOTPEmail(userVerifiedCtx, &user.AddOTPEmailRequest{UserId: userVerified.GetUserId()}) + require.NoError(t, err) + + type args struct { + ctx context.Context + req *user.RemoveOTPEmailRequest + } + tests := []struct { + name string + args args + want *user.RemoveOTPEmailResponse + wantErr bool + }{ + { + name: "not added", + args: args{ + ctx: Tester.WithAuthorizationToken(context.Background(), sessionToken), + req: &user.RemoveOTPEmailRequest{ + UserId: userID, + }, + }, + wantErr: true, + }, + { + name: "success", + args: args{ + ctx: userVerifiedCtx, + req: &user.RemoveOTPEmailRequest{ + UserId: userVerified.GetUserId(), + }, + }, + want: &user.RemoveOTPEmailResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Organisation.ResourceOwner, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Client.RemoveOTPEmail(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + require.NotNil(t, got) + integration.AssertDetails(t, tt.want, got) + }) + } +} diff --git a/internal/api/grpc/user/v2beta/passkey.go b/internal/api/grpc/user/v2beta/passkey.go new file mode 100644 index 0000000000..2df267f3fd --- /dev/null +++ b/internal/api/grpc/user/v2beta/passkey.go @@ -0,0 +1,118 @@ +package user + +import ( + "context" + + "google.golang.org/protobuf/types/known/structpb" + + object "github.com/zitadel/zitadel/internal/api/grpc/object/v2beta" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/zerrors" + object_pb "github.com/zitadel/zitadel/pkg/grpc/object/v2beta" + user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta" +) + +func (s *Server) RegisterPasskey(ctx context.Context, req *user.RegisterPasskeyRequest) (resp *user.RegisterPasskeyResponse, err error) { + var ( + authenticator = passkeyAuthenticatorToDomain(req.GetAuthenticator()) + ) + if code := req.GetCode(); code != nil { + return passkeyRegistrationDetailsToPb( + s.command.RegisterUserPasskeyWithCode(ctx, req.GetUserId(), "", authenticator, code.Id, code.Code, req.GetDomain(), s.userCodeAlg), + ) + } + return passkeyRegistrationDetailsToPb( + s.command.RegisterUserPasskey(ctx, req.GetUserId(), "", req.GetDomain(), authenticator), + ) +} + +func passkeyAuthenticatorToDomain(pa user.PasskeyAuthenticator) domain.AuthenticatorAttachment { + switch pa { + case user.PasskeyAuthenticator_PASSKEY_AUTHENTICATOR_UNSPECIFIED: + return domain.AuthenticatorAttachmentUnspecified + case user.PasskeyAuthenticator_PASSKEY_AUTHENTICATOR_PLATFORM: + return domain.AuthenticatorAttachmentPlattform + case user.PasskeyAuthenticator_PASSKEY_AUTHENTICATOR_CROSS_PLATFORM: + return domain.AuthenticatorAttachmentCrossPlattform + default: + return domain.AuthenticatorAttachmentUnspecified + } +} + +func webAuthNRegistrationDetailsToPb(details *domain.WebAuthNRegistrationDetails, err error) (*object_pb.Details, *structpb.Struct, error) { + if err != nil { + return nil, nil, err + } + options := new(structpb.Struct) + if err := options.UnmarshalJSON(details.PublicKeyCredentialCreationOptions); err != nil { + return nil, nil, zerrors.ThrowInternal(err, "USERv2-Dohr6", "Errors.Internal") + } + return object.DomainToDetailsPb(details.ObjectDetails), options, nil +} + +func passkeyRegistrationDetailsToPb(details *domain.WebAuthNRegistrationDetails, err error) (*user.RegisterPasskeyResponse, error) { + objectDetails, options, err := webAuthNRegistrationDetailsToPb(details, err) + if err != nil { + return nil, err + } + return &user.RegisterPasskeyResponse{ + Details: objectDetails, + PasskeyId: details.ID, + PublicKeyCredentialCreationOptions: options, + }, nil +} + +func (s *Server) VerifyPasskeyRegistration(ctx context.Context, req *user.VerifyPasskeyRegistrationRequest) (*user.VerifyPasskeyRegistrationResponse, error) { + pkc, err := req.GetPublicKeyCredential().MarshalJSON() + if err != nil { + return nil, zerrors.ThrowInternal(err, "USERv2-Pha2o", "Errors.Internal") + } + objectDetails, err := s.command.HumanHumanPasswordlessSetup(ctx, req.GetUserId(), "", req.GetPasskeyName(), "", pkc) + if err != nil { + return nil, err + } + return &user.VerifyPasskeyRegistrationResponse{ + Details: object.DomainToDetailsPb(objectDetails), + }, nil +} + +func (s *Server) CreatePasskeyRegistrationLink(ctx context.Context, req *user.CreatePasskeyRegistrationLinkRequest) (resp *user.CreatePasskeyRegistrationLinkResponse, err error) { + switch medium := req.Medium.(type) { + case nil: + return passkeyDetailsToPb( + s.command.AddUserPasskeyCode(ctx, req.GetUserId(), "", s.userCodeAlg), + ) + case *user.CreatePasskeyRegistrationLinkRequest_SendLink: + return passkeyDetailsToPb( + s.command.AddUserPasskeyCodeURLTemplate(ctx, req.GetUserId(), "", s.userCodeAlg, medium.SendLink.GetUrlTemplate()), + ) + case *user.CreatePasskeyRegistrationLinkRequest_ReturnCode: + return passkeyCodeDetailsToPb( + s.command.AddUserPasskeyCodeReturn(ctx, req.GetUserId(), "", s.userCodeAlg), + ) + default: + return nil, zerrors.ThrowUnimplementedf(nil, "USERv2-gaD8y", "verification oneOf %T in method CreatePasskeyRegistrationLink not implemented", medium) + } +} + +func passkeyDetailsToPb(details *domain.ObjectDetails, err error) (*user.CreatePasskeyRegistrationLinkResponse, error) { + if err != nil { + return nil, err + } + return &user.CreatePasskeyRegistrationLinkResponse{ + Details: object.DomainToDetailsPb(details), + }, nil +} + +func passkeyCodeDetailsToPb(details *domain.PasskeyCodeDetails, err error) (*user.CreatePasskeyRegistrationLinkResponse, error) { + if err != nil { + return nil, err + } + return &user.CreatePasskeyRegistrationLinkResponse{ + Details: object.DomainToDetailsPb(details.ObjectDetails), + Code: &user.PasskeyRegistrationCode{ + Id: details.CodeID, + Code: details.Code, + }, + }, nil +} diff --git a/internal/api/grpc/user/v2beta/passkey_integration_test.go b/internal/api/grpc/user/v2beta/passkey_integration_test.go new file mode 100644 index 0000000000..230a744a64 --- /dev/null +++ b/internal/api/grpc/user/v2beta/passkey_integration_test.go @@ -0,0 +1,319 @@ +//go:build integration + +package user_test + +import ( + "context" + "testing" + + "github.com/muhlemmer/gu" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/structpb" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/integration" + object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta" + user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta" +) + +func TestServer_RegisterPasskey(t *testing.T) { + userID := Tester.CreateHumanUser(CTX).GetUserId() + reg, err := Client.CreatePasskeyRegistrationLink(CTX, &user.CreatePasskeyRegistrationLinkRequest{ + UserId: userID, + Medium: &user.CreatePasskeyRegistrationLinkRequest_ReturnCode{}, + }) + require.NoError(t, err) + + // We also need a user session + Tester.RegisterUserPasskey(CTX, userID) + _, sessionToken, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, userID) + + type args struct { + ctx context.Context + req *user.RegisterPasskeyRequest + } + tests := []struct { + name string + args args + want *user.RegisterPasskeyResponse + wantErr bool + }{ + { + name: "missing user id", + args: args{ + ctx: CTX, + req: &user.RegisterPasskeyRequest{}, + }, + wantErr: true, + }, + { + name: "register code", + args: args{ + ctx: CTX, + req: &user.RegisterPasskeyRequest{ + UserId: userID, + Code: reg.GetCode(), + Authenticator: user.PasskeyAuthenticator_PASSKEY_AUTHENTICATOR_PLATFORM, + }, + }, + want: &user.RegisterPasskeyResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Organisation.ID, + }, + }, + }, + { + name: "reuse code (not allowed)", + args: args{ + ctx: CTX, + req: &user.RegisterPasskeyRequest{ + UserId: userID, + Code: reg.GetCode(), + Authenticator: user.PasskeyAuthenticator_PASSKEY_AUTHENTICATOR_PLATFORM, + }, + }, + wantErr: true, + }, + { + name: "wrong code", + args: args{ + ctx: CTX, + req: &user.RegisterPasskeyRequest{ + UserId: userID, + Code: &user.PasskeyRegistrationCode{ + Id: reg.GetCode().GetId(), + Code: "foobar", + }, + Authenticator: user.PasskeyAuthenticator_PASSKEY_AUTHENTICATOR_CROSS_PLATFORM, + }, + }, + wantErr: true, + }, + { + name: "user mismatch", + args: args{ + ctx: CTX, + req: &user.RegisterPasskeyRequest{ + UserId: userID, + }, + }, + wantErr: true, + }, + { + name: "user setting its own passkey", + args: args{ + ctx: Tester.WithAuthorizationToken(CTX, sessionToken), + req: &user.RegisterPasskeyRequest{ + UserId: userID, + }, + }, + want: &user.RegisterPasskeyResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Organisation.ID, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Client.RegisterPasskey(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + require.NotNil(t, got) + integration.AssertDetails(t, tt.want, got) + if tt.want != nil { + assert.NotEmpty(t, got.GetPasskeyId()) + assert.NotEmpty(t, got.GetPublicKeyCredentialCreationOptions()) + _, err = Tester.WebAuthN.CreateAttestationResponse(got.GetPublicKeyCredentialCreationOptions()) + require.NoError(t, err) + } + }) + } +} + +func TestServer_VerifyPasskeyRegistration(t *testing.T) { + userID := Tester.CreateHumanUser(CTX).GetUserId() + reg, err := Client.CreatePasskeyRegistrationLink(CTX, &user.CreatePasskeyRegistrationLinkRequest{ + UserId: userID, + Medium: &user.CreatePasskeyRegistrationLinkRequest_ReturnCode{}, + }) + require.NoError(t, err) + pkr, err := Client.RegisterPasskey(CTX, &user.RegisterPasskeyRequest{ + UserId: userID, + Code: reg.GetCode(), + }) + require.NoError(t, err) + require.NotEmpty(t, pkr.GetPasskeyId()) + require.NotEmpty(t, pkr.GetPublicKeyCredentialCreationOptions()) + + attestationResponse, err := Tester.WebAuthN.CreateAttestationResponse(pkr.GetPublicKeyCredentialCreationOptions()) + require.NoError(t, err) + + type args struct { + ctx context.Context + req *user.VerifyPasskeyRegistrationRequest + } + tests := []struct { + name string + args args + want *user.VerifyPasskeyRegistrationResponse + wantErr bool + }{ + { + name: "missing user id", + args: args{ + ctx: CTX, + req: &user.VerifyPasskeyRegistrationRequest{ + PasskeyId: pkr.GetPasskeyId(), + PublicKeyCredential: attestationResponse, + PasskeyName: "nice name", + }, + }, + wantErr: true, + }, + { + name: "success", + args: args{ + ctx: CTX, + req: &user.VerifyPasskeyRegistrationRequest{ + UserId: userID, + PasskeyId: pkr.GetPasskeyId(), + PublicKeyCredential: attestationResponse, + PasskeyName: "nice name", + }, + }, + want: &user.VerifyPasskeyRegistrationResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Organisation.ID, + }, + }, + }, + { + name: "wrong credential", + args: args{ + ctx: CTX, + req: &user.VerifyPasskeyRegistrationRequest{ + UserId: userID, + PasskeyId: pkr.GetPasskeyId(), + PublicKeyCredential: &structpb.Struct{ + Fields: map[string]*structpb.Value{"foo": {Kind: &structpb.Value_StringValue{StringValue: "bar"}}}, + }, + PasskeyName: "nice name", + }, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Client.VerifyPasskeyRegistration(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + require.NotNil(t, got) + integration.AssertDetails(t, tt.want, got) + }) + } +} + +func TestServer_CreatePasskeyRegistrationLink(t *testing.T) { + userID := Tester.CreateHumanUser(CTX).GetUserId() + + type args struct { + ctx context.Context + req *user.CreatePasskeyRegistrationLinkRequest + } + tests := []struct { + name string + args args + want *user.CreatePasskeyRegistrationLinkResponse + wantCode bool + wantErr bool + }{ + { + name: "missing user id", + args: args{ + ctx: CTX, + req: &user.CreatePasskeyRegistrationLinkRequest{}, + }, + wantErr: true, + }, + { + name: "send default mail", + args: args{ + ctx: CTX, + req: &user.CreatePasskeyRegistrationLinkRequest{ + UserId: userID, + }, + }, + want: &user.CreatePasskeyRegistrationLinkResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Organisation.ID, + }, + }, + }, + { + name: "send custom url", + args: args{ + ctx: CTX, + req: &user.CreatePasskeyRegistrationLinkRequest{ + UserId: userID, + Medium: &user.CreatePasskeyRegistrationLinkRequest_SendLink{ + SendLink: &user.SendPasskeyRegistrationLink{ + UrlTemplate: gu.Ptr("https://example.com/passkey/register?userID={{.UserID}}&orgID={{.OrgID}}&codeID={{.CodeID}}&code={{.Code}}"), + }, + }, + }, + }, + want: &user.CreatePasskeyRegistrationLinkResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Organisation.ID, + }, + }, + }, + { + name: "return code", + args: args{ + ctx: CTX, + req: &user.CreatePasskeyRegistrationLinkRequest{ + UserId: userID, + Medium: &user.CreatePasskeyRegistrationLinkRequest_ReturnCode{}, + }, + }, + want: &user.CreatePasskeyRegistrationLinkResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Organisation.ID, + }, + }, + wantCode: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Client.CreatePasskeyRegistrationLink(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + require.NotNil(t, got) + integration.AssertDetails(t, tt.want, got) + if tt.wantCode { + assert.NotEmpty(t, got.GetCode().GetId()) + assert.NotEmpty(t, got.GetCode().GetId()) + } + }) + } +} diff --git a/internal/api/grpc/user/v2beta/passkey_test.go b/internal/api/grpc/user/v2beta/passkey_test.go new file mode 100644 index 0000000000..7d45c41756 --- /dev/null +++ b/internal/api/grpc/user/v2beta/passkey_test.go @@ -0,0 +1,235 @@ +package user + +import ( + "io" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/structpb" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/api/grpc" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/zerrors" + object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta" + user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta" +) + +func Test_passkeyAuthenticatorToDomain(t *testing.T) { + tests := []struct { + pa user.PasskeyAuthenticator + want domain.AuthenticatorAttachment + }{ + { + pa: user.PasskeyAuthenticator_PASSKEY_AUTHENTICATOR_UNSPECIFIED, + want: domain.AuthenticatorAttachmentUnspecified, + }, + { + pa: user.PasskeyAuthenticator_PASSKEY_AUTHENTICATOR_PLATFORM, + want: domain.AuthenticatorAttachmentPlattform, + }, + { + pa: user.PasskeyAuthenticator_PASSKEY_AUTHENTICATOR_CROSS_PLATFORM, + want: domain.AuthenticatorAttachmentCrossPlattform, + }, + { + pa: 999, + want: domain.AuthenticatorAttachmentUnspecified, + }, + } + for _, tt := range tests { + t.Run(tt.pa.String(), func(t *testing.T) { + got := passkeyAuthenticatorToDomain(tt.pa) + assert.Equal(t, tt.want, got) + }) + } +} + +func Test_passkeyRegistrationDetailsToPb(t *testing.T) { + type args struct { + details *domain.WebAuthNRegistrationDetails + err error + } + tests := []struct { + name string + args args + want *user.RegisterPasskeyResponse + wantErr error + }{ + { + name: "an error", + args: args{ + details: nil, + err: io.ErrClosedPipe, + }, + wantErr: io.ErrClosedPipe, + }, + { + name: "unmarshall error", + args: args{ + details: &domain.WebAuthNRegistrationDetails{ + ObjectDetails: &domain.ObjectDetails{ + Sequence: 22, + EventDate: time.Unix(3000, 22), + ResourceOwner: "me", + }, + ID: "123", + PublicKeyCredentialCreationOptions: []byte(`\\`), + }, + err: nil, + }, + wantErr: zerrors.ThrowInternal(nil, "USERv2-Dohr6", "Errors.Internal"), + }, + { + name: "ok", + args: args{ + details: &domain.WebAuthNRegistrationDetails{ + ObjectDetails: &domain.ObjectDetails{ + Sequence: 22, + EventDate: time.Unix(3000, 22), + ResourceOwner: "me", + }, + ID: "123", + PublicKeyCredentialCreationOptions: []byte(`{"foo": "bar"}`), + }, + err: nil, + }, + want: &user.RegisterPasskeyResponse{ + Details: &object.Details{ + Sequence: 22, + ChangeDate: ×tamppb.Timestamp{ + Seconds: 3000, + Nanos: 22, + }, + ResourceOwner: "me", + }, + PasskeyId: "123", + PublicKeyCredentialCreationOptions: &structpb.Struct{ + Fields: map[string]*structpb.Value{"foo": {Kind: &structpb.Value_StringValue{StringValue: "bar"}}}, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := passkeyRegistrationDetailsToPb(tt.args.details, tt.args.err) + require.ErrorIs(t, err, tt.wantErr) + if !proto.Equal(tt.want, got) { + t.Errorf("Not equal:\nExpected\n%s\nActual:%s", tt.want, got) + } + if tt.want != nil { + grpc.AllFieldsSet(t, got.ProtoReflect()) + } + }) + } +} + +func Test_passkeyDetailsToPb(t *testing.T) { + type args struct { + details *domain.ObjectDetails + err error + } + tests := []struct { + name string + args args + want *user.CreatePasskeyRegistrationLinkResponse + }{ + { + name: "an error", + args: args{ + details: nil, + err: io.ErrClosedPipe, + }, + }, + { + name: "ok", + args: args{ + details: &domain.ObjectDetails{ + Sequence: 22, + EventDate: time.Unix(3000, 22), + ResourceOwner: "me", + }, + err: nil, + }, + want: &user.CreatePasskeyRegistrationLinkResponse{ + Details: &object.Details{ + Sequence: 22, + ChangeDate: ×tamppb.Timestamp{ + Seconds: 3000, + Nanos: 22, + }, + ResourceOwner: "me", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := passkeyDetailsToPb(tt.args.details, tt.args.err) + require.ErrorIs(t, err, tt.args.err) + assert.Equal(t, tt.want, got) + }) + } +} + +func Test_passkeyCodeDetailsToPb(t *testing.T) { + type args struct { + details *domain.PasskeyCodeDetails + err error + } + tests := []struct { + name string + args args + want *user.CreatePasskeyRegistrationLinkResponse + }{ + { + name: "an error", + args: args{ + details: nil, + err: io.ErrClosedPipe, + }, + }, + { + name: "ok", + args: args{ + details: &domain.PasskeyCodeDetails{ + ObjectDetails: &domain.ObjectDetails{ + Sequence: 22, + EventDate: time.Unix(3000, 22), + ResourceOwner: "me", + }, + CodeID: "123", + Code: "456", + }, + err: nil, + }, + want: &user.CreatePasskeyRegistrationLinkResponse{ + Details: &object.Details{ + Sequence: 22, + ChangeDate: ×tamppb.Timestamp{ + Seconds: 3000, + Nanos: 22, + }, + ResourceOwner: "me", + }, + Code: &user.PasskeyRegistrationCode{ + Id: "123", + Code: "456", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := passkeyCodeDetailsToPb(tt.args.details, tt.args.err) + require.ErrorIs(t, err, tt.args.err) + assert.Equal(t, tt.want, got) + if tt.want != nil { + grpc.AllFieldsSet(t, got.ProtoReflect()) + } + }) + } +} diff --git a/internal/api/grpc/user/v2beta/password.go b/internal/api/grpc/user/v2beta/password.go new file mode 100644 index 0000000000..0de1262215 --- /dev/null +++ b/internal/api/grpc/user/v2beta/password.go @@ -0,0 +1,69 @@ +package user + +import ( + "context" + + object "github.com/zitadel/zitadel/internal/api/grpc/object/v2beta" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/zerrors" + user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta" +) + +func (s *Server) PasswordReset(ctx context.Context, req *user.PasswordResetRequest) (_ *user.PasswordResetResponse, err error) { + var details *domain.ObjectDetails + var code *string + + switch m := req.GetMedium().(type) { + case *user.PasswordResetRequest_SendLink: + details, code, err = s.command.RequestPasswordResetURLTemplate(ctx, req.GetUserId(), m.SendLink.GetUrlTemplate(), notificationTypeToDomain(m.SendLink.GetNotificationType())) + case *user.PasswordResetRequest_ReturnCode: + details, code, err = s.command.RequestPasswordResetReturnCode(ctx, req.GetUserId()) + case nil: + details, code, err = s.command.RequestPasswordReset(ctx, req.GetUserId()) + default: + err = zerrors.ThrowUnimplementedf(nil, "USERv2-SDeeg", "verification oneOf %T in method RequestPasswordReset not implemented", m) + } + if err != nil { + return nil, err + } + + return &user.PasswordResetResponse{ + Details: object.DomainToDetailsPb(details), + VerificationCode: code, + }, nil +} + +func notificationTypeToDomain(notificationType user.NotificationType) domain.NotificationType { + switch notificationType { + case user.NotificationType_NOTIFICATION_TYPE_Email: + return domain.NotificationTypeEmail + case user.NotificationType_NOTIFICATION_TYPE_SMS: + return domain.NotificationTypeSms + case user.NotificationType_NOTIFICATION_TYPE_Unspecified: + return domain.NotificationTypeEmail + default: + return domain.NotificationTypeEmail + } +} + +func (s *Server) SetPassword(ctx context.Context, req *user.SetPasswordRequest) (_ *user.SetPasswordResponse, err error) { + var details *domain.ObjectDetails + + switch v := req.GetVerification().(type) { + case *user.SetPasswordRequest_CurrentPassword: + details, err = s.command.ChangePassword(ctx, "", req.GetUserId(), v.CurrentPassword, req.GetNewPassword().GetPassword(), "", req.GetNewPassword().GetChangeRequired()) + case *user.SetPasswordRequest_VerificationCode: + details, err = s.command.SetPasswordWithVerifyCode(ctx, "", req.GetUserId(), v.VerificationCode, req.GetNewPassword().GetPassword(), "", req.GetNewPassword().GetChangeRequired()) + case nil: + details, err = s.command.SetPassword(ctx, "", req.GetUserId(), req.GetNewPassword().GetPassword(), req.GetNewPassword().GetChangeRequired()) + default: + err = zerrors.ThrowUnimplementedf(nil, "USERv2-SFdf2", "verification oneOf %T in method SetPasswordRequest not implemented", v) + } + if err != nil { + return nil, err + } + + return &user.SetPasswordResponse{ + Details: object.DomainToDetailsPb(details), + }, nil +} diff --git a/internal/api/grpc/user/v2beta/password_integration_test.go b/internal/api/grpc/user/v2beta/password_integration_test.go new file mode 100644 index 0000000000..03b18a5fa7 --- /dev/null +++ b/internal/api/grpc/user/v2beta/password_integration_test.go @@ -0,0 +1,232 @@ +//go:build integration + +package user_test + +import ( + "context" + "testing" + + "github.com/muhlemmer/gu" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/integration" + object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta" + user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta" +) + +func TestServer_RequestPasswordReset(t *testing.T) { + userID := Tester.CreateHumanUser(CTX).GetUserId() + + tests := []struct { + name string + req *user.PasswordResetRequest + want *user.PasswordResetResponse + wantErr bool + }{ + { + name: "default medium", + req: &user.PasswordResetRequest{ + UserId: userID, + }, + want: &user.PasswordResetResponse{ + Details: &object.Details{ + Sequence: 1, + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Organisation.ID, + }, + }, + }, + { + name: "custom url template", + req: &user.PasswordResetRequest{ + UserId: userID, + Medium: &user.PasswordResetRequest_SendLink{ + SendLink: &user.SendPasswordResetLink{ + NotificationType: user.NotificationType_NOTIFICATION_TYPE_Email, + UrlTemplate: gu.Ptr("https://example.com/password/change?userID={{.UserID}}&code={{.Code}}&orgID={{.OrgID}}"), + }, + }, + }, + want: &user.PasswordResetResponse{ + Details: &object.Details{ + Sequence: 1, + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Organisation.ID, + }, + }, + }, + { + name: "template error", + req: &user.PasswordResetRequest{ + UserId: userID, + Medium: &user.PasswordResetRequest_SendLink{ + SendLink: &user.SendPasswordResetLink{ + UrlTemplate: gu.Ptr("{{"), + }, + }, + }, + wantErr: true, + }, + { + name: "return code", + req: &user.PasswordResetRequest{ + UserId: userID, + Medium: &user.PasswordResetRequest_ReturnCode{ + ReturnCode: &user.ReturnPasswordResetCode{}, + }, + }, + want: &user.PasswordResetResponse{ + Details: &object.Details{ + Sequence: 1, + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Organisation.ID, + }, + VerificationCode: gu.Ptr("xxx"), + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Client.PasswordReset(CTX, tt.req) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + integration.AssertDetails(t, tt.want, got) + if tt.want.GetVerificationCode() != "" { + assert.NotEmpty(t, got.GetVerificationCode()) + } + }) + } +} + +func TestServer_SetPassword(t *testing.T) { + type args struct { + ctx context.Context + req *user.SetPasswordRequest + } + tests := []struct { + name string + prepare func(request *user.SetPasswordRequest) error + args args + want *user.SetPasswordResponse + wantErr bool + }{ + { + name: "missing user id", + prepare: func(request *user.SetPasswordRequest) error { + return nil + }, + args: args{ + ctx: CTX, + req: &user.SetPasswordRequest{}, + }, + wantErr: true, + }, + { + name: "set successful", + prepare: func(request *user.SetPasswordRequest) error { + userID := Tester.CreateHumanUser(CTX).GetUserId() + request.UserId = userID + return nil + }, + args: args{ + ctx: CTX, + req: &user.SetPasswordRequest{ + NewPassword: &user.Password{ + Password: "Secr3tP4ssw0rd!", + }, + }, + }, + want: &user.SetPasswordResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Organisation.ID, + }, + }, + }, + { + name: "change successful", + prepare: func(request *user.SetPasswordRequest) error { + userID := Tester.CreateHumanUser(CTX).GetUserId() + request.UserId = userID + _, err := Client.SetPassword(CTX, &user.SetPasswordRequest{ + UserId: userID, + NewPassword: &user.Password{ + Password: "InitialPassw0rd!", + }, + }) + return err + }, + args: args{ + ctx: CTX, + req: &user.SetPasswordRequest{ + NewPassword: &user.Password{ + Password: "Secr3tP4ssw0rd!", + }, + Verification: &user.SetPasswordRequest_CurrentPassword{ + CurrentPassword: "InitialPassw0rd!", + }, + }, + }, + want: &user.SetPasswordResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Organisation.ID, + }, + }, + }, + { + name: "set with code successful", + prepare: func(request *user.SetPasswordRequest) error { + userID := Tester.CreateHumanUser(CTX).GetUserId() + request.UserId = userID + resp, err := Client.PasswordReset(CTX, &user.PasswordResetRequest{ + UserId: userID, + Medium: &user.PasswordResetRequest_ReturnCode{ + ReturnCode: &user.ReturnPasswordResetCode{}, + }, + }) + if err != nil { + return err + } + request.Verification = &user.SetPasswordRequest_VerificationCode{ + VerificationCode: resp.GetVerificationCode(), + } + return nil + }, + args: args{ + ctx: CTX, + req: &user.SetPasswordRequest{ + NewPassword: &user.Password{ + Password: "Secr3tP4ssw0rd!", + }, + }, + }, + want: &user.SetPasswordResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Organisation.ID, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.prepare(tt.args.req) + require.NoError(t, err) + + got, err := Client.SetPassword(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + require.NotNil(t, got) + integration.AssertDetails(t, tt.want, got) + }) + } +} diff --git a/internal/api/grpc/user/v2beta/password_test.go b/internal/api/grpc/user/v2beta/password_test.go new file mode 100644 index 0000000000..5ce9930b39 --- /dev/null +++ b/internal/api/grpc/user/v2beta/password_test.go @@ -0,0 +1,39 @@ +package user + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/zitadel/zitadel/internal/domain" + user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta" +) + +func Test_notificationTypeToDomain(t *testing.T) { + tests := []struct { + name string + notificationType user.NotificationType + want domain.NotificationType + }{ + { + "unspecified", + user.NotificationType_NOTIFICATION_TYPE_Unspecified, + domain.NotificationTypeEmail, + }, + { + "email", + user.NotificationType_NOTIFICATION_TYPE_Email, + domain.NotificationTypeEmail, + }, + { + "sms", + user.NotificationType_NOTIFICATION_TYPE_SMS, + domain.NotificationTypeSms, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equalf(t, tt.want, notificationTypeToDomain(tt.notificationType), "notificationTypeToDomain(%v)", tt.notificationType) + }) + } +} diff --git a/internal/api/grpc/user/v2beta/phone.go b/internal/api/grpc/user/v2beta/phone.go new file mode 100644 index 0000000000..eac7eb4e31 --- /dev/null +++ b/internal/api/grpc/user/v2beta/phone.go @@ -0,0 +1,102 @@ +package user + +import ( + "context" + + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/zerrors" + object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta" + user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta" +) + +func (s *Server) SetPhone(ctx context.Context, req *user.SetPhoneRequest) (resp *user.SetPhoneResponse, err error) { + var phone *domain.Phone + + switch v := req.GetVerification().(type) { + case *user.SetPhoneRequest_SendCode: + phone, err = s.command.ChangeUserPhone(ctx, req.GetUserId(), req.GetPhone(), s.userCodeAlg) + case *user.SetPhoneRequest_ReturnCode: + phone, err = s.command.ChangeUserPhoneReturnCode(ctx, req.GetUserId(), req.GetPhone(), s.userCodeAlg) + case *user.SetPhoneRequest_IsVerified: + phone, err = s.command.ChangeUserPhoneVerified(ctx, req.GetUserId(), req.GetPhone()) + case nil: + phone, err = s.command.ChangeUserPhone(ctx, req.GetUserId(), req.GetPhone(), s.userCodeAlg) + default: + err = zerrors.ThrowUnimplementedf(nil, "USERv2-Ahng0", "verification oneOf %T in method SetPhone not implemented", v) + } + if err != nil { + return nil, err + } + + return &user.SetPhoneResponse{ + Details: &object.Details{ + Sequence: phone.Sequence, + ChangeDate: timestamppb.New(phone.ChangeDate), + ResourceOwner: phone.ResourceOwner, + }, + VerificationCode: phone.PlainCode, + }, nil +} + +func (s *Server) RemovePhone(ctx context.Context, req *user.RemovePhoneRequest) (resp *user.RemovePhoneResponse, err error) { + details, err := s.command.RemoveUserPhone(ctx, + req.GetUserId(), + ) + if err != nil { + return nil, err + } + + return &user.RemovePhoneResponse{ + Details: &object.Details{ + Sequence: details.Sequence, + ChangeDate: timestamppb.New(details.EventDate), + ResourceOwner: details.ResourceOwner, + }, + }, nil +} + +func (s *Server) ResendPhoneCode(ctx context.Context, req *user.ResendPhoneCodeRequest) (resp *user.ResendPhoneCodeResponse, err error) { + var phone *domain.Phone + switch v := req.GetVerification().(type) { + case *user.ResendPhoneCodeRequest_SendCode: + phone, err = s.command.ResendUserPhoneCode(ctx, req.GetUserId(), s.userCodeAlg) + case *user.ResendPhoneCodeRequest_ReturnCode: + phone, err = s.command.ResendUserPhoneCodeReturnCode(ctx, req.GetUserId(), s.userCodeAlg) + case nil: + phone, err = s.command.ResendUserPhoneCode(ctx, req.GetUserId(), s.userCodeAlg) + default: + err = zerrors.ThrowUnimplementedf(nil, "USERv2-ResendUserPhoneCode", "verification oneOf %T in method SetPhone not implemented", v) + } + if err != nil { + return nil, err + } + + return &user.ResendPhoneCodeResponse{ + Details: &object.Details{ + Sequence: phone.Sequence, + ChangeDate: timestamppb.New(phone.ChangeDate), + ResourceOwner: phone.ResourceOwner, + }, + VerificationCode: phone.PlainCode, + }, nil +} + +func (s *Server) VerifyPhone(ctx context.Context, req *user.VerifyPhoneRequest) (*user.VerifyPhoneResponse, error) { + details, err := s.command.VerifyUserPhone(ctx, + req.GetUserId(), + req.GetVerificationCode(), + s.userCodeAlg, + ) + if err != nil { + return nil, err + } + return &user.VerifyPhoneResponse{ + Details: &object.Details{ + Sequence: details.Sequence, + ChangeDate: timestamppb.New(details.EventDate), + ResourceOwner: details.ResourceOwner, + }, + }, nil +} diff --git a/internal/api/grpc/user/v2beta/phone_integration_test.go b/internal/api/grpc/user/v2beta/phone_integration_test.go new file mode 100644 index 0000000000..692f7af5f7 --- /dev/null +++ b/internal/api/grpc/user/v2beta/phone_integration_test.go @@ -0,0 +1,344 @@ +//go:build integration + +package user_test + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/muhlemmer/gu" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/integration" + object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta" + user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta" +) + +func TestServer_SetPhone(t *testing.T) { + userID := Tester.CreateHumanUser(CTX).GetUserId() + + tests := []struct { + name string + req *user.SetPhoneRequest + want *user.SetPhoneResponse + wantErr bool + }{ + { + name: "default verification", + req: &user.SetPhoneRequest{ + UserId: userID, + Phone: "+41791234568", + }, + want: &user.SetPhoneResponse{ + Details: &object.Details{ + Sequence: 1, + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Organisation.ID, + }, + }, + }, + { + name: "send verification", + req: &user.SetPhoneRequest{ + UserId: userID, + Phone: "+41791234569", + Verification: &user.SetPhoneRequest_SendCode{ + SendCode: &user.SendPhoneVerificationCode{}, + }, + }, + want: &user.SetPhoneResponse{ + Details: &object.Details{ + Sequence: 1, + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Organisation.ID, + }, + }, + }, + { + name: "return code", + req: &user.SetPhoneRequest{ + UserId: userID, + Phone: "+41791234566", + Verification: &user.SetPhoneRequest_ReturnCode{ + ReturnCode: &user.ReturnPhoneVerificationCode{}, + }, + }, + want: &user.SetPhoneResponse{ + Details: &object.Details{ + Sequence: 1, + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Organisation.ID, + }, + VerificationCode: gu.Ptr("xxx"), + }, + }, + { + name: "is verified true", + req: &user.SetPhoneRequest{ + UserId: userID, + Phone: "+41791234565", + Verification: &user.SetPhoneRequest_IsVerified{ + IsVerified: true, + }, + }, + want: &user.SetPhoneResponse{ + Details: &object.Details{ + Sequence: 1, + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Organisation.ID, + }, + }, + }, + { + name: "is verified false", + req: &user.SetPhoneRequest{ + UserId: userID, + Phone: "+41791234564", + Verification: &user.SetPhoneRequest_IsVerified{ + IsVerified: false, + }, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Client.SetPhone(CTX, tt.req) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + integration.AssertDetails(t, tt.want, got) + if tt.want.GetVerificationCode() != "" { + assert.NotEmpty(t, got.GetVerificationCode()) + } + }) + } +} + +func TestServer_ResendPhoneCode(t *testing.T) { + userID := Tester.CreateHumanUser(CTX).GetUserId() + verifiedUserID := Tester.CreateHumanUserVerified(CTX, Tester.Organisation.ID, fmt.Sprintf("%d@mouse.com", time.Now().UnixNano())).GetUserId() + + tests := []struct { + name string + req *user.ResendPhoneCodeRequest + want *user.ResendPhoneCodeResponse + wantErr bool + }{ + { + name: "user not existing", + req: &user.ResendPhoneCodeRequest{ + UserId: "xxx", + }, + wantErr: true, + }, + { + name: "user not existing", + req: &user.ResendPhoneCodeRequest{ + UserId: verifiedUserID, + }, + wantErr: true, + }, + { + name: "resend code", + req: &user.ResendPhoneCodeRequest{ + UserId: userID, + Verification: &user.ResendPhoneCodeRequest_SendCode{ + SendCode: &user.SendPhoneVerificationCode{}, + }, + }, + want: &user.ResendPhoneCodeResponse{ + Details: &object.Details{ + Sequence: 1, + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Organisation.ID, + }, + }, + }, + { + name: "return code", + req: &user.ResendPhoneCodeRequest{ + UserId: userID, + Verification: &user.ResendPhoneCodeRequest_ReturnCode{ + ReturnCode: &user.ReturnPhoneVerificationCode{}, + }, + }, + want: &user.ResendPhoneCodeResponse{ + Details: &object.Details{ + Sequence: 1, + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Organisation.ID, + }, + VerificationCode: gu.Ptr("xxx"), + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Client.ResendPhoneCode(CTX, tt.req) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + integration.AssertDetails(t, tt.want, got) + if tt.want.GetVerificationCode() != "" { + assert.NotEmpty(t, got.GetVerificationCode()) + } + }) + } +} + +func TestServer_VerifyPhone(t *testing.T) { + userResp := Tester.CreateHumanUser(CTX) + tests := []struct { + name string + req *user.VerifyPhoneRequest + want *user.VerifyPhoneResponse + wantErr bool + }{ + { + name: "wrong code", + req: &user.VerifyPhoneRequest{ + UserId: userResp.GetUserId(), + VerificationCode: "xxx", + }, + wantErr: true, + }, + { + name: "wrong user", + req: &user.VerifyPhoneRequest{ + UserId: "xxx", + VerificationCode: userResp.GetPhoneCode(), + }, + wantErr: true, + }, + { + name: "verify user", + req: &user.VerifyPhoneRequest{ + UserId: userResp.GetUserId(), + VerificationCode: userResp.GetPhoneCode(), + }, + want: &user.VerifyPhoneResponse{ + Details: &object.Details{ + Sequence: 1, + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Organisation.ID, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Client.VerifyPhone(CTX, tt.req) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + integration.AssertDetails(t, tt.want, got) + }) + } +} + +func TestServer_RemovePhone(t *testing.T) { + userResp := Tester.CreateHumanUser(CTX) + failResp := Tester.CreateHumanUserNoPhone(CTX) + otherUser := Tester.CreateHumanUser(CTX).GetUserId() + doubleRemoveUser := Tester.CreateHumanUser(CTX) + + Tester.RegisterUserPasskey(CTX, otherUser) + _, sessionTokenOtherUser, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, otherUser) + + tests := []struct { + name string + ctx context.Context + req *user.RemovePhoneRequest + want *user.RemovePhoneResponse + wantErr bool + dep func(ctx context.Context, userID string) (*user.RemovePhoneResponse, error) + }{ + { + name: "remove phone", + ctx: CTX, + req: &user.RemovePhoneRequest{ + UserId: userResp.GetUserId(), + }, + want: &user.RemovePhoneResponse{ + Details: &object.Details{ + Sequence: 1, + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Organisation.ID, + }, + }, + dep: func(ctx context.Context, userID string) (*user.RemovePhoneResponse, error) { + return nil, nil + }, + }, + { + name: "user without phone", + ctx: CTX, + req: &user.RemovePhoneRequest{ + UserId: failResp.GetUserId(), + }, + wantErr: true, + dep: func(ctx context.Context, userID string) (*user.RemovePhoneResponse, error) { + return nil, nil + }, + }, + { + name: "remove previously deleted phone", + ctx: CTX, + req: &user.RemovePhoneRequest{ + UserId: doubleRemoveUser.GetUserId(), + }, + wantErr: true, + dep: func(ctx context.Context, userID string) (*user.RemovePhoneResponse, error) { + return Client.RemovePhone(ctx, &user.RemovePhoneRequest{ + UserId: doubleRemoveUser.GetUserId(), + }) + }, + }, + { + name: "no user id", + ctx: CTX, + req: &user.RemovePhoneRequest{}, + wantErr: true, + dep: func(ctx context.Context, userID string) (*user.RemovePhoneResponse, error) { + return nil, nil + }, + }, + { + name: "other user, no permission", + ctx: Tester.WithAuthorizationToken(CTX, sessionTokenOtherUser), + req: &user.RemovePhoneRequest{ + UserId: userResp.GetUserId(), + }, + wantErr: true, + dep: func(ctx context.Context, userID string) (*user.RemovePhoneResponse, error) { + return nil, nil + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, depErr := tt.dep(tt.ctx, tt.req.UserId) + require.NoError(t, depErr) + + got, err := Client.RemovePhone(tt.ctx, tt.req) + + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + integration.AssertDetails(t, tt.want, got) + }) + } +} diff --git a/internal/api/grpc/user/v2beta/query.go b/internal/api/grpc/user/v2beta/query.go new file mode 100644 index 0000000000..0eaeba5ca1 --- /dev/null +++ b/internal/api/grpc/user/v2beta/query.go @@ -0,0 +1,338 @@ +package user + +import ( + "context" + + "github.com/muhlemmer/gu" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/api/authz" + object "github.com/zitadel/zitadel/internal/api/grpc/object/v2beta" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/query" + "github.com/zitadel/zitadel/internal/zerrors" + user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta" +) + +func (s *Server) GetUserByID(ctx context.Context, req *user.GetUserByIDRequest) (_ *user.GetUserByIDResponse, err error) { + resp, err := s.query.GetUserByID(ctx, true, req.GetUserId()) + if err != nil { + return nil, err + } + if authz.GetCtxData(ctx).UserID != req.GetUserId() { + if err := s.checkPermission(ctx, domain.PermissionUserRead, resp.ResourceOwner, req.GetUserId()); err != nil { + return nil, err + } + } + return &user.GetUserByIDResponse{ + Details: object.DomainToDetailsPb(&domain.ObjectDetails{ + Sequence: resp.Sequence, + EventDate: resp.ChangeDate, + ResourceOwner: resp.ResourceOwner, + }), + User: userToPb(resp, s.assetAPIPrefix(ctx)), + }, nil +} + +func (s *Server) ListUsers(ctx context.Context, req *user.ListUsersRequest) (*user.ListUsersResponse, error) { + queries, err := listUsersRequestToModel(req) + if err != nil { + return nil, err + } + res, err := s.query.SearchUsers(ctx, queries) + if err != nil { + return nil, err + } + res.RemoveNoPermission(ctx, s.checkPermission) + return &user.ListUsersResponse{ + Result: UsersToPb(res.Users, s.assetAPIPrefix(ctx)), + Details: object.ToListDetails(res.SearchResponse), + }, nil +} + +func UsersToPb(users []*query.User, assetPrefix string) []*user.User { + u := make([]*user.User, len(users)) + for i, user := range users { + u[i] = userToPb(user, assetPrefix) + } + return u +} + +func userToPb(userQ *query.User, assetPrefix string) *user.User { + return &user.User{ + UserId: userQ.ID, + Details: object.DomainToDetailsPb(&domain.ObjectDetails{ + Sequence: userQ.Sequence, + EventDate: userQ.ChangeDate, + ResourceOwner: userQ.ResourceOwner, + }), + State: userStateToPb(userQ.State), + Username: userQ.Username, + LoginNames: userQ.LoginNames, + PreferredLoginName: userQ.PreferredLoginName, + Type: userTypeToPb(userQ, assetPrefix), + } +} + +func userTypeToPb(userQ *query.User, assetPrefix string) user.UserType { + if userQ.Human != nil { + return &user.User_Human{ + Human: humanToPb(userQ.Human, assetPrefix, userQ.ResourceOwner), + } + } + if userQ.Machine != nil { + return &user.User_Machine{ + Machine: machineToPb(userQ.Machine), + } + } + return nil +} + +func humanToPb(userQ *query.Human, assetPrefix, owner string) *user.HumanUser { + var passwordChanged *timestamppb.Timestamp + if !userQ.PasswordChanged.IsZero() { + passwordChanged = timestamppb.New(userQ.PasswordChanged) + } + return &user.HumanUser{ + Profile: &user.HumanProfile{ + GivenName: userQ.FirstName, + FamilyName: userQ.LastName, + NickName: gu.Ptr(userQ.NickName), + DisplayName: gu.Ptr(userQ.DisplayName), + PreferredLanguage: gu.Ptr(userQ.PreferredLanguage.String()), + Gender: gu.Ptr(genderToPb(userQ.Gender)), + AvatarUrl: domain.AvatarURL(assetPrefix, owner, userQ.AvatarKey), + }, + Email: &user.HumanEmail{ + Email: string(userQ.Email), + IsVerified: userQ.IsEmailVerified, + }, + Phone: &user.HumanPhone{ + Phone: string(userQ.Phone), + IsVerified: userQ.IsPhoneVerified, + }, + PasswordChangeRequired: userQ.PasswordChangeRequired, + PasswordChanged: passwordChanged, + } +} + +func machineToPb(userQ *query.Machine) *user.MachineUser { + return &user.MachineUser{ + Name: userQ.Name, + Description: userQ.Description, + HasSecret: userQ.EncodedSecret != "", + AccessTokenType: accessTokenTypeToPb(userQ.AccessTokenType), + } +} + +func userStateToPb(state domain.UserState) user.UserState { + switch state { + case domain.UserStateActive: + return user.UserState_USER_STATE_ACTIVE + case domain.UserStateInactive: + return user.UserState_USER_STATE_INACTIVE + case domain.UserStateDeleted: + return user.UserState_USER_STATE_DELETED + case domain.UserStateInitial: + return user.UserState_USER_STATE_INITIAL + case domain.UserStateLocked: + return user.UserState_USER_STATE_LOCKED + case domain.UserStateUnspecified: + return user.UserState_USER_STATE_UNSPECIFIED + case domain.UserStateSuspend: + return user.UserState_USER_STATE_UNSPECIFIED + default: + return user.UserState_USER_STATE_UNSPECIFIED + } +} + +func genderToPb(gender domain.Gender) user.Gender { + switch gender { + case domain.GenderDiverse: + return user.Gender_GENDER_DIVERSE + case domain.GenderFemale: + return user.Gender_GENDER_FEMALE + case domain.GenderMale: + return user.Gender_GENDER_MALE + case domain.GenderUnspecified: + return user.Gender_GENDER_UNSPECIFIED + default: + return user.Gender_GENDER_UNSPECIFIED + } +} + +func accessTokenTypeToPb(accessTokenType domain.OIDCTokenType) user.AccessTokenType { + switch accessTokenType { + case domain.OIDCTokenTypeBearer: + return user.AccessTokenType_ACCESS_TOKEN_TYPE_BEARER + case domain.OIDCTokenTypeJWT: + return user.AccessTokenType_ACCESS_TOKEN_TYPE_JWT + default: + return user.AccessTokenType_ACCESS_TOKEN_TYPE_BEARER + } +} + +func listUsersRequestToModel(req *user.ListUsersRequest) (*query.UserSearchQueries, error) { + offset, limit, asc := object.ListQueryToQuery(req.Query) + queries, err := userQueriesToQuery(req.Queries, 0 /*start from level 0*/) + if err != nil { + return nil, err + } + return &query.UserSearchQueries{ + SearchRequest: query.SearchRequest{ + Offset: offset, + Limit: limit, + Asc: asc, + SortingColumn: userFieldNameToSortingColumn(req.SortingColumn), + }, + Queries: queries, + }, nil +} + +func userFieldNameToSortingColumn(field user.UserFieldName) query.Column { + switch field { + case user.UserFieldName_USER_FIELD_NAME_EMAIL: + return query.HumanEmailCol + case user.UserFieldName_USER_FIELD_NAME_FIRST_NAME: + return query.HumanFirstNameCol + case user.UserFieldName_USER_FIELD_NAME_LAST_NAME: + return query.HumanLastNameCol + case user.UserFieldName_USER_FIELD_NAME_DISPLAY_NAME: + return query.HumanDisplayNameCol + case user.UserFieldName_USER_FIELD_NAME_USER_NAME: + return query.UserUsernameCol + case user.UserFieldName_USER_FIELD_NAME_STATE: + return query.UserStateCol + case user.UserFieldName_USER_FIELD_NAME_TYPE: + return query.UserTypeCol + case user.UserFieldName_USER_FIELD_NAME_NICK_NAME: + return query.HumanNickNameCol + case user.UserFieldName_USER_FIELD_NAME_CREATION_DATE: + return query.UserCreationDateCol + case user.UserFieldName_USER_FIELD_NAME_UNSPECIFIED: + return query.UserIDCol + default: + return query.UserIDCol + } +} + +func userQueriesToQuery(queries []*user.SearchQuery, level uint8) (_ []query.SearchQuery, err error) { + q := make([]query.SearchQuery, len(queries)) + for i, query := range queries { + q[i], err = userQueryToQuery(query, level) + if err != nil { + return nil, err + } + } + return q, nil +} + +func userQueryToQuery(query *user.SearchQuery, level uint8) (query.SearchQuery, error) { + if level > 20 { + // can't go deeper than 20 levels of nesting. + return nil, zerrors.ThrowInvalidArgument(nil, "USER-zsQ97", "Errors.Query.TooManyNestingLevels") + } + switch q := query.Query.(type) { + case *user.SearchQuery_UserNameQuery: + return userNameQueryToQuery(q.UserNameQuery) + case *user.SearchQuery_FirstNameQuery: + return firstNameQueryToQuery(q.FirstNameQuery) + case *user.SearchQuery_LastNameQuery: + return lastNameQueryToQuery(q.LastNameQuery) + case *user.SearchQuery_NickNameQuery: + return nickNameQueryToQuery(q.NickNameQuery) + case *user.SearchQuery_DisplayNameQuery: + return displayNameQueryToQuery(q.DisplayNameQuery) + case *user.SearchQuery_EmailQuery: + return emailQueryToQuery(q.EmailQuery) + case *user.SearchQuery_StateQuery: + return stateQueryToQuery(q.StateQuery) + case *user.SearchQuery_TypeQuery: + return typeQueryToQuery(q.TypeQuery) + case *user.SearchQuery_LoginNameQuery: + return loginNameQueryToQuery(q.LoginNameQuery) + case *user.SearchQuery_OrganizationIdQuery: + return resourceOwnerQueryToQuery(q.OrganizationIdQuery) + case *user.SearchQuery_InUserIdsQuery: + return inUserIdsQueryToQuery(q.InUserIdsQuery) + case *user.SearchQuery_OrQuery: + return orQueryToQuery(q.OrQuery, level) + case *user.SearchQuery_AndQuery: + return andQueryToQuery(q.AndQuery, level) + case *user.SearchQuery_NotQuery: + return notQueryToQuery(q.NotQuery, level) + case *user.SearchQuery_InUserEmailsQuery: + return inUserEmailsQueryToQuery(q.InUserEmailsQuery) + default: + return nil, zerrors.ThrowInvalidArgument(nil, "GRPC-vR9nC", "List.Query.Invalid") + } +} + +func userNameQueryToQuery(q *user.UserNameQuery) (query.SearchQuery, error) { + return query.NewUserUsernameSearchQuery(q.UserName, object.TextMethodToQuery(q.Method)) +} + +func firstNameQueryToQuery(q *user.FirstNameQuery) (query.SearchQuery, error) { + return query.NewUserFirstNameSearchQuery(q.FirstName, object.TextMethodToQuery(q.Method)) +} + +func lastNameQueryToQuery(q *user.LastNameQuery) (query.SearchQuery, error) { + return query.NewUserLastNameSearchQuery(q.LastName, object.TextMethodToQuery(q.Method)) +} + +func nickNameQueryToQuery(q *user.NickNameQuery) (query.SearchQuery, error) { + return query.NewUserNickNameSearchQuery(q.NickName, object.TextMethodToQuery(q.Method)) +} + +func displayNameQueryToQuery(q *user.DisplayNameQuery) (query.SearchQuery, error) { + return query.NewUserDisplayNameSearchQuery(q.DisplayName, object.TextMethodToQuery(q.Method)) +} + +func emailQueryToQuery(q *user.EmailQuery) (query.SearchQuery, error) { + return query.NewUserEmailSearchQuery(q.EmailAddress, object.TextMethodToQuery(q.Method)) +} + +func stateQueryToQuery(q *user.StateQuery) (query.SearchQuery, error) { + return query.NewUserStateSearchQuery(int32(q.State)) +} + +func typeQueryToQuery(q *user.TypeQuery) (query.SearchQuery, error) { + return query.NewUserTypeSearchQuery(int32(q.Type)) +} + +func loginNameQueryToQuery(q *user.LoginNameQuery) (query.SearchQuery, error) { + return query.NewUserLoginNameExistsQuery(q.LoginName, object.TextMethodToQuery(q.Method)) +} + +func resourceOwnerQueryToQuery(q *user.OrganizationIdQuery) (query.SearchQuery, error) { + return query.NewUserResourceOwnerSearchQuery(q.OrganizationId, query.TextEquals) +} + +func inUserIdsQueryToQuery(q *user.InUserIDQuery) (query.SearchQuery, error) { + return query.NewUserInUserIdsSearchQuery(q.UserIds) +} +func orQueryToQuery(q *user.OrQuery, level uint8) (query.SearchQuery, error) { + mappedQueries, err := userQueriesToQuery(q.Queries, level+1) + if err != nil { + return nil, err + } + return query.NewUserOrSearchQuery(mappedQueries) +} +func andQueryToQuery(q *user.AndQuery, level uint8) (query.SearchQuery, error) { + mappedQueries, err := userQueriesToQuery(q.Queries, level+1) + if err != nil { + return nil, err + } + return query.NewUserAndSearchQuery(mappedQueries) +} +func notQueryToQuery(q *user.NotQuery, level uint8) (query.SearchQuery, error) { + mappedQuery, err := userQueryToQuery(q.Query, level+1) + if err != nil { + return nil, err + } + return query.NewUserNotSearchQuery(mappedQuery) +} + +func inUserEmailsQueryToQuery(q *user.InUserEmailsQuery) (query.SearchQuery, error) { + return query.NewUserInUserEmailsSearchQuery(q.UserEmails) +} diff --git a/internal/api/grpc/user/v2beta/query_integration_test.go b/internal/api/grpc/user/v2beta/query_integration_test.go new file mode 100644 index 0000000000..124e47bb27 --- /dev/null +++ b/internal/api/grpc/user/v2beta/query_integration_test.go @@ -0,0 +1,982 @@ +//go:build integration + +package user_test + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/muhlemmer/gu" + "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" + object_v2beta "github.com/zitadel/zitadel/pkg/grpc/object/v2beta" + user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta" +) + +func detailsV2ToV2beta(obj *object.Details) *object_v2beta.Details { + return &object_v2beta.Details{ + Sequence: obj.GetSequence(), + ChangeDate: obj.GetChangeDate(), + ResourceOwner: obj.GetResourceOwner(), + } +} + +func TestServer_GetUserByID(t *testing.T) { + orgResp := Tester.CreateOrganization(IamCTX, fmt.Sprintf("GetUserByIDOrg%d", time.Now().UnixNano()), fmt.Sprintf("%d@mouse.com", time.Now().UnixNano())) + type args struct { + ctx context.Context + req *user.GetUserByIDRequest + dep func(ctx context.Context, username string, request *user.GetUserByIDRequest) (*userAttr, error) + } + tests := []struct { + name string + args args + want *user.GetUserByIDResponse + wantErr bool + }{ + { + name: "user by ID, no id provided", + args: args{ + IamCTX, + &user.GetUserByIDRequest{ + UserId: "", + }, + func(ctx context.Context, username string, request *user.GetUserByIDRequest) (*userAttr, error) { + return nil, nil + }, + }, + wantErr: true, + }, + { + name: "user by ID, not found", + args: args{ + IamCTX, + &user.GetUserByIDRequest{ + UserId: "unknown", + }, + func(ctx context.Context, username string, request *user.GetUserByIDRequest) (*userAttr, error) { + return nil, nil + }, + }, + wantErr: true, + }, + { + name: "user by ID, ok", + args: args{ + IamCTX, + &user.GetUserByIDRequest{}, + func(ctx context.Context, username string, request *user.GetUserByIDRequest) (*userAttr, error) { + resp := Tester.CreateHumanUserVerified(ctx, orgResp.OrganizationId, username) + request.UserId = resp.GetUserId() + return &userAttr{resp.GetUserId(), username, nil, resp.GetDetails()}, nil + }, + }, + want: &user.GetUserByIDResponse{ + User: &user.User{ + State: user.UserState_USER_STATE_ACTIVE, + Username: "", + LoginNames: nil, + PreferredLoginName: "", + Type: &user.User_Human{ + Human: &user.HumanUser{ + Profile: &user.HumanProfile{ + GivenName: "Mickey", + FamilyName: "Mouse", + NickName: gu.Ptr("Mickey"), + DisplayName: gu.Ptr("Mickey Mouse"), + PreferredLanguage: gu.Ptr("nl"), + Gender: user.Gender_GENDER_MALE.Enum(), + AvatarUrl: "", + }, + Email: &user.HumanEmail{ + IsVerified: true, + }, + Phone: &user.HumanPhone{ + Phone: "+41791234567", + IsVerified: true, + }, + }, + }, + }, + Details: &object_v2beta.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: orgResp.OrganizationId, + }, + }, + }, + { + name: "user by ID, passwordChangeRequired, ok", + args: args{ + IamCTX, + &user.GetUserByIDRequest{}, + func(ctx context.Context, username string, request *user.GetUserByIDRequest) (*userAttr, error) { + resp := Tester.CreateHumanUserVerified(ctx, orgResp.OrganizationId, username) + request.UserId = resp.GetUserId() + details := Tester.SetUserPassword(ctx, resp.GetUserId(), integration.UserPassword, true) + return &userAttr{resp.GetUserId(), username, details.GetChangeDate(), resp.GetDetails()}, nil + }, + }, + want: &user.GetUserByIDResponse{ + User: &user.User{ + State: user.UserState_USER_STATE_ACTIVE, + Username: "", + LoginNames: nil, + PreferredLoginName: "", + Type: &user.User_Human{ + Human: &user.HumanUser{ + Profile: &user.HumanProfile{ + GivenName: "Mickey", + FamilyName: "Mouse", + NickName: gu.Ptr("Mickey"), + DisplayName: gu.Ptr("Mickey Mouse"), + PreferredLanguage: gu.Ptr("nl"), + Gender: user.Gender_GENDER_MALE.Enum(), + AvatarUrl: "", + }, + Email: &user.HumanEmail{ + IsVerified: true, + }, + Phone: &user.HumanPhone{ + Phone: "+41791234567", + IsVerified: true, + }, + PasswordChangeRequired: true, + PasswordChanged: timestamppb.Now(), + }, + }, + }, + Details: &object_v2beta.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: orgResp.OrganizationId, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + username := fmt.Sprintf("%d@mouse.com", time.Now().UnixNano()) + userAttr, err := tt.args.dep(tt.args.ctx, username, tt.args.req) + require.NoError(t, err) + retryDuration := time.Minute + if ctxDeadline, ok := CTX.Deadline(); ok { + retryDuration = time.Until(ctxDeadline) + } + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + got, getErr := Client.GetUserByID(tt.args.ctx, tt.args.req) + assertErr := assert.NoError + if tt.wantErr { + assertErr = assert.Error + } + assertErr(ttt, getErr) + if getErr != nil { + return + } + tt.want.User.Details = detailsV2ToV2beta(userAttr.Details) + tt.want.User.UserId = userAttr.UserID + tt.want.User.Username = userAttr.Username + tt.want.User.PreferredLoginName = userAttr.Username + tt.want.User.LoginNames = []string{userAttr.Username} + if human := tt.want.User.GetHuman(); human != nil { + human.Email.Email = userAttr.Username + if tt.want.User.GetHuman().GetPasswordChanged() != nil { + human.PasswordChanged = userAttr.Changed + } + } + assert.Equal(ttt, tt.want.User, got.User) + integration.AssertDetails(t, tt.want, got) + }, retryDuration, time.Second) + }) + } +} + +func TestServer_GetUserByID_Permission(t *testing.T) { + timeNow := time.Now().UTC() + newOrgOwnerEmail := fmt.Sprintf("%d@permission.get.com", timeNow.UnixNano()) + newOrg := Tester.CreateOrganization(IamCTX, fmt.Sprintf("GetHuman%d", time.Now().UnixNano()), newOrgOwnerEmail) + newUserID := newOrg.CreatedAdmins[0].GetUserId() + type args struct { + ctx context.Context + req *user.GetUserByIDRequest + } + tests := []struct { + name string + args args + want *user.GetUserByIDResponse + wantErr bool + }{ + { + name: "System, ok", + args: args{ + SystemCTX, + &user.GetUserByIDRequest{ + UserId: newUserID, + }, + }, + want: &user.GetUserByIDResponse{ + User: &user.User{ + State: user.UserState_USER_STATE_ACTIVE, + Username: "", + LoginNames: nil, + PreferredLoginName: "", + Type: &user.User_Human{ + Human: &user.HumanUser{ + Profile: &user.HumanProfile{ + GivenName: "firstname", + FamilyName: "lastname", + NickName: gu.Ptr(""), + DisplayName: gu.Ptr("firstname lastname"), + PreferredLanguage: gu.Ptr("und"), + Gender: user.Gender_GENDER_UNSPECIFIED.Enum(), + AvatarUrl: "", + }, + Email: &user.HumanEmail{ + Email: newOrgOwnerEmail, + }, + Phone: &user.HumanPhone{}, + }, + }, + }, + Details: &object_v2beta.Details{ + ChangeDate: timestamppb.New(timeNow), + ResourceOwner: newOrg.GetOrganizationId(), + }, + }, + }, + { + name: "Instance, ok", + args: args{ + IamCTX, + &user.GetUserByIDRequest{ + UserId: newUserID, + }, + }, + want: &user.GetUserByIDResponse{ + User: &user.User{ + State: user.UserState_USER_STATE_ACTIVE, + Username: "", + LoginNames: nil, + PreferredLoginName: "", + Type: &user.User_Human{ + Human: &user.HumanUser{ + Profile: &user.HumanProfile{ + GivenName: "firstname", + FamilyName: "lastname", + NickName: gu.Ptr(""), + DisplayName: gu.Ptr("firstname lastname"), + PreferredLanguage: gu.Ptr("und"), + Gender: user.Gender_GENDER_UNSPECIFIED.Enum(), + AvatarUrl: "", + }, + Email: &user.HumanEmail{ + Email: newOrgOwnerEmail, + }, + Phone: &user.HumanPhone{}, + }, + }, + }, + Details: &object_v2beta.Details{ + ChangeDate: timestamppb.New(timeNow), + ResourceOwner: newOrg.GetOrganizationId(), + }, + }, + }, + { + name: "Org, error", + args: args{ + CTX, + &user.GetUserByIDRequest{ + UserId: newUserID, + }, + }, + wantErr: true, + }, + { + name: "User, error", + args: args{ + UserCTX, + &user.GetUserByIDRequest{ + UserId: newUserID, + }, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Client.GetUserByID(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + tt.want.User.UserId = tt.args.req.GetUserId() + tt.want.User.Username = newOrgOwnerEmail + tt.want.User.PreferredLoginName = newOrgOwnerEmail + tt.want.User.LoginNames = []string{newOrgOwnerEmail} + if human := tt.want.User.GetHuman(); human != nil { + human.Email.Email = newOrgOwnerEmail + } + // details tested in GetUserByID + tt.want.User.Details = got.User.GetDetails() + + assert.Equal(t, tt.want.User, got.User) + } + }) + } +} + +type userAttr struct { + UserID string + Username string + Changed *timestamppb.Timestamp + Details *object.Details +} + +func TestServer_ListUsers(t *testing.T) { + orgResp := Tester.CreateOrganization(IamCTX, fmt.Sprintf("ListUsersOrg%d", time.Now().UnixNano()), fmt.Sprintf("%d@mouse.com", time.Now().UnixNano())) + userResp := Tester.CreateHumanUserVerified(IamCTX, orgResp.OrganizationId, fmt.Sprintf("%d@listusers.com", time.Now().UnixNano())) + type args struct { + ctx context.Context + count int + req *user.ListUsersRequest + dep func(ctx context.Context, usernames []string, request *user.ListUsersRequest) ([]userAttr, error) + } + tests := []struct { + name string + args args + want *user.ListUsersResponse + wantErr bool + }{ + { + name: "list user by id, no permission", + args: args{ + UserCTX, + 0, + &user.ListUsersRequest{}, + func(ctx context.Context, usernames []string, request *user.ListUsersRequest) ([]userAttr, error) { + request.Queries = append(request.Queries, InUserIDsQuery([]string{userResp.UserId})) + return []userAttr{}, nil + }, + }, + want: &user.ListUsersResponse{ + Details: &object_v2beta.ListDetails{ + TotalResult: 0, + Timestamp: timestamppb.Now(), + }, + SortingColumn: 0, + Result: []*user.User{}, + }, + }, + { + name: "list user by id, ok", + args: args{ + IamCTX, + 1, + &user.ListUsersRequest{ + Queries: []*user.SearchQuery{ + OrganizationIdQuery(orgResp.OrganizationId), + }, + }, + func(ctx context.Context, usernames []string, request *user.ListUsersRequest) ([]userAttr, error) { + infos := make([]userAttr, len(usernames)) + userIDs := make([]string, len(usernames)) + for i, username := range usernames { + resp := Tester.CreateHumanUserVerified(ctx, orgResp.OrganizationId, username) + userIDs[i] = resp.GetUserId() + infos[i] = userAttr{resp.GetUserId(), username, nil, resp.GetDetails()} + } + request.Queries = append(request.Queries, InUserIDsQuery(userIDs)) + return infos, nil + }, + }, + want: &user.ListUsersResponse{ + Details: &object_v2beta.ListDetails{ + TotalResult: 1, + Timestamp: timestamppb.Now(), + }, + SortingColumn: 0, + Result: []*user.User{ + { + State: user.UserState_USER_STATE_ACTIVE, + Type: &user.User_Human{ + Human: &user.HumanUser{ + Profile: &user.HumanProfile{ + GivenName: "Mickey", + FamilyName: "Mouse", + NickName: gu.Ptr("Mickey"), + DisplayName: gu.Ptr("Mickey Mouse"), + PreferredLanguage: gu.Ptr("nl"), + Gender: user.Gender_GENDER_MALE.Enum(), + }, + Email: &user.HumanEmail{ + IsVerified: true, + }, + Phone: &user.HumanPhone{ + Phone: "+41791234567", + IsVerified: true, + }, + }, + }, + }, + }, + }, + }, + { + name: "list user by id, passwordChangeRequired, ok", + args: args{ + IamCTX, + 1, + &user.ListUsersRequest{ + Queries: []*user.SearchQuery{ + OrganizationIdQuery(orgResp.OrganizationId), + }, + }, + func(ctx context.Context, usernames []string, request *user.ListUsersRequest) ([]userAttr, error) { + infos := make([]userAttr, len(usernames)) + userIDs := make([]string, len(usernames)) + for i, username := range usernames { + resp := Tester.CreateHumanUserVerified(ctx, orgResp.OrganizationId, username) + userIDs[i] = resp.GetUserId() + details := Tester.SetUserPassword(ctx, resp.GetUserId(), integration.UserPassword, true) + infos[i] = userAttr{resp.GetUserId(), username, details.GetChangeDate(), resp.GetDetails()} + } + request.Queries = append(request.Queries, InUserIDsQuery(userIDs)) + return infos, nil + }, + }, + want: &user.ListUsersResponse{ + Details: &object_v2beta.ListDetails{ + TotalResult: 1, + Timestamp: timestamppb.Now(), + }, + SortingColumn: 0, + Result: []*user.User{ + { + State: user.UserState_USER_STATE_ACTIVE, + Type: &user.User_Human{ + Human: &user.HumanUser{ + Profile: &user.HumanProfile{ + GivenName: "Mickey", + FamilyName: "Mouse", + NickName: gu.Ptr("Mickey"), + DisplayName: gu.Ptr("Mickey Mouse"), + PreferredLanguage: gu.Ptr("nl"), + Gender: user.Gender_GENDER_MALE.Enum(), + }, + Email: &user.HumanEmail{ + IsVerified: true, + }, + Phone: &user.HumanPhone{ + Phone: "+41791234567", + IsVerified: true, + }, + PasswordChangeRequired: true, + PasswordChanged: timestamppb.Now(), + }, + }, + }, + }, + }, + }, + { + name: "list user by id multiple, ok", + args: args{ + IamCTX, + 3, + &user.ListUsersRequest{ + Queries: []*user.SearchQuery{ + OrganizationIdQuery(orgResp.OrganizationId), + }, + }, + func(ctx context.Context, usernames []string, request *user.ListUsersRequest) ([]userAttr, error) { + infos := make([]userAttr, len(usernames)) + userIDs := make([]string, len(usernames)) + for i, username := range usernames { + resp := Tester.CreateHumanUserVerified(ctx, orgResp.OrganizationId, username) + userIDs[i] = resp.GetUserId() + infos[i] = userAttr{resp.GetUserId(), username, nil, resp.GetDetails()} + } + request.Queries = append(request.Queries, InUserIDsQuery(userIDs)) + return infos, nil + }, + }, + want: &user.ListUsersResponse{ + Details: &object_v2beta.ListDetails{ + TotalResult: 3, + Timestamp: timestamppb.Now(), + }, + SortingColumn: 0, + Result: []*user.User{ + { + State: user.UserState_USER_STATE_ACTIVE, + Type: &user.User_Human{ + Human: &user.HumanUser{ + Profile: &user.HumanProfile{ + GivenName: "Mickey", + FamilyName: "Mouse", + NickName: gu.Ptr("Mickey"), + DisplayName: gu.Ptr("Mickey Mouse"), + PreferredLanguage: gu.Ptr("nl"), + Gender: user.Gender_GENDER_MALE.Enum(), + }, + Email: &user.HumanEmail{ + IsVerified: true, + }, + Phone: &user.HumanPhone{ + Phone: "+41791234567", + IsVerified: true, + }, + }, + }, + }, { + State: user.UserState_USER_STATE_ACTIVE, + Type: &user.User_Human{ + Human: &user.HumanUser{ + Profile: &user.HumanProfile{ + GivenName: "Mickey", + FamilyName: "Mouse", + NickName: gu.Ptr("Mickey"), + DisplayName: gu.Ptr("Mickey Mouse"), + PreferredLanguage: gu.Ptr("nl"), + Gender: user.Gender_GENDER_MALE.Enum(), + }, + Email: &user.HumanEmail{ + IsVerified: true, + }, + Phone: &user.HumanPhone{ + Phone: "+41791234567", + IsVerified: true, + }, + }, + }, + }, { + State: user.UserState_USER_STATE_ACTIVE, + Type: &user.User_Human{ + Human: &user.HumanUser{ + Profile: &user.HumanProfile{ + GivenName: "Mickey", + FamilyName: "Mouse", + NickName: gu.Ptr("Mickey"), + DisplayName: gu.Ptr("Mickey Mouse"), + PreferredLanguage: gu.Ptr("nl"), + Gender: user.Gender_GENDER_MALE.Enum(), + }, + Email: &user.HumanEmail{ + IsVerified: true, + }, + Phone: &user.HumanPhone{ + Phone: "+41791234567", + IsVerified: true, + }, + }, + }, + }, + }, + }, + }, + { + name: "list user by username, ok", + args: args{ + IamCTX, + 1, + &user.ListUsersRequest{ + Queries: []*user.SearchQuery{ + OrganizationIdQuery(orgResp.OrganizationId), + }, + }, + func(ctx context.Context, usernames []string, request *user.ListUsersRequest) ([]userAttr, error) { + infos := make([]userAttr, len(usernames)) + userIDs := make([]string, len(usernames)) + for i, username := range usernames { + resp := Tester.CreateHumanUserVerified(ctx, orgResp.OrganizationId, username) + userIDs[i] = resp.GetUserId() + infos[i] = userAttr{resp.GetUserId(), username, nil, resp.GetDetails()} + request.Queries = append(request.Queries, UsernameQuery(username)) + } + return infos, nil + }, + }, + want: &user.ListUsersResponse{ + Details: &object_v2beta.ListDetails{ + TotalResult: 1, + Timestamp: timestamppb.Now(), + }, + SortingColumn: 0, + Result: []*user.User{ + { + State: user.UserState_USER_STATE_ACTIVE, + Type: &user.User_Human{ + Human: &user.HumanUser{ + Profile: &user.HumanProfile{ + GivenName: "Mickey", + FamilyName: "Mouse", + NickName: gu.Ptr("Mickey"), + DisplayName: gu.Ptr("Mickey Mouse"), + PreferredLanguage: gu.Ptr("nl"), + Gender: user.Gender_GENDER_MALE.Enum(), + }, + Email: &user.HumanEmail{ + IsVerified: true, + }, + Phone: &user.HumanPhone{ + Phone: "+41791234567", + IsVerified: true, + }, + }, + }, + }, + }, + }, + }, + { + name: "list user in emails, ok", + args: args{ + IamCTX, + 1, + &user.ListUsersRequest{ + Queries: []*user.SearchQuery{ + OrganizationIdQuery(orgResp.OrganizationId), + }, + }, + func(ctx context.Context, usernames []string, request *user.ListUsersRequest) ([]userAttr, error) { + infos := make([]userAttr, len(usernames)) + for i, username := range usernames { + resp := Tester.CreateHumanUserVerified(ctx, orgResp.OrganizationId, username) + infos[i] = userAttr{resp.GetUserId(), username, nil, resp.GetDetails()} + } + request.Queries = append(request.Queries, InUserEmailsQuery(usernames)) + return infos, nil + }, + }, + want: &user.ListUsersResponse{ + Details: &object_v2beta.ListDetails{ + TotalResult: 1, + Timestamp: timestamppb.Now(), + }, + SortingColumn: 0, + Result: []*user.User{ + { + State: user.UserState_USER_STATE_ACTIVE, + Type: &user.User_Human{ + Human: &user.HumanUser{ + Profile: &user.HumanProfile{ + GivenName: "Mickey", + FamilyName: "Mouse", + NickName: gu.Ptr("Mickey"), + DisplayName: gu.Ptr("Mickey Mouse"), + PreferredLanguage: gu.Ptr("nl"), + Gender: user.Gender_GENDER_MALE.Enum(), + }, + Email: &user.HumanEmail{ + IsVerified: true, + }, + Phone: &user.HumanPhone{ + Phone: "+41791234567", + IsVerified: true, + }, + }, + }, + }, + }, + }, + }, + { + name: "list user in emails multiple, ok", + args: args{ + IamCTX, + 3, + &user.ListUsersRequest{ + Queries: []*user.SearchQuery{ + OrganizationIdQuery(orgResp.OrganizationId), + }, + }, + func(ctx context.Context, usernames []string, request *user.ListUsersRequest) ([]userAttr, error) { + infos := make([]userAttr, len(usernames)) + for i, username := range usernames { + resp := Tester.CreateHumanUserVerified(ctx, orgResp.OrganizationId, username) + infos[i] = userAttr{resp.GetUserId(), username, nil, resp.GetDetails()} + } + request.Queries = append(request.Queries, InUserEmailsQuery(usernames)) + return infos, nil + }, + }, + want: &user.ListUsersResponse{ + Details: &object_v2beta.ListDetails{ + TotalResult: 3, + Timestamp: timestamppb.Now(), + }, + SortingColumn: 0, + Result: []*user.User{ + { + State: user.UserState_USER_STATE_ACTIVE, + Type: &user.User_Human{ + Human: &user.HumanUser{ + Profile: &user.HumanProfile{ + GivenName: "Mickey", + FamilyName: "Mouse", + NickName: gu.Ptr("Mickey"), + DisplayName: gu.Ptr("Mickey Mouse"), + PreferredLanguage: gu.Ptr("nl"), + Gender: user.Gender_GENDER_MALE.Enum(), + }, + Email: &user.HumanEmail{ + IsVerified: true, + }, + Phone: &user.HumanPhone{ + Phone: "+41791234567", + IsVerified: true, + }, + }, + }, + }, { + State: user.UserState_USER_STATE_ACTIVE, + Type: &user.User_Human{ + Human: &user.HumanUser{ + Profile: &user.HumanProfile{ + GivenName: "Mickey", + FamilyName: "Mouse", + NickName: gu.Ptr("Mickey"), + DisplayName: gu.Ptr("Mickey Mouse"), + PreferredLanguage: gu.Ptr("nl"), + Gender: user.Gender_GENDER_MALE.Enum(), + }, + Email: &user.HumanEmail{ + IsVerified: true, + }, + Phone: &user.HumanPhone{ + Phone: "+41791234567", + IsVerified: true, + }, + }, + }, + }, { + State: user.UserState_USER_STATE_ACTIVE, + Type: &user.User_Human{ + Human: &user.HumanUser{ + Profile: &user.HumanProfile{ + GivenName: "Mickey", + FamilyName: "Mouse", + NickName: gu.Ptr("Mickey"), + DisplayName: gu.Ptr("Mickey Mouse"), + PreferredLanguage: gu.Ptr("nl"), + Gender: user.Gender_GENDER_MALE.Enum(), + }, + Email: &user.HumanEmail{ + IsVerified: true, + }, + Phone: &user.HumanPhone{ + Phone: "+41791234567", + IsVerified: true, + }, + }, + }, + }, + }, + }, + }, + { + name: "list user in emails no found, ok", + args: args{ + IamCTX, + 3, + &user.ListUsersRequest{Queries: []*user.SearchQuery{ + OrganizationIdQuery(orgResp.OrganizationId), + InUserEmailsQuery([]string{"notfound"}), + }, + }, + func(ctx context.Context, usernames []string, request *user.ListUsersRequest) ([]userAttr, error) { + return []userAttr{}, nil + }, + }, + want: &user.ListUsersResponse{ + Details: &object_v2beta.ListDetails{ + TotalResult: 0, + Timestamp: timestamppb.Now(), + }, + SortingColumn: 0, + Result: []*user.User{}, + }, + }, + { + name: "list user resourceowner multiple, ok", + args: args{ + IamCTX, + 3, + &user.ListUsersRequest{}, + func(ctx context.Context, usernames []string, request *user.ListUsersRequest) ([]userAttr, error) { + orgResp := Tester.CreateOrganization(ctx, fmt.Sprintf("ListUsersResourceowner%d", time.Now().UnixNano()), fmt.Sprintf("%d@mouse.com", time.Now().UnixNano())) + + infos := make([]userAttr, len(usernames)) + for i, username := range usernames { + resp := Tester.CreateHumanUserVerified(ctx, orgResp.OrganizationId, username) + infos[i] = userAttr{resp.GetUserId(), username, nil, resp.GetDetails()} + } + request.Queries = append(request.Queries, OrganizationIdQuery(orgResp.OrganizationId)) + request.Queries = append(request.Queries, InUserEmailsQuery(usernames)) + return infos, nil + }, + }, + want: &user.ListUsersResponse{ + Details: &object_v2beta.ListDetails{ + TotalResult: 3, + Timestamp: timestamppb.Now(), + }, + SortingColumn: 0, + Result: []*user.User{ + { + State: user.UserState_USER_STATE_ACTIVE, + Type: &user.User_Human{ + Human: &user.HumanUser{ + Profile: &user.HumanProfile{ + GivenName: "Mickey", + FamilyName: "Mouse", + NickName: gu.Ptr("Mickey"), + DisplayName: gu.Ptr("Mickey Mouse"), + PreferredLanguage: gu.Ptr("nl"), + Gender: user.Gender_GENDER_MALE.Enum(), + }, + Email: &user.HumanEmail{ + IsVerified: true, + }, + Phone: &user.HumanPhone{ + Phone: "+41791234567", + IsVerified: true, + }, + }, + }, + }, { + State: user.UserState_USER_STATE_ACTIVE, + Type: &user.User_Human{ + Human: &user.HumanUser{ + Profile: &user.HumanProfile{ + GivenName: "Mickey", + FamilyName: "Mouse", + NickName: gu.Ptr("Mickey"), + DisplayName: gu.Ptr("Mickey Mouse"), + PreferredLanguage: gu.Ptr("nl"), + Gender: user.Gender_GENDER_MALE.Enum(), + }, + Email: &user.HumanEmail{ + IsVerified: true, + }, + Phone: &user.HumanPhone{ + Phone: "+41791234567", + IsVerified: true, + }, + }, + }, + }, { + State: user.UserState_USER_STATE_ACTIVE, + Type: &user.User_Human{ + Human: &user.HumanUser{ + Profile: &user.HumanProfile{ + GivenName: "Mickey", + FamilyName: "Mouse", + NickName: gu.Ptr("Mickey"), + DisplayName: gu.Ptr("Mickey Mouse"), + PreferredLanguage: gu.Ptr("nl"), + Gender: user.Gender_GENDER_MALE.Enum(), + }, + Email: &user.HumanEmail{ + IsVerified: true, + }, + Phone: &user.HumanPhone{ + Phone: "+41791234567", + IsVerified: true, + }, + }, + }, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + usernames := make([]string, tt.args.count) + for i := 0; i < tt.args.count; i++ { + usernames[i] = fmt.Sprintf("%d%d@mouse.com", time.Now().UnixNano(), i) + } + infos, err := tt.args.dep(tt.args.ctx, usernames, tt.args.req) + require.NoError(t, err) + retryDuration := time.Minute + if ctxDeadline, ok := CTX.Deadline(); ok { + retryDuration = time.Until(ctxDeadline) + } + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + got, listErr := Client.ListUsers(tt.args.ctx, tt.args.req) + assertErr := assert.NoError + if tt.wantErr { + assertErr = assert.Error + } + assertErr(ttt, listErr) + if listErr != nil { + return + } + // always only give back dependency infos which are required for the response + assert.Len(ttt, tt.want.Result, len(infos)) + // always first check length, otherwise its failed anyway + assert.Len(ttt, got.Result, len(tt.want.Result)) + // fill in userid and username as it is generated + for i := range infos { + tt.want.Result[i].UserId = infos[i].UserID + tt.want.Result[i].Username = infos[i].Username + tt.want.Result[i].PreferredLoginName = infos[i].Username + tt.want.Result[i].LoginNames = []string{infos[i].Username} + if human := tt.want.Result[i].GetHuman(); human != nil { + human.Email.Email = infos[i].Username + if tt.want.Result[i].GetHuman().GetPasswordChanged() != nil { + human.PasswordChanged = infos[i].Changed + } + } + tt.want.Result[i].Details = detailsV2ToV2beta(infos[i].Details) + } + for i := range tt.want.Result { + assert.Contains(ttt, got.Result, tt.want.Result[i]) + } + integration.AssertListDetails(t, tt.want, got) + }, retryDuration, time.Millisecond*100, "timeout waiting for expected user result") + }) + } +} + +func InUserIDsQuery(ids []string) *user.SearchQuery { + return &user.SearchQuery{Query: &user.SearchQuery_InUserIdsQuery{ + InUserIdsQuery: &user.InUserIDQuery{ + UserIds: ids, + }, + }, + } +} + +func InUserEmailsQuery(emails []string) *user.SearchQuery { + return &user.SearchQuery{Query: &user.SearchQuery_InUserEmailsQuery{ + InUserEmailsQuery: &user.InUserEmailsQuery{ + UserEmails: emails, + }, + }, + } +} + +func UsernameQuery(username string) *user.SearchQuery { + return &user.SearchQuery{Query: &user.SearchQuery_UserNameQuery{ + UserNameQuery: &user.UserNameQuery{ + UserName: username, + }, + }, + } +} + +func OrganizationIdQuery(resourceowner string) *user.SearchQuery { + return &user.SearchQuery{Query: &user.SearchQuery_OrganizationIdQuery{ + OrganizationIdQuery: &user.OrganizationIdQuery{ + OrganizationId: resourceowner, + }, + }, + } +} diff --git a/internal/api/grpc/user/v2beta/server.go b/internal/api/grpc/user/v2beta/server.go new file mode 100644 index 0000000000..93af47f58b --- /dev/null +++ b/internal/api/grpc/user/v2beta/server.go @@ -0,0 +1,75 @@ +package user + +import ( + "context" + + "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/crypto" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/query" + user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta" +) + +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 + + assetAPIPrefix func(context.Context) string + + checkPermission domain.PermissionCheck +} + +type Config struct{} + +func CreateServer( + 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(ctx context.Context) string, + checkPermission domain.PermissionCheck, +) *Server { + return &Server{ + command: command, + query: query, + userCodeAlg: userCodeAlg, + idpAlg: idpAlg, + idpCallback: idpCallback, + samlRootURL: samlRootURL, + assetAPIPrefix: assetAPIPrefix, + checkPermission: checkPermission, + } +} + +func (s *Server) RegisterServer(grpcServer *grpc.Server) { + user.RegisterUserServiceServer(grpcServer, s) +} + +func (s *Server) AppName() string { + return user.UserService_ServiceDesc.ServiceName +} + +func (s *Server) MethodPrefix() string { + return user.UserService_ServiceDesc.ServiceName +} + +func (s *Server) AuthMethods() authz.MethodMapping { + return user.UserService_AuthMethods +} + +func (s *Server) RegisterGateway() server.RegisterGatewayFunc { + return user.RegisterUserServiceHandler +} diff --git a/internal/api/grpc/user/v2beta/totp.go b/internal/api/grpc/user/v2beta/totp.go new file mode 100644 index 0000000000..2ef47a9817 --- /dev/null +++ b/internal/api/grpc/user/v2beta/totp.go @@ -0,0 +1,44 @@ +package user + +import ( + "context" + + object "github.com/zitadel/zitadel/internal/api/grpc/object/v2beta" + "github.com/zitadel/zitadel/internal/domain" + user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta" +) + +func (s *Server) RegisterTOTP(ctx context.Context, req *user.RegisterTOTPRequest) (*user.RegisterTOTPResponse, error) { + return totpDetailsToPb( + s.command.AddUserTOTP(ctx, req.GetUserId(), ""), + ) +} + +func totpDetailsToPb(totp *domain.TOTP, err error) (*user.RegisterTOTPResponse, error) { + if err != nil { + return nil, err + } + return &user.RegisterTOTPResponse{ + Details: object.DomainToDetailsPb(totp.ObjectDetails), + Uri: totp.URI, + Secret: totp.Secret, + }, nil +} + +func (s *Server) VerifyTOTPRegistration(ctx context.Context, req *user.VerifyTOTPRegistrationRequest) (*user.VerifyTOTPRegistrationResponse, error) { + objectDetails, err := s.command.CheckUserTOTP(ctx, req.GetUserId(), req.GetCode(), "") + if err != nil { + return nil, err + } + return &user.VerifyTOTPRegistrationResponse{ + Details: object.DomainToDetailsPb(objectDetails), + }, nil +} + +func (s *Server) RemoveTOTP(ctx context.Context, req *user.RemoveTOTPRequest) (*user.RemoveTOTPResponse, error) { + objectDetails, err := s.command.HumanRemoveTOTP(ctx, req.GetUserId(), "") + if err != nil { + return nil, err + } + return &user.RemoveTOTPResponse{Details: object.DomainToDetailsPb(objectDetails)}, nil +} diff --git a/internal/api/grpc/user/v2beta/totp_integration_test.go b/internal/api/grpc/user/v2beta/totp_integration_test.go new file mode 100644 index 0000000000..47b2952afd --- /dev/null +++ b/internal/api/grpc/user/v2beta/totp_integration_test.go @@ -0,0 +1,284 @@ +//go:build integration + +package user_test + +import ( + "context" + "testing" + "time" + + "github.com/pquerna/otp/totp" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/integration" + object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta" + user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta" +) + +func TestServer_RegisterTOTP(t *testing.T) { + userID := Tester.CreateHumanUser(CTX).GetUserId() + Tester.RegisterUserPasskey(CTX, userID) + _, sessionToken, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, userID) + ctx := Tester.WithAuthorizationToken(CTX, sessionToken) + + otherUser := Tester.CreateHumanUser(CTX).GetUserId() + Tester.RegisterUserPasskey(CTX, otherUser) + _, sessionTokenOtherUser, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, otherUser) + ctxOtherUser := Tester.WithAuthorizationToken(CTX, sessionTokenOtherUser) + + type args struct { + ctx context.Context + req *user.RegisterTOTPRequest + } + tests := []struct { + name string + args args + want *user.RegisterTOTPResponse + wantErr bool + }{ + { + name: "missing user id", + args: args{ + ctx: ctx, + req: &user.RegisterTOTPRequest{}, + }, + wantErr: true, + }, + { + name: "user mismatch", + args: args{ + ctx: ctxOtherUser, + req: &user.RegisterTOTPRequest{ + UserId: userID, + }, + }, + wantErr: true, + }, + { + name: "admin", + args: args{ + ctx: CTX, + req: &user.RegisterTOTPRequest{ + UserId: userID, + }, + }, + want: &user.RegisterTOTPResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Organisation.ID, + }, + }, + }, + { + name: "success", + args: args{ + ctx: ctx, + req: &user.RegisterTOTPRequest{ + UserId: userID, + }, + }, + want: &user.RegisterTOTPResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Organisation.ID, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Client.RegisterTOTP(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + require.NotNil(t, got) + integration.AssertDetails(t, tt.want, got) + assert.NotEmpty(t, got.GetUri()) + assert.NotEmpty(t, got.GetSecret()) + }) + } +} + +func TestServer_VerifyTOTPRegistration(t *testing.T) { + userID := Tester.CreateHumanUser(CTX).GetUserId() + Tester.RegisterUserPasskey(CTX, userID) + _, sessionToken, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, userID) + ctx := Tester.WithAuthorizationToken(CTX, sessionToken) + + reg, err := Client.RegisterTOTP(ctx, &user.RegisterTOTPRequest{ + UserId: userID, + }) + require.NoError(t, err) + code, err := totp.GenerateCode(reg.Secret, time.Now()) + require.NoError(t, err) + + otherUser := Tester.CreateHumanUser(CTX).GetUserId() + Tester.RegisterUserPasskey(CTX, otherUser) + _, sessionTokenOtherUser, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, otherUser) + ctxOtherUser := Tester.WithAuthorizationToken(CTX, sessionTokenOtherUser) + + regOtherUser, err := Client.RegisterTOTP(CTX, &user.RegisterTOTPRequest{ + UserId: otherUser, + }) + require.NoError(t, err) + codeOtherUser, err := totp.GenerateCode(regOtherUser.Secret, time.Now()) + require.NoError(t, err) + + type args struct { + ctx context.Context + req *user.VerifyTOTPRegistrationRequest + } + tests := []struct { + name string + args args + want *user.VerifyTOTPRegistrationResponse + wantErr bool + }{ + { + name: "user mismatch", + args: args{ + ctx: ctxOtherUser, + req: &user.VerifyTOTPRegistrationRequest{ + UserId: userID, + }, + }, + wantErr: true, + }, + { + name: "wrong code", + args: args{ + ctx: ctx, + req: &user.VerifyTOTPRegistrationRequest{ + UserId: userID, + Code: "123", + }, + }, + wantErr: true, + }, + { + name: "success", + args: args{ + ctx: ctx, + req: &user.VerifyTOTPRegistrationRequest{ + UserId: userID, + Code: code, + }, + }, + want: &user.VerifyTOTPRegistrationResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Organisation.ResourceOwner, + }, + }, + }, + { + name: "success, admin", + args: args{ + ctx: CTX, + req: &user.VerifyTOTPRegistrationRequest{ + UserId: otherUser, + Code: codeOtherUser, + }, + }, + want: &user.VerifyTOTPRegistrationResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Organisation.ResourceOwner, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Client.VerifyTOTPRegistration(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + require.NotNil(t, got) + integration.AssertDetails(t, tt.want, got) + }) + } +} + +func TestServer_RemoveTOTP(t *testing.T) { + userID := Tester.CreateHumanUser(CTX).GetUserId() + Tester.RegisterUserPasskey(CTX, userID) + _, sessionToken, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, userID) + + userVerified := Tester.CreateHumanUser(CTX) + Tester.RegisterUserPasskey(CTX, userVerified.GetUserId()) + _, sessionTokenVerified, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, userVerified.GetUserId()) + userVerifiedCtx := Tester.WithAuthorizationToken(context.Background(), sessionTokenVerified) + _, err := Client.VerifyPhone(userVerifiedCtx, &user.VerifyPhoneRequest{ + UserId: userVerified.GetUserId(), + VerificationCode: userVerified.GetPhoneCode(), + }) + require.NoError(t, err) + + regOtherUser, err := Client.RegisterTOTP(CTX, &user.RegisterTOTPRequest{ + UserId: userVerified.GetUserId(), + }) + require.NoError(t, err) + codeOtherUser, err := totp.GenerateCode(regOtherUser.Secret, time.Now()) + require.NoError(t, err) + _, err = Client.VerifyTOTPRegistration(userVerifiedCtx, &user.VerifyTOTPRegistrationRequest{ + UserId: userVerified.GetUserId(), + Code: codeOtherUser, + }, + ) + require.NoError(t, err) + + type args struct { + ctx context.Context + req *user.RemoveTOTPRequest + } + tests := []struct { + name string + args args + want *user.RemoveTOTPResponse + wantErr bool + }{ + { + name: "not added", + args: args{ + ctx: Tester.WithAuthorizationToken(context.Background(), sessionToken), + req: &user.RemoveTOTPRequest{ + UserId: userID, + }, + }, + wantErr: true, + }, + { + name: "success", + args: args{ + ctx: userVerifiedCtx, + req: &user.RemoveTOTPRequest{ + UserId: userVerified.GetUserId(), + }, + }, + want: &user.RemoveTOTPResponse{ + Details: &object.Details{ + ResourceOwner: Tester.Organisation.ResourceOwner, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Client.RemoveTOTP(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + require.NotNil(t, got) + integration.AssertDetails(t, tt.want, got) + }) + } +} diff --git a/internal/api/grpc/user/v2beta/totp_test.go b/internal/api/grpc/user/v2beta/totp_test.go new file mode 100644 index 0000000000..81a54675f2 --- /dev/null +++ b/internal/api/grpc/user/v2beta/totp_test.go @@ -0,0 +1,71 @@ +package user + +import ( + "io" + "testing" + "time" + + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/domain" + object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta" + user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta" +) + +func Test_totpDetailsToPb(t *testing.T) { + type args struct { + otp *domain.TOTP + err error + } + tests := []struct { + name string + args args + want *user.RegisterTOTPResponse + wantErr error + }{ + { + name: "error", + args: args{ + err: io.ErrClosedPipe, + }, + wantErr: io.ErrClosedPipe, + }, + { + name: "success", + args: args{ + otp: &domain.TOTP{ + ObjectDetails: &domain.ObjectDetails{ + Sequence: 123, + EventDate: time.Unix(456, 789), + ResourceOwner: "me", + }, + Secret: "secret", + URI: "URI", + }, + }, + want: &user.RegisterTOTPResponse{ + Details: &object.Details{ + Sequence: 123, + ChangeDate: ×tamppb.Timestamp{ + Seconds: 456, + Nanos: 789, + }, + ResourceOwner: "me", + }, + Secret: "secret", + Uri: "URI", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := totpDetailsToPb(tt.args.otp, tt.args.err) + require.ErrorIs(t, err, tt.wantErr) + if !proto.Equal(tt.want, got) { + t.Errorf("RegisterTOTPResponse =\n%v\nwant\n%v", got, tt.want) + } + }) + } +} diff --git a/internal/api/grpc/user/v2beta/u2f.go b/internal/api/grpc/user/v2beta/u2f.go new file mode 100644 index 0000000000..e23a22b8b5 --- /dev/null +++ b/internal/api/grpc/user/v2beta/u2f.go @@ -0,0 +1,42 @@ +package user + +import ( + "context" + + object "github.com/zitadel/zitadel/internal/api/grpc/object/v2beta" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/zerrors" + user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta" +) + +func (s *Server) RegisterU2F(ctx context.Context, req *user.RegisterU2FRequest) (*user.RegisterU2FResponse, error) { + return u2fRegistrationDetailsToPb( + s.command.RegisterUserU2F(ctx, req.GetUserId(), "", req.GetDomain()), + ) +} + +func u2fRegistrationDetailsToPb(details *domain.WebAuthNRegistrationDetails, err error) (*user.RegisterU2FResponse, error) { + objectDetails, options, err := webAuthNRegistrationDetailsToPb(details, err) + if err != nil { + return nil, err + } + return &user.RegisterU2FResponse{ + Details: objectDetails, + U2FId: details.ID, + PublicKeyCredentialCreationOptions: options, + }, nil +} + +func (s *Server) VerifyU2FRegistration(ctx context.Context, req *user.VerifyU2FRegistrationRequest) (*user.VerifyU2FRegistrationResponse, error) { + pkc, err := req.GetPublicKeyCredential().MarshalJSON() + if err != nil { + return nil, zerrors.ThrowInternal(err, "USERv2-IeTh4", "Errors.Internal") + } + objectDetails, err := s.command.HumanVerifyU2FSetup(ctx, req.GetUserId(), "", req.GetTokenName(), "", pkc) + if err != nil { + return nil, err + } + return &user.VerifyU2FRegistrationResponse{ + Details: object.DomainToDetailsPb(objectDetails), + }, nil +} diff --git a/internal/api/grpc/user/v2beta/u2f_integration_test.go b/internal/api/grpc/user/v2beta/u2f_integration_test.go new file mode 100644 index 0000000000..3b7fbd293c --- /dev/null +++ b/internal/api/grpc/user/v2beta/u2f_integration_test.go @@ -0,0 +1,190 @@ +//go:build integration + +package user_test + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/structpb" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/integration" + object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta" + user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta" +) + +func TestServer_RegisterU2F(t *testing.T) { + userID := Tester.CreateHumanUser(CTX).GetUserId() + otherUser := Tester.CreateHumanUser(CTX).GetUserId() + + // We also need a user session + Tester.RegisterUserPasskey(CTX, userID) + _, sessionToken, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, userID) + Tester.RegisterUserPasskey(CTX, otherUser) + _, sessionTokenOtherUser, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, otherUser) + + type args struct { + ctx context.Context + req *user.RegisterU2FRequest + } + tests := []struct { + name string + args args + want *user.RegisterU2FResponse + wantErr bool + }{ + { + name: "missing user id", + args: args{ + ctx: CTX, + req: &user.RegisterU2FRequest{}, + }, + wantErr: true, + }, + { + name: "admin user", + args: args{ + ctx: CTX, + req: &user.RegisterU2FRequest{ + UserId: userID, + }, + }, + want: &user.RegisterU2FResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Organisation.ID, + }, + }, + }, + { + name: "other user, no permission", + args: args{ + ctx: Tester.WithAuthorizationToken(CTX, sessionTokenOtherUser), + req: &user.RegisterU2FRequest{ + UserId: userID, + }, + }, + wantErr: true, + }, + { + name: "user setting its own passkey", + args: args{ + ctx: Tester.WithAuthorizationToken(CTX, sessionToken), + req: &user.RegisterU2FRequest{ + UserId: userID, + }, + }, + want: &user.RegisterU2FResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Organisation.ID, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Client.RegisterU2F(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + require.NotNil(t, got) + integration.AssertDetails(t, tt.want, got) + if tt.want != nil { + assert.NotEmpty(t, got.GetU2FId()) + assert.NotEmpty(t, got.GetPublicKeyCredentialCreationOptions()) + _, err = Tester.WebAuthN.CreateAttestationResponse(got.GetPublicKeyCredentialCreationOptions()) + require.NoError(t, err) + } + }) + } +} + +func TestServer_VerifyU2FRegistration(t *testing.T) { + userID := Tester.CreateHumanUser(CTX).GetUserId() + Tester.RegisterUserPasskey(CTX, userID) + _, sessionToken, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, userID) + ctx := Tester.WithAuthorizationToken(CTX, sessionToken) + + pkr, err := Client.RegisterU2F(ctx, &user.RegisterU2FRequest{ + UserId: userID, + }) + require.NoError(t, err) + require.NotEmpty(t, pkr.GetPublicKeyCredentialCreationOptions()) + + attestationResponse, err := Tester.WebAuthN.CreateAttestationResponse(pkr.GetPublicKeyCredentialCreationOptions()) + require.NoError(t, err) + + type args struct { + ctx context.Context + req *user.VerifyU2FRegistrationRequest + } + tests := []struct { + name string + args args + want *user.VerifyU2FRegistrationResponse + wantErr bool + }{ + { + name: "missing user id", + args: args{ + ctx: ctx, + req: &user.VerifyU2FRegistrationRequest{ + U2FId: "123", + TokenName: "nice name", + }, + }, + wantErr: true, + }, + { + name: "success", + args: args{ + ctx: ctx, + req: &user.VerifyU2FRegistrationRequest{ + UserId: userID, + U2FId: pkr.GetU2FId(), + PublicKeyCredential: attestationResponse, + TokenName: "nice name", + }, + }, + want: &user.VerifyU2FRegistrationResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Organisation.ID, + }, + }, + }, + { + name: "wrong credential", + args: args{ + ctx: ctx, + req: &user.VerifyU2FRegistrationRequest{ + UserId: userID, + U2FId: "123", + PublicKeyCredential: &structpb.Struct{ + Fields: map[string]*structpb.Value{"foo": {Kind: &structpb.Value_StringValue{StringValue: "bar"}}}, + }, + TokenName: "nice name", + }, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Client.VerifyU2FRegistration(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + require.NotNil(t, got) + integration.AssertDetails(t, tt.want, got) + }) + } +} diff --git a/internal/api/grpc/user/v2beta/u2f_test.go b/internal/api/grpc/user/v2beta/u2f_test.go new file mode 100644 index 0000000000..087837ce3c --- /dev/null +++ b/internal/api/grpc/user/v2beta/u2f_test.go @@ -0,0 +1,97 @@ +package user + +import ( + "io" + "testing" + "time" + + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/structpb" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/api/grpc" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/zerrors" + object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta" + user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta" +) + +func Test_u2fRegistrationDetailsToPb(t *testing.T) { + type args struct { + details *domain.WebAuthNRegistrationDetails + err error + } + tests := []struct { + name string + args args + want *user.RegisterU2FResponse + wantErr error + }{ + { + name: "an error", + args: args{ + details: nil, + err: io.ErrClosedPipe, + }, + wantErr: io.ErrClosedPipe, + }, + { + name: "unmarshall error", + args: args{ + details: &domain.WebAuthNRegistrationDetails{ + ObjectDetails: &domain.ObjectDetails{ + Sequence: 22, + EventDate: time.Unix(3000, 22), + ResourceOwner: "me", + }, + ID: "123", + PublicKeyCredentialCreationOptions: []byte(`\\`), + }, + err: nil, + }, + wantErr: zerrors.ThrowInternal(nil, "USERv2-Dohr6", "Errors.Internal"), + }, + { + name: "ok", + args: args{ + details: &domain.WebAuthNRegistrationDetails{ + ObjectDetails: &domain.ObjectDetails{ + Sequence: 22, + EventDate: time.Unix(3000, 22), + ResourceOwner: "me", + }, + ID: "123", + PublicKeyCredentialCreationOptions: []byte(`{"foo": "bar"}`), + }, + err: nil, + }, + want: &user.RegisterU2FResponse{ + Details: &object.Details{ + Sequence: 22, + ChangeDate: ×tamppb.Timestamp{ + Seconds: 3000, + Nanos: 22, + }, + ResourceOwner: "me", + }, + U2FId: "123", + PublicKeyCredentialCreationOptions: &structpb.Struct{ + Fields: map[string]*structpb.Value{"foo": {Kind: &structpb.Value_StringValue{StringValue: "bar"}}}, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := u2fRegistrationDetailsToPb(tt.args.details, tt.args.err) + require.ErrorIs(t, err, tt.wantErr) + if !proto.Equal(tt.want, got) { + t.Errorf("Not equal:\nExpected\n%s\nActual:%s", tt.want, got) + } + if tt.want != nil { + grpc.AllFieldsSet(t, got.ProtoReflect()) + } + }) + } +} diff --git a/internal/api/grpc/user/v2beta/user.go b/internal/api/grpc/user/v2beta/user.go new file mode 100644 index 0000000000..8e3151a0b0 --- /dev/null +++ b/internal/api/grpc/user/v2beta/user.go @@ -0,0 +1,633 @@ +package user + +import ( + "context" + "errors" + "io" + + "golang.org/x/text/language" + "google.golang.org/protobuf/types/known/structpb" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/api/authz" + object "github.com/zitadel/zitadel/internal/api/grpc/object/v2beta" + "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/crypto" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/idp" + "github.com/zitadel/zitadel/internal/idp/providers/ldap" + "github.com/zitadel/zitadel/internal/query" + "github.com/zitadel/zitadel/internal/zerrors" + object_pb "github.com/zitadel/zitadel/pkg/grpc/object/v2beta" + user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta" +) + +func (s *Server) AddHumanUser(ctx context.Context, req *user.AddHumanUserRequest) (_ *user.AddHumanUserResponse, err error) { + human, err := AddUserRequestToAddHuman(req) + if err != nil { + return nil, err + } + orgID := authz.GetCtxData(ctx).OrgID + if err = s.command.AddUserHuman(ctx, orgID, human, false, s.userCodeAlg); err != nil { + return nil, err + } + return &user.AddHumanUserResponse{ + UserId: human.ID, + Details: object.DomainToDetailsPb(human.Details), + EmailCode: human.EmailCode, + PhoneCode: human.PhoneCode, + }, nil +} + +func AddUserRequestToAddHuman(req *user.AddHumanUserRequest) (*command.AddHuman, error) { + username := req.GetUsername() + if username == "" { + username = req.GetEmail().GetEmail() + } + var urlTemplate string + if req.GetEmail().GetSendCode() != nil { + urlTemplate = req.GetEmail().GetSendCode().GetUrlTemplate() + // test the template execution so the async notification will not fail because of it and the user won't realize + if err := domain.RenderConfirmURLTemplate(io.Discard, urlTemplate, req.GetUserId(), "code", "orgID"); err != nil { + return nil, err + } + } + passwordChangeRequired := req.GetPassword().GetChangeRequired() || req.GetHashedPassword().GetChangeRequired() + metadata := make([]*command.AddMetadataEntry, len(req.Metadata)) + for i, metadataEntry := range req.Metadata { + metadata[i] = &command.AddMetadataEntry{ + Key: metadataEntry.GetKey(), + Value: metadataEntry.GetValue(), + } + } + links := make([]*command.AddLink, len(req.GetIdpLinks())) + for i, link := range req.GetIdpLinks() { + links[i] = &command.AddLink{ + IDPID: link.GetIdpId(), + IDPExternalID: link.GetUserId(), + DisplayName: link.GetUserName(), + } + } + return &command.AddHuman{ + ID: req.GetUserId(), + Username: username, + FirstName: req.GetProfile().GetGivenName(), + LastName: req.GetProfile().GetFamilyName(), + NickName: req.GetProfile().GetNickName(), + DisplayName: req.GetProfile().GetDisplayName(), + Email: command.Email{ + Address: domain.EmailAddress(req.GetEmail().GetEmail()), + Verified: req.GetEmail().GetIsVerified(), + ReturnCode: req.GetEmail().GetReturnCode() != nil, + URLTemplate: urlTemplate, + }, + Phone: command.Phone{ + Number: domain.PhoneNumber(req.GetPhone().GetPhone()), + Verified: req.GetPhone().GetIsVerified(), + ReturnCode: req.GetPhone().GetReturnCode() != nil, + }, + PreferredLanguage: language.Make(req.GetProfile().GetPreferredLanguage()), + Gender: genderToDomain(req.GetProfile().GetGender()), + Password: req.GetPassword().GetPassword(), + EncodedPasswordHash: req.GetHashedPassword().GetHash(), + PasswordChangeRequired: passwordChangeRequired, + Passwordless: false, + Register: false, + Metadata: metadata, + Links: links, + TOTPSecret: req.GetTotpSecret(), + }, nil +} + +func genderToDomain(gender user.Gender) domain.Gender { + switch gender { + case user.Gender_GENDER_UNSPECIFIED: + return domain.GenderUnspecified + case user.Gender_GENDER_FEMALE: + return domain.GenderFemale + case user.Gender_GENDER_MALE: + return domain.GenderMale + case user.Gender_GENDER_DIVERSE: + return domain.GenderDiverse + default: + return domain.GenderUnspecified + } +} + +func (s *Server) UpdateHumanUser(ctx context.Context, req *user.UpdateHumanUserRequest) (_ *user.UpdateHumanUserResponse, err error) { + human, err := UpdateUserRequestToChangeHuman(req) + if err != nil { + return nil, err + } + err = s.command.ChangeUserHuman(ctx, human, s.userCodeAlg) + if err != nil { + return nil, err + } + return &user.UpdateHumanUserResponse{ + Details: object.DomainToDetailsPb(human.Details), + EmailCode: human.EmailCode, + PhoneCode: human.PhoneCode, + }, nil +} + +func (s *Server) LockUser(ctx context.Context, req *user.LockUserRequest) (_ *user.LockUserResponse, err error) { + details, err := s.command.LockUserV2(ctx, req.UserId) + if err != nil { + return nil, err + } + return &user.LockUserResponse{ + Details: object.DomainToDetailsPb(details), + }, nil +} + +func (s *Server) UnlockUser(ctx context.Context, req *user.UnlockUserRequest) (_ *user.UnlockUserResponse, err error) { + details, err := s.command.UnlockUserV2(ctx, req.UserId) + if err != nil { + return nil, err + } + return &user.UnlockUserResponse{ + Details: object.DomainToDetailsPb(details), + }, nil +} + +func (s *Server) DeactivateUser(ctx context.Context, req *user.DeactivateUserRequest) (_ *user.DeactivateUserResponse, err error) { + details, err := s.command.DeactivateUserV2(ctx, req.UserId) + if err != nil { + return nil, err + } + return &user.DeactivateUserResponse{ + Details: object.DomainToDetailsPb(details), + }, nil +} + +func (s *Server) ReactivateUser(ctx context.Context, req *user.ReactivateUserRequest) (_ *user.ReactivateUserResponse, err error) { + details, err := s.command.ReactivateUserV2(ctx, req.UserId) + if err != nil { + return nil, err + } + return &user.ReactivateUserResponse{ + Details: object.DomainToDetailsPb(details), + }, nil +} + +func ifNotNilPtr[v, p any](value *v, conv func(v) p) *p { + var pNil *p + if value == nil { + return pNil + } + pVal := conv(*value) + 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) AddIDPLink(ctx context.Context, req *user.AddIDPLinkRequest) (_ *user.AddIDPLinkResponse, err error) { + details, err := s.command.AddUserIDPLink(ctx, req.UserId, "", &command.AddLink{ + IDPID: req.GetIdpLink().GetIdpId(), + DisplayName: req.GetIdpLink().GetUserName(), + IDPExternalID: req.GetIdpLink().GetUserId(), + }) + if err != nil { + return nil, err + } + return &user.AddIDPLinkResponse{ + Details: object.DomainToDetailsPb(details), + }, nil +} + +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 { + return nil, err + } + details, err := s.command.RemoveUserV2(ctx, req.UserId, memberships, grants...) + if err != nil { + return nil, err + } + return &user.DeleteUserResponse{ + Details: object.DomainToDetailsPb(details), + }, nil +} + +func (s *Server) removeUserDependencies(ctx context.Context, userID string) ([]*command.CascadingMembership, []string, error) { + userGrantUserQuery, err := query.NewUserGrantUserIDSearchQuery(userID) + if err != nil { + return nil, nil, err + } + grants, err := s.query.UserGrants(ctx, &query.UserGrantsQueries{ + Queries: []query.SearchQuery{userGrantUserQuery}, + }, true) + if err != nil { + return nil, nil, err + } + membershipsUserQuery, err := query.NewMembershipUserIDQuery(userID) + if err != nil { + return nil, nil, err + } + memberships, err := s.query.Memberships(ctx, &query.MembershipSearchQuery{ + Queries: []query.SearchQuery{membershipsUserQuery}, + }, false) + if err != nil { + return nil, nil, err + } + return cascadingMemberships(memberships.Memberships), userGrantsToIDs(grants.UserGrants), nil +} + +func cascadingMemberships(memberships []*query.Membership) []*command.CascadingMembership { + cascades := make([]*command.CascadingMembership, len(memberships)) + for i, membership := range memberships { + cascades[i] = &command.CascadingMembership{ + UserID: membership.UserID, + ResourceOwner: membership.ResourceOwner, + IAM: cascadingIAMMembership(membership.IAM), + Org: cascadingOrgMembership(membership.Org), + Project: cascadingProjectMembership(membership.Project), + ProjectGrant: cascadingProjectGrantMembership(membership.ProjectGrant), + } + } + return cascades +} + +func cascadingIAMMembership(membership *query.IAMMembership) *command.CascadingIAMMembership { + if membership == nil { + return nil + } + 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 + } + return &command.CascadingProjectGrantMembership{ProjectID: membership.ProjectID, GrantID: membership.GrantID} +} + +func userGrantsToIDs(userGrants []*query.UserGrant) []string { + converted := make([]string, len(userGrants)) + for i, grant := range userGrants { + converted[i] = grant.ID + } + return converted +} + +func (s *Server) StartIdentityProviderIntent(ctx context.Context, req *user.StartIdentityProviderIntentRequest) (_ *user.StartIdentityProviderIntentResponse, err error) { + switch t := req.GetContent().(type) { + case *user.StartIdentityProviderIntentRequest_Urls: + return s.startIDPIntent(ctx, req.GetIdpId(), t.Urls) + case *user.StartIdentityProviderIntentRequest_Ldap: + return s.startLDAPIntent(ctx, req.GetIdpId(), t.Ldap) + default: + return nil, zerrors.ThrowUnimplementedf(nil, "USERv2-S2g21", "type oneOf %T in method StartIdentityProviderIntent not implemented", t) + } +} + +func (s *Server) startIDPIntent(ctx context.Context, idpID string, urls *user.RedirectURLs) (*user.StartIdentityProviderIntentResponse, error) { + intentWriteModel, details, err := s.command.CreateIntent(ctx, idpID, urls.GetSuccessUrl(), urls.GetFailureUrl(), authz.GetInstance(ctx).InstanceID()) + if err != nil { + return nil, err + } + content, redirect, err := s.command.AuthFromProvider(ctx, idpID, intentWriteModel.AggregateID, s.idpCallback(ctx), s.samlRootURL(ctx, idpID)) + if err != nil { + return nil, err + } + if redirect { + return &user.StartIdentityProviderIntentResponse{ + Details: object.DomainToDetailsPb(details), + NextStep: &user.StartIdentityProviderIntentResponse_AuthUrl{AuthUrl: content}, + }, nil + } else { + return &user.StartIdentityProviderIntentResponse{ + Details: object.DomainToDetailsPb(details), + NextStep: &user.StartIdentityProviderIntentResponse_PostForm{ + PostForm: []byte(content), + }, + }, nil + } +} + +func (s *Server) startLDAPIntent(ctx context.Context, idpID string, ldapCredentials *user.LDAPCredentials) (*user.StartIdentityProviderIntentResponse, error) { + intentWriteModel, details, err := s.command.CreateIntent(ctx, idpID, "", "", authz.GetInstance(ctx).InstanceID()) + if err != nil { + return nil, err + } + externalUser, userID, attributes, 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) + if err != nil { + return nil, err + } + return &user.StartIdentityProviderIntentResponse{ + Details: object.DomainToDetailsPb(details), + NextStep: &user.StartIdentityProviderIntentResponse_IdpIntent{ + IdpIntent: &user.IDPIntent{ + IdpIntentId: intentWriteModel.AggregateID, + IdpIntentToken: token, + UserId: userID, + }, + }, + }, nil +} + +func (s *Server) checkLinkedExternalUser(ctx context.Context, idpID, externalUserID string) (string, error) { + idQuery, err := query.NewIDPUserLinkIDPIDSearchQuery(idpID) + if err != nil { + return "", err + } + externalIDQuery, err := query.NewIDPUserLinksExternalIDSearchQuery(externalUserID) + if err != nil { + return "", err + } + queries := []query.SearchQuery{ + idQuery, externalIDQuery, + } + links, err := s.query.IDPUserLinks(ctx, &query.IDPUserLinksSearchQuery{Queries: queries}, false) + if err != nil { + return "", err + } + if len(links.Links) == 1 { + return links.Links[0].UserID, nil + } + return "", nil +} + +func (s *Server) ldapLogin(ctx context.Context, idpID, username, password string) (idp.User, string, map[string][]string, error) { + provider, err := s.command.GetProvider(ctx, idpID, "", "") + if err != nil { + return nil, "", nil, err + } + ldapProvider, ok := provider.(*ldap.Provider) + if !ok { + return nil, "", nil, zerrors.ThrowInvalidArgument(nil, "IDP-9a02j2n2bh", "Errors.ExternalIDP.IDPTypeNotImplemented") + } + session := ldapProvider.GetSession(username, password) + externalUser, err := session.FetchUser(ctx) + if errors.Is(err, ldap.ErrFailedLogin) || errors.Is(err, ldap.ErrNoSingleUser) { + return nil, "", nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-nzun2i", "Errors.User.ExternalIDP.LoginFailed") + } + if err != nil { + return nil, "", nil, err + } + userID, err := s.checkLinkedExternalUser(ctx, idpID, externalUser.GetID()) + 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 +} + +func (s *Server) RetrieveIdentityProviderIntent(ctx context.Context, req *user.RetrieveIdentityProviderIntentRequest) (_ *user.RetrieveIdentityProviderIntentResponse, err error) { + intent, err := s.command.GetIntentWriteModel(ctx, req.GetIdpIntentId(), "") + if err != nil { + return nil, err + } + if err := s.checkIntentToken(req.GetIdpIntentToken(), intent.AggregateID); err != nil { + return nil, err + } + if intent.State != domain.IDPIntentStateSucceeded { + return nil, zerrors.ThrowPreconditionFailed(nil, "IDP-nme4gszsvx", "Errors.Intent.NotSucceeded") + } + return idpIntentToIDPIntentPb(intent, s.idpAlg) +} + +func idpIntentToIDPIntentPb(intent *command.IDPIntentWriteModel, alg crypto.EncryptionAlgorithm) (_ *user.RetrieveIdentityProviderIntentResponse, err error) { + rawInformation := new(structpb.Struct) + err = rawInformation.UnmarshalJSON(intent.IDPUser) + if err != nil { + return nil, err + } + information := &user.RetrieveIdentityProviderIntentResponse{ + Details: intentToDetailsPb(intent), + IdpInformation: &user.IDPInformation{ + IdpId: intent.IDPID, + UserId: intent.IDPUserID, + UserName: intent.IDPUserName, + RawInformation: rawInformation, + }, + UserId: intent.UserID, + } + if intent.IDPIDToken != "" || intent.IDPAccessToken != nil { + information.IdpInformation.Access, err = idpOAuthTokensToPb(intent.IDPIDToken, intent.IDPAccessToken, alg) + if err != nil { + return nil, err + } + } + + if intent.IDPEntryAttributes != nil { + access, err := IDPEntryAttributesToPb(intent.IDPEntryAttributes) + if err != nil { + return nil, err + } + information.IdpInformation.Access = access + } + + if intent.Assertion != nil { + assertion, err := crypto.Decrypt(intent.Assertion, alg) + if err != nil { + return nil, err + } + information.IdpInformation.Access = IDPSAMLResponseToPb(assertion) + } + + return information, nil +} + +func idpOAuthTokensToPb(idpIDToken string, idpAccessToken *crypto.CryptoValue, alg crypto.EncryptionAlgorithm) (_ *user.IDPInformation_Oauth, err error) { + var idToken *string + if idpIDToken != "" { + idToken = &idpIDToken + } + var accessToken string + if idpAccessToken != nil { + accessToken, err = crypto.DecryptString(idpAccessToken, alg) + if err != nil { + return nil, err + } + } + return &user.IDPInformation_Oauth{ + Oauth: &user.IDPOAuthAccessInformation{ + AccessToken: accessToken, + IdToken: idToken, + }, + }, nil +} + +func intentToDetailsPb(intent *command.IDPIntentWriteModel) *object_pb.Details { + return &object_pb.Details{ + Sequence: intent.ProcessedSequence, + ChangeDate: timestamppb.New(intent.ChangeDate), + ResourceOwner: intent.ResourceOwner, + } +} + +func IDPEntryAttributesToPb(entryAttributes map[string][]string) (*user.IDPInformation_Ldap, error) { + values := make(map[string]interface{}, 0) + for k, v := range entryAttributes { + intValues := make([]interface{}, len(v)) + for i, value := range v { + intValues[i] = value + } + values[k] = intValues + } + attributes, err := structpb.NewStruct(values) + if err != nil { + return nil, err + } + return &user.IDPInformation_Ldap{ + Ldap: &user.IDPLDAPAccessInformation{ + Attributes: attributes, + }, + }, nil +} + +func IDPSAMLResponseToPb(assertion []byte) *user.IDPInformation_Saml { + return &user.IDPInformation_Saml{ + Saml: &user.IDPSAMLAccessInformation{ + Assertion: assertion, + }, + } +} + +func (s *Server) checkIntentToken(token string, intentID string) error { + return crypto.CheckToken(s.idpAlg, token, intentID) +} + +func (s *Server) ListAuthenticationMethodTypes(ctx context.Context, req *user.ListAuthenticationMethodTypesRequest) (*user.ListAuthenticationMethodTypesResponse, error) { + authMethods, err := s.query.ListUserAuthMethodTypes(ctx, req.GetUserId(), true) + if err != nil { + return nil, err + } + return &user.ListAuthenticationMethodTypesResponse{ + Details: object.ToListDetails(authMethods.SearchResponse), + AuthMethodTypes: authMethodTypesToPb(authMethods.AuthMethodTypes), + }, nil +} + +func authMethodTypesToPb(methodTypes []domain.UserAuthMethodType) []user.AuthenticationMethodType { + methods := make([]user.AuthenticationMethodType, len(methodTypes)) + for i, method := range methodTypes { + methods[i] = authMethodTypeToPb(method) + } + return methods +} + +func authMethodTypeToPb(methodType domain.UserAuthMethodType) user.AuthenticationMethodType { + switch methodType { + case domain.UserAuthMethodTypeTOTP: + return user.AuthenticationMethodType_AUTHENTICATION_METHOD_TYPE_TOTP + case domain.UserAuthMethodTypeU2F: + return user.AuthenticationMethodType_AUTHENTICATION_METHOD_TYPE_U2F + case domain.UserAuthMethodTypePasswordless: + return user.AuthenticationMethodType_AUTHENTICATION_METHOD_TYPE_PASSKEY + case domain.UserAuthMethodTypePassword: + return user.AuthenticationMethodType_AUTHENTICATION_METHOD_TYPE_PASSWORD + case domain.UserAuthMethodTypeIDP: + return user.AuthenticationMethodType_AUTHENTICATION_METHOD_TYPE_IDP + case domain.UserAuthMethodTypeOTPSMS: + return user.AuthenticationMethodType_AUTHENTICATION_METHOD_TYPE_OTP_SMS + case domain.UserAuthMethodTypeOTPEmail: + return user.AuthenticationMethodType_AUTHENTICATION_METHOD_TYPE_OTP_EMAIL + case domain.UserAuthMethodTypeUnspecified, domain.UserAuthMethodTypeOTP, domain.UserAuthMethodTypePrivateKey: + // Handle all remaining cases so the linter succeeds + return user.AuthenticationMethodType_AUTHENTICATION_METHOD_TYPE_UNSPECIFIED + default: + return user.AuthenticationMethodType_AUTHENTICATION_METHOD_TYPE_UNSPECIFIED + } +} diff --git a/internal/api/grpc/user/v2beta/user_integration_test.go b/internal/api/grpc/user/v2beta/user_integration_test.go new file mode 100644 index 0000000000..d808e46c5f --- /dev/null +++ b/internal/api/grpc/user/v2beta/user_integration_test.go @@ -0,0 +1,2521 @@ +//go:build integration + +package user_test + +import ( + "context" + "fmt" + "net/url" + "os" + "testing" + "time" + + "github.com/muhlemmer/gu" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/structpb" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/api/grpc" + "github.com/zitadel/zitadel/internal/integration" + "github.com/zitadel/zitadel/pkg/grpc/idp" + mgmt "github.com/zitadel/zitadel/pkg/grpc/management" + object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta" + user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta" +) + +var ( + CTX context.Context + IamCTX context.Context + UserCTX context.Context + SystemCTX context.Context + ErrCTX context.Context + Tester *integration.Tester + Client user.UserServiceClient +) + +func TestMain(m *testing.M) { + os.Exit(func() int { + ctx, errCtx, cancel := integration.Contexts(time.Hour) + defer cancel() + + Tester = integration.NewTester(ctx) + defer Tester.Done() + + UserCTX = Tester.WithAuthorization(ctx, integration.Login) + IamCTX = Tester.WithAuthorization(ctx, integration.IAMOwner) + SystemCTX = Tester.WithAuthorization(ctx, integration.SystemUser) + CTX, ErrCTX = Tester.WithAuthorization(ctx, integration.OrgOwner), errCtx + Client = Tester.Client.UserV2beta + return m.Run() + }()) +} + +func TestServer_AddHumanUser(t *testing.T) { + idpID := Tester.AddGenericOAuthProvider(t, CTX) + type args struct { + ctx context.Context + req *user.AddHumanUserRequest + } + tests := []struct { + name string + args args + want *user.AddHumanUserResponse + wantErr bool + }{ + { + name: "default verification", + args: args{ + CTX, + &user.AddHumanUserRequest{ + Organization: &object.Organization{ + Org: &object.Organization_OrgId{ + OrgId: Tester.Organisation.ID, + }, + }, + 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{}, + Phone: &user.SetHumanPhone{}, + Metadata: []*user.SetMetadataEntry{ + { + Key: "somekey", + Value: []byte("somevalue"), + }, + }, + PasswordType: &user.AddHumanUserRequest_Password{ + Password: &user.Password{ + Password: "DifficultPW666!", + ChangeRequired: true, + }, + }, + }, + }, + want: &user.AddHumanUserResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Organisation.ID, + }, + }, + }, + { + name: "return email verification code", + args: args{ + CTX, + &user.AddHumanUserRequest{ + Organization: &object.Organization{ + Org: &object.Organization_OrgId{ + OrgId: Tester.Organisation.ID, + }, + }, + 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{ + Verification: &user.SetHumanEmail_ReturnCode{ + ReturnCode: &user.ReturnEmailVerificationCode{}, + }, + }, + Metadata: []*user.SetMetadataEntry{ + { + Key: "somekey", + Value: []byte("somevalue"), + }, + }, + PasswordType: &user.AddHumanUserRequest_Password{ + Password: &user.Password{ + Password: "DifficultPW666!", + ChangeRequired: true, + }, + }, + }, + }, + want: &user.AddHumanUserResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Organisation.ID, + }, + EmailCode: gu.Ptr("something"), + }, + }, + { + name: "custom template", + args: args{ + CTX, + &user.AddHumanUserRequest{ + Organization: &object.Organization{ + Org: &object.Organization_OrgId{ + OrgId: Tester.Organisation.ID, + }, + }, + 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{ + Verification: &user.SetHumanEmail_SendCode{ + SendCode: &user.SendEmailVerificationCode{ + UrlTemplate: gu.Ptr("https://example.com/email/verify?userID={{.UserID}}&code={{.Code}}&orgID={{.OrgID}}"), + }, + }, + }, + Metadata: []*user.SetMetadataEntry{ + { + Key: "somekey", + Value: []byte("somevalue"), + }, + }, + PasswordType: &user.AddHumanUserRequest_Password{ + Password: &user.Password{ + Password: "DifficultPW666!", + ChangeRequired: true, + }, + }, + }, + }, + want: &user.AddHumanUserResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Organisation.ID, + }, + }, + }, + { + name: "return phone verification code", + args: args{ + CTX, + &user.AddHumanUserRequest{ + Organization: &object.Organization{ + Org: &object.Organization_OrgId{ + OrgId: Tester.Organisation.ID, + }, + }, + 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{}, + Phone: &user.SetHumanPhone{ + Phone: "+41791234567", + Verification: &user.SetHumanPhone_ReturnCode{ + ReturnCode: &user.ReturnPhoneVerificationCode{}, + }, + }, + Metadata: []*user.SetMetadataEntry{ + { + Key: "somekey", + Value: []byte("somevalue"), + }, + }, + PasswordType: &user.AddHumanUserRequest_Password{ + Password: &user.Password{ + Password: "DifficultPW666!", + ChangeRequired: true, + }, + }, + }, + }, + want: &user.AddHumanUserResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Organisation.ID, + }, + PhoneCode: gu.Ptr("something"), + }, + }, + { + name: "custom template error", + args: args{ + CTX, + &user.AddHumanUserRequest{ + Organization: &object.Organization{ + Org: &object.Organization_OrgId{ + OrgId: Tester.Organisation.ID, + }, + }, + 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{ + Verification: &user.SetHumanEmail_SendCode{ + SendCode: &user.SendEmailVerificationCode{ + UrlTemplate: gu.Ptr("{{"), + }, + }, + }, + Metadata: []*user.SetMetadataEntry{ + { + Key: "somekey", + Value: []byte("somevalue"), + }, + }, + PasswordType: &user.AddHumanUserRequest_Password{ + Password: &user.Password{ + Password: "DifficultPW666!", + ChangeRequired: true, + }, + }, + }, + }, + wantErr: true, + }, + { + name: "missing REQUIRED profile", + args: args{ + CTX, + &user.AddHumanUserRequest{ + Organization: &object.Organization{ + Org: &object.Organization_OrgId{ + OrgId: Tester.Organisation.ID, + }, + }, + Email: &user.SetHumanEmail{ + Verification: &user.SetHumanEmail_ReturnCode{ + ReturnCode: &user.ReturnEmailVerificationCode{}, + }, + }, + Metadata: []*user.SetMetadataEntry{ + { + Key: "somekey", + Value: []byte("somevalue"), + }, + }, + PasswordType: &user.AddHumanUserRequest_Password{ + Password: &user.Password{ + Password: "DifficultPW666!", + ChangeRequired: true, + }, + }, + }, + }, + wantErr: true, + }, + { + name: "missing REQUIRED email", + args: args{ + CTX, + &user.AddHumanUserRequest{ + Organization: &object.Organization{ + Org: &object.Organization_OrgId{ + OrgId: Tester.Organisation.ID, + }, + }, + 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(), + }, + Metadata: []*user.SetMetadataEntry{ + { + Key: "somekey", + Value: []byte("somevalue"), + }, + }, + PasswordType: &user.AddHumanUserRequest_Password{ + Password: &user.Password{ + Password: "DifficultPW666!", + ChangeRequired: true, + }, + }, + }, + }, + wantErr: true, + }, + { + name: "missing idp", + args: args{ + CTX, + &user.AddHumanUserRequest{ + Organization: &object.Organization{ + Org: &object.Organization_OrgId{ + OrgId: Tester.Organisation.ID, + }, + }, + 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: "livio@zitadel.com", + Verification: &user.SetHumanEmail_IsVerified{ + IsVerified: true, + }, + }, + Metadata: []*user.SetMetadataEntry{ + { + Key: "somekey", + Value: []byte("somevalue"), + }, + }, + PasswordType: &user.AddHumanUserRequest_Password{ + Password: &user.Password{ + Password: "DifficultPW666!", + ChangeRequired: false, + }, + }, + IdpLinks: []*user.IDPLink{ + { + IdpId: "idpID", + UserId: "userID", + UserName: "username", + }, + }, + }, + }, + wantErr: true, + }, + { + name: "with idp", + args: args{ + CTX, + &user.AddHumanUserRequest{ + Organization: &object.Organization{ + Org: &object.Organization_OrgId{ + OrgId: Tester.Organisation.ID, + }, + }, + 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: "livio@zitadel.com", + Verification: &user.SetHumanEmail_IsVerified{ + IsVerified: true, + }, + }, + Metadata: []*user.SetMetadataEntry{ + { + Key: "somekey", + Value: []byte("somevalue"), + }, + }, + PasswordType: &user.AddHumanUserRequest_Password{ + Password: &user.Password{ + Password: "DifficultPW666!", + ChangeRequired: false, + }, + }, + IdpLinks: []*user.IDPLink{ + { + IdpId: idpID, + UserId: "userID", + UserName: "username", + }, + }, + }, + }, + want: &user.AddHumanUserResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Organisation.ID, + }, + }, + }, + { + name: "with totp", + args: args{ + CTX, + &user.AddHumanUserRequest{ + Organization: &object.Organization{ + Org: &object.Organization_OrgId{ + OrgId: Tester.Organisation.ID, + }, + }, + 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: "livio@zitadel.com", + Verification: &user.SetHumanEmail_IsVerified{ + IsVerified: true, + }, + }, + Metadata: []*user.SetMetadataEntry{ + { + Key: "somekey", + Value: []byte("somevalue"), + }, + }, + PasswordType: &user.AddHumanUserRequest_Password{ + Password: &user.Password{ + Password: "DifficultPW666!", + ChangeRequired: false, + }, + }, + TotpSecret: gu.Ptr("secret"), + }, + }, + want: &user.AddHumanUserResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Organisation.ID, + }, + }, + }, + { + name: "password not complexity conform", + args: args{ + CTX, + &user.AddHumanUserRequest{ + Organization: &object.Organization{ + Org: &object.Organization_OrgId{ + OrgId: Tester.Organisation.ID, + }, + }, + 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{}, + Metadata: []*user.SetMetadataEntry{ + { + Key: "somekey", + Value: []byte("somevalue"), + }, + }, + PasswordType: &user.AddHumanUserRequest_Password{ + Password: &user.Password{ + Password: "insufficient", + }, + }, + }, + }, + wantErr: true, + }, + { + name: "hashed password", + args: args{ + CTX, + &user.AddHumanUserRequest{ + Organization: &object.Organization{ + Org: &object.Organization_OrgId{ + OrgId: Tester.Organisation.ID, + }, + }, + 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{}, + Metadata: []*user.SetMetadataEntry{ + { + Key: "somekey", + Value: []byte("somevalue"), + }, + }, + PasswordType: &user.AddHumanUserRequest_HashedPassword{ + HashedPassword: &user.HashedPassword{ + Hash: "$2y$12$hXUrnqdq1RIIYZ2HPytIIe5lXdIvbhqrTvdPsSF7o.jFh817Z6lwm", + }, + }, + }, + }, + want: &user.AddHumanUserResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Organisation.ID, + }, + }, + }, + { + name: "unsupported hashed password", + args: args{ + CTX, + &user.AddHumanUserRequest{ + Organization: &object.Organization{ + Org: &object.Organization_OrgId{ + OrgId: Tester.Organisation.ID, + }, + }, + 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{}, + Metadata: []*user.SetMetadataEntry{ + { + Key: "somekey", + Value: []byte("somevalue"), + }, + }, + PasswordType: &user.AddHumanUserRequest_HashedPassword{ + HashedPassword: &user.HashedPassword{ + Hash: "$scrypt$ln=16,r=8,p=1$cmFuZG9tc2FsdGlzaGFyZA$Rh+NnJNo1I6nRwaNqbDm6kmADswD1+7FTKZ7Ln9D8nQ", + }, + }, + }, + }, + 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.GetEmail(); email != nil { + email.Email = fmt.Sprintf("%s@me.now", userID) + } + + if tt.want != nil { + tt.want.UserId = userID + } + + got, err := Client.AddHumanUser(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + + assert.Equal(t, tt.want.GetUserId(), got.GetUserId()) + if tt.want.GetEmailCode() != "" { + assert.NotEmpty(t, got.GetEmailCode()) + } + if tt.want.GetPhoneCode() != "" { + assert.NotEmpty(t, got.GetPhoneCode()) + } + integration.AssertDetails(t, tt.want, got) + }) + } +} + +func TestServer_AddHumanUser_Permission(t *testing.T) { + newOrgOwnerEmail := fmt.Sprintf("%d@permission.com", time.Now().UnixNano()) + newOrg := Tester.CreateOrganization(IamCTX, fmt.Sprintf("AddHuman%d", time.Now().UnixNano()), newOrgOwnerEmail) + type args struct { + ctx context.Context + req *user.AddHumanUserRequest + } + tests := []struct { + name string + args args + want *user.AddHumanUserResponse + wantErr bool + }{ + { + name: "System, ok", + args: args{ + SystemCTX, + &user.AddHumanUserRequest{ + Organization: &object.Organization{ + Org: &object.Organization_OrgId{ + OrgId: newOrg.GetOrganizationId(), + }, + }, + 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{}, + Phone: &user.SetHumanPhone{}, + Metadata: []*user.SetMetadataEntry{ + { + Key: "somekey", + Value: []byte("somevalue"), + }, + }, + PasswordType: &user.AddHumanUserRequest_Password{ + Password: &user.Password{ + Password: "DifficultPW666!", + ChangeRequired: true, + }, + }, + }, + }, + want: &user.AddHumanUserResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: newOrg.GetOrganizationId(), + }, + }, + }, + { + name: "Instance, ok", + args: args{ + IamCTX, + &user.AddHumanUserRequest{ + Organization: &object.Organization{ + Org: &object.Organization_OrgId{ + OrgId: newOrg.GetOrganizationId(), + }, + }, + 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{}, + Phone: &user.SetHumanPhone{}, + Metadata: []*user.SetMetadataEntry{ + { + Key: "somekey", + Value: []byte("somevalue"), + }, + }, + PasswordType: &user.AddHumanUserRequest_Password{ + Password: &user.Password{ + Password: "DifficultPW666!", + ChangeRequired: true, + }, + }, + }, + }, + want: &user.AddHumanUserResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: newOrg.GetOrganizationId(), + }, + }, + }, + { + name: "Org, error", + args: args{ + CTX, + &user.AddHumanUserRequest{ + Organization: &object.Organization{ + Org: &object.Organization_OrgId{ + OrgId: newOrg.GetOrganizationId(), + }, + }, + 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{}, + Phone: &user.SetHumanPhone{}, + Metadata: []*user.SetMetadataEntry{ + { + Key: "somekey", + Value: []byte("somevalue"), + }, + }, + PasswordType: &user.AddHumanUserRequest_Password{ + Password: &user.Password{ + Password: "DifficultPW666!", + ChangeRequired: true, + }, + }, + }, + }, + wantErr: true, + }, + { + name: "User, error", + args: args{ + UserCTX, + &user.AddHumanUserRequest{ + Organization: &object.Organization{ + Org: &object.Organization_OrgId{ + OrgId: newOrg.GetOrganizationId(), + }, + }, + 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{}, + Phone: &user.SetHumanPhone{}, + Metadata: []*user.SetMetadataEntry{ + { + Key: "somekey", + Value: []byte("somevalue"), + }, + }, + PasswordType: &user.AddHumanUserRequest_Password{ + Password: &user.Password{ + Password: "DifficultPW666!", + ChangeRequired: true, + }, + }, + }, + }, + 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.GetEmail(); email != nil { + email.Email = fmt.Sprintf("%s@me.now", userID) + } + + if tt.want != nil { + tt.want.UserId = userID + } + + got, err := Client.AddHumanUser(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + + assert.Equal(t, tt.want.GetUserId(), got.GetUserId()) + integration.AssertDetails(t, tt.want, got) + }) + } +} + +func TestServer_UpdateHumanUser(t *testing.T) { + type args struct { + ctx context.Context + req *user.UpdateHumanUserRequest + } + tests := []struct { + name string + prepare func(request *user.UpdateHumanUserRequest) error + args args + want *user.UpdateHumanUserResponse + wantErr bool + }{ + { + name: "not exisiting", + prepare: func(request *user.UpdateHumanUserRequest) error { + request.UserId = "notexisiting" + return nil + }, + args: args{ + CTX, + &user.UpdateHumanUserRequest{ + Username: gu.Ptr("changed"), + }, + }, + wantErr: true, + }, + { + name: "change username, ok", + prepare: func(request *user.UpdateHumanUserRequest) error { + userID := Tester.CreateHumanUser(CTX).GetUserId() + request.UserId = userID + return nil + }, + args: args{ + CTX, + &user.UpdateHumanUserRequest{ + Username: gu.Ptr(fmt.Sprint(time.Now().UnixNano() + 1)), + }, + }, + want: &user.UpdateHumanUserResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Organisation.ID, + }, + }, + }, + { + name: "change profile, ok", + prepare: func(request *user.UpdateHumanUserRequest) error { + userID := Tester.CreateHumanUser(CTX).GetUserId() + request.UserId = userID + return nil + }, + args: args{ + CTX, + &user.UpdateHumanUserRequest{ + 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(), + }, + }, + }, + want: &user.UpdateHumanUserResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Organisation.ID, + }, + }, + }, + { + name: "change email, ok", + prepare: func(request *user.UpdateHumanUserRequest) error { + userID := Tester.CreateHumanUser(CTX).GetUserId() + request.UserId = userID + return nil + }, + args: args{ + CTX, + &user.UpdateHumanUserRequest{ + Email: &user.SetHumanEmail{ + Email: "changed@test.com", + Verification: &user.SetHumanEmail_IsVerified{IsVerified: true}, + }, + }, + }, + want: &user.UpdateHumanUserResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Organisation.ID, + }, + }, + }, + { + name: "change email, code, ok", + prepare: func(request *user.UpdateHumanUserRequest) error { + userID := Tester.CreateHumanUser(CTX).GetUserId() + request.UserId = userID + return nil + }, + args: args{ + CTX, + &user.UpdateHumanUserRequest{ + Email: &user.SetHumanEmail{ + Email: "changed@test.com", + Verification: &user.SetHumanEmail_ReturnCode{}, + }, + }, + }, + want: &user.UpdateHumanUserResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Organisation.ID, + }, + EmailCode: gu.Ptr("something"), + }, + }, + { + name: "change phone, ok", + prepare: func(request *user.UpdateHumanUserRequest) error { + userID := Tester.CreateHumanUser(CTX).GetUserId() + request.UserId = userID + return nil + }, + args: args{ + CTX, + &user.UpdateHumanUserRequest{ + Phone: &user.SetHumanPhone{ + Phone: "+41791234567", + Verification: &user.SetHumanPhone_IsVerified{IsVerified: true}, + }, + }, + }, + want: &user.UpdateHumanUserResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Organisation.ID, + }, + }, + }, + { + name: "change phone, code, ok", + prepare: func(request *user.UpdateHumanUserRequest) error { + userID := Tester.CreateHumanUser(CTX).GetUserId() + request.UserId = userID + return nil + }, + args: args{ + CTX, + &user.UpdateHumanUserRequest{ + Phone: &user.SetHumanPhone{ + Phone: "+41791234568", + Verification: &user.SetHumanPhone_ReturnCode{}, + }, + }, + }, + want: &user.UpdateHumanUserResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Organisation.ID, + }, + PhoneCode: gu.Ptr("something"), + }, + }, + { + name: "change password, code, ok", + prepare: func(request *user.UpdateHumanUserRequest) error { + userID := Tester.CreateHumanUser(CTX).GetUserId() + request.UserId = userID + resp, err := Client.PasswordReset(CTX, &user.PasswordResetRequest{ + UserId: userID, + Medium: &user.PasswordResetRequest_ReturnCode{ + ReturnCode: &user.ReturnPasswordResetCode{}, + }, + }) + if err != nil { + return err + } + request.Password.Verification = &user.SetPassword_VerificationCode{ + VerificationCode: resp.GetVerificationCode(), + } + return nil + }, + args: args{ + CTX, + &user.UpdateHumanUserRequest{ + Password: &user.SetPassword{ + PasswordType: &user.SetPassword_Password{ + Password: &user.Password{ + Password: "Password1!", + ChangeRequired: true, + }, + }, + }, + }, + }, + want: &user.UpdateHumanUserResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Organisation.ID, + }, + }, + }, + { + name: "change hashed password, code, ok", + prepare: func(request *user.UpdateHumanUserRequest) error { + userID := Tester.CreateHumanUser(CTX).GetUserId() + request.UserId = userID + resp, err := Client.PasswordReset(CTX, &user.PasswordResetRequest{ + UserId: userID, + Medium: &user.PasswordResetRequest_ReturnCode{ + ReturnCode: &user.ReturnPasswordResetCode{}, + }, + }) + if err != nil { + return err + } + request.Password.Verification = &user.SetPassword_VerificationCode{ + VerificationCode: resp.GetVerificationCode(), + } + return nil + }, + args: args{ + CTX, + &user.UpdateHumanUserRequest{ + Password: &user.SetPassword{ + PasswordType: &user.SetPassword_HashedPassword{ + HashedPassword: &user.HashedPassword{ + Hash: "$2y$12$hXUrnqdq1RIIYZ2HPytIIe5lXdIvbhqrTvdPsSF7o.jFh817Z6lwm", + }, + }, + }, + }, + }, + want: &user.UpdateHumanUserResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Organisation.ID, + }, + }, + }, + { + name: "change hashed password, code, not supported", + prepare: func(request *user.UpdateHumanUserRequest) error { + userID := Tester.CreateHumanUser(CTX).GetUserId() + request.UserId = userID + resp, err := Client.PasswordReset(CTX, &user.PasswordResetRequest{ + UserId: userID, + Medium: &user.PasswordResetRequest_ReturnCode{ + ReturnCode: &user.ReturnPasswordResetCode{}, + }, + }) + if err != nil { + return err + } + request.Password = &user.SetPassword{ + Verification: &user.SetPassword_VerificationCode{ + VerificationCode: resp.GetVerificationCode(), + }, + } + return nil + }, + args: args{ + CTX, + &user.UpdateHumanUserRequest{ + 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: "change password, old password, ok", + prepare: func(request *user.UpdateHumanUserRequest) error { + userID := Tester.CreateHumanUser(CTX).GetUserId() + request.UserId = userID + + resp, err := Client.PasswordReset(CTX, &user.PasswordResetRequest{ + UserId: userID, + Medium: &user.PasswordResetRequest_ReturnCode{ + ReturnCode: &user.ReturnPasswordResetCode{}, + }, + }) + if err != nil { + return err + } + pw := "Password1." + _, err = Client.SetPassword(CTX, &user.SetPasswordRequest{ + UserId: userID, + NewPassword: &user.Password{ + Password: pw, + ChangeRequired: true, + }, + Verification: &user.SetPasswordRequest_VerificationCode{ + VerificationCode: resp.GetVerificationCode(), + }, + }) + if err != nil { + return err + } + request.Password.Verification = &user.SetPassword_CurrentPassword{ + CurrentPassword: pw, + } + return nil + }, + args: args{ + CTX, + &user.UpdateHumanUserRequest{ + Password: &user.SetPassword{ + PasswordType: &user.SetPassword_Password{ + Password: &user.Password{ + Password: "Password1!", + ChangeRequired: true, + }, + }, + }, + }, + }, + want: &user.UpdateHumanUserResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Organisation.ID, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.prepare(tt.args.req) + require.NoError(t, err) + + got, err := Client.UpdateHumanUser(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + if tt.want.GetEmailCode() != "" { + assert.NotEmpty(t, got.GetEmailCode()) + } + if tt.want.GetPhoneCode() != "" { + assert.NotEmpty(t, got.GetPhoneCode()) + } + integration.AssertDetails(t, tt.want, got) + }) + } +} + +func TestServer_UpdateHumanUser_Permission(t *testing.T) { + newOrgOwnerEmail := fmt.Sprintf("%d@permission.update.com", time.Now().UnixNano()) + newOrg := Tester.CreateOrganization(IamCTX, fmt.Sprintf("UpdateHuman%d", time.Now().UnixNano()), newOrgOwnerEmail) + newUserID := newOrg.CreatedAdmins[0].GetUserId() + type args struct { + ctx context.Context + req *user.UpdateHumanUserRequest + } + tests := []struct { + name string + args args + want *user.UpdateHumanUserResponse + wantErr bool + }{ + { + name: "system, ok", + args: args{ + SystemCTX, + &user.UpdateHumanUserRequest{ + UserId: newUserID, + Username: gu.Ptr(fmt.Sprint("system", time.Now().UnixNano()+1)), + }, + }, + want: &user.UpdateHumanUserResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: newOrg.GetOrganizationId(), + }, + }, + }, + { + name: "instance, ok", + args: args{ + IamCTX, + &user.UpdateHumanUserRequest{ + UserId: newUserID, + Username: gu.Ptr(fmt.Sprint("instance", time.Now().UnixNano()+1)), + }, + }, + want: &user.UpdateHumanUserResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: newOrg.GetOrganizationId(), + }, + }, + }, + { + name: "org, error", + args: args{ + CTX, + &user.UpdateHumanUserRequest{ + UserId: newUserID, + Username: gu.Ptr(fmt.Sprint("org", time.Now().UnixNano()+1)), + }, + }, + wantErr: true, + }, + { + name: "user, error", + args: args{ + UserCTX, + &user.UpdateHumanUserRequest{ + UserId: newUserID, + Username: gu.Ptr(fmt.Sprint("user", time.Now().UnixNano()+1)), + }, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + got, err := Client.UpdateHumanUser(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + integration.AssertDetails(t, tt.want, got) + }) + } +} + +func TestServer_LockUser(t *testing.T) { + type args struct { + ctx context.Context + req *user.LockUserRequest + prepare func(request *user.LockUserRequest) error + } + tests := []struct { + name string + args args + want *user.LockUserResponse + wantErr bool + }{ + { + name: "lock, not existing", + args: args{ + CTX, + &user.LockUserRequest{ + UserId: "notexisting", + }, + func(request *user.LockUserRequest) error { return nil }, + }, + wantErr: true, + }, + { + name: "lock, ok", + args: args{ + CTX, + &user.LockUserRequest{}, + func(request *user.LockUserRequest) error { + resp := Tester.CreateHumanUser(CTX) + request.UserId = resp.GetUserId() + return nil + }, + }, + want: &user.LockUserResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Organisation.ID, + }, + }, + }, + { + name: "lock machine, ok", + args: args{ + CTX, + &user.LockUserRequest{}, + func(request *user.LockUserRequest) error { + resp := Tester.CreateMachineUser(CTX) + request.UserId = resp.GetUserId() + return nil + }, + }, + want: &user.LockUserResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Organisation.ID, + }, + }, + }, + { + name: "lock, already locked", + args: args{ + CTX, + &user.LockUserRequest{}, + func(request *user.LockUserRequest) error { + resp := Tester.CreateHumanUser(CTX) + request.UserId = resp.GetUserId() + _, err := Client.LockUser(CTX, &user.LockUserRequest{ + UserId: resp.GetUserId(), + }) + return err + }, + }, + wantErr: true, + }, + { + name: "lock machine, already locked", + args: args{ + CTX, + &user.LockUserRequest{}, + func(request *user.LockUserRequest) error { + resp := Tester.CreateMachineUser(CTX) + request.UserId = resp.GetUserId() + _, err := Client.LockUser(CTX, &user.LockUserRequest{ + UserId: resp.GetUserId(), + }) + return err + }, + }, + wantErr: true, + }, + } + 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.LockUser(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + integration.AssertDetails(t, tt.want, got) + }) + } +} + +func TestServer_UnLockUser(t *testing.T) { + type args struct { + ctx context.Context + req *user.UnlockUserRequest + prepare func(request *user.UnlockUserRequest) error + } + tests := []struct { + name string + args args + want *user.UnlockUserResponse + wantErr bool + }{ + { + name: "unlock, not existing", + args: args{ + CTX, + &user.UnlockUserRequest{ + UserId: "notexisting", + }, + func(request *user.UnlockUserRequest) error { return nil }, + }, + wantErr: true, + }, + { + name: "unlock, not locked", + args: args{ + ctx: CTX, + req: &user.UnlockUserRequest{}, + prepare: func(request *user.UnlockUserRequest) error { + resp := Tester.CreateHumanUser(CTX) + request.UserId = resp.GetUserId() + return nil + }, + }, + wantErr: true, + }, + { + name: "unlock machine, not locked", + args: args{ + ctx: CTX, + req: &user.UnlockUserRequest{}, + prepare: func(request *user.UnlockUserRequest) error { + resp := Tester.CreateMachineUser(CTX) + request.UserId = resp.GetUserId() + return nil + }, + }, + wantErr: true, + }, + { + name: "unlock, ok", + args: args{ + ctx: CTX, + req: &user.UnlockUserRequest{}, + prepare: func(request *user.UnlockUserRequest) error { + resp := Tester.CreateHumanUser(CTX) + request.UserId = resp.GetUserId() + _, err := Client.LockUser(CTX, &user.LockUserRequest{ + UserId: resp.GetUserId(), + }) + return err + }, + }, + want: &user.UnlockUserResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Organisation.ID, + }, + }, + }, + { + name: "unlock machine, ok", + args: args{ + ctx: CTX, + req: &user.UnlockUserRequest{}, + prepare: func(request *user.UnlockUserRequest) error { + resp := Tester.CreateMachineUser(CTX) + request.UserId = resp.GetUserId() + _, err := Client.LockUser(CTX, &user.LockUserRequest{ + UserId: resp.GetUserId(), + }) + return err + }, + }, + want: &user.UnlockUserResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Organisation.ID, + }, + }, + }, + } + 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.UnlockUser(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + integration.AssertDetails(t, tt.want, got) + }) + } +} + +func TestServer_DeactivateUser(t *testing.T) { + type args struct { + ctx context.Context + req *user.DeactivateUserRequest + prepare func(request *user.DeactivateUserRequest) error + } + tests := []struct { + name string + args args + want *user.DeactivateUserResponse + wantErr bool + }{ + { + name: "deactivate, not existing", + args: args{ + CTX, + &user.DeactivateUserRequest{ + UserId: "notexisting", + }, + func(request *user.DeactivateUserRequest) error { return nil }, + }, + wantErr: true, + }, + { + name: "deactivate, ok", + args: args{ + CTX, + &user.DeactivateUserRequest{}, + func(request *user.DeactivateUserRequest) error { + resp := Tester.CreateHumanUser(CTX) + request.UserId = resp.GetUserId() + return nil + }, + }, + want: &user.DeactivateUserResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Organisation.ID, + }, + }, + }, + { + name: "deactivate machine, ok", + args: args{ + CTX, + &user.DeactivateUserRequest{}, + func(request *user.DeactivateUserRequest) error { + resp := Tester.CreateMachineUser(CTX) + request.UserId = resp.GetUserId() + return nil + }, + }, + want: &user.DeactivateUserResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Organisation.ID, + }, + }, + }, + { + name: "deactivate, already deactivated", + args: args{ + CTX, + &user.DeactivateUserRequest{}, + func(request *user.DeactivateUserRequest) error { + resp := Tester.CreateHumanUser(CTX) + request.UserId = resp.GetUserId() + _, err := Client.DeactivateUser(CTX, &user.DeactivateUserRequest{ + UserId: resp.GetUserId(), + }) + return err + }, + }, + wantErr: true, + }, + { + name: "deactivate machine, already deactivated", + args: args{ + CTX, + &user.DeactivateUserRequest{}, + func(request *user.DeactivateUserRequest) error { + resp := Tester.CreateMachineUser(CTX) + request.UserId = resp.GetUserId() + _, err := Client.DeactivateUser(CTX, &user.DeactivateUserRequest{ + UserId: resp.GetUserId(), + }) + return err + }, + }, + wantErr: true, + }, + } + 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.DeactivateUser(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + integration.AssertDetails(t, tt.want, got) + }) + } +} + +func TestServer_ReactivateUser(t *testing.T) { + type args struct { + ctx context.Context + req *user.ReactivateUserRequest + prepare func(request *user.ReactivateUserRequest) error + } + tests := []struct { + name string + args args + want *user.ReactivateUserResponse + wantErr bool + }{ + { + name: "reactivate, not existing", + args: args{ + CTX, + &user.ReactivateUserRequest{ + UserId: "notexisting", + }, + func(request *user.ReactivateUserRequest) error { return nil }, + }, + wantErr: true, + }, + { + name: "reactivate, not deactivated", + args: args{ + ctx: CTX, + req: &user.ReactivateUserRequest{}, + prepare: func(request *user.ReactivateUserRequest) error { + resp := Tester.CreateHumanUser(CTX) + request.UserId = resp.GetUserId() + return nil + }, + }, + wantErr: true, + }, + { + name: "reactivate machine, not deactivated", + args: args{ + ctx: CTX, + req: &user.ReactivateUserRequest{}, + prepare: func(request *user.ReactivateUserRequest) error { + resp := Tester.CreateMachineUser(CTX) + request.UserId = resp.GetUserId() + return nil + }, + }, + wantErr: true, + }, + { + name: "reactivate, ok", + args: args{ + ctx: CTX, + req: &user.ReactivateUserRequest{}, + prepare: func(request *user.ReactivateUserRequest) error { + resp := Tester.CreateHumanUser(CTX) + request.UserId = resp.GetUserId() + _, err := Client.DeactivateUser(CTX, &user.DeactivateUserRequest{ + UserId: resp.GetUserId(), + }) + return err + }, + }, + want: &user.ReactivateUserResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Organisation.ID, + }, + }, + }, + { + name: "reactivate machine, ok", + args: args{ + ctx: CTX, + req: &user.ReactivateUserRequest{}, + prepare: func(request *user.ReactivateUserRequest) error { + resp := Tester.CreateMachineUser(CTX) + request.UserId = resp.GetUserId() + _, err := Client.DeactivateUser(CTX, &user.DeactivateUserRequest{ + UserId: resp.GetUserId(), + }) + return err + }, + }, + want: &user.ReactivateUserResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Organisation.ID, + }, + }, + }, + } + 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.ReactivateUser(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + integration.AssertDetails(t, tt.want, got) + }) + } +} + +func TestServer_DeleteUser(t *testing.T) { + projectResp, err := Tester.CreateProject(CTX) + require.NoError(t, err) + type args struct { + ctx context.Context + req *user.DeleteUserRequest + prepare func(request *user.DeleteUserRequest) error + } + tests := []struct { + name string + args args + want *user.DeleteUserResponse + wantErr bool + }{ + { + name: "remove, not existing", + args: args{ + CTX, + &user.DeleteUserRequest{ + UserId: "notexisting", + }, + func(request *user.DeleteUserRequest) error { return nil }, + }, + wantErr: true, + }, + { + name: "remove human, ok", + args: args{ + ctx: CTX, + req: &user.DeleteUserRequest{}, + prepare: func(request *user.DeleteUserRequest) error { + resp := Tester.CreateHumanUser(CTX) + request.UserId = resp.GetUserId() + return err + }, + }, + want: &user.DeleteUserResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Organisation.ID, + }, + }, + }, + { + name: "remove machine, ok", + args: args{ + ctx: CTX, + req: &user.DeleteUserRequest{}, + prepare: func(request *user.DeleteUserRequest) error { + resp := Tester.CreateMachineUser(CTX) + request.UserId = resp.GetUserId() + return err + }, + }, + want: &user.DeleteUserResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Organisation.ID, + }, + }, + }, + { + name: "remove dependencies, ok", + args: args{ + ctx: CTX, + req: &user.DeleteUserRequest{}, + prepare: func(request *user.DeleteUserRequest) error { + resp := Tester.CreateHumanUser(CTX) + request.UserId = resp.GetUserId() + Tester.CreateProjectUserGrant(t, CTX, projectResp.GetId(), request.UserId) + Tester.CreateProjectMembership(t, CTX, projectResp.GetId(), request.UserId) + Tester.CreateOrgMembership(t, CTX, request.UserId) + return err + }, + }, + want: &user.DeleteUserResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Organisation.ID, + }, + }, + }, + } + 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) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + integration.AssertDetails(t, tt.want, got) + }) + } +} + +func TestServer_AddIDPLink(t *testing.T) { + idpID := Tester.AddGenericOAuthProvider(t, CTX) + type args struct { + ctx context.Context + req *user.AddIDPLinkRequest + } + tests := []struct { + name string + args args + want *user.AddIDPLinkResponse + wantErr bool + }{ + { + name: "user does not exist", + args: args{ + CTX, + &user.AddIDPLinkRequest{ + UserId: "userID", + IdpLink: &user.IDPLink{ + IdpId: idpID, + UserId: "userID", + UserName: "username", + }, + }, + }, + want: nil, + wantErr: true, + }, + { + name: "idp does not exist", + args: args{ + CTX, + &user.AddIDPLinkRequest{ + UserId: Tester.Users[integration.FirstInstanceUsersKey][integration.OrgOwner].ID, + IdpLink: &user.IDPLink{ + IdpId: "idpID", + UserId: "userID", + UserName: "username", + }, + }, + }, + want: nil, + wantErr: true, + }, + { + name: "add link", + args: args{ + CTX, + &user.AddIDPLinkRequest{ + UserId: Tester.Users[integration.FirstInstanceUsersKey][integration.OrgOwner].ID, + IdpLink: &user.IDPLink{ + IdpId: idpID, + UserId: "userID", + UserName: "username", + }, + }, + }, + want: &user.AddIDPLinkResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Organisation.ID, + }, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Client.AddIDPLink(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + + integration.AssertDetails(t, tt.want, got) + }) + } +} + +func TestServer_StartIdentityProviderIntent(t *testing.T) { + idpID := Tester.AddGenericOAuthProvider(t, CTX) + orgIdpID := Tester.AddOrgGenericOAuthProvider(t, CTX, Tester.Organisation.ID) + orgResp := Tester.CreateOrganization(IamCTX, fmt.Sprintf("NotDefaultOrg%d", time.Now().UnixNano()), fmt.Sprintf("%d@mouse.com", time.Now().UnixNano())) + notDefaultOrgIdpID := Tester.AddOrgGenericOAuthProvider(t, CTX, orgResp.OrganizationId) + samlIdpID := Tester.AddSAMLProvider(t, CTX) + samlRedirectIdpID := Tester.AddSAMLRedirectProvider(t, CTX, "") + samlPostIdpID := Tester.AddSAMLPostProvider(t, CTX) + type args struct { + ctx context.Context + req *user.StartIdentityProviderIntentRequest + } + type want struct { + details *object.Details + url string + parametersExisting []string + parametersEqual map[string]string + postForm bool + } + tests := []struct { + name string + args args + want want + wantErr bool + }{ + { + name: "missing urls", + args: args{ + CTX, + &user.StartIdentityProviderIntentRequest{ + IdpId: idpID, + }, + }, + wantErr: true, + }, + { + name: "next step oauth auth url", + args: args{ + CTX, + &user.StartIdentityProviderIntentRequest{ + IdpId: idpID, + 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: Tester.Instance.InstanceID(), + }, + url: "https://example.com/oauth/v2/authorize", + parametersEqual: map[string]string{ + "client_id": "clientID", + "prompt": "select_account", + "redirect_uri": "http://" + Tester.Config.ExternalDomain + ":8080/idps/callback", + "response_type": "code", + "scope": "openid profile email", + }, + parametersExisting: []string{"state"}, + }, + wantErr: false, + }, + { + name: "next step oauth auth url, default org", + args: args{ + CTX, + &user.StartIdentityProviderIntentRequest{ + IdpId: orgIdpID, + 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: Tester.Instance.InstanceID(), + }, + url: "https://example.com/oauth/v2/authorize", + parametersEqual: map[string]string{ + "client_id": "clientID", + "prompt": "select_account", + "redirect_uri": "http://" + Tester.Config.ExternalDomain + ":8080/idps/callback", + "response_type": "code", + "scope": "openid profile email", + }, + parametersExisting: []string{"state"}, + }, + wantErr: false, + }, + { + name: "next step oauth auth url, default org", + args: args{ + CTX, + &user.StartIdentityProviderIntentRequest{ + IdpId: notDefaultOrgIdpID, + 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: Tester.Instance.InstanceID(), + }, + url: "https://example.com/oauth/v2/authorize", + parametersEqual: map[string]string{ + "client_id": "clientID", + "prompt": "select_account", + "redirect_uri": "http://" + Tester.Config.ExternalDomain + ":8080/idps/callback", + "response_type": "code", + "scope": "openid profile email", + }, + parametersExisting: []string{"state"}, + }, + wantErr: false, + }, + { + name: "next step oauth auth url org", + args: args{ + CTX, + &user.StartIdentityProviderIntentRequest{ + IdpId: orgIdpID, + 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: Tester.Instance.InstanceID(), + }, + url: "https://example.com/oauth/v2/authorize", + parametersEqual: map[string]string{ + "client_id": "clientID", + "prompt": "select_account", + "redirect_uri": "http://" + Tester.Config.ExternalDomain + ":8080/idps/callback", + "response_type": "code", + "scope": "openid profile email", + }, + parametersExisting: []string{"state"}, + }, + wantErr: false, + }, + { + name: "next step saml default", + args: args{ + CTX, + &user.StartIdentityProviderIntentRequest{ + IdpId: samlIdpID, + 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: Tester.Instance.InstanceID(), + }, + url: "http://" + Tester.Config.ExternalDomain + ":8000/sso", + parametersExisting: []string{"RelayState", "SAMLRequest"}, + }, + wantErr: false, + }, + { + name: "next step saml auth url", + args: args{ + CTX, + &user.StartIdentityProviderIntentRequest{ + IdpId: samlRedirectIdpID, + 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: Tester.Instance.InstanceID(), + }, + url: "http://" + Tester.Config.ExternalDomain + ":8000/sso", + parametersExisting: []string{"RelayState", "SAMLRequest"}, + }, + wantErr: false, + }, + { + name: "next step saml form", + args: args{ + CTX, + &user.StartIdentityProviderIntentRequest{ + IdpId: samlPostIdpID, + 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: Tester.Instance.InstanceID(), + }, + postForm: true, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Client.StartIdentityProviderIntent(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + + if tt.want.url != "" { + authUrl, err := url.Parse(got.GetAuthUrl()) + assert.NoError(t, err) + + assert.Len(t, authUrl.Query(), len(tt.want.parametersEqual)+len(tt.want.parametersExisting)) + + for _, existing := range tt.want.parametersExisting { + assert.True(t, authUrl.Query().Has(existing)) + } + for key, equal := range tt.want.parametersEqual { + assert.Equal(t, equal, authUrl.Query().Get(key)) + } + } + if tt.want.postForm { + assert.NotEmpty(t, got.GetPostForm()) + } + integration.AssertDetails(t, &user.StartIdentityProviderIntentResponse{ + Details: tt.want.details, + }, got) + }) + } +} + +func TestServer_RetrieveIdentityProviderIntent(t *testing.T) { + idpID := Tester.AddGenericOAuthProvider(t, CTX) + intentID := Tester.CreateIntent(t, CTX, idpID) + successfulID, token, changeDate, sequence := Tester.CreateSuccessfulOAuthIntent(t, CTX, idpID, "", "id") + successfulWithUserID, withUsertoken, withUserchangeDate, withUsersequence := Tester.CreateSuccessfulOAuthIntent(t, CTX, idpID, "user", "id") + ldapSuccessfulID, ldapToken, ldapChangeDate, ldapSequence := Tester.CreateSuccessfulLDAPIntent(t, CTX, idpID, "", "id") + ldapSuccessfulWithUserID, ldapWithUserToken, ldapWithUserChangeDate, ldapWithUserSequence := Tester.CreateSuccessfulLDAPIntent(t, CTX, idpID, "user", "id") + samlSuccessfulID, samlToken, samlChangeDate, samlSequence := Tester.CreateSuccessfulSAMLIntent(t, CTX, idpID, "", "id") + type args struct { + ctx context.Context + req *user.RetrieveIdentityProviderIntentRequest + } + tests := []struct { + name string + args args + want *user.RetrieveIdentityProviderIntentResponse + wantErr bool + }{ + { + name: "failed intent", + args: args{ + CTX, + &user.RetrieveIdentityProviderIntentRequest{ + IdpIntentId: intentID, + IdpIntentToken: "", + }, + }, + wantErr: true, + }, + { + name: "wrong token", + args: args{ + CTX, + &user.RetrieveIdentityProviderIntentRequest{ + IdpIntentId: successfulID, + IdpIntentToken: "wrong token", + }, + }, + wantErr: true, + }, + { + name: "retrieve successful intent", + args: args{ + CTX, + &user.RetrieveIdentityProviderIntentRequest{ + IdpIntentId: successfulID, + IdpIntentToken: token, + }, + }, + want: &user.RetrieveIdentityProviderIntentResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.New(changeDate), + ResourceOwner: Tester.Instance.InstanceID(), + Sequence: sequence, + }, + IdpInformation: &user.IDPInformation{ + Access: &user.IDPInformation_Oauth{ + Oauth: &user.IDPOAuthAccessInformation{ + AccessToken: "accessToken", + IdToken: gu.Ptr("idToken"), + }, + }, + IdpId: idpID, + UserId: "id", + UserName: "username", + RawInformation: func() *structpb.Struct { + s, err := structpb.NewStruct(map[string]interface{}{ + "sub": "id", + "preferred_username": "username", + }) + require.NoError(t, err) + return s + }(), + }, + }, + wantErr: false, + }, + { + name: "retrieve successful intent with linked user", + args: args{ + CTX, + &user.RetrieveIdentityProviderIntentRequest{ + IdpIntentId: successfulWithUserID, + IdpIntentToken: withUsertoken, + }, + }, + want: &user.RetrieveIdentityProviderIntentResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.New(withUserchangeDate), + ResourceOwner: Tester.Instance.InstanceID(), + Sequence: withUsersequence, + }, + UserId: "user", + IdpInformation: &user.IDPInformation{ + Access: &user.IDPInformation_Oauth{ + Oauth: &user.IDPOAuthAccessInformation{ + AccessToken: "accessToken", + IdToken: gu.Ptr("idToken"), + }, + }, + IdpId: idpID, + UserId: "id", + UserName: "username", + RawInformation: func() *structpb.Struct { + s, err := structpb.NewStruct(map[string]interface{}{ + "sub": "id", + "preferred_username": "username", + }) + require.NoError(t, err) + return s + }(), + }, + }, + wantErr: false, + }, + { + name: "retrieve successful ldap intent", + args: args{ + CTX, + &user.RetrieveIdentityProviderIntentRequest{ + IdpIntentId: ldapSuccessfulID, + IdpIntentToken: ldapToken, + }, + }, + want: &user.RetrieveIdentityProviderIntentResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.New(ldapChangeDate), + ResourceOwner: Tester.Instance.InstanceID(), + Sequence: ldapSequence, + }, + IdpInformation: &user.IDPInformation{ + Access: &user.IDPInformation_Ldap{ + Ldap: &user.IDPLDAPAccessInformation{ + Attributes: func() *structpb.Struct { + s, err := structpb.NewStruct(map[string]interface{}{ + "id": []interface{}{"id"}, + "username": []interface{}{"username"}, + "language": []interface{}{"en"}, + }) + require.NoError(t, err) + return s + }(), + }, + }, + IdpId: idpID, + UserId: "id", + UserName: "username", + RawInformation: func() *structpb.Struct { + s, err := structpb.NewStruct(map[string]interface{}{ + "id": "id", + "preferredUsername": "username", + "preferredLanguage": "en", + }) + require.NoError(t, err) + return s + }(), + }, + }, + wantErr: false, + }, + { + name: "retrieve successful ldap intent with linked user", + args: args{ + CTX, + &user.RetrieveIdentityProviderIntentRequest{ + IdpIntentId: ldapSuccessfulWithUserID, + IdpIntentToken: ldapWithUserToken, + }, + }, + want: &user.RetrieveIdentityProviderIntentResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.New(ldapWithUserChangeDate), + ResourceOwner: Tester.Instance.InstanceID(), + Sequence: ldapWithUserSequence, + }, + UserId: "user", + IdpInformation: &user.IDPInformation{ + Access: &user.IDPInformation_Ldap{ + Ldap: &user.IDPLDAPAccessInformation{ + Attributes: func() *structpb.Struct { + s, err := structpb.NewStruct(map[string]interface{}{ + "id": []interface{}{"id"}, + "username": []interface{}{"username"}, + "language": []interface{}{"en"}, + }) + require.NoError(t, err) + return s + }(), + }, + }, + IdpId: idpID, + UserId: "id", + UserName: "username", + RawInformation: func() *structpb.Struct { + s, err := structpb.NewStruct(map[string]interface{}{ + "id": "id", + "preferredUsername": "username", + "preferredLanguage": "en", + }) + require.NoError(t, err) + return s + }(), + }, + }, + wantErr: false, + }, + { + name: "retrieve successful saml intent", + args: args{ + CTX, + &user.RetrieveIdentityProviderIntentRequest{ + IdpIntentId: samlSuccessfulID, + IdpIntentToken: samlToken, + }, + }, + want: &user.RetrieveIdentityProviderIntentResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.New(samlChangeDate), + ResourceOwner: Tester.Instance.InstanceID(), + Sequence: samlSequence, + }, + IdpInformation: &user.IDPInformation{ + Access: &user.IDPInformation_Saml{ + Saml: &user.IDPSAMLAccessInformation{ + Assertion: []byte(""), + }, + }, + IdpId: idpID, + UserId: "id", + UserName: "", + RawInformation: func() *structpb.Struct { + s, err := structpb.NewStruct(map[string]interface{}{ + "id": "id", + "attributes": map[string]interface{}{ + "attribute1": []interface{}{"value1"}, + }, + }) + require.NoError(t, err) + return s + }(), + }, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Client.RetrieveIdentityProviderIntent(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + + grpc.AllFieldsEqual(t, tt.want.ProtoReflect(), got.ProtoReflect(), grpc.CustomMappers) + }) + } +} + +func TestServer_ListAuthenticationMethodTypes(t *testing.T) { + userIDWithoutAuth := Tester.CreateHumanUser(CTX).GetUserId() + + userIDWithPasskey := Tester.CreateHumanUser(CTX).GetUserId() + Tester.RegisterUserPasskey(CTX, userIDWithPasskey) + + userMultipleAuth := Tester.CreateHumanUser(CTX).GetUserId() + Tester.RegisterUserPasskey(CTX, userMultipleAuth) + provider, err := Tester.Client.Mgmt.AddGenericOIDCProvider(CTX, &mgmt.AddGenericOIDCProviderRequest{ + Name: "ListAuthenticationMethodTypes", + Issuer: "https://example.com", + ClientId: "client_id", + ClientSecret: "client_secret", + }) + require.NoError(t, err) + _, err = Tester.Client.Mgmt.AddCustomLoginPolicy(CTX, &mgmt.AddCustomLoginPolicyRequest{}) + require.Condition(t, func() bool { + code := status.Convert(err).Code() + return code == codes.AlreadyExists || code == codes.OK + }) + _, err = Tester.Client.Mgmt.AddIDPToLoginPolicy(CTX, &mgmt.AddIDPToLoginPolicyRequest{ + IdpId: provider.GetId(), + OwnerType: idp.IDPOwnerType_IDP_OWNER_TYPE_ORG, + }) + require.NoError(t, err) + idpLink, err := Client.AddIDPLink(CTX, &user.AddIDPLinkRequest{UserId: userMultipleAuth, IdpLink: &user.IDPLink{ + IdpId: provider.GetId(), + UserId: "external-id", + UserName: "displayName", + }}) + require.NoError(t, err) + // This should not remove the user IDP links + _, err = Tester.Client.Mgmt.RemoveIDPFromLoginPolicy(CTX, &mgmt.RemoveIDPFromLoginPolicyRequest{ + IdpId: provider.GetId(), + }) + require.NoError(t, err) + + type args struct { + ctx context.Context + req *user.ListAuthenticationMethodTypesRequest + } + tests := []struct { + name string + args args + want *user.ListAuthenticationMethodTypesResponse + }{ + { + name: "no auth", + args: args{ + CTX, + &user.ListAuthenticationMethodTypesRequest{ + UserId: userIDWithoutAuth, + }, + }, + want: &user.ListAuthenticationMethodTypesResponse{ + Details: &object.ListDetails{ + TotalResult: 0, + }, + }, + }, + { + name: "with auth (passkey)", + args: args{ + CTX, + &user.ListAuthenticationMethodTypesRequest{ + UserId: userIDWithPasskey, + }, + }, + want: &user.ListAuthenticationMethodTypesResponse{ + Details: &object.ListDetails{ + TotalResult: 1, + }, + AuthMethodTypes: []user.AuthenticationMethodType{ + user.AuthenticationMethodType_AUTHENTICATION_METHOD_TYPE_PASSKEY, + }, + }, + }, + { + name: "multiple auth", + args: args{ + CTX, + &user.ListAuthenticationMethodTypesRequest{ + UserId: userMultipleAuth, + }, + }, + want: &user.ListAuthenticationMethodTypesResponse{ + Details: &object.ListDetails{ + TotalResult: 2, + }, + AuthMethodTypes: []user.AuthenticationMethodType{ + user.AuthenticationMethodType_AUTHENTICATION_METHOD_TYPE_PASSKEY, + user.AuthenticationMethodType_AUTHENTICATION_METHOD_TYPE_IDP, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var got *user.ListAuthenticationMethodTypesResponse + var err error + + for { + got, err = Client.ListAuthenticationMethodTypes(tt.args.ctx, tt.args.req) + if err == nil && !got.GetDetails().GetTimestamp().AsTime().Before(idpLink.GetDetails().GetChangeDate().AsTime()) { + break + } + select { + case <-CTX.Done(): + t.Fatal(CTX.Err(), err) + case <-time.After(time.Second): + t.Log("retrying ListAuthenticationMethodTypes") + continue + } + } + require.NoError(t, err) + assert.Equal(t, tt.want.GetDetails().GetTotalResult(), got.GetDetails().GetTotalResult()) + require.Equal(t, tt.want.GetAuthMethodTypes(), got.GetAuthMethodTypes()) + }) + } +} diff --git a/internal/api/grpc/user/v2beta/user_test.go b/internal/api/grpc/user/v2beta/user_test.go new file mode 100644 index 0000000000..9e398e83ff --- /dev/null +++ b/internal/api/grpc/user/v2beta/user_test.go @@ -0,0 +1,410 @@ +package user + +import ( + "testing" + "time" + + "github.com/muhlemmer/gu" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" + "google.golang.org/protobuf/types/known/structpb" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/api/grpc" + "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/crypto" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/zerrors" + object_pb "github.com/zitadel/zitadel/pkg/grpc/object/v2beta" + user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta" +) + +func Test_idpIntentToIDPIntentPb(t *testing.T) { + decryption := func(err error) crypto.EncryptionAlgorithm { + mCrypto := crypto.NewMockEncryptionAlgorithm(gomock.NewController(t)) + mCrypto.EXPECT().Algorithm().Return("enc") + mCrypto.EXPECT().DecryptionKeyIDs().Return([]string{"id"}) + mCrypto.EXPECT().DecryptString(gomock.Any(), gomock.Any()).DoAndReturn( + func(code []byte, keyID string) (string, error) { + if err != nil { + return "", err + } + return string(code), nil + }) + return mCrypto + } + + type args struct { + intent *command.IDPIntentWriteModel + alg crypto.EncryptionAlgorithm + } + type res struct { + resp *user.RetrieveIdentityProviderIntentResponse + err error + } + tests := []struct { + name string + args args + res res + }{ + { + "decryption invalid key id error", + args{ + intent: &command.IDPIntentWriteModel{ + WriteModel: eventstore.WriteModel{ + AggregateID: "intentID", + ProcessedSequence: 123, + ResourceOwner: "ro", + InstanceID: "instanceID", + ChangeDate: time.Date(2019, 4, 1, 1, 1, 1, 1, time.Local), + }, + IDPID: "idpID", + IDPUser: []byte(`{"userID": "idpUserID", "username": "username"}`), + IDPUserID: "idpUserID", + IDPUserName: "username", + IDPAccessToken: &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("accessToken"), + }, + IDPIDToken: "idToken", + IDPEntryAttributes: map[string][]string{}, + UserID: "userID", + State: domain.IDPIntentStateSucceeded, + }, + alg: decryption(zerrors.ThrowInternal(nil, "id", "invalid key id")), + }, + res{ + resp: nil, + err: zerrors.ThrowInternal(nil, "id", "invalid key id"), + }, + }, { + "successful oauth", + args{ + intent: &command.IDPIntentWriteModel{ + WriteModel: eventstore.WriteModel{ + AggregateID: "intentID", + ProcessedSequence: 123, + ResourceOwner: "ro", + InstanceID: "instanceID", + ChangeDate: time.Date(2019, 4, 1, 1, 1, 1, 1, time.Local), + }, + IDPID: "idpID", + IDPUser: []byte(`{"userID": "idpUserID", "username": "username"}`), + IDPUserID: "idpUserID", + IDPUserName: "username", + IDPAccessToken: &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("accessToken"), + }, + IDPIDToken: "idToken", + UserID: "", + State: domain.IDPIntentStateSucceeded, + }, + alg: decryption(nil), + }, + res{ + resp: &user.RetrieveIdentityProviderIntentResponse{ + Details: &object_pb.Details{ + Sequence: 123, + ChangeDate: timestamppb.New(time.Date(2019, 4, 1, 1, 1, 1, 1, time.Local)), + ResourceOwner: "ro", + }, + IdpInformation: &user.IDPInformation{ + Access: &user.IDPInformation_Oauth{ + Oauth: &user.IDPOAuthAccessInformation{ + AccessToken: "accessToken", + IdToken: gu.Ptr("idToken"), + }, + }, + IdpId: "idpID", + UserId: "idpUserID", + UserName: "username", + RawInformation: func() *structpb.Struct { + s, err := structpb.NewStruct(map[string]interface{}{ + "userID": "idpUserID", + "username": "username", + }) + require.NoError(t, err) + return s + }(), + }, + }, + err: nil, + }, + }, + { + "successful oauth with linked user", + args{ + intent: &command.IDPIntentWriteModel{ + WriteModel: eventstore.WriteModel{ + AggregateID: "intentID", + ProcessedSequence: 123, + ResourceOwner: "ro", + InstanceID: "instanceID", + ChangeDate: time.Date(2019, 4, 1, 1, 1, 1, 1, time.Local), + }, + IDPID: "idpID", + IDPUser: []byte(`{"userID": "idpUserID", "username": "username"}`), + IDPUserID: "idpUserID", + IDPUserName: "username", + IDPAccessToken: &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("accessToken"), + }, + IDPIDToken: "idToken", + UserID: "userID", + State: domain.IDPIntentStateSucceeded, + }, + alg: decryption(nil), + }, + res{ + resp: &user.RetrieveIdentityProviderIntentResponse{ + Details: &object_pb.Details{ + Sequence: 123, + ChangeDate: timestamppb.New(time.Date(2019, 4, 1, 1, 1, 1, 1, time.Local)), + ResourceOwner: "ro", + }, + IdpInformation: &user.IDPInformation{ + Access: &user.IDPInformation_Oauth{ + Oauth: &user.IDPOAuthAccessInformation{ + AccessToken: "accessToken", + IdToken: gu.Ptr("idToken"), + }, + }, + IdpId: "idpID", + UserId: "idpUserID", + UserName: "username", + RawInformation: func() *structpb.Struct { + s, err := structpb.NewStruct(map[string]interface{}{ + "userID": "idpUserID", + "username": "username", + }) + require.NoError(t, err) + return s + }(), + }, + UserId: "userID", + }, + err: nil, + }, + }, { + "successful ldap", + args{ + intent: &command.IDPIntentWriteModel{ + WriteModel: eventstore.WriteModel{ + AggregateID: "intentID", + ProcessedSequence: 123, + ResourceOwner: "ro", + InstanceID: "instanceID", + ChangeDate: time.Date(2019, 4, 1, 1, 1, 1, 1, time.Local), + }, + IDPID: "idpID", + IDPUser: []byte(`{"userID": "idpUserID", "username": "username"}`), + IDPUserID: "idpUserID", + IDPUserName: "username", + IDPEntryAttributes: map[string][]string{ + "id": {"idpUserID"}, + "firstName": {"firstname1", "firstname2"}, + "lastName": {"lastname"}, + }, + UserID: "", + State: domain.IDPIntentStateSucceeded, + }, + }, + res{ + resp: &user.RetrieveIdentityProviderIntentResponse{ + Details: &object_pb.Details{ + Sequence: 123, + ChangeDate: timestamppb.New(time.Date(2019, 4, 1, 1, 1, 1, 1, time.Local)), + ResourceOwner: "ro", + }, + IdpInformation: &user.IDPInformation{ + Access: &user.IDPInformation_Ldap{ + Ldap: &user.IDPLDAPAccessInformation{ + Attributes: func() *structpb.Struct { + s, err := structpb.NewStruct(map[string]interface{}{ + "id": []interface{}{"idpUserID"}, + "firstName": []interface{}{"firstname1", "firstname2"}, + "lastName": []interface{}{"lastname"}, + }) + require.NoError(t, err) + return s + }(), + }, + }, + IdpId: "idpID", + UserId: "idpUserID", + UserName: "username", + RawInformation: func() *structpb.Struct { + s, err := structpb.NewStruct(map[string]interface{}{ + "userID": "idpUserID", + "username": "username", + }) + require.NoError(t, err) + return s + }(), + }, + }, + err: nil, + }, + }, { + "successful ldap with linked user", + args{ + intent: &command.IDPIntentWriteModel{ + WriteModel: eventstore.WriteModel{ + AggregateID: "intentID", + ProcessedSequence: 123, + ResourceOwner: "ro", + InstanceID: "instanceID", + ChangeDate: time.Date(2019, 4, 1, 1, 1, 1, 1, time.Local), + }, + IDPID: "idpID", + IDPUser: []byte(`{"userID": "idpUserID", "username": "username"}`), + IDPUserID: "idpUserID", + IDPUserName: "username", + IDPEntryAttributes: map[string][]string{ + "id": {"idpUserID"}, + "firstName": {"firstname1", "firstname2"}, + "lastName": {"lastname"}, + }, + UserID: "userID", + State: domain.IDPIntentStateSucceeded, + }, + }, + res{ + resp: &user.RetrieveIdentityProviderIntentResponse{ + Details: &object_pb.Details{ + Sequence: 123, + ChangeDate: timestamppb.New(time.Date(2019, 4, 1, 1, 1, 1, 1, time.Local)), + ResourceOwner: "ro", + }, + IdpInformation: &user.IDPInformation{ + Access: &user.IDPInformation_Ldap{ + Ldap: &user.IDPLDAPAccessInformation{ + Attributes: func() *structpb.Struct { + s, err := structpb.NewStruct(map[string]interface{}{ + "id": []interface{}{"idpUserID"}, + "firstName": []interface{}{"firstname1", "firstname2"}, + "lastName": []interface{}{"lastname"}, + }) + require.NoError(t, err) + return s + }(), + }, + }, + IdpId: "idpID", + UserId: "idpUserID", + UserName: "username", + RawInformation: func() *structpb.Struct { + s, err := structpb.NewStruct(map[string]interface{}{ + "userID": "idpUserID", + "username": "username", + }) + require.NoError(t, err) + return s + }(), + }, + UserId: "userID", + }, + err: nil, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := idpIntentToIDPIntentPb(tt.args.intent, tt.args.alg) + require.ErrorIs(t, err, tt.res.err) + grpc.AllFieldsEqual(t, tt.res.resp.ProtoReflect(), got.ProtoReflect(), grpc.CustomMappers) + }) + } +} + +func Test_authMethodTypesToPb(t *testing.T) { + tests := []struct { + name string + methodTypes []domain.UserAuthMethodType + want []user.AuthenticationMethodType + }{ + { + "empty list", + nil, + []user.AuthenticationMethodType{}, + }, + { + "list", + []domain.UserAuthMethodType{ + domain.UserAuthMethodTypePasswordless, + }, + []user.AuthenticationMethodType{ + user.AuthenticationMethodType_AUTHENTICATION_METHOD_TYPE_PASSKEY, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equalf(t, tt.want, authMethodTypesToPb(tt.methodTypes), "authMethodTypesToPb(%v)", tt.methodTypes) + }) + } +} + +func Test_authMethodTypeToPb(t *testing.T) { + tests := []struct { + name string + methodType domain.UserAuthMethodType + want user.AuthenticationMethodType + }{ + { + "uspecified", + domain.UserAuthMethodTypeUnspecified, + user.AuthenticationMethodType_AUTHENTICATION_METHOD_TYPE_UNSPECIFIED, + }, + { + "totp", + domain.UserAuthMethodTypeTOTP, + user.AuthenticationMethodType_AUTHENTICATION_METHOD_TYPE_TOTP, + }, + { + "u2f", + domain.UserAuthMethodTypeU2F, + user.AuthenticationMethodType_AUTHENTICATION_METHOD_TYPE_U2F, + }, + { + "passkey", + domain.UserAuthMethodTypePasswordless, + user.AuthenticationMethodType_AUTHENTICATION_METHOD_TYPE_PASSKEY, + }, + { + "password", + domain.UserAuthMethodTypePassword, + user.AuthenticationMethodType_AUTHENTICATION_METHOD_TYPE_PASSWORD, + }, + { + "idp", + domain.UserAuthMethodTypeIDP, + user.AuthenticationMethodType_AUTHENTICATION_METHOD_TYPE_IDP, + }, + { + "otp sms", + domain.UserAuthMethodTypeOTPSMS, + user.AuthenticationMethodType_AUTHENTICATION_METHOD_TYPE_OTP_SMS, + }, + { + "otp email", + domain.UserAuthMethodTypeOTPEmail, + user.AuthenticationMethodType_AUTHENTICATION_METHOD_TYPE_OTP_EMAIL, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equalf(t, tt.want, authMethodTypeToPb(tt.methodType), "authMethodTypeToPb(%v)", tt.methodType) + }) + } +} diff --git a/internal/api/http/domain_check.go b/internal/api/http/domain_check.go index 00ab0597dc..616c28cdfc 100644 --- a/internal/api/http/domain_check.go +++ b/internal/api/http/domain_check.go @@ -3,7 +3,7 @@ package http import ( errorsAs "errors" "fmt" - "io/ioutil" + "io" "net" "net/http" @@ -43,7 +43,7 @@ func ValidateDomainHTTP(domain, token, verifier string) error { return zerrors.ThrowInternal(err, "HTTP-G2zsw", "Errors.Internal") } defer resp.Body.Close() - body, err := ioutil.ReadAll(resp.Body) + body, err := io.ReadAll(resp.Body) if err != nil { return zerrors.ThrowInternal(err, "HTTP-HB432", "Errors.Internal") } diff --git a/internal/api/http/marshal.go b/internal/api/http/marshal.go index 35fdda5f51..193196e07f 100644 --- a/internal/api/http/marshal.go +++ b/internal/api/http/marshal.go @@ -19,5 +19,5 @@ func MarshalJSON(w http.ResponseWriter, i interface{}, err error, statusCode int } w.Header().Set("content-type", "application/json") _, err = w.Write(b) - logging.Log("HTTP-sdgT2").OnError(err).Error("error writing response") + logging.WithFields("logID", "HTTP-sdgT2").OnError(err).Error("error writing response") } diff --git a/internal/api/idp/idp_integration_test.go b/internal/api/idp/idp_integration_test.go index 8fdc24539c..51d9bbfeea 100644 --- a/internal/api/idp/idp_integration_test.go +++ b/internal/api/idp/idp_integration_test.go @@ -27,7 +27,7 @@ import ( http_util "github.com/zitadel/zitadel/internal/api/http" "github.com/zitadel/zitadel/internal/integration" - user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta" + "github.com/zitadel/zitadel/pkg/grpc/user/v2" ) var ( diff --git a/internal/api/oidc/auth_request_integration_test.go b/internal/api/oidc/auth_request_integration_test.go index 630b20bc09..faa5126676 100644 --- a/internal/api/oidc/auth_request_integration_test.go +++ b/internal/api/oidc/auth_request_integration_test.go @@ -18,8 +18,8 @@ import ( oidc_api "github.com/zitadel/zitadel/internal/api/oidc" "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/integration" - oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2beta" - session "github.com/zitadel/zitadel/pkg/grpc/session/v2beta" + oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2" + "github.com/zitadel/zitadel/pkg/grpc/session/v2" ) var ( diff --git a/internal/api/oidc/client_integration_test.go b/internal/api/oidc/client_integration_test.go index 65cc9309d5..9ff0b104d9 100644 --- a/internal/api/oidc/client_integration_test.go +++ b/internal/api/oidc/client_integration_test.go @@ -20,7 +20,7 @@ import ( "github.com/zitadel/zitadel/internal/integration" "github.com/zitadel/zitadel/pkg/grpc/authn" "github.com/zitadel/zitadel/pkg/grpc/management" - oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2beta" + oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2" ) func TestServer_Introspect(t *testing.T) { diff --git a/internal/api/oidc/oidc_integration_test.go b/internal/api/oidc/oidc_integration_test.go index 0baeb53363..1f7f615809 100644 --- a/internal/api/oidc/oidc_integration_test.go +++ b/internal/api/oidc/oidc_integration_test.go @@ -19,9 +19,9 @@ import ( "github.com/zitadel/zitadel/internal/integration" "github.com/zitadel/zitadel/pkg/grpc/auth" mgmt "github.com/zitadel/zitadel/pkg/grpc/management" - oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2beta" - session "github.com/zitadel/zitadel/pkg/grpc/session/v2beta" - user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta" + oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2" + "github.com/zitadel/zitadel/pkg/grpc/session/v2" + "github.com/zitadel/zitadel/pkg/grpc/user/v2" ) var ( diff --git a/internal/api/oidc/token_exchange_integration_test.go b/internal/api/oidc/token_exchange_integration_test.go index 2636b85d86..b8b4b0bbfe 100644 --- a/internal/api/oidc/token_exchange_integration_test.go +++ b/internal/api/oidc/token_exchange_integration_test.go @@ -22,7 +22,7 @@ import ( oidc_api "github.com/zitadel/zitadel/internal/api/oidc" "github.com/zitadel/zitadel/internal/integration" "github.com/zitadel/zitadel/pkg/grpc/admin" - feature "github.com/zitadel/zitadel/pkg/grpc/feature/v2beta" + "github.com/zitadel/zitadel/pkg/grpc/feature/v2" ) func setTokenExchangeFeature(t *testing.T, value bool) { diff --git a/internal/api/oidc/userinfo_integration_test.go b/internal/api/oidc/userinfo_integration_test.go index 350b2267d3..ad95defd6f 100644 --- a/internal/api/oidc/userinfo_integration_test.go +++ b/internal/api/oidc/userinfo_integration_test.go @@ -18,9 +18,9 @@ import ( oidc_api "github.com/zitadel/zitadel/internal/api/oidc" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/integration" - feature "github.com/zitadel/zitadel/pkg/grpc/feature/v2beta" + "github.com/zitadel/zitadel/pkg/grpc/feature/v2" "github.com/zitadel/zitadel/pkg/grpc/management" - oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2beta" + oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2" ) // TestServer_UserInfo is a top-level test which re-executes the actual diff --git a/internal/command/org_idp_config_test.go b/internal/command/org_idp_config_test.go index 2897997695..eb38ca962c 100644 --- a/internal/command/org_idp_config_test.go +++ b/internal/command/org_idp_config_test.go @@ -398,7 +398,8 @@ func newIDPConfigChangedEvent(ctx context.Context, orgID, configID, oldName, new func TestCommands_RemoveIDPConfig(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore *eventstore.Eventstore + checkPermission domain.PermissionCheck } type args struct { ctx context.Context @@ -423,6 +424,7 @@ func TestCommands_RemoveIDPConfig(t *testing.T) { eventstore: eventstoreExpect(t, expectFilter(), ), + checkPermission: newMockPermissionCheckAllowed(), }, args{ context.Background(), @@ -460,6 +462,7 @@ func TestCommands_RemoveIDPConfig(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args{ context.Background(), @@ -532,6 +535,84 @@ func TestCommands_RemoveIDPConfig(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args{ + context.Background(), + "idp1", + "org1", + true, + []*domain.UserIDPLink{ + { + ObjectRoot: models.ObjectRoot{ + AggregateID: "user1", + }, + IDPConfigID: "idp1", + ExternalUserID: "id1", + DisplayName: "name", + }, + }, + }, + res{ + &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + nil, + }, + }, + { + "cascade, permission error", + fields{ + eventstore: eventstoreExpect(t, + expectFilter( + eventFromEventPusher( + org.NewIDPConfigAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "idp1", + "name1", + domain.IDPConfigTypeOIDC, + domain.IDPConfigStylingTypeGoogle, + false, + ), + ), + ), + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayName", + language.German, + domain.GenderUnspecified, + "email@test.com", + true, + ), + ), + eventFromEventPusher( + user.NewUserIDPLinkAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "idp1", + "name", + "id1", + ), + ), + ), + expectPush( + org.NewIDPConfigRemovedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "idp1", + "name1", + ), + org.NewIdentityProviderCascadeRemovedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "idp1", + ), + ), + ), + checkPermission: newMockPermissionCheckNotAllowed(), }, args{ context.Background(), @@ -560,7 +641,8 @@ func TestCommands_RemoveIDPConfig(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { c := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore, + checkPermission: tt.fields.checkPermission, } got, err := c.RemoveIDPConfig(tt.args.ctx, tt.args.idpID, tt.args.orgID, tt.args.cascadeRemoveProvider, tt.args.cascadeExternalIDPs...) if tt.res.err == nil { diff --git a/internal/command/user_human_webauthn.go b/internal/command/user_human_webauthn.go index 6988b0052a..3555466359 100644 --- a/internal/command/user_human_webauthn.go +++ b/internal/command/user_human_webauthn.go @@ -603,6 +603,11 @@ func (c *Commands) removeHumanWebAuthN(ctx context.Context, userID, webAuthNID, if existingWebAuthN.State == domain.MFAStateUnspecified || existingWebAuthN.State == domain.MFAStateRemoved { return nil, zerrors.ThrowNotFound(nil, "COMMAND-DAfb2", "Errors.User.WebAuthN.NotFound") } + if userID != authz.GetCtxData(ctx).UserID { + if err := c.checkPermission(ctx, domain.PermissionUserWrite, existingWebAuthN.ResourceOwner, existingWebAuthN.AggregateID); err != nil { + return nil, err + } + } userAgg := UserAggregateFromWriteModel(&existingWebAuthN.WriteModel) pushedEvents, err := c.eventstore.Push(ctx, preparedEvent(userAgg)) diff --git a/internal/command/user_idp_link.go b/internal/command/user_idp_link.go index 14f05964a1..761cebb7d2 100644 --- a/internal/command/user_idp_link.go +++ b/internal/command/user_idp_link.go @@ -126,6 +126,11 @@ func (c *Commands) removeUserIDPLink(ctx context.Context, link *domain.UserIDPLi if existingLink.State == domain.UserIDPLinkStateUnspecified || existingLink.State == domain.UserIDPLinkStateRemoved { return nil, nil, zerrors.ThrowNotFound(nil, "COMMAND-1M9xR", "Errors.User.ExternalIDP.NotFound") } + if existingLink.AggregateID != authz.GetCtxData(ctx).UserID { + if err := c.checkPermission(ctx, domain.PermissionUserWrite, existingLink.ResourceOwner, existingLink.AggregateID); err != nil { + return nil, nil, err + } + } userAgg := UserAggregateFromWriteModel(&existingLink.WriteModel) if cascade { return user.NewUserIDPLinkCascadeRemovedEvent(ctx, userAgg, link.IDPConfigID, link.ExternalUserID), existingLink, nil diff --git a/internal/command/user_idp_link_test.go b/internal/command/user_idp_link_test.go index f1f7929686..67d1c005ef 100644 --- a/internal/command/user_idp_link_test.go +++ b/internal/command/user_idp_link_test.go @@ -519,7 +519,8 @@ func TestCommandSide_BulkAddUserIDPLinks(t *testing.T) { func TestCommandSide_RemoveUserIDPLink(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore *eventstore.Eventstore + checkPermission domain.PermissionCheck } type args struct { ctx context.Context @@ -541,6 +542,7 @@ func TestCommandSide_RemoveUserIDPLink(t *testing.T) { eventstore: eventstoreExpect( t, ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -562,6 +564,7 @@ func TestCommandSide_RemoveUserIDPLink(t *testing.T) { eventstore: eventstoreExpect( t, ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -598,6 +601,7 @@ func TestCommandSide_RemoveUserIDPLink(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -620,6 +624,7 @@ func TestCommandSide_RemoveUserIDPLink(t *testing.T) { t, expectFilter(), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -635,6 +640,38 @@ func TestCommandSide_RemoveUserIDPLink(t *testing.T) { err: zerrors.IsNotFound, }, }, + { + name: "remove external idp, permission error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewUserIDPLinkAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "config1", + "name", + "externaluser1", + ), + ), + ), + ), + checkPermission: newMockPermissionCheckNotAllowed(), + }, + args: args{ + ctx: context.Background(), + link: &domain.UserIDPLink{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "user1", + }, + IDPConfigID: "config1", + ExternalUserID: "externaluser1", + }, + }, + res: res{ + err: zerrors.IsPermissionDenied, + }, + }, { name: "remove external idp, ok", fields: fields{ @@ -658,6 +695,7 @@ func TestCommandSide_RemoveUserIDPLink(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -679,7 +717,8 @@ func TestCommandSide_RemoveUserIDPLink(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, + checkPermission: tt.fields.checkPermission, } got, err := r.RemoveUserIDPLink(tt.args.ctx, tt.args.link) if tt.res.err == nil { diff --git a/internal/config/config.go b/internal/config/config.go index b454cea41f..a0e7eceb3b 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -2,7 +2,6 @@ package config import ( "encoding/json" - "io/ioutil" "os" "path/filepath" @@ -50,7 +49,7 @@ func Read(obj interface{}, configFiles ...string) error { func readConfigFile(readerFunc ReaderFunc, configFile string, obj interface{}) error { configFile = os.ExpandEnv(configFile) - configStr, err := ioutil.ReadFile(configFile) + configStr, err := os.ReadFile(configFile) if err != nil { return zerrors.ThrowInternalf(err, "CONFI-nJk2a", "failed to read config file %s", configFile) } diff --git a/internal/integration/assert.go b/internal/integration/assert.go index 610c48cf31..6928054e8e 100644 --- a/internal/integration/assert.go +++ b/internal/integration/assert.go @@ -9,8 +9,6 @@ import ( "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/timestamppb" - - object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta" ) // Details is the interface that covers both v1 and v2 proto generated object details. @@ -26,8 +24,14 @@ type DetailsMsg[D Details] interface { GetDetails() D } -type ListDetailsMsg interface { - GetDetails() *object.ListDetails +type ListDetails interface { + comparable + GetTotalResult() uint64 + GetTimestamp() *timestamppb.Timestamp +} + +type ListDetailsMsg[L ListDetails] interface { + GetDetails() L } // AssertDetails asserts values in a message's object Details, @@ -59,13 +63,13 @@ func AssertDetails[D Details, M DetailsMsg[D]](t testing.TB, expected, actual M) assert.Equal(t, wantDetails.GetResourceOwner(), gotDetails.GetResourceOwner()) } -func AssertListDetails[D ListDetailsMsg](t testing.TB, expected, actual D) { +func AssertListDetails[L ListDetails, D ListDetailsMsg[L]](t testing.TB, expected, actual D) { wantDetails, gotDetails := expected.GetDetails(), actual.GetDetails() - if wantDetails == nil { + var nilDetails L + if wantDetails == nilDetails { assert.Nil(t, gotDetails) return } - assert.Equal(t, wantDetails.GetTotalResult(), gotDetails.GetTotalResult()) if wantDetails.GetTimestamp() != nil { diff --git a/internal/integration/client.go b/internal/integration/client.go index bd7c8eb400..7cb3af4c7b 100644 --- a/internal/integration/client.go +++ b/internal/integration/client.go @@ -28,51 +28,69 @@ import ( action "github.com/zitadel/zitadel/pkg/grpc/action/v3alpha" "github.com/zitadel/zitadel/pkg/grpc/admin" "github.com/zitadel/zitadel/pkg/grpc/auth" - feature "github.com/zitadel/zitadel/pkg/grpc/feature/v2beta" + "github.com/zitadel/zitadel/pkg/grpc/feature/v2" + feature_v2beta "github.com/zitadel/zitadel/pkg/grpc/feature/v2beta" mgmt "github.com/zitadel/zitadel/pkg/grpc/management" - object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta" - oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2beta" - org "github.com/zitadel/zitadel/pkg/grpc/org/v2beta" - organisation "github.com/zitadel/zitadel/pkg/grpc/org/v2beta" - session "github.com/zitadel/zitadel/pkg/grpc/session/v2beta" - settings "github.com/zitadel/zitadel/pkg/grpc/settings/v2beta" + "github.com/zitadel/zitadel/pkg/grpc/object/v2" + oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2" + oidc_pb_v2beta "github.com/zitadel/zitadel/pkg/grpc/oidc/v2beta" + "github.com/zitadel/zitadel/pkg/grpc/org/v2" + organisation "github.com/zitadel/zitadel/pkg/grpc/org/v2" + org_v2beta "github.com/zitadel/zitadel/pkg/grpc/org/v2beta" + "github.com/zitadel/zitadel/pkg/grpc/session/v2" + session_v2beta "github.com/zitadel/zitadel/pkg/grpc/session/v2beta" + "github.com/zitadel/zitadel/pkg/grpc/settings/v2" + settings_v2beta "github.com/zitadel/zitadel/pkg/grpc/settings/v2beta" "github.com/zitadel/zitadel/pkg/grpc/system" user_pb "github.com/zitadel/zitadel/pkg/grpc/user" schema "github.com/zitadel/zitadel/pkg/grpc/user/schema/v3alpha" - user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta" + "github.com/zitadel/zitadel/pkg/grpc/user/v2" + user_v2beta "github.com/zitadel/zitadel/pkg/grpc/user/v2beta" ) type Client struct { - CC *grpc.ClientConn - Admin admin.AdminServiceClient - Mgmt mgmt.ManagementServiceClient - Auth auth.AuthServiceClient - UserV2 user.UserServiceClient - SessionV2 session.SessionServiceClient - SettingsV2 settings.SettingsServiceClient - OIDCv2 oidc_pb.OIDCServiceClient - OrgV2 organisation.OrganizationServiceClient - System system.SystemServiceClient - ActionV3 action.ActionServiceClient - FeatureV2 feature.FeatureServiceClient - UserSchemaV3 schema.UserSchemaServiceClient + CC *grpc.ClientConn + Admin admin.AdminServiceClient + Mgmt mgmt.ManagementServiceClient + Auth auth.AuthServiceClient + UserV2beta user_v2beta.UserServiceClient + UserV2 user.UserServiceClient + SessionV2beta session_v2beta.SessionServiceClient + SessionV2 session.SessionServiceClient + SettingsV2beta settings_v2beta.SettingsServiceClient + SettingsV2 settings.SettingsServiceClient + OIDCv2beta oidc_pb_v2beta.OIDCServiceClient + OIDCv2 oidc_pb.OIDCServiceClient + OrgV2beta org_v2beta.OrganizationServiceClient + OrgV2 organisation.OrganizationServiceClient + System system.SystemServiceClient + ActionV3 action.ActionServiceClient + FeatureV2beta feature_v2beta.FeatureServiceClient + FeatureV2 feature.FeatureServiceClient + UserSchemaV3 schema.UserSchemaServiceClient } func newClient(cc *grpc.ClientConn) Client { return Client{ - CC: cc, - Admin: admin.NewAdminServiceClient(cc), - Mgmt: mgmt.NewManagementServiceClient(cc), - Auth: auth.NewAuthServiceClient(cc), - UserV2: user.NewUserServiceClient(cc), - SessionV2: session.NewSessionServiceClient(cc), - SettingsV2: settings.NewSettingsServiceClient(cc), - OIDCv2: oidc_pb.NewOIDCServiceClient(cc), - OrgV2: organisation.NewOrganizationServiceClient(cc), - System: system.NewSystemServiceClient(cc), - ActionV3: action.NewActionServiceClient(cc), - FeatureV2: feature.NewFeatureServiceClient(cc), - UserSchemaV3: schema.NewUserSchemaServiceClient(cc), + CC: cc, + Admin: admin.NewAdminServiceClient(cc), + Mgmt: mgmt.NewManagementServiceClient(cc), + Auth: auth.NewAuthServiceClient(cc), + UserV2beta: user_v2beta.NewUserServiceClient(cc), + UserV2: user.NewUserServiceClient(cc), + SessionV2beta: session_v2beta.NewSessionServiceClient(cc), + SessionV2: session.NewSessionServiceClient(cc), + SettingsV2beta: settings_v2beta.NewSettingsServiceClient(cc), + SettingsV2: settings.NewSettingsServiceClient(cc), + OIDCv2beta: oidc_pb_v2beta.NewOIDCServiceClient(cc), + OIDCv2: oidc_pb.NewOIDCServiceClient(cc), + OrgV2beta: org_v2beta.NewOrganizationServiceClient(cc), + OrgV2: organisation.NewOrganizationServiceClient(cc), + System: system.NewSystemServiceClient(cc), + ActionV3: action.NewActionServiceClient(cc), + FeatureV2beta: feature_v2beta.NewFeatureServiceClient(cc), + FeatureV2: feature.NewFeatureServiceClient(cc), + UserSchemaV3: schema.NewUserSchemaServiceClient(cc), } } diff --git a/internal/notification/channels/log/channel.go b/internal/notification/channels/log/channel.go index ecdb59c642..42efbbe842 100644 --- a/internal/notification/channels/log/channel.go +++ b/internal/notification/channels/log/channel.go @@ -11,7 +11,7 @@ import ( func InitStdoutChannel(config Config) channels.NotificationChannel { - logging.Log("NOTIF-D0164").Debug("successfully initialized stdout email and sms channel") + logging.WithFields("logID", "NOTIF-D0164").Debug("successfully initialized stdout email and sms channel") return channels.HandleMessageFunc(func(message channels.Message) error { @@ -23,7 +23,7 @@ func InitStdoutChannel(config Config) channels.NotificationChannel { content = html2text.HTML2Text(content) } - logging.Log("NOTIF-c73ba").WithFields(map[string]interface{}{ + logging.WithFields("logID", "NOTIF-c73ba").WithFields(map[string]interface{}{ "type": fmt.Sprintf("%T", message), "content": content, }).Info("handling notification message") diff --git a/internal/notification/handlers/telemetry_pusher_integration_test.go b/internal/notification/handlers/telemetry_pusher_integration_test.go index b84f02fa79..8f207b9de3 100644 --- a/internal/notification/handlers/telemetry_pusher_integration_test.go +++ b/internal/notification/handlers/telemetry_pusher_integration_test.go @@ -19,7 +19,7 @@ import ( "github.com/zitadel/zitadel/pkg/grpc/app" "github.com/zitadel/zitadel/pkg/grpc/management" "github.com/zitadel/zitadel/pkg/grpc/object" - oidc_v2 "github.com/zitadel/zitadel/pkg/grpc/oidc/v2beta" + oidc_v2 "github.com/zitadel/zitadel/pkg/grpc/oidc/v2" "github.com/zitadel/zitadel/pkg/grpc/project" "github.com/zitadel/zitadel/pkg/grpc/system" ) diff --git a/internal/query/idp_user_link.go b/internal/query/idp_user_link.go index 50e780c372..4f13d9d315 100644 --- a/internal/query/idp_user_link.go +++ b/internal/query/idp_user_link.go @@ -89,6 +89,29 @@ var ( } ) +func (l *IDPUserLinks) RemoveNoPermission(ctx context.Context, permissionCheck domain.PermissionCheck) { + removableIndexes := make([]int, 0) + for i := range l.Links { + ctxData := authz.GetCtxData(ctx) + if ctxData.UserID != l.Links[i].UserID { + if err := permissionCheck(ctx, domain.PermissionUserRead, l.Links[i].ResourceOwner, l.Links[i].UserID); err != nil { + removableIndexes = append(removableIndexes, i) + } + } + } + removed := 0 + for _, removeIndex := range removableIndexes { + l.Links = removeIDPLink(l.Links, removeIndex-removed) + removed++ + } + // reset count as some users could be removed + l.SearchResponse.Count = uint64(len(l.Links)) +} + +func removeIDPLink(slice []*IDPUserLink, s int) []*IDPUserLink { + return append(slice[:s], slice[s+1:]...) +} + func (q *Queries) IDPUserLinks(ctx context.Context, queries *IDPUserLinksSearchQuery, withOwnerRemoved bool) (idps *IDPUserLinks, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() diff --git a/internal/query/user_auth_method.go b/internal/query/user_auth_method.go index 243c465a84..6a93f069b0 100644 --- a/internal/query/user_auth_method.go +++ b/internal/query/user_auth_method.go @@ -98,6 +98,29 @@ type AuthMethods struct { AuthMethods []*AuthMethod } +func (l *AuthMethods) RemoveNoPermission(ctx context.Context, permissionCheck domain.PermissionCheck) { + removableIndexes := make([]int, 0) + for i := range l.AuthMethods { + ctxData := authz.GetCtxData(ctx) + if ctxData.UserID != l.AuthMethods[i].UserID { + if err := permissionCheck(ctx, domain.PermissionUserRead, l.AuthMethods[i].ResourceOwner, l.AuthMethods[i].UserID); err != nil { + removableIndexes = append(removableIndexes, i) + } + } + } + removed := 0 + for _, removeIndex := range removableIndexes { + l.AuthMethods = removeAuthMethod(l.AuthMethods, removeIndex-removed) + removed++ + } + // reset count as some users could be removed + l.SearchResponse.Count = uint64(len(l.AuthMethods)) +} + +func removeAuthMethod(slice []*AuthMethod, s int) []*AuthMethod { + return append(slice[:s], slice[s+1:]...) +} + type AuthMethod struct { UserID string CreationDate time.Time diff --git a/internal/repository/user/machine_key.go b/internal/repository/user/machine_key.go index aff1c3750e..b532616883 100644 --- a/internal/repository/user/machine_key.go +++ b/internal/repository/user/machine_key.go @@ -60,8 +60,9 @@ func MachineKeyAddedEventMapper(event eventstore.Event) (eventstore.Event, error } err := event.Unmarshal(machineKeyAdded) if err != nil { - //first events had wrong payload. + // first events had wrong payload. // the keys were removed later, that's why we ignore them here. + //nolint:errorlint if unwrapErr, ok := err.(*json.UnmarshalTypeError); ok && unwrapErr.Field == "publicKey" { return machineKeyAdded, nil } diff --git a/internal/telemetry/tracing/caller.go b/internal/telemetry/tracing/caller.go index da9cc2940b..a7612cab8d 100644 --- a/internal/telemetry/tracing/caller.go +++ b/internal/telemetry/tracing/caller.go @@ -10,12 +10,12 @@ func GetCaller() string { fpcs := make([]uintptr, 1) n := runtime.Callers(3, fpcs) if n == 0 { - logging.Log("TRACE-rWjfC").Debug("no caller") + logging.WithFields("logID", "TRACE-rWjfC").Debug("no caller") return "" } caller := runtime.FuncForPC(fpcs[0] - 1) if caller == nil { - logging.Log("TRACE-25POw").Debug("caller was nil") + logging.WithFields("logID", "TRACE-25POw").Debug("caller was nil") return "" } return caller.Name() diff --git a/pkg/grpc/user/v2/user.go b/pkg/grpc/user/v2/user.go new file mode 100644 index 0000000000..ec9245c8eb --- /dev/null +++ b/pkg/grpc/user/v2/user.go @@ -0,0 +1,3 @@ +package user + +type UserType = isUser_Type diff --git a/proto/zitadel/action/v3alpha/action_service.proto b/proto/zitadel/action/v3alpha/action_service.proto index da174b45d0..938c9e88fc 100644 --- a/proto/zitadel/action/v3alpha/action_service.proto +++ b/proto/zitadel/action/v3alpha/action_service.proto @@ -11,7 +11,7 @@ import "validate/validate.proto"; import "zitadel/action/v3alpha/target.proto"; import "zitadel/action/v3alpha/execution.proto"; import "zitadel/action/v3alpha/query.proto"; -import "zitadel/object/v2beta/object.proto"; +import "zitadel/object/v2/object.proto"; import "zitadel/protoc_gen_zitadel/v2/options.proto"; option go_package = "github.com/zitadel/zitadel/pkg/grpc/action/v3alpha;action"; @@ -449,7 +449,7 @@ message CreateTargetResponse { // ID is the read-only unique identifier of the target. string id = 1; // Details provide some base information (such as the last change date) of the target. - zitadel.object.v2beta.Details details = 2; + zitadel.object.v2.Details details = 2; } message UpdateTargetRequest { @@ -498,7 +498,7 @@ message UpdateTargetRequest { message UpdateTargetResponse { // Details provide some base information (such as the last change date) of the target. - zitadel.object.v2beta.Details details = 1; + zitadel.object.v2.Details details = 1; } message DeleteTargetRequest { @@ -516,12 +516,12 @@ message DeleteTargetRequest { message DeleteTargetResponse { // Details provide some base information (such as the last change date) of the target. - zitadel.object.v2beta.Details details = 1; + zitadel.object.v2.Details details = 1; } message ListTargetsRequest { // list limitations and ordering. - zitadel.object.v2beta.ListQuery query = 1; + zitadel.object.v2.ListQuery query = 1; // the field the result is sorted. zitadel.action.v3alpha.TargetFieldName sorting_column = 2 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -534,7 +534,7 @@ message ListTargetsRequest { message ListTargetsResponse { // Details provides information about the returned result including total amount found. - zitadel.object.v2beta.ListDetails details = 1; + zitadel.object.v2.ListDetails details = 1; // States by which field the results are sorted. zitadel.action.v3alpha.TargetFieldName sorting_column = 2; // The result contains the user schemas, which matched the queries. @@ -567,7 +567,7 @@ message SetExecutionRequest { message SetExecutionResponse { // Details provide some base information (such as the last change date) of the execution. - zitadel.object.v2beta.Details details = 2; + zitadel.object.v2.Details details = 2; } message DeleteExecutionRequest { @@ -577,19 +577,19 @@ message DeleteExecutionRequest { message DeleteExecutionResponse { // Details provide some base information (such as the last change date) of the execution. - zitadel.object.v2beta.Details details = 1; + zitadel.object.v2.Details details = 1; } message ListExecutionsRequest { // list limitations and ordering. - zitadel.object.v2beta.ListQuery query = 1; + zitadel.object.v2.ListQuery query = 1; // Define the criteria to query for. repeated zitadel.action.v3alpha.SearchQuery queries = 2; } message ListExecutionsResponse { // Details provides information about the returned result including total amount found. - zitadel.object.v2beta.ListDetails details = 1; + zitadel.object.v2.ListDetails details = 1; // The result contains the executions, which matched the queries. repeated zitadel.action.v3alpha.Execution result = 2; } diff --git a/proto/zitadel/action/v3alpha/execution.proto b/proto/zitadel/action/v3alpha/execution.proto index 6f24471185..797f997cd8 100644 --- a/proto/zitadel/action/v3alpha/execution.proto +++ b/proto/zitadel/action/v3alpha/execution.proto @@ -8,7 +8,7 @@ import "google/protobuf/duration.proto"; import "google/protobuf/struct.proto"; import "protoc-gen-openapiv2/options/annotations.proto"; import "validate/validate.proto"; -import "zitadel/object/v2beta/object.proto"; +import "zitadel/object/v2/object.proto"; import "zitadel/protoc_gen_zitadel/v2/options.proto"; option go_package = "github.com/zitadel/zitadel/pkg/grpc/action/v3alpha;action"; @@ -16,7 +16,7 @@ option go_package = "github.com/zitadel/zitadel/pkg/grpc/action/v3alpha;action"; message Execution { Condition Condition = 1; // Details provide some base information (such as the last change date) of the target. - zitadel.object.v2beta.Details details = 2; + zitadel.object.v2.Details details = 2; // List of ordered list of targets/includes called during the execution. repeated ExecutionTargetType targets = 3; } @@ -55,7 +55,7 @@ message RequestExecution { (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { min_length: 1, max_length: 1000, - example: "\"/zitadel.session.v2beta.SessionService/ListSessions\""; + example: "\"/zitadel.session.v2.SessionService/ListSessions\""; } ]; // GRPC-service as condition. @@ -64,7 +64,7 @@ message RequestExecution { (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { min_length: 1, max_length: 1000, - example: "\"zitadel.session.v2beta.SessionService\""; + example: "\"zitadel.session.v2.SessionService\""; } ]; // All calls to any available service and endpoint as condition. @@ -81,7 +81,7 @@ message ResponseExecution { (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { min_length: 1, max_length: 1000, - example: "\"/zitadel.session.v2beta.SessionService/ListSessions\""; + example: "\"/zitadel.session.v2.SessionService/ListSessions\""; } ]; // GRPC-service as condition. @@ -90,7 +90,7 @@ message ResponseExecution { (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { min_length: 1, max_length: 1000, - example: "\"zitadel.session.v2beta.SessionService\""; + example: "\"zitadel.session.v2.SessionService\""; } ]; // All calls to any available service and endpoint as condition. diff --git a/proto/zitadel/action/v3alpha/query.proto b/proto/zitadel/action/v3alpha/query.proto index a32caacfba..e32bda1d84 100644 --- a/proto/zitadel/action/v3alpha/query.proto +++ b/proto/zitadel/action/v3alpha/query.proto @@ -7,7 +7,7 @@ option go_package = "github.com/zitadel/zitadel/pkg/grpc/action/v3alpha;action"; import "google/api/field_behavior.proto"; import "protoc-gen-openapiv2/options/annotations.proto"; import "validate/validate.proto"; -import "zitadel/object/v2beta/object.proto"; +import "zitadel/object/v2/object.proto"; import "zitadel/action/v3alpha/execution.proto"; message SearchQuery { @@ -46,7 +46,7 @@ message IncludeQuery { Condition include = 1 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { description: "the id of the include" - example: "\"request.zitadel.session.v2beta.SessionService\""; + example: "\"request.zitadel.session.v2.SessionService\""; } ]; } @@ -70,7 +70,7 @@ message TargetNameQuery { } ]; // Defines which text comparison method used for the name query. - zitadel.object.v2beta.TextQueryMethod method = 2 [ + 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"; diff --git a/proto/zitadel/action/v3alpha/target.proto b/proto/zitadel/action/v3alpha/target.proto index 92dda32bbb..bea5a4b756 100644 --- a/proto/zitadel/action/v3alpha/target.proto +++ b/proto/zitadel/action/v3alpha/target.proto @@ -8,7 +8,7 @@ import "google/protobuf/duration.proto"; import "google/protobuf/struct.proto"; import "protoc-gen-openapiv2/options/annotations.proto"; import "validate/validate.proto"; -import "zitadel/object/v2beta/object.proto"; +import "zitadel/object/v2/object.proto"; import "zitadel/protoc_gen_zitadel/v2/options.proto"; option go_package = "github.com/zitadel/zitadel/pkg/grpc/action/v3alpha;action"; @@ -36,7 +36,7 @@ message Target { } ]; // Details provide some base information (such as the last change date) of the target. - zitadel.object.v2beta.Details details = 2; + zitadel.object.v2.Details details = 2; // Unique name of the target. string name = 3 [ diff --git a/proto/zitadel/management.proto b/proto/zitadel/management.proto index 64ad68c53a..e02e23da6b 100644 --- a/proto/zitadel/management.proto +++ b/proto/zitadel/management.proto @@ -1005,6 +1005,7 @@ service ManagementService { } // Deprecated: not used anymore in user state + // To resend a verification email use the user service v2 ResendEmailCode rpc ResendHumanInitialization(ResendHumanInitializationRequest) returns (ResendHumanInitializationResponse) { option (google.api.http) = { post: "/users/{user_id}/_resend_initialization" diff --git a/proto/zitadel/user/schema/v3alpha/user_schema.proto b/proto/zitadel/user/schema/v3alpha/user_schema.proto index c75d7d8194..e7a7a0737a 100644 --- a/proto/zitadel/user/schema/v3alpha/user_schema.proto +++ b/proto/zitadel/user/schema/v3alpha/user_schema.proto @@ -6,7 +6,7 @@ import "google/api/field_behavior.proto"; import "google/protobuf/struct.proto"; import "validate/validate.proto"; import "protoc-gen-openapiv2/options/annotations.proto"; -import "zitadel/object/v2beta/object.proto"; +import "zitadel/object/v2/object.proto"; option go_package = "github.com/zitadel/zitadel/pkg/grpc/user/schema/v3alpha"; @@ -19,7 +19,7 @@ message UserSchema { } ]; // Details provide some base information (such as the last change date) of the schema. - zitadel.object.v2beta.Details details = 2; + zitadel.object.v2.Details details = 2; // Type is a human readable text describing the schema. string type = 3 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -119,7 +119,7 @@ message IDQuery { } ]; // Defines which text comparison method used for the id query. - zitadel.object.v2beta.TextQueryMethod method = 2 [ + zitadel.object.v2.TextQueryMethod method = 2 [ (validate.rules).enum.defined_only = true ]; } @@ -135,7 +135,7 @@ message TypeQuery { } ]; // Defines which text comparison method used for the type query. - zitadel.object.v2beta.TextQueryMethod method = 2 [ + zitadel.object.v2.TextQueryMethod method = 2 [ (validate.rules).enum.defined_only = true ]; } diff --git a/proto/zitadel/user/schema/v3alpha/user_schema_service.proto b/proto/zitadel/user/schema/v3alpha/user_schema_service.proto index f9d2181dbf..14e59f1eab 100644 --- a/proto/zitadel/user/schema/v3alpha/user_schema_service.proto +++ b/proto/zitadel/user/schema/v3alpha/user_schema_service.proto @@ -8,7 +8,7 @@ import "google/protobuf/duration.proto"; import "google/protobuf/struct.proto"; import "protoc-gen-openapiv2/options/annotations.proto"; import "validate/validate.proto"; -import "zitadel/object/v2beta/object.proto"; +import "zitadel/object/v2/object.proto"; import "zitadel/protoc_gen_zitadel/v2/options.proto"; import "zitadel/user/schema/v3alpha/user_schema.proto"; @@ -299,7 +299,7 @@ service UserSchemaService { message ListUserSchemasRequest { // list limitations and ordering. - zitadel.object.v2beta.ListQuery query = 1; + zitadel.object.v2.ListQuery query = 1; // the field the result is sorted. zitadel.user.schema.v3alpha.FieldName sorting_column = 2 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -312,7 +312,7 @@ message ListUserSchemasRequest { message ListUserSchemasResponse { // Details provides information about the returned result including total amount found. - zitadel.object.v2beta.ListDetails details = 1; + zitadel.object.v2.ListDetails details = 1; // States by which field the results are sorted. zitadel.user.schema.v3alpha.FieldName sorting_column = 2; // The result contains the user schemas, which matched the queries. @@ -376,7 +376,7 @@ message CreateUserSchemaResponse { // ID is the read-only unique identifier of the schema. string id = 1; // Details provide some base information (such as the last change date) of the schema. - zitadel.object.v2beta.Details details = 2; + zitadel.object.v2.Details details = 2; } @@ -416,7 +416,7 @@ message UpdateUserSchemaRequest { message UpdateUserSchemaResponse { // Details provide some base information (such as the last change date) of the schema. - zitadel.object.v2beta.Details details = 1; + zitadel.object.v2.Details details = 1; } message DeactivateUserSchemaRequest { @@ -426,7 +426,7 @@ message DeactivateUserSchemaRequest { message DeactivateUserSchemaResponse { // Details provide some base information (such as the last change date) of the schema. - zitadel.object.v2beta.Details details = 1; + zitadel.object.v2.Details details = 1; } message ReactivateUserSchemaRequest { @@ -436,7 +436,7 @@ message ReactivateUserSchemaRequest { message ReactivateUserSchemaResponse { // Details provide some base information (such as the last change date) of the schema. - zitadel.object.v2beta.Details details = 1; + zitadel.object.v2.Details details = 1; } message DeleteUserSchemaRequest { @@ -446,7 +446,7 @@ message DeleteUserSchemaRequest { message DeleteUserSchemaResponse { // Details provide some base information (such as the last change date) of the schema. - zitadel.object.v2beta.Details details = 1; + zitadel.object.v2.Details details = 1; } diff --git a/statik/doc.go b/statik/doc.go index bbcf4923da..6cbe78ef8d 100644 --- a/statik/doc.go +++ b/statik/doc.go @@ -1,2 +1,2 @@ -//Package statik is needed for building migration files into binary +// Package statik is needed for building migration files into binary package statik From 3f77d87b52031eb5e5c8679db23aede19c2bedd5 Mon Sep 17 00:00:00 2001 From: Silvan Date: Mon, 29 Jul 2024 06:53:31 +0200 Subject: [PATCH 06/39] chore(stable): update to v2.52.2 (#8349) # Which Problems Are Solved Update stable release to next minor --- release-channels.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/release-channels.yaml b/release-channels.yaml index 102bfc9e81..4fd5200d60 100644 --- a/release-channels.yaml +++ b/release-channels.yaml @@ -1 +1 @@ -stable: "v2.51.4" +stable: "v2.52.2" From 51210c8e34580709b94c0d12d365b0512c4b10f1 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Tue, 30 Jul 2024 13:38:20 +0200 Subject: [PATCH 07/39] fix(console): fill cachedorgs when read from local storage (#8363) This fixes a problem where the org settings were hidden. The console reads the context from either a query param or the local storage. When one context was found, it executed a single request with orgId filter. This let to a single org and then to a hidden org setting, as we hide org settings for instances with a single result. --- console/src/app/services/grpc-auth.service.ts | 28 +++++++++++-------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/console/src/app/services/grpc-auth.service.ts b/console/src/app/services/grpc-auth.service.ts index c7e379c84f..24feee0ad1 100644 --- a/console/src/app/services/grpc-auth.service.ts +++ b/console/src/app/services/grpc-auth.service.ts @@ -261,27 +261,33 @@ export class GrpcAuthService { this.setActiveOrg(orgs[0]); return Promise.resolve(orgs[0]); } else { + // throw error if the org was specifically requested but not found return Promise.reject(new Error('requested organization not found')); } } } else { let orgs = this.cachedOrgs.getValue(); - const org = this.storage.getItem(StorageKey.organization, StorageLocation.local); if (org) { - const orgQuery = new OrgQuery(); - const orgIdQuery = new OrgIDQuery(); - orgIdQuery.setId(org.id); - orgQuery.setIdQuery(orgIdQuery); + orgs = (await this.listMyProjectOrgs(ORG_LIMIT, 0)).resultList; + this.cachedOrgs.next(orgs); - const specificOrg = (await this.listMyProjectOrgs(ORG_LIMIT, 0, [orgQuery])).resultList; - if (specificOrg.length === 1) { - this.setActiveOrg(specificOrg[0]); - return Promise.resolve(specificOrg[0]); + const find = this.cachedOrgs.getValue().find((tmp) => tmp.id === id); + if (find) { + this.setActiveOrg(find); + return Promise.resolve(find); } else { - orgs = (await this.listMyProjectOrgs(ORG_LIMIT, 0)).resultList; - this.cachedOrgs.next(orgs); + const orgQuery = new OrgQuery(); + const orgIdQuery = new OrgIDQuery(); + orgIdQuery.setId(org.id); + orgQuery.setIdQuery(orgIdQuery); + + const specificOrg = (await this.listMyProjectOrgs(ORG_LIMIT, 0, [orgQuery])).resultList; + if (specificOrg.length === 1) { + this.setActiveOrg(specificOrg[0]); + return Promise.resolve(specificOrg[0]); + } } } else { orgs = (await this.listMyProjectOrgs(ORG_LIMIT, 0)).resultList; From 918736c02681aaab96490a161e27b03f60d13f6f Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Tue, 30 Jul 2024 16:12:39 +0200 Subject: [PATCH 08/39] chore(console): upgrade dependencies (#8368) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- console/package.json | 20 +- .../modules/app-card/app-card.component.html | 2 +- .../app/modules/avatar/avatar.component.html | 2 +- .../display-json-dialog.component.html | 4 +- .../idp-table/idp-table.component.html | 8 +- .../modules/info-row/info-row.component.html | 8 +- .../info-section/info-section.component.html | 2 +- .../onboarding/onboarding.component.html | 4 +- .../org-context/org-context.component.html | 2 +- .../org-table/org-table.component.html | 2 +- .../domain-policy.component.html | 10 +- .../login-policy/login-policy.component.html | 30 +- .../notification-sms-provider.component.html | 2 +- .../private-labeling-policy.component.html | 38 +- .../project-members.component.html | 8 +- .../shortcuts/shortcuts.component.html | 8 +- .../smtp-table/smtp-table.component.html | 2 +- .../action-table/action-table.component.html | 2 +- .../app/pages/actions/actions.component.html | 2 +- .../add-action-dialog.component.html | 2 +- .../app/pages/grants/grants.component.html | 2 +- .../src/app/pages/home/home.component.html | 12 +- .../apps/app-create/app-create.component.html | 4 +- .../apps/app-detail/app-detail.component.html | 2 +- .../applications/applications.component.html | 2 +- .../project-grant-illustration.component.html | 2 +- .../project-grants.component.html | 2 +- .../project-grid/project-grid.component.html | 4 +- .../project-list/project-list.component.html | 2 +- .../auth-passwordless.component.html | 2 +- .../auth-user-mfa.component.html | 2 +- .../passwordless/passwordless.component.html | 2 +- .../user-mfa/user-mfa.component.html | 2 +- .../user-table/user-table.component.html | 2 +- console/yarn.lock | 5133 ++++++++--------- 35 files changed, 2533 insertions(+), 2800 deletions(-) diff --git a/console/package.json b/console/package.json index 70390d87e1..524ebe833f 100644 --- a/console/package.json +++ b/console/package.json @@ -28,8 +28,8 @@ "@fortawesome/angular-fontawesome": "^0.13.0", "@fortawesome/fontawesome-svg-core": "^6.4.2", "@fortawesome/free-brands-svg-icons": "^6.4.2", - "@grpc/grpc-js": "^1.9.3", - "@netlify/framework-info": "^9.8.10", + "@grpc/grpc-js": "^1.11.1", + "@netlify/framework-info": "^9.8.13", "@ngx-translate/core": "^15.0.0", "angular-oauth2-oidc": "^15.0.1", "angularx-qrcode": "^16.0.0", @@ -42,7 +42,7 @@ "google-protobuf": "^3.21.2", "grpc-web": "^1.4.1", "i18n-iso-countries": "^7.7.0", - "libphonenumber-js": "^1.10.49", + "libphonenumber-js": "^1.11.4", "material-design-icons-iconfont": "^6.1.1", "moment": "^2.29.4", "ngx-color": "^9.0.0", @@ -50,7 +50,7 @@ "rxjs": "~7.8.0", "tinycolor2": "^1.6.0", "tslib": "^2.6.2", - "uuid": "^9.0.1", + "uuid": "^10.0.0", "zone.js": "~0.13.3" }, "devDependencies": { @@ -60,24 +60,24 @@ "@angular-eslint/eslint-plugin-template": "16.2.0", "@angular-eslint/schematics": "16.2.0", "@angular-eslint/template-parser": "16.2.0", - "@angular/cli": "^16.2.2", + "@angular/cli": "^16.2.14", "@angular/compiler-cli": "^16.2.5", "@angular/language-service": "^16.2.5", - "@bufbuild/buf": "^1.23.1", + "@bufbuild/buf": "^1.34.0", "@types/file-saver": "^2.0.7", "@types/google-protobuf": "^3.15.3", "@types/jasmine": "~5.1.4", "@types/jasminewd2": "~2.0.13", - "@types/jsonwebtoken": "^9.0.5", + "@types/jsonwebtoken": "^9.0.6", "@types/node": "^20.7.0", "@types/opentype.js": "^1.3.8", "@types/qrcode": "^1.5.2", - "@types/uuid": "^9.0.7", - "@typescript-eslint/eslint-plugin": "^5.59.11", + "@types/uuid": "^10.0.0", + "@typescript-eslint/eslint-plugin": "^5.62.0", "@typescript-eslint/parser": "^5.60.1", "codelyzer": "^6.0.2", "eslint": "^8.50.0", - "jasmine-core": "~4.6.0", + "jasmine-core": "~5.2.0", "jasmine-spec-reporter": "~7.0.0", "karma": "^6.4.2", "karma-chrome-launcher": "^3.2.0", diff --git a/console/src/app/modules/app-card/app-card.component.html b/console/src/app/modules/app-card/app-card.component.html index a6cc480ac6..6840e4e28e 100644 --- a/console/src/app/modules/app-card/app-card.component.html +++ b/console/src/app/modules/app-card/app-card.component.html @@ -6,7 +6,7 @@ useragent: type === OIDCAppType.OIDC_APP_TYPE_USER_AGENT, native: type === OIDCAppType.OIDC_APP_TYPE_NATIVE, api: isApiApp, - saml: type === 'SAML' + saml: type === 'SAML', }" > diff --git a/console/src/app/modules/avatar/avatar.component.html b/console/src/app/modules/avatar/avatar.component.html index 7be0728c83..94461ab01f 100644 --- a/console/src/app/modules/avatar/avatar.component.html +++ b/console/src/app/modules/avatar/avatar.component.html @@ -9,7 +9,7 @@ fontSize: fontSize - 1 + 'px', fontWeight: fontWeight, background: (themeService.isDarkTheme | async) ? color[900] : color[300], - color: (themeService.isDarkTheme | async) ? color[200] : color[900] + color: (themeService.isDarkTheme | async) ? color[200] : color[900], }" [ngClass]="{ active: active }" > diff --git a/console/src/app/modules/display-json-dialog/display-json-dialog.component.html b/console/src/app/modules/display-json-dialog/display-json-dialog.component.html index 1e38fd5c52..24df49a959 100644 --- a/console/src/app/modules/display-json-dialog/display-json-dialog.component.html +++ b/console/src/app/modules/display-json-dialog/display-json-dialog.component.html @@ -66,8 +66,8 @@ mode: { name: 'javascript', json: true, - statementIndent: 2 - } + statementIndent: 2, + }, }" > diff --git a/console/src/app/modules/idp-table/idp-table.component.html b/console/src/app/modules/idp-table/idp-table.component.html index db4f04216b..7188040897 100644 --- a/console/src/app/modules/idp-table/idp-table.component.html +++ b/console/src/app/modules/idp-table/idp-table.component.html @@ -101,7 +101,7 @@ class="state" [ngClass]="{ active: idp.state === IDPState.IDP_STATE_ACTIVE, - inactive: idp.state === IDPState.IDP_STATE_INACTIVE + inactive: idp.state === IDPState.IDP_STATE_INACTIVE, }" >{{ 'IDP.STATES.' + idp.state | translate }} @@ -142,7 +142,7 @@ ? 'iam.idp.write' : serviceType === PolicyComponentServiceType.MGMT ? 'org.idp.write' - : '' + : '', ] | hasRole | async) === false @@ -162,7 +162,7 @@ ? 'iam.idp.write' : serviceType === PolicyComponentServiceType.MGMT ? 'org.idp.write' - : '' + : '', ] | hasRole | async) === false @@ -186,7 +186,7 @@ ? 'iam.idp.write' : serviceType === PolicyComponentServiceType.MGMT ? 'org.idp.write' - : '' + : '', ] | hasRole | async) === false diff --git a/console/src/app/modules/info-row/info-row.component.html b/console/src/app/modules/info-row/info-row.component.html index 6824d2e2d1..db68931eeb 100644 --- a/console/src/app/modules/info-row/info-row.component.html +++ b/console/src/app/modules/info-row/info-row.component.html @@ -6,7 +6,7 @@ class="state" [ngClass]="{ active: user.state === UserState.USER_STATE_ACTIVE, - inactive: user.state === UserState.USER_STATE_INACTIVE + inactive: user.state === UserState.USER_STATE_INACTIVE, }" > {{ 'USER.DATA.STATE' + user.state | translate }} @@ -57,7 +57,7 @@ class="state" [ngClass]="{ active: instance.state === State.INSTANCE_STATE_RUNNING, - inactive: instance.state === State.INSTANCE_STATE_STOPPED || instance.state === State.INSTANCE_STATE_STOPPING + inactive: instance.state === State.INSTANCE_STATE_STOPPED || instance.state === State.INSTANCE_STATE_STOPPING, }" > {{ 'IAM.STATE.' + instance.state | translate }} @@ -164,7 +164,7 @@ class="state" [ngClass]="{ active: project.state === ProjectState.PROJECT_STATE_ACTIVE, - inactive: project.state === ProjectState.PROJECT_STATE_INACTIVE + inactive: project.state === ProjectState.PROJECT_STATE_INACTIVE, }" > {{ 'PROJECT.STATE.' + project.state | translate }} @@ -199,7 +199,7 @@ class="state" [ngClass]="{ active: grantedProject.state === ProjectGrantState.PROJECT_GRANT_STATE_ACTIVE, - inactive: grantedProject.state === ProjectGrantState.PROJECT_GRANT_STATE_INACTIVE + inactive: grantedProject.state === ProjectGrantState.PROJECT_GRANT_STATE_INACTIVE, }" > {{ 'PROJECT.STATE.' + grantedProject.state | translate }} diff --git a/console/src/app/modules/info-section/info-section.component.html b/console/src/app/modules/info-section/info-section.component.html index 277944ba51..b27fdea148 100644 --- a/console/src/app/modules/info-section/info-section.component.html +++ b/console/src/app/modules/info-section/info-section.component.html @@ -5,7 +5,7 @@ warn: type === 'WARN', alert: type === 'ALERT', success: type === 'SUCCESS', - fit: fitWidth + fit: fitWidth, }" > diff --git a/console/src/app/modules/onboarding/onboarding.component.html b/console/src/app/modules/onboarding/onboarding.component.html index 60398abb74..ea89494051 100644 --- a/console/src/app/modules/onboarding/onboarding.component.html +++ b/console/src/app/modules/onboarding/onboarding.component.html @@ -64,14 +64,14 @@
diff --git a/console/src/app/modules/org-context/org-context.component.html b/console/src/app/modules/org-context/org-context.component.html index 154cb0dbd7..9c0bf6b1e4 100644 --- a/console/src/app/modules/org-context/org-context.component.html +++ b/console/src/app/modules/org-context/org-context.component.html @@ -21,7 +21,7 @@ mat-button [ngClass]="{ active: pinnedorg.id === org.id, - 'border-bottom': pinned.selected.length && i === pinned.selected.length - 1 + 'border-bottom': pinned.selected.length && i === pinned.selected.length - 1, }" [disabled]="!pinnedorg.id" *ngFor="let pinnedorg of pinned.selected; index as i" diff --git a/console/src/app/modules/org-table/org-table.component.html b/console/src/app/modules/org-table/org-table.component.html index b16310d1d8..042988054b 100644 --- a/console/src/app/modules/org-table/org-table.component.html +++ b/console/src/app/modules/org-table/org-table.component.html @@ -70,7 +70,7 @@ class="state" [ngClass]="{ active: org.state === OrgState.ORG_STATE_ACTIVE, - inactive: org.state === OrgState.ORG_STATE_INACTIVE + inactive: org.state === OrgState.ORG_STATE_INACTIVE, }" *ngIf="org.state" >{{ 'ORG.STATE.' + org.state | translate }}{{ 'SETTING.SMS.SMSPROVIDERSTATE.' + twilio.state | translate }} diff --git a/console/src/app/modules/policies/private-labeling-policy/private-labeling-policy.component.html b/console/src/app/modules/policies/private-labeling-policy/private-labeling-policy.component.html index 5f78230409..13c154b1cf 100644 --- a/console/src/app/modules/policies/private-labeling-policy/private-labeling-policy.component.html +++ b/console/src/app/modules/policies/private-labeling-policy/private-labeling-policy.component.html @@ -48,7 +48,7 @@ ? 'iam.policy.write' : serviceType === PolicyComponentServiceType.MGMT ? 'policy.write' - : '' + : '', ] | hasRole | async) === false @@ -152,7 +152,7 @@ ? 'iam.policy.delete' : serviceType === PolicyComponentServiceType.MGMT ? 'policy.delete' - : '' + : '', ] | hasRole | async) === false @@ -176,7 +176,7 @@ ? 'iam.policy.write' : serviceType === PolicyComponentServiceType.MGMT ? 'policy.write' - : '' + : '', ] | hasRole | async) === false @@ -243,7 +243,7 @@ ? 'iam.policy.write' : serviceType === PolicyComponentServiceType.MGMT ? 'policy.write' - : '' + : '', ] | hasRole | async) === false @@ -273,7 +273,7 @@ ? 'iam.policy.write' : serviceType === PolicyComponentServiceType.MGMT ? 'policy.write' - : '' + : '', ] | hasRole | async) === false @@ -314,7 +314,7 @@ ? 'iam.policy.write' : serviceType === PolicyComponentServiceType.MGMT ? 'policy.write' - : '' + : '', ] | hasRole | async) === false @@ -344,7 +344,7 @@ ? 'iam.policy.write' : serviceType === PolicyComponentServiceType.MGMT ? 'policy.write' - : '' + : '', ] | hasRole | async) === false @@ -387,7 +387,7 @@ ? 'iam.policy.write' : serviceType === PolicyComponentServiceType.MGMT ? 'policy.write' - : '' + : '', ] | hasRole | async) === false @@ -409,7 +409,7 @@ ? 'iam.policy.write' : serviceType === PolicyComponentServiceType.MGMT ? 'policy.write' - : '' + : '', ] | hasRole | async) === false @@ -432,7 +432,7 @@ ? 'iam.policy.write' : serviceType === PolicyComponentServiceType.MGMT ? 'policy.write' - : '' + : '', ] | hasRole | async) === false @@ -455,7 +455,7 @@ ? 'iam.policy.write' : serviceType === PolicyComponentServiceType.MGMT ? 'policy.write' - : '' + : '', ] | hasRole | async) === false @@ -482,7 +482,7 @@ ? 'iam.policy.write' : serviceType === PolicyComponentServiceType.MGMT ? 'policy.write' - : '' + : '', ] | hasRole | async) === false @@ -504,7 +504,7 @@ ? 'iam.policy.write' : serviceType === PolicyComponentServiceType.MGMT ? 'policy.write' - : '' + : '', ] | hasRole | async) === false @@ -527,7 +527,7 @@ ? 'iam.policy.write' : serviceType === PolicyComponentServiceType.MGMT ? 'policy.write' - : '' + : '', ] | hasRole | async) === false @@ -549,7 +549,7 @@ ? 'iam.policy.write' : serviceType === PolicyComponentServiceType.MGMT ? 'policy.write' - : '' + : '', ] | hasRole | async) === false @@ -594,7 +594,7 @@ ? 'iam.policy.write' : serviceType === PolicyComponentServiceType.MGMT ? 'policy.write' - : '' + : '', ] | hasRole | async) === false @@ -632,7 +632,7 @@ ? 'iam.policy.write' : serviceType === PolicyComponentServiceType.MGMT ? 'policy.write' - : '' + : '', ] | hasRole | async) === false @@ -671,7 +671,7 @@ ? 'iam.policy.write' : serviceType === PolicyComponentServiceType.MGMT ? 'policy.write' - : '' + : '', ] | hasRole | async) === false @@ -699,7 +699,7 @@ ? 'iam.policy.write' : serviceType === PolicyComponentServiceType.MGMT ? 'policy.write' - : '' + : '', ] | hasRole | async) === false diff --git a/console/src/app/modules/project-members/project-members.component.html b/console/src/app/modules/project-members/project-members.component.html index 4be94376c1..9f42d32c37 100644 --- a/console/src/app/modules/project-members/project-members.component.html +++ b/console/src/app/modules/project-members/project-members.component.html @@ -24,7 +24,7 @@ ? $any(project)?.id : projectType === ProjectType.PROJECTTYPE_GRANTED ? $any(project)?.projectId - : '' + : '', ] | hasRole | async @@ -36,7 +36,7 @@ ? $any(project)?.id : projectType === ProjectType.PROJECTTYPE_GRANTED ? $any(project)?.projectId - : '' + : '', ] | hasRole | async @@ -52,7 +52,7 @@ : projectType === ProjectType.PROJECTTYPE_GRANTED ? $any(project)?.projectId : '', - 'project.member.delete' + 'project.member.delete', ]" >
@@ -85,7 +85,7 @@ matTooltip="{{ 'PROJECT.STATE.' + shortcut.state | translate }}" [ngClass]="{ active: shortcut.state === ProjectState.PROJECT_STATE_ACTIVE, - inactive: shortcut.state === ProjectState.PROJECT_STATE_INACTIVE + inactive: shortcut.state === ProjectState.PROJECT_STATE_INACTIVE, }" >
@@ -124,7 +124,7 @@ matTooltip="{{ 'PROJECT.STATE.' + shortcut.state | translate }}" [ngClass]="{ active: shortcut.state === ProjectState.PROJECT_STATE_ACTIVE, - inactive: shortcut.state === ProjectState.PROJECT_STATE_INACTIVE + inactive: shortcut.state === ProjectState.PROJECT_STATE_INACTIVE, }" > @@ -174,7 +174,7 @@ matTooltip="{{ 'PROJECT.STATE.' + shortcut.state | translate }}" [ngClass]="{ active: shortcut.state === ProjectState.PROJECT_STATE_ACTIVE, - inactive: shortcut.state === ProjectState.PROJECT_STATE_INACTIVE + inactive: shortcut.state === ProjectState.PROJECT_STATE_INACTIVE, }" > diff --git a/console/src/app/modules/smtp-table/smtp-table.component.html b/console/src/app/modules/smtp-table/smtp-table.component.html index 91d244f8b1..3359eda596 100644 --- a/console/src/app/modules/smtp-table/smtp-table.component.html +++ b/console/src/app/modules/smtp-table/smtp-table.component.html @@ -12,7 +12,7 @@ '/instance/smtpprovider/sendgrid/create', '/instance/smtpprovider/mailchimp/create', '/instance/smtpprovider/brevo/create', - '/instance/smtpprovider/outlook/create' + '/instance/smtpprovider/outlook/create', ]" [timestamp]="configsResult?.details?.viewTimestamp" [selection]="selection" diff --git a/console/src/app/pages/actions/action-table/action-table.component.html b/console/src/app/pages/actions/action-table/action-table.component.html index 1f68faa6f4..a54a97379c 100644 --- a/console/src/app/pages/actions/action-table/action-table.component.html +++ b/console/src/app/pages/actions/action-table/action-table.component.html @@ -71,7 +71,7 @@ class="state" [ngClass]="{ active: action.state === ActionState.ACTION_STATE_ACTIVE, - inactive: action.state === ActionState.ACTION_STATE_INACTIVE + inactive: action.state === ActionState.ACTION_STATE_INACTIVE, }" > {{ 'FLOWS.STATES.' + action.state | translate }} {{ 'FLOWS.STATES.' + action.state | translate }} diff --git a/console/src/app/pages/grants/grants.component.html b/console/src/app/pages/grants/grants.component.html index 03a5c2ca90..5eef251b5f 100644 --- a/console/src/app/pages/grants/grants.component.html +++ b/console/src/app/pages/grants/grants.component.html @@ -18,7 +18,7 @@ 'creationDate', 'changeDate', 'roleNamesList', - 'actions' + 'actions', ]" [disableWrite]="(['user.grant.write$'] | hasRole | async) === false" [disableDelete]="(['user.grant.delete$'] | hasRole | async) === false" diff --git a/console/src/app/pages/home/home.component.html b/console/src/app/pages/home/home.component.html index 6eee775556..c5c3485ea9 100644 --- a/console/src/app/pages/home/home.component.html +++ b/console/src/app/pages/home/home.component.html @@ -21,14 +21,14 @@
@@ -45,14 +45,14 @@
@@ -69,14 +69,14 @@
diff --git a/console/src/app/pages/projects/apps/app-create/app-create.component.html b/console/src/app/pages/projects/apps/app-create/app-create.component.html index af6a5328ed..2e6787b036 100644 --- a/console/src/app/pages/projects/apps/app-create/app-create.component.html +++ b/console/src/app/pages/projects/apps/app-create/app-create.component.html @@ -233,7 +233,7 @@ [options]="{ lineNumbers: true, theme: 'material', - mode: 'application/xml' + mode: 'application/xml', }" >
@@ -524,7 +524,7 @@ [options]="{ lineNumbers: true, theme: 'material', - mode: 'application/xml' + mode: 'application/xml', }" >
diff --git a/console/src/app/pages/projects/apps/app-detail/app-detail.component.html b/console/src/app/pages/projects/apps/app-detail/app-detail.component.html index 0f795200b1..c034af991e 100644 --- a/console/src/app/pages/projects/apps/app-detail/app-detail.component.html +++ b/console/src/app/pages/projects/apps/app-detail/app-detail.component.html @@ -218,7 +218,7 @@ [options]="{ lineNumbers: true, theme: 'material', - mode: 'application/xml' + mode: 'application/xml', }" >
diff --git a/console/src/app/pages/projects/owned-projects/owned-project-detail/applications/applications.component.html b/console/src/app/pages/projects/owned-projects/owned-project-detail/applications/applications.component.html index 1feee4cc93..e8d93a6654 100644 --- a/console/src/app/pages/projects/owned-projects/owned-project-detail/applications/applications.component.html +++ b/console/src/app/pages/projects/owned-projects/owned-project-detail/applications/applications.component.html @@ -65,7 +65,7 @@ class="state" [ngClass]="{ active: app.state === AppState.APP_STATE_ACTIVE, - inactive: app.state === AppState.APP_STATE_INACTIVE + inactive: app.state === AppState.APP_STATE_INACTIVE, }" > {{ 'APP.PAGES.DETAIL.STATE.' + app?.state | translate }} diff --git a/console/src/app/pages/projects/owned-projects/project-grant-detail/project-grant-illustration/project-grant-illustration.component.html b/console/src/app/pages/projects/owned-projects/project-grant-detail/project-grant-illustration/project-grant-illustration.component.html index fa1829ab45..7966dc21f2 100644 --- a/console/src/app/pages/projects/owned-projects/project-grant-detail/project-grant-illustration/project-grant-illustration.component.html +++ b/console/src/app/pages/projects/owned-projects/project-grant-detail/project-grant-illustration/project-grant-illustration.component.html @@ -30,7 +30,7 @@ matTooltip="{{ 'PROJECT.STATE.' + grantedProject.state | translate }}" [ngClass]="{ active: grantedProject.state === ProjectGrantState.PROJECT_GRANT_STATE_ACTIVE, - inactive: grantedProject.state === ProjectGrantState.PROJECT_GRANT_STATE_INACTIVE + inactive: grantedProject.state === ProjectGrantState.PROJECT_GRANT_STATE_INACTIVE, }" >
diff --git a/console/src/app/pages/projects/owned-projects/project-grants/project-grants.component.html b/console/src/app/pages/projects/owned-projects/project-grants/project-grants.component.html index 92d4459525..07d5baaa8c 100644 --- a/console/src/app/pages/projects/owned-projects/project-grants/project-grants.component.html +++ b/console/src/app/pages/projects/owned-projects/project-grants/project-grants.component.html @@ -104,7 +104,7 @@ class="state" [ngClass]="{ active: grant.state === ProjectGrantState.PROJECT_GRANT_STATE_ACTIVE, - inactive: grant.state === ProjectGrantState.PROJECT_GRANT_STATE_INACTIVE + inactive: grant.state === ProjectGrantState.PROJECT_GRANT_STATE_INACTIVE, }" > {{ 'PROJECT.GRANT.STATES.' + grant.state | translate }} diff --git a/console/src/app/pages/projects/project-grid/project-grid.component.html b/console/src/app/pages/projects/project-grid/project-grid.component.html index df618192ed..9cbc1f9e96 100644 --- a/console/src/app/pages/projects/project-grid/project-grid.component.html +++ b/console/src/app/pages/projects/project-grid/project-grid.component.html @@ -24,7 +24,7 @@ class="state-dot" [ngClass]="{ active: item.state === ProjectState.PROJECT_STATE_ACTIVE, - inactive: item.state === ProjectState.PROJECT_STATE_INACTIVE + inactive: item.state === ProjectState.PROJECT_STATE_INACTIVE, }" >
@@ -62,7 +62,7 @@ class="state-dot" [ngClass]="{ active: item.state === ProjectState.PROJECT_STATE_ACTIVE, - inactive: item.state === ProjectState.PROJECT_STATE_INACTIVE + inactive: item.state === ProjectState.PROJECT_STATE_INACTIVE, }" > diff --git a/console/src/app/pages/projects/project-list/project-list.component.html b/console/src/app/pages/projects/project-list/project-list.component.html index 77c340b1cd..d51d9f9ba2 100644 --- a/console/src/app/pages/projects/project-list/project-list.component.html +++ b/console/src/app/pages/projects/project-list/project-list.component.html @@ -95,7 +95,7 @@ class="state" [ngClass]="{ active: project.state === ProjectState.PROJECT_STATE_ACTIVE, - inactive: project.state === ProjectState.PROJECT_STATE_INACTIVE + inactive: project.state === ProjectState.PROJECT_STATE_INACTIVE, }" *ngIf="project.state" >{{ 'PROJECT.STATE.' + project.state | translate }}{{ 'USER.PASSWORDLESS.STATE.' + mfa.state | translate }} diff --git a/console/src/app/pages/users/user-detail/auth-user-detail/auth-user-mfa/auth-user-mfa.component.html b/console/src/app/pages/users/user-detail/auth-user-detail/auth-user-mfa/auth-user-mfa.component.html index 3e1705f81a..04cc701c08 100644 --- a/console/src/app/pages/users/user-detail/auth-user-detail/auth-user-mfa/auth-user-mfa.component.html +++ b/console/src/app/pages/users/user-detail/auth-user-detail/auth-user-mfa/auth-user-mfa.component.html @@ -57,7 +57,7 @@ class="state" [ngClass]="{ active: mfa.state === AuthFactorState.AUTH_FACTOR_STATE_READY, - inactive: mfa.state === AuthFactorState.AUTH_FACTOR_STATE_NOT_READY + inactive: mfa.state === AuthFactorState.AUTH_FACTOR_STATE_NOT_READY, }" >{{ 'USER.MFA.STATE.' + mfa.state | translate }} diff --git a/console/src/app/pages/users/user-detail/user-detail/passwordless/passwordless.component.html b/console/src/app/pages/users/user-detail/user-detail/passwordless/passwordless.component.html index 3e46560d0e..6726f98f98 100644 --- a/console/src/app/pages/users/user-detail/user-detail/passwordless/passwordless.component.html +++ b/console/src/app/pages/users/user-detail/user-detail/passwordless/passwordless.component.html @@ -43,7 +43,7 @@ class="state" [ngClass]="{ active: mfa.state === AuthFactorState.AUTH_FACTOR_STATE_READY, - inactive: mfa.state === AuthFactorState.AUTH_FACTOR_STATE_NOT_READY + inactive: mfa.state === AuthFactorState.AUTH_FACTOR_STATE_NOT_READY, }" >{{ 'USER.PASSWORDLESS.STATE.' + mfa.state | translate }} diff --git a/console/src/app/pages/users/user-detail/user-detail/user-mfa/user-mfa.component.html b/console/src/app/pages/users/user-detail/user-detail/user-mfa/user-mfa.component.html index 0369be9356..a6d0bf652f 100644 --- a/console/src/app/pages/users/user-detail/user-detail/user-mfa/user-mfa.component.html +++ b/console/src/app/pages/users/user-detail/user-detail/user-mfa/user-mfa.component.html @@ -41,7 +41,7 @@ class="state" [ngClass]="{ active: mfa.state === AuthFactorState.AUTH_FACTOR_STATE_READY, - inactive: mfa.state === AuthFactorState.AUTH_FACTOR_STATE_NOT_READY + inactive: mfa.state === AuthFactorState.AUTH_FACTOR_STATE_NOT_READY, }" > {{ 'USER.MFA.STATE.' + mfa.state | translate }} diff --git a/console/src/app/pages/users/user-list/user-table/user-table.component.html b/console/src/app/pages/users/user-list/user-table/user-table.component.html index 3be48e7fab..d7bfecb8ad 100644 --- a/console/src/app/pages/users/user-list/user-table/user-table.component.html +++ b/console/src/app/pages/users/user-list/user-table/user-table.component.html @@ -194,7 +194,7 @@ class="state" [ngClass]="{ active: user.state === UserState.USER_STATE_ACTIVE, - inactive: user.state === UserState.USER_STATE_INACTIVE + inactive: user.state === UserState.USER_STATE_INACTIVE, }" > {{ 'USER.DATA.STATE' + user.state | translate }} diff --git a/console/yarn.lock b/console/yarn.lock index de6a28e268..9ed63fac2b 100644 --- a/console/yarn.lock +++ b/console/yarn.lock @@ -2,36 +2,39 @@ # yarn lockfile v1 -"@aashutoshrathi/word-wrap@^1.2.3": - version "1.2.6" - resolved "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz" - integrity sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA== - -"@ampproject/remapping@2.2.1", "@ampproject/remapping@^2.2.0": +"@ampproject/remapping@2.2.1": version "2.2.1" - resolved "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz" + resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.1.tgz#99e8e11851128b8702cd57c33684f1d0f260b630" integrity sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg== dependencies: "@jridgewell/gen-mapping" "^0.3.0" "@jridgewell/trace-mapping" "^0.3.9" -"@angular-devkit/architect@0.1602.2": - version "0.1602.2" - resolved "https://registry.yarnpkg.com/@angular-devkit/architect/-/architect-0.1602.2.tgz#03e9f562d559f88e6ec315f81ed9c363e69194dd" - integrity sha512-JFIeKKW7V2+/C8+pTReM6gfQkVU9l1IR1OCb9vvHWTRvuTr7E5h2L1rUInnmLiRWkEvkYfG29B+UPpYlkVl9oQ== +"@ampproject/remapping@^2.2.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.3.0.tgz#ed441b6fa600072520ce18b43d2c8cc8caecc7f4" + integrity sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw== dependencies: - "@angular-devkit/core" "16.2.2" + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.24" + +"@angular-devkit/architect@0.1602.14": + version "0.1602.14" + resolved "https://registry.yarnpkg.com/@angular-devkit/architect/-/architect-0.1602.14.tgz#fa8551f03432a767aa64c5802ef20f8affae99bd" + integrity sha512-eSdONEV5dbtLNiOMBy9Ue9DdJ1ct6dH9RdZfYiedq6VZn0lejePAjY36MYVXgq2jTE+v/uIiaNy7caea5pt55A== + dependencies: + "@angular-devkit/core" "16.2.14" rxjs "7.8.1" "@angular-devkit/build-angular@^16.2.2": - version "16.2.2" - resolved "https://registry.yarnpkg.com/@angular-devkit/build-angular/-/build-angular-16.2.2.tgz#5a88d701abed34c49121aeb9445883c65b588364" - integrity sha512-j2lni4mN6NaMLT85sJUPSz/pNuaTCAYG3EYUeuMRNkC5keH/f4W0Tiuq6DxY4OLEF1JnEnfkp+k0Z84mEti/xA== + version "16.2.14" + resolved "https://registry.yarnpkg.com/@angular-devkit/build-angular/-/build-angular-16.2.14.tgz#0c4e41aa3f67e52b474b2fabeb027aebf6e76566" + integrity sha512-bXQ6i7QPhwmYHuh+DSNkBhjTIHQF0C6fqZEg2ApJA3NmnzE98oQnmJ9AnGnAkdf1Mjn3xi2gxoZWPDDxGEINMw== dependencies: "@ampproject/remapping" "2.2.1" - "@angular-devkit/architect" "0.1602.2" - "@angular-devkit/build-webpack" "0.1602.2" - "@angular-devkit/core" "16.2.2" + "@angular-devkit/architect" "0.1602.14" + "@angular-devkit/build-webpack" "0.1602.14" + "@angular-devkit/core" "16.2.14" "@babel/core" "7.22.9" "@babel/generator" "7.22.9" "@babel/helper-annotate-as-pure" "7.22.5" @@ -43,7 +46,7 @@ "@babel/runtime" "7.22.6" "@babel/template" "7.22.5" "@discoveryjs/json-ext" "0.5.7" - "@ngtools/webpack" "16.2.2" + "@ngtools/webpack" "16.2.14" "@vitejs/plugin-basic-ssl" "1.0.1" ansi-colors "4.1.3" autoprefixer "10.4.14" @@ -73,7 +76,7 @@ parse5-html-rewriting-stream "7.0.0" picomatch "2.3.1" piscina "4.0.0" - postcss "8.4.27" + postcss "8.4.31" postcss-loader "7.3.3" resolve-url-loader "5.0.0" rxjs "7.8.1" @@ -86,27 +89,27 @@ text-table "0.2.0" tree-kill "1.2.2" tslib "2.6.1" - vite "4.4.7" + vite "4.5.3" webpack "5.88.2" - webpack-dev-middleware "6.1.1" + webpack-dev-middleware "6.1.2" webpack-dev-server "4.15.1" webpack-merge "5.9.0" webpack-subresource-integrity "5.1.0" optionalDependencies: esbuild "0.18.17" -"@angular-devkit/build-webpack@0.1602.2": - version "0.1602.2" - resolved "https://registry.yarnpkg.com/@angular-devkit/build-webpack/-/build-webpack-0.1602.2.tgz#2b3813d31a91b1d457f01f0aadf3d7d872df8648" - integrity sha512-V9+tsBgNrXJPeabq9vJzN3Cfz9joaNOxs6l6M4XItcMGmAtzvxxGZ7qS5uRH1RE+SOMpYyh9uPY4QMHRNRD/gA== +"@angular-devkit/build-webpack@0.1602.14": + version "0.1602.14" + resolved "https://registry.yarnpkg.com/@angular-devkit/build-webpack/-/build-webpack-0.1602.14.tgz#754fc15837a4a12875ee338184867b6220416912" + integrity sha512-f+ZTCjOoA1SCQEaX3L/63ubqr/vlHkwDXAtKjBsQgyz6srnETcjy96Us5k/LoK7/hPc85zFneqLinfqOMVWHJQ== dependencies: - "@angular-devkit/architect" "0.1602.2" + "@angular-devkit/architect" "0.1602.14" rxjs "7.8.1" -"@angular-devkit/core@16.2.2": - version "16.2.2" - resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-16.2.2.tgz#de9e8572cf5cc3d81106cc0aec21a51ee8e5a093" - integrity sha512-6H4FsvP3rLJaGiWpIhCFPS7ZeNoM4sSrnFtRhhecu6s7MidzE4aqzuGdzJpzLammw1KL+DuTlN0gpLtM1Bvcwg== +"@angular-devkit/core@16.2.14": + version "16.2.14" + resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-16.2.14.tgz#63e4651f655571c94508dd30e4a3ea0832d579ad" + integrity sha512-Ui14/d2+p7lnmXlK/AX2ieQEGInBV75lonNtPQgwrYgskF8ufCuN0DyVZQUy9fJDkC+xQxbJyYrby/BS0R0e7w== dependencies: ajv "8.12.0" ajv-formats "2.1.1" @@ -115,12 +118,12 @@ rxjs "7.8.1" source-map "0.7.4" -"@angular-devkit/schematics@16.2.2": - version "16.2.2" - resolved "https://registry.yarnpkg.com/@angular-devkit/schematics/-/schematics-16.2.2.tgz#a67128ce5239f0f95e1cb596e9e8de511e2d8cf6" - integrity sha512-KeXIlibVrQEwIKbR9GViLKc3m1SXi/xuSXgIvSv+22FNu5i91ScsAhYLe65sDUL6m6MM1XQQMS46XN1Z9bRqQw== +"@angular-devkit/schematics@16.2.14": + version "16.2.14" + resolved "https://registry.yarnpkg.com/@angular-devkit/schematics/-/schematics-16.2.14.tgz#819c2ef8bb298e383cb312d9d1411f5970f0328f" + integrity sha512-B6LQKInCT8w5zx5Pbroext5eFFRTCJdTwHN8GhcVS8IeKCnkeqVTQLjB4lBUg7LEm8Y7UHXwzrVxmk+f+MBXhw== dependencies: - "@angular-devkit/core" "16.2.2" + "@angular-devkit/core" "16.2.14" jsonc-parser "3.2.0" magic-string "0.30.1" ora "5.4.1" @@ -189,30 +192,30 @@ "@typescript-eslint/utils" "5.62.0" "@angular/animations@^16.2.5": - version "16.2.5" - resolved "https://registry.yarnpkg.com/@angular/animations/-/animations-16.2.5.tgz#f10acb403830cd930bf4e3efd894b0fc30fb058e" - integrity sha512-2reD50S9zWvhewRvwl320iuRICN9s0fI+3nKULlwcyJ0praLRhJ1SnaAK3NEEu7MWo3n9sb3iVTzA6S9qZRJ4g== + version "16.2.12" + resolved "https://registry.yarnpkg.com/@angular/animations/-/animations-16.2.12.tgz#27744d8176e09e70e0f6d837c3abcfcee843a936" + integrity sha512-MD0ElviEfAJY8qMOd6/jjSSvtqER2RDAi0lxe6EtUacC1DHCYkaPrKW4vLqY+tmZBg1yf+6n+uS77pXcHHcA3w== dependencies: tslib "^2.3.0" "@angular/cdk@^16.2.4": - version "16.2.4" - resolved "https://registry.yarnpkg.com/@angular/cdk/-/cdk-16.2.4.tgz#dded7507e12f292fcf9bb04427b1dfe7545d59be" - integrity sha512-Hnh7Gs+gAkBnRYIMkDXRElEPAmBFas37isIfOtiqEmkgmSPFxsPpDOXK1soXeDk8U+yNmDWnO0fcHPp/pobHCw== + version "16.2.14" + resolved "https://registry.yarnpkg.com/@angular/cdk/-/cdk-16.2.14.tgz#d26f8f1e7d2466b509e60489b6acf31bfe923acf" + integrity sha512-n6PrGdiVeSTEmM/HEiwIyg6YQUUymZrb5afaNLGFRM5YL0Y8OBqd+XhCjb0OfD/AfgCUtedVEPwNqrfW8KzgGw== dependencies: tslib "^2.3.0" optionalDependencies: parse5 "^7.1.2" -"@angular/cli@^16.2.2": - version "16.2.2" - resolved "https://registry.yarnpkg.com/@angular/cli/-/cli-16.2.2.tgz#b07e47a0864f360d0fd7e8199d8881eaff5e7671" - integrity sha512-PmhR/NMVVCiATXxHLkVCV781Q5aa5DaYye9+plZGX3rdKTilEunRNIfT13w7IuRfa0K/pKZj6PJU1S6yb7sqZg== +"@angular/cli@^16.2.14": + version "16.2.14" + resolved "https://registry.yarnpkg.com/@angular/cli/-/cli-16.2.14.tgz#ab58910ae354ee31b89a7479efd5978fd1a3042e" + integrity sha512-0y71jtitigVolm4Rim1b8xPQ+B22cGp4Spef2Wunpqj67UowN6tsZaVuWBEQh4u5xauX8LAHKqsvy37ZPWCc4A== dependencies: - "@angular-devkit/architect" "0.1602.2" - "@angular-devkit/core" "16.2.2" - "@angular-devkit/schematics" "16.2.2" - "@schematics/angular" "16.2.2" + "@angular-devkit/architect" "0.1602.14" + "@angular-devkit/core" "16.2.14" + "@angular-devkit/schematics" "16.2.14" + "@schematics/angular" "16.2.14" "@yarnpkg/lockfile" "1.1.0" ansi-colors "4.1.3" ini "4.1.1" @@ -229,18 +232,18 @@ yargs "17.7.2" "@angular/common@^16.2.5": - version "16.2.5" - resolved "https://registry.yarnpkg.com/@angular/common/-/common-16.2.5.tgz#cd79a4b54f988933d686f4f940636ac98f5a1f28" - integrity sha512-MCPSZfPXTEqdkswPczivwjqV117YeVjObtyxZsDAwrTZHzYBtfQreQG1XJ1IRRgDncznP6ke0mdH9LyD2LgZKQ== + version "16.2.12" + resolved "https://registry.yarnpkg.com/@angular/common/-/common-16.2.12.tgz#aa1d1522701833f1998001caa1ac95c3ac11d077" + integrity sha512-B+WY/cT2VgEaz9HfJitBmgdk4I333XG/ybC98CMC4Wz8E49T8yzivmmxXB3OD6qvjcOB6ftuicl6WBqLbZNg2w== dependencies: tslib "^2.3.0" "@angular/compiler-cli@^16.2.5": - version "16.2.5" - resolved "https://registry.yarnpkg.com/@angular/compiler-cli/-/compiler-cli-16.2.5.tgz#d1fca488e1c4f85c178cb1a4e1c4e92bbb02a579" - integrity sha512-6TtyFxro4iukVXhLlzxz7sVCMfAlNQhSYnizIJRSW31uQ0Uku8rjlUmX1tCAmhW6CacLumiz2tcy04Xn/QFWyw== + version "16.2.12" + resolved "https://registry.yarnpkg.com/@angular/compiler-cli/-/compiler-cli-16.2.12.tgz#e24b4bdaf23047b23d7b39e295b7d25b38c5734c" + integrity sha512-pWSrr152562ujh6lsFZR8NfNc5Ljj+zSTQO44DsuB0tZjwEpnRcjJEgzuhGXr+CoiBf+jTSPZKemtSktDk5aaA== dependencies: - "@babel/core" "7.22.5" + "@babel/core" "7.23.2" "@jridgewell/sourcemap-codec" "^1.4.14" chokidar "^3.0.0" convert-source-map "^1.5.1" @@ -251,51 +254,51 @@ "@angular/compiler@9.0.0": version "9.0.0" - resolved "https://registry.npmjs.org/@angular/compiler/-/compiler-9.0.0.tgz" + resolved "https://registry.yarnpkg.com/@angular/compiler/-/compiler-9.0.0.tgz#87e0bef4c369b6cadae07e3a4295778fc93799d5" integrity sha512-ctjwuntPfZZT2mNj2NDIVu51t9cvbhl/16epc5xEwyzyDt76pX9UgwvY+MbXrf/C/FWwdtmNtfP698BKI+9leQ== "@angular/compiler@^16.2.5": - version "16.2.5" - resolved "https://registry.yarnpkg.com/@angular/compiler/-/compiler-16.2.5.tgz#28033f2f964033bfa10a80436fb2bdc0bbd229c6" - integrity sha512-DpLfWWZFk4lbr81W7sLRt15+/nbyyqTvz+UmGcrSfKBTSbV0VSoUjC3XZeIdPWoIgQXiKUCpaC0YXw0BjaOl0g== + version "16.2.12" + resolved "https://registry.yarnpkg.com/@angular/compiler/-/compiler-16.2.12.tgz#d13366f190706c270b925495fbc12c29097e6b6c" + integrity sha512-6SMXUgSVekGM7R6l1Z9rCtUGtlg58GFmgbpMCsGf+VXxP468Njw8rjT2YZkf5aEPxEuRpSHhDYjqz7n14cwCXQ== dependencies: tslib "^2.3.0" "@angular/core@9.0.0": version "9.0.0" - resolved "https://registry.npmjs.org/@angular/core/-/core-9.0.0.tgz" + resolved "https://registry.yarnpkg.com/@angular/core/-/core-9.0.0.tgz#227dc53e1ac81824f998c6e76000b7efc522641e" integrity sha512-6Pxgsrf0qF9iFFqmIcWmjJGkkCaCm6V5QNnxMy2KloO3SDq6QuMVRbN9RtC8Urmo25LP+eZ6ZgYqFYpdD8Hd9w== "@angular/core@^16.2.5": - version "16.2.5" - resolved "https://registry.yarnpkg.com/@angular/core/-/core-16.2.5.tgz#1efb3d34b3ede3a8d5226c0d673d273cb77b5a2d" - integrity sha512-Po2LMUnPg23D2qI7EYaoA4x3lRswx9nxfpwROzfFPbMNJ3JVbTK0HkTD2dFPGxRua2UjfJTb1um23tEGO4OGMQ== + version "16.2.12" + resolved "https://registry.yarnpkg.com/@angular/core/-/core-16.2.12.tgz#f664204275ee5f5eb46bddc0867e7a514731605f" + integrity sha512-GLLlDeke/NjroaLYOks0uyzFVo6HyLl7VOm0K1QpLXnYvW63W9Ql/T3yguRZa7tRkOAeFZ3jw+1wnBD4O8MoUA== dependencies: tslib "^2.3.0" "@angular/forms@^16.2.5": - version "16.2.5" - resolved "https://registry.yarnpkg.com/@angular/forms/-/forms-16.2.5.tgz#c69891012566f75bbca0e1662c22d2b021007135" - integrity sha512-iYJImRji1OiYIcC2mDBcXhtvPfAoEGT+HqZpivu+/ZPLuf+QegC9+ktJw90SQXR+xccmpkUb9MsJ52SN2MgkPA== + version "16.2.12" + resolved "https://registry.yarnpkg.com/@angular/forms/-/forms-16.2.12.tgz#a533ad61a65080281e709ca68840a1da9f189afc" + integrity sha512-1Eao89hlBgLR3v8tU91vccn21BBKL06WWxl7zLpQmG6Hun+2jrThgOE4Pf3os4fkkbH4Apj0tWL2fNIWe/blbw== dependencies: tslib "^2.3.0" "@angular/language-service@^16.2.5": - version "16.2.5" - resolved "https://registry.yarnpkg.com/@angular/language-service/-/language-service-16.2.5.tgz#2c7854c37e9b8483d8ca4d192a238145c9d71770" - integrity sha512-lYNRN4+iavDuAs86lRHuiTUxtVtsarCZPeoG6K1TEvrXrvmIbTtAbvONNMMnteO9ltCTduyREF9/sefE2Qw/Rg== + version "16.2.12" + resolved "https://registry.yarnpkg.com/@angular/language-service/-/language-service-16.2.12.tgz#e81d9667ec96eac214b0dd54275bdfb835db3f3f" + integrity sha512-sZwB+ZEjChx9EYcqPaS4OnhC/q5RcedZjIdM9mCxuU/MtseURRYRI/8Hnm1RHo9qyc5PmsQpg7p9Vp/5hXLUjw== "@angular/material-moment-adapter@^16.2.4": - version "16.2.4" - resolved "https://registry.yarnpkg.com/@angular/material-moment-adapter/-/material-moment-adapter-16.2.4.tgz#b001419381a773f22fa3608288d5fd06b1dadd06" - integrity sha512-2kmSsgrSOzWf7B8lU6xJYYdvr0h++/BbyPWODK8g3cC9odtvIDxxZYnixmgBSgpnproLKwW/XGvbLlMUVzqc7Q== + version "16.2.14" + resolved "https://registry.yarnpkg.com/@angular/material-moment-adapter/-/material-moment-adapter-16.2.14.tgz#d6972a50fcbb21483ba2888c577e443bebd0b6e6" + integrity sha512-LagTDXEq8XOVLy8CVswCbmq7v9bb84+VikEEN09tz831U/7PHjDZ3xRgpKtv7hXrh8cTZOg3UPQw5tZk0hwh3Q== dependencies: tslib "^2.3.0" "@angular/material@^16.2.4": - version "16.2.4" - resolved "https://registry.yarnpkg.com/@angular/material/-/material-16.2.4.tgz#8325c79799910190c02c5874cac9061246728604" - integrity sha512-TIZ/0MKObn5YU9n/VReghJJKqgkqyzrWVNEJ8UgOP6MV5o+kAbqLSmlDJEyjLIwJF0vPnJ3UP6qbEOfEi1OLaA== + version "16.2.14" + resolved "https://registry.yarnpkg.com/@angular/material/-/material-16.2.14.tgz#4db0c7d14d3d6ac6c8dac83dced0fb8a030b3b49" + integrity sha512-zQIxUb23elPfiIvddqkIDYqQhAHa9ZwMblfbv+ug8bxr4D0Dw360jIarxCgMjAcLj7Ccl3GBqZMUnVeM6cjthw== dependencies: "@material/animation" "15.0.0-canary.bc9ae6c9c.0" "@material/auto-init" "15.0.0-canary.bc9ae6c9c.0" @@ -347,95 +350,50 @@ tslib "^2.3.0" "@angular/platform-browser-dynamic@^16.2.5": - version "16.2.5" - resolved "https://registry.yarnpkg.com/@angular/platform-browser-dynamic/-/platform-browser-dynamic-16.2.5.tgz#79c19e871e414bccdf69db68fcf88482e2d04734" - integrity sha512-kzC4z/KmLss8Du9uM1Q16r+3EqDExKKHnrb3G3tuEQ1jTvYCysdWoooVSBmtIlQUw13znpBm1B7XLoyviFvnwA== + version "16.2.12" + resolved "https://registry.yarnpkg.com/@angular/platform-browser-dynamic/-/platform-browser-dynamic-16.2.12.tgz#14488188c06013eb4153ac6e0603975f8b679f70" + integrity sha512-ya54jerNgreCVAR278wZavwjrUWImMr2F8yM5n9HBvsMBbFaAQ83anwbOEiHEF2BlR+gJiEBLfpuPRMw20pHqw== dependencies: tslib "^2.3.0" "@angular/platform-browser@^16.2.5": - version "16.2.5" - resolved "https://registry.yarnpkg.com/@angular/platform-browser/-/platform-browser-16.2.5.tgz#a312c6875cb304f8c38f13d35eb3a57ee9988814" - integrity sha512-p+1GH/M4Vwoyp7brKkNBcMTxscoZxA1zehetFlNr8kArXWiISgPolyqOVzvT6cycYKu5uSRLnvHOTDss6xrAuA== + version "16.2.12" + resolved "https://registry.yarnpkg.com/@angular/platform-browser/-/platform-browser-16.2.12.tgz#66b5611066cb3f8bb55f035658e978b50720f3b0" + integrity sha512-NnH7ju1iirmVEsUq432DTm0nZBGQsBrU40M3ZeVHMQ2subnGiyUs3QyzDz8+VWLL/T5xTxWLt9BkDn65vgzlIQ== dependencies: tslib "^2.3.0" "@angular/router@^16.2.5": - version "16.2.5" - resolved "https://registry.yarnpkg.com/@angular/router/-/router-16.2.5.tgz#6559689ed465999d092dd707d7f408220672ce3e" - integrity sha512-5IXhe6G7zYFUwHSfUgPw+I/q6M1AcfSyaOVcjMFQ94bVSWEMq5KrGCDc8HQtkdw7GqJ4txwbyQKSKp7khpqShQ== + version "16.2.12" + resolved "https://registry.yarnpkg.com/@angular/router/-/router-16.2.12.tgz#2f4cae64ddb7f998832aa340dd3f843cfb85cbc8" + integrity sha512-aU6QnYSza005V9P3W6PpkieL56O0IHps96DjqI1RS8yOJUl3THmokqYN4Fm5+HXy4f390FN9i6ftadYQDKeWmA== dependencies: tslib "^2.3.0" "@angular/service-worker@^16.2.5": - version "16.2.5" - resolved "https://registry.yarnpkg.com/@angular/service-worker/-/service-worker-16.2.5.tgz#19b09b7be54a0d9fc0434616c6f19429f998689b" - integrity sha512-rHSFkrzyOunWwAQNtTC01ry2inrutlCad8MChK+fHCAhD2maWbNHtIelXR5ylojx7EyTUY0TPL30D60z2mXbwA== + version "16.2.12" + resolved "https://registry.yarnpkg.com/@angular/service-worker/-/service-worker-16.2.12.tgz#359e72693de7d1e8015d1beb02689753ede96de6" + integrity sha512-o0z0s4c76NmRASa+mUHn/q6vUKQNa06mGmLBDKm84vRQ1sQ2TJv+R1p8K9WkiM5mGy6tjQCDOgaz13TcxMFWOQ== dependencies: tslib "^2.3.0" "@assemblyscript/loader@^0.10.1": version "0.10.1" - resolved "https://registry.npmjs.org/@assemblyscript/loader/-/loader-0.10.1.tgz" + resolved "https://registry.yarnpkg.com/@assemblyscript/loader/-/loader-0.10.1.tgz#70e45678f06c72fa2e350e8553ec4a4d72b92e06" integrity sha512-H71nDOOL8Y7kWRLqf6Sums+01Q5msqBW2KhDUTemh1tvY04eSkSXrK0uj/4mmY0Xr16/3zyZmsrxN7CKuRbNRg== -"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.18.6", "@babel/code-frame@^7.21.4": - version "7.21.4" - resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.21.4.tgz" - integrity sha512-LYvhNKfwWSPpocw8GI7gpK2nq3HSDuEPC/uSYaALSJu9xjsalaaYFOq0Pwt5KmVqwEbZlDu81aLXwBOmD/Fv9g== +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.22.13", "@babel/code-frame@^7.22.5", "@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== dependencies: - "@babel/highlight" "^7.18.6" + "@babel/highlight" "^7.24.7" + picocolors "^1.0.0" -"@babel/code-frame@^7.22.13": - version "7.22.13" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.22.13.tgz#e3c1c099402598483b7a8c46a721d1038803755e" - integrity sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w== - dependencies: - "@babel/highlight" "^7.22.13" - chalk "^2.4.2" - -"@babel/code-frame@^7.22.5": - version "7.22.5" - resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.5.tgz" - integrity sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ== - dependencies: - "@babel/highlight" "^7.22.5" - -"@babel/compat-data@^7.21.5": - version "7.21.7" - resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.21.7.tgz" - integrity sha512-KYMqFYTaenzMK4yUtf4EW9wc4N9ef80FsbMtkwool5zpwl4YrT1SdWYSTRcT94KO4hannogdS+LxY7L+arP3gA== - -"@babel/compat-data@^7.22.5", "@babel/compat-data@^7.22.6": - version "7.22.6" - resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.6.tgz" - integrity sha512-29tfsWTq2Ftu7MXmimyC0C5FDZv5DYxOZkh3XD3+QW4V/BYuv/LyEsjj3c0hqedEaDt6DBfDvexMKU8YevdqFg== - -"@babel/compat-data@^7.22.9": - version "7.22.9" - resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.22.9.tgz#71cdb00a1ce3a329ce4cbec3a44f9fef35669730" - integrity sha512-5UamI7xkUcJ3i9qVDS+KFDEK8/7oJ55/sJMB1Ge7IEapr7KfdfV/HErR+koZwOfd+SgtFKOKRhRakdg++DcJpQ== - -"@babel/core@7.22.5": - version "7.22.5" - resolved "https://registry.npmjs.org/@babel/core/-/core-7.22.5.tgz" - integrity sha512-SBuTAjg91A3eKOvD+bPEz3LlhHZRNu1nFOVts9lzDJTXshHTjII0BAtDS3Y2DAkdZdDKWVZGVwkDfc4Clxn1dg== - dependencies: - "@ampproject/remapping" "^2.2.0" - "@babel/code-frame" "^7.22.5" - "@babel/generator" "^7.22.5" - "@babel/helper-compilation-targets" "^7.22.5" - "@babel/helper-module-transforms" "^7.22.5" - "@babel/helpers" "^7.22.5" - "@babel/parser" "^7.22.5" - "@babel/template" "^7.22.5" - "@babel/traverse" "^7.22.5" - "@babel/types" "^7.22.5" - convert-source-map "^1.7.0" - debug "^4.1.0" - gensync "^1.0.0-beta.2" - json5 "^2.2.2" - semver "^6.3.0" +"@babel/compat-data@^7.22.6", "@babel/compat-data@^7.22.9", "@babel/compat-data@^7.25.2": + version "7.25.2" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.25.2.tgz#e41928bd33475305c586f6acbbb7e3ade7a6f7f5" + integrity sha512-bYcppcpKBvX4znYaPEeFau03bp89ShqNMLs+rmdptMw+heSZh9+z84d2YG+K7cYLbWwzdjtDoW/uqZmPjulClQ== "@babel/core@7.22.9": version "7.22.9" @@ -458,26 +416,47 @@ json5 "^2.2.2" semver "^6.3.1" -"@babel/core@^7.12.3": - version "7.21.8" - resolved "https://registry.npmjs.org/@babel/core/-/core-7.21.8.tgz" - integrity sha512-YeM22Sondbo523Sz0+CirSPnbj9bG3P0CdHcBZdqUuaeOaYEFbOLoGU7lebvGP6P5J/WE9wOn7u7C4J9HvS1xQ== +"@babel/core@7.23.2": + version "7.23.2" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.23.2.tgz#ed10df0d580fff67c5f3ee70fd22e2e4c90a9f94" + integrity sha512-n7s51eWdaWZ3vGT2tD4T7J6eJs3QoBXydv7vkUM06Bf1cbVD2Kc2UrkzhiQwobfV7NwOnQXYL7UBJ5VPU+RGoQ== dependencies: "@ampproject/remapping" "^2.2.0" - "@babel/code-frame" "^7.21.4" - "@babel/generator" "^7.21.5" - "@babel/helper-compilation-targets" "^7.21.5" - "@babel/helper-module-transforms" "^7.21.5" - "@babel/helpers" "^7.21.5" - "@babel/parser" "^7.21.8" - "@babel/template" "^7.20.7" - "@babel/traverse" "^7.21.5" - "@babel/types" "^7.21.5" - convert-source-map "^1.7.0" + "@babel/code-frame" "^7.22.13" + "@babel/generator" "^7.23.0" + "@babel/helper-compilation-targets" "^7.22.15" + "@babel/helper-module-transforms" "^7.23.0" + "@babel/helpers" "^7.23.2" + "@babel/parser" "^7.23.0" + "@babel/template" "^7.22.15" + "@babel/traverse" "^7.23.2" + "@babel/types" "^7.23.0" + convert-source-map "^2.0.0" debug "^4.1.0" gensync "^1.0.0-beta.2" - json5 "^2.2.2" - semver "^6.3.0" + json5 "^2.2.3" + semver "^6.3.1" + +"@babel/core@^7.12.3": + version "7.25.2" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.25.2.tgz#ed8eec275118d7613e77a352894cd12ded8eba77" + integrity sha512-BBt3opiCOxUr9euZ5/ro/Xv8/V7yJ5bjYMqG/C1YAo8MIKAnumZalCN+msbci3Pigy4lIQfPUpfMM27HMGaYEA== + dependencies: + "@ampproject/remapping" "^2.2.0" + "@babel/code-frame" "^7.24.7" + "@babel/generator" "^7.25.0" + "@babel/helper-compilation-targets" "^7.25.2" + "@babel/helper-module-transforms" "^7.25.2" + "@babel/helpers" "^7.25.0" + "@babel/parser" "^7.25.0" + "@babel/template" "^7.25.0" + "@babel/traverse" "^7.25.2" + "@babel/types" "^7.25.2" + 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.22.9": version "7.22.9" @@ -489,137 +468,75 @@ "@jridgewell/trace-mapping" "^0.3.17" jsesc "^2.5.1" -"@babel/generator@^7.21.5": - version "7.21.5" - resolved "https://registry.npmjs.org/@babel/generator/-/generator-7.21.5.tgz" - integrity sha512-SrKK/sRv8GesIW1bDagf9cCG38IOMYZusoe1dfg0D8aiUe3Amvoj1QtjTPAWcfrZFvIwlleLb0gxzQidL9w14w== +"@babel/generator@^7.22.9", "@babel/generator@^7.23.0", "@babel/generator@^7.25.0": + version "7.25.0" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.25.0.tgz#f858ddfa984350bc3d3b7f125073c9af6988f18e" + integrity sha512-3LEEcj3PVW8pW2R1SR1M89g/qrYk/m/mB/tLqn7dn4sbBUQyTqnlod+II2U4dqiGtUmkcnAmkMDralTFZttRiw== dependencies: - "@babel/types" "^7.21.5" - "@jridgewell/gen-mapping" "^0.3.2" - "@jridgewell/trace-mapping" "^0.3.17" + "@babel/types" "^7.25.0" + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.25" jsesc "^2.5.1" -"@babel/generator@^7.22.5": - version "7.22.7" - resolved "https://registry.npmjs.org/@babel/generator/-/generator-7.22.7.tgz" - integrity sha512-p+jPjMG+SI8yvIaxGgeW24u7q9+5+TGpZh8/CuB7RhBKd7RCy8FayNEFNNKrNK/eUcY/4ExQqLmyrvBXKsIcwQ== - dependencies: - "@babel/types" "^7.22.5" - "@jridgewell/gen-mapping" "^0.3.2" - "@jridgewell/trace-mapping" "^0.3.17" - jsesc "^2.5.1" - -"@babel/generator@^7.22.9": - version "7.22.10" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.22.10.tgz#c92254361f398e160645ac58831069707382b722" - integrity sha512-79KIf7YiWjjdZ81JnLujDRApWtl7BxTqWD88+FFdQEIOG8LJ0etDOM7CXuIgGJa55sGOwZVwuEsaLEm0PJ5/+A== - dependencies: - "@babel/types" "^7.22.10" - "@jridgewell/gen-mapping" "^0.3.2" - "@jridgewell/trace-mapping" "^0.3.17" - jsesc "^2.5.1" - -"@babel/generator@^7.23.0": - version "7.23.0" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.23.0.tgz#df5c386e2218be505b34837acbcb874d7a983420" - integrity sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g== - dependencies: - "@babel/types" "^7.23.0" - "@jridgewell/gen-mapping" "^0.3.2" - "@jridgewell/trace-mapping" "^0.3.17" - jsesc "^2.5.1" - -"@babel/helper-annotate-as-pure@7.22.5", "@babel/helper-annotate-as-pure@^7.22.5": +"@babel/helper-annotate-as-pure@7.22.5": version "7.22.5" - resolved "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz" + resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz#e7f06737b197d580a01edf75d97e2c8be99d3882" integrity sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg== dependencies: "@babel/types" "^7.22.5" -"@babel/helper-annotate-as-pure@^7.18.6": - version "7.18.6" - resolved "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz" - integrity sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA== +"@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" + integrity sha512-BaDeOonYvhdKw+JoMVkAixAAJzG2jVPIwWoKBPdYuY9b452e2rPuI9QPYh3KpofZ3pW2akOmwZLOiOsHMiqRAg== dependencies: - "@babel/types" "^7.18.6" + "@babel/types" "^7.24.7" -"@babel/helper-builder-binary-assignment-operator-visitor@^7.22.5": - version "7.22.5" - resolved "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.22.5.tgz" - integrity sha512-m1EP3lVOPptR+2DwD125gziZNcmoNSHGmJROKoy87loWUQyJaVXDgpmruWqDARZSmtYQ+Dl25okU8+qhVzuykw== +"@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" + integrity sha512-xZeCVVdwb4MsDBkkyZ64tReWYrLRHlMN72vP7Bdm3OUOuyFZExhsHUUnuWnm2/XOlAJzR0LfPpB56WXZn0X/lA== dependencies: - "@babel/types" "^7.22.5" + "@babel/traverse" "^7.24.7" + "@babel/types" "^7.24.7" -"@babel/helper-compilation-targets@^7.21.5": - version "7.21.5" - resolved "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.21.5.tgz" - integrity sha512-1RkbFGUKex4lvsB9yhIfWltJM5cZKUftB2eNajaDv3dCMEp49iBG0K14uH8NnX9IPux2+mK7JGEOB0jn48/J6w== +"@babel/helper-compilation-targets@^7.22.15", "@babel/helper-compilation-targets@^7.22.6", "@babel/helper-compilation-targets@^7.22.9", "@babel/helper-compilation-targets@^7.24.7", "@babel/helper-compilation-targets@^7.24.8", "@babel/helper-compilation-targets@^7.25.2": + version "7.25.2" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.2.tgz#e1d9410a90974a3a5a66e84ff55ef62e3c02d06c" + integrity sha512-U2U5LsSaZ7TAt3cfaymQ8WHh0pxvdHoEk6HVpaexxixjyEquMh0L0YNJNM6CTGKMXV1iksi0iZkGw4AcFkPaaw== dependencies: - "@babel/compat-data" "^7.21.5" - "@babel/helper-validator-option" "^7.21.0" - browserslist "^4.21.3" - lru-cache "^5.1.1" - semver "^6.3.0" - -"@babel/helper-compilation-targets@^7.22.5", "@babel/helper-compilation-targets@^7.22.6": - version "7.22.6" - resolved "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.6.tgz" - integrity sha512-534sYEqWD9VfUm3IPn2SLcH4Q3P86XL+QvqdC7ZsFrzyyPF3T4XGiVghF6PTYNdWg6pXuoqXxNQAhbYeEInTzA== - dependencies: - "@babel/compat-data" "^7.22.6" - "@babel/helper-validator-option" "^7.22.5" - "@nicolo-ribaudo/semver-v6" "^6.3.3" - browserslist "^4.21.9" - lru-cache "^5.1.1" - -"@babel/helper-compilation-targets@^7.22.9": - version "7.22.10" - resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.10.tgz#01d648bbc25dd88f513d862ee0df27b7d4e67024" - integrity sha512-JMSwHD4J7SLod0idLq5PKgI+6g/hLD/iuWBq08ZX49xE14VpVEojJ5rHWptpirV2j020MvypRLAXAO50igCJ5Q== - dependencies: - "@babel/compat-data" "^7.22.9" - "@babel/helper-validator-option" "^7.22.5" - browserslist "^4.21.9" + "@babel/compat-data" "^7.25.2" + "@babel/helper-validator-option" "^7.24.8" + browserslist "^4.23.1" lru-cache "^5.1.1" semver "^6.3.1" -"@babel/helper-create-class-features-plugin@^7.22.5": - version "7.22.6" - resolved "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.6.tgz" - integrity sha512-iwdzgtSiBxF6ni6mzVnZCF3xt5qE6cEA0J7nFt8QOAWZ0zjCFceEgpn3vtb2V7WFR6QzP2jmIFOHMTRo7eNJjQ== +"@babel/helper-create-class-features-plugin@^7.24.7": + version "7.25.0" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.25.0.tgz#a109bf9c3d58dfed83aaf42e85633c89f43a6253" + integrity sha512-GYM6BxeQsETc9mnct+nIIpf63SAyzvyYN7UB/IlTyd+MBg06afFGp0mIeUqGyWgS2mxad6vqbMrHVlaL3m70sQ== dependencies: - "@babel/helper-annotate-as-pure" "^7.22.5" - "@babel/helper-environment-visitor" "^7.22.5" - "@babel/helper-function-name" "^7.22.5" - "@babel/helper-member-expression-to-functions" "^7.22.5" - "@babel/helper-optimise-call-expression" "^7.22.5" - "@babel/helper-replace-supers" "^7.22.5" - "@babel/helper-skip-transparent-expression-wrappers" "^7.22.5" - "@babel/helper-split-export-declaration" "^7.22.6" - "@nicolo-ribaudo/semver-v6" "^6.3.3" + "@babel/helper-annotate-as-pure" "^7.24.7" + "@babel/helper-member-expression-to-functions" "^7.24.8" + "@babel/helper-optimise-call-expression" "^7.24.7" + "@babel/helper-replace-supers" "^7.25.0" + "@babel/helper-skip-transparent-expression-wrappers" "^7.24.7" + "@babel/traverse" "^7.25.0" + semver "^6.3.1" -"@babel/helper-create-regexp-features-plugin@^7.18.6": - version "7.21.8" - resolved "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.21.8.tgz" - integrity sha512-zGuSdedkFtsFHGbexAvNuipg1hbtitDLo2XE8/uf6Y9sOQV1xsYX/2pNbtedp/X0eU1pIt+kGvaqHCowkRbS5g== +"@babel/helper-create-regexp-features-plugin@^7.18.6", "@babel/helper-create-regexp-features-plugin@^7.24.7": + version "7.25.2" + resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.25.2.tgz#24c75974ed74183797ffd5f134169316cd1808d9" + integrity sha512-+wqVGP+DFmqwFD3EH6TMTfUNeqDehV3E/dl+Sd54eaXqm17tEUNbEIn4sVivVowbvUpOtIGxdo3GoXyDH9N/9g== dependencies: - "@babel/helper-annotate-as-pure" "^7.18.6" + "@babel/helper-annotate-as-pure" "^7.24.7" regexpu-core "^5.3.1" - semver "^6.3.0" + semver "^6.3.1" -"@babel/helper-create-regexp-features-plugin@^7.22.5": - version "7.22.6" - resolved "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.6.tgz" - integrity sha512-nBookhLKxAWo/TUCmhnaEJyLz2dekjQvv5SRpE9epWQBcpedWLKt8aZdsuT9XV5ovzR3fENLjRXVT0GsSlGGhA== - dependencies: - "@babel/helper-annotate-as-pure" "^7.22.5" - "@nicolo-ribaudo/semver-v6" "^6.3.3" - regexpu-core "^5.3.1" - -"@babel/helper-define-polyfill-provider@^0.4.2": - version "0.4.2" - resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.2.tgz#82c825cadeeeee7aad237618ebbe8fa1710015d7" - integrity sha512-k0qnnOqHn5dK9pZpfD5XXZ9SojAITdCKRn2Lp6rnDGzIbaP0rHyMPk/4wsSxVBVz4RfN0q6VpXWP2pDGIoQ7hw== +"@babel/helper-define-polyfill-provider@^0.4.4": + version "0.4.4" + resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.4.tgz#64df615451cb30e94b59a9696022cffac9a10088" + integrity sha512-QcJMILQCu2jm5TFPGA3lCpJJTeEP+mqeXooG/NZbg/h5FTFi6V0+99ahlRsW8/kRLyb24LZVCCiclDedhLKcBA== dependencies: "@babel/helper-compilation-targets" "^7.22.6" "@babel/helper-plugin-utils" "^7.22.5" @@ -627,357 +544,180 @@ lodash.debounce "^4.0.8" resolve "^1.14.2" -"@babel/helper-environment-visitor@^7.18.9", "@babel/helper-environment-visitor@^7.21.5": - version "7.21.5" - resolved "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.21.5.tgz" - integrity sha512-IYl4gZ3ETsWocUWgsFZLM5i1BYx9SoemminVEXadgLBa9TdeorzgLKm8wWLA6J1N/kT3Kch8XIk1laNzYoHKvQ== - -"@babel/helper-environment-visitor@^7.22.20": - version "7.22.20" - resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz#96159db61d34a29dba454c959f5ae4a649ba9167" - integrity sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA== - -"@babel/helper-environment-visitor@^7.22.5": - version "7.22.5" - resolved "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz" - integrity sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q== - -"@babel/helper-function-name@^7.19.0": - version "7.21.0" - resolved "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.21.0.tgz" - integrity sha512-HfK1aMRanKHpxemaY2gqBmL04iAPOPRj7DxtNbiDOrJK+gdwkiNRVpCpUJYbUT+aZyemKN8brqTOxzCaG6ExRg== +"@babel/helper-define-polyfill-provider@^0.5.0": + version "0.5.0" + resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.5.0.tgz#465805b7361f461e86c680f1de21eaf88c25901b" + integrity sha512-NovQquuQLAQ5HuyjCz7WQP9MjRj7dx++yspwiyUiGl9ZyadHRSql1HZh5ogRd8W8w6YM6EQ/NTB8rgjLt5W65Q== dependencies: - "@babel/template" "^7.20.7" - "@babel/types" "^7.21.0" + "@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-function-name@^7.22.5": - version "7.22.5" - resolved "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz" - integrity sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ== +"@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" + integrity sha512-LV76g+C502biUK6AyZ3LK10vDpDyCzZnhZFXkH1L75zHPj68+qc8Zfpx2th+gzwA2MzyK+1g/3EPl62yFnVttQ== dependencies: - "@babel/template" "^7.22.5" - "@babel/types" "^7.22.5" + "@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-function-name@^7.23.0": - version "7.23.0" - resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz#1f9a3cdbd5b2698a670c30d2735f9af95ed52759" - integrity sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw== +"@babel/helper-environment-visitor@^7.18.9": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.7.tgz#4b31ba9551d1f90781ba83491dd59cf9b269f7d9" + integrity sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ== dependencies: - "@babel/template" "^7.22.15" - "@babel/types" "^7.23.0" + "@babel/types" "^7.24.7" -"@babel/helper-hoist-variables@^7.22.5": - version "7.22.5" - resolved "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz" - integrity sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw== +"@babel/helper-member-expression-to-functions@^7.24.8": + version "7.24.8" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.24.8.tgz#6155e079c913357d24a4c20480db7c712a5c3fb6" + integrity sha512-LABppdt+Lp/RlBxqrh4qgf1oEH/WxdzQNDJIu5gC/W1GyvPVrOBiItmmM8wan2fm4oYqFuFfkXmlGpLQhPY8CA== dependencies: - "@babel/types" "^7.22.5" + "@babel/traverse" "^7.24.8" + "@babel/types" "^7.24.8" -"@babel/helper-member-expression-to-functions@^7.22.5": - version "7.22.5" - resolved "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.22.5.tgz" - integrity sha512-aBiH1NKMG0H2cGZqspNvsaBe6wNGjbJjuLy29aU+eDZjSbbN53BaxlpB02xm9v34pLTZ1nIQPFYn2qMZoa5BQQ== +"@babel/helper-module-imports@^7.22.5", "@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" + integrity sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA== dependencies: - "@babel/types" "^7.22.5" + "@babel/traverse" "^7.24.7" + "@babel/types" "^7.24.7" -"@babel/helper-module-imports@^7.21.4": - version "7.21.4" - resolved "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.21.4.tgz" - integrity sha512-orajc5T2PsRYUN3ZryCEFeMDYwyw09c/pZeaQEZPH0MpKzSvn3e0uXsDBu3k03VI+9DBiRo+l22BfKTpKwa/Wg== +"@babel/helper-module-transforms@^7.22.9", "@babel/helper-module-transforms@^7.23.0", "@babel/helper-module-transforms@^7.24.7", "@babel/helper-module-transforms@^7.24.8", "@babel/helper-module-transforms@^7.25.0", "@babel/helper-module-transforms@^7.25.2": + version "7.25.2" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.25.2.tgz#ee713c29768100f2776edf04d4eb23b8d27a66e6" + integrity sha512-BjyRAbix6j/wv83ftcVJmBt72QtHI56C7JXZoG2xATiLpmoC7dpd8WnkikExHDVPpi/3qCmO6WY1EaXOluiecQ== dependencies: - "@babel/types" "^7.21.4" + "@babel/helper-module-imports" "^7.24.7" + "@babel/helper-simple-access" "^7.24.7" + "@babel/helper-validator-identifier" "^7.24.7" + "@babel/traverse" "^7.25.2" -"@babel/helper-module-imports@^7.22.5": - version "7.22.5" - resolved "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.5.tgz" - integrity sha512-8Dl6+HD/cKifutF5qGd/8ZJi84QeAKh+CEe1sBzz8UayBBGg1dAIJrdHOcOM5b2MpzWL2yuotJTtGjETq0qjXg== +"@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" + integrity sha512-jKiTsW2xmWwxT1ixIdfXUZp+P5yURx2suzLZr5Hi64rURpDYdMW0pv+Uf17EYk2Rd428Lx4tLsnjGJzYKDM/6A== dependencies: - "@babel/types" "^7.22.5" + "@babel/types" "^7.24.7" -"@babel/helper-module-transforms@^7.21.5": - version "7.21.5" - resolved "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.21.5.tgz" - integrity sha512-bI2Z9zBGY2q5yMHoBvJ2a9iX3ZOAzJPm7Q8Yz6YeoUjU/Cvhmi2G4QyTNyPBqqXSgTjUxRg3L0xV45HvkNWWBw== +"@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.20.2", "@babel/helper-plugin-utils@^7.22.5", "@babel/helper-plugin-utils@^7.24.7", "@babel/helper-plugin-utils@^7.24.8", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3": + version "7.24.8" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.8.tgz#94ee67e8ec0e5d44ea7baeb51e571bd26af07878" + integrity sha512-FFWx5142D8h2Mgr/iPVGH5G7w6jDn4jUSpZTyDnQO0Yn7Ks2Kuz6Pci8H6MPCoUJegd/UZQ3tAvfLCxQSnWWwg== + +"@babel/helper-remap-async-to-generator@^7.18.9", "@babel/helper-remap-async-to-generator@^7.22.5", "@babel/helper-remap-async-to-generator@^7.24.7", "@babel/helper-remap-async-to-generator@^7.25.0": + version "7.25.0" + resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.25.0.tgz#d2f0fbba059a42d68e5e378feaf181ef6055365e" + integrity sha512-NhavI2eWEIz/H9dbrG0TuOicDhNexze43i5z7lEqwYm0WEZVTwnPpA0EafUTP7+6/W79HWIP2cTe3Z5NiSTVpw== dependencies: - "@babel/helper-environment-visitor" "^7.21.5" - "@babel/helper-module-imports" "^7.21.4" - "@babel/helper-simple-access" "^7.21.5" - "@babel/helper-split-export-declaration" "^7.18.6" - "@babel/helper-validator-identifier" "^7.19.1" - "@babel/template" "^7.20.7" - "@babel/traverse" "^7.21.5" - "@babel/types" "^7.21.5" + "@babel/helper-annotate-as-pure" "^7.24.7" + "@babel/helper-wrap-function" "^7.25.0" + "@babel/traverse" "^7.25.0" -"@babel/helper-module-transforms@^7.22.5": - version "7.22.5" - resolved "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.22.5.tgz" - integrity sha512-+hGKDt/Ze8GFExiVHno/2dvG5IdstpzCq0y4Qc9OJ25D4q3pKfiIP/4Vp3/JvhDkLKsDK2api3q3fpIgiIF5bw== +"@babel/helper-replace-supers@^7.24.7", "@babel/helper-replace-supers@^7.25.0": + version "7.25.0" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.25.0.tgz#ff44deac1c9f619523fe2ca1fd650773792000a9" + integrity sha512-q688zIvQVYtZu+i2PsdIu/uWGRpfxzr5WESsfpShfZECkO+d2o+WROWezCi/Q6kJ0tfPa5+pUGUlfx2HhrA3Bg== dependencies: - "@babel/helper-environment-visitor" "^7.22.5" - "@babel/helper-module-imports" "^7.22.5" - "@babel/helper-simple-access" "^7.22.5" - "@babel/helper-split-export-declaration" "^7.22.5" - "@babel/helper-validator-identifier" "^7.22.5" - "@babel/template" "^7.22.5" - "@babel/traverse" "^7.22.5" - "@babel/types" "^7.22.5" + "@babel/helper-member-expression-to-functions" "^7.24.8" + "@babel/helper-optimise-call-expression" "^7.24.7" + "@babel/traverse" "^7.25.0" -"@babel/helper-module-transforms@^7.22.9": - version "7.22.9" - resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.22.9.tgz#92dfcb1fbbb2bc62529024f72d942a8c97142129" - integrity sha512-t+WA2Xn5K+rTeGtC8jCsdAH52bjggG5TKRuRrAGNM/mjIbO4GxvlLMFOEz9wXY5I2XQ60PMFsAG2WIcG82dQMQ== +"@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" + integrity sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg== dependencies: - "@babel/helper-environment-visitor" "^7.22.5" - "@babel/helper-module-imports" "^7.22.5" - "@babel/helper-simple-access" "^7.22.5" - "@babel/helper-split-export-declaration" "^7.22.6" - "@babel/helper-validator-identifier" "^7.22.5" + "@babel/traverse" "^7.24.7" + "@babel/types" "^7.24.7" -"@babel/helper-optimise-call-expression@^7.22.5": - version "7.22.5" - resolved "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.22.5.tgz" - integrity sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw== +"@babel/helper-skip-transparent-expression-wrappers@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.24.7.tgz#5f8fa83b69ed5c27adc56044f8be2b3ea96669d9" + integrity sha512-IO+DLT3LQUElMbpzlatRASEyQtfhSE0+m465v++3jyyXeBTBUjtVZg28/gHeV5mrTJqvEKhKroBGAvhW+qPHiQ== dependencies: - "@babel/types" "^7.22.5" + "@babel/traverse" "^7.24.7" + "@babel/types" "^7.24.7" -"@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.20.2", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3": - version "7.21.5" - resolved "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.21.5.tgz" - integrity sha512-0WDaIlXKOX/3KfBK/dwP1oQGiPh6rjMkT7HIRv7i5RR2VUMwrx5ZL0dwBkKx7+SW1zwNdgjHd34IMk5ZjTeHVg== - -"@babel/helper-plugin-utils@^7.22.5": - version "7.22.5" - resolved "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz" - integrity sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg== - -"@babel/helper-remap-async-to-generator@^7.18.9": - version "7.18.9" - resolved "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.18.9.tgz" - integrity sha512-dI7q50YKd8BAv3VEfgg7PS7yD3Rtbi2J1XMXaalXO0W0164hYLnh8zpjRS0mte9MfVp/tltvr/cfdXPvJr1opA== - dependencies: - "@babel/helper-annotate-as-pure" "^7.18.6" - "@babel/helper-environment-visitor" "^7.18.9" - "@babel/helper-wrap-function" "^7.18.9" - "@babel/types" "^7.18.9" - -"@babel/helper-remap-async-to-generator@^7.22.5": - version "7.22.5" - resolved "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.22.5.tgz" - integrity sha512-cU0Sq1Rf4Z55fgz7haOakIyM7+x/uCFwXpLPaeRzfoUtAEAuUZjZvFPjL/rk5rW693dIgn2hng1W7xbT7lWT4g== - dependencies: - "@babel/helper-annotate-as-pure" "^7.22.5" - "@babel/helper-environment-visitor" "^7.22.5" - "@babel/helper-wrap-function" "^7.22.5" - "@babel/types" "^7.22.5" - -"@babel/helper-remap-async-to-generator@^7.22.9": - version "7.22.9" - resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.22.9.tgz#53a25b7484e722d7efb9c350c75c032d4628de82" - integrity sha512-8WWC4oR4Px+tr+Fp0X3RHDVfINGpF3ad1HIbrc8A77epiR6eMMc6jsgozkzT2uDiOOdoS9cLIQ+XD2XvI2WSmQ== - dependencies: - "@babel/helper-annotate-as-pure" "^7.22.5" - "@babel/helper-environment-visitor" "^7.22.5" - "@babel/helper-wrap-function" "^7.22.9" - -"@babel/helper-replace-supers@^7.22.5": - version "7.22.5" - resolved "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.22.5.tgz" - integrity sha512-aLdNM5I3kdI/V9xGNyKSF3X/gTyMUBohTZ+/3QdQKAA9vxIiy12E+8E2HoOP1/DjeqU+g6as35QHJNMDDYpuCg== - dependencies: - "@babel/helper-environment-visitor" "^7.22.5" - "@babel/helper-member-expression-to-functions" "^7.22.5" - "@babel/helper-optimise-call-expression" "^7.22.5" - "@babel/template" "^7.22.5" - "@babel/traverse" "^7.22.5" - "@babel/types" "^7.22.5" - -"@babel/helper-simple-access@^7.21.5": - version "7.21.5" - resolved "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.21.5.tgz" - integrity sha512-ENPDAMC1wAjR0uaCUwliBdiSl1KBJAVnMTzXqi64c2MG8MPR6ii4qf7bSXDqSFbr4W6W028/rf5ivoHop5/mkg== - dependencies: - "@babel/types" "^7.21.5" - -"@babel/helper-simple-access@^7.22.5": - version "7.22.5" - resolved "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz" - integrity sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w== - dependencies: - "@babel/types" "^7.22.5" - -"@babel/helper-skip-transparent-expression-wrappers@^7.22.5": - version "7.22.5" - resolved "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.22.5.tgz" - integrity sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q== - dependencies: - "@babel/types" "^7.22.5" - -"@babel/helper-split-export-declaration@7.22.6", "@babel/helper-split-export-declaration@^7.22.5", "@babel/helper-split-export-declaration@^7.22.6": +"@babel/helper-split-export-declaration@7.22.6": version "7.22.6" - resolved "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz#322c61b7310c0997fe4c323955667f18fcefb91c" integrity sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g== dependencies: "@babel/types" "^7.22.5" -"@babel/helper-split-export-declaration@^7.18.6": - version "7.18.6" - resolved "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz" - integrity sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA== +"@babel/helper-string-parser@^7.24.8": + version "7.24.8" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz#5b3329c9a58803d5df425e5785865881a81ca48d" + integrity sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ== + +"@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" + integrity sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w== + +"@babel/helper-validator-option@^7.22.5", "@babel/helper-validator-option@^7.24.8": + version "7.24.8" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.24.8.tgz#3725cdeea8b480e86d34df15304806a06975e33d" + integrity sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q== + +"@babel/helper-wrap-function@^7.25.0": + version "7.25.0" + resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.25.0.tgz#dab12f0f593d6ca48c0062c28bcfb14ebe812f81" + integrity sha512-s6Q1ebqutSiZnEjaofc/UKDyC4SbzV5n5SrA2Gq8UawLycr3i04f1dX4OzoQVnexm6aOCh37SQNYlJ/8Ku+PMQ== dependencies: - "@babel/types" "^7.18.6" + "@babel/template" "^7.25.0" + "@babel/traverse" "^7.25.0" + "@babel/types" "^7.25.0" -"@babel/helper-string-parser@^7.21.5": - version "7.21.5" - resolved "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.21.5.tgz" - integrity sha512-5pTUx3hAJaZIdW99sJ6ZUUgWq/Y+Hja7TowEnLNMm1VivRgZQL3vpBY3qUACVsvw+yQU6+YgfBVmcbLaZtrA1w== - -"@babel/helper-string-parser@^7.22.5": - version "7.22.5" - resolved "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz" - integrity sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw== - -"@babel/helper-validator-identifier@^7.18.6", "@babel/helper-validator-identifier@^7.19.1": - version "7.19.1" - resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz" - integrity sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w== - -"@babel/helper-validator-identifier@^7.22.20": - version "7.22.20" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz#c4ae002c61d2879e724581d96665583dbc1dc0e0" - integrity sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A== - -"@babel/helper-validator-identifier@^7.22.5": - version "7.22.5" - resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz" - integrity sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ== - -"@babel/helper-validator-option@^7.21.0": - version "7.21.0" - resolved "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.21.0.tgz" - integrity sha512-rmL/B8/f0mKS2baE9ZpyTcTavvEuWhTTW8amjzXNvYG4AwBsqTLikfXsEofsJEfKHf+HQVQbFOHy6o+4cnC/fQ== - -"@babel/helper-validator-option@^7.22.5": - version "7.22.5" - resolved "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.5.tgz" - integrity sha512-R3oB6xlIVKUnxNUxbmgq7pKjxpru24zlimpE8WK47fACIlM0II/Hm1RS8IaOI7NgCr6LNS+jl5l75m20npAziw== - -"@babel/helper-wrap-function@^7.18.9": - version "7.20.5" - resolved "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.20.5.tgz" - integrity sha512-bYMxIWK5mh+TgXGVqAtnu5Yn1un+v8DDZtqyzKRLUzrh70Eal2O3aZ7aPYiMADO4uKlkzOiRiZ6GX5q3qxvW9Q== +"@babel/helpers@^7.22.6", "@babel/helpers@^7.23.2", "@babel/helpers@^7.25.0": + version "7.25.0" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.25.0.tgz#e69beb7841cb93a6505531ede34f34e6a073650a" + integrity sha512-MjgLZ42aCm0oGjJj8CtSM3DB8NOOf8h2l7DCTePJs29u+v7yO/RBX9nShlKMgFnRks/Q4tBAe7Hxnov9VkGwLw== dependencies: - "@babel/helper-function-name" "^7.19.0" - "@babel/template" "^7.18.10" - "@babel/traverse" "^7.20.5" - "@babel/types" "^7.20.5" + "@babel/template" "^7.25.0" + "@babel/types" "^7.25.0" -"@babel/helper-wrap-function@^7.22.5": - version "7.22.5" - resolved "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.22.5.tgz" - integrity sha512-bYqLIBSEshYcYQyfks8ewYA8S30yaGSeRslcvKMvoUk6HHPySbxHq9YRi6ghhzEU+yhQv9bP/jXnygkStOcqZw== +"@babel/highlight@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.24.7.tgz#a05ab1df134b286558aae0ed41e6c5f731bf409d" + integrity sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw== dependencies: - "@babel/helper-function-name" "^7.22.5" - "@babel/template" "^7.22.5" - "@babel/traverse" "^7.22.5" - "@babel/types" "^7.22.5" - -"@babel/helper-wrap-function@^7.22.9": - version "7.22.10" - resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.22.10.tgz#d845e043880ed0b8c18bd194a12005cb16d2f614" - integrity sha512-OnMhjWjuGYtdoO3FmsEFWvBStBAe2QOgwOLsLNDjN+aaiMD8InJk1/O3HSD8lkqTjCgg5YI34Tz15KNNA3p+nQ== - dependencies: - "@babel/helper-function-name" "^7.22.5" - "@babel/template" "^7.22.5" - "@babel/types" "^7.22.10" - -"@babel/helpers@^7.21.5": - version "7.21.5" - resolved "https://registry.npmjs.org/@babel/helpers/-/helpers-7.21.5.tgz" - integrity sha512-BSY+JSlHxOmGsPTydUkPf1MdMQ3M81x5xGCOVgWM3G8XH77sJ292Y2oqcp0CbbgxhqBuI46iUz1tT7hqP7EfgA== - dependencies: - "@babel/template" "^7.20.7" - "@babel/traverse" "^7.21.5" - "@babel/types" "^7.21.5" - -"@babel/helpers@^7.22.5": - version "7.22.6" - resolved "https://registry.npmjs.org/@babel/helpers/-/helpers-7.22.6.tgz" - integrity sha512-YjDs6y/fVOYFV8hAf1rxd1QvR9wJe1pDBZ2AREKq/SDayfPzgk0PBnVuTCE5X1acEpMMNOVUqoe+OwiZGJ+OaA== - dependencies: - "@babel/template" "^7.22.5" - "@babel/traverse" "^7.22.6" - "@babel/types" "^7.22.5" - -"@babel/helpers@^7.22.6": - version "7.22.10" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.22.10.tgz#ae6005c539dfbcb5cd71fb51bfc8a52ba63bc37a" - integrity sha512-a41J4NW8HyZa1I1vAndrraTlPZ/eZoga2ZgS7fEr0tZJGVU4xqdE80CEm0CcNjha5EZ8fTBYLKHF0kqDUuAwQw== - dependencies: - "@babel/template" "^7.22.5" - "@babel/traverse" "^7.22.10" - "@babel/types" "^7.22.10" - -"@babel/highlight@^7.18.6": - version "7.18.6" - resolved "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz" - integrity sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g== - dependencies: - "@babel/helper-validator-identifier" "^7.18.6" - chalk "^2.0.0" - js-tokens "^4.0.0" - -"@babel/highlight@^7.22.13": - version "7.22.20" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.22.20.tgz#4ca92b71d80554b01427815e06f2df965b9c1f54" - integrity sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg== - dependencies: - "@babel/helper-validator-identifier" "^7.22.20" + "@babel/helper-validator-identifier" "^7.24.7" chalk "^2.4.2" js-tokens "^4.0.0" + picocolors "^1.0.0" -"@babel/highlight@^7.22.5": - version "7.22.5" - resolved "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.5.tgz" - integrity sha512-BSKlD1hgnedS5XRnGOljZawtag7H1yPfQp0tdNJCHoH6AZ+Pcm9VvkrK59/Yy593Ypg0zMxH2BxD1VPYUQ7UIw== - dependencies: - "@babel/helper-validator-identifier" "^7.22.5" - chalk "^2.0.0" - js-tokens "^4.0.0" - -"@babel/parser@^7.14.7", "@babel/parser@^7.20.7", "@babel/parser@^7.21.8": - version "7.21.8" - resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.21.8.tgz" - integrity sha512-6zavDGdzG3gUqAdWvlLFfk+36RilI+Pwyuuh7HItyeScCWP3k6i8vKclAQ0bM/0y/Kz/xiwvxhMv9MgTJP5gmA== - -"@babel/parser@^7.22.15", "@babel/parser@^7.23.0": - version "7.23.0" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.23.0.tgz#da950e622420bf96ca0d0f2909cdddac3acd8719" - integrity sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw== - -"@babel/parser@^7.22.5", "@babel/parser@^7.22.7": - version "7.22.7" - resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.22.7.tgz" - integrity sha512-7NF8pOkHP5o2vpmGgNGcfAeCvOYhGLyA3Z4eBQkT1RJlWu47n63bCs93QfJ2hIAFCil7L5P2IWhs1oToVgrL0Q== +"@babel/parser@^7.14.7", "@babel/parser@^7.22.5", "@babel/parser@^7.22.7", "@babel/parser@^7.23.0", "@babel/parser@^7.25.0": + version "7.25.0" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.25.0.tgz#9fdc9237504d797b6e7b8f66e78ea7f570d256ad" + integrity sha512-CzdIU9jdP0dg7HdyB+bHvDJGagUv+qtzZt5rYCWwW6tITNqV9odjp6Qu41gkG0ca5UfdDUWrKkiAnHHdGRnOrA== "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.22.5": - version "7.22.5" - resolved "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.22.5.tgz" - integrity sha512-NP1M5Rf+u2Gw9qfSO4ihjcTGW5zXTi36ITLd4/EoAcEhIZ0yjMqmftDNl3QC19CX7olhrjpyU454g/2W7X0jvQ== + version "7.25.0" + 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.25.0.tgz#749bde80356b295390954643de7635e0dffabe73" + integrity sha512-lXwdNZtTmeVOOFtwM/WDe7yg1PL8sYhRk/XH0FzbR2HDQ0xC+EnQ/JHeoMYSavtU115tnUk0q9CDyq8si+LMAA== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.8" "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.22.5": - version "7.22.5" - resolved "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.22.5.tgz" - integrity sha512-31Bb65aZaUwqCbWMnZPduIZxCBngHFlzyN6Dq6KAJjtx+lx6ohKHubc61OomYi7XwVD4Ol0XCVz4h+pYFR048g== + 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" + integrity sha512-+izXIbke1T33mY4MSNnrqhPXDz01WYhEf3yF5NbnUtkiNnm+XBZJl3kNfoK6NKmYlz/D07+l2GWVK/QfDkNCuQ== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" - "@babel/helper-skip-transparent-expression-wrappers" "^7.22.5" - "@babel/plugin-transform-optional-chaining" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.7" + "@babel/helper-skip-transparent-expression-wrappers" "^7.24.7" + "@babel/plugin-transform-optional-chaining" "^7.24.7" "@babel/plugin-proposal-async-generator-functions@7.20.7": version "7.20.7" - resolved "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.20.7.tgz" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.20.7.tgz#bfb7276d2d573cb67ba379984a2334e262ba5326" integrity sha512-xMbiLsn/8RK7Wq7VeVytytS2L6qE69bXPB10YCmMdDZbKF4okCqY74pI/jJQ/8U0b/F6NrT2+14b8/P9/3AMGA== dependencies: "@babel/helper-environment-visitor" "^7.18.9" @@ -987,12 +727,12 @@ "@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.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz" + 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" integrity sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w== "@babel/plugin-proposal-unicode-property-regex@^7.4.4": version "7.18.6" - resolved "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.18.6.tgz" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.18.6.tgz#af613d2cd5e643643b65cded64207b15c85cb78e" integrity sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w== dependencies: "@babel/helper-create-regexp-features-plugin" "^7.18.6" @@ -1000,468 +740,457 @@ "@babel/plugin-syntax-async-generators@^7.8.4": version "7.8.4" - resolved "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d" integrity sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw== dependencies: "@babel/helper-plugin-utils" "^7.8.0" "@babel/plugin-syntax-class-properties@^7.12.13": version "7.12.13" - resolved "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz#b5c987274c4a3a82b89714796931a6b53544ae10" integrity sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA== dependencies: "@babel/helper-plugin-utils" "^7.12.13" "@babel/plugin-syntax-class-static-block@^7.14.5": version "7.14.5" - resolved "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz#195df89b146b4b78b3bf897fd7a257c84659d406" integrity sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw== dependencies: "@babel/helper-plugin-utils" "^7.14.5" "@babel/plugin-syntax-dynamic-import@^7.8.3": version "7.8.3" - resolved "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz#62bf98b2da3cd21d626154fc96ee5b3cb68eacb3" integrity sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ== dependencies: "@babel/helper-plugin-utils" "^7.8.0" "@babel/plugin-syntax-export-namespace-from@^7.8.3": version "7.8.3" - resolved "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz#028964a9ba80dbc094c915c487ad7c4e7a66465a" integrity sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q== dependencies: "@babel/helper-plugin-utils" "^7.8.3" "@babel/plugin-syntax-import-assertions@^7.22.5": - version "7.22.5" - resolved "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.22.5.tgz" - integrity sha512-rdV97N7KqsRzeNGoWUOK6yUsWarLjE5Su/Snk9IYPU9CwkWHs4t+rTGOvffTR8XGkJMTAdLfO0xVnXm8wugIJg== + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.24.7.tgz#2a0b406b5871a20a841240586b1300ce2088a778" + integrity sha512-Ec3NRUMoi8gskrkBe3fNmEQfxDvY8bgfQpz6jlk/41kX9eUjvpyqWU7PBP/pLAvMaSQjbMNKJmvX57jP+M6bPg== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.7" "@babel/plugin-syntax-import-attributes@^7.22.5": - version "7.22.5" - resolved "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.22.5.tgz" - integrity sha512-KwvoWDeNKPETmozyFE0P2rOLqh39EoQHNjqizrI5B8Vt0ZNS7M56s7dAiAqbYfiAYOuIzIh96z3iR2ktgu3tEg== + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.24.7.tgz#b4f9ea95a79e6912480c4b626739f86a076624ca" + integrity sha512-hbX+lKKeUMGihnK8nvKqmXBInriT3GVjzXKFriV3YC6APGxMbP8RZNFwy91+hocLXq90Mta+HshoB31802bb8A== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.7" "@babel/plugin-syntax-import-meta@^7.10.4": version "7.10.4" - resolved "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz#ee601348c370fa334d2207be158777496521fd51" integrity sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g== dependencies: "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-json-strings@^7.8.3": version "7.8.3" - resolved "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz#01ca21b668cd8218c9e640cb6dd88c5412b2c96a" integrity sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA== dependencies: "@babel/helper-plugin-utils" "^7.8.0" "@babel/plugin-syntax-logical-assignment-operators@^7.10.4": version "7.10.4" - resolved "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699" integrity sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig== dependencies: "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-nullish-coalescing-operator@^7.8.3": version "7.8.3" - resolved "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz#167ed70368886081f74b5c36c65a88c03b66d1a9" integrity sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ== dependencies: "@babel/helper-plugin-utils" "^7.8.0" "@babel/plugin-syntax-numeric-separator@^7.10.4": version "7.10.4" - resolved "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz#b9b070b3e33570cd9fd07ba7fa91c0dd37b9af97" integrity sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug== dependencies: "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-object-rest-spread@^7.8.3": version "7.8.3" - resolved "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871" integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA== dependencies: "@babel/helper-plugin-utils" "^7.8.0" "@babel/plugin-syntax-optional-catch-binding@^7.8.3": version "7.8.3" - resolved "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz#6111a265bcfb020eb9efd0fdfd7d26402b9ed6c1" integrity sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q== dependencies: "@babel/helper-plugin-utils" "^7.8.0" "@babel/plugin-syntax-optional-chaining@^7.8.3": version "7.8.3" - resolved "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz#4f69c2ab95167e0180cd5336613f8c5788f7d48a" integrity sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg== dependencies: "@babel/helper-plugin-utils" "^7.8.0" "@babel/plugin-syntax-private-property-in-object@^7.14.5": version "7.14.5" - resolved "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz#0dc6671ec0ea22b6e94a1114f857970cd39de1ad" integrity sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg== dependencies: "@babel/helper-plugin-utils" "^7.14.5" "@babel/plugin-syntax-top-level-await@^7.14.5": version "7.14.5" - resolved "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz#c1cfdadc35a646240001f06138247b741c34d94c" integrity sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw== dependencies: "@babel/helper-plugin-utils" "^7.14.5" "@babel/plugin-syntax-unicode-sets-regex@^7.18.6": version "7.18.6" - resolved "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz#d49a3b3e6b52e5be6740022317580234a6a47357" integrity sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg== dependencies: "@babel/helper-create-regexp-features-plugin" "^7.18.6" "@babel/helper-plugin-utils" "^7.18.6" "@babel/plugin-transform-arrow-functions@^7.22.5": - version "7.22.5" - resolved "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.22.5.tgz" - integrity sha512-26lTNXoVRdAnsaDXPpvCNUq+OVWEVC6bx7Vvz9rC53F2bagUWW4u4ii2+h8Fejfh7RYqPxn+libeFBBck9muEw== + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.24.7.tgz#4f6886c11e423bd69f3ce51dbf42424a5f275514" + integrity sha512-Dt9LQs6iEY++gXUwY03DNFat5C2NbO48jj+j/bSAz6b3HgPs39qcPiYt77fDObIcFwj3/C2ICX9YMwGflUoSHQ== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.7" "@babel/plugin-transform-async-generator-functions@^7.22.7": - version "7.22.10" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.22.10.tgz#45946cd17f915b10e65c29b8ed18a0a50fc648c8" - integrity sha512-eueE8lvKVzq5wIObKK/7dvoeKJ+xc6TvRn6aysIjS6pSCeLy7S/eVi7pEQknZqyqvzaNKdDtem8nUNTBgDVR2g== + version "7.25.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.25.0.tgz#b785cf35d73437f6276b1e30439a57a50747bddf" + integrity sha512-uaIi2FdqzjpAMvVqvB51S42oC2JEVgh0LDsGfZVDysWE8LrJtQC2jvKmOqEYThKyB7bDEb7BP1GYWDm7tABA0Q== dependencies: - "@babel/helper-environment-visitor" "^7.22.5" - "@babel/helper-plugin-utils" "^7.22.5" - "@babel/helper-remap-async-to-generator" "^7.22.9" + "@babel/helper-plugin-utils" "^7.24.8" + "@babel/helper-remap-async-to-generator" "^7.25.0" "@babel/plugin-syntax-async-generators" "^7.8.4" + "@babel/traverse" "^7.25.0" -"@babel/plugin-transform-async-to-generator@7.22.5", "@babel/plugin-transform-async-to-generator@^7.22.5": +"@babel/plugin-transform-async-to-generator@7.22.5": version "7.22.5" - resolved "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.22.5.tgz" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.22.5.tgz#c7a85f44e46f8952f6d27fe57c2ed3cc084c3775" integrity sha512-b1A8D8ZzE/VhNDoV1MSJTnpKkCG5bJo+19R4o4oy03zM7ws8yEMK755j61Dc3EyvdysbqH5BOOTquJ7ZX9C6vQ== dependencies: "@babel/helper-module-imports" "^7.22.5" "@babel/helper-plugin-utils" "^7.22.5" "@babel/helper-remap-async-to-generator" "^7.22.5" -"@babel/plugin-transform-block-scoped-functions@^7.22.5": - version "7.22.5" - resolved "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.22.5.tgz" - integrity sha512-tdXZ2UdknEKQWKJP1KMNmuF5Lx3MymtMN/pvA+p/VEkhK8jVcQ1fzSy8KM9qRYhAf2/lV33hoMPKI/xaI9sADA== +"@babel/plugin-transform-async-to-generator@^7.22.5": + 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" + integrity sha512-SQY01PcJfmQ+4Ash7NE+rpbLFbmqA2GPIgqzxfFTL4t1FKRq4zTms/7htKpoCUI9OcFYgzqfmCdH53s6/jn5fA== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-module-imports" "^7.24.7" + "@babel/helper-plugin-utils" "^7.24.7" + "@babel/helper-remap-async-to-generator" "^7.24.7" + +"@babel/plugin-transform-block-scoped-functions@^7.22.5": + 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" + integrity sha512-yO7RAz6EsVQDaBH18IDJcMB1HnrUn2FJ/Jslc/WtPPWcjhpUJXU/rjbwmluzp7v/ZzWcEhTMXELnnsz8djWDwQ== + dependencies: + "@babel/helper-plugin-utils" "^7.24.7" "@babel/plugin-transform-block-scoping@^7.22.5": - version "7.22.5" - resolved "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.22.5.tgz" - integrity sha512-EcACl1i5fSQ6bt+YGuU/XGCeZKStLmyVGytWkpyhCLeQVA0eu6Wtiw92V+I1T/hnezUv7j74dA/Ro69gWcU+hg== + version "7.25.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.25.0.tgz#23a6ed92e6b006d26b1869b1c91d1b917c2ea2ac" + integrity sha512-yBQjYoOjXlFv9nlXb3f1casSHOZkWr29NX+zChVanLg5Nc157CrbEX9D7hxxtTpuFy7Q0YzmmWfJxzvps4kXrQ== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.8" "@babel/plugin-transform-class-properties@^7.22.5": - version "7.22.5" - resolved "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.22.5.tgz" - integrity sha512-nDkQ0NfkOhPTq8YCLiWNxp1+f9fCobEjCb0n8WdbNUBc4IB5V7P1QnX9IjpSoquKrXF5SKojHleVNs2vGeHCHQ== + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.24.7.tgz#256879467b57b0b68c7ddfc5b76584f398cd6834" + integrity sha512-vKbfawVYayKcSeSR5YYzzyXvsDFWU2mD8U5TFeXtbCPLFUqe7GyCgvO6XDHzje862ODrOwy6WCPmKeWHbCFJ4w== dependencies: - "@babel/helper-create-class-features-plugin" "^7.22.5" - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-create-class-features-plugin" "^7.24.7" + "@babel/helper-plugin-utils" "^7.24.7" "@babel/plugin-transform-class-static-block@^7.22.5": - version "7.22.5" - resolved "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.22.5.tgz" - integrity sha512-SPToJ5eYZLxlnp1UzdARpOGeC2GbHvr9d/UV0EukuVx8atktg194oe+C5BqQ8jRTkgLRVOPYeXRSBg1IlMoVRA== + 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" + integrity sha512-HMXK3WbBPpZQufbMG4B46A90PkuuhN9vBCb5T8+VAHqvAqvcLi+2cKoukcpmUYkszLhScU3l1iudhrks3DggRQ== dependencies: - "@babel/helper-create-class-features-plugin" "^7.22.5" - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-create-class-features-plugin" "^7.24.7" + "@babel/helper-plugin-utils" "^7.24.7" "@babel/plugin-syntax-class-static-block" "^7.14.5" "@babel/plugin-transform-classes@^7.22.6": - version "7.22.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.22.6.tgz#e04d7d804ed5b8501311293d1a0e6d43e94c3363" - integrity sha512-58EgM6nuPNG6Py4Z3zSuu0xWu2VfodiMi72Jt5Kj2FECmaYk1RrTXA45z6KBFsu9tRgwQDwIiY4FXTt+YsSFAQ== + version "7.25.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.25.0.tgz#63122366527d88e0ef61b612554fe3f8c793991e" + integrity sha512-xyi6qjr/fYU304fiRwFbekzkqVJZ6A7hOjWZd+89FVcBqPV3S9Wuozz82xdpLspckeaafntbzglaW4pqpzvtSw== dependencies: - "@babel/helper-annotate-as-pure" "^7.22.5" - "@babel/helper-compilation-targets" "^7.22.6" - "@babel/helper-environment-visitor" "^7.22.5" - "@babel/helper-function-name" "^7.22.5" - "@babel/helper-optimise-call-expression" "^7.22.5" - "@babel/helper-plugin-utils" "^7.22.5" - "@babel/helper-replace-supers" "^7.22.5" - "@babel/helper-split-export-declaration" "^7.22.6" + "@babel/helper-annotate-as-pure" "^7.24.7" + "@babel/helper-compilation-targets" "^7.24.8" + "@babel/helper-plugin-utils" "^7.24.8" + "@babel/helper-replace-supers" "^7.25.0" + "@babel/traverse" "^7.25.0" globals "^11.1.0" "@babel/plugin-transform-computed-properties@^7.22.5": - version "7.22.5" - resolved "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.22.5.tgz" - integrity sha512-4GHWBgRf0krxPX+AaPtgBAlTgTeZmqDynokHOX7aqqAB4tHs3U2Y02zH6ETFdLZGcg9UQSD1WCmkVrE9ErHeOg== + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.24.7.tgz#4cab3214e80bc71fae3853238d13d097b004c707" + integrity sha512-25cS7v+707Gu6Ds2oY6tCkUwsJ9YIDbggd9+cu9jzzDgiNq7hR/8dkzxWfKWnTic26vsI3EsCXNd4iEB6e8esQ== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" - "@babel/template" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.7" + "@babel/template" "^7.24.7" "@babel/plugin-transform-destructuring@^7.22.5": - version "7.22.5" - resolved "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.22.5.tgz" - integrity sha512-GfqcFuGW8vnEqTUBM7UtPd5A4q797LTvvwKxXTgRsFjoqaJiEg9deBG6kWeQYkVEL569NpnmpC0Pkr/8BLKGnQ== + version "7.24.8" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.24.8.tgz#c828e814dbe42a2718a838c2a2e16a408e055550" + integrity sha512-36e87mfY8TnRxc7yc6M9g9gOB7rKgSahqkIKwLpz4Ppk2+zC2Cy1is0uwtuSG6AE4zlTOUa+7JGz9jCJGLqQFQ== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.8" -"@babel/plugin-transform-dotall-regex@^7.22.5": - version "7.22.5" - resolved "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.22.5.tgz" - integrity sha512-5/Yk9QxCQCl+sOIB1WelKnVRxTJDSAIxtJLL2/pqL14ZVlbH0fUQUZa/T5/UnQtBNgghR7mfB8ERBKyKPCi7Vw== +"@babel/plugin-transform-dotall-regex@^7.22.5", "@babel/plugin-transform-dotall-regex@^7.4.4": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.24.7.tgz#5f8bf8a680f2116a7207e16288a5f974ad47a7a0" + integrity sha512-ZOA3W+1RRTSWvyqcMJDLqbchh7U4NRGqwRfFSVbOLS/ePIP4vHB5e8T8eXcuqyN1QkgKyj5wuW0lcS85v4CrSw== dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.22.5" - "@babel/helper-plugin-utils" "^7.22.5" - -"@babel/plugin-transform-dotall-regex@^7.4.4": - version "7.18.6" - resolved "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.18.6.tgz" - integrity sha512-6S3jpun1eEbAxq7TdjLotAsl4WpQI9DxfkycRcKrjhQYzU87qpXdknpBg/e+TdcMehqGnLFi7tnFUBR02Vq6wg== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.18.6" - "@babel/helper-plugin-utils" "^7.18.6" + "@babel/helper-create-regexp-features-plugin" "^7.24.7" + "@babel/helper-plugin-utils" "^7.24.7" "@babel/plugin-transform-duplicate-keys@^7.22.5": - version "7.22.5" - resolved "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.22.5.tgz" - integrity sha512-dEnYD+9BBgld5VBXHnF/DbYGp3fqGMsyxKbtD1mDyIA7AkTSpKXFhCVuj/oQVOoALfBs77DudA0BE4d5mcpmqw== + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.24.7.tgz#dd20102897c9a2324e5adfffb67ff3610359a8ee" + integrity sha512-JdYfXyCRihAe46jUIliuL2/s0x0wObgwwiGxw/UbgJBr20gQBThrokO4nYKgWkD7uBaqM7+9x5TU7NkExZJyzw== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.7" "@babel/plugin-transform-dynamic-import@^7.22.5": - version "7.22.5" - resolved "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.22.5.tgz" - integrity sha512-0MC3ppTB1AMxd8fXjSrbPa7LT9hrImt+/fcj+Pg5YMD7UQyWp/02+JWpdnCymmsXwIx5Z+sYn1bwCn4ZJNvhqQ== + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.24.7.tgz#4d8b95e3bae2b037673091aa09cd33fecd6419f4" + integrity sha512-sc3X26PhZQDb3JhORmakcbvkeInvxz+A8oda99lj7J60QRuPZvNAk9wQlTBS1ZynelDrDmTU4pw1tyc5d5ZMUg== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.7" "@babel/plugin-syntax-dynamic-import" "^7.8.3" "@babel/plugin-transform-exponentiation-operator@^7.22.5": - version "7.22.5" - resolved "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.22.5.tgz" - integrity sha512-vIpJFNM/FjZ4rh1myqIya9jXwrwwgFRHPjT3DkUA9ZLHuzox8jiXkOLvwm1H+PQIP3CqfC++WPKeuDi0Sjdj1g== + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.24.7.tgz#b629ee22645f412024297d5245bce425c31f9b0d" + integrity sha512-Rqe/vSc9OYgDajNIK35u7ot+KeCoetqQYFXM4Epf7M7ez3lWlOjrDjrwMei6caCVhfdw+mIKD4cgdGNy5JQotQ== dependencies: - "@babel/helper-builder-binary-assignment-operator-visitor" "^7.22.5" - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-builder-binary-assignment-operator-visitor" "^7.24.7" + "@babel/helper-plugin-utils" "^7.24.7" "@babel/plugin-transform-export-namespace-from@^7.22.5": - version "7.22.5" - resolved "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.22.5.tgz" - integrity sha512-X4hhm7FRnPgd4nDA4b/5V280xCx6oL7Oob5+9qVS5C13Zq4bh1qq7LU0GgRU6b5dBWBvhGaXYVB4AcN6+ol6vg== + 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" + integrity sha512-v0K9uNYsPL3oXZ/7F9NNIbAj2jv1whUEtyA6aujhekLs56R++JDQuzRcP2/z4WX5Vg/c5lE9uWZA0/iUoFhLTA== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.7" "@babel/plugin-syntax-export-namespace-from" "^7.8.3" "@babel/plugin-transform-for-of@^7.22.5": - version "7.22.5" - resolved "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.22.5.tgz" - integrity sha512-3kxQjX1dU9uudwSshyLeEipvrLjBCVthCgeTp6CzE/9JYrlAIaeekVxRpCWsDDfYTfRZRoCeZatCQvwo+wvK8A== + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.24.7.tgz#f25b33f72df1d8be76399e1b8f3f9d366eb5bc70" + integrity sha512-wo9ogrDG1ITTTBsy46oGiN1dS9A7MROBTcYsfS8DtsImMkHk9JXJ3EWQM6X2SUw4x80uGPlwj0o00Uoc6nEE3g== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.7" + "@babel/helper-skip-transparent-expression-wrappers" "^7.24.7" "@babel/plugin-transform-function-name@^7.22.5": - version "7.22.5" - resolved "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.22.5.tgz" - integrity sha512-UIzQNMS0p0HHiQm3oelztj+ECwFnj+ZRV4KnguvlsD2of1whUeM6o7wGNj6oLwcDoAXQ8gEqfgC24D+VdIcevg== + version "7.25.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.25.1.tgz#b85e773097526c1a4fc4ba27322748643f26fc37" + integrity sha512-TVVJVdW9RKMNgJJlLtHsKDTydjZAbwIsn6ySBPQaEAUU5+gVvlJt/9nRmqVbsV/IBanRjzWoaAQKLoamWVOUuA== dependencies: - "@babel/helper-compilation-targets" "^7.22.5" - "@babel/helper-function-name" "^7.22.5" - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-compilation-targets" "^7.24.8" + "@babel/helper-plugin-utils" "^7.24.8" + "@babel/traverse" "^7.25.1" "@babel/plugin-transform-json-strings@^7.22.5": - version "7.22.5" - resolved "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.22.5.tgz" - integrity sha512-DuCRB7fu8MyTLbEQd1ew3R85nx/88yMoqo2uPSjevMj3yoN7CDM8jkgrY0wmVxfJZyJ/B9fE1iq7EQppWQmR5A== + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.24.7.tgz#f3e9c37c0a373fee86e36880d45b3664cedaf73a" + integrity sha512-2yFnBGDvRuxAaE/f0vfBKvtnvvqU8tGpMHqMNpTN2oWMKIR3NqFkjaAgGwawhqK/pIN2T3XdjGPdaG0vDhOBGw== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.7" "@babel/plugin-syntax-json-strings" "^7.8.3" "@babel/plugin-transform-literals@^7.22.5": - version "7.22.5" - resolved "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.22.5.tgz" - integrity sha512-fTLj4D79M+mepcw3dgFBTIDYpbcB9Sm0bpm4ppXPaO+U+PKFFyV9MGRvS0gvGw62sd10kT5lRMKXAADb9pWy8g== + version "7.25.2" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.25.2.tgz#deb1ad14fc5490b9a65ed830e025bca849d8b5f3" + integrity sha512-HQI+HcTbm9ur3Z2DkO+jgESMAMcYLuN/A7NRw9juzxAezN9AvqvUTnpKP/9kkYANz6u7dFlAyOu44ejuGySlfw== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.8" "@babel/plugin-transform-logical-assignment-operators@^7.22.5": - version "7.22.5" - resolved "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.22.5.tgz" - integrity sha512-MQQOUW1KL8X0cDWfbwYP+TbVbZm16QmQXJQ+vndPtH/BoO0lOKpVoEDMI7+PskYxH+IiE0tS8xZye0qr1lGzSA== + 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" + integrity sha512-4D2tpwlQ1odXmTEIFWy9ELJcZHqrStlzK/dAOWYyxX3zT0iXQB6banjgeOJQXzEc4S0E0a5A+hahxPaEFYftsw== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.7" "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" "@babel/plugin-transform-member-expression-literals@^7.22.5": - version "7.22.5" - resolved "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.22.5.tgz" - integrity sha512-RZEdkNtzzYCFl9SE9ATaUMTj2hqMb4StarOJLrZRbqqU4HSBE7UlBw9WBWQiDzrJZJdUWiMTVDI6Gv/8DPvfew== + 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" + integrity sha512-T/hRC1uqrzXMKLQ6UCwMT85S3EvqaBXDGf0FaMf4446Qx9vKwlghvee0+uuZcDUCZU5RuNi4781UQ7R308zzBw== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.7" "@babel/plugin-transform-modules-amd@^7.22.5": - version "7.22.5" - resolved "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.22.5.tgz" - integrity sha512-R+PTfLTcYEmb1+kK7FNkhQ1gP4KgjpSO6HfH9+f8/yfp2Nt3ggBjiVpRwmwTlfqZLafYKJACy36yDXlEmI9HjQ== + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.24.7.tgz#65090ed493c4a834976a3ca1cde776e6ccff32d7" + integrity sha512-9+pB1qxV3vs/8Hdmz/CulFB8w2tuu6EB94JZFsjdqxQokwGa9Unap7Bo2gGBGIvPmDIVvQrom7r5m/TCDMURhg== dependencies: - "@babel/helper-module-transforms" "^7.22.5" - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-module-transforms" "^7.24.7" + "@babel/helper-plugin-utils" "^7.24.7" "@babel/plugin-transform-modules-commonjs@^7.22.5": - version "7.22.5" - resolved "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.22.5.tgz" - integrity sha512-B4pzOXj+ONRmuaQTg05b3y/4DuFz3WcCNAXPLb2Q0GT0TrGKGxNKV4jwsXts+StaM0LQczZbOpj8o1DLPDJIiA== + version "7.24.8" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.24.8.tgz#ab6421e564b717cb475d6fff70ae7f103536ea3c" + integrity sha512-WHsk9H8XxRs3JXKWFiqtQebdh9b/pTk4EgueygFzYlTKAg0Ud985mSevdNjdXdFBATSKVJGQXP1tv6aGbssLKA== dependencies: - "@babel/helper-module-transforms" "^7.22.5" - "@babel/helper-plugin-utils" "^7.22.5" - "@babel/helper-simple-access" "^7.22.5" + "@babel/helper-module-transforms" "^7.24.8" + "@babel/helper-plugin-utils" "^7.24.8" + "@babel/helper-simple-access" "^7.24.7" "@babel/plugin-transform-modules-systemjs@^7.22.5": - version "7.22.5" - resolved "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.22.5.tgz" - integrity sha512-emtEpoaTMsOs6Tzz+nbmcePl6AKVtS1yC4YNAeMun9U8YCsgadPNxnOPQ8GhHFB2qdx+LZu9LgoC0Lthuu05DQ== + version "7.25.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.25.0.tgz#8f46cdc5f9e5af74f3bd019485a6cbe59685ea33" + integrity sha512-YPJfjQPDXxyQWg/0+jHKj1llnY5f/R6a0p/vP4lPymxLu7Lvl4k2WMitqi08yxwQcCVUUdG9LCUj4TNEgAp3Jw== dependencies: - "@babel/helper-hoist-variables" "^7.22.5" - "@babel/helper-module-transforms" "^7.22.5" - "@babel/helper-plugin-utils" "^7.22.5" - "@babel/helper-validator-identifier" "^7.22.5" + "@babel/helper-module-transforms" "^7.25.0" + "@babel/helper-plugin-utils" "^7.24.8" + "@babel/helper-validator-identifier" "^7.24.7" + "@babel/traverse" "^7.25.0" "@babel/plugin-transform-modules-umd@^7.22.5": - version "7.22.5" - resolved "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.22.5.tgz" - integrity sha512-+S6kzefN/E1vkSsKx8kmQuqeQsvCKCd1fraCM7zXm4SFoggI099Tr4G8U81+5gtMdUeMQ4ipdQffbKLX0/7dBQ== + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.24.7.tgz#edd9f43ec549099620df7df24e7ba13b5c76efc8" + integrity sha512-3aytQvqJ/h9z4g8AsKPLvD4Zqi2qT+L3j7XoFFu1XBlZWEl2/1kWnhmAbxpLgPrHSY0M6UA02jyTiwUVtiKR6A== dependencies: - "@babel/helper-module-transforms" "^7.22.5" - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-module-transforms" "^7.24.7" + "@babel/helper-plugin-utils" "^7.24.7" "@babel/plugin-transform-named-capturing-groups-regex@^7.22.5": - version "7.22.5" - resolved "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.22.5.tgz" - integrity sha512-YgLLKmS3aUBhHaxp5hi1WJTgOUb/NCuDHzGT9z9WTt3YG+CPRhJs6nprbStx6DnWM4dh6gt7SU3sZodbZ08adQ== + 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" + integrity sha512-/jr7h/EWeJtk1U/uz2jlsCioHkZk1JJZVcc8oQsJ1dUlaJD83f4/6Zeh2aHt9BIFokHIsSeDfhUmju0+1GPd6g== dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.22.5" - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-create-regexp-features-plugin" "^7.24.7" + "@babel/helper-plugin-utils" "^7.24.7" "@babel/plugin-transform-new-target@^7.22.5": - version "7.22.5" - resolved "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.22.5.tgz" - integrity sha512-AsF7K0Fx/cNKVyk3a+DW0JLo+Ua598/NxMRvxDnkpCIGFh43+h/v2xyhRUYf6oD8gE4QtL83C7zZVghMjHd+iw== + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.24.7.tgz#31ff54c4e0555cc549d5816e4ab39241dfb6ab00" + integrity sha512-RNKwfRIXg4Ls/8mMTza5oPF5RkOW8Wy/WgMAp1/F1yZ8mMbtwXW+HDoJiOsagWrAhI5f57Vncrmr9XeT4CVapA== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.7" "@babel/plugin-transform-nullish-coalescing-operator@^7.22.5": - version "7.22.5" - resolved "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.22.5.tgz" - integrity sha512-6CF8g6z1dNYZ/VXok5uYkkBBICHZPiGEl7oDnAx2Mt1hlHVHOSIKWJaXHjQJA5VB43KZnXZDIexMchY4y2PGdA== + 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" + integrity sha512-Ts7xQVk1OEocqzm8rHMXHlxvsfZ0cEF2yomUqpKENHWMF4zKk175Y4q8H5knJes6PgYad50uuRmt3UJuhBw8pQ== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.7" "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" "@babel/plugin-transform-numeric-separator@^7.22.5": - version "7.22.5" - resolved "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.22.5.tgz" - integrity sha512-NbslED1/6M+sXiwwtcAB/nieypGw02Ejf4KtDeMkCEpP6gWFMX1wI9WKYua+4oBneCCEmulOkRpwywypVZzs/g== + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.24.7.tgz#bea62b538c80605d8a0fac9b40f48e97efa7de63" + integrity sha512-e6q1TiVUzvH9KRvicuxdBTUj4AdKSRwzIyFFnfnezpCfP2/7Qmbb8qbU2j7GODbl4JMkblitCQjKYUaX/qkkwA== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.7" "@babel/plugin-syntax-numeric-separator" "^7.10.4" "@babel/plugin-transform-object-rest-spread@^7.22.5": - version "7.22.5" - resolved "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.22.5.tgz" - integrity sha512-Kk3lyDmEslH9DnvCDA1s1kkd3YWQITiBOHngOtDL9Pt6BZjzqb6hiOlb8VfjiiQJ2unmegBqZu0rx5RxJb5vmQ== + 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" + integrity sha512-4QrHAr0aXQCEFni2q4DqKLD31n2DL+RxcwnNjDFkSG0eNQ/xCavnRkfCUjsyqGC2OviNJvZOF/mQqZBw7i2C5Q== dependencies: - "@babel/compat-data" "^7.22.5" - "@babel/helper-compilation-targets" "^7.22.5" - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-compilation-targets" "^7.24.7" + "@babel/helper-plugin-utils" "^7.24.7" "@babel/plugin-syntax-object-rest-spread" "^7.8.3" - "@babel/plugin-transform-parameters" "^7.22.5" + "@babel/plugin-transform-parameters" "^7.24.7" "@babel/plugin-transform-object-super@^7.22.5": - version "7.22.5" - resolved "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.22.5.tgz" - integrity sha512-klXqyaT9trSjIUrcsYIfETAzmOEZL3cBYqOYLJxBHfMFFggmXOv+NYSX/Jbs9mzMVESw/WycLFPRx8ba/b2Ipw== + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.24.7.tgz#66eeaff7830bba945dd8989b632a40c04ed625be" + integrity sha512-A/vVLwN6lBrMFmMDmPPz0jnE6ZGx7Jq7d6sT/Ev4H65RER6pZ+kczlf1DthF5N0qaPHBsI7UXiE8Zy66nmAovg== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" - "@babel/helper-replace-supers" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.7" + "@babel/helper-replace-supers" "^7.24.7" "@babel/plugin-transform-optional-catch-binding@^7.22.5": - version "7.22.5" - resolved "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.22.5.tgz" - integrity sha512-pH8orJahy+hzZje5b8e2QIlBWQvGpelS76C63Z+jhZKsmzfNaPQ+LaW6dcJ9bxTpo1mtXbgHwy765Ro3jftmUg== + 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" + integrity sha512-uLEndKqP5BfBbC/5jTwPxLh9kqPWWgzN/f8w6UwAIirAEqiIVJWWY312X72Eub09g5KF9+Zn7+hT7sDxmhRuKA== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.7" "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" -"@babel/plugin-transform-optional-chaining@^7.22.5": - version "7.22.6" - resolved "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.22.6.tgz" - integrity sha512-Vd5HiWml0mDVtcLHIoEU5sw6HOUW/Zk0acLs/SAeuLzkGNOPc9DB4nkUajemhCmTIz3eiaKREZn2hQQqF79YTg== +"@babel/plugin-transform-optional-chaining@^7.22.6", "@babel/plugin-transform-optional-chaining@^7.24.7": + version "7.24.8" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.24.8.tgz#bb02a67b60ff0406085c13d104c99a835cdf365d" + integrity sha512-5cTOLSMs9eypEy8JUVvIKOu6NgvbJMnpG62VpIHrTmROdQ+L5mDAaI40g25k5vXti55JWNX5jCkq3HZxXBQANw== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" - "@babel/helper-skip-transparent-expression-wrappers" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.8" + "@babel/helper-skip-transparent-expression-wrappers" "^7.24.7" "@babel/plugin-syntax-optional-chaining" "^7.8.3" -"@babel/plugin-transform-optional-chaining@^7.22.6": - version "7.22.10" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.22.10.tgz#076d28a7e074392e840d4ae587d83445bac0372a" - integrity sha512-MMkQqZAZ+MGj+jGTG3OTuhKeBpNcO+0oCEbrGNEaOmiEn+1MzRyQlYsruGiU8RTK3zV6XwrVJTmwiDOyYK6J9g== +"@babel/plugin-transform-parameters@^7.22.5", "@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" + integrity sha512-yGWW5Rr+sQOhK0Ot8hjDJuxU3XLRQGflvT4lhlSY0DFvdb3TwKaY26CJzHtYllU0vT9j58hc37ndFPsqT1SrzA== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" - "@babel/helper-skip-transparent-expression-wrappers" "^7.22.5" - "@babel/plugin-syntax-optional-chaining" "^7.8.3" - -"@babel/plugin-transform-parameters@^7.22.5": - version "7.22.5" - resolved "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.22.5.tgz" - integrity sha512-AVkFUBurORBREOmHRKo06FjHYgjrabpdqRSwq6+C7R5iTCZOsM4QbcB27St0a4U6fffyAOqh3s/qEfybAhfivg== - dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.7" "@babel/plugin-transform-private-methods@^7.22.5": - version "7.22.5" - resolved "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.22.5.tgz" - integrity sha512-PPjh4gyrQnGe97JTalgRGMuU4icsZFnWkzicB/fUtzlKUqvsWBKEpPPfr5a2JiyirZkHxnAqkQMO5Z5B2kK3fA== + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.24.7.tgz#e6318746b2ae70a59d023d5cc1344a2ba7a75f5e" + integrity sha512-COTCOkG2hn4JKGEKBADkA8WNb35TGkkRbI5iT845dB+NyqgO8Hn+ajPbSnIQznneJTa3d30scb6iz/DhH8GsJQ== dependencies: - "@babel/helper-create-class-features-plugin" "^7.22.5" - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-create-class-features-plugin" "^7.24.7" + "@babel/helper-plugin-utils" "^7.24.7" "@babel/plugin-transform-private-property-in-object@^7.22.5": - version "7.22.5" - resolved "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.22.5.tgz" - integrity sha512-/9xnaTTJcVoBtSSmrVyhtSvO3kbqS2ODoh2juEU72c3aYonNF0OMGiaz2gjukyKM2wBBYJP38S4JiE0Wfb5VMQ== + 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" + integrity sha512-9z76mxwnwFxMyxZWEgdgECQglF2Q7cFLm0kMf8pGwt+GSJsY0cONKj/UuO4bOH0w/uAel3ekS4ra5CEAyJRmDA== dependencies: - "@babel/helper-annotate-as-pure" "^7.22.5" - "@babel/helper-create-class-features-plugin" "^7.22.5" - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-annotate-as-pure" "^7.24.7" + "@babel/helper-create-class-features-plugin" "^7.24.7" + "@babel/helper-plugin-utils" "^7.24.7" "@babel/plugin-syntax-private-property-in-object" "^7.14.5" "@babel/plugin-transform-property-literals@^7.22.5": - version "7.22.5" - resolved "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.22.5.tgz" - integrity sha512-TiOArgddK3mK/x1Qwf5hay2pxI6wCZnvQqrFSqbtg1GLl2JcNMitVH/YnqjP+M31pLUeTfzY1HAXFDnUBV30rQ== + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.24.7.tgz#f0d2ed8380dfbed949c42d4d790266525d63bbdc" + integrity sha512-EMi4MLQSHfd2nrCqQEWxFdha2gBCqU4ZcCng4WBGZ5CJL4bBRW0ptdqqDdeirGZcpALazVVNJqRmsO8/+oNCBA== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.7" "@babel/plugin-transform-regenerator@^7.22.5": - version "7.22.5" - resolved "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.22.5.tgz" - integrity sha512-rR7KePOE7gfEtNTh9Qw+iO3Q/e4DEsoQ+hdvM6QUDH7JRJ5qxq5AA52ZzBWbI5i9lfNuvySgOGP8ZN7LAmaiPw== + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.24.7.tgz#021562de4534d8b4b1851759fd7af4e05d2c47f8" + integrity sha512-lq3fvXPdimDrlg6LWBoqj+r/DEWgONuwjuOuQCSYgRroXDH/IdM1C0IZf59fL5cHLpjEH/O6opIRBbqv7ELnuA== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" - regenerator-transform "^0.15.1" + "@babel/helper-plugin-utils" "^7.24.7" + regenerator-transform "^0.15.2" "@babel/plugin-transform-reserved-words@^7.22.5": - version "7.22.5" - resolved "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.22.5.tgz" - integrity sha512-DTtGKFRQUDm8svigJzZHzb/2xatPc6TzNvAIJ5GqOKDsGFYgAskjRulbR/vGsPKq3OPqtexnz327qYpP57RFyA== + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.24.7.tgz#80037fe4fbf031fc1125022178ff3938bb3743a4" + integrity sha512-0DUq0pHcPKbjFZCfTss/pGkYMfy3vFWydkUBd9r0GHpIyfs2eCDENvqadMycRS9wZCXR41wucAfJHJmwA0UmoQ== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.7" "@babel/plugin-transform-runtime@7.22.9": version "7.22.9" @@ -1476,71 +1205,71 @@ semver "^6.3.1" "@babel/plugin-transform-shorthand-properties@^7.22.5": - version "7.22.5" - resolved "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.22.5.tgz" - integrity sha512-vM4fq9IXHscXVKzDv5itkO1X52SmdFBFcMIBZ2FRn2nqVYqw6dBexUgMvAjHW+KXpPPViD/Yo3GrDEBaRC0QYA== + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.24.7.tgz#85448c6b996e122fa9e289746140aaa99da64e73" + integrity sha512-KsDsevZMDsigzbA09+vacnLpmPH4aWjcZjXdyFKGzpplxhbeB4wYtury3vglQkg6KM/xEPKt73eCjPPf1PgXBA== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.7" "@babel/plugin-transform-spread@^7.22.5": - version "7.22.5" - resolved "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.22.5.tgz" - integrity sha512-5ZzDQIGyvN4w8+dMmpohL6MBo+l2G7tfC/O2Dg7/hjpgeWvUx8FzfeOKxGog9IimPa4YekaQ9PlDqTLOljkcxg== + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.24.7.tgz#e8a38c0fde7882e0fb8f160378f74bd885cc7bb3" + integrity sha512-x96oO0I09dgMDxJaANcRyD4ellXFLLiWhuwDxKZX5g2rWP1bTPkBSwCYv96VDXVT1bD9aPj8tppr5ITIh8hBng== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" - "@babel/helper-skip-transparent-expression-wrappers" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.7" + "@babel/helper-skip-transparent-expression-wrappers" "^7.24.7" "@babel/plugin-transform-sticky-regex@^7.22.5": - version "7.22.5" - resolved "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.22.5.tgz" - integrity sha512-zf7LuNpHG0iEeiyCNwX4j3gDg1jgt1k3ZdXBKbZSoA3BbGQGvMiSvfbZRR3Dr3aeJe3ooWFZxOOG3IRStYp2Bw== + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.24.7.tgz#96ae80d7a7e5251f657b5cf18f1ea6bf926f5feb" + integrity sha512-kHPSIJc9v24zEml5geKg9Mjx5ULpfncj0wRpYtxbvKyTtHCYDkVE3aHQ03FrpEo4gEe2vrJJS1Y9CJTaThA52g== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.7" "@babel/plugin-transform-template-literals@^7.22.5": - version "7.22.5" - resolved "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.22.5.tgz" - integrity sha512-5ciOehRNf+EyUeewo8NkbQiUs4d6ZxiHo6BcBcnFlgiJfu16q0bQUw9Jvo0b0gBKFG1SMhDSjeKXSYuJLeFSMA== + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.24.7.tgz#a05debb4a9072ae8f985bcf77f3f215434c8f8c8" + integrity sha512-AfDTQmClklHCOLxtGoP7HkeMw56k1/bTQjwsfhL6pppo/M4TOBSq+jjBUBLmV/4oeFg4GWMavIl44ZeCtmmZTw== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.7" "@babel/plugin-transform-typeof-symbol@^7.22.5": - version "7.22.5" - resolved "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.22.5.tgz" - integrity sha512-bYkI5lMzL4kPii4HHEEChkD0rkc+nvnlR6+o/qdqR6zrm0Sv/nodmyLhlq2DO0YKLUNd2VePmPRjJXSBh9OIdA== + version "7.24.8" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.24.8.tgz#383dab37fb073f5bfe6e60c654caac309f92ba1c" + integrity sha512-adNTUpDCVnmAE58VEqKlAA6ZBlNkMnWD0ZcW76lyNFN3MJniyGFZfNwERVk8Ap56MCnXztmDr19T4mPTztcuaw== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.8" "@babel/plugin-transform-unicode-escapes@^7.22.5": - version "7.22.5" - resolved "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.22.5.tgz" - integrity sha512-biEmVg1IYB/raUO5wT1tgfacCef15Fbzhkx493D3urBI++6hpJ+RFG4SrWMn0NEZLfvilqKf3QDrRVZHo08FYg== + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.24.7.tgz#2023a82ced1fb4971630a2e079764502c4148e0e" + integrity sha512-U3ap1gm5+4edc2Q/P+9VrBNhGkfnf+8ZqppY71Bo/pzZmXhhLdqgaUl6cuB07O1+AQJtCLfaOmswiNbSQ9ivhw== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.7" "@babel/plugin-transform-unicode-property-regex@^7.22.5": - version "7.22.5" - resolved "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.22.5.tgz" - integrity sha512-HCCIb+CbJIAE6sXn5CjFQXMwkCClcOfPCzTlilJ8cUatfzwHlWQkbtV0zD338u9dZskwvuOYTuuaMaA8J5EI5A== + 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" + integrity sha512-uH2O4OV5M9FZYQrwc7NdVmMxQJOCCzFeYudlZSzUAHRFeOujQefa92E74TQDVskNHCzOXoigEuoyzHDhaEaK5w== dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.22.5" - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-create-regexp-features-plugin" "^7.24.7" + "@babel/helper-plugin-utils" "^7.24.7" "@babel/plugin-transform-unicode-regex@^7.22.5": - version "7.22.5" - resolved "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.22.5.tgz" - integrity sha512-028laaOKptN5vHJf9/Arr/HiJekMd41hOEZYvNsrsXqJ7YPYuX2bQxh31fkZzGmq3YqHRJzYFFAVYvKfMPKqyg== + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.24.7.tgz#dfc3d4a51127108099b19817c0963be6a2adf19f" + integrity sha512-hlQ96MBZSAXUq7ltkjtu3FJCCSMx/j629ns3hA3pXnBXjanNP0LHi+JpPeA81zaWgVK1VGH95Xuy7u0RyQ8kMg== dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.22.5" - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-create-regexp-features-plugin" "^7.24.7" + "@babel/helper-plugin-utils" "^7.24.7" "@babel/plugin-transform-unicode-sets-regex@^7.22.5": - version "7.22.5" - resolved "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.22.5.tgz" - integrity sha512-lhMfi4FC15j13eKrh3DnYHjpGj6UKQHtNKTbtc1igvAhRy4+kLhV07OpLcsN0VgDEw/MjAvJO4BdMJsHwMhzCg== + 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" + integrity sha512-2G8aAvF4wy1w/AGZkemprdGMRg5o6zPNhbHVImRz3lss55TYCBd6xStN19rt8XJHq20sqV0JbyWjOWwQRwV/wg== dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.22.5" - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-create-regexp-features-plugin" "^7.24.7" + "@babel/helper-plugin-utils" "^7.24.7" "@babel/preset-env@7.22.9": version "7.22.9" @@ -1629,9 +1358,9 @@ semver "^6.3.1" "@babel/preset-modules@^0.1.5": - version "0.1.5" - resolved "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.5.tgz" - integrity sha512-A57th6YRG7oR3cq/yt/Y84MvGgE0eJG2F1JLhKuyG+jFxEgrd/HAMJatiFtmOiZurz+0DkrvbheCLaV5f2JfjA== + version "0.1.6" + resolved "https://registry.yarnpkg.com/@babel/preset-modules/-/preset-modules-0.1.6.tgz#31bcdd8f19538437339d17af00d177d854d9d458" + integrity sha512-ID2yj6K/4lKfhuU3+EX4UvNbIt7eACFbHmNUjzA+ep+B5971CknnA/9DEWKbRokfbbtblxxxXFJJrH47UEAMVg== dependencies: "@babel/helper-plugin-utils" "^7.0.0" "@babel/plugin-proposal-unicode-property-regex" "^7.4.4" @@ -1641,7 +1370,7 @@ "@babel/regjsgen@^0.8.0": version "0.8.0" - resolved "https://registry.npmjs.org/@babel/regjsgen/-/regjsgen-0.8.0.tgz" + resolved "https://registry.yarnpkg.com/@babel/regjsgen/-/regjsgen-0.8.0.tgz#f0ba69b075e1f05fb2825b7fad991e7adbb18310" integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA== "@babel/runtime@7.22.6": @@ -1652,154 +1381,115 @@ regenerator-runtime "^0.13.11" "@babel/runtime@^7.8.4": - version "7.21.5" - resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.5.tgz" - integrity sha512-8jI69toZqqcsnqGGqwGS4Qb1VwLOEp4hz+CXPywcvjs60u3B4Pom/U/7rm4W8tMOYEB+E9wgD0mW1l3r8qlI9Q== + version "7.25.0" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.25.0.tgz#3af9a91c1b739c569d5d80cc917280919c544ecb" + integrity sha512-7dRy4DwXwtzBrPbZflqxnvfxLF8kdZXPkhymtDeFoFqE6ldzjQFgYTtYIFARcLEYDrqfBfYcZt1WqFxRoyC9Rw== dependencies: - regenerator-runtime "^0.13.11" + regenerator-runtime "^0.14.0" -"@babel/template@7.22.5", "@babel/template@^7.22.5": +"@babel/template@7.22.5": version "7.22.5" - resolved "https://registry.npmjs.org/@babel/template/-/template-7.22.5.tgz" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.5.tgz#0c8c4d944509875849bd0344ff0050756eefc6ec" integrity sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw== dependencies: "@babel/code-frame" "^7.22.5" "@babel/parser" "^7.22.5" "@babel/types" "^7.22.5" -"@babel/template@^7.18.10", "@babel/template@^7.20.7": - version "7.20.7" - resolved "https://registry.npmjs.org/@babel/template/-/template-7.20.7.tgz" - integrity sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw== +"@babel/template@^7.22.15", "@babel/template@^7.22.5", "@babel/template@^7.24.7", "@babel/template@^7.25.0": + version "7.25.0" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.25.0.tgz#e733dc3134b4fede528c15bc95e89cb98c52592a" + integrity sha512-aOOgh1/5XzKvg1jvVz7AVrx2piJ2XBi227DHmbY6y+bM9H2FlN+IfecYu4Xl0cNiiVejlsCri89LUsbj8vJD9Q== dependencies: - "@babel/code-frame" "^7.18.6" - "@babel/parser" "^7.20.7" - "@babel/types" "^7.20.7" + "@babel/code-frame" "^7.24.7" + "@babel/parser" "^7.25.0" + "@babel/types" "^7.25.0" -"@babel/template@^7.22.15": - version "7.22.15" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.15.tgz#09576efc3830f0430f4548ef971dde1350ef2f38" - integrity sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w== +"@babel/traverse@^7.22.8", "@babel/traverse@^7.23.2", "@babel/traverse@^7.24.7", "@babel/traverse@^7.24.8", "@babel/traverse@^7.25.0", "@babel/traverse@^7.25.1", "@babel/traverse@^7.25.2": + version "7.25.2" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.25.2.tgz#1a0a4aef53177bead359ccd0c89f4426c805b2ae" + integrity sha512-s4/r+a7xTnny2O6FcZzqgT6nE4/GHEdcqj4qAeglbUOh0TeglEfmNJFAd/OLoVtGd6ZhAO8GCVvCNUO5t/VJVQ== dependencies: - "@babel/code-frame" "^7.22.13" - "@babel/parser" "^7.22.15" - "@babel/types" "^7.22.15" - -"@babel/traverse@^7.20.5", "@babel/traverse@^7.21.5", "@babel/traverse@^7.22.10", "@babel/traverse@^7.22.5", "@babel/traverse@^7.22.6", "@babel/traverse@^7.22.8": - version "7.23.2" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.23.2.tgz#329c7a06735e144a506bdb2cad0268b7f46f4ad8" - integrity sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw== - dependencies: - "@babel/code-frame" "^7.22.13" - "@babel/generator" "^7.23.0" - "@babel/helper-environment-visitor" "^7.22.20" - "@babel/helper-function-name" "^7.23.0" - "@babel/helper-hoist-variables" "^7.22.5" - "@babel/helper-split-export-declaration" "^7.22.6" - "@babel/parser" "^7.23.0" - "@babel/types" "^7.23.0" - debug "^4.1.0" + "@babel/code-frame" "^7.24.7" + "@babel/generator" "^7.25.0" + "@babel/parser" "^7.25.0" + "@babel/template" "^7.25.0" + "@babel/types" "^7.25.2" + debug "^4.3.1" globals "^11.1.0" -"@babel/types@^7.18.6", "@babel/types@^7.18.9", "@babel/types@^7.20.5", "@babel/types@^7.20.7", "@babel/types@^7.21.0", "@babel/types@^7.21.4", "@babel/types@^7.21.5", "@babel/types@^7.4.4": - version "7.21.5" - resolved "https://registry.npmjs.org/@babel/types/-/types-7.21.5.tgz" - integrity sha512-m4AfNvVF2mVC/F7fDEdH2El3HzUg9It/XsCxZiOTTA3m3qYfcSVSbTfM6Q9xG+hYDniZssYhlXKKUMD5m8tF4Q== +"@babel/types@^7.22.5", "@babel/types@^7.23.0", "@babel/types@^7.24.7", "@babel/types@^7.24.8", "@babel/types@^7.25.0", "@babel/types@^7.25.2", "@babel/types@^7.4.4": + version "7.25.2" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.25.2.tgz#55fb231f7dc958cd69ea141a4c2997e819646125" + integrity sha512-YTnYtra7W9e6/oAZEHj0bJehPRUlLH9/fbpT5LfB0NhQXyALCRkRs3zH9v07IYhkgpqX6Z78FnuccZr/l4Fs4Q== dependencies: - "@babel/helper-string-parser" "^7.21.5" - "@babel/helper-validator-identifier" "^7.19.1" + "@babel/helper-string-parser" "^7.24.8" + "@babel/helper-validator-identifier" "^7.24.7" to-fast-properties "^2.0.0" -"@babel/types@^7.22.10": - version "7.22.10" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.22.10.tgz#4a9e76446048f2c66982d1a989dd12b8a2d2dc03" - integrity sha512-obaoigiLrlDZ7TUQln/8m4mSqIW2QFeOrCQc9r+xsaHGNoplVNYlRVpsfE8Vj35GEm2ZH4ZhrNYogs/3fj85kg== - dependencies: - "@babel/helper-string-parser" "^7.22.5" - "@babel/helper-validator-identifier" "^7.22.5" - to-fast-properties "^2.0.0" +"@bufbuild/buf-darwin-arm64@1.35.1": + version "1.35.1" + resolved "https://registry.yarnpkg.com/@bufbuild/buf-darwin-arm64/-/buf-darwin-arm64-1.35.1.tgz#7d7567180df771e94cc95267276da7966be7b90a" + integrity sha512-Yy+sk+8sg3LDvMSZLGUIoMCkZajkQSZkdxO96mpqJagKlEYPLGTtakVFCVNX9KgK/sv1bd9sU55iMGXE3+eIYw== -"@babel/types@^7.22.15", "@babel/types@^7.23.0": - version "7.23.0" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.23.0.tgz#8c1f020c9df0e737e4e247c0619f58c68458aaeb" - integrity sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg== - dependencies: - "@babel/helper-string-parser" "^7.22.5" - "@babel/helper-validator-identifier" "^7.22.20" - to-fast-properties "^2.0.0" +"@bufbuild/buf-darwin-x64@1.35.1": + version "1.35.1" + resolved "https://registry.yarnpkg.com/@bufbuild/buf-darwin-x64/-/buf-darwin-x64-1.35.1.tgz#05b6dc00944aa2150acf67f4c6f1d8592312f0de" + integrity sha512-LcscoNTCHFeb5y9sitw4w6HWZtJ4Ja/MDBCUU9A8/OGHJSESV0JjhbvVHGNOIsKUbPq5p/SVjYA/Ab/wlmmpaA== -"@babel/types@^7.22.5": - version "7.22.5" - resolved "https://registry.npmjs.org/@babel/types/-/types-7.22.5.tgz" - integrity sha512-zo3MIHGOkPOfoRXitsgHLjEXmlDaD/5KU1Uzuc9GNiZPhSqVxVRtxuPaSBZDsYZ9qV88AjtMtWW7ww98loJ9KA== - dependencies: - "@babel/helper-string-parser" "^7.22.5" - "@babel/helper-validator-identifier" "^7.22.5" - to-fast-properties "^2.0.0" +"@bufbuild/buf-linux-aarch64@1.35.1": + version "1.35.1" + resolved "https://registry.yarnpkg.com/@bufbuild/buf-linux-aarch64/-/buf-linux-aarch64-1.35.1.tgz#fb26dbe860229759c224a3c91d5e77dab1874113" + integrity sha512-bPeiSURl8WFxCdawtJjAjUOMqknVTw763NLIDcbYSH1/wTiUbM5QeXCORRlHKXtMGM89SYU5AatcY9UhQ+sn9g== -"@bufbuild/buf-darwin-arm64@1.26.1": - version "1.26.1" - resolved "https://registry.yarnpkg.com/@bufbuild/buf-darwin-arm64/-/buf-darwin-arm64-1.26.1.tgz#6085067537074440c098abe1f9cf86be637ad6f2" - integrity sha512-nmyWiT/59RFja0ZuXFxjNGoAMDPTApU66CZUUevkFVWbNB9nzeQDjx2vsJyACY64k5fTgZiaelSiyppwObQknw== +"@bufbuild/buf-linux-x64@1.35.1": + version "1.35.1" + resolved "https://registry.yarnpkg.com/@bufbuild/buf-linux-x64/-/buf-linux-x64-1.35.1.tgz#552399c5f42dbbef21968771e6364306c4667313" + integrity sha512-n6ziazYjNH9H1JjHiacGi20rIyZuKnsHjF8qWisO8KGajhnS/7tpq0VzYdorqqWyJ1TcnLBWHj+dWYuGay9Nag== -"@bufbuild/buf-darwin-x64@1.26.1": - version "1.26.1" - resolved "https://registry.npmjs.org/@bufbuild/buf-darwin-x64/-/buf-darwin-x64-1.26.1.tgz" - integrity sha512-jl5WmUv30OW8JiRLid9+mVx1XVH0XttpUfkQfmqDFdUHGfdy4XWYK8kr84YyWu0SiMTIt1mPXkqG5UM3x+tdIQ== +"@bufbuild/buf-win32-arm64@1.35.1": + version "1.35.1" + resolved "https://registry.yarnpkg.com/@bufbuild/buf-win32-arm64/-/buf-win32-arm64-1.35.1.tgz#efd0a6a2159a135173becfbe362651a4a4e1dd4d" + integrity sha512-3B65+iA1i/LDjJBseEpAvrkEI7VJqrvW39PyYVkIXSHHT917O+n95g74pn38A0XkggN5lEibLEkipBMDUfwMew== -"@bufbuild/buf-linux-aarch64@1.26.1": - version "1.26.1" - resolved "https://registry.yarnpkg.com/@bufbuild/buf-linux-aarch64/-/buf-linux-aarch64-1.26.1.tgz#45cc1f69ce64beb6aa1dce0eefd7d04db2ed7790" - integrity sha512-EedR2KDW/yDIxQKWuq1Y/g7IuwTgvelqylGVO7muMxt2JWShobyUaU6GIU8JB4yhIbqRQYCL2KqBsvDJbJtCUw== +"@bufbuild/buf-win32-x64@1.35.1": + version "1.35.1" + resolved "https://registry.yarnpkg.com/@bufbuild/buf-win32-x64/-/buf-win32-x64-1.35.1.tgz#24cd639b4b692233c4ba6004263933b433a2ff13" + integrity sha512-iafrcs+1FMlD+3ZjI1kVBHGOluT6YcoAUETrGMbQjRha6dL5s2Ldr0G7zCKLIT13yEKG5QTyP8z8gVEpk8C8wg== -"@bufbuild/buf-linux-x64@1.26.1": - version "1.26.1" - resolved "https://registry.yarnpkg.com/@bufbuild/buf-linux-x64/-/buf-linux-x64-1.26.1.tgz#32c1395497d9a877671b74191d2d199d078b1e34" - integrity sha512-5iFL+MmWqR4cBLVNpgsjRecdHgcTxFaIkVYlQV9q8acbaJn5rgOIjUr1tzcBao9YsL3rdBhHvKkgnQ9gi1IiTw== - -"@bufbuild/buf-win32-arm64@1.26.1": - version "1.26.1" - resolved "https://registry.yarnpkg.com/@bufbuild/buf-win32-arm64/-/buf-win32-arm64-1.26.1.tgz#839001f409c111ddf5745a1278128b4c49e9c8d0" - integrity sha512-/ayymSD12gBetN98ErkH0CBGRLTmtYAp4fmbPuvq8zuJcL0eiAnK6d7ZFjTc+vDMuKY/aelQN7dj9WhzdYAQSQ== - -"@bufbuild/buf-win32-x64@1.26.1": - version "1.26.1" - resolved "https://registry.yarnpkg.com/@bufbuild/buf-win32-x64/-/buf-win32-x64-1.26.1.tgz#483ebcdc31944cef6e78c0a4e01208252baf7ab2" - integrity sha512-k9Dy3Z9P96wYR43lUhUo0jbjMSo001+MRBlsadEYiw85POqx6RWVaGyHLrxC2Ly7g+aGMisey050OjqfCWtKTA== - -"@bufbuild/buf@^1.23.1": - version "1.26.1" - resolved "https://registry.npmjs.org/@bufbuild/buf/-/buf-1.26.1.tgz" - integrity sha512-NyYx4T//3ndtFYV3BfqX9Xrm1NZEx3eChXniAKc/osCVViFooC5nuLQUbyqglMonH0w39RohiURMXN+e/oEB4g== +"@bufbuild/buf@^1.34.0": + version "1.35.1" + resolved "https://registry.yarnpkg.com/@bufbuild/buf/-/buf-1.35.1.tgz#46a700b94b463919f21313962e539f63448c7d90" + integrity sha512-POtbb4wRhvgCmmClnuaQTpkHL4ukhFItuS/AaD7QDY0kamn4ExNJz4XlHG5jeJODaQ1Wq3f9qn7UIgUr6CUODw== optionalDependencies: - "@bufbuild/buf-darwin-arm64" "1.26.1" - "@bufbuild/buf-darwin-x64" "1.26.1" - "@bufbuild/buf-linux-aarch64" "1.26.1" - "@bufbuild/buf-linux-x64" "1.26.1" - "@bufbuild/buf-win32-arm64" "1.26.1" - "@bufbuild/buf-win32-x64" "1.26.1" + "@bufbuild/buf-darwin-arm64" "1.35.1" + "@bufbuild/buf-darwin-x64" "1.35.1" + "@bufbuild/buf-linux-aarch64" "1.35.1" + "@bufbuild/buf-linux-x64" "1.35.1" + "@bufbuild/buf-win32-arm64" "1.35.1" + "@bufbuild/buf-win32-x64" "1.35.1" "@colors/colors@1.5.0": version "1.5.0" - resolved "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz" + resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ== "@ctrl/ngx-codemirror@^6.1.0": version "6.1.0" - resolved "https://registry.npmjs.org/@ctrl/ngx-codemirror/-/ngx-codemirror-6.1.0.tgz" + resolved "https://registry.yarnpkg.com/@ctrl/ngx-codemirror/-/ngx-codemirror-6.1.0.tgz#9324a56e4b709be9c515364d21e05e1d7589f009" integrity sha512-73QeoNbnluZalWmNw+SOFctsE+oz0+4Bl9KhlJOIfbCPK/U6OIc+vQNr28hMAp15Y/Idde3LntTwLBDFW0bRVA== dependencies: "@types/codemirror" "^5.60.5" tslib "^2.3.0" "@ctrl/tinycolor@^3.6.0": - version "3.6.0" - resolved "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-3.6.0.tgz" - integrity sha512-/Z3l6pXthq0JvMYdUFyX9j0MaCltlIn6mfh9jLyQwg5aPKxkyNa0PTHtU1AlFXLNk55ZuAeJRcpvq+tmLfKmaQ== + version "3.6.1" + resolved "https://registry.yarnpkg.com/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz#b6c75a56a1947cc916ea058772d666a2c8932f31" + integrity sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA== "@discoveryjs/json-ext@0.5.7": version "0.5.7" - resolved "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz" + resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70" integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw== "@esbuild/android-arm64@0.18.17": @@ -2024,20 +1714,20 @@ "@eslint-community/eslint-utils@^4.2.0": version "4.4.0" - resolved "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz" + resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" integrity sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA== dependencies: eslint-visitor-keys "^3.3.0" "@eslint-community/regexpp@^4.4.0", "@eslint-community/regexpp@^4.6.1": - version "4.6.2" - resolved "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.6.2.tgz" - integrity sha512-pPTNuaAG3QMH+buKyBIGJs3g/S5y0caxw0ygM3YyE6yJFySwiGGSzA+mM3KJ8QQvzeLh3blwgSonkFjgQdxzMw== + version "4.11.0" + resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.11.0.tgz#b0ffd0312b4a3fd2d6f77237e7248a5ad3a680ae" + integrity sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A== -"@eslint/eslintrc@^2.1.2": - version "2.1.2" - resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.2.tgz#c6936b4b328c64496692f76944e755738be62396" - integrity sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g== +"@eslint/eslintrc@^2.1.4": + version "2.1.4" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.4.tgz#388a269f0f25c1b6adc317b5a2c55714894c70ad" + integrity sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ== dependencies: ajv "^6.12.4" debug "^4.3.2" @@ -2049,10 +1739,10 @@ minimatch "^3.1.2" strip-json-comments "^3.1.1" -"@eslint/js@8.50.0": - version "8.50.0" - resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.50.0.tgz#9e93b850f0f3fa35f5fa59adfd03adae8488e484" - integrity sha512-NCC3zz2+nvYd+Ckfh87rA47zfu2QsQpvc6k1yzTk+b9KzRj0wkGa8LSoGOXN6Zv4lRf/EIoZ80biDh9HOI+RNQ== +"@eslint/js@8.57.0": + version "8.57.0" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.0.tgz#a5417ae8427873f1dd08b70b3574b453e67b5f7f" + integrity sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g== "@fortawesome/angular-fontawesome@^0.13.0": version "0.13.0" @@ -2061,70 +1751,70 @@ dependencies: tslib "^2.4.1" -"@fortawesome/fontawesome-common-types@6.4.2": - version "6.4.2" - resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.4.2.tgz#1766039cad33f8ad87f9467b98e0d18fbc8f01c5" - integrity sha512-1DgP7f+XQIJbLFCTX1V2QnxVmpLdKdzzo2k8EmvDOePfchaIGQ9eCHj2up3/jNEbZuBqel5OxiaOJf37TWauRA== +"@fortawesome/fontawesome-common-types@6.6.0": + version "6.6.0" + resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.6.0.tgz#31ab07ca6a06358c5de4d295d4711b675006163f" + integrity sha512-xyX0X9mc0kyz9plIyryrRbl7ngsA9jz77mCZJsUkLl+ZKs0KWObgaEBoSgQiYWAsSmjz/yjl0F++Got0Mdp4Rw== "@fortawesome/fontawesome-svg-core@^6.4.2": - version "6.4.2" - resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.4.2.tgz#37f4507d5ec645c8b50df6db14eced32a6f9be09" - integrity sha512-gjYDSKv3TrM2sLTOKBc5rH9ckje8Wrwgx1CxAPbN5N3Fm4prfi7NsJVWd1jklp7i5uSCVwhZS5qlhMXqLrpAIg== + version "6.6.0" + resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.6.0.tgz#2a24c32ef92136e98eae2ff334a27145188295ff" + integrity sha512-KHwPkCk6oRT4HADE7smhfsKudt9N/9lm6EJ5BVg0tD1yPA5hht837fB87F8pn15D8JfTqQOjhKTktwmLMiD7Kg== dependencies: - "@fortawesome/fontawesome-common-types" "6.4.2" + "@fortawesome/fontawesome-common-types" "6.6.0" "@fortawesome/free-brands-svg-icons@^6.4.2": - version "6.4.2" - resolved "https://registry.yarnpkg.com/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-6.4.2.tgz#9b8e78066ea6dd563da5dfa686615791d0f7cc71" - integrity sha512-LKOwJX0I7+mR/cvvf6qIiqcERbdnY+24zgpUSouySml+5w8B4BJOx8EhDR/FTKAu06W12fmUIcv6lzPSwYKGGg== + version "6.6.0" + resolved "https://registry.yarnpkg.com/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-6.6.0.tgz#2797f2cc66d21e7e47fa64e680b8835e8d30e825" + integrity sha512-1MPD8lMNW/earme4OQi1IFHtmHUwAKgghXlNwWi9GO7QkTfD+IIaYpIai4m2YJEzqfEji3jFHX1DZI5pbY/biQ== dependencies: - "@fortawesome/fontawesome-common-types" "6.4.2" + "@fortawesome/fontawesome-common-types" "6.6.0" "@gar/promisify@^1.1.3": version "1.1.3" - resolved "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz" + resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.3.tgz#555193ab2e3bb3b6adc3d551c9c030d9e860daf6" integrity sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw== -"@grpc/grpc-js@^1.9.3": - version "1.9.3" - resolved "https://registry.yarnpkg.com/@grpc/grpc-js/-/grpc-js-1.9.3.tgz#811cc49966ab7ed96efa31d213e80d671fd13839" - integrity sha512-b8iWtdrYIeT5fdZdS4Br/6h/kuk0PW5EVBUGk1amSbrpL8DlktJD43CdcCWwRdd6+jgwHhADSbL9CsNnm6EUPA== +"@grpc/grpc-js@^1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@grpc/grpc-js/-/grpc-js-1.11.1.tgz#a92f33e98f1959feffcd1b25a33b113d2c977b70" + integrity sha512-gyt/WayZrVPH2w/UTLansS7F9Nwld472JxxaETamrM8HNlsa+jSLNyKAZmhxI2Me4c3mQHFiS1wWHDY1g1Kthw== dependencies: - "@grpc/proto-loader" "^0.7.8" - "@types/node" ">=12.12.47" + "@grpc/proto-loader" "^0.7.13" + "@js-sdsl/ordered-map" "^4.4.2" -"@grpc/proto-loader@^0.7.8": - version "0.7.10" - resolved "https://registry.yarnpkg.com/@grpc/proto-loader/-/proto-loader-0.7.10.tgz#6bf26742b1b54d0a473067743da5d3189d06d720" - integrity sha512-CAqDfoaQ8ykFd9zqBDn4k6iWT9loLAlc2ETmDFS9JCD70gDcnA4L3AFEo2iV7KyAtAAHFW9ftq1Fz+Vsgq80RQ== +"@grpc/proto-loader@^0.7.13": + version "0.7.13" + resolved "https://registry.yarnpkg.com/@grpc/proto-loader/-/proto-loader-0.7.13.tgz#f6a44b2b7c9f7b609f5748c6eac2d420e37670cf" + integrity sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw== dependencies: lodash.camelcase "^4.3.0" long "^5.0.0" - protobufjs "^7.2.4" + protobufjs "^7.2.5" yargs "^17.7.2" -"@humanwhocodes/config-array@^0.11.11": - version "0.11.11" - resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.11.tgz#88a04c570dbbc7dd943e4712429c3df09bc32844" - integrity sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA== +"@humanwhocodes/config-array@^0.11.14": + version "0.11.14" + resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.14.tgz#d78e481a039f7566ecc9660b4ea7fe6b1fec442b" + integrity sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg== dependencies: - "@humanwhocodes/object-schema" "^1.2.1" - debug "^4.1.1" + "@humanwhocodes/object-schema" "^2.0.2" + debug "^4.3.1" minimatch "^3.0.5" "@humanwhocodes/module-importer@^1.0.1": version "1.0.1" - resolved "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz" + resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c" integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== -"@humanwhocodes/object-schema@^1.2.1": - version "1.2.1" - resolved "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz" - integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== +"@humanwhocodes/object-schema@^2.0.2": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz#4a2868d75d6d6963e423bcf90b7fd1be343409d3" + integrity sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA== "@isaacs/cliui@^8.0.2": version "8.0.2" - resolved "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz" + resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550" integrity sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA== dependencies: string-width "^5.1.2" @@ -2136,7 +1826,7 @@ "@istanbuljs/load-nyc-config@^1.0.0": version "1.1.0" - resolved "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz" + resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced" integrity sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ== dependencies: camelcase "^5.3.1" @@ -2147,66 +1837,58 @@ "@istanbuljs/schema@^0.1.2": version "0.1.3" - resolved "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz" + resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.3.tgz#e45e384e4b8ec16bce2fd903af78450f6bf7ec98" integrity sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA== -"@jridgewell/gen-mapping@^0.3.0", "@jridgewell/gen-mapping@^0.3.2": - version "0.3.3" - resolved "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz" - integrity sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ== +"@jridgewell/gen-mapping@^0.3.0", "@jridgewell/gen-mapping@^0.3.2", "@jridgewell/gen-mapping@^0.3.5": + version "0.3.5" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz#dcce6aff74bdf6dad1a95802b69b04a2fcb1fb36" + integrity sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg== dependencies: - "@jridgewell/set-array" "^1.0.1" + "@jridgewell/set-array" "^1.2.1" "@jridgewell/sourcemap-codec" "^1.4.10" - "@jridgewell/trace-mapping" "^0.3.9" + "@jridgewell/trace-mapping" "^0.3.24" -"@jridgewell/resolve-uri@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz" - integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w== +"@jridgewell/resolve-uri@^3.1.0": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" + integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== -"@jridgewell/set-array@^1.0.1": - version "1.1.2" - resolved "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz" - integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw== - -"@jridgewell/source-map@^0.3.2": - version "0.3.3" - resolved "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.3.tgz" - integrity sha512-b+fsZXeLYi9fEULmfBrhxn4IrPlINf8fiNarzTof004v3lFdntdwa9PF7vFJqm3mg7s+ScJMxXaE3Acp1irZcg== - dependencies: - "@jridgewell/gen-mapping" "^0.3.0" - "@jridgewell/trace-mapping" "^0.3.9" +"@jridgewell/set-array@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.2.1.tgz#558fb6472ed16a4c850b889530e6b36438c49280" + integrity sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A== "@jridgewell/source-map@^0.3.3": - version "0.3.5" - resolved "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz" - integrity sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ== + version "0.3.6" + resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.6.tgz#9d71ca886e32502eb9362c9a74a46787c36df81a" + integrity sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ== dependencies: - "@jridgewell/gen-mapping" "^0.3.0" - "@jridgewell/trace-mapping" "^0.3.9" - -"@jridgewell/sourcemap-codec@1.4.14": - version "1.4.14" - resolved "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz" - integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.25" "@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.4.15": - version "1.4.15" - resolved "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz" - integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== + version "1.5.0" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz#3188bcb273a414b0d215fd22a58540b989b9409a" + integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ== -"@jridgewell/trace-mapping@^0.3.17", "@jridgewell/trace-mapping@^0.3.9": - version "0.3.18" - resolved "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz" - integrity sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA== +"@jridgewell/trace-mapping@^0.3.17", "@jridgewell/trace-mapping@^0.3.20", "@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25", "@jridgewell/trace-mapping@^0.3.9": + version "0.3.25" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz#15f190e98895f3fc23276ee14bc76b675c2e50f0" + integrity sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ== dependencies: - "@jridgewell/resolve-uri" "3.1.0" - "@jridgewell/sourcemap-codec" "1.4.14" + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" + +"@js-sdsl/ordered-map@^4.4.2": + version "4.4.2" + resolved "https://registry.yarnpkg.com/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz#9299f82874bab9e4c7f9c48d865becbfe8d6907c" + integrity sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw== "@leichtgewicht/ip-codec@^2.0.1": - version "2.0.4" - resolved "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz" - integrity sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A== + version "2.0.5" + resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz#4fc56c15c580b9adb7dc3c333a134e540b44bfb1" + integrity sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw== "@material/animation@15.0.0-canary.bc9ae6c9c.0": version "15.0.0-canary.bc9ae6c9c.0" @@ -2910,10 +2592,10 @@ "@material/theme" "15.0.0-canary.bc9ae6c9c.0" tslib "^2.1.0" -"@netlify/framework-info@^9.8.10": - version "9.8.10" - resolved "https://registry.yarnpkg.com/@netlify/framework-info/-/framework-info-9.8.10.tgz#a18589f132dafb5cb7f86c05a9895b9118633fe1" - integrity sha512-VT8ejAaB/XU2xRpdpQinHUO1YL3+BMx6LJ49wJk2u9Yq/VI1/gYCi5VqbqTHBQXJUlOi84YuiRlrDBsLpPr8eg== +"@netlify/framework-info@^9.8.13": + version "9.8.13" + resolved "https://registry.yarnpkg.com/@netlify/framework-info/-/framework-info-9.8.13.tgz#0a4cc2be4c2439089f9b630e19d73e2f4b09289d" + integrity sha512-ZZXCggokY/y5Sz93XYbl/Lig1UAUSWPMBiQRpkVfbrrkjmW2ZPkYS/BgrM2/MxwXRvYhc/TQpZX6y5JPe3quQg== dependencies: ajv "^8.12.0" filter-obj "^5.0.0" @@ -2923,27 +2605,22 @@ p-filter "^3.0.0" p-locate "^6.0.0" process "^0.11.10" - read-pkg-up "^9.0.0" + read-pkg-up "^9.1.0" semver "^7.3.8" -"@ngtools/webpack@16.2.2": - version "16.2.2" - resolved "https://registry.yarnpkg.com/@ngtools/webpack/-/webpack-16.2.2.tgz#55fac744d1aca4542fb9a4ff16a48d2b384ffd37" - integrity sha512-BDZ2yyXdzVE8kILOM0lhRpmKlvfLMluuZvqVa1r5dHkjCLbyOo1jXoYTCXvrQ2JU5GXc/MBBLXwmIHgtPWk8/A== +"@ngtools/webpack@16.2.14": + version "16.2.14" + resolved "https://registry.yarnpkg.com/@ngtools/webpack/-/webpack-16.2.14.tgz#7af4a1afa7c5b8e6ec8bca9491b91cfade694ff7" + integrity sha512-3+zPP3Wir46qrZ3FEiTz5/emSoVHYUCH+WgBmJ57mZCx1qBOYh2VgllnPr/Yusl1sc/jUZjdwq/es/9ZNw+zDQ== "@ngx-translate/core@^15.0.0": version "15.0.0" resolved "https://registry.yarnpkg.com/@ngx-translate/core/-/core-15.0.0.tgz#0fe55b9bd47e75b03d1123658f15fb7b5a534f3c" integrity sha512-Am5uiuR0bOOxyoercDnAA3rJVizo4RRqJHo8N3RqJ+XfzVP/I845yEnMADykOHvM6HkVm4SZSnJBOiz0Anx5BA== -"@nicolo-ribaudo/semver-v6@^6.3.3": - version "6.3.3" - resolved "https://registry.npmjs.org/@nicolo-ribaudo/semver-v6/-/semver-v6-6.3.3.tgz" - integrity sha512-3Yc1fUTs69MG/uZbJlLSI3JISMn2UV2rg+1D/vROUqZyh3l6iYHCs7GMp+M40ZD7yOdDbYjJcU1oTJhrc+dGKg== - "@nodelib/fs.scandir@2.1.5": version "2.1.5" - resolved "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz" + resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== dependencies: "@nodelib/fs.stat" "2.0.5" @@ -2951,12 +2628,12 @@ "@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": version "2.0.5" - resolved "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz" + resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== "@nodelib/fs.walk@^1.2.3", "@nodelib/fs.walk@^1.2.8": version "1.2.8" - resolved "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz" + resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== dependencies: "@nodelib/fs.scandir" "2.1.5" @@ -2964,23 +2641,23 @@ "@npmcli/fs@^2.1.0": version "2.1.2" - resolved "https://registry.npmjs.org/@npmcli/fs/-/fs-2.1.2.tgz" + resolved "https://registry.yarnpkg.com/@npmcli/fs/-/fs-2.1.2.tgz#a9e2541a4a2fec2e69c29b35e6060973da79b865" integrity sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ== dependencies: "@gar/promisify" "^1.1.3" semver "^7.3.5" "@npmcli/fs@^3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@npmcli/fs/-/fs-3.1.0.tgz" - integrity sha512-7kZUAaLscfgbwBQRbvdMYaZOWyMEcPTH/tJjnyAWJ/dvvs9Ef+CERx/qJb9GExJpl1qipaDGn7KqHnFGGixd0w== + version "3.1.1" + resolved "https://registry.yarnpkg.com/@npmcli/fs/-/fs-3.1.1.tgz#59cdaa5adca95d135fc00f2bb53f5771575ce726" + integrity sha512-q9CRWjpHCMIh5sVyefoD1cA7PkvILqCZsnSOEUUivORLjxCO/Irmue2DprETiNgEqktDBZaM1Bi+jrarx1XdCg== dependencies: semver "^7.3.5" "@npmcli/git@^4.0.0": - version "4.0.4" - resolved "https://registry.npmjs.org/@npmcli/git/-/git-4.0.4.tgz" - integrity sha512-5yZghx+u5M47LghaybLCkdSyFzV/w4OuH12d96HO389Ik9CDsLaDZJVynSGGVJOLn6gy/k7Dz5XYcplM3uxXRg== + version "4.1.0" + resolved "https://registry.yarnpkg.com/@npmcli/git/-/git-4.1.0.tgz#ab0ad3fd82bc4d8c1351b6c62f0fa56e8fe6afa6" + integrity sha512-9hwoB3gStVfa0N31ymBmrX+GuDGdVA/QWShZVqE0HK2Af+7QGGrCTbZia/SW0ImUTjTne7SP91qxDmtXvDHRPQ== dependencies: "@npmcli/promise-spawn" "^6.0.0" lru-cache "^7.4.4" @@ -2992,16 +2669,16 @@ which "^3.0.0" "@npmcli/installed-package-contents@^2.0.1": - version "2.0.2" - resolved "https://registry.npmjs.org/@npmcli/installed-package-contents/-/installed-package-contents-2.0.2.tgz" - integrity sha512-xACzLPhnfD51GKvTOOuNX2/V4G4mz9/1I2MfDoye9kBM3RYe5g2YbscsaGoTlaWqkxeiapBWyseULVKpSVHtKQ== + version "2.1.0" + resolved "https://registry.yarnpkg.com/@npmcli/installed-package-contents/-/installed-package-contents-2.1.0.tgz#63048e5f6e40947a3a88dcbcb4fd9b76fdd37c17" + integrity sha512-c8UuGLeZpm69BryRykLuKRyKFZYJsZSCT4aVY5ds4omyZqJ172ApzgfKJ5eV/r3HgLdUYgFVe54KSFVjKoe27w== dependencies: npm-bundled "^3.0.0" npm-normalize-package-bin "^3.0.0" "@npmcli/move-file@^2.0.0": version "2.0.1" - resolved "https://registry.npmjs.org/@npmcli/move-file/-/move-file-2.0.1.tgz" + resolved "https://registry.yarnpkg.com/@npmcli/move-file/-/move-file-2.0.1.tgz#26f6bdc379d87f75e55739bab89db525b06100e4" integrity sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ== dependencies: mkdirp "^1.0.4" @@ -3009,19 +2686,19 @@ "@npmcli/node-gyp@^3.0.0": version "3.0.0" - resolved "https://registry.npmjs.org/@npmcli/node-gyp/-/node-gyp-3.0.0.tgz" + resolved "https://registry.yarnpkg.com/@npmcli/node-gyp/-/node-gyp-3.0.0.tgz#101b2d0490ef1aa20ed460e4c0813f0db560545a" integrity sha512-gp8pRXC2oOxu0DUE1/M3bYtb1b3/DbJ5aM113+XJBgfXdussRAsX0YOrOhdd8WvnAR6auDBvJomGAkLKA5ydxA== "@npmcli/promise-spawn@^6.0.0", "@npmcli/promise-spawn@^6.0.1": version "6.0.2" - resolved "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-6.0.2.tgz" + resolved "https://registry.yarnpkg.com/@npmcli/promise-spawn/-/promise-spawn-6.0.2.tgz#c8bc4fa2bd0f01cb979d8798ba038f314cfa70f2" integrity sha512-gGq0NJkIGSwdbUt4yhdF8ZrmkGKVz9vAdVzpOfnom+V8PLSmSOVhZwbNvZZS1EYcJN5hzzKBxmmVVAInM6HQLg== dependencies: which "^3.0.0" "@npmcli/run-script@^6.0.0": version "6.0.2" - resolved "https://registry.npmjs.org/@npmcli/run-script/-/run-script-6.0.2.tgz" + resolved "https://registry.yarnpkg.com/@npmcli/run-script/-/run-script-6.0.2.tgz#a25452d45ee7f7fb8c16dfaf9624423c0c0eb885" integrity sha512-NCcr1uQo1k5U+SYlnIrbAh3cxy+OQT1VtqiAbxdymSlptbzBb62AjH2xXgjNCoP073hoa1CfCAcwoZ8k96C4nA== dependencies: "@npmcli/node-gyp" "^3.0.0" @@ -3108,7 +2785,7 @@ "@parcel/watcher@2.0.4": version "2.0.4" - resolved "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.0.4.tgz" + resolved "https://registry.yarnpkg.com/@parcel/watcher/-/watcher-2.0.4.tgz#f300fef4cc38008ff4b8c29d92588eced3ce014b" integrity sha512-cTDi+FUDBIUOBKEtj+nhiJ71AZVlkAsQFuGQTun5tV9mwQBQgZvhCzG+URPQc8myeN32yRVZEfVAPCs1RW+Jvg== dependencies: node-addon-api "^3.2.1" @@ -3116,32 +2793,32 @@ "@pkgjs/parseargs@^0.11.0": version "0.11.0" - resolved "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz" + resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== "@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2": version "1.1.2" - resolved "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz" + resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf" integrity sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ== "@protobufjs/base64@^1.1.2": version "1.1.2" - resolved "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz" + resolved "https://registry.yarnpkg.com/@protobufjs/base64/-/base64-1.1.2.tgz#4c85730e59b9a1f1f349047dbf24296034bb2735" integrity sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg== "@protobufjs/codegen@^2.0.4": version "2.0.4" - resolved "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz" + resolved "https://registry.yarnpkg.com/@protobufjs/codegen/-/codegen-2.0.4.tgz#7ef37f0d010fb028ad1ad59722e506d9262815cb" integrity sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg== "@protobufjs/eventemitter@^1.1.0": version "1.1.0" - resolved "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz" + resolved "https://registry.yarnpkg.com/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz#355cbc98bafad5978f9ed095f397621f1d066b70" integrity sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q== "@protobufjs/fetch@^1.1.0": version "1.1.0" - resolved "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz" + resolved "https://registry.yarnpkg.com/@protobufjs/fetch/-/fetch-1.1.0.tgz#ba99fb598614af65700c1619ff06d454b0d84c45" integrity sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ== dependencies: "@protobufjs/aspromise" "^1.1.1" @@ -3149,47 +2826,71 @@ "@protobufjs/float@^1.0.2": version "1.0.2" - resolved "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz" + resolved "https://registry.yarnpkg.com/@protobufjs/float/-/float-1.0.2.tgz#5e9e1abdcb73fc0a7cb8b291df78c8cbd97b87d1" integrity sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ== "@protobufjs/inquire@^1.1.0": version "1.1.0" - resolved "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz" + resolved "https://registry.yarnpkg.com/@protobufjs/inquire/-/inquire-1.1.0.tgz#ff200e3e7cf2429e2dcafc1140828e8cc638f089" integrity sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q== "@protobufjs/path@^1.1.2": version "1.1.2" - resolved "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz" + resolved "https://registry.yarnpkg.com/@protobufjs/path/-/path-1.1.2.tgz#6cc2b20c5c9ad6ad0dccfd21ca7673d8d7fbf68d" integrity sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA== "@protobufjs/pool@^1.1.0": version "1.1.0" - resolved "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz" + resolved "https://registry.yarnpkg.com/@protobufjs/pool/-/pool-1.1.0.tgz#09fd15f2d6d3abfa9b65bc366506d6ad7846ff54" integrity sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw== "@protobufjs/utf8@^1.1.0": version "1.1.0" - resolved "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz" + resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" integrity sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw== -"@schematics/angular@16.2.2": - version "16.2.2" - resolved "https://registry.yarnpkg.com/@schematics/angular/-/angular-16.2.2.tgz#8dd022499c736c930621a16bdbce5b21caa44ce9" - integrity sha512-OqPhpodkQx9pzSz7H2AGeEbf3ut6WOkJFP2YlX2JIGholfG/0FQMJmfTEyRoFXCBeVIDGt3sOmlfK7An0PS8uA== +"@schematics/angular@16.2.14": + version "16.2.14" + resolved "https://registry.yarnpkg.com/@schematics/angular/-/angular-16.2.14.tgz#3aac7e05b6e3919195275cf06ac403d7a3567876" + integrity sha512-YqIv727l9Qze8/OL6H9mBHc2jVXzAGRNBYnxYWqWhLbfvuVbbldo6NNIIjgv6lrl2LJSdPAAMNOD5m/f6210ug== dependencies: - "@angular-devkit/core" "16.2.2" - "@angular-devkit/schematics" "16.2.2" + "@angular-devkit/core" "16.2.14" + "@angular-devkit/schematics" "16.2.14" jsonc-parser "3.2.0" -"@sigstore/protobuf-specs@^0.1.0": - version "0.1.0" - resolved "https://registry.npmjs.org/@sigstore/protobuf-specs/-/protobuf-specs-0.1.0.tgz" - integrity sha512-a31EnjuIDSX8IXBUib3cYLDRlPMU36AWX4xS8ysLaNu4ZzUesDiPt83pgrW2X1YLMe5L2HbDyaKK5BrL4cNKaQ== +"@sigstore/bundle@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@sigstore/bundle/-/bundle-1.1.0.tgz#17f8d813b09348b16eeed66a8cf1c3d6bd3d04f1" + integrity sha512-PFutXEy0SmQxYI4texPw3dd2KewuNqv7OuK1ZFtY2fM754yhvG2KdgwIhRnoEE2uHdtdGNQ8s0lb94dW9sELog== + dependencies: + "@sigstore/protobuf-specs" "^0.2.0" + +"@sigstore/protobuf-specs@^0.2.0": + version "0.2.1" + resolved "https://registry.yarnpkg.com/@sigstore/protobuf-specs/-/protobuf-specs-0.2.1.tgz#be9ef4f3c38052c43bd399d3f792c97ff9e2277b" + integrity sha512-XTWVxnWJu+c1oCshMLwnKvz8ZQJJDVOlciMfgpJBQbThVjKTCG8dwyhgLngBD2KN0ap9F/gOV8rFDEx8uh7R2A== + +"@sigstore/sign@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@sigstore/sign/-/sign-1.0.0.tgz#6b08ebc2f6c92aa5acb07a49784cb6738796f7b4" + integrity sha512-INxFVNQteLtcfGmcoldzV6Je0sbbfh9I16DM4yJPw3j5+TFP8X6uIiA18mvpEa9yyeycAKgPmOA3X9hVdVTPUA== + dependencies: + "@sigstore/bundle" "^1.1.0" + "@sigstore/protobuf-specs" "^0.2.0" + make-fetch-happen "^11.0.1" + +"@sigstore/tuf@^1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@sigstore/tuf/-/tuf-1.0.3.tgz#2a65986772ede996485728f027b0514c0b70b160" + integrity sha512-2bRovzs0nJZFlCN3rXirE4gwxCn97JNjMmwpecqlbgV9WcxX7WRuIrgzx/X7Ib7MYRbyUTpBYE0s2x6AmZXnlg== + dependencies: + "@sigstore/protobuf-specs" "^0.2.0" + tuf-js "^1.1.7" "@socket.io/component-emitter@~3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz" - integrity sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg== + version "3.1.2" + resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz#821f8442f4175d8f0467b9daf26e3a18e2d02af2" + integrity sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA== "@tootallnate/once@1": version "1.1.2" @@ -3198,96 +2899,96 @@ "@tootallnate/once@2": version "2.0.0" - resolved "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz" + resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf" integrity sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A== "@tufjs/canonical-json@1.0.0": version "1.0.0" - resolved "https://registry.npmjs.org/@tufjs/canonical-json/-/canonical-json-1.0.0.tgz" + resolved "https://registry.yarnpkg.com/@tufjs/canonical-json/-/canonical-json-1.0.0.tgz#eade9fd1f537993bc1f0949f3aea276ecc4fab31" integrity sha512-QTnf++uxunWvG2z3UFNzAoQPHxnSXOwtaI3iJ+AohhV+5vONuArPjJE7aPXPVXfXJsqrVbZBu9b81AJoSd09IQ== "@tufjs/models@1.0.4": version "1.0.4" - resolved "https://registry.npmjs.org/@tufjs/models/-/models-1.0.4.tgz" + resolved "https://registry.yarnpkg.com/@tufjs/models/-/models-1.0.4.tgz#5a689630f6b9dbda338d4b208019336562f176ef" integrity sha512-qaGV9ltJP0EO25YfFUPhxRVK0evXFIAGicsVXuRim4Ed9cjPxYhNnNJ49SFmbeLgtxpslIkX317IgpfcHPVj/A== dependencies: "@tufjs/canonical-json" "1.0.0" minimatch "^9.0.0" "@types/body-parser@*": - version "1.19.2" - resolved "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz" - integrity sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g== + version "1.19.5" + resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.5.tgz#04ce9a3b677dc8bd681a17da1ab9835dc9d3ede4" + integrity sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg== dependencies: "@types/connect" "*" "@types/node" "*" "@types/bonjour@^3.5.9": - version "3.5.10" - resolved "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.10.tgz" - integrity sha512-p7ienRMiS41Nu2/igbJxxLDWrSZ0WxM8UQgCeO9KhoVF7cOVFkrKsiDr1EsJIla8vV3oEEjGcz11jc5yimhzZw== + version "3.5.13" + resolved "https://registry.yarnpkg.com/@types/bonjour/-/bonjour-3.5.13.tgz#adf90ce1a105e81dd1f9c61fdc5afda1bfb92956" + integrity sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ== dependencies: "@types/node" "*" "@types/codemirror@^5.60.5": - version "5.60.7" - resolved "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.7.tgz" - integrity sha512-QXIC+RPzt/1BGSuD6iFn6UMC9TDp+9hkOANYNPVsjjrDdzKphfRkwQDKGp2YaC54Yhz0g6P5uYTCCibZZEiMAA== + version "5.60.15" + resolved "https://registry.yarnpkg.com/@types/codemirror/-/codemirror-5.60.15.tgz#0f82be6f4126d1e59cf4c4830e56dcd49d3c3e8a" + integrity sha512-dTOvwEQ+ouKJ/rE9LT1Ue2hmP6H1mZv5+CCnNWu2qtiOe2LQa9lCprEY20HxiDmV/Bxh+dXjywmy5aKvoGjULA== dependencies: "@types/tern" "*" "@types/connect-history-api-fallback@^1.3.5": - version "1.5.0" - resolved "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.0.tgz" - integrity sha512-4x5FkPpLipqwthjPsF7ZRbOv3uoLUFkTA9G9v583qi4pACvq0uTELrB8OLUzPWUI4IJIyvM85vzkV1nyiI2Lig== + version "1.5.4" + resolved "https://registry.yarnpkg.com/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz#7de71645a103056b48ac3ce07b3520b819c1d5b3" + integrity sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw== dependencies: "@types/express-serve-static-core" "*" "@types/node" "*" "@types/connect@*": - version "3.4.35" - resolved "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz" - integrity sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ== + version "3.4.38" + resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.38.tgz#5ba7f3bc4fbbdeaff8dded952e5ff2cc53f8d858" + integrity sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug== dependencies: "@types/node" "*" "@types/cookie@^0.4.1": version "0.4.1" - resolved "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz" + resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.4.1.tgz#bfd02c1f2224567676c1545199f87c3a861d878d" integrity sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q== "@types/cors@^2.8.12": - version "2.8.13" - resolved "https://registry.npmjs.org/@types/cors/-/cors-2.8.13.tgz" - integrity sha512-RG8AStHlUiV5ysZQKq97copd2UmVYw3/pRMLefISZ3S1hK104Cwm7iLQ3fTKx+lsUH2CE8FlLaYeEA2LSeqYUA== + version "2.8.17" + resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.17.tgz#5d718a5e494a8166f569d986794e49c48b216b2b" + integrity sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA== dependencies: "@types/node" "*" "@types/eslint-scope@^3.7.3": - version "3.7.4" - resolved "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.4.tgz" - integrity sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA== + 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== dependencies: "@types/eslint" "*" "@types/estree" "*" "@types/eslint@*": - version "8.37.0" - resolved "https://registry.npmjs.org/@types/eslint/-/eslint-8.37.0.tgz" - integrity sha512-Piet7dG2JBuDIfohBngQ3rCt7MgO9xCO4xIMKxBThCq5PNRB91IjlJ10eJVwfoNtvTErmxLzwBZ7rHZtbOMmFQ== + version "9.6.0" + resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-9.6.0.tgz#51d4fe4d0316da9e9f2c80884f2c20ed5fb022ff" + integrity sha512-gi6WQJ7cHRgZxtkQEoyHMppPjq9Kxo5Tjn2prSKDSmZrCz8TZ3jSRCeTJm+WoM+oB0WG37bRqLzaaU3q7JypGg== dependencies: "@types/estree" "*" "@types/json-schema" "*" "@types/estree@*", "@types/estree@^1.0.0": - version "1.0.1" - resolved "https://registry.npmjs.org/@types/estree/-/estree-1.0.1.tgz" - integrity sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA== + version "1.0.5" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4" + integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw== "@types/express-serve-static-core@*", "@types/express-serve-static-core@^4.17.33": - version "4.17.35" - resolved "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.35.tgz" - integrity sha512-wALWQwrgiB2AWTT91CB62b6Yt0sNHpznUXeZEcnPU3DRdlDIz74x8Qg1UUYKSVFi+va5vKOLYRBI1bRKiLLKIg== + version "4.19.5" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.19.5.tgz#218064e321126fcf9048d1ca25dd2465da55d9c6" + integrity sha512-y6W03tvrACO72aijJ5uF02FRq5cgDR9lUxddQ8vyF+GvmjJQqbzDcJngEjURc+ZsG31VI3hODNZJ2URj86pzmg== dependencies: "@types/node" "*" "@types/qs" "*" @@ -3295,9 +2996,9 @@ "@types/send" "*" "@types/express@*", "@types/express@^4.17.13": - version "4.17.17" - resolved "https://registry.npmjs.org/@types/express/-/express-4.17.17.tgz" - integrity sha512-Q4FmmuLGBG58btUnfS1c1r/NQdlp3DMfGDGig8WhfpA2YRUtEkxAjkZb0yvplJGYdF1fsQ81iMDcH24sSCNC/Q== + version "4.17.21" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.21.tgz#c26d4a151e60efe0084b23dc3369ebc631ed192d" + integrity sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ== dependencies: "@types/body-parser" "*" "@types/express-serve-static-core" "^4.17.33" @@ -3310,14 +3011,19 @@ integrity sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A== "@types/google-protobuf@^3.15.3": - version "3.15.6" - resolved "https://registry.npmjs.org/@types/google-protobuf/-/google-protobuf-3.15.6.tgz" - integrity sha512-pYVNNJ+winC4aek+lZp93sIKxnXt5qMkuKmaqS3WGuTq0Bw1ZDYNBgzG5kkdtwcv+GmYJGo3yEg6z2cKKAiEdw== + version "3.15.12" + resolved "https://registry.yarnpkg.com/@types/google-protobuf/-/google-protobuf-3.15.12.tgz#eb2ba0eddd65712211a2b455dc6071d665ccf49b" + integrity sha512-40um9QqwHjRS92qnOaDpL7RmDK15NuZYo9HihiJRbYkMQZlWnuH8AdvbMy8/o6lgLmKbDUKa+OALCltHdbOTpQ== + +"@types/http-errors@*": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.4.tgz#7eb47726c391b7345a6ec35ad7f4de469cf5ba4f" + integrity sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA== "@types/http-proxy@^1.17.8": - version "1.17.11" - resolved "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.11.tgz" - integrity sha512-HC8G7c1WmaF2ekqpnFq626xd3Zz0uvaqFmBJNRZCGEZCXkvSdJoNFn/8Ygbd9fKNQj8UzLdCETaI0UWPAjK7IA== + version "1.17.14" + resolved "https://registry.yarnpkg.com/@types/http-proxy/-/http-proxy-1.17.14.tgz#57f8ccaa1c1c3780644f8a94f9c6b5000b5e2eec" + integrity sha512-SSrD0c1OQzlFX7pGu1eXxSEjemej64aaNPRhhVYUGqXh0BtldAAx37MG8btcumvpgKyZp1F5Gn3JkktdxiFv6w== dependencies: "@types/node" "*" @@ -3334,31 +3040,42 @@ "@types/jasmine" "*" "@types/json-schema@*", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": - version "7.0.11" - resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz" - integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ== + version "7.0.15" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" + integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== -"@types/jsonwebtoken@^9.0.5": - version "9.0.5" - resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-9.0.5.tgz#0bd9b841c9e6c5a937c17656e2368f65da025588" - integrity sha512-VRLSGzik+Unrup6BsouBeHsf4d1hOEgYWTm/7Nmw1sXoN1+tRly/Gy/po3yeahnP4jfnQWWAhQAqcNfH7ngOkA== +"@types/jsonwebtoken@^9.0.6": + version "9.0.6" + resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-9.0.6.tgz#d1af3544d99ad992fb6681bbe60676e06b032bd3" + integrity sha512-/5hndP5dCjloafCXns6SZyESp3Ldq7YjH3zwzwczYnjxIT0Fqzk5ROSYVGfFyczIue7IUEj8hkvLbPoLQ18vQw== dependencies: "@types/node" "*" -"@types/mime@*": - version "3.0.1" - resolved "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz" - integrity sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA== - "@types/mime@^1": - version "1.3.2" - resolved "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz" - integrity sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw== + version "1.3.5" + resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.5.tgz#1ef302e01cf7d2b5a0fa526790c9123bf1d06690" + integrity sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w== -"@types/node@*", "@types/node@>=10.0.0", "@types/node@>=12.12.47", "@types/node@>=13.7.0", "@types/node@^20.7.0": - version "20.7.0" - resolved "https://registry.yarnpkg.com/@types/node/-/node-20.7.0.tgz#c03de4572f114a940bc2ca909a33ddb2b925e470" - integrity sha512-zI22/pJW2wUZOVyguFaUL1HABdmSVxpXrzIqkjsHmyUjNhPoWM1CKfvVuXfetHhIok4RY573cqS0mZ1SJEnoTg== +"@types/node-forge@^1.3.0": + version "1.3.11" + resolved "https://registry.yarnpkg.com/@types/node-forge/-/node-forge-1.3.11.tgz#0972ea538ddb0f4d9c2fa0ec5db5724773a604da" + integrity sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ== + dependencies: + "@types/node" "*" + +"@types/node@*", "@types/node@>=10.0.0", "@types/node@>=13.7.0": + version "22.0.0" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.0.0.tgz#04862a2a71e62264426083abe1e27e87cac05a30" + integrity sha512-VT7KSYudcPOzP5Q0wfbowyNLaVR8QWUdw+088uFWwfvpY6uCWaXpqV6ieLAu9WBcnTa7H4Z5RLK8I5t2FuOcqw== + dependencies: + undici-types "~6.11.1" + +"@types/node@^20.7.0": + version "20.14.13" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.14.13.tgz#bf4fe8959ae1c43bc284de78bd6c01730933736b" + integrity sha512-+bHoGiZb8UiQ0+WEtmph2IWQCjIqg8MDZMAV+ppRRhUZnquF5mQkP/9vpSwJClEiSM/C7fZZExPzfU0vJTyp8w== + dependencies: + undici-types "~5.26.4" "@types/normalize-package-data@^2.4.1": version "2.4.4" @@ -3372,106 +3089,100 @@ "@types/q@^0.0.32": version "0.0.32" - resolved "https://registry.npmjs.org/@types/q/-/q-0.0.32.tgz" + resolved "https://registry.yarnpkg.com/@types/q/-/q-0.0.32.tgz#bd284e57c84f1325da702babfc82a5328190c0c5" integrity sha512-qYi3YV9inU/REEfxwVcGZzbS3KG/Xs90lv0Pr+lDtuVjBPGd1A+eciXzVSaRvLify132BfcvhvEjeVahrUl0Ug== -"@types/qrcode@1.5.0": - version "1.5.0" - resolved "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.0.tgz" - integrity sha512-x5ilHXRxUPIMfjtM+1vf/GPTRWZ81nqscursm5gMznJeK9M0YnZ1c3bEvRLQ0zSSgedLx1J6MGL231ObQGGhaA== - dependencies: - "@types/node" "*" - "@types/qrcode@^1.5.2": - version "1.5.2" - resolved "https://registry.yarnpkg.com/@types/qrcode/-/qrcode-1.5.2.tgz#27633439b7fbe88cc3043b29c8e7612a8a789e15" - integrity sha512-W4KDz75m7rJjFbyCctzCtRzZUj+PrUHV+YjqDp50sSRezTbrtEAIq2iTzC6lISARl3qw+8IlcCyljdcVJE0Wug== + version "1.5.5" + resolved "https://registry.yarnpkg.com/@types/qrcode/-/qrcode-1.5.5.tgz#993ff7c6b584277eee7aac0a20861eab682f9dac" + integrity sha512-CdfBi/e3Qk+3Z/fXYShipBT13OJ2fDO2Q2w5CIP5anLTLIndQG9z6P1cnm+8zCWSpm5dnxMFd/uREtb0EXuQzg== dependencies: "@types/node" "*" "@types/qs@*": - version "6.9.7" - resolved "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz" - integrity sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw== + version "6.9.15" + resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.15.tgz#adde8a060ec9c305a82de1babc1056e73bd64dce" + integrity sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg== "@types/range-parser@*": - version "1.2.4" - resolved "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz" - integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw== + version "1.2.7" + resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.7.tgz#50ae4353eaaddc04044279812f52c8c65857dbcb" + integrity sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ== "@types/retry@0.12.0": version "0.12.0" - resolved "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz" + resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d" integrity sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA== "@types/selenium-webdriver@^3.0.0": - version "3.0.21" - resolved "https://registry.npmjs.org/@types/selenium-webdriver/-/selenium-webdriver-3.0.21.tgz" - integrity sha512-DRHyGEr25ra2C4+eU7eiCSto2j9eUa9pR4z5uiLRBXWFlmfMCAeXwecZnAhuB3eOOCA8OkwcNlb6QUkVZFlKTA== + version "3.0.26" + resolved "https://registry.yarnpkg.com/@types/selenium-webdriver/-/selenium-webdriver-3.0.26.tgz#fc7d87d580affa2e52685b2e881bc201819a5836" + integrity sha512-dyIGFKXfUFiwkMfNGn1+F6b80ZjR3uSYv1j6xVJSDlft5waZ2cwkHW4e7zNzvq7hiEackcgvBpmnXZrI1GltPg== "@types/semver@^7.3.12": - version "7.5.0" - resolved "https://registry.npmjs.org/@types/semver/-/semver-7.5.0.tgz" - integrity sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw== + version "7.5.8" + resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.8.tgz#8268a8c57a3e4abd25c165ecd36237db7948a55e" + integrity sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ== "@types/send@*": - version "0.17.1" - resolved "https://registry.npmjs.org/@types/send/-/send-0.17.1.tgz" - integrity sha512-Cwo8LE/0rnvX7kIIa3QHCkcuF21c05Ayb0ZfxPiv0W8VRiZiNW/WuRupHKpqqGVGf7SUA44QSOUKaEd9lIrd/Q== + version "0.17.4" + resolved "https://registry.yarnpkg.com/@types/send/-/send-0.17.4.tgz#6619cd24e7270793702e4e6a4b958a9010cfc57a" + integrity sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA== dependencies: "@types/mime" "^1" "@types/node" "*" "@types/serve-index@^1.9.1": - version "1.9.1" - resolved "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.1.tgz" - integrity sha512-d/Hs3nWDxNL2xAczmOVZNj92YZCS6RGxfBPjKzuu/XirCgXdpKEb88dYNbrYGint6IVWLNP+yonwVAuRC0T2Dg== + version "1.9.4" + resolved "https://registry.yarnpkg.com/@types/serve-index/-/serve-index-1.9.4.tgz#e6ae13d5053cb06ed36392110b4f9a49ac4ec898" + integrity sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug== dependencies: "@types/express" "*" "@types/serve-static@*", "@types/serve-static@^1.13.10": - version "1.15.1" - resolved "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.1.tgz" - integrity sha512-NUo5XNiAdULrJENtJXZZ3fHtfMolzZwczzBbnAeBbqBwG+LaG6YaJtuwzwGSQZ2wsCrxjEhNNjAkKigy3n8teQ== + version "1.15.7" + resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.7.tgz#22174bbd74fb97fe303109738e9b5c2f3064f714" + integrity sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw== dependencies: - "@types/mime" "*" + "@types/http-errors" "*" "@types/node" "*" + "@types/send" "*" "@types/sockjs@^0.3.33": - version "0.3.33" - resolved "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.33.tgz" - integrity sha512-f0KEEe05NvUnat+boPTZ0dgaLZ4SfSouXUgv5noUiefG2ajgKjmETo9ZJyuqsl7dfl2aHlLJUiki6B4ZYldiiw== + version "0.3.36" + resolved "https://registry.yarnpkg.com/@types/sockjs/-/sockjs-0.3.36.tgz#ce322cf07bcc119d4cbf7f88954f3a3bd0f67535" + integrity sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q== dependencies: "@types/node" "*" "@types/tern@*": - version "0.23.4" - resolved "https://registry.npmjs.org/@types/tern/-/tern-0.23.4.tgz" - integrity sha512-JAUw1iXGO1qaWwEOzxTKJZ/5JxVeON9kvGZ/osgZaJImBnyjyn0cjovPsf6FNLmyGY8Vw9DoXZCMlfMkMwHRWg== + version "0.23.9" + resolved "https://registry.yarnpkg.com/@types/tern/-/tern-0.23.9.tgz#6f6093a4a9af3e6bb8dde528e024924d196b367c" + integrity sha512-ypzHFE/wBzh+BlH6rrBgS5I/Z7RD21pGhZ2rltb/+ZrVM1awdZwjx7hE5XfuYgHWk9uvV5HLZN3SloevCAp3Bw== dependencies: "@types/estree" "*" -"@types/uuid@^9.0.7": - version "9.0.7" - resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-9.0.7.tgz#b14cebc75455eeeb160d5fe23c2fcc0c64f724d8" - integrity sha512-WUtIVRUZ9i5dYXefDEAI7sh9/O7jGvHg7Df/5O/gtH3Yabe5odI3UWopVR1qbPXQtvOxWu3mM4XxlYeZtMWF4g== +"@types/uuid@^10.0.0": + version "10.0.0" + resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-10.0.0.tgz#e9c07fe50da0f53dc24970cca94d619ff03f6f6d" + integrity sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ== "@types/ws@^8.5.5": - version "8.5.5" - resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.5.tgz#af587964aa06682702ee6dcbc7be41a80e4b28eb" - integrity sha512-lwhs8hktwxSjf9UaZ9tG5M03PGogvFaH8gUgLNbN9HKIg0dvv6q+gkSuJ8HN4/VbyxkuLzCjlN7GquQ0gUJfIg== + version "8.5.12" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.12.tgz#619475fe98f35ccca2a2f6c137702d85ec247b7e" + integrity sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ== dependencies: "@types/node" "*" -"@typescript-eslint/eslint-plugin@^5.59.11": - version "5.61.0" - resolved "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.61.0.tgz" - integrity sha512-A5l/eUAug103qtkwccSCxn8ZRwT+7RXWkFECdA4Cvl1dOlDUgTpAOfSEElZn2uSUxhdDpnCdetrf0jvU4qrL+g== +"@typescript-eslint/eslint-plugin@^5.62.0": + version "5.62.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz#aeef0328d172b9e37d9bab6dbc13b87ed88977db" + integrity sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag== dependencies: "@eslint-community/regexpp" "^4.4.0" - "@typescript-eslint/scope-manager" "5.61.0" - "@typescript-eslint/type-utils" "5.61.0" - "@typescript-eslint/utils" "5.61.0" + "@typescript-eslint/scope-manager" "5.62.0" + "@typescript-eslint/type-utils" "5.62.0" + "@typescript-eslint/utils" "5.62.0" debug "^4.3.4" graphemer "^1.4.0" ignore "^5.2.0" @@ -3481,7 +3192,7 @@ "@typescript-eslint/parser@^5.60.1": version "5.62.0" - resolved "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.62.0.tgz#1b63d082d849a2fcae8a569248fbe2ee1b8a56c7" integrity sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA== dependencies: "@typescript-eslint/scope-manager" "5.62.0" @@ -3489,32 +3200,14 @@ "@typescript-eslint/typescript-estree" "5.62.0" debug "^4.3.4" -"@typescript-eslint/scope-manager@5.61.0": - version "5.61.0" - resolved "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.61.0.tgz" - integrity sha512-W8VoMjoSg7f7nqAROEmTt6LoBpn81AegP7uKhhW5KzYlehs8VV0ZW0fIDVbcZRcaP3aPSW+JZFua+ysQN+m/Nw== - dependencies: - "@typescript-eslint/types" "5.61.0" - "@typescript-eslint/visitor-keys" "5.61.0" - "@typescript-eslint/scope-manager@5.62.0": version "5.62.0" - resolved "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz#d9457ccc6a0b8d6b37d0eb252a23022478c5460c" integrity sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w== dependencies: "@typescript-eslint/types" "5.62.0" "@typescript-eslint/visitor-keys" "5.62.0" -"@typescript-eslint/type-utils@5.61.0": - version "5.61.0" - resolved "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.61.0.tgz" - integrity sha512-kk8u//r+oVK2Aj3ph/26XdH0pbAkC2RiSjUYhKD+PExemG4XSjpGFeyZ/QM8lBOa7O8aGOU+/yEbMJgQv/DnCg== - dependencies: - "@typescript-eslint/typescript-estree" "5.61.0" - "@typescript-eslint/utils" "5.61.0" - debug "^4.3.4" - tsutils "^3.21.0" - "@typescript-eslint/type-utils@5.62.0": version "5.62.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz#286f0389c41681376cdad96b309cedd17d70346a" @@ -3525,32 +3218,14 @@ debug "^4.3.4" tsutils "^3.21.0" -"@typescript-eslint/types@5.61.0": - version "5.61.0" - resolved "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.61.0.tgz" - integrity sha512-ldyueo58KjngXpzloHUog/h9REmHl59G1b3a5Sng1GfBo14BkS3ZbMEb3693gnP1k//97lh7bKsp6/V/0v1veQ== - "@typescript-eslint/types@5.62.0": version "5.62.0" - resolved "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.62.0.tgz#258607e60effa309f067608931c3df6fed41fd2f" integrity sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ== -"@typescript-eslint/typescript-estree@5.61.0": - version "5.61.0" - resolved "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.61.0.tgz" - integrity sha512-Fud90PxONnnLZ36oR5ClJBLTLfU4pIWBmnvGwTbEa2cXIqj70AEDEmOmpkFComjBZ/037ueKrOdHuYmSFVD7Rw== - dependencies: - "@typescript-eslint/types" "5.61.0" - "@typescript-eslint/visitor-keys" "5.61.0" - debug "^4.3.4" - globby "^11.1.0" - is-glob "^4.0.3" - semver "^7.3.7" - tsutils "^3.21.0" - "@typescript-eslint/typescript-estree@5.62.0": version "5.62.0" - resolved "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz#7d17794b77fabcac615d6a48fb143330d962eb9b" integrity sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA== dependencies: "@typescript-eslint/types" "5.62.0" @@ -3561,20 +3236,6 @@ semver "^7.3.7" tsutils "^3.21.0" -"@typescript-eslint/utils@5.61.0": - version "5.61.0" - resolved "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.61.0.tgz" - integrity sha512-mV6O+6VgQmVE6+xzlA91xifndPW9ElFW8vbSF0xCT/czPXVhwDewKila1jOyRwa9AE19zKnrr7Cg5S3pJVrTWQ== - dependencies: - "@eslint-community/eslint-utils" "^4.2.0" - "@types/json-schema" "^7.0.9" - "@types/semver" "^7.3.12" - "@typescript-eslint/scope-manager" "5.61.0" - "@typescript-eslint/types" "5.61.0" - "@typescript-eslint/typescript-estree" "5.61.0" - eslint-scope "^5.1.1" - semver "^7.3.7" - "@typescript-eslint/utils@5.62.0": version "5.62.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.62.0.tgz#141e809c71636e4a75daa39faed2fb5f4b10df86" @@ -3589,53 +3250,50 @@ eslint-scope "^5.1.1" semver "^7.3.7" -"@typescript-eslint/visitor-keys@5.61.0": - version "5.61.0" - resolved "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.61.0.tgz" - integrity sha512-50XQ5VdbWrX06mQXhy93WywSFZZGsv3EOjq+lqp6WC2t+j3mb6A9xYVdrRxafvK88vg9k9u+CT4l6D8PEatjKg== - dependencies: - "@typescript-eslint/types" "5.61.0" - eslint-visitor-keys "^3.3.0" - "@typescript-eslint/visitor-keys@5.62.0": version "5.62.0" - resolved "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz#2174011917ce582875954ffe2f6912d5931e353e" integrity sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw== dependencies: "@typescript-eslint/types" "5.62.0" eslint-visitor-keys "^3.3.0" +"@ungap/structured-clone@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" + integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== + "@vitejs/plugin-basic-ssl@1.0.1": version "1.0.1" - resolved "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-1.0.1.tgz" + resolved "https://registry.yarnpkg.com/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-1.0.1.tgz#48c46eab21e0730921986ce742563ae83fe7fe34" integrity sha512-pcub+YbFtFhaGRTo1832FQHQSHvMrlb43974e2eS8EKleR3p1cDdkJFPci1UhwkEf1J9Bz+wKBSzqpKp7nNj2A== -"@webassemblyjs/ast@1.11.6", "@webassemblyjs/ast@^1.11.5": - version "1.11.6" - resolved "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.6.tgz" - integrity sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q== +"@webassemblyjs/ast@1.12.1", "@webassemblyjs/ast@^1.11.5": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.12.1.tgz#bb16a0e8b1914f979f45864c23819cc3e3f0d4bb" + integrity sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg== dependencies: "@webassemblyjs/helper-numbers" "1.11.6" "@webassemblyjs/helper-wasm-bytecode" "1.11.6" "@webassemblyjs/floating-point-hex-parser@1.11.6": version "1.11.6" - resolved "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz" + 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/helper-api-error@1.11.6": version "1.11.6" - resolved "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz" + 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-buffer@1.11.6": - version "1.11.6" - resolved "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.6.tgz" - integrity sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA== +"@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-numbers@1.11.6": version "1.11.6" - resolved "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz#cbce5e7e0c1bd32cf4905ae444ef64cea919f1b5" integrity sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g== dependencies: "@webassemblyjs/floating-point-hex-parser" "1.11.6" @@ -3644,91 +3302,91 @@ "@webassemblyjs/helper-wasm-bytecode@1.11.6": version "1.11.6" - resolved "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz#bb2ebdb3b83aa26d9baad4c46d4315283acd51e9" integrity sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA== -"@webassemblyjs/helper-wasm-section@1.11.6": - version "1.11.6" - resolved "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.6.tgz" - integrity sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g== +"@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" + integrity sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g== dependencies: - "@webassemblyjs/ast" "1.11.6" - "@webassemblyjs/helper-buffer" "1.11.6" + "@webassemblyjs/ast" "1.12.1" + "@webassemblyjs/helper-buffer" "1.12.1" "@webassemblyjs/helper-wasm-bytecode" "1.11.6" - "@webassemblyjs/wasm-gen" "1.11.6" + "@webassemblyjs/wasm-gen" "1.12.1" "@webassemblyjs/ieee754@1.11.6": version "1.11.6" - resolved "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz#bb665c91d0b14fffceb0e38298c329af043c6e3a" integrity sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg== dependencies: "@xtuc/ieee754" "^1.2.0" "@webassemblyjs/leb128@1.11.6": version "1.11.6" - resolved "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz" + resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.11.6.tgz#70e60e5e82f9ac81118bc25381a0b283893240d7" integrity sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ== dependencies: "@xtuc/long" "4.2.2" "@webassemblyjs/utf8@1.11.6": version "1.11.6" - resolved "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz" + resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.11.6.tgz#90f8bc34c561595fe156603be7253cdbcd0fab5a" integrity sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA== "@webassemblyjs/wasm-edit@^1.11.5": - version "1.11.6" - resolved "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.6.tgz" - integrity sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw== + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz#9f9f3ff52a14c980939be0ef9d5df9ebc678ae3b" + integrity sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g== dependencies: - "@webassemblyjs/ast" "1.11.6" - "@webassemblyjs/helper-buffer" "1.11.6" + "@webassemblyjs/ast" "1.12.1" + "@webassemblyjs/helper-buffer" "1.12.1" "@webassemblyjs/helper-wasm-bytecode" "1.11.6" - "@webassemblyjs/helper-wasm-section" "1.11.6" - "@webassemblyjs/wasm-gen" "1.11.6" - "@webassemblyjs/wasm-opt" "1.11.6" - "@webassemblyjs/wasm-parser" "1.11.6" - "@webassemblyjs/wast-printer" "1.11.6" + "@webassemblyjs/helper-wasm-section" "1.12.1" + "@webassemblyjs/wasm-gen" "1.12.1" + "@webassemblyjs/wasm-opt" "1.12.1" + "@webassemblyjs/wasm-parser" "1.12.1" + "@webassemblyjs/wast-printer" "1.12.1" -"@webassemblyjs/wasm-gen@1.11.6": - version "1.11.6" - resolved "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.6.tgz" - integrity sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA== +"@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" + integrity sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w== dependencies: - "@webassemblyjs/ast" "1.11.6" + "@webassemblyjs/ast" "1.12.1" "@webassemblyjs/helper-wasm-bytecode" "1.11.6" "@webassemblyjs/ieee754" "1.11.6" "@webassemblyjs/leb128" "1.11.6" "@webassemblyjs/utf8" "1.11.6" -"@webassemblyjs/wasm-opt@1.11.6": - version "1.11.6" - resolved "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.6.tgz" - integrity sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g== +"@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" + integrity sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg== dependencies: - "@webassemblyjs/ast" "1.11.6" - "@webassemblyjs/helper-buffer" "1.11.6" - "@webassemblyjs/wasm-gen" "1.11.6" - "@webassemblyjs/wasm-parser" "1.11.6" + "@webassemblyjs/ast" "1.12.1" + "@webassemblyjs/helper-buffer" "1.12.1" + "@webassemblyjs/wasm-gen" "1.12.1" + "@webassemblyjs/wasm-parser" "1.12.1" -"@webassemblyjs/wasm-parser@1.11.6", "@webassemblyjs/wasm-parser@^1.11.5": - version "1.11.6" - resolved "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.6.tgz" - integrity sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ== +"@webassemblyjs/wasm-parser@1.12.1", "@webassemblyjs/wasm-parser@^1.11.5": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz#c47acb90e6f083391e3fa61d113650eea1e95937" + integrity sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ== dependencies: - "@webassemblyjs/ast" "1.11.6" + "@webassemblyjs/ast" "1.12.1" "@webassemblyjs/helper-api-error" "1.11.6" "@webassemblyjs/helper-wasm-bytecode" "1.11.6" "@webassemblyjs/ieee754" "1.11.6" "@webassemblyjs/leb128" "1.11.6" "@webassemblyjs/utf8" "1.11.6" -"@webassemblyjs/wast-printer@1.11.6": - version "1.11.6" - resolved "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.6.tgz" - integrity sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A== +"@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" + integrity sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA== dependencies: - "@webassemblyjs/ast" "1.11.6" + "@webassemblyjs/ast" "1.12.1" "@xtuc/long" "4.2.2" "@wessberg/ts-evaluator@0.0.27": @@ -3743,17 +3401,17 @@ "@xtuc/ieee754@^1.2.0": version "1.2.0" - resolved "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz" + resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790" integrity sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA== "@xtuc/long@4.2.2": version "4.2.2" - resolved "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz" + resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d" integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== "@yarnpkg/lockfile@1.1.0", "@yarnpkg/lockfile@^1.1.0": version "1.1.0" - resolved "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz" + resolved "https://registry.yarnpkg.com/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz#e77a97fbd345b76d83245edcd17d393b1b41fb31" integrity sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ== "@yarnpkg/parsers@3.0.0-rc.46": @@ -3766,24 +3424,24 @@ "@zkochan/js-yaml@0.0.6": version "0.0.6" - resolved "https://registry.npmjs.org/@zkochan/js-yaml/-/js-yaml-0.0.6.tgz" + resolved "https://registry.yarnpkg.com/@zkochan/js-yaml/-/js-yaml-0.0.6.tgz#975f0b306e705e28b8068a07737fa46d3fc04826" integrity sha512-nzvgl3VfhcELQ8LyVrYOru+UtAy1nrygk2+AGbTm8a5YcO6o8lSjAT+pfg3vJWxIoZKOUhrK6UU7xW/+00kQrg== dependencies: argparse "^2.0.1" abab@^2.0.3, abab@^2.0.5, abab@^2.0.6: version "2.0.6" - resolved "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz" + resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.6.tgz#41b80f2c871d19686216b82309231cfd3cb3d291" integrity sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA== abbrev@^1.0.0: version "1.1.1" - resolved "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz" + resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.8: version "1.3.8" - resolved "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== dependencies: mime-types "~2.1.34" @@ -3799,12 +3457,12 @@ acorn-globals@^6.0.0: acorn-import-assertions@^1.9.0: version "1.9.0" - resolved "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz" + resolved "https://registry.yarnpkg.com/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz#507276249d684797c84e0734ef84860334cfb1ac" integrity sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA== acorn-jsx@^5.3.2: version "5.3.2" - resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== acorn-walk@^7.1.1: @@ -3817,50 +3475,48 @@ acorn@^7.1.1: resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== -acorn@^8.2.4, acorn@^8.5.0, acorn@^8.7.1, acorn@^8.8.2, acorn@^8.9.0: - version "8.10.0" - resolved "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz" - integrity sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw== +acorn@^8.2.4, acorn@^8.7.1, acorn@^8.8.2, acorn@^8.9.0: + version "8.12.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.12.1.tgz#71616bdccbe25e27a54439e0046e89ca76df2248" + integrity sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg== adjust-sourcemap-loader@^4.0.0: version "4.0.0" - resolved "https://registry.npmjs.org/adjust-sourcemap-loader/-/adjust-sourcemap-loader-4.0.0.tgz" + resolved "https://registry.yarnpkg.com/adjust-sourcemap-loader/-/adjust-sourcemap-loader-4.0.0.tgz#fc4a0fd080f7d10471f30a7320f25560ade28c99" integrity sha512-OXwN5b9pCUXNQHJpwwD2qP40byEmSgzj8B4ydSN0uMNYWiFmJ6x6KwUllMmfk8Rwu/HJDFR7U8ubsWBoN0Xp0A== dependencies: loader-utils "^2.0.0" regex-parser "^2.2.11" adm-zip@^0.5.2: - version "0.5.10" - resolved "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.10.tgz" - integrity sha512-x0HvcHqVJNTPk/Bw8JbLWlWoo6Wwnsug0fnYYro1HBrjxZ3G7/AZk7Ahv8JwDe1uIcz8eBqvu86FuF1POiG7vQ== + version "0.5.14" + resolved "https://registry.yarnpkg.com/adm-zip/-/adm-zip-0.5.14.tgz#2c557c0bf12af4311cf6d32970f4060cf8133b2a" + integrity sha512-DnyqqifT4Jrcvb8USYjp6FHtBpEIz1mnXu6pTRHZ0RL69LbQYiO+0lDFg5+OKA7U29oWSs3a/i8fhn8ZcceIWg== agent-base@6, agent-base@^6.0.2: version "6.0.2" - resolved "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== dependencies: debug "4" agent-base@^4.3.0: version "4.3.0" - resolved "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.3.0.tgz#8165f01c436009bccad0b1d122f05ed770efc6ee" integrity sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg== dependencies: es6-promisify "^5.0.0" agentkeepalive@^4.2.1: - version "4.3.0" - resolved "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.3.0.tgz" - integrity sha512-7Epl1Blf4Sy37j4v9f9FjICCh4+KAQOyXgHEwlyBiAQLbhKdq/i2QQU3amQalS/wPhdPzDXPL5DMR5bkn+YeWg== + version "4.5.0" + resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-4.5.0.tgz#2673ad1389b3c418c5a20c5d7364f93ca04be923" + integrity sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew== dependencies: - debug "^4.1.0" - depd "^2.0.0" humanize-ms "^1.2.1" aggregate-error@^3.0.0: version "3.1.0" - resolved "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz" + resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a" integrity sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA== dependencies: clean-stack "^2.0.0" @@ -3876,26 +3532,26 @@ aggregate-error@^4.0.0: ajv-formats@2.1.1, ajv-formats@^2.1.1: version "2.1.1" - resolved "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz" + resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-2.1.1.tgz#6e669400659eb74973bbf2e33327180a0996b520" integrity sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA== dependencies: ajv "^8.0.0" ajv-keywords@^3.5.2: version "3.5.2" - resolved "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== ajv-keywords@^5.1.0: version "5.1.0" - resolved "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-5.1.0.tgz#69d4d385a4733cdbeab44964a1170a88f87f0e16" integrity sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw== dependencies: fast-deep-equal "^3.1.3" -ajv@8.12.0, ajv@^8.0.0, ajv@^8.12.0, ajv@^8.9.0: +ajv@8.12.0: version "8.12.0" - resolved "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.12.0.tgz#d1a0527323e22f53562c567c00991577dfbe19d1" integrity sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA== dependencies: fast-deep-equal "^3.1.1" @@ -3905,7 +3561,7 @@ ajv@8.12.0, ajv@^8.0.0, ajv@^8.12.0, ajv@^8.9.0: ajv@^6.12.3, ajv@^6.12.4, ajv@^6.12.5: version "6.12.6" - resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== dependencies: fast-deep-equal "^3.1.1" @@ -3913,81 +3569,90 @@ ajv@^6.12.3, ajv@^6.12.4, ajv@^6.12.5: json-schema-traverse "^0.4.1" uri-js "^4.2.2" +ajv@^8.0.0, ajv@^8.12.0, ajv@^8.9.0: + version "8.17.1" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.17.1.tgz#37d9a5c776af6bc92d7f4f9510eba4c0a60d11a6" + integrity sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g== + dependencies: + fast-deep-equal "^3.1.3" + fast-uri "^3.0.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + angular-oauth2-oidc@^15.0.1: version "15.0.1" - resolved "https://registry.npmjs.org/angular-oauth2-oidc/-/angular-oauth2-oidc-15.0.1.tgz" + resolved "https://registry.yarnpkg.com/angular-oauth2-oidc/-/angular-oauth2-oidc-15.0.1.tgz#ba3bcb88e565be7ea4c635a48524963c677be2fe" integrity sha512-5gpqO9QL+qFqMItYFHe8F6H5nOIEaowcNUc9iTDs3P1bfVYnoKoVAaijob53PuPTF4YwzdfwKWZi4Mq6P7GENQ== dependencies: tslib "^2.0.0" angularx-qrcode@^16.0.0: - version "16.0.0" - resolved "https://registry.npmjs.org/angularx-qrcode/-/angularx-qrcode-16.0.0.tgz" - integrity sha512-j6IndIU3m4zfqSPKraJPFgigdHa+pM3kapRPBnKSwgKNSpljPQu3XNiRUCmQmfGfnh39ShDVca/k091WTjngAA== + version "16.0.2" + resolved "https://registry.yarnpkg.com/angularx-qrcode/-/angularx-qrcode-16.0.2.tgz#86af924191546394cb93f9fb8d0c42edd0132894" + integrity sha512-FztOM7vjNu88sGxUU5jG2I+A9TxZBXXYBWINjpwIBbTL+COMgrtzXnScG7TyQeNknv5w3WFJWn59PcngRRYVXA== dependencies: - "@types/qrcode" "1.5.0" - qrcode "1.5.1" + qrcode "1.5.3" tslib "^2.3.0" ansi-colors@4.1.3, ansi-colors@^4.1.1: version "4.1.3" - resolved "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.3.tgz#37611340eb2243e70cc604cad35d63270d48781b" integrity sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw== ansi-escapes@^4.2.1: version "4.3.2" - resolved "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz" + 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.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz" + resolved "https://registry.yarnpkg.com/ansi-html-community/-/ansi-html-community-0.0.8.tgz#69fbc4d6ccbe383f9736934ae34c3f8290f1bf41" integrity sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw== ansi-regex@^2.0.0: version "2.1.1" - resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" integrity sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA== ansi-regex@^5.0.1: version "5.0.1" - resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== ansi-regex@^6.0.1: version "6.0.1" - resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.0.1.tgz#3183e38fae9a65d7cb5e53945cd5897d0260a06a" integrity sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA== ansi-styles@^2.2.1: version "2.2.1" - resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" integrity sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA== ansi-styles@^3.2.1: version "3.2.1" - resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== dependencies: color-convert "^1.9.0" ansi-styles@^4.0.0, ansi-styles@^4.1.0: version "4.3.0" - resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== dependencies: color-convert "^2.0.1" ansi-styles@^6.1.0: version "6.2.1" - resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5" integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== anymatch@~3.1.2: version "3.1.3" - resolved "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== dependencies: normalize-path "^3.0.0" @@ -3995,17 +3660,17 @@ anymatch@~3.1.2: app-root-path@^3.0.0: version "3.1.0" - resolved "https://registry.npmjs.org/app-root-path/-/app-root-path-3.1.0.tgz" + resolved "https://registry.yarnpkg.com/app-root-path/-/app-root-path-3.1.0.tgz#5971a2fc12ba170369a7a1ef018c71e6e47c2e86" integrity sha512-biN3PwB2gUtjaYy/isrU3aNWI5w+fAfvHkSvCKeQGxhmYpwKFUxudR3Yya+KqVRHBmEDYh+/lTozYCFbmzX4nA== "aproba@^1.0.3 || ^2.0.0": version "2.0.0" - resolved "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz" + resolved "https://registry.yarnpkg.com/aproba/-/aproba-2.0.0.tgz#52520b8ae5b569215b354efc0caa3fe1e45a8adc" integrity sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ== are-we-there-yet@^3.0.0: version "3.0.1" - resolved "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz" + resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz#679df222b278c64f2cdba1175cdc00b0d96164bd" integrity sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg== dependencies: delegates "^1.0.0" @@ -4013,14 +3678,14 @@ are-we-there-yet@^3.0.0: argparse@^1.0.7: version "1.0.10" - resolved "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== dependencies: sprintf-js "~1.0.2" argparse@^2.0.1: version "2.0.1" - resolved "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== aria-query@5.3.0: @@ -4032,7 +3697,7 @@ aria-query@5.3.0: aria-query@^3.0.0: version "3.0.0" - resolved "https://registry.npmjs.org/aria-query/-/aria-query-3.0.0.tgz" + resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-3.0.0.tgz#65b3fcc1ca1155a8c9ae64d6eee297f15d5133cc" integrity sha512-majUxHgLehQTeSA+hClx+DY09OVUqG3GtezWkF1krgLGNdlDu9l9V8DaqNMWbq4Eddc8wsyDA0hpDUtnYxQEXw== dependencies: ast-types-flow "0.0.7" @@ -4040,66 +3705,61 @@ aria-query@^3.0.0: array-flatten@1.1.1: version "1.1.1" - resolved "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== -array-flatten@^2.1.2: - version "2.1.2" - resolved "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz" - integrity sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ== - array-union@^1.0.1: version "1.0.2" - resolved "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz" + resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39" integrity sha512-Dxr6QJj/RdU/hCaBjOfxW+q6lyuVE6JFWIrAUpuOOhoJJoQ99cUn3igRaHVB5P9WrgFVN0FfArM3x0cueOU8ng== dependencies: array-uniq "^1.0.1" array-union@^2.1.0: version "2.1.0" - resolved "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz" + resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== array-uniq@^1.0.1: version "1.0.3" - resolved "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz" + resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6" integrity sha512-MNha4BWQ6JbwhFhj03YK552f7cb3AzoE8SzeljgChvL1dl3IcvggXVz1DilzySZkCja+CXuZbdW7yATchWn8/Q== arrify@^1.0.0: version "1.0.1" - resolved "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz" + resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" integrity sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA== asn1@~0.2.3: version "0.2.6" - resolved "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz" + resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.6.tgz#0d3a7bb6e64e02a90c0303b31f292868ea09a08d" integrity sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ== dependencies: safer-buffer "~2.1.0" assert-plus@1.0.0, assert-plus@^1.0.0: version "1.0.0" - resolved "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz" + resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" integrity sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw== ast-types-flow@0.0.7: version "0.0.7" - resolved "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz" + resolved "https://registry.yarnpkg.com/ast-types-flow/-/ast-types-flow-0.0.7.tgz#f70b735c6bca1a5c9c22d982c3e39e7feba3bdad" integrity sha512-eBvWn1lvIApYMhzQMsu9ciLfkBY499mFZlNqG+/9WR7PVlroQw0vG30cOQQbaKz3sCEc44TAOu2ykzqXSNnwag== async@^3.2.3: - version "3.2.4" - resolved "https://registry.npmjs.org/async/-/async-3.2.4.tgz" - integrity sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ== + version "3.2.5" + resolved "https://registry.yarnpkg.com/async/-/async-3.2.5.tgz#ebd52a8fdaf7a2289a24df399f8d8485c8a46b66" + integrity sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg== asynckit@^0.4.0: version "0.4.0" - resolved "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== autoprefixer@10.4.14: version "10.4.14" - resolved "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.14.tgz" + resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.14.tgz#e28d49902f8e759dd25b153264e862df2705f79d" integrity sha512-FQzyfOsTlwVzjHxKEqRIAdJx9niO6VCBCoEwax/VLSoQF29ggECcPuBqUMZ+u8jCZOPSy8b8/8KnuFbp0SaFZQ== dependencies: browserslist "^4.21.5" @@ -4111,26 +3771,26 @@ autoprefixer@10.4.14: aws-sign2@~0.7.0: version "0.7.0" - resolved "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz" + resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" integrity sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA== aws4@^1.8.0: - version "1.12.0" - resolved "https://registry.npmjs.org/aws4/-/aws4-1.12.0.tgz" - integrity sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg== + version "1.13.0" + resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.13.0.tgz#d9b802e9bb9c248d7be5f7f5ef178dc3684e9dcc" + integrity sha512-3AungXC4I8kKsS9PuS4JH2nc+0bVY/mjgrephHTIi8fpEeGsTHBUJeosp0Wc1myYMElmD0B3Oc4XL/HVJ4PV2g== axios@^1.0.0: - version "1.6.1" - resolved "https://registry.yarnpkg.com/axios/-/axios-1.6.1.tgz#76550d644bf0a2d469a01f9244db6753208397d7" - integrity sha512-vfBmhDpKafglh0EldBEbVuoe7DyAavGSLWhuSm5ZSEKQnHhBf0xAAwybbNH1IkrJNGnS/VG4I5yxig1pCEXE4g== + version "1.7.2" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.2.tgz#b625db8a7051fbea61c35a3cbb3a1daa7b9c7621" + integrity sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw== dependencies: - follow-redirects "^1.15.0" + follow-redirects "^1.15.6" form-data "^4.0.0" proxy-from-env "^1.1.0" axobject-query@2.0.2: version "2.0.2" - resolved "https://registry.npmjs.org/axobject-query/-/axobject-query-2.0.2.tgz" + resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-2.0.2.tgz#ea187abe5b9002b377f925d8bf7d1c561adf38f9" integrity sha512-MCeek8ZH7hKyO1rWUbKNQBbl4l2eY0ntk7OGi+q0RlafrCnfPxC06WZA+uebCfmYp4mNU9jRBP1AhGyf8+W3ww== dependencies: ast-types-flow "0.0.7" @@ -4152,7 +3812,7 @@ babel-loader@9.1.3: babel-plugin-istanbul@6.1.1: version "6.1.1" - resolved "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz" + resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz#fa88ec59232fd9b4e36dbbc540a8ec9a9b47da73" integrity sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA== dependencies: "@babel/helper-plugin-utils" "^7.0.0" @@ -4162,69 +3822,69 @@ babel-plugin-istanbul@6.1.1: test-exclude "^6.0.0" babel-plugin-polyfill-corejs2@^0.4.4: - version "0.4.5" - resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.5.tgz#8097b4cb4af5b64a1d11332b6fb72ef5e64a054c" - integrity sha512-19hwUH5FKl49JEsvyTcoHakh6BE0wgXLLptIyKZ3PijHc/Ci521wygORCUCCred+E/twuqRyAkE02BAWPmsHOg== + version "0.4.11" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.11.tgz#30320dfe3ffe1a336c15afdcdafd6fd615b25e33" + integrity sha512-sMEJ27L0gRHShOh5G54uAAPaiCOygY/5ratXuiyb2G46FmlSpc9eFCzYVyDiPxfNbwzA7mYahmjQc5q+CZQ09Q== dependencies: "@babel/compat-data" "^7.22.6" - "@babel/helper-define-polyfill-provider" "^0.4.2" + "@babel/helper-define-polyfill-provider" "^0.6.2" semver "^6.3.1" babel-plugin-polyfill-corejs3@^0.8.2: - version "0.8.3" - resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.8.3.tgz#b4f719d0ad9bb8e0c23e3e630c0c8ec6dd7a1c52" - integrity sha512-z41XaniZL26WLrvjy7soabMXrfPWARN25PZoriDEiLMxAp50AUW3t35BGQUMg5xK3UrpVTtagIDklxYa+MhiNA== + version "0.8.7" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.8.7.tgz#941855aa7fdaac06ed24c730a93450d2b2b76d04" + integrity sha512-KyDvZYxAzkC0Aj2dAPyDzi2Ym15e5JKZSK+maI7NAwSqofvuFglbSsxE7wUOvTg9oFVnHMzVzBKcqEb4PJgtOA== dependencies: - "@babel/helper-define-polyfill-provider" "^0.4.2" - core-js-compat "^3.31.0" + "@babel/helper-define-polyfill-provider" "^0.4.4" + core-js-compat "^3.33.1" babel-plugin-polyfill-regenerator@^0.5.1: - version "0.5.2" - resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.2.tgz#80d0f3e1098c080c8b5a65f41e9427af692dc326" - integrity sha512-tAlOptU0Xj34V1Y2PNTL4Y0FOJMDB6bZmoW39FeCQIhigGLkqu3Fj6uiXpxIf6Ij274ENdYx64y6Au+ZKlb1IA== + version "0.5.5" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.5.tgz#8b0c8fc6434239e5d7b8a9d1f832bb2b0310f06a" + integrity sha512-OJGYZlhLqBh2DDHeqAxWB1XIvr49CxiJ2gIt61/PU55CQK4Z58OzMqjDe1zwQdQk+rBYsRc+1rJmdajM3gimHg== dependencies: - "@babel/helper-define-polyfill-provider" "^0.4.2" + "@babel/helper-define-polyfill-provider" "^0.5.0" balanced-match@^1.0.0: version "1.0.2" - resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== base64-js@^1.2.0, base64-js@^1.3.1: version "1.5.1" - resolved "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== base64id@2.0.0, base64id@~2.0.0: version "2.0.0" - resolved "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz" + resolved "https://registry.yarnpkg.com/base64id/-/base64id-2.0.0.tgz#2770ac6bc47d312af97a8bf9a634342e0cd25cb6" integrity sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog== batch@0.6.1: version "0.6.1" - resolved "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz" + resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16" integrity sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw== bcrypt-pbkdf@^1.0.0: version "1.0.2" - resolved "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz" + resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" integrity sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w== dependencies: tweetnacl "^0.14.3" big.js@^5.2.2: version "5.2.2" - resolved "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz" + resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== binary-extensions@^2.0.0: - version "2.2.0" - resolved "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz" - integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== + version "2.3.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522" + integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw== bl@^4.0.3, bl@^4.1.0: version "4.1.0" - resolved "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz" + resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a" integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w== dependencies: buffer "^5.5.0" @@ -4233,32 +3893,14 @@ bl@^4.0.3, bl@^4.1.0: blocking-proxy@^1.0.0: version "1.0.1" - resolved "https://registry.npmjs.org/blocking-proxy/-/blocking-proxy-1.0.1.tgz" + resolved "https://registry.yarnpkg.com/blocking-proxy/-/blocking-proxy-1.0.1.tgz#81d6fd1fe13a4c0d6957df7f91b75e98dac40cb2" integrity sha512-KE8NFMZr3mN2E0HcvCgRtX7DjhiIQrwle+nSVJVC/yqFb9+xznHl2ZcoBp2L9qzkI4t4cBFJ1efXF8Dwi132RA== dependencies: minimist "^1.2.0" -body-parser@1.20.1: - version "1.20.1" - resolved "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz" - integrity sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw== - dependencies: - bytes "3.1.2" - content-type "~1.0.4" - debug "2.6.9" - depd "2.0.0" - destroy "1.2.0" - http-errors "2.0.0" - iconv-lite "0.4.24" - on-finished "2.4.1" - qs "6.11.0" - raw-body "2.5.1" - type-is "~1.6.18" - unpipe "1.0.0" - -body-parser@^1.19.0: +body-parser@1.20.2, body-parser@^1.19.0: version "1.20.2" - resolved "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.2.tgz#6feb0e21c4724d06de7ff38da36dad4f57a747fd" integrity sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA== dependencies: bytes "3.1.2" @@ -4275,23 +3917,21 @@ body-parser@^1.19.0: unpipe "1.0.0" bonjour-service@^1.0.11: - version "1.1.1" - resolved "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.1.1.tgz" - integrity sha512-Z/5lQRMOG9k7W+FkeGTNjh7htqn/2LMnfOvBZ8pynNZCM9MwkQkI3zeI4oz09uWdcgmgHugVvBqxGg4VQJ5PCg== + version "1.2.1" + resolved "https://registry.yarnpkg.com/bonjour-service/-/bonjour-service-1.2.1.tgz#eb41b3085183df3321da1264719fbada12478d02" + integrity sha512-oSzCS2zV14bh2kji6vNe7vrpJYCHGvcZnlffFQ1MEoX/WOeQ/teD8SYWKR942OI3INjq8OMNJlbPK5LLLUxFDw== dependencies: - array-flatten "^2.1.2" - dns-equal "^1.0.0" fast-deep-equal "^3.1.3" multicast-dns "^7.2.5" boolbase@^1.0.0: version "1.0.0" - resolved "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz" + resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" integrity sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww== brace-expansion@^1.1.7: version "1.1.11" - resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== dependencies: balanced-match "^1.0.0" @@ -4299,58 +3939,48 @@ brace-expansion@^1.1.7: brace-expansion@^2.0.1: version "2.0.1" - resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== dependencies: balanced-match "^1.0.0" -braces@^3.0.2, braces@~3.0.2: - version "3.0.2" - resolved "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz" - integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== +braces@^3.0.2, braces@^3.0.3, braces@~3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== dependencies: - fill-range "^7.0.1" + fill-range "^7.1.1" browser-process-hrtime@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz#3c9b4b7d782c8121e56f10106d84c0d0ffc94626" integrity sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow== -browserslist@^4.14.5, browserslist@^4.21.3, browserslist@^4.21.5: - version "4.21.5" - resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.21.5.tgz" - integrity sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w== +browserslist@^4.14.5, browserslist@^4.21.5, browserslist@^4.23.0, browserslist@^4.23.1: + version "4.23.2" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.23.2.tgz#244fe803641f1c19c28c48c4b6ec9736eb3d32ed" + integrity sha512-qkqSyistMYdxAcw+CzbZwlBy8AGmS/eEWs+sEV5TnLRGDOL+C5M2EnH6tlZyg0YoAxGJAFKh61En9BR941GnHA== dependencies: - caniuse-lite "^1.0.30001449" - electron-to-chromium "^1.4.284" - node-releases "^2.0.8" - update-browserslist-db "^1.0.10" - -browserslist@^4.21.9: - version "4.21.9" - resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.21.9.tgz" - integrity sha512-M0MFoZzbUrRU4KNfCrDLnvyE7gub+peetoTid3TBIqtunaDJyXlwhakT+/VkvSXcfIzFfK/nkCs4nmyTmxdNSg== - dependencies: - caniuse-lite "^1.0.30001503" - electron-to-chromium "^1.4.431" - node-releases "^2.0.12" - update-browserslist-db "^1.0.11" + caniuse-lite "^1.0.30001640" + electron-to-chromium "^1.4.820" + node-releases "^2.0.14" + update-browserslist-db "^1.1.0" browserstack@^1.5.1: version "1.6.1" - resolved "https://registry.npmjs.org/browserstack/-/browserstack-1.6.1.tgz" + resolved "https://registry.yarnpkg.com/browserstack/-/browserstack-1.6.1.tgz#e051f9733ec3b507659f395c7a4765a1b1e358b3" integrity sha512-GxtFjpIaKdbAyzHfFDKixKO8IBT7wR3NjbzrGc78nNs/Ciys9wU3/nBtsqsWv5nDSrdI5tz0peKuzCPuNXNUiw== dependencies: https-proxy-agent "^2.2.1" buffer-from@^1.0.0: version "1.1.2" - resolved "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== buffer@^5.5.0: version "5.7.1" - resolved "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== dependencies: base64-js "^1.3.1" @@ -4358,32 +3988,25 @@ buffer@^5.5.0: buffer@^6.0.3: version "6.0.3" - resolved "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== dependencies: base64-js "^1.3.1" ieee754 "^1.2.1" -builtins@^5.0.0: - version "5.0.1" - resolved "https://registry.npmjs.org/builtins/-/builtins-5.0.1.tgz" - integrity sha512-qwVpFEHNfhYJIzNRBvd2C1kyo6jz3ZSMPyyuR47OPdiKWlbYnZNyDWuyR175qDnAJLiCo5fBBqPb3RiXgWlkOQ== - dependencies: - semver "^7.0.0" - bytes@3.0.0: version "3.0.0" - resolved "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" integrity sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw== bytes@3.1.2: version "3.1.2" - resolved "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== cacache@^16.1.0: version "16.1.3" - resolved "https://registry.npmjs.org/cacache/-/cacache-16.1.3.tgz" + resolved "https://registry.yarnpkg.com/cacache/-/cacache-16.1.3.tgz#a02b9f34ecfaf9a78c9f4bc16fceb94d5d67a38e" integrity sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ== dependencies: "@npmcli/fs" "^2.1.0" @@ -4406,15 +4029,15 @@ cacache@^16.1.0: unique-filename "^2.0.0" cacache@^17.0.0: - version "17.1.0" - resolved "https://registry.npmjs.org/cacache/-/cacache-17.1.0.tgz" - integrity sha512-hXpFU+Z3AfVmNuiLve1qxWHMq0RSIt5gjCKAHi/M6DktwFwDdAXAtunl1i4WSKaaVcU9IsRvXFg42jTHigcC6Q== + version "17.1.4" + resolved "https://registry.yarnpkg.com/cacache/-/cacache-17.1.4.tgz#b3ff381580b47e85c6e64f801101508e26604b35" + integrity sha512-/aJwG2l3ZMJ1xNAnqbMpA40of9dj/pIH3QfiuQSqjfPJF747VR0J/bHn+/KdNnHKc6XQcWt/AfRSBft82W1d2A== dependencies: "@npmcli/fs" "^3.1.0" fs-minipass "^3.0.0" glob "^10.2.2" lru-cache "^7.7.1" - minipass "^5.0.0" + minipass "^7.0.3" minipass-collect "^1.0.2" minipass-flush "^1.0.5" minipass-pipeline "^1.2.4" @@ -4423,42 +4046,40 @@ cacache@^17.0.0: tar "^6.1.11" unique-filename "^3.0.0" -call-bind@^1.0.0: - version "1.0.2" - resolved "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz" - integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA== +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== dependencies: - function-bind "^1.1.1" - get-intrinsic "^1.0.2" + es-define-property "^1.0.0" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + set-function-length "^1.2.1" callsites@^3.0.0: version "3.1.0" - resolved "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== camelcase@^5.0.0, camelcase@^5.3.1: version "5.3.1" - resolved "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== -caniuse-lite@^1.0.30001449, caniuse-lite@^1.0.30001464: - version "1.0.30001487" - resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001487.tgz" - integrity sha512-83564Z3yWGqXsh2vaH/mhXfEM0wX+NlBCm1jYHOb97TrTWJEmPTccZgeLTPBUUb0PNVo+oomb7wkimZBIERClA== - -caniuse-lite@^1.0.30001503: - version "1.0.30001513" - resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001513.tgz" - integrity sha512-pnjGJo7SOOjAGytZZ203Em95MRM8Cr6jhCXNF/FAXTpCTRTECnqQWLpiTRqrFtdYcth8hf4WECUpkezuYsMVww== +caniuse-lite@^1.0.30001464, caniuse-lite@^1.0.30001640: + version "1.0.30001644" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001644.tgz#bcd4212a7a03bdedba1ea850b8a72bfe4bec2395" + integrity sha512-YGvlOZB4QhZuiis+ETS0VXR+MExbFf4fZYYeMTEE0aTQd/RdIjkTyZjLrbYVKnHzppDvnOhritRVv+i7Go6mHw== caseless@~0.12.0: version "0.12.0" - resolved "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz" + resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" integrity sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw== chalk@^1.1.1, chalk@^1.1.3: version "1.1.3" - resolved "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" integrity sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A== dependencies: ansi-styles "^2.2.1" @@ -4467,9 +4088,9 @@ chalk@^1.1.1, chalk@^1.1.3: strip-ansi "^3.0.0" supports-color "^2.0.0" -chalk@^2.0.0, chalk@^2.4.2: +chalk@^2.4.2: version "2.4.2" - resolved "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== dependencies: ansi-styles "^3.2.1" @@ -4478,7 +4099,7 @@ chalk@^2.0.0, chalk@^2.4.2: chalk@^4.0.0, chalk@^4.0.2, chalk@^4.1.0, chalk@^4.1.1: version "4.1.2" - resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== dependencies: ansi-styles "^4.1.0" @@ -4486,12 +4107,12 @@ chalk@^4.0.0, chalk@^4.0.2, chalk@^4.1.0, chalk@^4.1.1: chardet@^0.7.0: version "0.7.0" - resolved "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz" + resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== -chokidar@3.5.3, "chokidar@>=3.0.0 <4.0.0", chokidar@^3.0.0, chokidar@^3.5.1, chokidar@^3.5.3: +chokidar@3.5.3: version "3.5.3" - resolved "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== dependencies: anymatch "~3.1.2" @@ -4504,19 +4125,34 @@ chokidar@3.5.3, "chokidar@>=3.0.0 <4.0.0", chokidar@^3.0.0, chokidar@^3.5.1, cho optionalDependencies: fsevents "~2.3.2" +"chokidar@>=3.0.0 <4.0.0", chokidar@^3.0.0, chokidar@^3.5.1, 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== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + chownr@^2.0.0: version "2.0.0" - resolved "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece" integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ== chrome-trace-event@^1.0.2: - version "1.0.3" - resolved "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz" - integrity sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg== + version "1.0.4" + resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz#05bffd7ff928465093314708c93bdfa9bd1f0f5b" + integrity sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ== clean-stack@^2.0.0: version "2.2.0" - resolved "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz" + resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== clean-stack@^4.0.0: @@ -4528,29 +4164,29 @@ clean-stack@^4.0.0: cli-cursor@3.1.0, cli-cursor@^3.1.0: version "3.1.0" - resolved "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz" + resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307" integrity sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw== dependencies: restore-cursor "^3.1.0" cli-spinners@2.6.1: version "2.6.1" - resolved "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.6.1.tgz" + resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.6.1.tgz#adc954ebe281c37a6319bfa401e6dd2488ffb70d" integrity sha512-x/5fWmGMnbKQAaNwN+UZlV79qBLM9JFnJuJ03gIi5whrob0xV0ofNVHy9DhwGdsMJQc2OKv0oGmLzvaqvAVv+g== cli-spinners@^2.5.0: - version "2.9.0" - resolved "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.0.tgz" - integrity sha512-4/aL9X3Wh0yiMQlE+eeRhWP6vclO3QRtw1JHKIT0FFUs5FjpFmESqtMvYZ0+lbzBw900b95mS0hohy+qn2VK/g== + version "2.9.2" + resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.9.2.tgz#1773a8f4b9c4d6ac31563df53b3fc1d79462fe41" + integrity sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg== cli-width@^3.0.0: version "3.0.0" - resolved "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz" + resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-3.0.0.tgz#a2f48437a2caa9a22436e794bf071ec9e61cedf6" integrity sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw== cliui@^6.0.0: version "6.0.0" - resolved "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-6.0.0.tgz#511d702c0c4e41ca156d7d0e96021f23e13225b1" integrity sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ== dependencies: string-width "^4.2.0" @@ -4559,7 +4195,7 @@ cliui@^6.0.0: cliui@^7.0.2: version "7.0.4" - resolved "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== dependencies: string-width "^4.2.0" @@ -4568,7 +4204,7 @@ cliui@^7.0.2: cliui@^8.0.1: version "8.0.1" - resolved "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa" integrity sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ== dependencies: string-width "^4.2.0" @@ -4577,7 +4213,7 @@ cliui@^8.0.1: clone-deep@^4.0.1: version "4.0.1" - resolved "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz" + resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387" integrity sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ== dependencies: is-plain-object "^2.0.4" @@ -4586,12 +4222,12 @@ clone-deep@^4.0.1: clone@^1.0.2: version "1.0.4" - resolved "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz" + resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" integrity sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg== codelyzer@^6.0.2: version "6.0.2" - resolved "https://registry.npmjs.org/codelyzer/-/codelyzer-6.0.2.tgz" + resolved "https://registry.yarnpkg.com/codelyzer/-/codelyzer-6.0.2.tgz#25d72eae641e8ff13ffd7d99b27c9c7ad5d7e135" integrity sha512-v3+E0Ucu2xWJMOJ2fA/q9pDT/hlxHftHGPUay1/1cTgyPV5JTHFdO9hqo837Sx2s9vKBMTt5gO+lhF95PO6J+g== dependencies: "@angular/compiler" "9.0.0" @@ -4610,59 +4246,59 @@ codelyzer@^6.0.2: zone.js "~0.10.3" codemirror@^5.65.8: - version "5.65.13" - resolved "https://registry.npmjs.org/codemirror/-/codemirror-5.65.13.tgz" - integrity sha512-SVWEzKXmbHmTQQWaz03Shrh4nybG0wXx2MEu3FO4ezbPW8IbnZEd5iGHGEffSUaitKYa3i+pHpBsSvw8sPHtzg== + version "5.65.17" + resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-5.65.17.tgz#00d71f34c3518471ae4c0de23a2f8bb39a6df6ca" + integrity sha512-1zOsUx3lzAOu/gnMAZkQ9kpIHcPYOc9y1Fbm2UVk5UBPkdq380nhkelG0qUwm1f7wPvTbndu9ZYlug35EwAZRQ== color-convert@^1.9.0: version "1.9.3" - resolved "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== dependencies: color-name "1.1.3" color-convert@^2.0.1: version "2.0.1" - resolved "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== dependencies: color-name "~1.1.4" color-name@1.1.3: version "1.1.3" - resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== color-name@~1.1.4: version "1.1.4" - resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== color-support@^1.1.3: version "1.1.3" - resolved "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz" + resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2" integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg== colorette@^2.0.10: version "2.0.20" - resolved "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a" integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w== colors@1.4.0: version "1.4.0" - resolved "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz" + resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78" integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA== combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.6: version "1.0.8" - resolved "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== dependencies: delayed-stream "~1.0.0" commander@^2.11.0, commander@^2.20.0: version "2.20.3" - resolved "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== common-path-prefix@^3.0.0: @@ -4672,14 +4308,14 @@ common-path-prefix@^3.0.0: compressible@~2.0.16: version "2.0.18" - resolved "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz" + resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba" integrity sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg== dependencies: mime-db ">= 1.43.0 < 2" compression@^1.7.4: version "1.7.4" - resolved "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz" + resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.4.tgz#95523eff170ca57c29a0ca41e6fe131f41e5bb8f" integrity sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ== dependencies: accepts "~1.3.5" @@ -4692,17 +4328,17 @@ compression@^1.7.4: concat-map@0.0.1: version "0.0.1" - resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== connect-history-api-fallback@^2.0.0: version "2.0.0" - resolved "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz" + resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz#647264845251a0daf25b97ce87834cace0f5f1c8" integrity sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA== connect@^3.7.0: version "3.7.0" - resolved "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz" + resolved "https://registry.yarnpkg.com/connect/-/connect-3.7.0.tgz#5d49348910caa5e07a01800b030d0c35f20484f8" integrity sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ== dependencies: debug "2.6.9" @@ -4712,51 +4348,56 @@ connect@^3.7.0: console-control-strings@^1.1.0: version "1.1.0" - resolved "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz" + resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" integrity sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ== content-disposition@0.5.4: version "0.5.4" - resolved "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== dependencies: safe-buffer "5.2.1" content-type@~1.0.4, content-type@~1.0.5: version "1.0.5" - resolved "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== convert-source-map@^1.5.1, convert-source-map@^1.7.0: version "1.9.0" - resolved "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.9.0.tgz#7faae62353fb4213366d0ca98358d22e8368b05f" integrity sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A== +convert-source-map@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" + integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== + cookie-signature@1.0.6: version "1.0.6" - resolved "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== -cookie@0.5.0: - version "0.5.0" - resolved "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz" - integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== +cookie@0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.6.0.tgz#2798b04b071b0ecbff0dbb62a505a8efa4e19051" + integrity sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw== cookie@~0.4.1: version "0.4.2" - resolved "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432" integrity sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA== copy-anything@^2.0.1: version "2.0.6" - resolved "https://registry.npmjs.org/copy-anything/-/copy-anything-2.0.6.tgz" + resolved "https://registry.yarnpkg.com/copy-anything/-/copy-anything-2.0.6.tgz#092454ea9584a7b7ad5573062b2a87f5900fc480" integrity sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw== dependencies: is-what "^3.14.1" copy-webpack-plugin@11.0.0: version "11.0.0" - resolved "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-11.0.0.tgz" + resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-11.0.0.tgz#96d4dbdb5f73d02dd72d0528d1958721ab72e04a" integrity sha512-fX2MWpamkW0hZxMEg0+mYnA40LTosOSa5TqZ9GYIBzyJa9C3QUaMPSE2xAi/buNr8u89SfD9wHSQVBzrRa/SOQ== dependencies: fast-glob "^3.2.11" @@ -4766,39 +4407,39 @@ copy-webpack-plugin@11.0.0: schema-utils "^4.0.0" serialize-javascript "^6.0.0" -core-js-compat@^3.31.0: - version "3.31.1" - resolved "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.31.1.tgz" - integrity sha512-wIDWd2s5/5aJSdpOJHfSibxNODxoGoWOBHt8JSPB41NOE94M7kuTPZCYLOlTtuoXTsBPKobpJ6T+y0SSy5L9SA== +core-js-compat@^3.31.0, core-js-compat@^3.33.1: + version "3.37.1" + resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.37.1.tgz#c844310c7852f4bdf49b8d339730b97e17ff09ee" + integrity sha512-9TNiImhKvQqSUkOvk/mMRZzOANTiEVC7WaBNhHcKM7x+/5E1l5NvsysR19zuDQScE8k+kfQXWRN3AtS/eOSHpg== dependencies: - browserslist "^4.21.9" + browserslist "^4.23.0" core-util-is@1.0.2: version "1.0.2" - resolved "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" integrity sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ== core-util-is@~1.0.0: version "1.0.3" - resolved "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== cors@^2.8.5, cors@~2.8.5: version "2.8.5" - resolved "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz" + resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29" integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g== dependencies: object-assign "^4" vary "^1" cosmiconfig@^8.2.0: - version "8.2.0" - resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-8.2.0.tgz#f7d17c56a590856cd1e7cee98734dca272b0d8fd" - integrity sha512-3rTMnFJA1tCOPwRxtgF4wd7Ab2qvDbL8jX+3smjIbS4HlZBagTlpERbdN7iAbWlrfxE3M8c27kTwTawQ7st+OQ== + version "8.3.6" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-8.3.6.tgz#060a2b871d66dba6c8538ea1118ba1ac16f5fae3" + integrity sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA== dependencies: - import-fresh "^3.2.1" + import-fresh "^3.3.0" js-yaml "^4.1.0" - parse-json "^5.0.0" + parse-json "^5.2.0" path-type "^4.0.0" critters@0.0.20: @@ -4816,7 +4457,7 @@ critters@0.0.20: cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3: version "7.0.3" - resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== dependencies: path-key "^3.1.0" @@ -4825,7 +4466,7 @@ cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3: css-loader@6.8.1: version "6.8.1" - resolved "https://registry.npmjs.org/css-loader/-/css-loader-6.8.1.tgz" + resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-6.8.1.tgz#0f8f52699f60f5e679eab4ec0fcd68b8e8a50a88" integrity sha512-xDAXtEVGlD0gJ07iclwWVkLoZOpEvAWaSyf6W18S2pOC//K8+qUDIx8IIT3D+HjnmkJPQeesOPv5aiUaJsCM2g== dependencies: icss-utils "^5.1.0" @@ -4839,7 +4480,7 @@ css-loader@6.8.1: css-select@^5.1.0: version "5.1.0" - resolved "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz" + resolved "https://registry.yarnpkg.com/css-select/-/css-select-5.1.0.tgz#b8ebd6554c3637ccc76688804ad3f6a6fdaea8a6" integrity sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg== dependencies: boolbase "^1.0.0" @@ -4850,7 +4491,7 @@ css-select@^5.1.0: css-selector-tokenizer@^0.7.1: version "0.7.3" - resolved "https://registry.npmjs.org/css-selector-tokenizer/-/css-selector-tokenizer-0.7.3.tgz" + resolved "https://registry.yarnpkg.com/css-selector-tokenizer/-/css-selector-tokenizer-0.7.3.tgz#735f26186e67c749aaf275783405cf0661fae8f1" integrity sha512-jWQv3oCEL5kMErj4wRnK/OPoBi0D+P1FR2cDCKYPaMeD2eW3/mttav8HT4hT1CKopiJI/psEULjkClhvJo4Lvg== dependencies: cssesc "^3.0.0" @@ -4858,19 +4499,19 @@ css-selector-tokenizer@^0.7.1: css-what@^6.1.0: version "6.1.0" - resolved "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz" + resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4" integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw== cssauron@^1.4.0: version "1.4.0" - resolved "https://registry.npmjs.org/cssauron/-/cssauron-1.4.0.tgz" + resolved "https://registry.yarnpkg.com/cssauron/-/cssauron-1.4.0.tgz#a6602dff7e04a8306dc0db9a551e92e8b5662ad8" integrity sha512-Ht70DcFBh+/ekjVrYS2PlDMdSQEl3OFNmjK6lcn49HptBgilXf/Zwg4uFh9Xn0pX3Q8YOkSjIFOfK2osvdqpBw== dependencies: through X.X.X cssesc@^3.0.0: version "3.0.0" - resolved "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz" + resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== cssom@^0.4.4: @@ -4892,17 +4533,17 @@ cssstyle@^2.3.0: custom-event@~1.0.0: version "1.0.1" - resolved "https://registry.npmjs.org/custom-event/-/custom-event-1.0.1.tgz" + resolved "https://registry.yarnpkg.com/custom-event/-/custom-event-1.0.1.tgz#5d02a46850adf1b4a317946a3928fccb5bfd0425" integrity sha512-GAj5FOq0Hd+RsCGVJxZuKaIDXDf3h6GQoNEjFgbLLI/trgtavwUbSnZ5pVfg27DVCaWjIohryS0JFwIJyT2cMg== damerau-levenshtein@^1.0.4: version "1.0.8" - resolved "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz" + resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7" integrity sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA== dashdash@^1.12.0: version "1.14.1" - resolved "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz" + resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" integrity sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g== dependencies: assert-plus "^1.0.0" @@ -4918,33 +4559,33 @@ data-urls@^2.0.0: date-format@^4.0.14: version "4.0.14" - resolved "https://registry.npmjs.org/date-format/-/date-format-4.0.14.tgz" + resolved "https://registry.yarnpkg.com/date-format/-/date-format-4.0.14.tgz#7a8e584434fb169a521c8b7aa481f355810d9400" integrity sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg== debug@2.6.9: version "2.6.9" - resolved "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== dependencies: ms "2.0.0" -debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4, debug@~4.3.1, debug@~4.3.2: - version "4.3.4" - resolved "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz" - integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== +debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4, debug@~4.3.1, debug@~4.3.2, debug@~4.3.4: + version "4.3.6" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.6.tgz#2ab2c38fbaffebf8aa95fdfe6d88438c7a13c52b" + integrity sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg== dependencies: ms "2.1.2" -debug@^3.1.0, debug@^3.2.6: +debug@^3.1.0: version "3.2.7" - resolved "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== dependencies: ms "^2.1.1" decamelize@^1.2.0: version "1.2.0" - resolved "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" integrity sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA== decimal.js@^10.2.1: @@ -4954,31 +4595,40 @@ decimal.js@^10.2.1: deep-is@^0.1.3: version "0.1.4" - resolved "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz" + resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== default-gateway@^6.0.3: version "6.0.3" - resolved "https://registry.npmjs.org/default-gateway/-/default-gateway-6.0.3.tgz" + resolved "https://registry.yarnpkg.com/default-gateway/-/default-gateway-6.0.3.tgz#819494c888053bdb743edbf343d6cdf7f2943a71" integrity sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg== dependencies: execa "^5.0.0" defaults@^1.0.3: version "1.0.4" - resolved "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz" + resolved "https://registry.yarnpkg.com/defaults/-/defaults-1.0.4.tgz#b0b02062c1e2aa62ff5d9528f0f98baa90978d7a" integrity sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A== dependencies: clone "^1.0.2" +define-data-property@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" + integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + gopd "^1.0.1" + define-lazy-prop@^2.0.0: version "2.0.0" - resolved "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz" + resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz#3f7ae421129bcaaac9bc74905c98a0009ec9ee7f" integrity sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og== del@^2.2.0: version "2.2.2" - resolved "https://registry.npmjs.org/del/-/del-2.2.2.tgz" + resolved "https://registry.yarnpkg.com/del/-/del-2.2.2.tgz#c12c981d067846c84bcaf862cff930d907ffd1a8" integrity sha512-Z4fzpbIRjOu7lO5jCETSWoqUDVe0IPOlfugBsF6suen2LKDlVb4QZpKEM9P+buNJ4KI1eN7I083w/pbKUpsrWQ== dependencies: globby "^5.0.0" @@ -4991,22 +4641,22 @@ del@^2.2.0: delayed-stream@~1.0.0: version "1.0.0" - resolved "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== delegates@^1.0.0: version "1.0.0" - resolved "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz" + resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" integrity sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ== -depd@2.0.0, depd@^2.0.0: +depd@2.0.0: version "2.0.0" - resolved "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz" + resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== depd@~1.1.2: version "1.1.2" - resolved "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz" + resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ== dequal@^2.0.3: @@ -5016,58 +4666,53 @@ dequal@^2.0.3: destroy@1.2.0: version "1.2.0" - resolved "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== detect-node@^2.0.4: version "2.1.0" - resolved "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz" + resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.1.0.tgz#c9c70775a49c3d03bc2c06d9a73be550f978f8b1" integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g== di@^0.0.1: version "0.0.1" - resolved "https://registry.npmjs.org/di/-/di-0.0.1.tgz" + resolved "https://registry.yarnpkg.com/di/-/di-0.0.1.tgz#806649326ceaa7caa3306d75d985ea2748ba913c" integrity sha512-uJaamHkagcZtHPqCIHZxnFrXlunQXgBOsZSUOWwFw31QJCAbyTBoHMW75YOTur5ZNx8pIeAKgf6GWIgaqqiLhA== diacritics@1.3.0: version "1.3.0" - resolved "https://registry.npmjs.org/diacritics/-/diacritics-1.3.0.tgz" + resolved "https://registry.yarnpkg.com/diacritics/-/diacritics-1.3.0.tgz#3efa87323ebb863e6696cebb0082d48ff3d6f7a1" integrity sha512-wlwEkqcsaxvPJML+rDh/2iS824jbREk6DUMUKkEaSlxdYHeS43cClJtsWglvw2RfeXGm6ohKDqsXteJ5sP5enA== dijkstrajs@^1.0.1: version "1.0.3" - resolved "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz" + resolved "https://registry.yarnpkg.com/dijkstrajs/-/dijkstrajs-1.0.3.tgz#4c8dbdea1f0f6478bff94d9c49c784d623e4fc23" integrity sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA== dir-glob@^3.0.1: version "3.0.1" - resolved "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz" + resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== dependencies: path-type "^4.0.0" -dns-equal@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz" - integrity sha512-z+paD6YUQsk+AbGCEM4PrOXSss5gd66QfcVBFTKR/HpFL9jCqikS94HYwKww6fQyO7IxrIIyUu+g0Ka9tUS2Cg== - dns-packet@^5.2.2: - version "5.6.0" - resolved "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.0.tgz" - integrity sha512-rza3UH1LwdHh9qyPXp8lkwpjSNk/AMD3dPytUoRoqnypDUhY0xvbdmVhWOfxO68frEfV9BU8V12Ez7ZsHGZpCQ== + version "5.6.1" + resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-5.6.1.tgz#ae888ad425a9d1478a0674256ab866de1012cf2f" + integrity sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw== dependencies: "@leichtgewicht/ip-codec" "^2.0.1" doctrine@^3.0.0: version "3.0.0" - resolved "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== dependencies: esutils "^2.0.2" dom-serialize@^2.2.1: version "2.2.1" - resolved "https://registry.npmjs.org/dom-serialize/-/dom-serialize-2.2.1.tgz" + resolved "https://registry.yarnpkg.com/dom-serialize/-/dom-serialize-2.2.1.tgz#562ae8999f44be5ea3076f5419dcd59eb43ac95b" integrity sha512-Yra4DbvoW7/Z6LBN560ZwXMjoNOSAN2wRsKFGc4iBeso+mpIA6qj1vfdf9HpMaKAqG6wXTy+1SYEzmNpKXOSsQ== dependencies: custom-event "~1.0.0" @@ -5077,7 +4722,7 @@ dom-serialize@^2.2.1: dom-serializer@^2.0.0: version "2.0.0" - resolved "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-2.0.0.tgz#e41b802e1eedf9f6cae183ce5e622d789d7d8e53" integrity sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg== dependencies: domelementtype "^2.3.0" @@ -5086,7 +4731,7 @@ dom-serializer@^2.0.0: domelementtype@^2.3.0: version "2.3.0" - resolved "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d" integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw== domexception@^2.0.1: @@ -5098,14 +4743,14 @@ domexception@^2.0.1: domhandler@^5.0.2, domhandler@^5.0.3: version "5.0.3" - resolved "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-5.0.3.tgz#cc385f7f751f1d1fc650c21374804254538c7d31" integrity sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w== dependencies: domelementtype "^2.3.0" domutils@^3.0.1: version "3.1.0" - resolved "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-3.1.0.tgz#c47f551278d3dc4b0b1ab8cbb42d751a6f0d824e" integrity sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA== dependencies: dom-serializer "^2.0.0" @@ -5114,22 +4759,22 @@ domutils@^3.0.1: dotenv@~10.0.0: version "10.0.0" - resolved "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-10.0.0.tgz#3d4227b8fb95f81096cdd2b66653fb2c7085ba81" integrity sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q== duplexer@^0.1.1: version "0.1.2" - resolved "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz" + resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.2.tgz#3abe43aef3835f8ae077d136ddce0f276b0400e6" integrity sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg== eastasianwidth@^0.2.0: version "0.2.0" - resolved "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz" + resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== ecc-jsbn@~0.1.1: version "0.1.2" - resolved "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz" + resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" integrity sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw== dependencies: jsbn "~0.1.0" @@ -5137,74 +4782,69 @@ ecc-jsbn@~0.1.1: ee-first@1.1.1: version "1.1.1" - resolved "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== ejs@^3.1.7: - version "3.1.9" - resolved "https://registry.npmjs.org/ejs/-/ejs-3.1.9.tgz" - integrity sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ== + version "3.1.10" + resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.10.tgz#69ab8358b14e896f80cc39e62087b88500c3ac3b" + integrity sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA== dependencies: jake "^10.8.5" -electron-to-chromium@^1.4.284: - version "1.4.396" - resolved "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.396.tgz" - integrity sha512-pqKTdqp/c5vsrc0xUPYXTDBo9ixZuGY8es4ZOjjd6HD6bFYbu5QA09VoW3fkY4LF1T0zYk86lN6bZnNlBuOpdQ== - -electron-to-chromium@^1.4.431: - version "1.4.453" - resolved "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.453.tgz" - integrity sha512-BU8UtQz6CB3T7RIGhId4BjmjJVXQDujb0+amGL8jpcluFJr6lwspBOvkUbnttfpZCm4zFMHmjrX1QrdPWBBMjQ== +electron-to-chromium@^1.4.820: + version "1.5.3" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.3.tgz#032bbb8661c0449656fd896e805c8f7150229a0f" + integrity sha512-QNdYSS5i8D9axWp/6XIezRObRHqaav/ur9z1VzCDUCH1XIFOr9WQk5xmgunhsTpjjgDy3oLxO/WMOVZlpUQrlA== emoji-regex@^8.0.0: version "8.0.0" - resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== emoji-regex@^9.2.2: version "9.2.2" - resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== emojis-list@^3.0.0: version "3.0.0" - resolved "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz" + resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78" integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q== encode-utf8@^1.0.3: version "1.0.3" - resolved "https://registry.npmjs.org/encode-utf8/-/encode-utf8-1.0.3.tgz" + resolved "https://registry.yarnpkg.com/encode-utf8/-/encode-utf8-1.0.3.tgz#f30fdd31da07fb596f281beb2f6b027851994cda" integrity sha512-ucAnuBEhUK4boH2HjVYG5Q2mQyPorvv0u/ocS+zhdw0S8AlHYY+GOFhP1Gio5z4icpP2ivFSvhtFjQi8+T9ppw== encodeurl@~1.0.2: version "1.0.2" - resolved "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== encoding@^0.1.13: version "0.1.13" - resolved "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz" + resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.13.tgz#56574afdd791f54a8e9b2785c0582a2d26210fa9" integrity sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A== dependencies: iconv-lite "^0.6.2" end-of-stream@^1.4.1: version "1.4.4" - resolved "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== dependencies: once "^1.4.0" -engine.io-parser@~5.0.3: - version "5.0.6" - resolved "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.6.tgz" - integrity sha512-tjuoZDMAdEhVnSFleYPCtdL2GXwVTGtNjoeJd9IhIG3C1xs9uwxqRNEu5WpnDZCaozwVlK/nuQhpodhXSIMaxw== +engine.io-parser@~5.2.1: + version "5.2.3" + resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.2.3.tgz#00dc5b97b1f233a23c9398d0209504cf5f94d92f" + integrity sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q== -engine.io@~6.4.1: - version "6.4.2" - resolved "https://registry.npmjs.org/engine.io/-/engine.io-6.4.2.tgz" - integrity sha512-FKn/3oMiJjrOEOeUub2WCox6JhxBXq/Zn3fZOMCBxKnNYtsdKjxhl7yR3fZhM9PV+rdE75SU5SYMc+2PGzo+Tg== +engine.io@~6.5.2: + version "6.5.5" + resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-6.5.5.tgz#430b80d8840caab91a50e9e23cb551455195fc93" + integrity sha512-C5Pn8Wk+1vKBoHghJODM63yk8MvrO9EWZUfkAt5HAqIgPE4/8FF0PEGHXtEd40l223+cE5ABWuPzm38PHFXfMA== dependencies: "@types/cookie" "^0.4.1" "@types/cors" "^2.8.12" @@ -5214,71 +4854,85 @@ engine.io@~6.4.1: cookie "~0.4.1" cors "~2.8.5" debug "~4.3.1" - engine.io-parser "~5.0.3" - ws "~8.11.0" + engine.io-parser "~5.2.1" + ws "~8.17.1" enhanced-resolve@^5.15.0: - version "5.15.0" - resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz#1af946c7d93603eb88e9896cee4904dc012e9c35" - integrity sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg== + version "5.17.1" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz#67bfbbcc2f81d511be77d686a90267ef7f898a15" + integrity sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg== dependencies: graceful-fs "^4.2.4" tapable "^2.2.0" enquirer@~2.3.6: version "2.3.6" - resolved "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz" + resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.6.tgz#2a7fe5dd634a1e4125a975ec994ff5456dc3734d" integrity sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg== dependencies: ansi-colors "^4.1.1" ent@~2.2.0: - version "2.2.0" - resolved "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz" - integrity sha512-GHrMyVZQWvTIdDtpiEXdHZnFQKzeO09apj8Cbl4pKWy4i0Oprcq17usfDt5aO63swf0JOeMWjWQE/LzgSRuWpA== + version "2.2.1" + resolved "https://registry.yarnpkg.com/ent/-/ent-2.2.1.tgz#68dc99a002f115792c26239baedaaea9e70c0ca2" + integrity sha512-QHuXVeZx9d+tIQAz/XztU0ZwZf2Agg9CcXcgE1rurqvdBeDBrpSwjl8/6XUqMg7tw2Y7uAdKb2sRv+bSEFqQ5A== + dependencies: + punycode "^1.4.1" entities@^4.2.0, entities@^4.3.0, entities@^4.4.0: version "4.5.0" - resolved "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz" + resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== env-paths@^2.2.0: version "2.2.1" - resolved "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz" + resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.1.tgz#420399d416ce1fbe9bc0a07c62fa68d67fd0f8f2" integrity sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A== err-code@^2.0.2: version "2.0.3" - resolved "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz" + resolved "https://registry.yarnpkg.com/err-code/-/err-code-2.0.3.tgz#23c2f3b756ffdfc608d30e27c9a941024807e7f9" integrity sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA== errno@^0.1.1: version "0.1.8" - resolved "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz" + resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.8.tgz#8bb3e9c7d463be4976ff888f76b4809ebc2e811f" integrity sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A== dependencies: prr "~1.0.1" error-ex@^1.3.1: version "1.3.2" - resolved "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz" + resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== dependencies: is-arrayish "^0.2.1" +es-define-property@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845" + integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ== + dependencies: + get-intrinsic "^1.2.4" + +es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== + es-module-lexer@^1.2.1: - version "1.2.1" - resolved "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.2.1.tgz" - integrity sha512-9978wrXM50Y4rTMmW5kXIC09ZdXQZqkE4mxhwkd8VbzsGkXGPgV4zWuqQJgCEzYngdo2dYDa0l8xhX4fkSwJSg== + version "1.5.4" + resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.5.4.tgz#a8efec3a3da991e60efa6b633a7cad6ab8d26b78" + integrity sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw== es6-promise@^4.0.3: version "4.2.8" - resolved "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz" + resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a" integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w== es6-promisify@^5.0.0: version "5.0.0" - resolved "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz" + resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-5.0.0.tgz#5109d62f3e56ea967c4b63505aef08291c8a5203" integrity sha512-C+d6UdsYDk0lMebHNR4S2NybQMMngAOnOwYBQjTOiv0MkoJMP0Myw2mgpDLBcpfCmRLxyFqYhS/CfOENq4SJhQ== dependencies: es6-promise "^4.0.3" @@ -5344,14 +4998,14 @@ esbuild@^0.18.10: "@esbuild/win32-ia32" "0.18.20" "@esbuild/win32-x64" "0.18.20" -escalade@^3.1.1: - version "3.1.1" - resolved "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz" - integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== +escalade@^3.1.1, escalade@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.2.tgz#54076e9ab29ea5bf3d8f1ed62acffbb88272df27" + integrity sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA== escape-html@~1.0.3: version "1.0.3" - resolved "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== escape-string-regexp@5.0.0: @@ -5361,12 +5015,12 @@ escape-string-regexp@5.0.0: escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: version "1.0.5" - resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== escape-string-regexp@^4.0.0: version "4.0.0" - resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== escodegen@^2.0.0: @@ -5382,7 +5036,7 @@ escodegen@^2.0.0: eslint-scope@5.1.1, eslint-scope@^5.1.1: version "5.1.1" - resolved "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== dependencies: esrecurse "^4.3.0" @@ -5390,7 +5044,7 @@ eslint-scope@5.1.1, eslint-scope@^5.1.1: eslint-scope@^7.0.0, eslint-scope@^7.2.2: version "7.2.2" - resolved "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.2.2.tgz#deb4f92563390f32006894af62a22dba1c46423f" integrity sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg== dependencies: esrecurse "^4.3.0" @@ -5402,17 +5056,18 @@ eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4 integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== eslint@^8.50.0: - version "8.50.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.50.0.tgz#2ae6015fee0240fcd3f83e1e25df0287f487d6b2" - integrity sha512-FOnOGSuFuFLv/Sa+FDVRZl4GGVAAFFi8LecRsI5a1tMO5HIE8nCm4ivAlzt4dT3ol/PaaGC0rJEEXQmHJBGoOg== + version "8.57.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.57.0.tgz#c786a6fd0e0b68941aaf624596fb987089195668" + integrity sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ== dependencies: "@eslint-community/eslint-utils" "^4.2.0" "@eslint-community/regexpp" "^4.6.1" - "@eslint/eslintrc" "^2.1.2" - "@eslint/js" "8.50.0" - "@humanwhocodes/config-array" "^0.11.11" + "@eslint/eslintrc" "^2.1.4" + "@eslint/js" "8.57.0" + "@humanwhocodes/config-array" "^0.11.14" "@humanwhocodes/module-importer" "^1.0.1" "@nodelib/fs.walk" "^1.2.8" + "@ungap/structured-clone" "^1.2.0" ajv "^6.12.4" chalk "^4.0.0" cross-spawn "^7.0.2" @@ -5446,7 +5101,7 @@ eslint@^8.50.0: espree@^9.6.0, espree@^9.6.1: version "9.6.1" - resolved "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz" + resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f" integrity sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ== dependencies: acorn "^8.9.0" @@ -5455,61 +5110,61 @@ espree@^9.6.0, espree@^9.6.1: esprima@^4.0.0, esprima@^4.0.1: version "4.0.1" - resolved "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== esquery@^1.4.2: - version "1.5.0" - resolved "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz" - integrity sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg== + version "1.6.0" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.6.0.tgz#91419234f804d852a82dceec3e16cdc22cf9dae7" + integrity sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg== dependencies: estraverse "^5.1.0" esrecurse@^4.3.0: version "4.3.0" - resolved "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== dependencies: estraverse "^5.2.0" estraverse@^4.1.1: version "4.3.0" - resolved "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== estraverse@^5.1.0, estraverse@^5.2.0: version "5.3.0" - resolved "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== esutils@^2.0.2: version "2.0.3" - resolved "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== etag@~1.8.1: version "1.8.1" - resolved "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== eventemitter-asyncresource@^1.0.0: version "1.0.0" - resolved "https://registry.npmjs.org/eventemitter-asyncresource/-/eventemitter-asyncresource-1.0.0.tgz" + resolved "https://registry.yarnpkg.com/eventemitter-asyncresource/-/eventemitter-asyncresource-1.0.0.tgz#734ff2e44bf448e627f7748f905d6bdd57bdb65b" integrity sha512-39F7TBIV0G7gTelxwbEqnwhp90eqCPON1k0NwNfwhgKn4Co4ybUbj2pECcXT0B3ztRKZ7Pw1JujUUgmQJHcVAQ== eventemitter3@^4.0.0: version "4.0.7" - resolved "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== events@^3.2.0: version "3.3.0" - resolved "https://registry.npmjs.org/events/-/events-3.3.0.tgz" + resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== execa@^5.0.0: version "5.1.1" - resolved "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz" + resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg== dependencies: cross-spawn "^7.0.3" @@ -5524,20 +5179,25 @@ execa@^5.0.0: exit@^0.1.2: version "0.1.2" - resolved "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz" + resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" integrity sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ== +exponential-backoff@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/exponential-backoff/-/exponential-backoff-3.1.1.tgz#64ac7526fe341ab18a39016cd22c787d01e00bf6" + integrity sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw== + express@^4.17.3: - version "4.18.2" - resolved "https://registry.npmjs.org/express/-/express-4.18.2.tgz" - integrity sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ== + version "4.19.2" + resolved "https://registry.yarnpkg.com/express/-/express-4.19.2.tgz#e25437827a3aa7f2a827bc8171bbbb664a356465" + integrity sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q== dependencies: accepts "~1.3.8" array-flatten "1.1.1" - body-parser "1.20.1" + body-parser "1.20.2" content-disposition "0.5.4" content-type "~1.0.4" - cookie "0.5.0" + cookie "0.6.0" cookie-signature "1.0.6" debug "2.6.9" depd "2.0.0" @@ -5566,12 +5226,12 @@ express@^4.17.3: extend@^3.0.0, extend@~3.0.2: version "3.0.2" - resolved "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== external-editor@^3.0.3: version "3.1.0" - resolved "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz" + resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.1.0.tgz#cb03f740befae03ea4d283caed2741a83f335495" integrity sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew== dependencies: chardet "^0.7.0" @@ -5580,22 +5240,22 @@ external-editor@^3.0.3: extsprintf@1.3.0: version "1.3.0" - resolved "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" integrity sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g== extsprintf@^1.2.0: version "1.4.1" - resolved "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.1.tgz#8d172c064867f235c0c84a596806d279bf4bcc07" integrity sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA== fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" - resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== fast-glob@3.2.7: version "3.2.7" - resolved "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.7.tgz" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.7.tgz#fd6cb7a2d7e9aa7a7846111e85a196d6b2f766a1" integrity sha512-rYGMRwip6lUMvYD3BTScMwT1HtAs2d71SMv66Vrxs0IekGZEjhM0pcMfjQPnknBt2zeCwQMEupiN02ZP4DiT1Q== dependencies: "@nodelib/fs.stat" "^2.0.2" @@ -5615,10 +5275,10 @@ fast-glob@3.3.1: merge2 "^1.3.0" micromatch "^4.0.4" -fast-glob@^3.2.11, fast-glob@^3.2.9: - version "3.2.12" - resolved "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz" - integrity sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w== +fast-glob@^3.2.11, fast-glob@^3.2.9, fast-glob@^3.3.0: + version "3.3.2" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129" + integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow== dependencies: "@nodelib/fs.stat" "^2.0.2" "@nodelib/fs.walk" "^1.2.3" @@ -5628,63 +5288,68 @@ fast-glob@^3.2.11, fast-glob@^3.2.9: fast-json-stable-stringify@^2.0.0: version "2.1.0" - resolved "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== fast-levenshtein@^2.0.6: version "2.0.6" - resolved "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz" + resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== +fast-uri@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.0.1.tgz#cddd2eecfc83a71c1be2cc2ef2061331be8a7134" + integrity sha512-MWipKbbYiYI0UC7cl8m/i/IWTqfC8YXsqjzybjddLsFjStroQzsHXkc73JutMvBiXmOvapk+axIl79ig5t55Bw== + fastparse@^1.1.2: version "1.1.2" - resolved "https://registry.npmjs.org/fastparse/-/fastparse-1.1.2.tgz" + resolved "https://registry.yarnpkg.com/fastparse/-/fastparse-1.1.2.tgz#91728c5a5942eced8531283c79441ee4122c35a9" integrity sha512-483XLLxTVIwWK3QTrMGRqUfUpoOs/0hbQrl2oz4J0pAcm3A3bu84wxTFqGqkJzewCLdME38xJLJAxBABfQT8sQ== fastq@^1.6.0: - version "1.15.0" - resolved "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz" - integrity sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw== + version "1.17.1" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.17.1.tgz#2a523f07a4e7b1e81a42b91b8bf2254107753b47" + integrity sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w== dependencies: reusify "^1.0.4" faye-websocket@^0.11.3: version "0.11.4" - resolved "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz" + resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.4.tgz#7f0d9275cfdd86a1c963dc8b65fcc451edcbb1da" integrity sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g== dependencies: websocket-driver ">=0.5.1" figures@3.2.0, figures@^3.0.0: version "3.2.0" - resolved "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz" + 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-entry-cache@^6.0.1: version "6.0.1" - resolved "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg== dependencies: flat-cache "^3.0.4" file-saver@^2.0.5: version "2.0.5" - resolved "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz" + resolved "https://registry.yarnpkg.com/file-saver/-/file-saver-2.0.5.tgz#d61cfe2ce059f414d899e9dd6d4107ee25670c38" integrity sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA== filelist@^1.0.4: version "1.0.4" - resolved "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz" + resolved "https://registry.yarnpkg.com/filelist/-/filelist-1.0.4.tgz#f78978a1e944775ff9e62e744424f215e58352b5" integrity sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q== dependencies: minimatch "^5.0.1" -fill-range@^7.0.1: - version "7.0.1" - resolved "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz" - integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== dependencies: to-regex-range "^5.0.1" @@ -5695,7 +5360,7 @@ filter-obj@^5.0.0: finalhandler@1.1.2: version "1.1.2" - resolved "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d" integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA== dependencies: debug "2.6.9" @@ -5708,7 +5373,7 @@ finalhandler@1.1.2: finalhandler@1.2.0: version "1.2.0" - resolved "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.2.0.tgz#7d23fe5731b207b4640e4fcd00aec1f9207a7b32" integrity sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg== dependencies: debug "2.6.9" @@ -5729,7 +5394,7 @@ find-cache-dir@^4.0.0: find-up@^4.1.0: version "4.1.0" - resolved "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== dependencies: locate-path "^5.0.0" @@ -5737,7 +5402,7 @@ find-up@^4.1.0: find-up@^5.0.0: version "5.0.0" - resolved "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz" + 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" @@ -5752,44 +5417,45 @@ find-up@^6.3.0: path-exists "^5.0.0" flag-icons@^7.1.0: - version "7.1.0" - resolved "https://registry.yarnpkg.com/flag-icons/-/flag-icons-7.1.0.tgz#6898ae3b3a57e5a363e12478c1ef384aa62d641f" - integrity sha512-AH4v++19bpC5P3Wh767top4wylJYJCWkFnvNiDqGHDxqSqdMZ49jpLXp8PWBHTTXaNQ+/A+QPrOwyiIGaiIhmw== + version "7.2.3" + resolved "https://registry.yarnpkg.com/flag-icons/-/flag-icons-7.2.3.tgz#b67f379fa0ef28c4e605319a78035131bdd8ced7" + integrity sha512-X2gUdteNuqdNqob2KKTJTS+ZCvyWeLCtDz9Ty8uJP17Y4o82Y+U/Vd4JNrdwTAjagYsRznOn9DZ+E/Q52qbmqg== flat-cache@^3.0.4: - version "3.0.4" - resolved "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz" - integrity sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg== + version "3.2.0" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.2.0.tgz#2c0c2d5040c99b1632771a9d105725c0115363ee" + integrity sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw== dependencies: - flatted "^3.1.0" + flatted "^3.2.9" + keyv "^4.5.3" rimraf "^3.0.2" flat@^5.0.2: version "5.0.2" - resolved "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz" + resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241" integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== -flatted@^3.1.0, flatted@^3.2.7: - version "3.2.7" - resolved "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz" - integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ== +flatted@^3.2.7, flatted@^3.2.9: + version "3.3.1" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.1.tgz#21db470729a6734d4997002f439cb308987f567a" + integrity sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw== -follow-redirects@^1.0.0, follow-redirects@^1.15.0: - version "1.15.2" - resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz" - integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== +follow-redirects@^1.0.0, follow-redirects@^1.15.6: + version "1.15.6" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b" + integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA== foreground-child@^3.1.0: - version "3.1.1" - resolved "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz" - integrity sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg== + version "3.2.1" + resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.2.1.tgz#767004ccf3a5b30df39bed90718bab43fe0a59f7" + integrity sha512-PXUUyLqrR2XCWICfv6ukppP96sdFwWbNEnfEMt7jNsISjMsvaLNinAHNDYyvkyU+SZG2BTSbT5NjG+vZslfGTA== dependencies: cross-spawn "^7.0.0" signal-exit "^4.0.1" forever-agent@~0.6.1: version "0.6.1" - resolved "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz" + resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" integrity sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw== form-data@^3.0.0: @@ -5803,7 +5469,7 @@ form-data@^3.0.0: form-data@^4.0.0: version "4.0.0" - resolved "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== dependencies: asynckit "^0.4.0" @@ -5812,7 +5478,7 @@ form-data@^4.0.0: form-data@~2.3.2: version "2.3.3" - resolved "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ== dependencies: asynckit "^0.4.0" @@ -5821,28 +5487,28 @@ form-data@~2.3.2: forwarded@0.2.0: version "0.2.0" - resolved "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== fraction.js@^4.2.0: - version "4.2.0" - resolved "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz" - integrity sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA== + version "4.3.7" + resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.3.7.tgz#06ca0085157e42fda7f9e726e79fefc4068840f7" + integrity sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew== fresh@0.5.2: version "0.5.2" - resolved "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== fs-constants@^1.0.0: version "1.0.0" - resolved "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz" + resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== fs-extra@^11.1.0: - version "11.1.1" - resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.1.tgz" - integrity sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ== + version "11.2.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.2.0.tgz#e70e17dfad64232287d01929399e0ea7c86b0e5b" + integrity sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw== dependencies: graceful-fs "^4.2.0" jsonfile "^6.0.1" @@ -5850,7 +5516,7 @@ fs-extra@^11.1.0: fs-extra@^8.1.0: version "8.1.0" - resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0" integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g== dependencies: graceful-fs "^4.2.0" @@ -5859,37 +5525,32 @@ fs-extra@^8.1.0: fs-minipass@^2.0.0, fs-minipass@^2.1.0: version "2.1.0" - resolved "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz" + resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb" integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg== dependencies: minipass "^3.0.0" fs-minipass@^3.0.0: - version "3.0.2" - resolved "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.2.tgz" - integrity sha512-2GAfyfoaCDRrM6jaOS3UsBts8yJ55VioXdWcOL7dK9zdAuKT71+WBA4ifnNYqVjYv+4SsPxjK0JT4yIIn4cA/g== + version "3.0.3" + resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-3.0.3.tgz#79a85981c4dc120065e96f62086bf6f9dc26cc54" + integrity sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw== dependencies: - minipass "^5.0.0" + minipass "^7.0.3" -fs-monkey@^1.0.3: - version "1.0.3" - resolved "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.3.tgz" - integrity sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q== +fs-monkey@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/fs-monkey/-/fs-monkey-1.0.6.tgz#8ead082953e88d992cf3ff844faa907b26756da2" + integrity sha512-b1FMfwetIKymC0eioW7mTywihSQE4oLzQn1dB6rZB5fx/3NpNEdAWeCSMB+60/AeT0TCXsxzAlcYVEFCTAksWg== fs.realpath@^1.0.0: version "1.0.0" - resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== fsevents@~2.3.2: - version "2.3.2" - resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz" - integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== - -function-bind@^1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz" - integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== function-bind@^1.1.2: version "1.1.2" @@ -5898,7 +5559,7 @@ function-bind@^1.1.2: gauge@^4.0.3: version "4.0.4" - resolved "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz" + resolved "https://registry.yarnpkg.com/gauge/-/gauge-4.0.4.tgz#52ff0652f2bbf607a989793d53b751bef2328dce" integrity sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg== dependencies: aproba "^1.0.3 || ^2.0.0" @@ -5912,63 +5573,64 @@ gauge@^4.0.3: gensync@^1.0.0-beta.2: version "1.0.0-beta.2" - resolved "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz" + resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== get-caller-file@^2.0.1, get-caller-file@^2.0.5: version "2.0.5" - resolved "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== -get-intrinsic@^1.0.2: - version "1.2.1" - resolved "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz" - integrity sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw== +get-intrinsic@^1.1.3, get-intrinsic@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd" + integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== dependencies: - function-bind "^1.1.1" - has "^1.0.3" + es-errors "^1.3.0" + function-bind "^1.1.2" has-proto "^1.0.1" has-symbols "^1.0.3" + hasown "^2.0.0" get-package-type@^0.1.0: version "0.1.0" - resolved "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz" + resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q== get-stream@^6.0.0: version "6.0.1" - resolved "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== getpass@^0.1.1: version "0.1.7" - resolved "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz" + resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" integrity sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng== dependencies: assert-plus "^1.0.0" glob-parent@^5.1.2, glob-parent@~5.1.2: version "5.1.2" - resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== dependencies: is-glob "^4.0.1" glob-parent@^6.0.1, glob-parent@^6.0.2: version "6.0.2" - resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== dependencies: is-glob "^4.0.3" glob-to-regexp@^0.4.1: version "0.4.1" - resolved "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz" + resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== glob@7.1.4: version "7.1.4" - resolved "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.4.tgz#aa608a2f6c577ad357e1ae5a5c26d9a8d1969255" integrity sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A== dependencies: fs.realpath "^1.0.0" @@ -5979,19 +5641,20 @@ glob@7.1.4: path-is-absolute "^1.0.0" glob@^10.2.2: - version "10.2.4" - resolved "https://registry.npmjs.org/glob/-/glob-10.2.4.tgz" - integrity sha512-fDboBse/sl1oXSLhIp0FcCJgzW9KmhC/q8ULTKC82zc+DL3TL7FNb8qlt5qqXN53MsKEUSIcb+7DLmEygOE5Yw== + version "10.4.5" + resolved "https://registry.yarnpkg.com/glob/-/glob-10.4.5.tgz#f4d9f0b90ffdbab09c9d77f5f29b4262517b0956" + integrity sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg== dependencies: foreground-child "^3.1.0" - jackspeak "^2.0.3" - minimatch "^9.0.0" - minipass "^5.0.0 || ^6.0.0" - path-scurry "^1.7.0" + jackspeak "^3.1.2" + minimatch "^9.0.4" + minipass "^7.1.2" + package-json-from-dist "^1.0.0" + path-scurry "^1.11.1" glob@^7.0.3, glob@^7.0.6, glob@^7.1.3, glob@^7.1.4, glob@^7.1.7: version "7.2.3" - resolved "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== dependencies: fs.realpath "^1.0.0" @@ -6003,7 +5666,7 @@ glob@^7.0.3, glob@^7.0.6, glob@^7.1.3, glob@^7.1.4, glob@^7.1.7: glob@^8.0.1: version "8.1.0" - resolved "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz" + resolved "https://registry.yarnpkg.com/glob/-/glob-8.1.0.tgz#d388f656593ef708ee3e34640fdfb99a9fd1c33e" integrity sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ== dependencies: fs.realpath "^1.0.0" @@ -6014,19 +5677,19 @@ glob@^8.0.1: globals@^11.1.0: version "11.12.0" - resolved "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz" + resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== globals@^13.19.0: - version "13.20.0" - resolved "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz" - integrity sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ== + version "13.24.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-13.24.0.tgz#8432a19d78ce0c1e833949c36adb345400bb1171" + integrity sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ== dependencies: type-fest "^0.20.2" globby@^11.1.0: version "11.1.0" - resolved "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz" + resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== dependencies: array-union "^2.1.0" @@ -6037,19 +5700,19 @@ globby@^11.1.0: slash "^3.0.0" globby@^13.1.1: - version "13.1.4" - resolved "https://registry.npmjs.org/globby/-/globby-13.1.4.tgz" - integrity sha512-iui/IiiW+QrJ1X1hKH5qwlMQyv34wJAYwH1vrf8b9kBA4sNiif3gKsMHa+BrdnOpEudWjpotfa7LrTzB1ERS/g== + version "13.2.2" + resolved "https://registry.yarnpkg.com/globby/-/globby-13.2.2.tgz#63b90b1bf68619c2135475cbd4e71e66aa090592" + integrity sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w== dependencies: dir-glob "^3.0.1" - fast-glob "^3.2.11" - ignore "^5.2.0" + fast-glob "^3.3.0" + ignore "^5.2.4" merge2 "^1.4.1" slash "^4.0.0" globby@^5.0.0: version "5.0.0" - resolved "https://registry.npmjs.org/globby/-/globby-5.0.0.tgz" + resolved "https://registry.yarnpkg.com/globby/-/globby-5.0.0.tgz#ebd84667ca0dbb330b99bcfc68eac2bc54370e0d" integrity sha512-HJRTIH2EeH44ka+LWig+EqT2ONSYpVlNfx6pyd592/VF1TbfljJ7elwie7oSwcViLGqOdWocSdu2txwBF9bjmQ== dependencies: array-union "^1.0.1" @@ -6060,32 +5723,39 @@ globby@^5.0.0: pinkie-promise "^2.0.0" google-proto-files@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/google-proto-files/-/google-proto-files-4.0.0.tgz#46de40ce994a4adf98094c9261cb36a851306aaf" - integrity sha512-SMzJqJkuwJ50ON15+UBF6KQWHNqnJZzlDGPq5Z6xgAFiQTlfXFVBiyFjz5jd4RHWWAsyEE6KGUMy3txul5dUsA== + version "4.2.0" + resolved "https://registry.yarnpkg.com/google-proto-files/-/google-proto-files-4.2.0.tgz#130a6caa307b02541cbc6005234e3fe156768027" + integrity sha512-Yl3ZtTSpkOLjHTqHn91NhDp2jMPzpHWowSGz3S30N6gkqOXrJwUu44alR9dX+NyHK3n165uR+jezOH365b1pPA== dependencies: protobufjs "^7.0.0" walkdir "^0.4.0" google-protobuf@^3.21.2: - version "3.21.2" - resolved "https://registry.npmjs.org/google-protobuf/-/google-protobuf-3.21.2.tgz" - integrity sha512-3MSOYFO5U9mPGikIYCzK0SaThypfGgS6bHqrUGXG3DPHCrb+txNqeEcns1W0lkGfk0rCyNXm7xB9rMxnCiZOoA== + version "3.21.4" + resolved "https://registry.yarnpkg.com/google-protobuf/-/google-protobuf-3.21.4.tgz#2f933e8b6e5e9f8edde66b7be0024b68f77da6c9" + integrity sha512-MnG7N936zcKTco4Jd2PX2U96Kf9PxygAPKBug+74LHzmHXmceN16MmRcdgZv+DGef/S9YvQAfRsNCn4cjf9yyQ== + +gopd@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" + integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== + dependencies: + get-intrinsic "^1.1.3" graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4, graceful-fs@^4.2.6, graceful-fs@^4.2.9: version "4.2.11" - resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== graphemer@^1.4.0: version "1.4.0" - resolved "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz" + resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== grpc-web@^1.4.1: - version "1.4.2" - resolved "https://registry.npmjs.org/grpc-web/-/grpc-web-1.4.2.tgz" - integrity sha512-gUxWq42l5ldaRplcKb4Pw5O4XBONWZgz3vxIIXnfIeJj8Jc3wYiq2O4c9xzx/NGbbPEej4rhI62C9eTENwLGNw== + version "1.5.0" + resolved "https://registry.yarnpkg.com/grpc-web/-/grpc-web-1.5.0.tgz#154e4007ab59a94bf7726b87ef6c5bd8815ecf6e" + integrity sha512-y1tS3BBIoiVSzKTDF3Hm7E8hV2n7YY7pO0Uo7depfWJqKzWE+SKr0jvHNIJsJJYILQlpYShpi/DRJJMbosgDMQ== guess-parser@0.4.22: version "0.4.22" @@ -6096,17 +5766,17 @@ guess-parser@0.4.22: handle-thing@^2.0.0: version "2.0.1" - resolved "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz" + resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.1.tgz#857f79ce359580c340d43081cc648970d0bb234e" integrity sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg== har-schema@^2.0.0: version "2.0.0" - resolved "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz" + resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" integrity sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q== har-validator@~5.1.3: version "5.1.5" - resolved "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz" + resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.5.tgz#1f0803b9f8cb20c0fa13822df1ecddb36bde1efd" integrity sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w== dependencies: ajv "^6.12.3" @@ -6114,53 +5784,53 @@ har-validator@~5.1.3: has-ansi@^2.0.0: version "2.0.0" - resolved "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz" + resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" integrity sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg== dependencies: ansi-regex "^2.0.0" has-flag@^3.0.0: version "3.0.0" - resolved "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== has-flag@^4.0.0: version "4.0.0" - resolved "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== +has-property-descriptors@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" + integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== + dependencies: + es-define-property "^1.0.0" + has-proto@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz" - integrity sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg== + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.3.tgz#b31ddfe9b0e6e9914536a6ab286426d0214f77fd" + integrity sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q== has-symbols@^1.0.3: version "1.0.3" - resolved "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== has-unicode@^2.0.1: version "2.0.1" - resolved "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz" + resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" integrity sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ== -has@^1.0.3: - version "1.0.3" - resolved "https://registry.npmjs.org/has/-/has-1.0.3.tgz" - integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== - dependencies: - function-bind "^1.1.1" - -hasown@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.1.tgz#26f48f039de2c0f8d3356c223fb8d50253519faa" - integrity sha512-1/th4MHjnwncwXsIW6QMzlvYL9kG5e/CpVvLRZe4XPa8TOUNbCELqmvhDmnkNsAjwaG4+I8gJJL0JBvTTLO9qA== +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== dependencies: function-bind "^1.1.2" hdr-histogram-js@^2.0.1: version "2.0.3" - resolved "https://registry.npmjs.org/hdr-histogram-js/-/hdr-histogram-js-2.0.3.tgz" + resolved "https://registry.yarnpkg.com/hdr-histogram-js/-/hdr-histogram-js-2.0.3.tgz#0b860534655722b6e3f3e7dca7b78867cf43dcb5" integrity sha512-Hkn78wwzWHNCp2uarhzQ2SGFLU3JY8SBDDd3TAABK4fc30wm+MuPOrg5QVFVfkKOQd6Bfz3ukJEI+q9sXEkK1g== dependencies: "@assemblyscript/loader" "^0.10.1" @@ -6169,7 +5839,7 @@ hdr-histogram-js@^2.0.1: hdr-histogram-percentiles-obj@^3.0.0: version "3.0.0" - resolved "https://registry.npmjs.org/hdr-histogram-percentiles-obj/-/hdr-histogram-percentiles-obj-3.0.0.tgz" + resolved "https://registry.yarnpkg.com/hdr-histogram-percentiles-obj/-/hdr-histogram-percentiles-obj-3.0.0.tgz#9409f4de0c2dda78e61de2d9d78b1e9f3cba283c" integrity sha512-7kIufnBqdsBGcSZLPJwqHT3yhk1QTsSlFsVD3kx5ixH/AlgBs9yM1q6DPhXZ8f8gtdqgh7N7/5btRLpQsS2gHw== hosted-git-info@^4.0.1: @@ -6181,14 +5851,14 @@ hosted-git-info@^4.0.1: hosted-git-info@^6.0.0: version "6.1.1" - resolved "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-6.1.1.tgz" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-6.1.1.tgz#629442c7889a69c05de604d52996b74fe6f26d58" integrity sha512-r0EI+HBMcXadMrugk0GCQ+6BQV39PiWAZVfq7oIckeGiN7sjRGyQxPdft3nQekFTCQbYxLBH+/axZMeH8UX6+w== dependencies: lru-cache "^7.5.1" hpack.js@^2.1.6: version "2.1.6" - resolved "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz" + resolved "https://registry.yarnpkg.com/hpack.js/-/hpack.js-2.1.6.tgz#87774c0949e513f42e84575b3c45681fade2a0b2" integrity sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ== dependencies: inherits "^2.0.1" @@ -6204,18 +5874,18 @@ html-encoding-sniffer@^2.0.1: whatwg-encoding "^1.0.5" html-entities@^2.3.2: - version "2.3.3" - resolved "https://registry.npmjs.org/html-entities/-/html-entities-2.3.3.tgz" - integrity sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA== + version "2.5.2" + resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-2.5.2.tgz#201a3cf95d3a15be7099521620d19dfb4f65359f" + integrity sha512-K//PSRMQk4FZ78Kyau+mZurHn3FH0Vwr+H36eE0rPbeYkRRi9YxceYPhuN60UwWorxyKHhqoAJl2OFKa4BVtaA== html-escaper@^2.0.0: version "2.0.2" - resolved "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz" + resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== htmlparser2@^8.0.2: version "8.0.2" - resolved "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz" + resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-8.0.2.tgz#f002151705b383e62433b5cf466f5b716edaec21" integrity sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA== dependencies: domelementtype "^2.3.0" @@ -6225,17 +5895,17 @@ htmlparser2@^8.0.2: http-cache-semantics@^4.1.0, http-cache-semantics@^4.1.1: version "4.1.1" - resolved "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz" + resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz#abe02fcb2985460bf0323be664436ec3476a6d5a" integrity sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ== http-deceiver@^1.2.7: version "1.2.7" - resolved "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz" + resolved "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87" integrity sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw== http-errors@2.0.0: version "2.0.0" - resolved "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== dependencies: depd "2.0.0" @@ -6246,7 +5916,7 @@ http-errors@2.0.0: http-errors@~1.6.2: version "1.6.3" - resolved "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d" integrity sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A== dependencies: depd "~1.1.2" @@ -6256,7 +5926,7 @@ http-errors@~1.6.2: http-parser-js@>=0.5.1: version "0.5.8" - resolved "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.8.tgz" + resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.8.tgz#af23090d9ac4e24573de6f6aecc9d84a48bf20e3" integrity sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q== http-proxy-agent@^4.0.1: @@ -6270,7 +5940,7 @@ http-proxy-agent@^4.0.1: http-proxy-agent@^5.0.0: version "5.0.0" - resolved "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz" + resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz#5129800203520d434f142bc78ff3c170800f2b43" integrity sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w== dependencies: "@tootallnate/once" "2" @@ -6279,7 +5949,7 @@ http-proxy-agent@^5.0.0: http-proxy-middleware@^2.0.3: version "2.0.6" - resolved "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz" + resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz#e1a4dd6979572c7ab5a4e4b55095d1f32a74963f" integrity sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw== dependencies: "@types/http-proxy" "^1.17.8" @@ -6290,7 +5960,7 @@ http-proxy-middleware@^2.0.3: http-proxy@^1.18.1: version "1.18.1" - resolved "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz" + resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549" integrity sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ== dependencies: eventemitter3 "^4.0.0" @@ -6299,7 +5969,7 @@ http-proxy@^1.18.1: http-signature@~1.2.0: version "1.2.0" - resolved "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz" + resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" integrity sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ== dependencies: assert-plus "^1.0.0" @@ -6308,7 +5978,7 @@ http-signature@~1.2.0: https-proxy-agent@5.0.1, https-proxy-agent@^5.0.0: version "5.0.1" - resolved "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== dependencies: agent-base "6" @@ -6316,7 +5986,7 @@ https-proxy-agent@5.0.1, https-proxy-agent@^5.0.0: https-proxy-agent@^2.2.1: version "2.2.4" - resolved "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz#4ee7a737abd92678a293d9b34a1af4d0d08c787b" integrity sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg== dependencies: agent-base "^4.3.0" @@ -6324,77 +5994,82 @@ https-proxy-agent@^2.2.1: human-signals@^2.1.0: version "2.1.0" - resolved "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz" + resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== humanize-ms@^1.2.1: version "1.2.1" - resolved "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz" + resolved "https://registry.yarnpkg.com/humanize-ms/-/humanize-ms-1.2.1.tgz#c46e3159a293f6b896da29316d8b6fe8bb79bbed" integrity sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ== dependencies: ms "^2.0.0" i18n-iso-countries@^7.7.0: - version "7.7.0" - resolved "https://registry.yarnpkg.com/i18n-iso-countries/-/i18n-iso-countries-7.7.0.tgz#33a0974d6ffbe864fb853325de3afb4717c54b52" - integrity sha512-07zMatrSsR1Z+cnxW//7s14Xf4v5g6U6ORHPaH8+Ox4uPqV+y46Uq78veYV8H1DKTr76EfdjSeaTxHpnaYq+bw== + version "7.11.3" + resolved "https://registry.yarnpkg.com/i18n-iso-countries/-/i18n-iso-countries-7.11.3.tgz#46cf3e067fbf8bc8dcf291ad33fdeb2852a74913" + integrity sha512-yxQVzNvxEaspSqNnCbqLvwTZNXXkGydWcSxytJYZYb0KH5pn13fdywuX0vFxmOg57Z8ff416AuKDx6Oqnx+j9w== dependencies: diacritics "1.3.0" iconv-lite@0.4.24, iconv-lite@^0.4.24: version "0.4.24" - resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== dependencies: safer-buffer ">= 2.1.2 < 3" iconv-lite@^0.6.2, iconv-lite@^0.6.3: version "0.6.3" - resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== dependencies: safer-buffer ">= 2.1.2 < 3.0.0" icss-utils@^5.0.0, icss-utils@^5.1.0: version "5.1.0" - resolved "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz" + resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-5.1.0.tgz#c6be6858abd013d768e98366ae47e25d5887b1ae" integrity sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA== ieee754@^1.1.13, ieee754@^1.2.1: version "1.2.1" - resolved "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== ignore-walk@^6.0.0: - version "6.0.3" - resolved "https://registry.npmjs.org/ignore-walk/-/ignore-walk-6.0.3.tgz" - integrity sha512-C7FfFoTA+bI10qfeydT8aZbvr91vAEU+2W5BZUlzPec47oNb07SsOfwYrtxuvOYdUApPP/Qlh4DtAO51Ekk2QA== + version "6.0.5" + resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-6.0.5.tgz#ef8d61eab7da169078723d1f82833b36e200b0dd" + integrity sha512-VuuG0wCnjhnylG1ABXT3dAuIpTNDs/G8jlpmwXY03fXoXy/8ZK8/T+hMzt8L4WnrLCJgdybqgPagnF/f97cg3A== dependencies: minimatch "^9.0.0" -ignore@5.2.4, ignore@^5.0.4, ignore@^5.2.0: +ignore@5.2.4: version "5.2.4" - resolved "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324" integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ== +ignore@^5.0.4, ignore@^5.2.0, ignore@^5.2.4: + version "5.3.1" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.1.tgz#5073e554cd42c5b33b394375f538b8593e34d4ef" + integrity sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw== + image-size@~0.5.0: version "0.5.5" - resolved "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz" + resolved "https://registry.yarnpkg.com/image-size/-/image-size-0.5.5.tgz#09dfd4ab9d20e29eb1c3e80b8990378df9e3cb9c" integrity sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ== immediate@~3.0.5: version "3.0.6" - resolved "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz" + resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" integrity sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ== immutable@^4.0.0: - version "4.3.0" - resolved "https://registry.npmjs.org/immutable/-/immutable-4.3.0.tgz" - integrity sha512-0AOCmOip+xgJwEVTQj1EfiDDOkPmuyllDuTuEX+DDXUgapLAsBIfkg3sxCYyCEA8mQqZrrxPUGjcOQ2JS3WLkg== + version "4.3.7" + resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.7.tgz#c70145fc90d89fb02021e65c84eb0226e4e5a381" + integrity sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw== -import-fresh@^3.2.1: +import-fresh@^3.2.1, import-fresh@^3.3.0: version "3.3.0" - resolved "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== dependencies: parent-module "^1.0.0" @@ -6402,12 +6077,12 @@ import-fresh@^3.2.1: imurmurhash@^0.1.4: version "0.1.4" - resolved "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== indent-string@^4.0.0: version "4.0.0" - resolved "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== indent-string@^5.0.0: @@ -6417,12 +6092,12 @@ indent-string@^5.0.0: infer-owner@^1.0.4: version "1.0.4" - resolved "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz" + resolved "https://registry.yarnpkg.com/infer-owner/-/infer-owner-1.0.4.tgz#c4cefcaa8e51051c2a40ba2ce8a3d27295af9467" integrity sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A== inflight@^1.0.4: version "1.0.6" - resolved "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== dependencies: once "^1.3.0" @@ -6430,27 +6105,27 @@ inflight@^1.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.npmjs.org/inherits/-/inherits-2.0.4.tgz" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== inherits@2.0.3: version "2.0.3" - resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" integrity sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw== ini@4.1.1: version "4.1.1" - resolved "https://registry.npmjs.org/ini/-/ini-4.1.1.tgz" + resolved "https://registry.yarnpkg.com/ini/-/ini-4.1.1.tgz#d95b3d843b1e906e56d6747d5447904ff50ce7a1" integrity sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g== ini@^1.3.4: version "1.3.8" - resolved "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== inquirer@8.2.4: version "8.2.4" - resolved "https://registry.npmjs.org/inquirer/-/inquirer-8.2.4.tgz" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-8.2.4.tgz#ddbfe86ca2f67649a67daa6f1051c128f684f0b4" integrity sha512-nn4F01dxU8VeKfq192IjLsxu0/OmMZ4Lg3xKAns148rCaXP6ntAoEkVYZThWjwON8AlzdZZi6oqnhNbxUG9hVg== dependencies: ansi-escapes "^4.2.1" @@ -6469,111 +6144,107 @@ inquirer@8.2.4: through "^2.3.6" wrap-ansi "^7.0.0" -ip@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz" - integrity sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ== +ip-address@^9.0.5: + version "9.0.5" + resolved "https://registry.yarnpkg.com/ip-address/-/ip-address-9.0.5.tgz#117a960819b08780c3bd1f14ef3c1cc1d3f3ea5a" + integrity sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g== + dependencies: + jsbn "1.1.0" + sprintf-js "^1.1.3" ipaddr.js@1.9.1: version "1.9.1" - resolved "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== ipaddr.js@^2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.0.1.tgz" - integrity sha512-1qTgH9NG+IIJ4yfKs2e6Pp1bZg8wbDbKHT21HrLIeYBTRLgMYKnMTPAuI3Lcs61nfx5h1xlXnbJtH1kX5/d/ng== + version "2.2.0" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-2.2.0.tgz#d33fa7bac284f4de7af949638c9d68157c6b92e8" + integrity sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA== is-arrayish@^0.2.1: version "0.2.1" - resolved "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== is-binary-path@~2.1.0: version "2.1.0" - resolved "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== dependencies: binary-extensions "^2.0.0" -is-core-module@^2.11.0, is-core-module@^2.8.1: - version "2.12.0" - resolved "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.0.tgz" - integrity sha512-RECHCBCd/viahWmwj6enj19sKbHfJrddi/6cBDsNTKbNq0f7VeaUkBo60BqzvPqo/W54ChS62Z5qyun7cfOMqQ== +is-core-module@^2.11.0, is-core-module@^2.13.0, is-core-module@^2.5.0, is-core-module@^2.8.1: + version "2.15.0" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.15.0.tgz#71c72ec5442ace7e76b306e9d48db361f22699ea" + integrity sha512-Dd+Lb2/zvk9SKy1TGCt1wFJFo/MWBPMX5x7KcvLajWTGuomczdQX61PvY5yK6SVACwpoexWo81IfFyoKY2QnTA== dependencies: - has "^1.0.3" - -is-core-module@^2.5.0: - version "2.13.1" - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.13.1.tgz#ad0d7532c6fea9da1ebdc82742d74525c6273384" - integrity sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw== - dependencies: - hasown "^2.0.0" + hasown "^2.0.2" is-docker@^2.0.0, is-docker@^2.1.1: version "2.2.1" - resolved "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz" + resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa" integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ== is-extglob@^2.1.1: version "2.1.1" - resolved "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== is-fullwidth-code-point@^3.0.0: version "3.0.0" - resolved "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz" + 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-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: version "4.0.3" - resolved "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== dependencies: is-extglob "^2.1.1" is-interactive@^1.0.0: version "1.0.0" - resolved "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz" + resolved "https://registry.yarnpkg.com/is-interactive/-/is-interactive-1.0.0.tgz#cea6e6ae5c870a7b0a0004070b7b587e0252912e" integrity sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w== is-lambda@^1.0.1: version "1.0.1" - resolved "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz" + resolved "https://registry.yarnpkg.com/is-lambda/-/is-lambda-1.0.1.tgz#3d9877899e6a53efc0160504cde15f82e6f061d5" integrity sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ== is-number@^7.0.0: version "7.0.0" - resolved "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== is-path-cwd@^1.0.0: version "1.0.0" - resolved "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz" + resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-1.0.0.tgz#d225ec23132e89edd38fda767472e62e65f1106d" integrity sha512-cnS56eR9SPAscL77ik76ATVqoPARTqPIVkMDVxRaWH06zT+6+CzIroYRJ0VVvm0Z1zfAvxvz9i/D3Ppjaqt5Nw== is-path-in-cwd@^1.0.0: version "1.0.1" - resolved "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-1.0.1.tgz" + resolved "https://registry.yarnpkg.com/is-path-in-cwd/-/is-path-in-cwd-1.0.1.tgz#5ac48b345ef675339bd6c7a48a912110b241cf52" integrity sha512-FjV1RTW48E7CWM7eE/J2NJvAEEVektecDBVBE5Hh3nM1Jd0kvhHtX68Pr3xsDf857xt3Y4AkwVULK1Vku62aaQ== dependencies: is-path-inside "^1.0.0" is-path-inside@^1.0.0: version "1.0.1" - resolved "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.1.tgz" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-1.0.1.tgz#8ef5b7de50437a3fdca6b4e865ef7aa55cb48036" integrity sha512-qhsCR/Esx4U4hg/9I19OVUAJkGWtjRYHMRgUMZE2TDdj+Ag+kttZanLupfddNyglzz50cUlmWzUaI37GDfNx/g== dependencies: path-is-inside "^1.0.1" is-path-inside@^3.0.3: version "3.0.3" - resolved "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== is-plain-obj@^3.0.0: version "3.0.0" - resolved "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-3.0.0.tgz#af6f2ea14ac5a646183a5bbdb5baabbc156ad9d7" integrity sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA== is-plain-obj@^4.0.0: @@ -6583,7 +6254,7 @@ is-plain-obj@^4.0.0: is-plain-object@^2.0.4: version "2.0.4" - resolved "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz" + resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== dependencies: isobject "^3.0.1" @@ -6595,69 +6266,69 @@ is-potential-custom-element-name@^1.0.1: is-stream@^2.0.0: version "2.0.1" - resolved "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== is-typedarray@~1.0.0: version "1.0.0" - resolved "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz" + resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" integrity sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA== is-unicode-supported@^0.1.0: version "0.1.0" - resolved "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz" + resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== is-what@^3.14.1: version "3.14.1" - resolved "https://registry.npmjs.org/is-what/-/is-what-3.14.1.tgz" + resolved "https://registry.yarnpkg.com/is-what/-/is-what-3.14.1.tgz#e1222f46ddda85dead0fd1c9df131760e77755c1" integrity sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA== is-wsl@^2.2.0: version "2.2.0" - resolved "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz" + resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271" integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== dependencies: is-docker "^2.0.0" isarray@~1.0.0: version "1.0.0" - resolved "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== isbinaryfile@^4.0.8: version "4.0.10" - resolved "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz" + resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-4.0.10.tgz#0c5b5e30c2557a2f06febd37b7322946aaee42b3" integrity sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw== isexe@^2.0.0: version "2.0.0" - resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== isobject@^3.0.1: version "3.0.1" - resolved "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" integrity sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg== isstream@~0.1.2: version "0.1.2" - resolved "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz" + resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" integrity sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g== istanbul-lib-coverage@^2.0.5: version "2.0.5" - resolved "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz" + resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz#675f0ab69503fad4b1d849f736baaca803344f49" integrity sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA== istanbul-lib-coverage@^3.0.0, istanbul-lib-coverage@^3.2.0: - version "3.2.0" - resolved "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz" - integrity sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw== + version "3.2.2" + resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz#2d166c4b0644d43a39f04bf6c2edd1e585f31756" + integrity sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg== istanbul-lib-instrument@^5.0.4: version "5.2.1" - resolved "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz" + resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz#d10c8885c2125574e1c231cacadf955675e1ce3d" integrity sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg== dependencies: "@babel/core" "^7.12.3" @@ -6667,17 +6338,17 @@ istanbul-lib-instrument@^5.0.4: semver "^6.3.0" istanbul-lib-report@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz" - integrity sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw== + version "3.0.1" + resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz#908305bac9a5bd175ac6a74489eafd0fc2445a7d" + integrity sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw== dependencies: istanbul-lib-coverage "^3.0.0" - make-dir "^3.0.0" + make-dir "^4.0.0" supports-color "^7.1.0" istanbul-lib-source-maps@^3.0.6: version "3.0.6" - resolved "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-3.0.6.tgz" + resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-3.0.6.tgz#284997c48211752ec486253da97e3879defba8c8" integrity sha512-R47KzMtDJH6X4/YW9XTx+jrLnZnscW4VpNN+1PViSYTejLVPWv7oov+Duf8YQSPyVRUvueQqz1TcsC6mooZTXw== dependencies: debug "^4.1.1" @@ -6687,52 +6358,57 @@ istanbul-lib-source-maps@^3.0.6: source-map "^0.6.1" istanbul-reports@^3.0.2: - version "3.1.5" - resolved "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.5.tgz" - integrity sha512-nUsEMa9pBt/NOHqbcbeJEgqIlY/K7rVWUX6Lql2orY5e9roQOthbR3vtY4zzf2orPELg80fnxxk9zUyPlgwD1w== + version "3.1.7" + resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.1.7.tgz#daed12b9e1dca518e15c056e1e537e741280fa0b" + integrity sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g== dependencies: html-escaper "^2.0.0" istanbul-lib-report "^3.0.0" -jackspeak@^2.0.3: - version "2.2.0" - resolved "https://registry.npmjs.org/jackspeak/-/jackspeak-2.2.0.tgz" - integrity sha512-r5XBrqIJfwRIjRt/Xr5fv9Wh09qyhHfKnYddDlpM+ibRR20qrYActpCAgU6U+d53EOEjzkvxPMVHSlgR7leXrQ== +jackspeak@^3.1.2: + version "3.4.3" + resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-3.4.3.tgz#8833a9d89ab4acde6188942bd1c53b6390ed5a8a" + integrity sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw== dependencies: "@isaacs/cliui" "^8.0.2" optionalDependencies: "@pkgjs/parseargs" "^0.11.0" jake@^10.8.5: - version "10.8.7" - resolved "https://registry.npmjs.org/jake/-/jake-10.8.7.tgz" - integrity sha512-ZDi3aP+fG/LchyBzUM804VjddnwfSfsdeYkwt8NcbKRvo4rFkjhs456iLFn3k2ZUWvNe4i48WACDbza8fhq2+w== + version "10.9.2" + resolved "https://registry.yarnpkg.com/jake/-/jake-10.9.2.tgz#6ae487e6a69afec3a5e167628996b59f35ae2b7f" + integrity sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA== dependencies: async "^3.2.3" chalk "^4.0.2" filelist "^1.0.4" minimatch "^3.1.2" -jasmine-core@^4.1.0, jasmine-core@~4.6.0: - version "4.6.0" - resolved "https://registry.npmjs.org/jasmine-core/-/jasmine-core-4.6.0.tgz" - integrity sha512-O236+gd0ZXS8YAjFx8xKaJ94/erqUliEkJTDedyE7iHvv4ZVqi+q+8acJxu05/WJDKm512EUNn809In37nWlAQ== +jasmine-core@^4.1.0: + version "4.6.1" + resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-4.6.1.tgz#5ebb8afa07282078f8d7b15871737a83b74e58f2" + integrity sha512-VYz/BjjmC3klLJlLwA4Kw8ytk0zDSmbbDLNs794VnWmkcCB7I9aAL/D48VNQtmITyPvea2C3jdUMfc3kAoy0PQ== jasmine-core@~2.8.0: version "2.8.0" - resolved "https://registry.npmjs.org/jasmine-core/-/jasmine-core-2.8.0.tgz" + resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-2.8.0.tgz#bcc979ae1f9fd05701e45e52e65d3a5d63f1a24e" integrity sha512-SNkOkS+/jMZvLhuSx1fjhcNWUC/KG6oVyFUGkSBEr9n1axSNduWU8GlI7suaHXr4yxjet6KjrUZxUTE5WzzWwQ== +jasmine-core@~5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-5.2.0.tgz#7d0aa4c26cb3dbaed201d0505489baf1e48faeca" + integrity sha512-tSAtdrvWybZkQmmaIoDgnvHG8ORUNw5kEVlO5CvrXj02Jjr9TZrmjFq7FUiOUzJiOP2wLGYT6PgrQgQF4R1xiw== + jasmine-spec-reporter@~7.0.0: version "7.0.0" - resolved "https://registry.npmjs.org/jasmine-spec-reporter/-/jasmine-spec-reporter-7.0.0.tgz" + resolved "https://registry.yarnpkg.com/jasmine-spec-reporter/-/jasmine-spec-reporter-7.0.0.tgz#94b939448e63d4e2bd01668142389f20f0a8ea49" integrity sha512-OtC7JRasiTcjsaCBPtMO0Tl8glCejM4J4/dNuOJdA8lBjz4PmWjYQ6pzb0uzpBNAWJMDudYuj9OdXJWqM2QTJg== dependencies: colors "1.4.0" jasmine@2.8.0: version "2.8.0" - resolved "https://registry.npmjs.org/jasmine/-/jasmine-2.8.0.tgz" + resolved "https://registry.yarnpkg.com/jasmine/-/jasmine-2.8.0.tgz#6b089c0a11576b1f16df11b80146d91d4e8b8a3e" integrity sha512-KbdGQTf5jbZgltoHs31XGiChAPumMSY64OZMWLNYnEnMfG5uwGBhffePwuskexjT+/Jea/gU3qAU8344hNohSw== dependencies: exit "^0.1.2" @@ -6741,12 +6417,12 @@ jasmine@2.8.0: jasminewd2@^2.1.0: version "2.2.0" - resolved "https://registry.npmjs.org/jasminewd2/-/jasminewd2-2.2.0.tgz" + resolved "https://registry.yarnpkg.com/jasminewd2/-/jasminewd2-2.2.0.tgz#e37cf0b17f199cce23bea71b2039395246b4ec4e" integrity sha512-Rn0nZe4rfDhzA63Al3ZGh0E+JTmM6ESZYXJGKuqKGZObsAB9fwXPD03GjtIEvJBDOhN94T5MzbwZSqzFHSQPzg== jest-worker@^27.4.5: version "27.5.1" - resolved "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.5.1.tgz#8d146f0900e8973b106b6f73cc1e9a8cb86f8db0" integrity sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg== dependencies: "@types/node" "*" @@ -6754,33 +6430,38 @@ jest-worker@^27.4.5: supports-color "^8.0.0" jiti@^1.18.2: - version "1.19.1" - resolved "https://registry.npmjs.org/jiti/-/jiti-1.19.1.tgz" - integrity sha512-oVhqoRDaBXf7sjkll95LHVS6Myyyb1zaunVwk4Z0+WPSW4gjS0pl01zYKHScTuyEhQsFxV5L4DR5r+YqSyqyyg== + version "1.21.6" + resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.21.6.tgz#6c7f7398dd4b3142767f9a168af2f317a428d268" + integrity sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w== js-tokens@^4.0.0: version "4.0.0" - resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== js-yaml@4.1.0, js-yaml@^4.1.0: version "4.1.0" - resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== dependencies: argparse "^2.0.1" js-yaml@^3.10.0, js-yaml@^3.13.1: version "3.14.1" - resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537" integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== dependencies: argparse "^1.0.7" esprima "^4.0.0" +jsbn@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-1.1.0.tgz#b01307cb29b618a1ed26ec79e911f803c4da0040" + integrity sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A== + jsbn@~0.1.0: version "0.1.1" - resolved "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz" + resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" integrity sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg== jsdom@^16.4.0: @@ -6818,69 +6499,74 @@ jsdom@^16.4.0: jsesc@^2.5.1: version "2.5.2" - resolved "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== jsesc@~0.5.0: version "0.5.0" - resolved "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" integrity sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA== +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-parse-even-better-errors@^2.3.0, json-parse-even-better-errors@^2.3.1: version "2.3.1" - resolved "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz" + resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== json-parse-even-better-errors@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.0.tgz" - integrity sha512-iZbGHafX/59r39gPwVPRBGw0QQKnA7tte5pSMrhWOW7swGsVvVTjmfyAV9pNqk8YGT7tRCdxRu8uzcgZwoDooA== + version "3.0.2" + resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.2.tgz#b43d35e89c0f3be6b5fbbe9dc6c82467b30c28da" + integrity sha512-fi0NG4bPjCHunUJffmLd0gxssIgkNmArMvis4iNah6Owg1MCJjWhEcDLmsK6iGkJq3tHwbDkTlce70/tmXN4cQ== json-schema-traverse@^0.4.1: version "0.4.1" - resolved "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== json-schema-traverse@^1.0.0: version "1.0.0" - resolved "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== json-schema@0.4.0: version "0.4.0" - resolved "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz" + resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.4.0.tgz#f7de4cf6efab838ebaeb3236474cbba5a1930ab5" integrity sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA== json-stable-stringify-without-jsonify@^1.0.1: version "1.0.1" - resolved "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz" + resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== json-stringify-safe@~5.0.1: version "5.0.1" - resolved "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz" + resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" integrity sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA== -json5@^2.1.2, json5@^2.2.2: +json5@^2.1.2, json5@^2.2.2, json5@^2.2.3: version "2.2.3" - resolved "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== jsonc-parser@3.2.0: version "3.2.0" - resolved "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz" + resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-3.2.0.tgz#31ff3f4c2b9793f89c67212627c51c6394f88e76" integrity sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w== jsonfile@^4.0.0: version "4.0.0" - resolved "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" integrity sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg== optionalDependencies: graceful-fs "^4.1.6" jsonfile@^6.0.1: version "6.1.0" - resolved "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== dependencies: universalify "^2.0.0" @@ -6889,12 +6575,12 @@ jsonfile@^6.0.1: jsonparse@^1.3.1: version "1.3.1" - resolved "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz" + resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280" integrity sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg== jsprim@^1.2.2: version "1.4.2" - resolved "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz" + resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.2.tgz#712c65533a15c878ba59e9ed5f0e26d5b77c5feb" integrity sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw== dependencies: assert-plus "1.0.0" @@ -6904,7 +6590,7 @@ jsprim@^1.2.2: jszip@^3.1.3: version "3.10.1" - resolved "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz" + resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.10.1.tgz#34aee70eb18ea1faec2f589208a157d1feb091c2" integrity sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g== dependencies: lie "~3.3.0" @@ -6914,14 +6600,14 @@ jszip@^3.1.3: karma-chrome-launcher@^3.2.0: version "3.2.0" - resolved "https://registry.npmjs.org/karma-chrome-launcher/-/karma-chrome-launcher-3.2.0.tgz" + resolved "https://registry.yarnpkg.com/karma-chrome-launcher/-/karma-chrome-launcher-3.2.0.tgz#eb9c95024f2d6dfbb3748d3415ac9b381906b9a9" integrity sha512-rE9RkUPI7I9mAxByQWkGJFXfFD6lE4gC5nPuZdobf/QdTEJI6EU4yIay/cfU/xV4ZxlM5JiTv7zWYgA64NpS5Q== dependencies: which "^1.2.1" karma-coverage-istanbul-reporter@^3.0.3: version "3.0.3" - resolved "https://registry.npmjs.org/karma-coverage-istanbul-reporter/-/karma-coverage-istanbul-reporter-3.0.3.tgz" + resolved "https://registry.yarnpkg.com/karma-coverage-istanbul-reporter/-/karma-coverage-istanbul-reporter-3.0.3.tgz#f3b5303553aadc8e681d40d360dfdc19bc7e9fe9" integrity sha512-wE4VFhG/QZv2Y4CdAYWDbMmcAHeS926ZIji4z+FkB2aF/EposRb6DP6G5ncT/wXhqUfAb/d7kZrNKPonbvsATw== dependencies: istanbul-lib-coverage "^3.0.0" @@ -6932,27 +6618,27 @@ karma-coverage-istanbul-reporter@^3.0.3: karma-jasmine-html-reporter@^2.1.0: version "2.1.0" - resolved "https://registry.npmjs.org/karma-jasmine-html-reporter/-/karma-jasmine-html-reporter-2.1.0.tgz" + resolved "https://registry.yarnpkg.com/karma-jasmine-html-reporter/-/karma-jasmine-html-reporter-2.1.0.tgz#f951ad00b08d61d03595402c914d1a589c4930e3" integrity sha512-sPQE1+nlsn6Hwb5t+HHwyy0A1FNCVKuL1192b+XNauMYWThz2kweiBVW1DqloRpVvZIJkIoHVB7XRpK78n1xbQ== karma-jasmine@^5.1.0: version "5.1.0" - resolved "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-5.1.0.tgz" + resolved "https://registry.yarnpkg.com/karma-jasmine/-/karma-jasmine-5.1.0.tgz#3af4558a6502fa16856a0f346ec2193d4b884b2f" integrity sha512-i/zQLFrfEpRyQoJF9fsCdTMOF5c2dK7C7OmsuKg2D0YSsuZSfQDiLuaiktbuio6F2wiCsZSnSnieIQ0ant/uzQ== dependencies: jasmine-core "^4.1.0" karma-source-map-support@1.4.0: version "1.4.0" - resolved "https://registry.npmjs.org/karma-source-map-support/-/karma-source-map-support-1.4.0.tgz" + resolved "https://registry.yarnpkg.com/karma-source-map-support/-/karma-source-map-support-1.4.0.tgz#58526ceccf7e8730e56effd97a4de8d712ac0d6b" integrity sha512-RsBECncGO17KAoJCYXjv+ckIz+Ii9NCi+9enk+rq6XC81ezYkb4/RHE6CTXdA7IOJqoF3wcaLfVG0CPmE5ca6A== dependencies: source-map-support "^0.5.5" karma@^6.4.2: - version "6.4.2" - resolved "https://registry.npmjs.org/karma/-/karma-6.4.2.tgz" - integrity sha512-C6SU/53LB31BEgRg+omznBEMY4SjHU3ricV6zBcAe1EeILKkeScr+fZXtaI5WyDbkVowJxxAI6h73NcFPmXolQ== + version "6.4.4" + resolved "https://registry.yarnpkg.com/karma/-/karma-6.4.4.tgz#dfa5a426cf5a8b53b43cd54ef0d0d09742351492" + integrity sha512-LrtUxbdvt1gOpo3gxG+VAJlJAEMhbWlM4YrFQgql98FwF7+K8K12LYO4hnDdUkNjeztYrOXEMqgTajSWgmtI/w== dependencies: "@colors/colors" "1.5.0" body-parser "^1.19.0" @@ -6973,40 +6659,47 @@ karma@^6.4.2: qjobs "^1.2.0" range-parser "^1.2.1" rimraf "^3.0.2" - socket.io "^4.4.1" + socket.io "^4.7.2" source-map "^0.6.1" tmp "^0.2.1" ua-parser-js "^0.7.30" yargs "^16.1.1" +keyv@^4.5.3: + version "4.5.4" + resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" + integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw== + dependencies: + json-buffer "3.0.1" + kind-of@^6.0.2: version "6.0.3" - resolved "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== klona@^2.0.4: version "2.0.6" - resolved "https://registry.npmjs.org/klona/-/klona-2.0.6.tgz" + resolved "https://registry.yarnpkg.com/klona/-/klona-2.0.6.tgz#85bffbf819c03b2f53270412420a4555ef882e22" integrity sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA== launch-editor@^2.6.0: - version "2.6.0" - resolved "https://registry.npmjs.org/launch-editor/-/launch-editor-2.6.0.tgz" - integrity sha512-JpDCcQnyAAzZZaZ7vEiSqL690w7dAEyLao+KC96zBplnYbJS7TYNjvM3M7y3dGz+v7aIsJk3hllWuc0kWAjyRQ== + version "2.8.0" + resolved "https://registry.yarnpkg.com/launch-editor/-/launch-editor-2.8.0.tgz#7255d90bdba414448e2138faa770a74f28451305" + integrity sha512-vJranOAJrI/llyWGRQqiDM+adrw+k83fvmmx3+nV47g3+36xM15jE+zyZ6Ffel02+xSvuM0b2GDRosXZkbb6wA== dependencies: picocolors "^1.0.0" - shell-quote "^1.7.3" + shell-quote "^1.8.1" less-loader@11.1.0: version "11.1.0" - resolved "https://registry.npmjs.org/less-loader/-/less-loader-11.1.0.tgz" + resolved "https://registry.yarnpkg.com/less-loader/-/less-loader-11.1.0.tgz#a452384259bdf8e4f6d5fdcc39543609e6313f82" integrity sha512-C+uDBV7kS7W5fJlUjq5mPBeBVhYpTIm5gB09APT9o3n/ILeaXVsiSFTbZpTJCJwQ/Crczfn3DmfQFwxYusWFug== dependencies: klona "^2.0.4" less@4.1.3: version "4.1.3" - resolved "https://registry.npmjs.org/less/-/less-4.1.3.tgz" + resolved "https://registry.yarnpkg.com/less/-/less-4.1.3.tgz#175be9ddcbf9b250173e0a00b4d6920a5b770246" integrity sha512-w16Xk/Ta9Hhyei0Gpz9m7VS8F28nieJaL/VyShID7cYvP6IL5oHeL6p4TXSDJqZE/lNv0oJ2pGVjJsRkfwm5FA== dependencies: copy-anything "^2.0.1" @@ -7023,54 +6716,54 @@ less@4.1.3: levn@^0.4.1: version "0.4.1" - resolved "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== dependencies: prelude-ls "^1.2.1" type-check "~0.4.0" -libphonenumber-js@^1.10.49: - version "1.10.49" - resolved "https://registry.yarnpkg.com/libphonenumber-js/-/libphonenumber-js-1.10.49.tgz#c871661c62452348d228c96425f75ddf7e10f05a" - integrity sha512-gvLtyC3tIuqfPzjvYLH9BmVdqzGDiSi4VjtWe2fAgSdBf0yt8yPmbNnRIHNbR5IdtVkm0ayGuzwQKTWmU0hdjQ== +libphonenumber-js@^1.11.4: + version "1.11.5" + resolved "https://registry.yarnpkg.com/libphonenumber-js/-/libphonenumber-js-1.11.5.tgz#50a441da5ff9ed9a322d796a14f1e9cbc0fdabdf" + integrity sha512-TwHR5BZxGRODtAfz03szucAkjT5OArXr+94SMtAM2pYXIlQNVMrxvb6uSCbnaJJV6QXEyICk7+l6QPgn72WHhg== license-webpack-plugin@4.0.2: version "4.0.2" - resolved "https://registry.npmjs.org/license-webpack-plugin/-/license-webpack-plugin-4.0.2.tgz" + resolved "https://registry.yarnpkg.com/license-webpack-plugin/-/license-webpack-plugin-4.0.2.tgz#1e18442ed20b754b82f1adeff42249b81d11aec6" integrity sha512-771TFWFD70G1wLTC4oU2Cw4qvtmNrIw+wRvBtn+okgHl7slJVi7zfNcdmqDL72BojM30VNJ2UHylr1o77U37Jw== dependencies: webpack-sources "^3.0.0" lie@~3.3.0: version "3.3.0" - resolved "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz" + resolved "https://registry.yarnpkg.com/lie/-/lie-3.3.0.tgz#dcf82dee545f46074daf200c7c1c5a08e0f40f6a" integrity sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ== dependencies: immediate "~3.0.5" lines-and-columns@^1.1.6: version "1.2.4" - resolved "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz" + resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== lines-and-columns@~2.0.3: - version "2.0.3" - resolved "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-2.0.3.tgz" - integrity sha512-cNOjgCnLB+FnvWWtyRTzmB3POJ+cXxTA81LoW7u8JdmhfXzriropYwpjShnz1QLLWsQwY7nIxoDmcPTwphDK9w== + version "2.0.4" + resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-2.0.4.tgz#d00318855905d2660d8c0822e3f5a4715855fc42" + integrity sha512-wM1+Z03eypVAVUCE7QdSqpVIvelbOakn1M0bPDoA4SGWPx3sNDVUiMo3L6To6WWGClB7VyXnhQ4Sn7gxiJbE6A== loader-runner@^4.2.0: version "4.3.0" - resolved "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz" + resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.3.0.tgz#c1b4a163b99f614830353b16755e7149ac2314e1" integrity sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg== loader-utils@3.2.1: version "3.2.1" - resolved "https://registry.npmjs.org/loader-utils/-/loader-utils-3.2.1.tgz" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-3.2.1.tgz#4fb104b599daafd82ef3e1a41fb9265f87e1f576" integrity sha512-ZvFw1KWS3GVyYBYb7qkmRM/WwL2TQQBxgCK62rlvm4WpVQ23Nb4tYjApUlfjrEGvOs7KHEsmyUn75OHZrJMWPw== loader-utils@^2.0.0: version "2.0.4" - resolved "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.4.tgz#8b5cb38b5c34a9a018ee1fc0e6a066d1dfcc528c" integrity sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw== dependencies: big.js "^5.2.2" @@ -7079,14 +6772,14 @@ loader-utils@^2.0.0: locate-path@^5.0.0: version "5.0.0" - resolved "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== dependencies: p-locate "^4.1.0" locate-path@^6.0.0: version "6.0.0" - resolved "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz" + 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" @@ -7100,27 +6793,27 @@ locate-path@^7.0.0, locate-path@^7.1.0: lodash.camelcase@^4.3.0: version "4.3.0" - resolved "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz" + resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" integrity sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA== lodash.debounce@^4.0.8: version "4.0.8" - resolved "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz" + resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow== lodash.merge@^4.6.2: version "4.6.2" - resolved "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz" + resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== lodash@^4.17.21, lodash@^4.7.0: version "4.17.21" - resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== log-symbols@^4.1.0: version "4.1.0" - resolved "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== dependencies: chalk "^4.1.0" @@ -7128,7 +6821,7 @@ log-symbols@^4.1.0: log4js@^6.4.1: version "6.9.1" - resolved "https://registry.npmjs.org/log4js/-/log4js-6.9.1.tgz" + resolved "https://registry.yarnpkg.com/log4js/-/log4js-6.9.1.tgz#aba5a3ff4e7872ae34f8b4c533706753709e38b6" integrity sha512-1somDdy9sChrr9/f4UlzhdaGfDR2c/SaD2a4T7qEkG4jTS57/B3qmnjLYePwQ8cqWnUHZI0iAKxMBpCZICiZ2g== dependencies: date-format "^4.0.14" @@ -7139,33 +6832,33 @@ log4js@^6.4.1: long@^5.0.0: version "5.2.3" - resolved "https://registry.npmjs.org/long/-/long-5.2.3.tgz" + resolved "https://registry.yarnpkg.com/long/-/long-5.2.3.tgz#a3ba97f3877cf1d778eccbcb048525ebb77499e1" integrity sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q== +lru-cache@^10.2.0: + version "10.4.3" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119" + integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ== + lru-cache@^5.1.1: version "5.1.1" - resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== dependencies: yallist "^3.0.2" lru-cache@^6.0.0: version "6.0.0" - resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== dependencies: yallist "^4.0.0" lru-cache@^7.4.4, lru-cache@^7.5.1, lru-cache@^7.7.1: version "7.18.3" - resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.18.3.tgz#f793896e0fd0e954a59dfdd82f0773808df6aa89" integrity sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA== -lru-cache@^9.1.1: - version "9.1.1" - resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-9.1.1.tgz" - integrity sha512-65/Jky17UwSb0BuB9V+MyDpsOtXKmYwzhyl+cOa9XUiI4uV2Ouy/2voFP3+al0BjZbJgMBD8FojMpAf+Z+qn4A== - magic-string@0.30.1: version "0.30.1" resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.1.tgz#ce5cd4b0a81a5d032bd69aab4522299b2166284d" @@ -7175,22 +6868,22 @@ magic-string@0.30.1: make-dir@^2.1.0: version "2.1.0" - resolved "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5" integrity sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA== dependencies: pify "^4.0.1" semver "^5.6.0" -make-dir@^3.0.0: - version "3.1.0" - resolved "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz" - integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== +make-dir@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-4.0.0.tgz#c3c2307a771277cd9638305f915c29ae741b614e" + integrity sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw== dependencies: - semver "^6.0.0" + semver "^7.5.3" make-fetch-happen@^10.0.3: version "10.2.1" - resolved "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-10.2.1.tgz" + resolved "https://registry.yarnpkg.com/make-fetch-happen/-/make-fetch-happen-10.2.1.tgz#f5e3835c5e9817b617f2770870d9492d28678164" integrity sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w== dependencies: agentkeepalive "^4.2.1" @@ -7210,9 +6903,9 @@ make-fetch-happen@^10.0.3: socks-proxy-agent "^7.0.0" ssri "^9.0.0" -make-fetch-happen@^11.0.0, make-fetch-happen@^11.0.1, make-fetch-happen@^11.1.0: +make-fetch-happen@^11.0.0, make-fetch-happen@^11.0.1, make-fetch-happen@^11.1.1: version "11.1.1" - resolved "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-11.1.1.tgz" + resolved "https://registry.yarnpkg.com/make-fetch-happen/-/make-fetch-happen-11.1.1.tgz#85ceb98079584a9523d4bf71d32996e7e208549f" integrity sha512-rLWS7GCSTcEujjVBs2YqG7Y4643u8ucvCJeSRqiLYhesrDuzeuFIk37xREzAsfQaqzl8b9rNCE4m6J8tvX4Q8w== dependencies: agentkeepalive "^4.2.1" @@ -7233,136 +6926,141 @@ make-fetch-happen@^11.0.0, make-fetch-happen@^11.0.1, make-fetch-happen@^11.1.0: material-colors@^1.2.6: version "1.2.6" - resolved "https://registry.npmjs.org/material-colors/-/material-colors-1.2.6.tgz" + resolved "https://registry.yarnpkg.com/material-colors/-/material-colors-1.2.6.tgz#6d1958871126992ceecc72f4bcc4d8f010865f46" integrity sha512-6qE4B9deFBIa9YSpOc9O0Sgc43zTeVYbgDT5veRKSlB2+ZuHNoVVxA1L/ckMUayV9Ay9y7Z/SZCLcGteW9i7bg== material-design-icons-iconfont@^6.1.1: version "6.7.0" - resolved "https://registry.npmjs.org/material-design-icons-iconfont/-/material-design-icons-iconfont-6.7.0.tgz" + resolved "https://registry.yarnpkg.com/material-design-icons-iconfont/-/material-design-icons-iconfont-6.7.0.tgz#55cf0f3d7e4c76e032855b7e810b6e30535eff3c" integrity sha512-lSj71DgVv20kO0kGbs42icDzbRot61gEDBLQACzkUuznRQBUYmbxzEkGU6dNBb5fRWHMaScYlAXX96HQ4/cJWA== media-typer@0.3.0: version "0.3.0" - resolved "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== memfs@^3.4.12, memfs@^3.4.3: - version "3.5.1" - resolved "https://registry.npmjs.org/memfs/-/memfs-3.5.1.tgz" - integrity sha512-UWbFJKvj5k+nETdteFndTpYxdeTMox/ULeqX5k/dpaQJCCFmj5EeKv3dBcyO2xmkRAx2vppRu5dVG7SOtsGOzA== + version "3.6.0" + resolved "https://registry.yarnpkg.com/memfs/-/memfs-3.6.0.tgz#d7a2110f86f79dd950a8b6df6d57bc984aa185f6" + integrity sha512-EGowvkkgbMcIChjMTMkESFDbZeSh8xZ7kNSF0hAiAN4Jh6jgHCRS0Ga/+C8y6Au+oqpezRHCfPsmJ2+DwAgiwQ== dependencies: - fs-monkey "^1.0.3" + fs-monkey "^1.0.4" merge-descriptors@1.0.1: version "1.0.1" - resolved "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w== merge-stream@^2.0.0: version "2.0.0" - resolved "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz" + resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== merge2@^1.3.0, merge2@^1.4.1: version "1.4.1" - resolved "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz" + resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== methods@~1.1.2: version "1.1.2" - resolved "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== micromatch@^4.0.2, micromatch@^4.0.4: - version "4.0.5" - resolved "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz" - integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== + version "4.0.7" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.7.tgz#33e8190d9fe474a9895525f5618eee136d46c2e5" + integrity sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q== dependencies: - braces "^3.0.2" + braces "^3.0.3" picomatch "^2.3.1" -mime-db@1.52.0, "mime-db@>= 1.43.0 < 2": +mime-db@1.52.0: version "1.52.0" - resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== +"mime-db@>= 1.43.0 < 2": + version "1.53.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.53.0.tgz#3cb63cd820fc29896d9d4e8c32ab4fcd74ccb447" + integrity sha512-oHlN/w+3MQ3rba9rqFr6V/ypF10LSkdwUysQL7GkXoTgIWeV+tcXGA852TBxH+gsh8UWoyhR1hKcoMJTuWflpg== + mime-types@^2.1.12, mime-types@^2.1.27, mime-types@^2.1.31, mime-types@~2.1.17, mime-types@~2.1.19, mime-types@~2.1.24, mime-types@~2.1.34: version "2.1.35" - resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== dependencies: mime-db "1.52.0" mime@1.6.0, mime@^1.4.1: version "1.6.0" - resolved "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== mime@^2.5.2: version "2.6.0" - resolved "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz" + resolved "https://registry.yarnpkg.com/mime/-/mime-2.6.0.tgz#a2a682a95cd4d0cb1d6257e28f83da7e35800367" integrity sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg== mimic-fn@^2.1.0: version "2.1.0" - resolved "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== mini-css-extract-plugin@2.7.6: version "2.7.6" - resolved "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.7.6.tgz" + resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-2.7.6.tgz#282a3d38863fddcd2e0c220aaed5b90bc156564d" integrity sha512-Qk7HcgaPkGG6eD77mLvZS1nmxlao3j+9PkrT9Uc7HAE1id3F41+DdBRYRYkbyfNRGzm8/YWtzhw7nVPmwhqTQw== dependencies: schema-utils "^4.0.0" minimalistic-assert@^1.0.0: version "1.0.1" - resolved "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz" + resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== minimatch@3.0.5: version "3.0.5" - resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.0.5.tgz" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.5.tgz#4da8f1290ee0f0f8e83d60ca69f8f134068604a3" integrity sha512-tUpxzX0VAzJHjLu0xUfFv1gwVp9ba3IOuRAVH2EGuRW8a5emA2FlACLqiT/lDVtS1W+TGNwqz3sWaNyLgDJWuw== dependencies: brace-expansion "^1.1.7" minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: version "3.1.2" - resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== dependencies: brace-expansion "^1.1.7" minimatch@^5.0.1: version "5.1.6" - resolved "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96" integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g== dependencies: brace-expansion "^2.0.1" -minimatch@^9.0.0: - version "9.0.0" - resolved "https://registry.npmjs.org/minimatch/-/minimatch-9.0.0.tgz" - integrity sha512-0jJj8AvgKqWN05mrwuqi8QYKx1WmYSUoKSxu5Qhs9prezTz10sxAHGNZe9J9cqIJzta8DWsleh2KaVaLl6Ru2w== +minimatch@^9.0.0, minimatch@^9.0.4: + version "9.0.5" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5" + integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow== dependencies: brace-expansion "^2.0.1" minimist@^1.2.0, minimist@^1.2.6: version "1.2.8" - resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== minipass-collect@^1.0.2: version "1.0.2" - resolved "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz" + resolved "https://registry.yarnpkg.com/minipass-collect/-/minipass-collect-1.0.2.tgz#22b813bf745dc6edba2576b940022ad6edc8c617" integrity sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA== dependencies: minipass "^3.0.0" minipass-fetch@^2.0.3: version "2.1.2" - resolved "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-2.1.2.tgz" + resolved "https://registry.yarnpkg.com/minipass-fetch/-/minipass-fetch-2.1.2.tgz#95560b50c472d81a3bc76f20ede80eaed76d8add" integrity sha512-LT49Zi2/WMROHYoqGgdlQIZh8mLPZmOrN2NdJjMXxYe4nkN6FUyuPuOAOedNJDrx0IRGg9+4guZewtp8hE6TxA== dependencies: minipass "^3.1.6" @@ -7372,11 +7070,11 @@ minipass-fetch@^2.0.3: encoding "^0.1.13" minipass-fetch@^3.0.0: - version "3.0.3" - resolved "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-3.0.3.tgz" - integrity sha512-n5ITsTkDqYkYJZjcRWzZt9qnZKCT7nKCosJhHoj7S7zD+BP4jVbWs+odsniw5TA3E0sLomhTKOKjF86wf11PuQ== + version "3.0.5" + resolved "https://registry.yarnpkg.com/minipass-fetch/-/minipass-fetch-3.0.5.tgz#f0f97e40580affc4a35cc4a1349f05ae36cb1e4c" + integrity sha512-2N8elDQAtSnFV0Dk7gt15KHsS0Fyz6CbYZ360h0WTYV1Ty46li3rAXVOQj1THMNLdmrD9Vt5pBPtWtVkpwGBqg== dependencies: - minipass "^5.0.0" + minipass "^7.0.3" minipass-sized "^1.0.3" minizlib "^2.1.2" optionalDependencies: @@ -7384,53 +7082,53 @@ minipass-fetch@^3.0.0: minipass-flush@^1.0.5: version "1.0.5" - resolved "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz" + resolved "https://registry.yarnpkg.com/minipass-flush/-/minipass-flush-1.0.5.tgz#82e7135d7e89a50ffe64610a787953c4c4cbb373" integrity sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw== dependencies: minipass "^3.0.0" minipass-json-stream@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/minipass-json-stream/-/minipass-json-stream-1.0.1.tgz" - integrity sha512-ODqY18UZt/I8k+b7rl2AENgbWE8IDYam+undIJONvigAz8KR5GWblsFTEfQs0WODsjbSXWlm+JHEv8Gr6Tfdbg== + version "1.0.2" + resolved "https://registry.yarnpkg.com/minipass-json-stream/-/minipass-json-stream-1.0.2.tgz#5121616c77a11c406c3ffa77509e0b77bb267ec3" + integrity sha512-myxeeTm57lYs8pH2nxPzmEEg8DGIgW+9mv6D4JZD2pa81I/OBjeU7PtICXV6c9eRGTA5JMDsuIPUZRCyBMYNhg== dependencies: jsonparse "^1.3.1" minipass "^3.0.0" minipass-pipeline@^1.2.4: version "1.2.4" - resolved "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz" + resolved "https://registry.yarnpkg.com/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz#68472f79711c084657c067c5c6ad93cddea8214c" integrity sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A== dependencies: minipass "^3.0.0" minipass-sized@^1.0.3: version "1.0.3" - resolved "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz" + resolved "https://registry.yarnpkg.com/minipass-sized/-/minipass-sized-1.0.3.tgz#70ee5a7c5052070afacfbc22977ea79def353b70" integrity sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g== dependencies: minipass "^3.0.0" minipass@^3.0.0, minipass@^3.1.1, minipass@^3.1.6: version "3.3.6" - resolved "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.3.6.tgz#7bba384db3a1520d18c9c0e5251c3444e95dd94a" integrity sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw== dependencies: yallist "^4.0.0" minipass@^5.0.0: version "5.0.0" - resolved "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-5.0.0.tgz#3e9788ffb90b694a5d0ec94479a45b5d8738133d" integrity sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ== -"minipass@^5.0.0 || ^6.0.0": - version "6.0.1" - resolved "https://registry.npmjs.org/minipass/-/minipass-6.0.1.tgz" - integrity sha512-Tenl5QPpgozlOGBiveNYHg2f6y+VpxsXRoIHFUVJuSmTonXRAE6q9b8Mp/O46762/2AlW4ye4Nkyvx0fgWDKbw== +"minipass@^5.0.0 || ^6.0.2 || ^7.0.0", minipass@^7.0.3, minipass@^7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707" + integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw== minizlib@^2.1.1, minizlib@^2.1.2: version "2.1.2" - resolved "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz" + resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931" integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg== dependencies: minipass "^3.0.0" @@ -7438,44 +7136,44 @@ minizlib@^2.1.1, minizlib@^2.1.2: mkdirp@^0.5.5: version "0.5.6" - resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6" integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw== dependencies: minimist "^1.2.6" mkdirp@^1.0.3, mkdirp@^1.0.4: version "1.0.4" - resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== moment@^2.29.4: - version "2.29.4" - resolved "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz" - integrity sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w== + version "2.30.1" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.30.1.tgz#f8c91c07b7a786e30c59926df530b4eac96974ae" + integrity sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how== mrmime@1.0.1: version "1.0.1" - resolved "https://registry.npmjs.org/mrmime/-/mrmime-1.0.1.tgz" + resolved "https://registry.yarnpkg.com/mrmime/-/mrmime-1.0.1.tgz#5f90c825fad4bdd41dc914eff5d1a8cfdaf24f27" integrity sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw== ms@2.0.0: version "2.0.0" - resolved "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== ms@2.1.2: version "2.1.2" - resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz" + 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.0.0, ms@^2.1.1: version "2.1.3" - resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== multicast-dns@^7.2.5: version "7.2.5" - resolved "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz" + resolved "https://registry.yarnpkg.com/multicast-dns/-/multicast-dns-7.2.5.tgz#77eb46057f4d7adbd16d9290fa7299f6fa64cced" integrity sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg== dependencies: dns-packet "^5.2.2" @@ -7483,46 +7181,45 @@ multicast-dns@^7.2.5: mute-stream@0.0.8: version "0.0.8" - resolved "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz" + resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== -nanoid@^3.3.6: - version "3.3.6" - resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz" - integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA== +nanoid@^3.3.6, nanoid@^3.3.7: + version "3.3.7" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8" + integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g== natural-compare-lite@^1.4.0: version "1.4.0" - resolved "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz" + resolved "https://registry.yarnpkg.com/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz#17b09581988979fddafe0201e931ba933c96cbb4" integrity sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g== natural-compare@^1.4.0: version "1.4.0" - resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz" + resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== needle@^3.1.0: - version "3.2.0" - resolved "https://registry.npmjs.org/needle/-/needle-3.2.0.tgz" - integrity sha512-oUvzXnyLiVyVGoianLijF9O/RecZUf7TkBfimjGrLM4eQhXyeJwM6GeAWccwfQ9aa4gMCZKqhAOuLaMIcQxajQ== + version "3.3.1" + resolved "https://registry.yarnpkg.com/needle/-/needle-3.3.1.tgz#63f75aec580c2e77e209f3f324e2cdf3d29bd049" + integrity sha512-6k0YULvhpw+RoLNiQCRKOl09Rv1dPLr8hHnVjHqdolKwDrdNyk+Hmrthi4lIGPPz3r39dLx0hsF5s40sZ3Us4Q== dependencies: - debug "^3.2.6" iconv-lite "^0.6.3" sax "^1.2.4" negotiator@0.6.3, negotiator@^0.6.3: version "0.6.3" - resolved "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== neo-async@^2.6.2: version "2.6.2" - resolved "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz" + resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== ngx-color@^9.0.0: version "9.0.0" - resolved "https://registry.npmjs.org/ngx-color/-/ngx-color-9.0.0.tgz" + resolved "https://registry.yarnpkg.com/ngx-color/-/ngx-color-9.0.0.tgz#7e58da081a3563c53607fc10090a61cfa27eaffc" integrity sha512-zyAFux+FRI4cACZ7g8DQQsBbNMhqmFkhtUPaxhkiVHhPzWU1iqXP8MqWH6By3guNOCch5oYrYNBWlHToklbdDg== dependencies: "@ctrl/tinycolor" "^3.6.0" @@ -7531,7 +7228,7 @@ ngx-color@^9.0.0: nice-napi@^1.0.2: version "1.0.2" - resolved "https://registry.npmjs.org/nice-napi/-/nice-napi-1.0.2.tgz" + resolved "https://registry.yarnpkg.com/nice-napi/-/nice-napi-1.0.2.tgz#dc0ab5a1eac20ce548802fc5686eaa6bc654927b" integrity sha512-px/KnJAJZf5RuBGcfD+Sp2pAKq0ytz8j+1NehvgIGFkvtvFrDM3T8E4x/JJODXK9WZow8RRGrbA9QQ3hs+pDhA== dependencies: node-addon-api "^3.0.0" @@ -7539,25 +7236,26 @@ nice-napi@^1.0.2: node-addon-api@^3.0.0, node-addon-api@^3.2.1: version "3.2.1" - resolved "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz" + resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.2.1.tgz#81325e0a2117789c0128dab65e7e38f07ceba161" integrity sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A== node-forge@^1: version "1.3.1" - resolved "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz" + resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3" integrity sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA== node-gyp-build@^4.2.2, node-gyp-build@^4.3.0: - version "4.6.0" - resolved "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.6.0.tgz" - integrity sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ== + version "4.8.1" + resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.8.1.tgz#976d3ad905e71b76086f4f0b0d3637fe79b6cda5" + integrity sha512-OSs33Z9yWr148JZcbZd5WiAXhh/n9z8TxQcdMhIOlpN9AhWpLfvVFO73+m77bBABQMaY9XSvIa+qk0jlI7Gcaw== node-gyp@^9.0.0: - version "9.3.1" - resolved "https://registry.npmjs.org/node-gyp/-/node-gyp-9.3.1.tgz" - integrity sha512-4Q16ZCqq3g8awk6UplT7AuxQ35XN4R/yf/+wSAwcBUAjg7l58RTactWaP8fIDTi0FzI7YcVLujwExakZlfWkXg== + version "9.4.1" + resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-9.4.1.tgz#8a1023e0d6766ecb52764cc3a734b36ff275e185" + integrity sha512-OQkWKbjQKbGkMf/xqI1jjy3oCTgMKJac58G2+bjZb3fza6gW2YrCSdMQYaoTb70crvE//Gngr4f0AgVHmqHvBQ== dependencies: env-paths "^2.2.0" + exponential-backoff "^3.1.1" glob "^7.1.4" graceful-fs "^4.2.6" make-fetch-happen "^10.0.3" @@ -7568,19 +7266,14 @@ node-gyp@^9.0.0: tar "^6.1.2" which "^2.0.2" -node-releases@^2.0.12: - version "2.0.13" - resolved "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz" - integrity sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ== - -node-releases@^2.0.8: - version "2.0.10" - resolved "https://registry.npmjs.org/node-releases/-/node-releases-2.0.10.tgz" - integrity sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w== +node-releases@^2.0.14: + version "2.0.18" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.18.tgz#f010e8d35e2fe8d6b2944f03f70213ecedc4ca3f" + integrity sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g== nopt@^6.0.0: version "6.0.0" - resolved "https://registry.npmjs.org/nopt/-/nopt-6.0.0.tgz" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-6.0.0.tgz#245801d8ebf409c6df22ab9d95b65e1309cdb16d" integrity sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g== dependencies: abbrev "^1.0.0" @@ -7597,7 +7290,7 @@ normalize-package-data@^3.0.2: normalize-package-data@^5.0.0: version "5.0.0" - resolved "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-5.0.0.tgz" + resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-5.0.0.tgz#abcb8d7e724c40d88462b84982f7cbf6859b4588" integrity sha512-h9iPVIfrVZ9wVYQnxFgtw1ugSvGEMOlyPWWtm8BMJhnwyEL/FLbYbTY3V3PpjI/BUK67n9PEWDu6eHzu1fB15Q== dependencies: hosted-git-info "^6.0.0" @@ -7607,36 +7300,36 @@ normalize-package-data@^5.0.0: normalize-path@^3.0.0, normalize-path@~3.0.0: version "3.0.0" - resolved "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== normalize-range@^0.1.2: version "0.1.2" - resolved "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz" + resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942" integrity sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA== npm-bundled@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/npm-bundled/-/npm-bundled-3.0.0.tgz" - integrity sha512-Vq0eyEQy+elFpzsKjMss9kxqb9tG3YHg4dsyWuUENuzvSUWe1TCnW/vV9FkhvBk/brEDoDiVd+M1Btosa6ImdQ== + version "3.0.1" + resolved "https://registry.yarnpkg.com/npm-bundled/-/npm-bundled-3.0.1.tgz#cca73e15560237696254b10170d8f86dad62da25" + integrity sha512-+AvaheE/ww1JEwRHOrn4WHNzOxGtVp+adrg2AeZS/7KuxGUYFuBta98wYpfHBbJp6Tg6j1NKSEVHNcfZzJHQwQ== dependencies: npm-normalize-package-bin "^3.0.0" npm-install-checks@^6.0.0: - version "6.1.1" - resolved "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-6.1.1.tgz" - integrity sha512-dH3GmQL4vsPtld59cOn8uY0iOqRmqKvV+DLGwNXV/Q7MDgD2QfOADWd/mFXcIE5LVhYYGjA3baz6W9JneqnuCw== + version "6.3.0" + resolved "https://registry.yarnpkg.com/npm-install-checks/-/npm-install-checks-6.3.0.tgz#046552d8920e801fa9f919cad569545d60e826fe" + integrity sha512-W29RiK/xtpCGqn6f3ixfRYGk+zRyr+Ew9F2E20BfXxT5/euLdA/Nm7fO7OeTGuAmTs30cpgInyJ0cYe708YTZw== dependencies: semver "^7.1.1" npm-normalize-package-bin@^3.0.0: version "3.0.1" - resolved "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-3.0.1.tgz" + resolved "https://registry.yarnpkg.com/npm-normalize-package-bin/-/npm-normalize-package-bin-3.0.1.tgz#25447e32a9a7de1f51362c61a559233b89947832" integrity sha512-dMxCf+zZ+3zeQZXKxmyuCKlIDPGuv8EF940xbkC4kQVDTtqoh6rJFO+JTKSA6/Rwi0getWmtuy4Itup0AMcaDQ== npm-package-arg@10.1.0, npm-package-arg@^10.0.0: version "10.1.0" - resolved "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-10.1.0.tgz" + resolved "https://registry.yarnpkg.com/npm-package-arg/-/npm-package-arg-10.1.0.tgz#827d1260a683806685d17193073cc152d3c7e9b1" integrity sha512-uFyyCEmgBfZTtrKk/5xDfHp6+MdrqGotX/VoOyEEl3mBwiEE5FlBaePanazJSVMPT7vKepcjYBY2ztg9A3yPIA== dependencies: hosted-git-info "^6.0.0" @@ -7646,14 +7339,14 @@ npm-package-arg@10.1.0, npm-package-arg@^10.0.0: npm-packlist@^7.0.0: version "7.0.4" - resolved "https://registry.npmjs.org/npm-packlist/-/npm-packlist-7.0.4.tgz" + resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-7.0.4.tgz#033bf74110eb74daf2910dc75144411999c5ff32" integrity sha512-d6RGEuRrNS5/N84iglPivjaJPxhDbZmlbTwTDX2IbcRHG5bZCdtysYMhwiPvcF4GisXHGn7xsxv+GQ7T/02M5Q== dependencies: ignore-walk "^6.0.0" -npm-pick-manifest@8.0.1, npm-pick-manifest@^8.0.0: +npm-pick-manifest@8.0.1: version "8.0.1" - resolved "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-8.0.1.tgz" + resolved "https://registry.yarnpkg.com/npm-pick-manifest/-/npm-pick-manifest-8.0.1.tgz#c6acd97d1ad4c5dbb80eac7b386b03ffeb289e5f" integrity sha512-mRtvlBjTsJvfCCdmPtiu2bdlx8d/KXtF7yNXNWe7G0Z36qWA9Ny5zXsI2PfBZEv7SXgoxTmNaTzGSbbzDZChoA== dependencies: npm-install-checks "^6.0.0" @@ -7661,9 +7354,19 @@ npm-pick-manifest@8.0.1, npm-pick-manifest@^8.0.0: npm-package-arg "^10.0.0" semver "^7.3.5" +npm-pick-manifest@^8.0.0: + version "8.0.2" + resolved "https://registry.yarnpkg.com/npm-pick-manifest/-/npm-pick-manifest-8.0.2.tgz#2159778d9c7360420c925c1a2287b5a884c713aa" + integrity sha512-1dKY+86/AIiq1tkKVD3l0WI+Gd3vkknVGAggsFeBkTvbhMQ1OND/LKkYv4JtXPKUJ8bOTCyLiqEg2P6QNdK+Gg== + dependencies: + npm-install-checks "^6.0.0" + npm-normalize-package-bin "^3.0.0" + npm-package-arg "^10.0.0" + semver "^7.3.5" + npm-registry-fetch@^14.0.0: version "14.0.5" - resolved "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-14.0.5.tgz" + resolved "https://registry.yarnpkg.com/npm-registry-fetch/-/npm-registry-fetch-14.0.5.tgz#fe7169957ba4986a4853a650278ee02e568d115d" integrity sha512-kIDMIo4aBm6xg7jOttupWZamsZRkAqMqwqqbVXnUqstY5+tapvv6bkH/qMR76jdgV+YljEUCyWx3hRYMrJiAgA== dependencies: make-fetch-happen "^11.0.0" @@ -7676,14 +7379,14 @@ npm-registry-fetch@^14.0.0: npm-run-path@^4.0.1: version "4.0.1" - resolved "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== dependencies: path-key "^3.0.0" npmlog@^6.0.0: version "6.0.2" - resolved "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz" + resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-6.0.2.tgz#c8166017a42f2dea92d6453168dd865186a70830" integrity sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg== dependencies: are-we-there-yet "^3.0.0" @@ -7693,15 +7396,15 @@ npmlog@^6.0.0: nth-check@^2.0.1: version "2.1.1" - resolved "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz" + resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.1.1.tgz#c9eab428effce36cd6b92c924bdb000ef1f1ed1d" integrity sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w== dependencies: boolbase "^1.0.0" nwsapi@^2.2.0: - version "2.2.7" - resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.7.tgz#738e0707d3128cb750dddcfe90e4610482df0f30" - integrity sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ== + version "2.2.12" + resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.12.tgz#fb6af5c0ec35b27b4581eb3bbad34ec9e5c696f8" + integrity sha512-qXDmcVlZV4XRtKFzddidpfVP4oMSGhga+xdMc25mv8kaLUHtgzCDhUxkrN8exkGdTlLNaXj7CV3GtON7zuGZ+w== nx@16.5.1: version "16.5.1" @@ -7756,18 +7459,18 @@ nx@16.5.1: oauth-sign@~0.9.0: version "0.9.0" - resolved "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz" + resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== object-assign@^4, object-assign@^4.0.1: version "4.1.1" - resolved "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== -object-inspect@^1.9.0: - version "1.12.3" - resolved "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz" - integrity sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g== +object-inspect@^1.13.1: + version "1.13.2" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.2.tgz#dea0088467fb991e67af4058147a24824a3043ff" + integrity sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g== object-path@^0.11.5: version "0.11.8" @@ -7776,45 +7479,45 @@ object-path@^0.11.5: obuf@^1.0.0, obuf@^1.1.2: version "1.1.2" - resolved "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz" + resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e" integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg== on-finished@2.4.1: version "2.4.1" - resolved "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== dependencies: ee-first "1.1.1" on-finished@~2.3.0: version "2.3.0" - resolved "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" integrity sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww== dependencies: ee-first "1.1.1" on-headers@~1.0.2: version "1.0.2" - resolved "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz" + resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f" integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== once@^1.3.0, once@^1.4.0: version "1.4.0" - resolved "https://registry.npmjs.org/once/-/once-1.4.0.tgz" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== dependencies: wrappy "1" onetime@^5.1.0, onetime@^5.1.2: version "5.1.2" - resolved "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== dependencies: mimic-fn "^2.1.0" open@8.4.2, open@^8.0.9, open@^8.4.0: version "8.4.2" - resolved "https://registry.npmjs.org/open/-/open-8.4.2.tgz" + resolved "https://registry.yarnpkg.com/open/-/open-8.4.2.tgz#5b5ffe2a8f793dcd2aad73e550cb87b59cb084f9" integrity sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ== dependencies: define-lazy-prop "^2.0.0" @@ -7823,27 +7526,27 @@ open@8.4.2, open@^8.0.9, open@^8.4.0: opentype.js@^1.3.4: version "1.3.4" - resolved "https://registry.npmjs.org/opentype.js/-/opentype.js-1.3.4.tgz" + resolved "https://registry.yarnpkg.com/opentype.js/-/opentype.js-1.3.4.tgz#1c0e72e46288473cc4a4c6a2dc60fd7fe6020d77" integrity sha512-d2JE9RP/6uagpQAVtJoF0pJJA/fgai89Cc50Yp0EJHk+eLp6QQ7gBoblsnubRULNY132I0J1QKMJ+JTbMqz4sw== dependencies: string.prototype.codepointat "^0.2.1" tiny-inflate "^1.0.3" optionator@^0.9.3: - version "0.9.3" - resolved "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz" - integrity sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg== + version "0.9.4" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.4.tgz#7ea1c1a5d91d764fb282139c88fe11e182a3a734" + integrity sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g== dependencies: - "@aashutoshrathi/word-wrap" "^1.2.3" deep-is "^0.1.3" fast-levenshtein "^2.0.6" levn "^0.4.1" prelude-ls "^1.2.1" type-check "^0.4.0" + word-wrap "^1.2.5" ora@5.4.1, ora@^5.4.1: version "5.4.1" - resolved "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz" + resolved "https://registry.yarnpkg.com/ora/-/ora-5.4.1.tgz#1b2678426af4ac4a509008e5e4ac9e9959db9e18" integrity sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ== dependencies: bl "^4.1.0" @@ -7858,7 +7561,7 @@ ora@5.4.1, ora@^5.4.1: os-tmpdir@~1.0.1, os-tmpdir@~1.0.2: version "1.0.2" - resolved "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz" + resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" integrity sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g== p-filter@^3.0.0: @@ -7870,14 +7573,14 @@ p-filter@^3.0.0: p-limit@^2.2.0: version "2.3.0" - resolved "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz" + 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.npmjs.org/p-limit/-/p-limit-3.1.0.tgz" + 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" @@ -7891,14 +7594,14 @@ p-limit@^4.0.0: p-locate@^4.1.0: version "4.1.0" - resolved "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== dependencies: p-limit "^2.2.0" p-locate@^5.0.0: version "5.0.0" - resolved "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz" + 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" @@ -7912,7 +7615,7 @@ p-locate@^6.0.0: p-map@^4.0.0: version "4.0.0" - resolved "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz" + resolved "https://registry.yarnpkg.com/p-map/-/p-map-4.0.0.tgz#bb2f95a5eda2ec168ec9274e06a747c3e2904d2b" integrity sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ== dependencies: aggregate-error "^3.0.0" @@ -7926,7 +7629,7 @@ p-map@^5.1.0: p-retry@^4.5.0: version "4.6.2" - resolved "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz" + resolved "https://registry.yarnpkg.com/p-retry/-/p-retry-4.6.2.tgz#9baae7184057edd4e17231cee04264106e092a16" integrity sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ== dependencies: "@types/retry" "0.12.0" @@ -7934,12 +7637,17 @@ p-retry@^4.5.0: p-try@^2.0.0: version "2.2.0" - resolved "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== +package-json-from-dist@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz#e501cd3094b278495eb4258d4c9f6d5ac3019f00" + integrity sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw== + pacote@15.2.0: version "15.2.0" - resolved "https://registry.npmjs.org/pacote/-/pacote-15.2.0.tgz" + resolved "https://registry.yarnpkg.com/pacote/-/pacote-15.2.0.tgz#0f0dfcc3e60c7b39121b2ac612bf8596e95344d3" integrity sha512-rJVZeIwHTUta23sIZgEIM62WYwbmGbThdbnkt81ravBplQv+HjyroqnLRNH2+sLJHcGZmLRmhPwACqhfTcOmnA== dependencies: "@npmcli/git" "^4.0.0" @@ -7963,19 +7671,19 @@ pacote@15.2.0: pako@^1.0.3, pako@~1.0.2: version "1.0.11" - resolved "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz" + resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf" integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw== parent-module@^1.0.0: version "1.0.1" - resolved "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz" + resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== dependencies: callsites "^3.0.0" -parse-json@^5.0.0, parse-json@^5.2.0: +parse-json@^5.2.0: version "5.2.0" - resolved "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== dependencies: "@babel/code-frame" "^7.0.0" @@ -7985,12 +7693,12 @@ parse-json@^5.0.0, parse-json@^5.2.0: parse-node-version@^1.0.1: version "1.0.1" - resolved "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz" + resolved "https://registry.yarnpkg.com/parse-node-version/-/parse-node-version-1.0.1.tgz#e2b5dbede00e7fa9bc363607f53327e8b073189b" integrity sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA== parse5-html-rewriting-stream@7.0.0: version "7.0.0" - resolved "https://registry.npmjs.org/parse5-html-rewriting-stream/-/parse5-html-rewriting-stream-7.0.0.tgz" + resolved "https://registry.yarnpkg.com/parse5-html-rewriting-stream/-/parse5-html-rewriting-stream-7.0.0.tgz#e376d3e762d2950ccbb6bb59823fc1d7e9fdac36" integrity sha512-mazCyGWkmCRWDI15Zp+UiCqMp/0dgEmkZRvhlsqqKYr4SsVm/TvnSpD9fCvqCA2zoWJcfRym846ejWBBHRiYEg== dependencies: entities "^4.3.0" @@ -7999,7 +7707,7 @@ parse5-html-rewriting-stream@7.0.0: parse5-sax-parser@^7.0.0: version "7.0.0" - resolved "https://registry.npmjs.org/parse5-sax-parser/-/parse5-sax-parser-7.0.0.tgz" + resolved "https://registry.yarnpkg.com/parse5-sax-parser/-/parse5-sax-parser-7.0.0.tgz#4c05064254f0488676aca75fb39ca069ec96dee5" integrity sha512-5A+v2SNsq8T6/mG3ahcz8ZtQ0OUFTatxPbeidoMB7tkJSGDY3tdfl4MHovtLQHkEn5CGxijNWRQHhRQ6IRpXKg== dependencies: parse5 "^7.0.0" @@ -8011,19 +7719,19 @@ parse5@6.0.1: parse5@^7.0.0, parse5@^7.1.2: version "7.1.2" - resolved "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz" + resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.1.2.tgz#0736bebbfd77793823240a23b7fc5e010b7f8e32" integrity sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw== dependencies: entities "^4.4.0" parseurl@~1.3.2, parseurl@~1.3.3: version "1.3.3" - resolved "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== path-exists@^4.0.0: version "4.0.0" - resolved "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== path-exists@^5.0.0: @@ -8033,77 +7741,77 @@ path-exists@^5.0.0: path-is-absolute@^1.0.0: version "1.0.1" - resolved "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== path-is-inside@^1.0.1: version "1.0.2" - resolved "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz" + resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" integrity sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w== path-key@^3.0.0, path-key@^3.1.0: version "3.1.1" - resolved "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== path-parse@^1.0.7: version "1.0.7" - resolved "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== -path-scurry@^1.7.0: - version "1.9.1" - resolved "https://registry.npmjs.org/path-scurry/-/path-scurry-1.9.1.tgz" - integrity sha512-UgmoiySyjFxP6tscZDgWGEAgsW5ok8W3F5CJDnnH2pozwSTGE6eH7vwTotMwATWA2r5xqdkKdxYPkwlJjAI/3g== +path-scurry@^1.11.1: + version "1.11.1" + resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.11.1.tgz#7960a668888594a0720b12a911d1a742ab9f11d2" + integrity sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA== dependencies: - lru-cache "^9.1.1" - minipass "^5.0.0 || ^6.0.0" + lru-cache "^10.2.0" + minipass "^5.0.0 || ^6.0.2 || ^7.0.0" path-to-regexp@0.1.7: version "0.1.7" - resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ== path-type@^4.0.0: version "4.0.0" - resolved "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== performance-now@^2.1.0: version "2.1.0" - resolved "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz" + resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" integrity sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow== -picocolors@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz" - integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== +picocolors@^1.0.0, picocolors@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.1.tgz#a8ad579b571952f0e5d25892de5445bcfe25aaa1" + integrity sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew== picomatch@2.3.1, picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1: version "2.3.1" - resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== pify@^2.0.0: version "2.3.0" - resolved "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz" + resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" integrity sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog== pify@^4.0.1: version "4.0.1" - resolved "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz" + resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231" integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g== pinkie-promise@^2.0.0: version "2.0.1" - resolved "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz" + resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" integrity sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw== dependencies: pinkie "^2.0.0" pinkie@^2.0.0: version "2.0.4" - resolved "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz" + resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" integrity sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg== piscina@4.0.0: @@ -8126,7 +7834,7 @@ pkg-dir@^7.0.0: pngjs@^5.0.0: version "5.0.0" - resolved "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz" + resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-5.0.0.tgz#e79dd2b215767fd9c04561c01236df960bce7fbb" integrity sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw== postcss-loader@7.3.3: @@ -8139,76 +7847,67 @@ postcss-loader@7.3.3: semver "^7.3.8" postcss-modules-extract-imports@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz" - integrity sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw== + version "3.1.0" + resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz#b4497cb85a9c0c4b5aabeb759bb25e8d89f15002" + integrity sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q== postcss-modules-local-by-default@^4.0.3: - version "4.0.3" - resolved "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.3.tgz" - integrity sha512-2/u2zraspoACtrbFRnTijMiQtb4GW4BvatjaG/bCjYQo8kLTdevCUlwuBHx2sCnSyrI3x3qj4ZK1j5LQBgzmwA== + version "4.0.5" + resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.5.tgz#f1b9bd757a8edf4d8556e8d0f4f894260e3df78f" + integrity sha512-6MieY7sIfTK0hYfafw1OMEG+2bg8Q1ocHCpoWLqOKj3JXlKu4G7btkmM/B7lFubYkYWmRSPLZi5chid63ZaZYw== dependencies: icss-utils "^5.0.0" postcss-selector-parser "^6.0.2" postcss-value-parser "^4.1.0" postcss-modules-scope@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz" - integrity sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg== + version "3.2.0" + resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-3.2.0.tgz#a43d28289a169ce2c15c00c4e64c0858e43457d5" + integrity sha512-oq+g1ssrsZOsx9M96c5w8laRmvEu9C3adDSjI8oTcbfkrTE8hx/zfyobUoWIxaKPO8bt6S62kxpw5GqypEw1QQ== dependencies: postcss-selector-parser "^6.0.4" postcss-modules-values@^4.0.0: version "4.0.0" - resolved "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz" + resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz#d7c5e7e68c3bb3c9b27cbf48ca0bb3ffb4602c9c" integrity sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ== dependencies: icss-utils "^5.0.0" postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4: - version "6.0.13" - resolved "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz" - integrity sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ== + version "6.1.1" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.1.1.tgz#5be94b277b8955904476a2400260002ce6c56e38" + integrity sha512-b4dlw/9V8A71rLIDsSwVmak9z2DuBUB7CA1/wSdelNEzqsjoSPeADTWNO09lpH49Diy3/JIZ2bSPB1dI3LJCHg== dependencies: cssesc "^3.0.0" util-deprecate "^1.0.2" postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0: version "4.2.0" - resolved "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz" + resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== -postcss@8.4.27, postcss@^8.4.26: - version "8.4.27" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.27.tgz#234d7e4b72e34ba5a92c29636734349e0d9c3057" - integrity sha512-gY/ACJtJPSmUFPDCHtX78+01fHa64FaU4zaaWfuh1MhGJISufJAH4cun6k/8fwsHYeK4UQmENQK+tRLCFJE8JQ== +postcss@8.4.31: + version "8.4.31" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.31.tgz#92b451050a9f914da6755af352bdc0192508656d" + integrity sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ== dependencies: nanoid "^3.3.6" picocolors "^1.0.0" source-map-js "^1.0.2" -postcss@^8.2.14, postcss@^8.4.21: - version "8.4.23" - resolved "https://registry.npmjs.org/postcss/-/postcss-8.4.23.tgz" - integrity sha512-bQ3qMcpF6A/YjR55xtoTr0jGOlnPOKAIMdOWiv0EIT6HVPEaJiJB4NLljSbiHoC2RX7DN5Uvjtpbg1NPdwv1oA== +postcss@^8.2.14, postcss@^8.4.21, postcss@^8.4.23, postcss@^8.4.27: + version "8.4.40" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.40.tgz#eb81f2a4dd7668ed869a6db25999e02e9ad909d8" + integrity sha512-YF2kKIUzAofPMpfH6hOi2cGnv/HrUlfucspc7pDyvv7kGdqXrfj8SCl/t8owkEgKEuu8ZcRjSOxFxVLqwChZ2Q== dependencies: - nanoid "^3.3.6" - picocolors "^1.0.0" - source-map-js "^1.0.2" - -postcss@^8.4.23: - version "8.4.25" - resolved "https://registry.npmjs.org/postcss/-/postcss-8.4.25.tgz" - integrity sha512-7taJ/8t2av0Z+sQEvNzCkpDynl0tX3uJMCODi6nT3PfASC7dYCWV9aQ+uiCf+KBD4SEFcu+GvJdGdwzQ6OSjCw== - dependencies: - nanoid "^3.3.6" - picocolors "^1.0.0" - source-map-js "^1.0.2" + nanoid "^3.3.7" + picocolors "^1.0.1" + source-map-js "^1.2.0" prelude-ls@^1.2.1: version "1.2.1" - resolved "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== prettier-plugin-organize-imports@^3.2.4: @@ -8217,23 +7916,23 @@ prettier-plugin-organize-imports@^3.2.4: integrity sha512-6m8WBhIp0dfwu0SkgfOxJqh+HpdyfqSSLfKKRZSFbDuEQXDDndb8fTpRWkUrX/uBenkex3MgnVk0J3b3Y5byog== prettier@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.1.1.tgz#6ba9f23165d690b6cbdaa88cb0807278f7019848" - integrity sha512-22UbSzg8luF4UuZtzgiUOfcGM8s4tjBv6dJRT7j275NXsy2jb4aJa4NNveul5x4eqlF1wuhuR2RElK71RvmVaw== + version "3.3.3" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.3.3.tgz#30c54fe0be0d8d12e6ae61dbb10109ea00d53105" + integrity sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew== pretty-bytes@^5.3.0: version "5.6.0" - resolved "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz" + resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb" integrity sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg== proc-log@^3.0.0: version "3.0.0" - resolved "https://registry.npmjs.org/proc-log/-/proc-log-3.0.0.tgz" + resolved "https://registry.yarnpkg.com/proc-log/-/proc-log-3.0.0.tgz#fb05ef83ccd64fd7b20bbe9c8c1070fc08338dd8" integrity sha512-++Vn7NS4Xf9NacaU9Xq3URUuqZETPsf8L4j5/ckhaRYsfPeRyzGw+iDjFhV/Jr3uNmTvvddEJFWh5R1gRgUH8A== process-nextick-args@~2.0.0: version "2.0.1" - resolved "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== process@^0.11.10: @@ -8243,21 +7942,21 @@ process@^0.11.10: promise-inflight@^1.0.1: version "1.0.1" - resolved "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz" + resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3" integrity sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g== promise-retry@^2.0.1: version "2.0.1" - resolved "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz" + resolved "https://registry.yarnpkg.com/promise-retry/-/promise-retry-2.0.1.tgz#ff747a13620ab57ba688f5fc67855410c370da22" integrity sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g== dependencies: err-code "^2.0.2" retry "^0.12.0" -protobufjs@^7.0.0, protobufjs@^7.2.4: - version "7.2.5" - resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-7.2.5.tgz#45d5c57387a6d29a17aab6846dcc283f9b8e7f2d" - integrity sha512-gGXRSXvxQ7UiPgfw8gevrfRWcTlSbOFg+p/N+JVJEK5VhueL2miT6qTymqAmjr1Q5WbOCyJbyrk6JfWKwlFn6A== +protobufjs@^7.0.0, protobufjs@^7.2.5: + version "7.3.2" + resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-7.3.2.tgz#60f3b7624968868f6f739430cfbc8c9370e26df4" + integrity sha512-RXyHaACeqXeqAKGLDl68rQKbmObRsTIn4TYVUUug1KfS47YWCo5MacGITEryugIgZqORCvJWEk4l449POg5Txg== dependencies: "@protobufjs/aspromise" "^1.1.2" "@protobufjs/base64" "^1.1.2" @@ -8274,7 +7973,7 @@ protobufjs@^7.0.0, protobufjs@^7.2.4: protractor@~7.0.0: version "7.0.0" - resolved "https://registry.npmjs.org/protractor/-/protractor-7.0.0.tgz" + resolved "https://registry.yarnpkg.com/protractor/-/protractor-7.0.0.tgz#c3e263608bd72e2c2dc802b11a772711a4792d03" integrity sha512-UqkFjivi4GcvUQYzqGYNe0mLzfn5jiLmO8w9nMhQoJRLhy2grJonpga2IWhI6yJO30LibWXJJtA4MOIZD2GgZw== dependencies: "@types/q" "^0.0.32" @@ -8295,7 +7994,7 @@ protractor@~7.0.0: proxy-addr@~2.0.7: version "2.0.7" - resolved "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== dependencies: forwarded "0.2.0" @@ -8303,12 +8002,12 @@ proxy-addr@~2.0.7: proxy-from-env@^1.1.0: version "1.1.0" - resolved "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== prr@~1.0.1: version "1.0.1" - resolved "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz" + resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476" integrity sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw== psl@^1.1.28, psl@^1.1.33: @@ -8316,30 +8015,35 @@ psl@^1.1.28, psl@^1.1.33: resolved "https://registry.yarnpkg.com/psl/-/psl-1.9.0.tgz#d0df2a137f00794565fcaf3b2c00cd09f8d5a5a7" integrity sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag== +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== + punycode@^2.1.0, punycode@^2.1.1: - version "2.3.0" - resolved "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz" - integrity sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA== + version "2.3.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" + integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== q@1.4.1: version "1.4.1" - resolved "https://registry.npmjs.org/q/-/q-1.4.1.tgz" + resolved "https://registry.yarnpkg.com/q/-/q-1.4.1.tgz#55705bcd93c5f3673530c2c2cbc0c2b3addc286e" integrity sha512-/CdEdaw49VZVmyIDGUQKDDT53c7qBkO6g5CefWz91Ae+l4+cRtcDYwMTXh6me4O8TMldeGHG3N2Bl84V78Ywbg== q@^1.4.1: version "1.5.1" - resolved "https://registry.npmjs.org/q/-/q-1.5.1.tgz" + resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" integrity sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw== qjobs@^1.2.0: version "1.2.0" - resolved "https://registry.npmjs.org/qjobs/-/qjobs-1.2.0.tgz" + resolved "https://registry.yarnpkg.com/qjobs/-/qjobs-1.2.0.tgz#c45e9c61800bd087ef88d7e256423bdd49e5d071" integrity sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg== -qrcode@1.5.1: - version "1.5.1" - resolved "https://registry.npmjs.org/qrcode/-/qrcode-1.5.1.tgz" - integrity sha512-nS8NJ1Z3md8uTjKtP+SGGhfqmTCs5flU/xR623oI0JX+Wepz9R8UrRVCTBTJm3qGw3rH6jJ6MUHjkDx15cxSSg== +qrcode@1.5.3: + version "1.5.3" + resolved "https://registry.yarnpkg.com/qrcode/-/qrcode-1.5.3.tgz#03afa80912c0dccf12bc93f615a535aad1066170" + integrity sha512-puyri6ApkEHYiVl4CFzo1tDkAZ+ATcnbJrJ6RiBM1Fhctdn/ix9MTE3hRph33omisEbC/2fcfemsseiKgBPKZg== dependencies: dijkstrajs "^1.0.1" encode-utf8 "^1.0.3" @@ -8348,14 +8052,14 @@ qrcode@1.5.1: qs@6.11.0: version "6.11.0" - resolved "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a" integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== dependencies: side-channel "^1.0.4" qs@~6.5.2: version "6.5.3" - resolved "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.3.tgz#3aeeffc91967ef6e35c0e488ef46fb296ab76aad" integrity sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA== querystringify@^2.1.1: @@ -8365,34 +8069,24 @@ querystringify@^2.1.1: queue-microtask@^1.2.2: version "1.2.3" - resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz" + resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== randombytes@^2.1.0: version "2.1.0" - resolved "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz" + resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== dependencies: safe-buffer "^5.1.0" range-parser@^1.2.1, range-parser@~1.2.1: version "1.2.1" - resolved "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== -raw-body@2.5.1: - version "2.5.1" - resolved "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz" - integrity sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig== - dependencies: - bytes "3.1.2" - http-errors "2.0.0" - iconv-lite "0.4.24" - unpipe "1.0.0" - raw-body@2.5.2: version "2.5.2" - resolved "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a" integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA== dependencies: bytes "3.1.2" @@ -8402,23 +8096,23 @@ raw-body@2.5.2: read-package-json-fast@^3.0.0: version "3.0.2" - resolved "https://registry.npmjs.org/read-package-json-fast/-/read-package-json-fast-3.0.2.tgz" + resolved "https://registry.yarnpkg.com/read-package-json-fast/-/read-package-json-fast-3.0.2.tgz#394908a9725dc7a5f14e70c8e7556dff1d2b1049" integrity sha512-0J+Msgym3vrLOUB3hzQCuZHII0xkNGCtz/HJH9xZshwv9DbDwkw1KaE3gx/e2J5rpEY5rtOy6cyhKOPrkP7FZw== dependencies: json-parse-even-better-errors "^3.0.0" npm-normalize-package-bin "^3.0.0" read-package-json@^6.0.0: - version "6.0.3" - resolved "https://registry.npmjs.org/read-package-json/-/read-package-json-6.0.3.tgz" - integrity sha512-4QbpReW4kxFgeBQ0vPAqh2y8sXEB3D4t3jsXbJKIhBiF80KT6XRo45reqwtftju5J6ru1ax06A2Gb/wM1qCOEQ== + version "6.0.4" + resolved "https://registry.yarnpkg.com/read-package-json/-/read-package-json-6.0.4.tgz#90318824ec456c287437ea79595f4c2854708836" + integrity sha512-AEtWXYfopBj2z5N5PbkAOeNHRPUg5q+Nen7QLxV8M2zJq1ym6/lCz3fYNTCXe19puu2d06jfHhrP7v/S2PtMMw== dependencies: glob "^10.2.2" json-parse-even-better-errors "^3.0.0" normalize-package-data "^5.0.0" npm-normalize-package-bin "^3.0.0" -read-pkg-up@^9.0.0: +read-pkg-up@^9.1.0: version "9.1.0" resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-9.1.0.tgz#38ca48e0bc6c6b260464b14aad9bcd4e5b1fbdc3" integrity sha512-vaMRR1AC1nrd5CQM0PhlRsO5oc2AAigqr7cCrZ/MW/Rsaflz4RlgzkpL4qoU/z1F6wrbd85iFv1OQj/y5RdGvg== @@ -8439,7 +8133,7 @@ read-pkg@^7.1.0: readable-stream@^2.0.1, readable-stream@~2.3.6: version "2.3.8" - resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== dependencies: core-util-is "~1.0.0" @@ -8452,7 +8146,7 @@ readable-stream@^2.0.1, readable-stream@~2.3.6: readable-stream@^3.0.6, readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.6.0: version "3.6.2" - resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== dependencies: inherits "^2.0.3" @@ -8461,48 +8155,53 @@ readable-stream@^3.0.6, readable-stream@^3.1.1, readable-stream@^3.4.0, readable readdirp@~3.6.0: version "3.6.0" - resolved "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== dependencies: picomatch "^2.2.1" reflect-metadata@^0.1.2: - version "0.1.13" - resolved "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz" - integrity sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg== + version "0.1.14" + resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.14.tgz#24cf721fe60677146bb77eeb0e1f9dece3d65859" + integrity sha512-ZhYeb6nRaXCfhnndflDK8qI6ZQ/YcWZCISRAWICW9XYqMUwjZM9Z0DveWX/ABN01oxSHwVxKQmxeYZSsm0jh5A== regenerate-unicode-properties@^10.1.0: - version "10.1.0" - resolved "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.0.tgz" - integrity sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ== + version "10.1.1" + resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.1.tgz#6b0e05489d9076b04c436f318d9b067bba459480" + integrity sha512-X007RyZLsCJVVrjgEFVpLUTZwyOZk3oiL75ZcuYjlIWd6rNJtOjkBwQc5AsRrpbKVkxN6sklw/k/9m2jJYOf8Q== dependencies: regenerate "^1.4.2" regenerate@^1.4.2: version "1.4.2" - resolved "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz" + resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a" integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A== regenerator-runtime@^0.13.11: version "0.13.11" - resolved "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9" integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg== -regenerator-transform@^0.15.1: - version "0.15.1" - resolved "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.1.tgz" - integrity sha512-knzmNAcuyxV+gQCufkYcvOqX/qIIfHLv0u5x79kRxuGojfYVky1f15TzZEu2Avte8QGepvUNTnLskf8E6X6Vyg== +regenerator-runtime@^0.14.0: + version "0.14.1" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f" + integrity sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw== + +regenerator-transform@^0.15.2: + version "0.15.2" + resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.15.2.tgz#5bbae58b522098ebdf09bca2f83838929001c7a4" + integrity sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg== dependencies: "@babel/runtime" "^7.8.4" regex-parser@^2.2.11: - version "2.2.11" - resolved "https://registry.npmjs.org/regex-parser/-/regex-parser-2.2.11.tgz" - integrity sha512-jbD/FT0+9MBU2XAZluI7w2OBs1RBi6p9M83nkoZayQXXU9e8Robt69FcZc7wU4eJD/YFTjn1JdCk3rbMJajz8Q== + version "2.3.0" + resolved "https://registry.yarnpkg.com/regex-parser/-/regex-parser-2.3.0.tgz#4bb61461b1a19b8b913f3960364bb57887f920ee" + integrity sha512-TVILVSz2jY5D47F4mA4MppkBrafEaiUWJO/TcZHEIuI13AqoZMkK1WMA4Om1YkYbTx+9Ki1/tSUXbceyr9saRg== regexpu-core@^5.3.1: version "5.3.2" - resolved "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.3.2.tgz" + resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-5.3.2.tgz#11a2b06884f3527aec3e93dbbf4a3b958a95546b" integrity sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ== dependencies: "@babel/regjsgen" "^0.8.0" @@ -8514,14 +8213,14 @@ regexpu-core@^5.3.1: regjsparser@^0.9.1: version "0.9.1" - resolved "https://registry.npmjs.org/regjsparser/-/regjsparser-0.9.1.tgz" + resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.9.1.tgz#272d05aa10c7c1f67095b1ff0addae8442fc5709" integrity sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ== dependencies: jsesc "~0.5.0" request@^2.87.0: version "2.88.2" - resolved "https://registry.npmjs.org/request/-/request-2.88.2.tgz" + resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3" integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw== dependencies: aws-sign2 "~0.7.0" @@ -8547,37 +8246,37 @@ request@^2.87.0: require-directory@^2.1.1: version "2.1.1" - resolved "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== require-from-string@^2.0.2: version "2.0.2" - resolved "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz" + resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== require-main-filename@^2.0.0: version "2.0.0" - resolved "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz" + resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== requires-port@^1.0.0: version "1.0.0" - resolved "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz" + resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ== resolve-from@^4.0.0: version "4.0.0" - resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== resolve-from@^5.0.0: version "5.0.0" - resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== resolve-url-loader@5.0.0: version "5.0.0" - resolved "https://registry.npmjs.org/resolve-url-loader/-/resolve-url-loader-5.0.0.tgz" + resolved "https://registry.yarnpkg.com/resolve-url-loader/-/resolve-url-loader-5.0.0.tgz#ee3142fb1f1e0d9db9524d539cfa166e9314f795" integrity sha512-uZtduh8/8srhBoMx//5bwqjQ+rfYOUq8zC9NrMUGtjBiGTtFJM42s58/36+hTqeqINcnYe08Nj3LkK9lW4N8Xg== dependencies: adjust-sourcemap-loader "^4.0.0" @@ -8586,18 +8285,27 @@ resolve-url-loader@5.0.0: postcss "^8.2.14" source-map "0.6.1" -resolve@1.22.2, resolve@^1.14.2: +resolve@1.22.2: version "1.22.2" - resolved "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.2.tgz#0ed0943d4e301867955766c9f3e1ae6d01c6845f" integrity sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g== dependencies: is-core-module "^2.11.0" path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" +resolve@^1.14.2: + version "1.22.8" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" + integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== + dependencies: + is-core-module "^2.13.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + restore-cursor@^3.1.0: version "3.1.0" - resolved "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz" + resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e" integrity sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA== dependencies: onetime "^5.1.0" @@ -8605,89 +8313,89 @@ restore-cursor@^3.1.0: retry@^0.12.0: version "0.12.0" - resolved "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz" + resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b" integrity sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow== retry@^0.13.1: version "0.13.1" - resolved "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz" + resolved "https://registry.yarnpkg.com/retry/-/retry-0.13.1.tgz#185b1587acf67919d63b357349e03537b2484658" integrity sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg== reusify@^1.0.4: version "1.0.4" - resolved "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz" + resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== rfdc@^1.3.0: - version "1.3.0" - resolved "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz" - integrity sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA== + version "1.4.1" + resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.4.1.tgz#778f76c4fb731d93414e8f925fbecf64cce7f6ca" + integrity sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA== rimraf@^2.2.8, rimraf@^2.5.2, rimraf@^2.5.4, rimraf@^2.6.3: version "2.7.1" - resolved "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== dependencies: glob "^7.1.3" rimraf@^3.0.0, rimraf@^3.0.2: version "3.0.2" - resolved "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== dependencies: glob "^7.1.3" -rollup@^3.25.2: - version "3.28.0" - resolved "https://registry.yarnpkg.com/rollup/-/rollup-3.28.0.tgz#a3c70004b01934760c0cb8df717c7a1d932389a2" - integrity sha512-d7zhvo1OUY2SXSM6pfNjgD5+d0Nz87CUp4mt8l/GgVP3oBsPwzNvSzyu1me6BSG9JIgWNTVcafIXBIyM8yQ3yw== +rollup@^3.27.1: + version "3.29.4" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-3.29.4.tgz#4d70c0f9834146df8705bfb69a9a19c9e1109981" + integrity sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw== optionalDependencies: fsevents "~2.3.2" run-async@^2.4.0: version "2.4.1" - resolved "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz" + resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455" integrity sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ== run-parallel@^1.1.9: version "1.2.0" - resolved "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz" + resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== dependencies: queue-microtask "^1.2.2" rxjs@7.8.1, rxjs@^7.5.5, rxjs@~7.8.0: version "7.8.1" - resolved "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.1.tgz#6f6f3d99ea8044291efd92e7c7fcf562c4057543" integrity sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg== dependencies: tslib "^2.1.0" rxjs@^6.5.3: version "6.6.7" - resolved "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.7.tgz#90ac018acabf491bf65044235d5863c4dab804c9" integrity sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ== dependencies: tslib "^1.9.0" safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.2" - resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz" + 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.2, safe-buffer@~5.2.0: version "5.2.1" - resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== "safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: version "2.1.2" - resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== safevalues@^0.3.4: version "0.3.4" - resolved "https://registry.npmjs.org/safevalues/-/safevalues-0.3.4.tgz" + resolved "https://registry.yarnpkg.com/safevalues/-/safevalues-0.3.4.tgz#82e846a02b6956d7d40bf9f41e92e13fce0186db" integrity sha512-LRneZZRXNgjzwG4bDQdOTSbze3fHm1EAKN/8bePxnlEZiBmkYEDggaHbuvHI9/hoqHbGfsEA7tWS9GhYHZBBsw== sass-loader@13.3.2: @@ -8708,15 +8416,15 @@ sass@1.64.1: saucelabs@^1.5.0: version "1.5.0" - resolved "https://registry.npmjs.org/saucelabs/-/saucelabs-1.5.0.tgz" + resolved "https://registry.yarnpkg.com/saucelabs/-/saucelabs-1.5.0.tgz#9405a73c360d449b232839919a86c396d379fd9d" integrity sha512-jlX3FGdWvYf4Q3LFfFWS1QvPg3IGCGWxIc8QBFdPTbpTJnt/v17FHXYVAn7C8sHf1yUXo2c7yIM0isDryfYtHQ== dependencies: https-proxy-agent "^2.2.1" sax@>=0.6.0, sax@^1.2.4: - version "1.2.4" - resolved "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz" - integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== + version "1.4.1" + resolved "https://registry.yarnpkg.com/sax/-/sax-1.4.1.tgz#44cc8988377f126304d3b3fc1010c733b929ef0f" + integrity sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg== saxes@^5.0.1: version "5.0.1" @@ -8725,16 +8433,7 @@ saxes@^5.0.1: dependencies: xmlchars "^2.2.0" -schema-utils@^3.1.1: - version "3.1.2" - resolved "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.2.tgz" - integrity sha512-pvjEHOgWc9OWA/f/DE3ohBWTD6EleVLf7iFUkoSwAxttdBhB9QUebQgxER2kWueOvRJXPHNnyrvvh9eZINB8Eg== - dependencies: - "@types/json-schema" "^7.0.8" - ajv "^6.12.5" - ajv-keywords "^3.5.2" - -schema-utils@^3.2.0: +schema-utils@^3.1.1, schema-utils@^3.2.0: version "3.3.0" resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.3.0.tgz#f50a88877c3c01652a15b622ae9e9795df7a60fe" integrity sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg== @@ -8744,9 +8443,9 @@ schema-utils@^3.2.0: ajv-keywords "^3.5.2" schema-utils@^4.0.0: - version "4.0.1" - resolved "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.1.tgz" - integrity sha512-lELhBAAly9NowEsX0yZBlw9ahZG+sK/1RJ21EpzdYHKEs13Vku3LJ+MIPhh4sMs0oCCeufZQEQbMekiA4vuVIQ== + version "4.2.0" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-4.2.0.tgz#70d7c93e153a273a805801882ebd3bff20d89c8b" + integrity sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw== dependencies: "@types/json-schema" "^7.0.9" ajv "^8.9.0" @@ -8755,12 +8454,12 @@ schema-utils@^4.0.0: select-hose@^2.0.0: version "2.0.0" - resolved "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz" + resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" integrity sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg== selenium-webdriver@3.6.0, selenium-webdriver@^3.0.1: version "3.6.0" - resolved "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-3.6.0.tgz" + resolved "https://registry.yarnpkg.com/selenium-webdriver/-/selenium-webdriver-3.6.0.tgz#2ba87a1662c020b8988c981ae62cb2a01298eafc" integrity sha512-WH7Aldse+2P5bbFBO4Gle/nuQOdVwpHMTL6raL3uuBj/vPG07k6uzt3aiahu352ONBr5xXh0hDlM3LhtXPOC4Q== dependencies: jszip "^3.1.3" @@ -8769,15 +8468,16 @@ selenium-webdriver@3.6.0, selenium-webdriver@^3.0.1: xml2js "^0.4.17" selfsigned@^2.1.1: - version "2.1.1" - resolved "https://registry.npmjs.org/selfsigned/-/selfsigned-2.1.1.tgz" - integrity sha512-GSL3aowiF7wa/WtSFwnUrludWFoNhftq8bUkH9pkzjpN2XSPOAYEgg6e0sS9s0rZwgJzJiQRPU18A6clnoW5wQ== + version "2.4.1" + resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-2.4.1.tgz#560d90565442a3ed35b674034cec4e95dceb4ae0" + integrity sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q== dependencies: + "@types/node-forge" "^1.3.0" node-forge "^1" semver-dsl@^1.0.1: version "1.0.1" - resolved "https://registry.npmjs.org/semver-dsl/-/semver-dsl-1.0.1.tgz" + resolved "https://registry.yarnpkg.com/semver-dsl/-/semver-dsl-1.0.1.tgz#d3678de5555e8a61f629eed025366ae5f27340a0" integrity sha512-e8BOaTo007E3dMuQQTnPdalbKTABKNS7UxoBIDnwOqRa+QwMrCPjynB8zAlPF6xlqUfdLPPLIJ13hJNmhtq8Ng== dependencies: semver "^5.3.0" @@ -8789,7 +8489,7 @@ semver@7.5.3: dependencies: lru-cache "^6.0.0" -semver@7.5.4, semver@^7.0.0, semver@^7.1.1, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8: +semver@7.5.4: version "7.5.4" resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== @@ -8801,21 +8501,19 @@ semver@^5.3.0, semver@^5.6.0: resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8" integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g== -semver@^6.0.0, semver@^6.3.0, semver@^6.3.1: +semver@^6.3.0, semver@^6.3.1: version "6.3.1" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== -semver@^7.3.4: - version "7.6.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.0.tgz#1a46a4db4bffcccd97b743b5005c8325f23d4e2d" - integrity sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg== - dependencies: - lru-cache "^6.0.0" +semver@^7.0.0, semver@^7.1.1, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8, semver@^7.5.3: + version "7.6.3" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143" + integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== send@0.18.0: version "0.18.0" - resolved "https://registry.npmjs.org/send/-/send-0.18.0.tgz" + resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" integrity sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg== dependencies: debug "2.6.9" @@ -8833,15 +8531,15 @@ send@0.18.0: statuses "2.0.1" serialize-javascript@^6.0.0, serialize-javascript@^6.0.1: - version "6.0.1" - resolved "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz" - integrity sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w== + 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-index@^1.9.1: version "1.9.1" - resolved "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz" + resolved "https://registry.yarnpkg.com/serve-index/-/serve-index-1.9.1.tgz#d3768d69b1e7d82e5ce050fff5b453bea12a9239" integrity sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw== dependencies: accepts "~1.3.4" @@ -8854,7 +8552,7 @@ serve-index@^1.9.1: serve-static@1.15.0: version "1.15.0" - resolved "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.15.0.tgz#faaef08cffe0a1a62f60cad0c4e513cff0ac9540" integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g== dependencies: encodeurl "~1.0.2" @@ -8864,99 +8562,115 @@ serve-static@1.15.0: set-blocking@^2.0.0: version "2.0.0" - resolved "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz" + resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw== +set-function-length@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" + integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== + dependencies: + define-data-property "^1.1.4" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + gopd "^1.0.1" + has-property-descriptors "^1.0.2" + setimmediate@^1.0.5: version "1.0.5" - resolved "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz" + 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.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" integrity sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ== setprototypeof@1.2.0: version "1.2.0" - resolved "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== shallow-clone@^3.0.0: version "3.0.1" - resolved "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz" + resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3" integrity sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA== dependencies: kind-of "^6.0.2" shebang-command@^2.0.0: version "2.0.0" - resolved "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== dependencies: shebang-regex "^3.0.0" shebang-regex@^3.0.0: version "3.0.0" - resolved "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz" + 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: version "1.8.1" - resolved "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz" + resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.1.tgz#6dbf4db75515ad5bac63b4f1894c3a154c766680" integrity sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA== side-channel@^1.0.4: - version "1.0.4" - resolved "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz" - integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw== + 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== dependencies: - call-bind "^1.0.0" - get-intrinsic "^1.0.2" - object-inspect "^1.9.0" + call-bind "^1.0.7" + es-errors "^1.3.0" + get-intrinsic "^1.2.4" + object-inspect "^1.13.1" signal-exit@^3.0.2, signal-exit@^3.0.3, signal-exit@^3.0.7: version "3.0.7" - resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== signal-exit@^4.0.1: - version "4.0.2" - resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-4.0.2.tgz" - integrity sha512-MY2/qGx4enyjprQnFaZsHib3Yadh3IXyV2C321GY0pjGfVBu4un0uDJkwgdxqO+Rdx8JMT8IfJIRwbYVz3Ob3Q== + version "4.1.0" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" + integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== sigstore@^1.3.0: - version "1.5.2" - resolved "https://registry.npmjs.org/sigstore/-/sigstore-1.5.2.tgz" - integrity sha512-X95v6xAAooVpn7PaB94TDmFeSO5SBfCtB1R23fvzr36WTfjtkiiyOeei979nbTjc8nzh6FSLeltQZuODsm1EjQ== + version "1.9.0" + resolved "https://registry.yarnpkg.com/sigstore/-/sigstore-1.9.0.tgz#1e7ad8933aa99b75c6898ddd0eeebc3eb0d59875" + integrity sha512-0Zjz0oe37d08VeOtBIuB6cRriqXse2e8w+7yIy2XSXjshRKxbc2KkhXjL229jXSxEm7UbcjS76wcJDGQddVI9A== dependencies: - "@sigstore/protobuf-specs" "^0.1.0" + "@sigstore/bundle" "^1.1.0" + "@sigstore/protobuf-specs" "^0.2.0" + "@sigstore/sign" "^1.0.0" + "@sigstore/tuf" "^1.0.3" make-fetch-happen "^11.0.1" - tuf-js "^1.1.3" slash@^3.0.0: version "3.0.0" - resolved "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz" + resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== slash@^4.0.0: version "4.0.0" - resolved "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz" + resolved "https://registry.yarnpkg.com/slash/-/slash-4.0.0.tgz#2422372176c4c6c5addb5e2ada885af984b396a7" integrity sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew== smart-buffer@^4.2.0: version "4.2.0" - resolved "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz" + resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.2.0.tgz#6e1d71fa4f18c05f7d0ff216dd16a481d0e8d9ae" integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg== socket.io-adapter@~2.5.2: - version "2.5.2" - resolved "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.2.tgz" - integrity sha512-87C3LO/NOMc+eMcpcxUBebGjkpMDkNBS9tf7KJqcDsmL936EChtVva71Dw2q4tQcuVC+hAUy4an2NO/sYXmwRA== + version "2.5.5" + resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz#c7a1f9c703d7756844751b6ff9abfc1780664082" + integrity sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg== dependencies: - ws "~8.11.0" + debug "~4.3.4" + ws "~8.17.1" -socket.io-parser@~4.2.1: +socket.io-parser@~4.2.4: version "4.2.4" resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.2.4.tgz#c806966cf7270601e47469ddeec30fbdfda44c83" integrity sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew== @@ -8964,21 +8678,22 @@ socket.io-parser@~4.2.1: "@socket.io/component-emitter" "~3.1.0" debug "~4.3.1" -socket.io@^4.4.1: - version "4.6.1" - resolved "https://registry.npmjs.org/socket.io/-/socket.io-4.6.1.tgz" - integrity sha512-KMcaAi4l/8+xEjkRICl6ak8ySoxsYG+gG6/XfRCPJPQ/haCRIJBTL4wIl8YCsmtaBovcAXGLOShyVWQ/FG8GZA== +socket.io@^4.7.2: + version "4.7.5" + resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-4.7.5.tgz#56eb2d976aef9d1445f373a62d781a41c7add8f8" + integrity sha512-DmeAkF6cwM9jSfmp6Dr/5/mfMwb5Z5qRrSXLpo3Fq5SqyU8CMF15jIN4ZhfSwu35ksM1qmHZDQ/DK5XTccSTvA== dependencies: accepts "~1.3.4" base64id "~2.0.0" + cors "~2.8.5" debug "~4.3.2" - engine.io "~6.4.1" + engine.io "~6.5.2" socket.io-adapter "~2.5.2" - socket.io-parser "~4.2.1" + socket.io-parser "~4.2.4" sockjs@^0.3.24: version "0.3.24" - resolved "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz" + resolved "https://registry.yarnpkg.com/sockjs/-/sockjs-0.3.24.tgz#c9bc8995f33a111bea0395ec30aa3206bdb5ccce" integrity sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ== dependencies: faye-websocket "^0.11.3" @@ -8987,7 +8702,7 @@ sockjs@^0.3.24: socks-proxy-agent@^7.0.0: version "7.0.0" - resolved "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-7.0.0.tgz" + resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-7.0.0.tgz#dc069ecf34436621acb41e3efa66ca1b5fed15b6" integrity sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww== dependencies: agent-base "^6.0.2" @@ -8995,21 +8710,21 @@ socks-proxy-agent@^7.0.0: socks "^2.6.2" socks@^2.6.2: - version "2.7.1" - resolved "https://registry.npmjs.org/socks/-/socks-2.7.1.tgz" - integrity sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ== + version "2.8.3" + resolved "https://registry.yarnpkg.com/socks/-/socks-2.8.3.tgz#1ebd0f09c52ba95a09750afe3f3f9f724a800cb5" + integrity sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw== dependencies: - ip "^2.0.0" + ip-address "^9.0.5" smart-buffer "^4.2.0" -"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz" - integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== +"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.2, source-map-js@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.0.tgz#16b809c162517b5b8c3e7dcd315a2a5c2612b2af" + integrity sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg== source-map-loader@4.0.1: version "4.0.1" - resolved "https://registry.npmjs.org/source-map-loader/-/source-map-loader-4.0.1.tgz" + resolved "https://registry.yarnpkg.com/source-map-loader/-/source-map-loader-4.0.1.tgz#72f00d05f5d1f90f80974eda781cbd7107c125f2" integrity sha512-oqXpzDIByKONVY8g1NUPOTQhe0UTU5bWUl32GSkqK2LjJj0HmwTMVKxcUip0RgAYhY1mqgOxjbQM48a0mmeNfA== dependencies: abab "^2.0.6" @@ -9018,7 +8733,7 @@ source-map-loader@4.0.1: source-map-support@0.5.21, source-map-support@^0.5.5, source-map-support@~0.5.20: version "0.5.21" - resolved "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== dependencies: buffer-from "^1.0.0" @@ -9026,55 +8741,55 @@ source-map-support@0.5.21, source-map-support@^0.5.5, source-map-support@~0.5.20 source-map-support@~0.4.0: version "0.4.18" - resolved "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.18.tgz" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.4.18.tgz#0286a6de8be42641338594e97ccea75f0a2c585f" integrity sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA== dependencies: source-map "^0.5.6" source-map@0.6.1, source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0, source-map@~0.6.1: version "0.6.1" - resolved "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== source-map@0.7.4: version "0.7.4" - resolved "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.4.tgz#a9bbe705c9d8846f4e08ff6765acf0f1b0898656" integrity sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA== source-map@^0.5.6, source-map@^0.5.7: version "0.5.7" - resolved "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" integrity sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ== spdx-correct@^3.0.0: version "3.2.0" - resolved "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz" + resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.2.0.tgz#4f5ab0668f0059e34f9c00dce331784a12de4e9c" integrity sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA== dependencies: spdx-expression-parse "^3.0.0" spdx-license-ids "^3.0.0" spdx-exceptions@^2.1.0: - version "2.3.0" - resolved "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz" - integrity sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A== + version "2.5.0" + resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz#5d607d27fc806f66d7b64a766650fa890f04ed66" + integrity sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w== spdx-expression-parse@^3.0.0: version "3.0.1" - resolved "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz" + resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz#cf70f50482eefdc98e3ce0a6833e4a53ceeba679" integrity sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q== dependencies: spdx-exceptions "^2.1.0" spdx-license-ids "^3.0.0" spdx-license-ids@^3.0.0: - version "3.0.13" - resolved "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.13.tgz" - integrity sha512-XkD+zwiqXHikFZm4AX/7JSCXA98U5Db4AFd5XUg/+9UNtnH75+Z9KxtpYiJZx36mUDVOwH83pl7yvCer6ewM3w== + version "3.0.18" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.18.tgz#22aa922dcf2f2885a6494a261f2d8b75345d0326" + integrity sha512-xxRs31BqRYHwiMzudOrpSiHtZ8i/GeionCBDSilhYRj+9gIcI8wCZTlXZKu9vZIVqViP3dcp9qE5G6AlIaD+TQ== spdy-transport@^3.0.0: version "3.0.0" - resolved "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz" + resolved "https://registry.yarnpkg.com/spdy-transport/-/spdy-transport-3.0.0.tgz#00d4863a6400ad75df93361a1608605e5dcdcf31" integrity sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw== dependencies: debug "^4.1.0" @@ -9086,7 +8801,7 @@ spdy-transport@^3.0.0: spdy@^4.0.2: version "4.0.2" - resolved "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz" + resolved "https://registry.yarnpkg.com/spdy/-/spdy-4.0.2.tgz#b74f466203a3eda452c02492b91fb9e84a27677b" integrity sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA== dependencies: debug "^4.1.0" @@ -9095,20 +8810,20 @@ spdy@^4.0.2: select-hose "^2.0.0" spdy-transport "^3.0.0" -sprintf-js@^1.1.2: - version "1.1.2" - resolved "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz" - integrity sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug== +sprintf-js@^1.1.2, sprintf-js@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.3.tgz#4914b903a2f8b685d17fdf78a70e917e872e444a" + integrity sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA== sprintf-js@~1.0.2: version "1.0.3" - resolved "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== sshpk@^1.7.0: - version "1.17.0" - resolved "https://registry.npmjs.org/sshpk/-/sshpk-1.17.0.tgz" - integrity sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ== + version "1.18.0" + resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.18.0.tgz#1663e55cddf4d688b86a46b77f0d5fe363aba028" + integrity sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ== dependencies: asn1 "~0.2.3" assert-plus "^1.0.0" @@ -9121,32 +8836,32 @@ sshpk@^1.7.0: tweetnacl "~0.14.0" ssri@^10.0.0: - version "10.0.4" - resolved "https://registry.npmjs.org/ssri/-/ssri-10.0.4.tgz" - integrity sha512-12+IR2CB2C28MMAw0Ncqwj5QbTcs0nGIhgJzYWzDkb21vWmfNI83KS4f3Ci6GI98WreIfG7o9UXp3C0qbpA8nQ== + version "10.0.6" + resolved "https://registry.yarnpkg.com/ssri/-/ssri-10.0.6.tgz#a8aade2de60ba2bce8688e3fa349bad05c7dc1e5" + integrity sha512-MGrFH9Z4NP9Iyhqn16sDtBpRRNJ0Y2hNa6D65h736fVSaPCHr4DM4sWUNvVaSuC+0OBGhwsrydQwmgfg5LncqQ== dependencies: - minipass "^5.0.0" + minipass "^7.0.3" ssri@^9.0.0: version "9.0.1" - resolved "https://registry.npmjs.org/ssri/-/ssri-9.0.1.tgz" + resolved "https://registry.yarnpkg.com/ssri/-/ssri-9.0.1.tgz#544d4c357a8d7b71a19700074b6883fcb4eae057" integrity sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q== dependencies: minipass "^3.1.1" statuses@2.0.1: version "2.0.1" - resolved "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== "statuses@>= 1.4.0 < 2", statuses@~1.5.0: version "1.5.0" - resolved "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== streamroller@^3.1.5: version "3.1.5" - resolved "https://registry.npmjs.org/streamroller/-/streamroller-3.1.5.tgz" + resolved "https://registry.yarnpkg.com/streamroller/-/streamroller-3.1.5.tgz#1263182329a45def1ffaef58d31b15d13d2ee7ff" integrity sha512-KFxaM7XT+irxvdqSP1LGLgNWbYN7ay5owZ3r/8t77p+EtSUAfUgtl7be3xtqtOmGUl9K9YPO2ca8133RlTjvKw== dependencies: date-format "^4.0.14" @@ -9155,7 +8870,7 @@ streamroller@^3.1.5: "string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" - resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== dependencies: emoji-regex "^8.0.0" @@ -9164,7 +8879,7 @@ streamroller@^3.1.5: string-width@^5.0.1, string-width@^5.1.2: version "5.1.2" - resolved "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA== dependencies: eastasianwidth "^0.2.0" @@ -9173,62 +8888,62 @@ string-width@^5.0.1, string-width@^5.1.2: string.prototype.codepointat@^0.2.1: version "0.2.1" - resolved "https://registry.npmjs.org/string.prototype.codepointat/-/string.prototype.codepointat-0.2.1.tgz" + resolved "https://registry.yarnpkg.com/string.prototype.codepointat/-/string.prototype.codepointat-0.2.1.tgz#004ad44c8afc727527b108cd462b4d971cd469bc" integrity sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg== string_decoder@^1.1.1: version "1.3.0" - resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== dependencies: safe-buffer "~5.2.0" string_decoder@~1.1.1: version "1.1.1" - resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== dependencies: safe-buffer "~5.1.0" "strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" - resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== dependencies: ansi-regex "^5.0.1" strip-ansi@^3.0.0: version "3.0.1" - resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" integrity sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg== dependencies: ansi-regex "^2.0.0" strip-ansi@^7.0.1: - version "7.0.1" - resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.0.1.tgz" - integrity sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw== + version "7.1.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" + integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ== dependencies: ansi-regex "^6.0.1" strip-bom@^3.0.0: version "3.0.0" - resolved "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" integrity sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA== strip-final-newline@^2.0.0: version "2.0.0" - resolved "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz" + resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== strip-json-comments@3.1.1, strip-json-comments@^3.1.1: version "3.1.1" - resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== strong-log-transformer@^2.1.0: version "2.1.0" - resolved "https://registry.npmjs.org/strong-log-transformer/-/strong-log-transformer-2.1.0.tgz" + resolved "https://registry.yarnpkg.com/strong-log-transformer/-/strong-log-transformer-2.1.0.tgz#0f5ed78d325e0421ac6f90f7f10e691d6ae3ae10" integrity sha512-B3Hgul+z0L9a236FAUC9iZsL+nVHgoCJnqCbN588DjYxvGXaXaaFbfmQ/JhvKjZwsOukuR72XbHv71Qkug0HxA== dependencies: duplexer "^0.1.1" @@ -9237,38 +8952,38 @@ strong-log-transformer@^2.1.0: supports-color@^2.0.0: version "2.0.0" - resolved "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" integrity sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g== supports-color@^5.3.0: version "5.5.0" - resolved "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== dependencies: has-flag "^3.0.0" supports-color@^7.1.0: version "7.2.0" - resolved "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== dependencies: has-flag "^4.0.0" supports-color@^8.0.0: version "8.1.1" - resolved "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== dependencies: has-flag "^4.0.0" supports-preserve-symlinks-flag@^1.0.0: version "1.0.0" - resolved "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz" + resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== symbol-observable@4.0.0: version "4.0.0" - resolved "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz" + resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-4.0.0.tgz#5b425f192279e87f2f9b937ac8540d1984b39205" integrity sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ== symbol-tree@^3.2.4: @@ -9278,12 +8993,12 @@ symbol-tree@^3.2.4: tapable@^2.1.1, tapable@^2.2.0: version "2.2.1" - resolved "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz" + resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== tar-stream@~2.2.0: version "2.2.0" - resolved "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz" + resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.2.0.tgz#acad84c284136b060dc3faa64474aa9aebd77287" integrity sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ== dependencies: bl "^4.0.3" @@ -9293,9 +9008,9 @@ tar-stream@~2.2.0: readable-stream "^3.1.1" tar@^6.1.11, tar@^6.1.2: - version "6.1.14" - resolved "https://registry.npmjs.org/tar/-/tar-6.1.14.tgz" - integrity sha512-piERznXu0U7/pW7cdSn7hjqySIVTYT6F76icmFk7ptU7dDYlXTm5r9A6K04R2vU3olYgoKeo1Cg3eeu5nhftAw== + version "6.2.1" + resolved "https://registry.yarnpkg.com/tar/-/tar-6.2.1.tgz#717549c541bc3c2af15751bea94b1dd068d4b03a" + integrity sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A== dependencies: chownr "^2.0.0" fs-minipass "^2.0.0" @@ -9305,15 +9020,15 @@ tar@^6.1.11, tar@^6.1.2: yallist "^4.0.0" terser-webpack-plugin@^5.3.7: - version "5.3.8" - resolved "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.8.tgz" - integrity sha512-WiHL3ElchZMsK27P8uIUh4604IgJyAW47LVXGbEoB21DbQcZ+OuMpGjVYnEUaqcWM6dO8uS2qUbA7LSCWqvsbg== + version "5.3.10" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz#904f4c9193c6fd2a03f693a2150c62a92f40d199" + integrity sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w== dependencies: - "@jridgewell/trace-mapping" "^0.3.17" + "@jridgewell/trace-mapping" "^0.3.20" jest-worker "^27.4.5" schema-utils "^3.1.1" serialize-javascript "^6.0.1" - terser "^5.16.8" + terser "^5.26.0" terser@5.19.2: version "5.19.2" @@ -9325,19 +9040,19 @@ terser@5.19.2: commander "^2.20.0" source-map-support "~0.5.20" -terser@^5.16.8: - version "5.17.4" - resolved "https://registry.npmjs.org/terser/-/terser-5.17.4.tgz" - integrity sha512-jcEKZw6UPrgugz/0Tuk/PVyLAPfMBJf5clnGueo45wTweoV8yh7Q7PEkhkJ5uuUbC7zAxEcG3tqNr1bstkQ8nw== +terser@^5.26.0: + version "5.31.3" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.31.3.tgz#b24b7beb46062f4653f049eea4f0cd165d0f0c38" + integrity sha512-pAfYn3NIZLyZpa83ZKigvj6Rn9c/vd5KfYGX7cN1mnzqgDcxWvrU5ZtAfIKhEXz9nRecw4z3LXkjaq96/qZqAA== dependencies: - "@jridgewell/source-map" "^0.3.2" - acorn "^8.5.0" + "@jridgewell/source-map" "^0.3.3" + acorn "^8.8.2" commander "^2.20.0" source-map-support "~0.5.20" test-exclude@^6.0.0: version "6.0.0" - resolved "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz" + resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-6.0.0.tgz#04a8698661d805ea6fa293b6cb9e63ac044ef15e" integrity sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w== dependencies: "@istanbuljs/schema" "^0.1.2" @@ -9346,71 +9061,76 @@ test-exclude@^6.0.0: text-table@0.2.0, text-table@^0.2.0: version "0.2.0" - resolved "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz" + resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== through@X.X.X, through@^2.3.4, through@^2.3.6: version "2.3.8" - resolved "https://registry.npmjs.org/through/-/through-2.3.8.tgz" + resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== thunky@^1.0.2: version "1.1.0" - resolved "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz" + resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.1.0.tgz#5abaf714a9405db0504732bbccd2cedd9ef9537d" integrity sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA== tiny-inflate@^1.0.3: version "1.0.3" - resolved "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz" + resolved "https://registry.yarnpkg.com/tiny-inflate/-/tiny-inflate-1.0.3.tgz#122715494913a1805166aaf7c93467933eea26c4" integrity sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw== tinycolor2@^1.6.0: version "1.6.0" - resolved "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz" + resolved "https://registry.yarnpkg.com/tinycolor2/-/tinycolor2-1.6.0.tgz#f98007460169b0263b97072c5ae92484ce02d09e" integrity sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw== tmp@0.0.30: version "0.0.30" - resolved "https://registry.npmjs.org/tmp/-/tmp-0.0.30.tgz" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.30.tgz#72419d4a8be7d6ce75148fd8b324e593a711c2ed" integrity sha512-HXdTB7lvMwcb55XFfrTM8CPr/IYREk4hVBFaQ4b/6nInrluSL86hfHm7vu0luYKCfyBZp2trCjpc8caC3vVM3w== dependencies: os-tmpdir "~1.0.1" -tmp@0.2.1, tmp@^0.2.1, tmp@~0.2.1: +tmp@0.2.1: version "0.2.1" - resolved "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.1.tgz#8457fc3037dcf4719c251367a1af6500ee1ccf14" integrity sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ== dependencies: rimraf "^3.0.0" tmp@^0.0.33: version "0.0.33" - resolved "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw== dependencies: os-tmpdir "~1.0.2" +tmp@^0.2.1, tmp@~0.2.1: + version "0.2.3" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.3.tgz#eb783cc22bc1e8bebd0671476d46ea4eb32a79ae" + integrity sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w== + to-fast-properties@^2.0.0: version "2.0.0" - resolved "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz" + resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" integrity sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog== to-regex-range@^5.0.1: version "5.0.1" - resolved "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== dependencies: is-number "^7.0.0" toidentifier@1.0.1: version "1.0.1" - resolved "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== tough-cookie@^4.0.0: - version "4.1.3" - resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.1.3.tgz#97b9adb0728b42280aa3d814b6b999b2ff0318bf" - integrity sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw== + version "4.1.4" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.1.4.tgz#945f1461b45b5a8c76821c33ea49c3ac192c1b36" + integrity sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag== dependencies: psl "^1.1.33" punycode "^2.1.1" @@ -9419,7 +9139,7 @@ tough-cookie@^4.0.0: tough-cookie@~2.5.0: version "2.5.0" - resolved "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g== dependencies: psl "^1.1.28" @@ -9434,12 +9154,12 @@ tr46@^2.1.0: tree-kill@1.2.2: version "1.2.2" - resolved "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz" + resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc" integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A== tsconfig-paths@^4.1.2: version "4.2.0" - resolved "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz" + resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz#ef78e19039133446d244beac0fd6a1632e2d107c" integrity sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg== dependencies: json5 "^2.2.2" @@ -9453,57 +9173,57 @@ tslib@2.6.1: tslib@^1.10.0, tslib@^1.8.1, tslib@^1.9.0: version "1.14.1" - resolved "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== tslib@^2.0.0, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.3.0, tslib@^2.4.0, tslib@^2.4.1, tslib@^2.6.2: - version "2.6.2" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" - integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== + version "2.6.3" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.3.tgz#0438f810ad7a9edcde7a241c3d80db693c8cbfe0" + integrity sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ== tsutils@^3.21.0: version "3.21.0" - resolved "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz" + resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA== dependencies: tslib "^1.8.1" -tuf-js@^1.1.3: - version "1.1.6" - resolved "https://registry.npmjs.org/tuf-js/-/tuf-js-1.1.6.tgz" - integrity sha512-CXwFVIsXGbVY4vFiWF7TJKWmlKJAT8TWkH4RmiohJRcDJInix++F0dznDmoVbtJNzZ8yLprKUG4YrDIhv3nBMg== +tuf-js@^1.1.7: + version "1.1.7" + resolved "https://registry.yarnpkg.com/tuf-js/-/tuf-js-1.1.7.tgz#21b7ae92a9373015be77dfe0cb282a80ec3bbe43" + integrity sha512-i3P9Kgw3ytjELUfpuKVDNBJvk4u5bXL6gskv572mcevPbSKCV3zt3djhmlEQ65yERjIbOSncy7U4cQJaB1CBCg== dependencies: "@tufjs/models" "1.0.4" debug "^4.3.4" - make-fetch-happen "^11.1.0" + make-fetch-happen "^11.1.1" tunnel-agent@^0.6.0: version "0.6.0" - resolved "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz" + resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" integrity sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w== dependencies: safe-buffer "^5.0.1" tweetnacl@^0.14.3, tweetnacl@~0.14.0: version "0.14.5" - resolved "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz" + resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" integrity sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA== type-check@^0.4.0, type-check@~0.4.0: version "0.4.0" - resolved "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== dependencies: prelude-ls "^1.2.1" type-fest@^0.20.2: version "0.20.2" - resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== type-fest@^0.21.3: version "0.21.3" - resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== type-fest@^2.0.0, type-fest@^2.5.0: @@ -9513,7 +9233,7 @@ type-fest@^2.0.0, type-fest@^2.5.0: type-is@~1.6.18: version "1.6.18" - resolved "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== dependencies: media-typer "0.3.0" @@ -9521,7 +9241,7 @@ type-is@~1.6.18: typed-assert@^1.0.8: version "1.0.9" - resolved "https://registry.npmjs.org/typed-assert/-/typed-assert-1.0.9.tgz" + resolved "https://registry.yarnpkg.com/typed-assert/-/typed-assert-1.0.9.tgz#8af9d4f93432c4970ec717e3006f33f135b06213" integrity sha512-KNNZtayBCtmnNmbo5mG47p1XsCyrx6iVqomjcZnec/1Y5GGARaxPs6r49RnSPeUP3YjNYiU9sQHAtY4BBvnZwg== typescript@^5.1.6: @@ -9530,18 +9250,28 @@ typescript@^5.1.6: integrity sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA== ua-parser-js@^0.7.30: - version "0.7.35" - resolved "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.35.tgz" - integrity sha512-veRf7dawaj9xaWEu9HoTVn5Pggtc/qj+kqTOFvNiN1l0YdxwC1kvel57UCjThjGa3BHBihE8/UJAHI+uQHmd/g== + version "0.7.38" + resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.38.tgz#f497d8a4dc1fec6e854e5caa4b2f9913422ef054" + integrity sha512-fYmIy7fKTSFAhG3fuPlubeGaMoAd6r0rSnfEsO5nEY55i26KSLt9EH7PLQiiqPUhNqYIJvSkTy1oArIcXAbPbA== + +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== + +undici-types@~6.11.1: + version "6.11.1" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.11.1.tgz#432ea6e8efd54a48569705a699e62d8f4981b197" + integrity sha512-mIDEX2ek50x0OlRgxryxsenE5XaQD4on5U2inY7RApK3SOJpofyw7uW2AyfMKkhAxXIceo2DeWGVGwyvng1GNQ== unicode-canonical-property-names-ecmascript@^2.0.0: version "2.0.0" - resolved "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz" + resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz#301acdc525631670d39f6146e0e77ff6bbdebddc" integrity sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ== unicode-match-property-ecmascript@^2.0.0: version "2.0.0" - resolved "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz" + resolved "https://registry.yarnpkg.com/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz#54fd16e0ecb167cf04cf1f756bdcc92eba7976c3" integrity sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q== dependencies: unicode-canonical-property-names-ecmascript "^2.0.0" @@ -9549,45 +9279,45 @@ unicode-match-property-ecmascript@^2.0.0: unicode-match-property-value-ecmascript@^2.1.0: version "2.1.0" - resolved "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz" + resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz#cb5fffdcd16a05124f5a4b0bf7c3770208acbbe0" integrity sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA== unicode-property-aliases-ecmascript@^2.0.0: version "2.1.0" - resolved "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz" + resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz#43d41e3be698bd493ef911077c9b131f827e8ccd" integrity sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w== unique-filename@^2.0.0: version "2.0.1" - resolved "https://registry.npmjs.org/unique-filename/-/unique-filename-2.0.1.tgz" + resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-2.0.1.tgz#e785f8675a9a7589e0ac77e0b5c34d2eaeac6da2" integrity sha512-ODWHtkkdx3IAR+veKxFV+VBkUMcN+FaqzUUd7IZzt+0zhDZFPFxhlqwPF3YQvMHx1TD0tdgYl+kuPnJ8E6ql7A== dependencies: unique-slug "^3.0.0" unique-filename@^3.0.0: version "3.0.0" - resolved "https://registry.npmjs.org/unique-filename/-/unique-filename-3.0.0.tgz" + resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-3.0.0.tgz#48ba7a5a16849f5080d26c760c86cf5cf05770ea" integrity sha512-afXhuC55wkAmZ0P18QsVE6kp8JaxrEokN2HGIoIVv2ijHQd419H0+6EigAFcIzXeMIkcIkNBpB3L/DXB3cTS/g== dependencies: unique-slug "^4.0.0" unique-slug@^3.0.0: version "3.0.0" - resolved "https://registry.npmjs.org/unique-slug/-/unique-slug-3.0.0.tgz" + resolved "https://registry.yarnpkg.com/unique-slug/-/unique-slug-3.0.0.tgz#6d347cf57c8a7a7a6044aabd0e2d74e4d76dc7c9" integrity sha512-8EyMynh679x/0gqE9fT9oilG+qEt+ibFyqjuVTsZn1+CMxH+XLlpvr2UZx4nVcCwTpx81nICr2JQFkM+HPLq4w== dependencies: imurmurhash "^0.1.4" unique-slug@^4.0.0: version "4.0.0" - resolved "https://registry.npmjs.org/unique-slug/-/unique-slug-4.0.0.tgz" + resolved "https://registry.yarnpkg.com/unique-slug/-/unique-slug-4.0.0.tgz#6bae6bb16be91351badd24cdce741f892a6532e3" integrity sha512-WrcA6AyEfqDX5bWige/4NQfPZMtASNVxdmWR76WESYQVAACSgWcR6e9i0mofqqBxYFtL4oAxPIptY73/0YE1DQ== dependencies: imurmurhash "^0.1.4" universalify@^0.1.0: version "0.1.2" - resolved "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== universalify@^0.2.0: @@ -9596,26 +9326,26 @@ universalify@^0.2.0: integrity sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg== universalify@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz" - integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== + version "2.0.1" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d" + integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw== unpipe@1.0.0, unpipe@~1.0.0: version "1.0.0" - resolved "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== -update-browserslist-db@^1.0.10, update-browserslist-db@^1.0.11: - version "1.0.11" - resolved "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz" - integrity sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA== +update-browserslist-db@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz#7ca61c0d8650766090728046e416a8cde682859e" + integrity sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ== dependencies: - escalade "^3.1.1" - picocolors "^1.0.0" + escalade "^3.1.2" + picocolors "^1.0.1" uri-js@^4.2.2: version "4.4.1" - resolved "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== dependencies: punycode "^2.1.0" @@ -9630,77 +9360,75 @@ url-parse@^1.5.3: util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: version "1.0.2" - resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== utils-merge@1.0.1: version "1.0.1" - resolved "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== +uuid@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-10.0.0.tgz#5a95aa454e6e002725c79055fd42aaba30ca6294" + integrity sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ== + uuid@^3.3.2: version "3.4.0" - resolved "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== uuid@^8.3.2: version "8.3.2" - resolved "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== -uuid@^9.0.1: - version "9.0.1" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30" - integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA== - v8-compile-cache@2.3.0: version "2.3.0" - resolved "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz" + resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA== validate-npm-package-license@^3.0.1, validate-npm-package-license@^3.0.4: version "3.0.4" - resolved "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz" + resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew== dependencies: spdx-correct "^3.0.0" spdx-expression-parse "^3.0.0" validate-npm-package-name@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-5.0.0.tgz" - integrity sha512-YuKoXDAhBYxY7SfOKxHBDoSyENFeW5VvIIQp2TGQuit8gpK6MnWaQelBKxso72DoxTZfZdcP3W90LqpSkgPzLQ== - dependencies: - builtins "^5.0.0" + version "5.0.1" + resolved "https://registry.yarnpkg.com/validate-npm-package-name/-/validate-npm-package-name-5.0.1.tgz#a316573e9b49f3ccd90dbb6eb52b3f06c6d604e8" + integrity sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ== vary@^1, vary@~1.1.2: version "1.1.2" - resolved "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== verror@1.10.0: version "1.10.0" - resolved "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz" + resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" integrity sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw== dependencies: assert-plus "^1.0.0" core-util-is "1.0.2" extsprintf "^1.2.0" -vite@4.4.7: - version "4.4.7" - resolved "https://registry.yarnpkg.com/vite/-/vite-4.4.7.tgz#71b8a37abaf8d50561aca084dbb77fa342824154" - integrity sha512-6pYf9QJ1mHylfVh39HpuSfMPojPSKVxZvnclX1K1FyZ1PXDOcLBibdq5t1qxJSnL63ca8Wf4zts6mD8u8oc9Fw== +vite@4.5.3: + version "4.5.3" + resolved "https://registry.yarnpkg.com/vite/-/vite-4.5.3.tgz#d88a4529ea58bae97294c7e2e6f0eab39a50fb1a" + integrity sha512-kQL23kMeX92v3ph7IauVkXkikdDRsYMGTVl5KY2E9OY4ONLvkHf04MDTbnfo6NKxZiDLWzVpP5oTa8hQD8U3dg== dependencies: esbuild "^0.18.10" - postcss "^8.4.26" - rollup "^3.25.2" + postcss "^8.4.27" + rollup "^3.27.1" optionalDependencies: fsevents "~2.3.2" void-elements@^2.0.0: version "2.0.1" - resolved "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz" + resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-2.0.1.tgz#c066afb582bb1cb4128d60ea92392e94d5e9dbec" integrity sha512-qZKX4RnBzH2ugr8Lxa7x+0V6XD9Sb/ouARtiasEQCHB1EVU4NXtmHsDDrx1dO4ne5fc3J6EW05BP1Dl0z0iung== w3c-hr-time@^1.0.2: @@ -9719,34 +9447,34 @@ w3c-xmlserializer@^2.0.0: walkdir@^0.4.0: version "0.4.1" - resolved "https://registry.npmjs.org/walkdir/-/walkdir-0.4.1.tgz" + resolved "https://registry.yarnpkg.com/walkdir/-/walkdir-0.4.1.tgz#dc119f83f4421df52e3061e514228a2db20afa39" integrity sha512-3eBwRyEln6E1MSzcxcVpQIhRG8Q1jLvEqRmCZqS3dsfXEDR/AhOF4d+jHg1qvDCpYaVRZjENPQyrVxAkQqxPgQ== watchpack@^2.4.0: - version "2.4.0" - resolved "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz" - integrity sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg== + version "2.4.1" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.1.tgz#29308f2cac150fa8e4c92f90e0ec954a9fed7fff" + integrity sha512-8wrBCMtVhqcXP2Sup1ctSkga6uc2Bx0IIvKyT7yTFier5AXHooSI+QyQQAtTb7+E0IUCCKyTFmXqdqgum2XWGg== dependencies: glob-to-regexp "^0.4.1" graceful-fs "^4.1.2" wbuf@^1.1.0, wbuf@^1.7.3: version "1.7.3" - resolved "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz" + resolved "https://registry.yarnpkg.com/wbuf/-/wbuf-1.7.3.tgz#c1d8d149316d3ea852848895cb6a0bfe887b87df" integrity sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA== dependencies: minimalistic-assert "^1.0.0" wcwidth@^1.0.1: version "1.0.1" - resolved "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz" + resolved "https://registry.yarnpkg.com/wcwidth/-/wcwidth-1.0.1.tgz#f0b0dcf915bc5ff1528afadb2c0e17b532da2fe8" integrity sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg== dependencies: defaults "^1.0.3" webdriver-js-extender@2.1.0: version "2.1.0" - resolved "https://registry.npmjs.org/webdriver-js-extender/-/webdriver-js-extender-2.1.0.tgz" + resolved "https://registry.yarnpkg.com/webdriver-js-extender/-/webdriver-js-extender-2.1.0.tgz#57d7a93c00db4cc8d556e4d3db4b5db0a80c3bb7" integrity sha512-lcUKrjbBfCK6MNsh7xaY2UAUmZwe+/ib03AjVOpFobX4O7+83BUveSrLfU0Qsyb1DaKJdQRbuU+kM9aZ6QUhiQ== dependencies: "@types/selenium-webdriver" "^3.0.0" @@ -9754,7 +9482,7 @@ webdriver-js-extender@2.1.0: webdriver-manager@^12.1.7: version "12.1.9" - resolved "https://registry.npmjs.org/webdriver-manager/-/webdriver-manager-12.1.9.tgz" + resolved "https://registry.yarnpkg.com/webdriver-manager/-/webdriver-manager-12.1.9.tgz#8d83543b92711b7217b39fef4cda958a4703d2df" integrity sha512-Yl113uKm8z4m/KMUVWHq1Sjtla2uxEBtx2Ue3AmIlnlPAKloDn/Lvmy6pqWCUersVISpdMeVpAaGbNnvMuT2LQ== dependencies: adm-zip "^0.5.2" @@ -9779,10 +9507,10 @@ webidl-conversions@^6.1.0: resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-6.1.0.tgz#9111b4d7ea80acd40f5270d666621afa78b69514" integrity sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w== -webpack-dev-middleware@6.1.1: - version "6.1.1" - resolved "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-6.1.1.tgz" - integrity sha512-y51HrHaFeeWir0YO4f0g+9GwZawuigzcAdRNon6jErXy/SqV/+O6eaVAzDqE6t3e3NpGeR5CS+cCDaTC+V3yEQ== +webpack-dev-middleware@6.1.2: + version "6.1.2" + resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-6.1.2.tgz#0463232e59b7d7330fa154121528d484d36eb973" + integrity sha512-Wu+EHmX326YPYUpQLKmKbTyZZJIB8/n6R09pTmB03kJmnMsVPTo9COzHZFr01txwaCAuZvfBJE4ZCHRcKs5JaQ== dependencies: colorette "^2.0.10" memfs "^3.4.12" @@ -9791,9 +9519,9 @@ webpack-dev-middleware@6.1.1: schema-utils "^4.0.0" webpack-dev-middleware@^5.3.1: - version "5.3.3" - resolved "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.3.tgz" - integrity sha512-hj5CYrY0bZLB+eTO+x/j67Pkrquiy7kWepMHmUMoPsmcUaeEnQJqFzHJOyxgWlq746/wUuA64p9ta34Kyb01pA== + version "5.3.4" + resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-5.3.4.tgz#eb7b39281cbce10e104eb2b8bf2b63fce49a3517" + integrity sha512-BVdTqhhs+0IfoeAf7EoH5WE+exCmqGerHfDM0IL096Px60Tq2Mn9MAbnaGUe6HiMa41KMCYF19gyzZmBcq/o4Q== dependencies: colorette "^2.0.10" memfs "^3.4.3" @@ -9839,7 +9567,7 @@ webpack-dev-server@4.15.1: webpack-merge@5.9.0: version "5.9.0" - resolved "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.9.0.tgz" + resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-5.9.0.tgz#dc160a1c4cf512ceca515cc231669e9ddb133826" integrity sha512-6NbRQw4+Sy50vYNTw7EyOn41OZItPiXB8GNv3INSoe3PSFaHJEz3SHTrYVaRm2LilNGnFUzh0FAwqPEmU/CwDg== dependencies: clone-deep "^4.0.1" @@ -9847,12 +9575,12 @@ webpack-merge@5.9.0: webpack-sources@^3.0.0, webpack-sources@^3.2.3: version "3.2.3" - resolved "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz" + resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde" integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== webpack-subresource-integrity@5.1.0: version "5.1.0" - resolved "https://registry.npmjs.org/webpack-subresource-integrity/-/webpack-subresource-integrity-5.1.0.tgz" + resolved "https://registry.yarnpkg.com/webpack-subresource-integrity/-/webpack-subresource-integrity-5.1.0.tgz#8b7606b033c6ccac14e684267cb7fb1f5c2a132a" integrity sha512-sacXoX+xd8r4WKsy9MvH/q/vBtEHr86cpImXwyg74pFIpERKt6FmB8cXpeuh0ZLgclOlHI4Wcll7+R5L02xk9Q== dependencies: typed-assert "^1.0.8" @@ -9889,7 +9617,7 @@ webpack@5.88.2: websocket-driver@>=0.5.1, websocket-driver@^0.7.4: version "0.7.4" - resolved "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz" + resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.4.tgz#89ad5295bbf64b480abcba31e4953aca706f5760" integrity sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg== dependencies: http-parser-js ">=0.5.1" @@ -9898,7 +9626,7 @@ websocket-driver@>=0.5.1, websocket-driver@^0.7.4: websocket-extensions@>=0.1.1: version "0.1.4" - resolved "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz" + resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42" integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg== whatwg-encoding@^1.0.5: @@ -9924,45 +9652,50 @@ whatwg-url@^8.0.0, whatwg-url@^8.5.0: which-module@^2.0.0: version "2.0.1" - resolved "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz" + resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.1.tgz#776b1fe35d90aebe99e8ac15eb24093389a4a409" integrity sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ== which@^1.2.1: version "1.3.1" - resolved "https://registry.npmjs.org/which/-/which-1.3.1.tgz" + 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, which@^2.0.2: version "2.0.2" - resolved "https://registry.npmjs.org/which/-/which-2.0.2.tgz" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== dependencies: isexe "^2.0.0" which@^3.0.0: version "3.0.1" - resolved "https://registry.npmjs.org/which/-/which-3.0.1.tgz" + resolved "https://registry.yarnpkg.com/which/-/which-3.0.1.tgz#89f1cd0c23f629a8105ffe69b8172791c87b4be1" integrity sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg== dependencies: isexe "^2.0.0" wide-align@^1.1.5: version "1.1.5" - resolved "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz" + resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.5.tgz#df1d4c206854369ecf3c9a4898f1b23fbd9d15d3" integrity sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg== dependencies: string-width "^1.0.2 || 2 || 3 || 4" wildcard@^2.0.0: version "2.0.1" - resolved "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz" + resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.1.tgz#5ab10d02487198954836b6349f74fff961e10f67" integrity sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ== +word-wrap@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" + integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== + "wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" - resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== dependencies: ansi-styles "^4.0.0" @@ -9971,7 +9704,7 @@ wildcard@^2.0.0: wrap-ansi@^6.2.0: version "6.2.0" - resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA== dependencies: ansi-styles "^4.0.0" @@ -9980,7 +9713,7 @@ wrap-ansi@^6.2.0: wrap-ansi@^8.1.0: version "8.1.0" - resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" integrity sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ== dependencies: ansi-styles "^6.1.0" @@ -9989,23 +9722,23 @@ wrap-ansi@^8.1.0: wrappy@1: version "1.0.2" - resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== ws@^7.4.6: - version "7.5.9" - resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.9.tgz#54fa7db29f4c7cec68b1ddd3a89de099942bb591" - integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q== + version "7.5.10" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.10.tgz#58b5c20dc281633f6c19113f39b349bd8bd558d9" + integrity sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ== ws@^8.13.0: - version "8.13.0" - resolved "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz" - integrity sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA== + version "8.18.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc" + integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw== -ws@~8.11.0: - version "8.11.0" - resolved "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz" - integrity sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg== +ws@~8.17.1: + version "8.17.1" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.17.1.tgz#9293da530bb548febc95371d90f9c878727d919b" + integrity sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ== xml-name-validator@^3.0.0: version "3.0.0" @@ -10014,7 +9747,7 @@ xml-name-validator@^3.0.0: xml2js@^0.4.17: version "0.4.23" - resolved "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz" + resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.23.tgz#a0c69516752421eb2ac758ee4d4ccf58843eac66" integrity sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug== dependencies: sax ">=0.6.0" @@ -10022,7 +9755,7 @@ xml2js@^0.4.17: xmlbuilder@~11.0.0: version "11.0.1" - resolved "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz" + resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-11.0.1.tgz#be9bae1c8a046e76b31127726347d0ad7002beb3" integrity sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA== xmlchars@^2.2.0: @@ -10032,32 +9765,32 @@ xmlchars@^2.2.0: y18n@^4.0.0: version "4.0.3" - resolved "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.3.tgz#b5f259c82cd6e336921efd7bfd8bf560de9eeedf" integrity sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ== y18n@^5.0.5: version "5.0.8" - resolved "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== yallist@^3.0.2: version "3.1.1" - resolved "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== yallist@^4.0.0: version "4.0.0" - resolved "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== yargs-parser@21.1.1, yargs-parser@^21.1.1: version "21.1.1" - resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== yargs-parser@^18.1.2: version "18.1.3" - resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0" integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ== dependencies: camelcase "^5.0.0" @@ -10065,12 +9798,12 @@ yargs-parser@^18.1.2: yargs-parser@^20.2.2: version "20.2.9" - resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== yargs@17.7.2, yargs@^17.2.1, yargs@^17.6.2, yargs@^17.7.2: version "17.7.2" - resolved "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w== dependencies: cliui "^8.0.1" @@ -10083,7 +9816,7 @@ yargs@17.7.2, yargs@^17.2.1, yargs@^17.6.2, yargs@^17.7.2: yargs@^15.3.1: version "15.4.1" - resolved "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8" integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A== dependencies: cliui "^6.0.0" @@ -10100,7 +9833,7 @@ yargs@^15.3.1: yargs@^16.1.1: version "16.2.0" - resolved "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== dependencies: cliui "^7.0.2" @@ -10113,17 +9846,17 @@ yargs@^16.1.1: yocto-queue@^0.1.0: version "0.1.0" - resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz" + 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" - integrity sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g== + version "1.1.1" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-1.1.1.tgz#fef65ce3ac9f8a32ceac5a634f74e17e5b232110" + integrity sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g== zone.js@~0.10.3: version "0.10.3" - resolved "https://registry.npmjs.org/zone.js/-/zone.js-0.10.3.tgz" + resolved "https://registry.yarnpkg.com/zone.js/-/zone.js-0.10.3.tgz#3e5e4da03c607c9dcd92e37dd35687a14a140c16" integrity sha512-LXVLVEq0NNOqK/fLJo3d0kfzd4sxwn2/h67/02pjCjfKDxgx1i9QqpvtHD8CrBnSSwMw5+dy11O7FRX5mkO7Cg== zone.js@~0.13.3: From 189505c80fa639108488f5979fe52967df9729fa Mon Sep 17 00:00:00 2001 From: Livio Spring Date: Wed, 31 Jul 2024 14:21:10 +0200 Subject: [PATCH 09/39] fix: sanitize output for email (#8373) # Which Problems Are Solved ZITADEL uses HTML for emails and renders certain information such as usernames dynamically. That information can be entered by users or administrators. Due to a missing output sanitization, these emails could include malicious code. This may potentially lead to a threat where an attacker, without privileges, could send out altered notifications that are part of the registration processes. An attacker could create a malicious link, where the injected code would be rendered as part of the email. During investigation of this issue a related issue was found and mitigated, where on the user's detail page the username was not sanitized and would also render HTML, giving an attacker the same vulnerability. While it was possible to inject HTML including javascript, the execution of such scripts would be prevented by most email clients and the Content Security Policy in Console UI. # How the Problems Are Solved - All arguments used for email are sanitized (`html.EscapeString`) - The email text no longer `html.UnescapeString` (HTML in custom text is still possible) - Console no longer uses `[innerHtml]` to render the username # Additional Changes None # Additional Context - raised via email --------- Co-authored-by: peintnermax --- .../modules/top-view/top-view.component.html | 2 +- .../modules/top-view/top-view.component.scss | 2 +- .../granted-project-detail.component.html | 4 +--- 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 | 2 +- console/src/assets/i18n/es.json | 2 +- console/src/assets/i18n/fr.json | 2 +- console/src/assets/i18n/it.json | 2 +- console/src/assets/i18n/ja.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/ru.json | 3 +-- console/src/assets/i18n/sv.json | 2 +- console/src/assets/i18n/zh.json | 2 +- .../notification/templates/templateData.go | 3 +-- internal/notification/types/notification.go | 20 +++++++++++++++++++ 20 files changed, 39 insertions(+), 23 deletions(-) diff --git a/console/src/app/modules/top-view/top-view.component.html b/console/src/app/modules/top-view/top-view.component.html index 65e68dec7a..7ef4c27e1d 100644 --- a/console/src/app/modules/top-view/top-view.component.html +++ b/console/src/app/modules/top-view/top-view.component.html @@ -16,7 +16,7 @@ >
- + {{ sub }} info_outline diff --git a/console/src/app/modules/top-view/top-view.component.scss b/console/src/app/modules/top-view/top-view.component.scss index a86282326f..4815cefef4 100644 --- a/console/src/app/modules/top-view/top-view.component.scss +++ b/console/src/app/modules/top-view/top-view.component.scss @@ -82,7 +82,7 @@ .cnsl-type { font-size: 14px; - margin-top: 2rem; + margin-top: 1.5rem; margin-bottom: 1rem; } diff --git a/console/src/app/pages/projects/granted-projects/granted-project-detail/granted-project-detail.component.html b/console/src/app/pages/projects/granted-projects/granted-project-detail/granted-project-detail.component.html index c04e428d05..17a41cce91 100644 --- a/console/src/app/pages/projects/granted-projects/granted-project-detail/granted-project-detail.component.html +++ b/console/src/app/pages/projects/granted-projects/granted-project-detail/granted-project-detail.component.html @@ -2,9 +2,7 @@ title="{{ project?.projectName }}" [hasActions]="false" docLink="https://zitadel.com/docs/guides/manage/console/projects#what-is-a-granted-project" - sub="{{ 'PROJECT.PAGES.TYPE.GRANTED_SINGULAR' | translate }} {{ 'ACTIONS.OF' | translate }} {{ - project?.projectOwnerName - }}" + sub="{{ 'PROJECT.PAGES.TYPE.GRANTED_SINGULAR' | translate: { name: project?.projectOwnerName } }}" [isActive]="project?.state === ProjectGrantState.PROJECT_GRANT_STATE_ACTIVE" [isInactive]="project?.state === ProjectGrantState.PROJECT_GRANT_STATE_INACTIVE" stateTooltip="{{ 'ORG.STATE.' + project?.state | translate }}" diff --git a/console/src/assets/i18n/bg.json b/console/src/assets/i18n/bg.json index 816b9fca04..920c5a6047 100644 --- a/console/src/assets/i18n/bg.json +++ b/console/src/assets/i18n/bg.json @@ -1781,7 +1781,7 @@ "TYPE": { "OWNED": "Притежавани проекти", "OWNED_SINGULAR": "Собствен проект", - "GRANTED_SINGULAR": "Приет проект" + "GRANTED_SINGULAR": "Отпуснат проект на {{name}}" }, "PRIVATELABEL": { "0": { diff --git a/console/src/assets/i18n/cs.json b/console/src/assets/i18n/cs.json index 82896ce743..579ccbf261 100644 --- a/console/src/assets/i18n/cs.json +++ b/console/src/assets/i18n/cs.json @@ -1783,7 +1783,7 @@ "TYPE": { "OWNED": "Vlastní projekty", "OWNED_SINGULAR": "Vlastní projekt", - "GRANTED_SINGULAR": "Přidělený projekt" + "GRANTED_SINGULAR": "Projekt přidělený {{name}}" }, "PRIVATELABEL": { "TITLE": "Nastavení brandingu", diff --git a/console/src/assets/i18n/de.json b/console/src/assets/i18n/de.json index f17835beba..e07c003497 100644 --- a/console/src/assets/i18n/de.json +++ b/console/src/assets/i18n/de.json @@ -1782,7 +1782,7 @@ "TYPE": { "OWNED": "Eigene Projekte", "OWNED_SINGULAR": "Eigenes Projekt", - "GRANTED_SINGULAR": "Berechtigtes Projekt" + "GRANTED_SINGULAR": "Berechtigtes Projekt von {{name}}" }, "PRIVATELABEL": { "TITLE": "Branding Verhalten", diff --git a/console/src/assets/i18n/en.json b/console/src/assets/i18n/en.json index dfacc12970..6fbfa6a6b2 100644 --- a/console/src/assets/i18n/en.json +++ b/console/src/assets/i18n/en.json @@ -1782,7 +1782,7 @@ "TYPE": { "OWNED": "Owned Projects", "OWNED_SINGULAR": "Owned Project", - "GRANTED_SINGULAR": "Granted Project" + "GRANTED_SINGULAR": "Granted Project of {{name}}" }, "PRIVATELABEL": { "TITLE": "Branding Setting", diff --git a/console/src/assets/i18n/es.json b/console/src/assets/i18n/es.json index 56e310c182..a252d8db0e 100644 --- a/console/src/assets/i18n/es.json +++ b/console/src/assets/i18n/es.json @@ -1783,7 +1783,7 @@ "TYPE": { "OWNED": "Proyectos propios", "OWNED_SINGULAR": "Proyecto propio", - "GRANTED_SINGULAR": "Proyecto concedido" + "GRANTED_SINGULAR": "Proyecto asignado {{name}}" }, "PRIVATELABEL": { "TITLE": "Ajustes de imagen de marca", diff --git a/console/src/assets/i18n/fr.json b/console/src/assets/i18n/fr.json index f91de2afff..a4ff97869e 100644 --- a/console/src/assets/i18n/fr.json +++ b/console/src/assets/i18n/fr.json @@ -1782,7 +1782,7 @@ "TYPE": { "OWNED": "Projets possédés", "OWNED_SINGULAR": "Projet possédé", - "GRANTED_SINGULAR": "Projet octroyé" + "GRANTED_SINGULAR": "Projet attribué à {{name}}" }, "PRIVATELABEL": { "TITLE": "Image de marque", diff --git a/console/src/assets/i18n/it.json b/console/src/assets/i18n/it.json index 770d3840d3..4693af6187 100644 --- a/console/src/assets/i18n/it.json +++ b/console/src/assets/i18n/it.json @@ -1782,7 +1782,7 @@ "TYPE": { "OWNED": "Progetti proprietari", "OWNED_SINGULAR": "Progetto proprietario", - "GRANTED_SINGULAR": "Progetto delegato" + "GRANTED_SINGULAR": "Progetto concesso di {{name}}" }, "PRIVATELABEL": { "TITLE": "Impostazione branding", diff --git a/console/src/assets/i18n/ja.json b/console/src/assets/i18n/ja.json index 11c2ae1f69..fd823d5243 100644 --- a/console/src/assets/i18n/ja.json +++ b/console/src/assets/i18n/ja.json @@ -1778,7 +1778,7 @@ "TYPE": { "OWNED": "所有プロジェクト", "OWNED_SINGULAR": "所有プロジェクト", - "GRANTED_SINGULAR": "グラントされたプロジェクト" + "GRANTED_SINGULAR": "{{name}}に付与されたプロジェクト" }, "PRIVATELABEL": { "TITLE": "ブランディング設定", diff --git a/console/src/assets/i18n/mk.json b/console/src/assets/i18n/mk.json index 6edd41879b..47b6f76ac9 100644 --- a/console/src/assets/i18n/mk.json +++ b/console/src/assets/i18n/mk.json @@ -1783,7 +1783,7 @@ "TYPE": { "OWNED": "Сопствени проекти", "OWNED_SINGULAR": "Сопствен проект", - "GRANTED_SINGULAR": "Доделен проект" + "GRANTED_SINGULAR": "Проект доделен на {{name}}" }, "PRIVATELABEL": { "TITLE": "Подесувања за брендирање", diff --git a/console/src/assets/i18n/nl.json b/console/src/assets/i18n/nl.json index a9b8e20398..f40fb493f1 100644 --- a/console/src/assets/i18n/nl.json +++ b/console/src/assets/i18n/nl.json @@ -1782,7 +1782,7 @@ "TYPE": { "OWNED": "Eigen Projecten", "OWNED_SINGULAR": "Eigen Project", - "GRANTED_SINGULAR": "Verleend Project" + "GRANTED_SINGULAR": "Project toegewezen {{name}}" }, "PRIVATELABEL": { "TITLE": "Branding Instelling", diff --git a/console/src/assets/i18n/pl.json b/console/src/assets/i18n/pl.json index 75a3f09025..7823271790 100644 --- a/console/src/assets/i18n/pl.json +++ b/console/src/assets/i18n/pl.json @@ -1781,7 +1781,7 @@ "TYPE": { "OWNED": "Własne Projekty", "OWNED_SINGULAR": "Własny Projekt", - "GRANTED_SINGULAR": "Udzielony Projekt" + "GRANTED_SINGULAR": "Projekt przydzielony {{name}}" }, "PRIVATELABEL": { "TITLE": "Ustawienia marka", diff --git a/console/src/assets/i18n/pt.json b/console/src/assets/i18n/pt.json index f98bb7feb2..ec2752086b 100644 --- a/console/src/assets/i18n/pt.json +++ b/console/src/assets/i18n/pt.json @@ -1781,7 +1781,7 @@ "TYPE": { "OWNED": "Projetos Próprios", "OWNED_SINGULAR": "Projeto Próprio", - "GRANTED_SINGULAR": "Projeto Concedido" + "GRANTED_SINGULAR": "Projeto atribuído a {{name}}" }, "PRIVATELABEL": { "TITLE": "Configuração de Marca", diff --git a/console/src/assets/i18n/ru.json b/console/src/assets/i18n/ru.json index a12954f455..d0d17758f1 100644 --- a/console/src/assets/i18n/ru.json +++ b/console/src/assets/i18n/ru.json @@ -1863,9 +1863,8 @@ "ZITADELPROJECT": "Это принадлежит проекту ZITADEL. Осторожно: Если вы внесёте изменения, ZITADEL может вести себя не так, как предполагалось.", "TYPE": { "OWNED": "Собственные проекты", - "GRANTED": "Проекты доступа", "OWNED_SINGULAR": "Собственный проект", - "GRANTED_SINGULAR": "Допуск проекта" + "GRANTED_SINGULAR": "Проект, предоставленный {{name}}" }, "PRIVATELABEL": { "0": { diff --git a/console/src/assets/i18n/sv.json b/console/src/assets/i18n/sv.json index 16c5fa16ba..10b0b86152 100644 --- a/console/src/assets/i18n/sv.json +++ b/console/src/assets/i18n/sv.json @@ -1786,7 +1786,7 @@ "TYPE": { "OWNED": "Ägda Projekt", "OWNED_SINGULAR": "Ägt Projekt", - "GRANTED_SINGULAR": "Beviljat Projekt" + "GRANTED_SINGULAR": "Projekt tilldelat {{name}}" }, "PRIVATELABEL": { "TITLE": "Varumärkesinställning", diff --git a/console/src/assets/i18n/zh.json b/console/src/assets/i18n/zh.json index 14226f8ecb..27b695e8a4 100644 --- a/console/src/assets/i18n/zh.json +++ b/console/src/assets/i18n/zh.json @@ -1781,7 +1781,7 @@ "TYPE": { "OWNED": "拥有的项目", "OWNED_SINGULAR": "拥有项目", - "GRANTED_SINGULAR": "被授予的项目" + "GRANTED_SINGULAR": "授予{{name}}的项目" }, "PRIVATELABEL": { "TITLE": "品牌标识设置", diff --git a/internal/notification/templates/templateData.go b/internal/notification/templates/templateData.go index f9572a4c5d..8ff750da53 100644 --- a/internal/notification/templates/templateData.go +++ b/internal/notification/templates/templateData.go @@ -2,7 +2,6 @@ package templates import ( "fmt" - "html" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/i18n" @@ -40,7 +39,7 @@ func (data *TemplateData) Translate(translator *i18n.Translator, msgType string, data.PreHeader = translator.Localize(fmt.Sprintf("%s.%s", msgType, domain.MessagePreHeader), args, langs...) data.Subject = translator.Localize(fmt.Sprintf("%s.%s", msgType, domain.MessageSubject), args, langs...) data.Greeting = translator.Localize(fmt.Sprintf("%s.%s", msgType, domain.MessageGreeting), args, langs...) - data.Text = html.UnescapeString(translator.Localize(fmt.Sprintf("%s.%s", msgType, domain.MessageText), args, langs...)) + data.Text = translator.Localize(fmt.Sprintf("%s.%s", msgType, domain.MessageText), args, langs...) data.ButtonText = translator.Localize(fmt.Sprintf("%s.%s", msgType, domain.MessageButtonText), args, langs...) // Footer text is neither included in i18n files nor defaults.yaml footerText := fmt.Sprintf("%s.%s", msgType, domain.MessageFooterText) diff --git a/internal/notification/types/notification.go b/internal/notification/types/notification.go index 1a5ccebb21..9bbfb66c8f 100644 --- a/internal/notification/types/notification.go +++ b/internal/notification/types/notification.go @@ -2,7 +2,9 @@ package types import ( "context" + "html" + "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/i18n" "github.com/zitadel/zitadel/internal/notification/channels/smtp" @@ -42,6 +44,7 @@ func SendEmail( allowUnverifiedNotificationChannel bool, ) error { args = mapNotifyUserToArgs(user, args) + sanitizeArgsForHTML(args) data := GetTemplateData(ctx, translator, args, url, messageType, user.PreferredLanguage.String(), colors) template, err := templates.GetParsedTemplate(mailhtml, data) if err != nil { @@ -59,6 +62,23 @@ func SendEmail( } } +func sanitizeArgsForHTML(args map[string]any) { + for key, arg := range args { + switch a := arg.(type) { + case string: + args[key] = html.EscapeString(a) + case []string: + for i, s := range a { + a[i] = html.EscapeString(s) + } + case database.TextArray[string]: + for i, s := range a { + a[i] = html.EscapeString(s) + } + } + } +} + func SendSMSTwilio( ctx context.Context, channels ChannelChains, From a1d24353db4d27136da54a494a09595c04121320 Mon Sep 17 00:00:00 2001 From: Livio Spring Date: Wed, 31 Jul 2024 14:23:57 +0200 Subject: [PATCH 10/39] fix: prevent error reason leakage in case of IgnoreUnknownUsernames (#8372) # Which Problems Are Solved ZITADEL administrators can enable a setting called "Ignoring unknown usernames" which helps mitigate attacks that try to guess/enumerate usernames. If enabled, ZITADEL will show the password prompt even if the user doesn't exist and report "Username or Password invalid". Due to a implementation change to prevent deadlocks calling the database, the flag would not be correctly respected in all cases and an attacker would gain information if an account exist within ZITADEL, since the error message shows "object not found" instead of the generic error message. # How the Problems Are Solved - Proper check of the error using an error function / type and `errors.Is` # Additional Changes None. # Additional Context - raised in a support request Co-authored-by: Silvan --- .../eventsourcing/eventstore/auth_request.go | 31 +++- .../eventstore/auth_request_test.go | 164 +++++++++++++++++- .../repository/eventsourcing/repository.go | 1 + .../repository/eventsourcing/view/user.go | 10 +- internal/command/user_human_password.go | 13 +- internal/user/repository/view/user_view.go | 10 +- 6 files changed, 207 insertions(+), 22 deletions(-) diff --git a/internal/auth/repository/eventsourcing/eventstore/auth_request.go b/internal/auth/repository/eventsourcing/eventstore/auth_request.go index ec8e85bb60..dda1a5fdbe 100644 --- a/internal/auth/repository/eventsourcing/eventstore/auth_request.go +++ b/internal/auth/repository/eventsourcing/eventstore/auth_request.go @@ -2,6 +2,7 @@ package eventstore import ( "context" + "errors" "slices" "strings" "time" @@ -29,6 +30,12 @@ import ( const unknownUserID = "UNKNOWN" +var ( + ErrUserNotFound = func(err error) error { + return zerrors.ThrowNotFound(err, "EVENT-hodc6", "Errors.User.NotFound") + } +) + type AuthRequestRepo struct { Command *command.Commands Query *query.Queries @@ -53,6 +60,7 @@ type AuthRequestRepo struct { ApplicationProvider applicationProvider CustomTextProvider customTextProvider PasswordReset passwordReset + PasswordChecker passwordChecker IdGenerator id.Generator } @@ -72,7 +80,7 @@ type userSessionViewProvider interface { } type userViewProvider interface { - UserByID(string, string) (*user_view_model.UserView, error) + UserByID(context.Context, string, string) (*user_view_model.UserView, error) } type loginPolicyViewProvider interface { @@ -131,6 +139,10 @@ type passwordReset interface { RequestSetPassword(ctx context.Context, userID, resourceOwner string, notifyType domain.NotificationType, authRequestID string) (objectDetails *domain.ObjectDetails, err error) } +type passwordChecker interface { + HumanCheckPassword(ctx context.Context, resourceOwner, userID, password string, authReq *domain.AuthRequest) error +} + func (repo *AuthRequestRepo) Health(ctx context.Context) error { return repo.AuthRequests.Health(ctx) } @@ -347,23 +359,25 @@ func (repo *AuthRequestRepo) VerifyPassword(ctx context.Context, authReqID, user request, err := repo.getAuthRequestEnsureUser(ctx, authReqID, userAgentID, userID) if err != nil { if isIgnoreUserNotFoundError(err, request) { + // use the same errorID as below (otherwise it would expose the error reason) return zerrors.ThrowInvalidArgument(nil, "EVENT-SDe2f", "Errors.User.UsernameOrPassword.Invalid") } return err } - err = repo.Command.HumanCheckPassword(ctx, resourceOwner, userID, password, request.WithCurrentInfo(info)) + err = repo.PasswordChecker.HumanCheckPassword(ctx, resourceOwner, userID, password, request.WithCurrentInfo(info)) if isIgnoreUserInvalidPasswordError(err, request) { - return zerrors.ThrowInvalidArgument(nil, "EVENT-Jsf32", "Errors.User.UsernameOrPassword.Invalid") + // use the same errorID as above (otherwise it would expose the error reason) + return zerrors.ThrowInvalidArgument(nil, "EVENT-SDe2f", "Errors.User.UsernameOrPassword.Invalid") } return err } func isIgnoreUserNotFoundError(err error, request *domain.AuthRequest) bool { - return request != nil && request.LoginPolicy != nil && request.LoginPolicy.IgnoreUnknownUsernames && zerrors.IsNotFound(err) && zerrors.Contains(err, "Errors.User.NotFound") + return request != nil && request.LoginPolicy != nil && request.LoginPolicy.IgnoreUnknownUsernames && errors.Is(err, ErrUserNotFound(nil)) } func isIgnoreUserInvalidPasswordError(err error, request *domain.AuthRequest) bool { - return request != nil && request.LoginPolicy != nil && request.LoginPolicy.IgnoreUnknownUsernames && zerrors.IsErrorInvalidArgument(err) && zerrors.Contains(err, "Errors.User.Password.Invalid") + return request != nil && request.LoginPolicy != nil && request.LoginPolicy.IgnoreUnknownUsernames && errors.Is(err, command.ErrPasswordInvalid(nil)) } func lockoutPolicyToDomain(policy *query.LockoutPolicy) *domain.LockoutPolicy { @@ -1646,7 +1660,7 @@ func userByID(ctx context.Context, viewProvider userViewProvider, eventProvider ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - user, viewErr := viewProvider.UserByID(userID, authz.GetInstance(ctx).InstanceID()) + user, viewErr := viewProvider.UserByID(ctx, userID, authz.GetInstance(ctx).InstanceID()) if viewErr != nil && !zerrors.IsNotFound(viewErr) { return nil, viewErr } else if user == nil { @@ -1659,9 +1673,10 @@ func userByID(ctx context.Context, viewProvider userViewProvider, eventProvider } if len(events) == 0 { if viewErr != nil { - return nil, viewErr + // We already returned all errors apart from not found, but need to make sure that can be checked in case IgnoreUnknownUsernames option is active. + return nil, ErrUserNotFound(viewErr) } - return user_view_model.UserToModel(user), viewErr + return user_view_model.UserToModel(user), nil } userCopy := *user for _, event := range events { diff --git a/internal/auth/repository/eventsourcing/eventstore/auth_request_test.go b/internal/auth/repository/eventsourcing/eventstore/auth_request_test.go index 4f05d75b45..c99ddc6f10 100644 --- a/internal/auth/repository/eventsourcing/eventstore/auth_request_test.go +++ b/internal/auth/repository/eventsourcing/eventstore/auth_request_test.go @@ -10,9 +10,11 @@ import ( "github.com/stretchr/testify/assert" "go.uber.org/mock/gomock" + "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/auth/repository/eventsourcing/view" cache "github.com/zitadel/zitadel/internal/auth_request/repository" "github.com/zitadel/zitadel/internal/auth_request/repository/mock" + "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/domain" @@ -103,7 +105,7 @@ func (m *mockViewUserSession) GetLatestUserSessionSequence(ctx context.Context, type mockViewNoUser struct{} -func (m *mockViewNoUser) UserByID(string, string) (*user_view_model.UserView, error) { +func (m *mockViewNoUser) UserByID(context.Context, string, string) (*user_view_model.UserView, error) { return nil, zerrors.ThrowNotFound(nil, "id", "user not found") } @@ -203,7 +205,7 @@ func (m *mockPasswordAgePolicy) PasswordAgePolicyByOrg(context.Context, bool, st return m.policy, nil } -func (m *mockViewUser) UserByID(string, string) (*user_view_model.UserView, error) { +func (m *mockViewUser) UserByID(context.Context, string, string) (*user_view_model.UserView, error) { return &user_view_model.UserView{ State: int32(user_model.UserStateActive), UserName: "UserName", @@ -325,6 +327,14 @@ func (m *mockPasswordReset) RequestSetPassword(ctx context.Context, userID, reso return nil, err } +type mockPasswordChecker struct { + err error +} + +func (m *mockPasswordChecker) HumanCheckPassword(ctx context.Context, resourceOwner, userID, password string, authReq *domain.AuthRequest) error { + return m.err +} + func TestAuthRequestRepo_nextSteps(t *testing.T) { type fields struct { AuthRequests cache.AuthRequestCache @@ -2482,3 +2492,153 @@ func Test_userByID(t *testing.T) { }) } } + +func TestAuthRequestRepo_VerifyPassword_IgnoreUnknownUsernames(t *testing.T) { + authRequest := func(userID string) *domain.AuthRequest { + a := &domain.AuthRequest{ + ID: "authRequestID", + AgentID: "userAgentID", + UserID: userID, + LoginPolicy: &domain.LoginPolicy{ + ObjectRoot: es_models.ObjectRoot{}, + Default: true, + AllowUsernamePassword: true, + AllowRegister: true, + AllowExternalIDP: true, + IDPProviders: []*domain.IDPProvider{ + { + ObjectRoot: es_models.ObjectRoot{}, + Type: domain.IdentityProviderTypeSystem, + IDPConfigID: "idpConfig1", + Name: "IdP", + IDPType: domain.IDPTypeOIDC, + IDPState: domain.IDPConfigStateActive, + }, + }, + IgnoreUnknownUsernames: true, + }, + AllowedExternalIDPs: []*domain.IDPProvider{ + { + ObjectRoot: es_models.ObjectRoot{}, + Type: domain.IdentityProviderTypeSystem, + IDPConfigID: "idpConfig1", + Name: "IdP", + IDPType: domain.IDPTypeOIDC, + IDPState: domain.IDPConfigStateActive, + }, + }, + LabelPolicy: &domain.LabelPolicy{ + ObjectRoot: es_models.ObjectRoot{}, + State: domain.LabelPolicyStateActive, + Default: true, + }, + PrivacyPolicy: &domain.PrivacyPolicy{ + ObjectRoot: es_models.ObjectRoot{}, + State: domain.PolicyStateActive, + Default: true, + }, + LockoutPolicy: &domain.LockoutPolicy{ + Default: true, + }, + PasswordAgePolicy: &domain.PasswordAgePolicy{ + ObjectRoot: es_models.ObjectRoot{}, + MaxAgeDays: 0, + ExpireWarnDays: 0, + }, + DefaultTranslations: []*domain.CustomText{{}}, + OrgTranslations: []*domain.CustomText{{}}, + SAMLRequestID: "", + } + a.SetPolicyOrgID("instance1") + return a + } + type fields struct { + AuthRequests func(*testing.T, string) cache.AuthRequestCache + UserViewProvider userViewProvider + UserEventProvider userEventProvider + OrgViewProvider orgViewProvider + PasswordChecker passwordChecker + } + type args struct { + ctx context.Context + authReqID string + userID string + resourceOwner string + password string + userAgentID string + info *domain.BrowserInfo + } + tests := []struct { + name string + fields fields + args args + }{ + { + name: "no user", + fields: fields{ + AuthRequests: func(tt *testing.T, userID string) cache.AuthRequestCache { + m := mock.NewMockAuthRequestCache(gomock.NewController(tt)) + a := authRequest(userID) + m.EXPECT().GetAuthRequestByID(gomock.Any(), "authRequestID").Return(a, nil) + m.EXPECT().CacheAuthRequest(gomock.Any(), a) + return m + }, + UserViewProvider: &mockViewNoUser{}, + UserEventProvider: &mockEventUser{}, + }, + args: args{ + ctx: authz.NewMockContext("instance1", "", ""), + authReqID: "authRequestID", + userID: unknownUserID, + resourceOwner: "org1", + password: "password", + userAgentID: "userAgentID", + info: &domain.BrowserInfo{ + UserAgent: "useragent", + }, + }, + }, + { + name: "invalid password", + fields: fields{ + AuthRequests: func(tt *testing.T, userID string) cache.AuthRequestCache { + m := mock.NewMockAuthRequestCache(gomock.NewController(tt)) + a := authRequest(userID) + m.EXPECT().GetAuthRequestByID(gomock.Any(), "authRequestID").Return(a, nil) + m.EXPECT().CacheAuthRequest(gomock.Any(), a) + return m + }, + UserViewProvider: &mockViewUser{}, + UserEventProvider: &mockEventUser{}, + OrgViewProvider: &mockViewOrg{State: domain.OrgStateActive}, + PasswordChecker: &mockPasswordChecker{ + err: command.ErrPasswordInvalid(nil), + }, + }, + args: args{ + ctx: authz.NewMockContext("instance1", "", ""), + authReqID: "authRequestID", + userID: "user1", + resourceOwner: "org1", + password: "password", + userAgentID: "userAgentID", + info: &domain.BrowserInfo{ + UserAgent: "useragent", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + repo := &AuthRequestRepo{ + AuthRequests: tt.fields.AuthRequests(t, tt.args.userID), + UserViewProvider: tt.fields.UserViewProvider, + UserEventProvider: tt.fields.UserEventProvider, + OrgViewProvider: tt.fields.OrgViewProvider, + PasswordChecker: tt.fields.PasswordChecker, + } + err := repo.VerifyPassword(tt.args.ctx, tt.args.authReqID, tt.args.userID, tt.args.resourceOwner, tt.args.password, tt.args.userAgentID, tt.args.info) + assert.ErrorIs(t, err, zerrors.ThrowInvalidArgument(nil, "EVENT-SDe2f", "Errors.User.UsernameOrPassword.Invalid")) + }) + } +} diff --git a/internal/auth/repository/eventsourcing/repository.go b/internal/auth/repository/eventsourcing/repository.go index c76efab284..9d7b574320 100644 --- a/internal/auth/repository/eventsourcing/repository.go +++ b/internal/auth/repository/eventsourcing/repository.go @@ -79,6 +79,7 @@ func Start(ctx context.Context, conf Config, systemDefaults sd.SystemDefaults, c ApplicationProvider: queries, CustomTextProvider: queries, PasswordReset: command, + PasswordChecker: command, IdGenerator: id.SonyFlakeGenerator(), }, eventstore.TokenRepo{ diff --git a/internal/auth/repository/eventsourcing/view/user.go b/internal/auth/repository/eventsourcing/view/user.go index e75846471d..812c36e62d 100644 --- a/internal/auth/repository/eventsourcing/view/user.go +++ b/internal/auth/repository/eventsourcing/view/user.go @@ -16,8 +16,8 @@ const ( userTable = "auth.users3" ) -func (v *View) UserByID(userID, instanceID string) (*model.UserView, error) { - return view.UserByID(v.Db, userTable, userID, instanceID) +func (v *View) UserByID(ctx context.Context, userID, instanceID string) (*model.UserView, error) { + return view.UserByID(ctx, v.Db, userID, instanceID) } func (v *View) UserByLoginName(ctx context.Context, loginName, instanceID string) (*model.UserView, error) { @@ -27,7 +27,7 @@ func (v *View) UserByLoginName(ctx context.Context, loginName, instanceID string } //nolint: contextcheck // no lint was added because refactor would change too much code - return view.UserByID(v.Db, userTable, queriedUser.ID, instanceID) + return view.UserByID(ctx, v.Db, queriedUser.ID, instanceID) } func (v *View) UserByLoginNameAndResourceOwner(ctx context.Context, loginName, resourceOwner, instanceID string) (*model.UserView, error) { @@ -37,7 +37,7 @@ func (v *View) UserByLoginNameAndResourceOwner(ctx context.Context, loginName, r } //nolint: contextcheck // no lint was added because refactor would change too much code - user, err := view.UserByID(v.Db, userTable, queriedUser.ID, instanceID) + user, err := view.UserByID(ctx, v.Db, queriedUser.ID, instanceID) if err != nil { return nil, err } @@ -103,7 +103,7 @@ func (v *View) userByID(ctx context.Context, instanceID string, queries ...query OnError(err). Errorf("could not get current sequence for userByID") - user, err := view.UserByID(v.Db, userTable, queriedUser.ID, instanceID) + user, err := view.UserByID(ctx, v.Db, queriedUser.ID, instanceID) if err != nil && !zerrors.IsNotFound(err) { return nil, err } diff --git a/internal/command/user_human_password.go b/internal/command/user_human_password.go index 6978f81ced..a94624231a 100644 --- a/internal/command/user_human_password.go +++ b/internal/command/user_human_password.go @@ -16,6 +16,15 @@ import ( "github.com/zitadel/zitadel/internal/zerrors" ) +var ( + ErrPasswordInvalid = func(err error) error { + return zerrors.ThrowInvalidArgument(err, "COMMAND-3M0fs", "Errors.User.Password.Invalid") + } + ErrPasswordUnchanged = func(err error) error { + return zerrors.ThrowPreconditionFailed(err, "COMMAND-Aesh5", "Errors.User.Password.NotChanged") + } +) + func (c *Commands) SetPassword(ctx context.Context, orgID, userID, password string, oneTime bool) (objectDetails *domain.ObjectDetails, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() @@ -393,10 +402,10 @@ func convertPasswapErr(err error) error { return nil } if errors.Is(err, passwap.ErrPasswordMismatch) { - return zerrors.ThrowInvalidArgument(err, "COMMAND-3M0fs", "Errors.User.Password.Invalid") + return ErrPasswordInvalid(err) } if errors.Is(err, passwap.ErrPasswordNoChange) { - return zerrors.ThrowPreconditionFailed(err, "COMMAND-Aesh5", "Errors.User.Password.NotChanged") + return ErrPasswordUnchanged(err) } return zerrors.ThrowInternal(err, "COMMAND-CahN2", "Errors.Internal") } diff --git a/internal/user/repository/view/user_view.go b/internal/user/repository/view/user_view.go index 98b23f4661..c09ef157c9 100644 --- a/internal/user/repository/view/user_view.go +++ b/internal/user/repository/view/user_view.go @@ -16,12 +16,12 @@ import ( //go:embed user_by_id.sql var userByIDQuery string -func UserByID(db *gorm.DB, table, userID, instanceID string) (*model.UserView, error) { +func UserByID(ctx context.Context, db *gorm.DB, userID, instanceID string) (*model.UserView, error) { user := new(model.UserView) query := db.Raw(userByIDQuery, instanceID, userID) - tx := query.BeginTx(context.Background(), &sql.TxOptions{ReadOnly: true}) + tx := query.BeginTx(ctx, &sql.TxOptions{ReadOnly: true}) defer func() { if err := tx.Commit().Error; err != nil { logging.OnError(err).Info("commit failed") @@ -35,8 +35,8 @@ func UserByID(db *gorm.DB, table, userID, instanceID string) (*model.UserView, e return user, nil } if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, zerrors.ThrowNotFound(err, "VIEW-hodc6", "object not found") + return nil, zerrors.ThrowNotFound(err, "VIEW-hodc6", "Errors.User.NotFound") } - logging.WithFields("table ", table).WithError(err).Warn("get from cache error") - return nil, zerrors.ThrowInternal(err, "VIEW-qJBg9", "cache error") + logging.WithError(err).Warn("unable to get user by id") + return nil, zerrors.ThrowInternal(err, "VIEW-qJBg9", "unable to get user by id") } From cc3ec1e2a7b904e38aa9d3b4897fd7b641e0aef3 Mon Sep 17 00:00:00 2001 From: Elio Bischof Date: Wed, 31 Jul 2024 14:42:12 +0200 Subject: [PATCH 11/39] feat(v3alpha): write actions (#8225) # Which Problems Are Solved The current v3alpha actions APIs don't exactly adhere to the [new resources API design](https://zitadel.com/docs/apis/v3#standard-resources). # How the Problems Are Solved - **Breaking**: The current v3alpha actions APIs are removed. This is breaking. - **Resource Namespace**: New v3alpha actions APIs for targets and executions are added under the namespace /resources. - **Feature Flag**: New v3alpha actions APIs still have to be activated using the actions feature flag - **Reduced Executions Overhead**: Executions are managed similar to settings according to the new API design: an empty list of targets basically makes an execution a Noop. So a single method, SetExecution is enough to cover all use cases. Noop executions are not returned in future search requests. - **Compatibility**: The executions created with previous v3alpha APIs are still available to be managed with the new executions API. # Additional Changes - Removed integration tests which test executions but rely on readable targets. They are added again with #8169 # Additional Context Closes #8168 --- cmd/defaults.yaml | 9 +- cmd/start/start.go | 2 +- docs/docusaurus.config.js | 2 +- .../v3alpha/execution_integration_test.go | 1322 ---------------- .../execution_target_integration_test.go | 323 ---- internal/api/grpc/action/v3alpha/query.go | 364 ----- .../action/v3alpha/query_integration_test.go | 877 ----------- internal/api/grpc/action/v3alpha/target.go | 112 -- .../action/v3alpha/target_integration_test.go | 423 ------ .../action/v3alpha/execution.go | 112 +- .../v3alpha/execution_integration_test.go | 805 ++++++++++ .../{ => resources}/action/v3alpha/server.go | 16 +- .../action/v3alpha/server_integration_test.go | 6 +- .../grpc/resources/action/v3alpha/target.go | 121 ++ .../action/v3alpha/target_integration_test.go | 447 ++++++ .../action/v3alpha/target_test.go | 42 +- .../resources/object/v3alpha/converter.go | 21 + .../middleware/execution_interceptor.go | 4 + .../grpc/settings/object/v3alpha/converter.go | 20 + internal/command/action_v2_execution.go | 90 +- internal/command/action_v2_execution_model.go | 12 + internal/command/action_v2_execution_test.go | 1328 ++++------------- internal/execution/execution.go | 12 +- internal/execution/execution_test.go | 502 ++++--- internal/integration/assert.go | 28 + internal/integration/client.go | 51 +- internal/repository/execution/execution.go | 8 + 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/it.yaml | 1 + internal/static/i18n/ja.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/ru.yaml | 1 + internal/static/i18n/sv.yaml | 1 + internal/static/i18n/zh.yaml | 1 + .../action/v3alpha/action_service.proto | 612 -------- proto/zitadel/action/v3alpha/query.proto | 110 -- proto/zitadel/object/v3alpha/object.proto | 22 + .../action/v3alpha/action_service.proto | 361 +++++ .../action/v3alpha/execution.proto | 39 +- .../action/v3alpha/target.proto | 93 +- .../resources/object/v3alpha/object.proto | 43 + .../settings/object/v3alpha/object.proto | 38 + 50 files changed, 2822 insertions(+), 5570 deletions(-) delete mode 100644 internal/api/grpc/action/v3alpha/execution_integration_test.go delete mode 100644 internal/api/grpc/action/v3alpha/execution_target_integration_test.go delete mode 100644 internal/api/grpc/action/v3alpha/query.go delete mode 100644 internal/api/grpc/action/v3alpha/query_integration_test.go delete mode 100644 internal/api/grpc/action/v3alpha/target.go delete mode 100644 internal/api/grpc/action/v3alpha/target_integration_test.go rename internal/api/grpc/{ => resources}/action/v3alpha/execution.go (64%) create mode 100644 internal/api/grpc/resources/action/v3alpha/execution_integration_test.go rename internal/api/grpc/{ => resources}/action/v3alpha/server.go (77%) rename internal/api/grpc/{ => resources}/action/v3alpha/server_integration_test.go (90%) create mode 100644 internal/api/grpc/resources/action/v3alpha/target.go create mode 100644 internal/api/grpc/resources/action/v3alpha/target_integration_test.go rename internal/api/grpc/{ => resources}/action/v3alpha/target_test.go (83%) create mode 100644 internal/api/grpc/resources/object/v3alpha/converter.go create mode 100644 internal/api/grpc/settings/object/v3alpha/converter.go delete mode 100644 proto/zitadel/action/v3alpha/action_service.proto delete mode 100644 proto/zitadel/action/v3alpha/query.proto create mode 100644 proto/zitadel/object/v3alpha/object.proto create mode 100644 proto/zitadel/resources/action/v3alpha/action_service.proto rename proto/zitadel/{ => resources}/action/v3alpha/execution.proto (76%) rename proto/zitadel/{ => resources}/action/v3alpha/target.proto (60%) create mode 100644 proto/zitadel/resources/object/v3alpha/object.proto create mode 100644 proto/zitadel/settings/object/v3alpha/object.proto diff --git a/cmd/defaults.yaml b/cmd/defaults.yaml index 786a829381..58847e2334 100644 --- a/cmd/defaults.yaml +++ b/cmd/defaults.yaml @@ -1041,12 +1041,9 @@ InternalAuthZ: - "events.read" - "milestones.read" - "session.delete" - - "execution.target.read" - - "execution.target.write" - - "execution.target.delete" - - "execution.read" - - "execution.write" - - "execution.delete" + - "action.target.write" + - "action.target.delete" + - "action.execution.write" - "userschema.read" - "userschema.write" - "userschema.delete" diff --git a/cmd/start/start.go b/cmd/start/start.go index 0969c5388a..6bed1168dc 100644 --- a/cmd/start/start.go +++ b/cmd/start/start.go @@ -34,7 +34,6 @@ import ( "github.com/zitadel/zitadel/internal/api" "github.com/zitadel/zitadel/internal/api/assets" internal_authz "github.com/zitadel/zitadel/internal/api/authz" - action_v3_alpha "github.com/zitadel/zitadel/internal/api/grpc/action/v3alpha" "github.com/zitadel/zitadel/internal/api/grpc/admin" "github.com/zitadel/zitadel/internal/api/grpc/auth" feature_v2 "github.com/zitadel/zitadel/internal/api/grpc/feature/v2" @@ -44,6 +43,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" + action_v3_alpha "github.com/zitadel/zitadel/internal/api/grpc/resources/action/v3alpha" session_v2 "github.com/zitadel/zitadel/internal/api/grpc/session/v2" session_v2beta "github.com/zitadel/zitadel/internal/api/grpc/session/v2beta" settings_v2 "github.com/zitadel/zitadel/internal/api/grpc/settings/v2" diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js index c7378c63ec..c067ef25c4 100644 --- a/docs/docusaurus.config.js +++ b/docs/docusaurus.config.js @@ -333,7 +333,7 @@ module.exports = { }, }, action_v3: { - specPath: ".artifacts/openapi/zitadel/action/v3alpha/action_service.swagger.json", + specPath: ".artifacts/openapi/zitadel/resources/action/v3alpha/action_service.swagger.json", outputDir: "docs/apis/resources/action_service_v3", sidebarOptions: { groupPathsBy: "tag", diff --git a/internal/api/grpc/action/v3alpha/execution_integration_test.go b/internal/api/grpc/action/v3alpha/execution_integration_test.go deleted file mode 100644 index 2a01e40383..0000000000 --- a/internal/api/grpc/action/v3alpha/execution_integration_test.go +++ /dev/null @@ -1,1322 +0,0 @@ -//go:build integration - -package action_test - -import ( - "context" - "testing" - - "github.com/stretchr/testify/require" - "google.golang.org/protobuf/types/known/timestamppb" - - "github.com/zitadel/zitadel/internal/domain" - "github.com/zitadel/zitadel/internal/integration" - action "github.com/zitadel/zitadel/pkg/grpc/action/v3alpha" - "github.com/zitadel/zitadel/pkg/grpc/object/v2" -) - -func executionTargetsSingleTarget(id string) []*action.ExecutionTargetType { - return []*action.ExecutionTargetType{{Type: &action.ExecutionTargetType_Target{Target: id}}} -} - -func executionTargetsSingleInclude(include *action.Condition) []*action.ExecutionTargetType { - return []*action.ExecutionTargetType{{Type: &action.ExecutionTargetType_Include{Include: include}}} -} - -func TestServer_SetExecution_Request(t *testing.T) { - ensureFeatureEnabled(t) - targetResp := Tester.CreateTarget(CTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false) - - tests := []struct { - name string - ctx context.Context - req *action.SetExecutionRequest - want *action.SetExecutionResponse - wantErr bool - }{ - { - name: "missing permission", - ctx: Tester.WithAuthorization(context.Background(), integration.OrgOwner), - req: &action.SetExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Request{ - Request: &action.RequestExecution{ - Condition: &action.RequestExecution_All{All: true}, - }, - }, - }, - }, - wantErr: true, - }, - { - name: "no condition, error", - ctx: CTX, - req: &action.SetExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Request{ - Request: &action.RequestExecution{}, - }, - }, - Targets: executionTargetsSingleTarget(targetResp.GetId()), - }, - wantErr: true, - }, - { - name: "method, not existing", - ctx: CTX, - req: &action.SetExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Request{ - Request: &action.RequestExecution{ - Condition: &action.RequestExecution_Method{ - Method: "/zitadel.session.v2.NotExistingService/List", - }, - }, - }, - }, - Targets: executionTargetsSingleTarget(targetResp.GetId()), - }, - wantErr: true, - }, - { - name: "method, ok", - ctx: CTX, - req: &action.SetExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Request{ - Request: &action.RequestExecution{ - Condition: &action.RequestExecution_Method{ - Method: "/zitadel.session.v2.SessionService/ListSessions", - }, - }, - }, - }, - Targets: executionTargetsSingleTarget(targetResp.GetId()), - }, - want: &action.SetExecutionResponse{ - Details: &object.Details{ - ChangeDate: timestamppb.Now(), - ResourceOwner: Tester.Instance.InstanceID(), - }, - }, - }, - { - name: "service, not existing", - ctx: CTX, - req: &action.SetExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Request{ - Request: &action.RequestExecution{ - Condition: &action.RequestExecution_Service{ - Service: "NotExistingService", - }, - }, - }, - }, - Targets: executionTargetsSingleTarget(targetResp.GetId()), - }, - wantErr: true, - }, - { - name: "service, ok", - ctx: CTX, - req: &action.SetExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Request{ - Request: &action.RequestExecution{ - Condition: &action.RequestExecution_Service{ - Service: "zitadel.session.v2.SessionService", - }, - }, - }, - }, - Targets: executionTargetsSingleTarget(targetResp.GetId()), - }, - want: &action.SetExecutionResponse{ - Details: &object.Details{ - ChangeDate: timestamppb.Now(), - ResourceOwner: Tester.Instance.InstanceID(), - }, - }, - }, - { - name: "all, ok", - ctx: CTX, - req: &action.SetExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Request{ - Request: &action.RequestExecution{ - Condition: &action.RequestExecution_All{ - All: true, - }, - }, - }, - }, - Targets: executionTargetsSingleTarget(targetResp.GetId()), - }, - want: &action.SetExecutionResponse{ - Details: &object.Details{ - ChangeDate: timestamppb.Now(), - ResourceOwner: Tester.Instance.InstanceID(), - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := Client.SetExecution(tt.ctx, tt.req) - if tt.wantErr { - require.Error(t, err) - return - } - require.NoError(t, err) - - integration.AssertDetails(t, tt.want, got) - - // cleanup to not impact other requests - Tester.DeleteExecution(tt.ctx, t, tt.req.GetCondition()) - }) - } -} - -func TestServer_SetExecution_Request_Include(t *testing.T) { - ensureFeatureEnabled(t) - targetResp := Tester.CreateTarget(CTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false) - executionCond := &action.Condition{ - ConditionType: &action.Condition_Request{ - Request: &action.RequestExecution{ - Condition: &action.RequestExecution_All{ - All: true, - }, - }, - }, - } - Tester.SetExecution(CTX, t, - executionCond, - executionTargetsSingleTarget(targetResp.GetId()), - ) - - circularExecutionService := &action.Condition{ - ConditionType: &action.Condition_Request{ - Request: &action.RequestExecution{ - Condition: &action.RequestExecution_Service{ - Service: "zitadel.session.v2.SessionService", - }, - }, - }, - } - Tester.SetExecution(CTX, t, - circularExecutionService, - executionTargetsSingleInclude(executionCond), - ) - circularExecutionMethod := &action.Condition{ - ConditionType: &action.Condition_Request{ - Request: &action.RequestExecution{ - Condition: &action.RequestExecution_Method{ - Method: "/zitadel.session.v2.SessionService/ListSessions", - }, - }, - }, - } - Tester.SetExecution(CTX, t, - circularExecutionMethod, - executionTargetsSingleInclude(circularExecutionService), - ) - - tests := []struct { - name string - ctx context.Context - req *action.SetExecutionRequest - want *action.SetExecutionResponse - wantErr bool - }{ - { - name: "method, circular error", - ctx: CTX, - req: &action.SetExecutionRequest{ - Condition: circularExecutionService, - Targets: executionTargetsSingleInclude(circularExecutionMethod), - }, - wantErr: true, - }, - { - name: "method, ok", - ctx: CTX, - req: &action.SetExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Request{ - Request: &action.RequestExecution{ - Condition: &action.RequestExecution_Method{ - Method: "/zitadel.session.v2.SessionService/ListSessions", - }, - }, - }, - }, - Targets: executionTargetsSingleInclude(executionCond), - }, - want: &action.SetExecutionResponse{ - Details: &object.Details{ - ChangeDate: timestamppb.Now(), - ResourceOwner: Tester.Instance.InstanceID(), - }, - }, - }, - { - name: "service, ok", - ctx: CTX, - req: &action.SetExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Request{ - Request: &action.RequestExecution{ - Condition: &action.RequestExecution_Service{ - Service: "zitadel.session.v2.SessionService", - }, - }, - }, - }, - Targets: executionTargetsSingleInclude(executionCond), - }, - want: &action.SetExecutionResponse{ - Details: &object.Details{ - ChangeDate: timestamppb.Now(), - ResourceOwner: Tester.Instance.InstanceID(), - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := Client.SetExecution(tt.ctx, tt.req) - if tt.wantErr { - require.Error(t, err) - return - } - require.NoError(t, err) - - integration.AssertDetails(t, tt.want, got) - - // cleanup to not impact other requests - Tester.DeleteExecution(tt.ctx, t, tt.req.GetCondition()) - }) - } -} - -func TestServer_DeleteExecution_Request(t *testing.T) { - ensureFeatureEnabled(t) - targetResp := Tester.CreateTarget(CTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false) - - tests := []struct { - name string - ctx context.Context - dep func(ctx context.Context, request *action.DeleteExecutionRequest) error - req *action.DeleteExecutionRequest - want *action.DeleteExecutionResponse - wantErr bool - }{ - { - name: "missing permission", - ctx: Tester.WithAuthorization(context.Background(), integration.OrgOwner), - req: &action.DeleteExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Request{ - Request: &action.RequestExecution{ - Condition: &action.RequestExecution_All{All: true}, - }, - }, - }, - }, - wantErr: true, - }, - { - name: "no condition, error", - ctx: CTX, - req: &action.DeleteExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Request{ - Request: &action.RequestExecution{}, - }, - }, - }, - wantErr: true, - }, - { - name: "method, not existing", - ctx: CTX, - req: &action.DeleteExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Request{ - Request: &action.RequestExecution{ - Condition: &action.RequestExecution_Method{ - Method: "/zitadel.session.v2.SessionService/NotExisting", - }, - }, - }, - }, - }, - wantErr: true, - }, - { - name: "method, ok", - ctx: CTX, - dep: func(ctx context.Context, request *action.DeleteExecutionRequest) error { - Tester.SetExecution(ctx, t, request.GetCondition(), executionTargetsSingleTarget(targetResp.GetId())) - return nil - }, - req: &action.DeleteExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Request{ - Request: &action.RequestExecution{ - Condition: &action.RequestExecution_Method{ - Method: "/zitadel.session.v2.SessionService/GetSession", - }, - }, - }, - }, - }, - want: &action.DeleteExecutionResponse{ - Details: &object.Details{ - ChangeDate: timestamppb.Now(), - ResourceOwner: Tester.Instance.InstanceID(), - }, - }, - }, - { - name: "service, not existing", - ctx: CTX, - req: &action.DeleteExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Request{ - Request: &action.RequestExecution{ - Condition: &action.RequestExecution_Service{ - Service: "NotExistingService", - }, - }, - }, - }, - }, - wantErr: true, - }, - { - name: "service, ok", - ctx: CTX, - dep: func(ctx context.Context, request *action.DeleteExecutionRequest) error { - Tester.SetExecution(ctx, t, request.GetCondition(), executionTargetsSingleTarget(targetResp.GetId())) - return nil - }, - req: &action.DeleteExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Request{ - Request: &action.RequestExecution{ - Condition: &action.RequestExecution_Service{ - Service: "zitadel.user.v2.UserService", - }, - }, - }, - }, - }, - want: &action.DeleteExecutionResponse{ - Details: &object.Details{ - ChangeDate: timestamppb.Now(), - ResourceOwner: Tester.Instance.InstanceID(), - }, - }, - }, - { - name: "all, ok", - ctx: CTX, - dep: func(ctx context.Context, request *action.DeleteExecutionRequest) error { - Tester.SetExecution(ctx, t, request.GetCondition(), executionTargetsSingleTarget(targetResp.GetId())) - return nil - }, - req: &action.DeleteExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Request{ - Request: &action.RequestExecution{ - Condition: &action.RequestExecution_All{ - All: true, - }, - }, - }, - }, - }, - want: &action.DeleteExecutionResponse{ - Details: &object.Details{ - ChangeDate: timestamppb.Now(), - ResourceOwner: Tester.Instance.InstanceID(), - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if tt.dep != nil { - err := tt.dep(tt.ctx, tt.req) - require.NoError(t, err) - } - - got, err := Client.DeleteExecution(tt.ctx, tt.req) - if tt.wantErr { - require.Error(t, err) - return - } - require.NoError(t, err) - - integration.AssertDetails(t, tt.want, got) - }) - } -} - -func TestServer_SetExecution_Response(t *testing.T) { - ensureFeatureEnabled(t) - targetResp := Tester.CreateTarget(CTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false) - - tests := []struct { - name string - ctx context.Context - req *action.SetExecutionRequest - want *action.SetExecutionResponse - wantErr bool - }{ - { - name: "missing permission", - ctx: Tester.WithAuthorization(context.Background(), integration.OrgOwner), - req: &action.SetExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Response{ - Response: &action.ResponseExecution{ - Condition: &action.ResponseExecution_All{All: true}, - }, - }, - }, - }, - wantErr: true, - }, - { - name: "no condition, error", - ctx: CTX, - req: &action.SetExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Response{ - Response: &action.ResponseExecution{}, - }, - }, - Targets: executionTargetsSingleTarget(targetResp.GetId()), - }, - wantErr: true, - }, - { - name: "method, not existing", - ctx: CTX, - req: &action.SetExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Response{ - Response: &action.ResponseExecution{ - Condition: &action.ResponseExecution_Method{ - Method: "/zitadel.session.v2.NotExistingService/List", - }, - }, - }, - }, - Targets: executionTargetsSingleTarget(targetResp.GetId()), - }, - wantErr: true, - }, - { - name: "method, ok", - ctx: CTX, - req: &action.SetExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Response{ - Response: &action.ResponseExecution{ - Condition: &action.ResponseExecution_Method{ - Method: "/zitadel.session.v2.SessionService/ListSessions", - }, - }, - }, - }, - Targets: executionTargetsSingleTarget(targetResp.GetId()), - }, - want: &action.SetExecutionResponse{ - Details: &object.Details{ - ChangeDate: timestamppb.Now(), - ResourceOwner: Tester.Instance.InstanceID(), - }, - }, - }, - { - name: "service, not existing", - ctx: CTX, - req: &action.SetExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Response{ - Response: &action.ResponseExecution{ - Condition: &action.ResponseExecution_Service{ - Service: "NotExistingService", - }, - }, - }, - }, - Targets: executionTargetsSingleTarget(targetResp.GetId()), - }, - wantErr: true, - }, - { - name: "service, ok", - ctx: CTX, - req: &action.SetExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Response{ - Response: &action.ResponseExecution{ - Condition: &action.ResponseExecution_Service{ - Service: "zitadel.session.v2.SessionService", - }, - }, - }, - }, - Targets: executionTargetsSingleTarget(targetResp.GetId()), - }, - want: &action.SetExecutionResponse{ - Details: &object.Details{ - ChangeDate: timestamppb.Now(), - ResourceOwner: Tester.Instance.InstanceID(), - }, - }, - }, - { - name: "all, ok", - ctx: CTX, - req: &action.SetExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Response{ - Response: &action.ResponseExecution{ - Condition: &action.ResponseExecution_All{ - All: true, - }, - }, - }, - }, - Targets: executionTargetsSingleTarget(targetResp.GetId()), - }, - want: &action.SetExecutionResponse{ - Details: &object.Details{ - ChangeDate: timestamppb.Now(), - ResourceOwner: Tester.Instance.InstanceID(), - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := Client.SetExecution(tt.ctx, tt.req) - if tt.wantErr { - require.Error(t, err) - return - } - require.NoError(t, err) - - integration.AssertDetails(t, tt.want, got) - - // cleanup to not impact other requests - Tester.DeleteExecution(tt.ctx, t, tt.req.GetCondition()) - }) - } -} - -func TestServer_DeleteExecution_Response(t *testing.T) { - ensureFeatureEnabled(t) - targetResp := Tester.CreateTarget(CTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false) - - tests := []struct { - name string - ctx context.Context - dep func(ctx context.Context, request *action.DeleteExecutionRequest) error - req *action.DeleteExecutionRequest - want *action.DeleteExecutionResponse - wantErr bool - }{ - { - name: "missing permission", - ctx: Tester.WithAuthorization(context.Background(), integration.OrgOwner), - req: &action.DeleteExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Response{ - Response: &action.ResponseExecution{ - Condition: &action.ResponseExecution_All{ - All: true, - }, - }, - }, - }, - }, - wantErr: true, - }, - { - name: "no condition, error", - ctx: CTX, - req: &action.DeleteExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Response{ - Response: &action.ResponseExecution{}, - }, - }, - }, - wantErr: true, - }, - { - name: "method, not existing", - ctx: CTX, - req: &action.DeleteExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Response{ - Response: &action.ResponseExecution{ - Condition: &action.ResponseExecution_Method{ - Method: "/zitadel.session.v2.SessionService/NotExisting", - }, - }, - }, - }, - }, - wantErr: true, - }, - { - name: "method, ok", - ctx: CTX, - dep: func(ctx context.Context, request *action.DeleteExecutionRequest) error { - Tester.SetExecution(ctx, t, request.GetCondition(), executionTargetsSingleTarget(targetResp.GetId())) - return nil - }, - req: &action.DeleteExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Response{ - Response: &action.ResponseExecution{ - Condition: &action.ResponseExecution_Method{ - Method: "/zitadel.session.v2.SessionService/GetSession", - }, - }, - }, - }, - }, - want: &action.DeleteExecutionResponse{ - Details: &object.Details{ - ChangeDate: timestamppb.Now(), - ResourceOwner: Tester.Instance.InstanceID(), - }, - }, - }, - { - name: "service, not existing", - ctx: CTX, - req: &action.DeleteExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Response{ - Response: &action.ResponseExecution{ - Condition: &action.ResponseExecution_Service{ - Service: "NotExistingService", - }, - }, - }, - }, - }, - wantErr: true, - }, - { - name: "service, ok", - ctx: CTX, - dep: func(ctx context.Context, request *action.DeleteExecutionRequest) error { - Tester.SetExecution(ctx, t, request.GetCondition(), executionTargetsSingleTarget(targetResp.GetId())) - return nil - }, - req: &action.DeleteExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Response{ - Response: &action.ResponseExecution{ - Condition: &action.ResponseExecution_Service{ - Service: "zitadel.user.v2.UserService", - }, - }, - }, - }, - }, - want: &action.DeleteExecutionResponse{ - Details: &object.Details{ - ChangeDate: timestamppb.Now(), - ResourceOwner: Tester.Instance.InstanceID(), - }, - }, - }, - { - name: "all, ok", - ctx: CTX, - dep: func(ctx context.Context, request *action.DeleteExecutionRequest) error { - Tester.SetExecution(ctx, t, request.GetCondition(), executionTargetsSingleTarget(targetResp.GetId())) - return nil - }, - req: &action.DeleteExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Response{ - Response: &action.ResponseExecution{ - Condition: &action.ResponseExecution_All{ - All: true, - }, - }, - }, - }, - }, - want: &action.DeleteExecutionResponse{ - Details: &object.Details{ - ChangeDate: timestamppb.Now(), - ResourceOwner: Tester.Instance.InstanceID(), - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if tt.dep != nil { - err := tt.dep(tt.ctx, tt.req) - require.NoError(t, err) - } - - got, err := Client.DeleteExecution(tt.ctx, tt.req) - if tt.wantErr { - require.Error(t, err) - return - } - require.NoError(t, err) - - integration.AssertDetails(t, tt.want, got) - }) - } -} - -func TestServer_SetExecution_Event(t *testing.T) { - ensureFeatureEnabled(t) - targetResp := Tester.CreateTarget(CTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false) - - tests := []struct { - name string - ctx context.Context - req *action.SetExecutionRequest - want *action.SetExecutionResponse - wantErr bool - }{ - { - name: "missing permission", - ctx: Tester.WithAuthorization(context.Background(), integration.OrgOwner), - req: &action.SetExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Event{ - Event: &action.EventExecution{ - Condition: &action.EventExecution_All{ - All: true, - }, - }, - }, - }, - }, - wantErr: true, - }, - { - name: "no condition, error", - ctx: CTX, - req: &action.SetExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Event{ - Event: &action.EventExecution{}, - }, - }, - Targets: executionTargetsSingleTarget(targetResp.GetId()), - }, - wantErr: true, - }, - /* - //TODO event existing check - - { - name: "event, not existing", - ctx: CTX, - req: &action.SetExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Event{ - Event: &action.EventExecution{ - Condition: &action.EventExecution_Event{ - Event: "xxx", - }, - }, - }, - }, - Targets: []string{targetResp.GetId()}, - }, - wantErr: true, - }, - */ - { - name: "event, ok", - ctx: CTX, - req: &action.SetExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Event{ - Event: &action.EventExecution{ - Condition: &action.EventExecution_Event{ - Event: "xxx", - }, - }, - }, - }, - Targets: executionTargetsSingleTarget(targetResp.GetId()), - }, - want: &action.SetExecutionResponse{ - Details: &object.Details{ - ChangeDate: timestamppb.Now(), - ResourceOwner: Tester.Instance.InstanceID(), - }, - }, - }, - /* - // TODO: - - { - name: "group, not existing", - ctx: CTX, - req: &action.SetExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Event{ - Event: &action.EventExecution{ - Condition: &action.EventExecution_Group{ - Group: "xxx", - }, - }, - }, - }, - Targets: []string{targetResp.GetId()}, - }, - wantErr: true, - }, - */ - { - name: "group, ok", - ctx: CTX, - req: &action.SetExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Event{ - Event: &action.EventExecution{ - Condition: &action.EventExecution_Group{ - Group: "xxx", - }, - }, - }, - }, - Targets: executionTargetsSingleTarget(targetResp.GetId()), - }, - want: &action.SetExecutionResponse{ - Details: &object.Details{ - ChangeDate: timestamppb.Now(), - ResourceOwner: Tester.Instance.InstanceID(), - }, - }, - }, - { - name: "all, ok", - ctx: CTX, - req: &action.SetExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Event{ - Event: &action.EventExecution{ - Condition: &action.EventExecution_All{ - All: true, - }, - }, - }, - }, - Targets: executionTargetsSingleTarget(targetResp.GetId()), - }, - want: &action.SetExecutionResponse{ - Details: &object.Details{ - ChangeDate: timestamppb.Now(), - ResourceOwner: Tester.Instance.InstanceID(), - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := Client.SetExecution(tt.ctx, tt.req) - if tt.wantErr { - require.Error(t, err) - return - } - require.NoError(t, err) - - integration.AssertDetails(t, tt.want, got) - - // cleanup to not impact other requests - Tester.DeleteExecution(tt.ctx, t, tt.req.GetCondition()) - }) - } -} - -func TestServer_DeleteExecution_Event(t *testing.T) { - ensureFeatureEnabled(t) - targetResp := Tester.CreateTarget(CTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false) - - tests := []struct { - name string - ctx context.Context - dep func(ctx context.Context, request *action.DeleteExecutionRequest) error - req *action.DeleteExecutionRequest - want *action.DeleteExecutionResponse - wantErr bool - }{ - { - name: "missing permission", - ctx: Tester.WithAuthorization(context.Background(), integration.OrgOwner), - req: &action.DeleteExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Event{ - Event: &action.EventExecution{ - Condition: &action.EventExecution_All{ - All: true, - }, - }, - }, - }, - }, - wantErr: true, - }, - { - name: "no condition, error", - ctx: CTX, - req: &action.DeleteExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Event{ - Event: &action.EventExecution{}, - }, - }, - }, - wantErr: true, - }, - /* - //TODO: add when check is implemented - { - name: "event, not existing", - ctx: CTX, - req: &action.DeleteExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Event{ - Event: &action.EventExecution{ - Condition: &action.EventExecution_Event{ - Event: "xxx", - }, - }, - }, - }, - }, - wantErr: true, - }, - */ - { - name: "event, ok", - ctx: CTX, - dep: func(ctx context.Context, request *action.DeleteExecutionRequest) error { - Tester.SetExecution(ctx, t, request.GetCondition(), executionTargetsSingleTarget(targetResp.GetId())) - return nil - }, - req: &action.DeleteExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Event{ - Event: &action.EventExecution{ - Condition: &action.EventExecution_Event{ - Event: "xxx", - }, - }, - }, - }, - }, - want: &action.DeleteExecutionResponse{ - Details: &object.Details{ - ChangeDate: timestamppb.Now(), - ResourceOwner: Tester.Instance.InstanceID(), - }, - }, - }, - { - name: "group, not existing", - ctx: CTX, - req: &action.DeleteExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Event{ - Event: &action.EventExecution{ - Condition: &action.EventExecution_Group{ - Group: "xxx", - }, - }, - }, - }, - }, - wantErr: true, - }, - { - name: "group, ok", - ctx: CTX, - dep: func(ctx context.Context, request *action.DeleteExecutionRequest) error { - Tester.SetExecution(ctx, t, request.GetCondition(), executionTargetsSingleTarget(targetResp.GetId())) - return nil - }, - req: &action.DeleteExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Event{ - Event: &action.EventExecution{ - Condition: &action.EventExecution_Group{ - Group: "xxx", - }, - }, - }, - }, - }, - want: &action.DeleteExecutionResponse{ - Details: &object.Details{ - ChangeDate: timestamppb.Now(), - ResourceOwner: Tester.Instance.InstanceID(), - }, - }, - }, - { - name: "all, not existing", - ctx: CTX, - req: &action.DeleteExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Event{ - Event: &action.EventExecution{ - Condition: &action.EventExecution_All{ - All: true, - }, - }, - }, - }, - }, - wantErr: true, - }, - { - name: "all, ok", - ctx: CTX, - dep: func(ctx context.Context, request *action.DeleteExecutionRequest) error { - Tester.SetExecution(ctx, t, request.GetCondition(), executionTargetsSingleTarget(targetResp.GetId())) - return nil - }, - req: &action.DeleteExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Event{ - Event: &action.EventExecution{ - Condition: &action.EventExecution_All{ - All: true, - }, - }, - }, - }, - }, - want: &action.DeleteExecutionResponse{ - Details: &object.Details{ - ChangeDate: timestamppb.Now(), - ResourceOwner: Tester.Instance.InstanceID(), - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if tt.dep != nil { - err := tt.dep(tt.ctx, tt.req) - require.NoError(t, err) - } - - got, err := Client.DeleteExecution(tt.ctx, tt.req) - if tt.wantErr { - require.Error(t, err) - return - } - require.NoError(t, err) - - integration.AssertDetails(t, tt.want, got) - }) - } -} - -func TestServer_SetExecution_Function(t *testing.T) { - ensureFeatureEnabled(t) - targetResp := Tester.CreateTarget(CTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false) - - tests := []struct { - name string - ctx context.Context - req *action.SetExecutionRequest - want *action.SetExecutionResponse - wantErr bool - }{ - { - name: "missing permission", - ctx: Tester.WithAuthorization(context.Background(), integration.OrgOwner), - req: &action.SetExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Response{ - Response: &action.ResponseExecution{ - Condition: &action.ResponseExecution_All{All: true}, - }, - }, - }, - }, - wantErr: true, - }, - { - name: "no condition, error", - ctx: CTX, - req: &action.SetExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Response{ - Response: &action.ResponseExecution{}, - }, - }, - Targets: executionTargetsSingleTarget(targetResp.GetId()), - }, - wantErr: true, - }, - { - name: "function, not existing", - ctx: CTX, - req: &action.SetExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Function{ - Function: &action.FunctionExecution{Name: "xxx"}, - }, - }, - Targets: executionTargetsSingleTarget(targetResp.GetId()), - }, - wantErr: true, - }, - { - name: "function, ok", - ctx: CTX, - req: &action.SetExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Function{ - Function: &action.FunctionExecution{Name: "Action.Flow.Type.ExternalAuthentication.Action.TriggerType.PostAuthentication"}, - }, - }, - Targets: executionTargetsSingleTarget(targetResp.GetId()), - }, - want: &action.SetExecutionResponse{ - Details: &object.Details{ - ChangeDate: timestamppb.Now(), - ResourceOwner: Tester.Instance.InstanceID(), - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := Client.SetExecution(tt.ctx, tt.req) - if tt.wantErr { - require.Error(t, err) - return - } - require.NoError(t, err) - - integration.AssertDetails(t, tt.want, got) - - // cleanup to not impact other requests - Tester.DeleteExecution(tt.ctx, t, tt.req.GetCondition()) - }) - } -} - -func TestServer_DeleteExecution_Function(t *testing.T) { - ensureFeatureEnabled(t) - targetResp := Tester.CreateTarget(CTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false) - - tests := []struct { - name string - ctx context.Context - dep func(ctx context.Context, request *action.DeleteExecutionRequest) error - req *action.DeleteExecutionRequest - want *action.DeleteExecutionResponse - wantErr bool - }{ - { - name: "missing permission", - ctx: Tester.WithAuthorization(context.Background(), integration.OrgOwner), - req: &action.DeleteExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Response{ - Response: &action.ResponseExecution{ - Condition: &action.ResponseExecution_All{ - All: true, - }, - }, - }, - }, - }, - wantErr: true, - }, - { - name: "no condition, error", - ctx: CTX, - req: &action.DeleteExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Response{ - Response: &action.ResponseExecution{}, - }, - }, - }, - wantErr: true, - }, - { - name: "function, not existing", - ctx: CTX, - req: &action.DeleteExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Function{ - Function: &action.FunctionExecution{Name: "xxx"}, - }, - }, - }, - wantErr: true, - }, - { - name: "function, ok", - ctx: CTX, - dep: func(ctx context.Context, request *action.DeleteExecutionRequest) error { - Tester.SetExecution(ctx, t, request.GetCondition(), executionTargetsSingleTarget(targetResp.GetId())) - return nil - }, - req: &action.DeleteExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Function{ - Function: &action.FunctionExecution{Name: "Action.Flow.Type.ExternalAuthentication.Action.TriggerType.PostAuthentication"}, - }, - }, - }, - want: &action.DeleteExecutionResponse{ - Details: &object.Details{ - ChangeDate: timestamppb.Now(), - ResourceOwner: Tester.Instance.InstanceID(), - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if tt.dep != nil { - err := tt.dep(tt.ctx, tt.req) - require.NoError(t, err) - } - - got, err := Client.DeleteExecution(tt.ctx, tt.req) - if tt.wantErr { - require.Error(t, err) - return - } - require.NoError(t, err) - - integration.AssertDetails(t, tt.want, got) - }) - } -} diff --git a/internal/api/grpc/action/v3alpha/execution_target_integration_test.go b/internal/api/grpc/action/v3alpha/execution_target_integration_test.go deleted file mode 100644 index 30afb1af6f..0000000000 --- a/internal/api/grpc/action/v3alpha/execution_target_integration_test.go +++ /dev/null @@ -1,323 +0,0 @@ -//go:build integration - -package action_test - -import ( - "context" - "encoding/json" - "fmt" - "io" - "net/http" - "net/http/httptest" - "reflect" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "google.golang.org/protobuf/types/known/durationpb" - - "github.com/zitadel/zitadel/internal/api/grpc/server/middleware" - "github.com/zitadel/zitadel/internal/domain" - "github.com/zitadel/zitadel/internal/integration" - action "github.com/zitadel/zitadel/pkg/grpc/action/v3alpha" -) - -func TestServer_ExecutionTarget(t *testing.T) { - ensureFeatureEnabled(t) - - fullMethod := "/zitadel.action.v3alpha.ActionService/GetTargetByID" - - tests := []struct { - name string - ctx context.Context - dep func(context.Context, *action.GetTargetByIDRequest, *action.GetTargetByIDResponse) (func(), error) - clean func(context.Context) - req *action.GetTargetByIDRequest - want *action.GetTargetByIDResponse - wantErr bool - }{ - { - name: "GetTargetByID, request and response, ok", - ctx: CTX, - dep: func(ctx context.Context, request *action.GetTargetByIDRequest, response *action.GetTargetByIDResponse) (func(), error) { - - instanceID := Tester.Instance.InstanceID() - orgID := Tester.Organisation.ID - projectID := "" - userID := Tester.Users.Get(integration.FirstInstanceUsersKey, integration.IAMOwner).ID - - // create target for target changes - targetCreatedName := fmt.Sprint("GetTargetByID", time.Now().UnixNano()+1) - targetCreatedURL := "https://nonexistent" - - targetCreated := Tester.CreateTarget(ctx, t, targetCreatedName, targetCreatedURL, domain.TargetTypeCall, false) - - // request received by target - wantRequest := &middleware.ContextInfoRequest{FullMethod: fullMethod, InstanceID: instanceID, OrgID: orgID, ProjectID: projectID, UserID: userID, Request: request} - changedRequest := &action.GetTargetByIDRequest{TargetId: targetCreated.GetId()} - // replace original request with different targetID - urlRequest, closeRequest := testServerCall(wantRequest, 0, http.StatusOK, changedRequest) - targetRequest := Tester.CreateTarget(ctx, t, "", urlRequest, domain.TargetTypeCall, false) - Tester.SetExecution(ctx, t, conditionRequestFullMethod(fullMethod), executionTargetsSingleTarget(targetRequest.GetId())) - // GetTargetByID with used target - request.TargetId = targetRequest.GetId() - - // expected response from the GetTargetByID - expectedResponse := &action.GetTargetByIDResponse{ - Target: &action.Target{ - TargetId: targetCreated.GetId(), - Details: targetCreated.GetDetails(), - Name: targetCreatedName, - Endpoint: targetCreatedURL, - TargetType: &action.Target_RestCall{ - RestCall: &action.SetRESTCall{ - InterruptOnError: false, - }, - }, - Timeout: durationpb.New(10 * time.Second), - }, - } - // has to be set separately because of the pointers - response.Target = &action.Target{ - TargetId: targetCreated.GetId(), - Details: targetCreated.GetDetails(), - Name: targetCreatedName, - TargetType: &action.Target_RestCall{ - RestCall: &action.SetRESTCall{ - InterruptOnError: false, - }, - }, - Timeout: durationpb.New(10 * time.Second), - } - - // content for partial update - changedResponse := &action.GetTargetByIDResponse{ - Target: &action.Target{ - TargetId: "changed", - }, - } - // change partial updated content on returned response - response.Target.TargetId = changedResponse.Target.TargetId - - // response received by target - wantResponse := &middleware.ContextInfoResponse{ - FullMethod: fullMethod, - InstanceID: instanceID, - OrgID: orgID, - ProjectID: projectID, - UserID: userID, - Request: changedRequest, - Response: expectedResponse, - } - // after request with different targetID, return changed response - targetResponseURL, closeResponse := testServerCall(wantResponse, 0, http.StatusOK, changedResponse) - targetResponse := Tester.CreateTarget(ctx, t, "", targetResponseURL, domain.TargetTypeCall, false) - Tester.SetExecution(ctx, t, conditionResponseFullMethod(fullMethod), executionTargetsSingleTarget(targetResponse.GetId())) - - return func() { - closeRequest() - closeResponse() - }, nil - }, - clean: func(ctx context.Context) { - Tester.DeleteExecution(ctx, t, conditionRequestFullMethod(fullMethod)) - Tester.DeleteExecution(ctx, t, conditionResponseFullMethod(fullMethod)) - }, - req: &action.GetTargetByIDRequest{}, - want: &action.GetTargetByIDResponse{}, - }, - /*{ - name: "GetTargetByID, request, interrupt", - ctx: CTX, - dep: func(ctx context.Context, request *action.GetTargetByIDRequest, response *action.GetTargetByIDResponse) (func(), error) { - - fullMethod := "/zitadel.action.v3alpha.ActionService/GetTargetByID" - instanceID := Tester.Instance.InstanceID() - orgID := Tester.Organisation.ID - projectID := "" - userID := Tester.Users.Get(integration.FirstInstanceUsersKey, integration.IAMOwner).ID - - // request received by target - wantRequest := &middleware.ContextInfoRequest{FullMethod: fullMethod, InstanceID: instanceID, OrgID: orgID, ProjectID: projectID, UserID: userID, Request: request} - urlRequest, closeRequest := testServerCall(wantRequest, 0, http.StatusInternalServerError, &action.GetTargetByIDRequest{TargetId: "notchanged"}) - - targetRequest := Tester.CreateTarget(ctx, t, "", urlRequest, domain.TargetTypeCall, true) - Tester.SetExecution(ctx, t, conditionRequestFullMethod(fullMethod), executionTargetsSingleTarget(targetRequest.GetId())) - // GetTargetByID with used target - request.TargetId = targetRequest.GetId() - - return func() { - closeRequest() - }, nil - }, - clean: func(ctx context.Context) { - Tester.DeleteExecution(ctx, t, conditionRequestFullMethod(fullMethod)) - }, - req: &action.GetTargetByIDRequest{}, - wantErr: true, - }, - { - name: "GetTargetByID, response, interrupt", - ctx: CTX, - dep: func(ctx context.Context, request *action.GetTargetByIDRequest, response *action.GetTargetByIDResponse) (func(), error) { - - fullMethod := "/zitadel.action.v3alpha.ActionService/GetTargetByID" - instanceID := Tester.Instance.InstanceID() - orgID := Tester.Organisation.ID - projectID := "" - userID := Tester.Users.Get(integration.FirstInstanceUsersKey, integration.IAMOwner).ID - - // create target for target changes - targetCreatedName := fmt.Sprint("GetTargetByID", time.Now().UnixNano()+1) - targetCreatedURL := "https://nonexistent" - - targetCreated := Tester.CreateTarget(ctx, t, targetCreatedName, targetCreatedURL, domain.TargetTypeCall, false) - - // GetTargetByID with used target - request.TargetId = targetCreated.GetId() - - // expected response from the GetTargetByID - expectedResponse := &action.GetTargetByIDResponse{ - Target: &action.Target{ - TargetId: targetCreated.GetId(), - Details: targetCreated.GetDetails(), - Name: targetCreatedName, - Endpoint: targetCreatedURL, - TargetType: &action.Target_RestCall{ - RestCall: &action.SetRESTCall{ - InterruptOnError: false, - }, - }, - Timeout: durationpb.New(10 * time.Second), - }, - } - - // content for partial update - changedResponse := &action.GetTargetByIDResponse{ - Target: &action.Target{ - TargetId: "changed", - }, - } - - // response received by target - wantResponse := &middleware.ContextInfoResponse{ - FullMethod: fullMethod, - InstanceID: instanceID, - OrgID: orgID, - ProjectID: projectID, - UserID: userID, - Request: request, - Response: expectedResponse, - } - // after request with different targetID, return changed response - targetResponseURL, closeResponse := testServerCall(wantResponse, 0, http.StatusInternalServerError, changedResponse) - targetResponse := Tester.CreateTarget(ctx, t, "", targetResponseURL, domain.TargetTypeCall, true) - Tester.SetExecution(ctx, t, conditionResponseFullMethod(fullMethod), executionTargetsSingleTarget(targetResponse.GetId())) - - return func() { - closeResponse() - }, nil - }, - clean: func(ctx context.Context) { - Tester.DeleteExecution(ctx, t, conditionResponseFullMethod(fullMethod)) - }, - req: &action.GetTargetByIDRequest{}, - wantErr: true, - },*/ - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if tt.dep != nil { - close, err := tt.dep(tt.ctx, tt.req, tt.want) - require.NoError(t, err) - defer close() - } - - got, err := Client.GetTargetByID(tt.ctx, tt.req) - if tt.wantErr { - require.Error(t, err) - return - } - require.NoError(t, err) - - integration.AssertDetails(t, tt.want.GetTarget(), got.GetTarget()) - - assert.Equal(t, tt.want.Target.TargetId, got.Target.TargetId) - - if tt.clean != nil { - tt.clean(tt.ctx) - } - }) - } -} - -func conditionRequestFullMethod(fullMethod string) *action.Condition { - return &action.Condition{ - ConditionType: &action.Condition_Request{ - Request: &action.RequestExecution{ - Condition: &action.RequestExecution_Method{ - Method: fullMethod, - }, - }, - }, - } -} - -func conditionResponseFullMethod(fullMethod string) *action.Condition { - return &action.Condition{ - ConditionType: &action.Condition_Response{ - Response: &action.ResponseExecution{ - Condition: &action.ResponseExecution_Method{ - Method: fullMethod, - }, - }, - }, - } -} - -func testServerCall( - reqBody interface{}, - sleep time.Duration, - statusCode int, - respBody interface{}, -) (string, func()) { - handler := func(w http.ResponseWriter, r *http.Request) { - data, err := json.Marshal(reqBody) - if err != nil { - http.Error(w, "error, marshall: "+err.Error(), http.StatusInternalServerError) - return - } - - sentBody, err := io.ReadAll(r.Body) - if err != nil { - http.Error(w, "error, read body: "+err.Error(), http.StatusInternalServerError) - return - } - if !reflect.DeepEqual(data, sentBody) { - http.Error(w, "error, equal:\n"+string(data)+"\nsent:\n"+string(sentBody), http.StatusInternalServerError) - return - } - if statusCode != http.StatusOK { - http.Error(w, "error, statusCode", statusCode) - return - } - - time.Sleep(sleep) - - w.Header().Set("Content-Type", "application/json") - resp, err := json.Marshal(respBody) - if err != nil { - http.Error(w, "error", http.StatusInternalServerError) - return - } - if _, err := io.WriteString(w, string(resp)); err != nil { - http.Error(w, "error", http.StatusInternalServerError) - return - } - } - - server := httptest.NewServer(http.HandlerFunc(handler)) - - return server.URL, server.Close -} diff --git a/internal/api/grpc/action/v3alpha/query.go b/internal/api/grpc/action/v3alpha/query.go deleted file mode 100644 index 095eaa7973..0000000000 --- a/internal/api/grpc/action/v3alpha/query.go +++ /dev/null @@ -1,364 +0,0 @@ -package action - -import ( - "context" - "strings" - - "google.golang.org/protobuf/types/known/durationpb" - - "github.com/zitadel/zitadel/internal/api/grpc/object/v2" - "github.com/zitadel/zitadel/internal/command" - "github.com/zitadel/zitadel/internal/domain" - "github.com/zitadel/zitadel/internal/query" - "github.com/zitadel/zitadel/internal/zerrors" - action "github.com/zitadel/zitadel/pkg/grpc/action/v3alpha" -) - -func (s *Server) ListTargets(ctx context.Context, req *action.ListTargetsRequest) (*action.ListTargetsResponse, error) { - if err := checkExecutionEnabled(ctx); err != nil { - return nil, err - } - - queries, err := listTargetsRequestToModel(req) - if err != nil { - return nil, err - } - resp, err := s.query.SearchTargets(ctx, queries) - if err != nil { - return nil, err - } - return &action.ListTargetsResponse{ - Result: targetsToPb(resp.Targets), - Details: object.ToListDetails(resp.SearchResponse), - }, nil -} - -func listTargetsRequestToModel(req *action.ListTargetsRequest) (*query.TargetSearchQueries, error) { - offset, limit, asc := object.ListQueryToQuery(req.Query) - queries, err := targetQueriesToQuery(req.Queries) - if err != nil { - return nil, err - } - return &query.TargetSearchQueries{ - SearchRequest: query.SearchRequest{ - Offset: offset, - Limit: limit, - Asc: asc, - SortingColumn: targetFieldNameToSortingColumn(req.SortingColumn), - }, - Queries: queries, - }, nil -} - -func targetFieldNameToSortingColumn(field action.TargetFieldName) query.Column { - switch field { - case action.TargetFieldName_FIELD_NAME_UNSPECIFIED: - return query.TargetColumnID - case action.TargetFieldName_FIELD_NAME_ID: - return query.TargetColumnID - case action.TargetFieldName_FIELD_NAME_CREATION_DATE: - return query.TargetColumnCreationDate - case action.TargetFieldName_FIELD_NAME_CHANGE_DATE: - return query.TargetColumnChangeDate - case action.TargetFieldName_FIELD_NAME_NAME: - return query.TargetColumnName - case action.TargetFieldName_FIELD_NAME_TARGET_TYPE: - return query.TargetColumnTargetType - case action.TargetFieldName_FIELD_NAME_URL: - return query.TargetColumnURL - case action.TargetFieldName_FIELD_NAME_TIMEOUT: - return query.TargetColumnTimeout - case action.TargetFieldName_FIELD_NAME_INTERRUPT_ON_ERROR: - return query.TargetColumnInterruptOnError - default: - return query.TargetColumnID - } -} - -func targetQueriesToQuery(queries []*action.TargetSearchQuery) (_ []query.SearchQuery, err error) { - q := make([]query.SearchQuery, len(queries)) - for i, query := range queries { - q[i], err = targetQueryToQuery(query) - if err != nil { - return nil, err - } - } - return q, nil -} - -func targetQueryToQuery(query *action.TargetSearchQuery) (query.SearchQuery, error) { - switch q := query.Query.(type) { - case *action.TargetSearchQuery_TargetNameQuery: - return targetNameQueryToQuery(q.TargetNameQuery) - case *action.TargetSearchQuery_InTargetIdsQuery: - return targetInTargetIdsQueryToQuery(q.InTargetIdsQuery) - default: - return nil, zerrors.ThrowInvalidArgument(nil, "GRPC-vR9nC", "List.Query.Invalid") - } -} - -func targetNameQueryToQuery(q *action.TargetNameQuery) (query.SearchQuery, error) { - return query.NewTargetNameSearchQuery(object.TextMethodToQuery(q.Method), q.GetTargetName()) -} - -func targetInTargetIdsQueryToQuery(q *action.InTargetIDsQuery) (query.SearchQuery, error) { - return query.NewTargetInIDsSearchQuery(q.GetTargetIds()) -} - -func (s *Server) GetTargetByID(ctx context.Context, req *action.GetTargetByIDRequest) (_ *action.GetTargetByIDResponse, err error) { - if err := checkExecutionEnabled(ctx); err != nil { - return nil, err - } - - resp, err := s.query.GetTargetByID(ctx, req.GetTargetId()) - if err != nil { - return nil, err - } - return &action.GetTargetByIDResponse{ - Target: targetToPb(resp), - }, nil -} - -func targetsToPb(targets []*query.Target) []*action.Target { - t := make([]*action.Target, len(targets)) - for i, target := range targets { - t[i] = targetToPb(target) - } - return t -} - -func targetToPb(t *query.Target) *action.Target { - target := &action.Target{ - Details: object.DomainToDetailsPb(&t.ObjectDetails), - TargetId: t.ID, - Name: t.Name, - Timeout: durationpb.New(t.Timeout), - Endpoint: t.Endpoint, - } - - switch t.TargetType { - case domain.TargetTypeWebhook: - target.TargetType = &action.Target_RestWebhook{RestWebhook: &action.SetRESTWebhook{InterruptOnError: t.InterruptOnError}} - case domain.TargetTypeCall: - target.TargetType = &action.Target_RestCall{RestCall: &action.SetRESTCall{InterruptOnError: t.InterruptOnError}} - case domain.TargetTypeAsync: - target.TargetType = &action.Target_RestAsync{RestAsync: &action.SetRESTAsync{}} - default: - target.TargetType = nil - } - return target -} - -func (s *Server) ListExecutions(ctx context.Context, req *action.ListExecutionsRequest) (*action.ListExecutionsResponse, error) { - if err := checkExecutionEnabled(ctx); err != nil { - return nil, err - } - - queries, err := listExecutionsRequestToModel(req) - if err != nil { - return nil, err - } - resp, err := s.query.SearchExecutions(ctx, queries) - if err != nil { - return nil, err - } - return &action.ListExecutionsResponse{ - Result: executionsToPb(resp.Executions), - Details: object.ToListDetails(resp.SearchResponse), - }, nil -} - -func listExecutionsRequestToModel(req *action.ListExecutionsRequest) (*query.ExecutionSearchQueries, error) { - offset, limit, asc := object.ListQueryToQuery(req.Query) - queries, err := executionQueriesToQuery(req.Queries) - if err != nil { - return nil, err - } - return &query.ExecutionSearchQueries{ - SearchRequest: query.SearchRequest{ - Offset: offset, - Limit: limit, - Asc: asc, - }, - Queries: queries, - }, nil -} - -func executionQueriesToQuery(queries []*action.SearchQuery) (_ []query.SearchQuery, err error) { - q := make([]query.SearchQuery, len(queries)) - for i, query := range queries { - q[i], err = executionQueryToQuery(query) - if err != nil { - return nil, err - } - } - return q, nil -} - -func executionQueryToQuery(searchQuery *action.SearchQuery) (query.SearchQuery, error) { - switch q := searchQuery.Query.(type) { - case *action.SearchQuery_InConditionsQuery: - return inConditionsQueryToQuery(q.InConditionsQuery) - case *action.SearchQuery_ExecutionTypeQuery: - return executionTypeToQuery(q.ExecutionTypeQuery) - case *action.SearchQuery_IncludeQuery: - include, err := conditionToInclude(q.IncludeQuery.GetInclude()) - if err != nil { - return nil, err - } - return query.NewIncludeSearchQuery(include) - case *action.SearchQuery_TargetQuery: - return query.NewTargetSearchQuery(q.TargetQuery.GetTargetId()) - default: - return nil, zerrors.ThrowInvalidArgument(nil, "GRPC-vR9nC", "List.Query.Invalid") - } -} - -func executionTypeToQuery(q *action.ExecutionTypeQuery) (query.SearchQuery, error) { - switch q.ExecutionType { - case action.ExecutionType_EXECUTION_TYPE_UNSPECIFIED: - return query.NewExecutionTypeSearchQuery(domain.ExecutionTypeUnspecified) - case action.ExecutionType_EXECUTION_TYPE_REQUEST: - return query.NewExecutionTypeSearchQuery(domain.ExecutionTypeRequest) - case action.ExecutionType_EXECUTION_TYPE_RESPONSE: - return query.NewExecutionTypeSearchQuery(domain.ExecutionTypeResponse) - case action.ExecutionType_EXECUTION_TYPE_EVENT: - return query.NewExecutionTypeSearchQuery(domain.ExecutionTypeEvent) - case action.ExecutionType_EXECUTION_TYPE_FUNCTION: - return query.NewExecutionTypeSearchQuery(domain.ExecutionTypeFunction) - default: - return query.NewExecutionTypeSearchQuery(domain.ExecutionTypeUnspecified) - } -} - -func inConditionsQueryToQuery(q *action.InConditionsQuery) (query.SearchQuery, error) { - values := make([]string, len(q.GetConditions())) - for i, condition := range q.GetConditions() { - id, err := conditionToID(condition) - if err != nil { - return nil, err - } - values[i] = id - } - return query.NewExecutionInIDsSearchQuery(values) -} - -func conditionToID(q *action.Condition) (string, error) { - switch t := q.GetConditionType().(type) { - case *action.Condition_Request: - cond := &command.ExecutionAPICondition{ - Method: t.Request.GetMethod(), - Service: t.Request.GetService(), - All: t.Request.GetAll(), - } - return cond.ID(domain.ExecutionTypeRequest), nil - case *action.Condition_Response: - cond := &command.ExecutionAPICondition{ - Method: t.Response.GetMethod(), - Service: t.Response.GetService(), - All: t.Response.GetAll(), - } - return cond.ID(domain.ExecutionTypeResponse), nil - case *action.Condition_Event: - cond := &command.ExecutionEventCondition{ - Event: t.Event.GetEvent(), - Group: t.Event.GetGroup(), - All: t.Event.GetAll(), - } - return cond.ID(), nil - case *action.Condition_Function: - return command.ExecutionFunctionCondition(t.Function.GetName()).ID(), nil - default: - return "", zerrors.ThrowInvalidArgument(nil, "GRPC-vR9nC", "List.Query.Invalid") - } -} - -func executionsToPb(executions []*query.Execution) []*action.Execution { - e := make([]*action.Execution, len(executions)) - for i, execution := range executions { - e[i] = executionToPb(execution) - } - return e -} - -func executionToPb(e *query.Execution) *action.Execution { - targets := make([]*action.ExecutionTargetType, len(e.Targets)) - for i := range e.Targets { - switch e.Targets[i].Type { - case domain.ExecutionTargetTypeInclude: - targets[i] = &action.ExecutionTargetType{Type: &action.ExecutionTargetType_Include{Include: executionIDToCondition(e.Targets[i].Target)}} - case domain.ExecutionTargetTypeTarget: - targets[i] = &action.ExecutionTargetType{Type: &action.ExecutionTargetType_Target{Target: e.Targets[i].Target}} - case domain.ExecutionTargetTypeUnspecified: - continue - default: - continue - } - } - - return &action.Execution{ - Details: object.DomainToDetailsPb(&e.ObjectDetails), - Condition: executionIDToCondition(e.ID), - Targets: targets, - } -} - -func executionIDToCondition(include string) *action.Condition { - if strings.HasPrefix(include, domain.ExecutionTypeRequest.String()) { - return includeRequestToCondition(strings.TrimPrefix(include, domain.ExecutionTypeRequest.String())) - } - if strings.HasPrefix(include, domain.ExecutionTypeResponse.String()) { - return includeResponseToCondition(strings.TrimPrefix(include, domain.ExecutionTypeResponse.String())) - } - if strings.HasPrefix(include, domain.ExecutionTypeEvent.String()) { - return includeEventToCondition(strings.TrimPrefix(include, domain.ExecutionTypeEvent.String())) - } - if strings.HasPrefix(include, domain.ExecutionTypeFunction.String()) { - return includeFunctionToCondition(strings.TrimPrefix(include, domain.ExecutionTypeFunction.String())) - } - return nil -} - -func includeRequestToCondition(id string) *action.Condition { - switch strings.Count(id, "/") { - case 2: - return &action.Condition{ConditionType: &action.Condition_Request{Request: &action.RequestExecution{Condition: &action.RequestExecution_Method{Method: id}}}} - case 1: - return &action.Condition{ConditionType: &action.Condition_Request{Request: &action.RequestExecution{Condition: &action.RequestExecution_Service{Service: strings.TrimPrefix(id, "/")}}}} - case 0: - return &action.Condition{ConditionType: &action.Condition_Request{Request: &action.RequestExecution{Condition: &action.RequestExecution_All{All: true}}}} - default: - return nil - } -} -func includeResponseToCondition(id string) *action.Condition { - switch strings.Count(id, "/") { - case 2: - return &action.Condition{ConditionType: &action.Condition_Response{Response: &action.ResponseExecution{Condition: &action.ResponseExecution_Method{Method: id}}}} - case 1: - return &action.Condition{ConditionType: &action.Condition_Response{Response: &action.ResponseExecution{Condition: &action.ResponseExecution_Service{Service: strings.TrimPrefix(id, "/")}}}} - case 0: - return &action.Condition{ConditionType: &action.Condition_Response{Response: &action.ResponseExecution{Condition: &action.ResponseExecution_All{All: true}}}} - default: - return nil - } -} - -func includeEventToCondition(id string) *action.Condition { - switch strings.Count(id, "/") { - case 1: - if strings.HasSuffix(id, command.EventGroupSuffix) { - return &action.Condition{ConditionType: &action.Condition_Event{Event: &action.EventExecution{Condition: &action.EventExecution_Group{Group: strings.TrimSuffix(strings.TrimPrefix(id, "/"), command.EventGroupSuffix)}}}} - } else { - return &action.Condition{ConditionType: &action.Condition_Event{Event: &action.EventExecution{Condition: &action.EventExecution_Event{Event: strings.TrimPrefix(id, "/")}}}} - } - case 0: - return &action.Condition{ConditionType: &action.Condition_Event{Event: &action.EventExecution{Condition: &action.EventExecution_All{All: true}}}} - default: - return nil - } -} - -func includeFunctionToCondition(id string) *action.Condition { - return &action.Condition{ConditionType: &action.Condition_Function{Function: &action.FunctionExecution{Name: strings.TrimPrefix(id, "/")}}} -} diff --git a/internal/api/grpc/action/v3alpha/query_integration_test.go b/internal/api/grpc/action/v3alpha/query_integration_test.go deleted file mode 100644 index 279109ef78..0000000000 --- a/internal/api/grpc/action/v3alpha/query_integration_test.go +++ /dev/null @@ -1,877 +0,0 @@ -//go:build integration - -package action_test - -import ( - "context" - "fmt" - "reflect" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "google.golang.org/protobuf/types/known/durationpb" - - "github.com/zitadel/zitadel/internal/domain" - "github.com/zitadel/zitadel/internal/integration" - action "github.com/zitadel/zitadel/pkg/grpc/action/v3alpha" - "github.com/zitadel/zitadel/pkg/grpc/object/v2" -) - -func TestServer_GetTargetByID(t *testing.T) { - ensureFeatureEnabled(t) - type args struct { - ctx context.Context - dep func(context.Context, *action.GetTargetByIDRequest, *action.GetTargetByIDResponse) error - req *action.GetTargetByIDRequest - } - tests := []struct { - name string - args args - want *action.GetTargetByIDResponse - wantErr bool - }{ - { - name: "missing permission", - args: args{ - ctx: Tester.WithAuthorization(context.Background(), integration.OrgOwner), - req: &action.GetTargetByIDRequest{}, - }, - wantErr: true, - }, - { - name: "not found", - args: args{ - ctx: CTX, - req: &action.GetTargetByIDRequest{TargetId: "notexisting"}, - }, - wantErr: true, - }, - { - name: "get, ok", - args: args{ - ctx: CTX, - dep: func(ctx context.Context, request *action.GetTargetByIDRequest, response *action.GetTargetByIDResponse) error { - name := fmt.Sprint(time.Now().UnixNano() + 1) - resp := Tester.CreateTarget(ctx, t, name, "https://example.com", domain.TargetTypeWebhook, false) - request.TargetId = resp.GetId() - - response.Target.TargetId = resp.GetId() - response.Target.Name = name - response.Target.Details.ResourceOwner = resp.GetDetails().GetResourceOwner() - response.Target.Details.ChangeDate = resp.GetDetails().GetChangeDate() - response.Target.Details.Sequence = resp.GetDetails().GetSequence() - return nil - }, - req: &action.GetTargetByIDRequest{}, - }, - want: &action.GetTargetByIDResponse{ - Target: &action.Target{ - Details: &object.Details{ - ResourceOwner: Tester.Instance.InstanceID(), - }, - Endpoint: "https://example.com", - TargetType: &action.Target_RestWebhook{ - RestWebhook: &action.SetRESTWebhook{}, - }, - Timeout: durationpb.New(10 * time.Second), - }, - }, - }, - { - name: "get, async, ok", - args: args{ - ctx: CTX, - dep: func(ctx context.Context, request *action.GetTargetByIDRequest, response *action.GetTargetByIDResponse) error { - name := fmt.Sprint(time.Now().UnixNano() + 1) - resp := Tester.CreateTarget(ctx, t, name, "https://example.com", domain.TargetTypeAsync, false) - request.TargetId = resp.GetId() - - response.Target.TargetId = resp.GetId() - response.Target.Name = name - response.Target.Details.ResourceOwner = resp.GetDetails().GetResourceOwner() - response.Target.Details.ChangeDate = resp.GetDetails().GetChangeDate() - response.Target.Details.Sequence = resp.GetDetails().GetSequence() - return nil - }, - req: &action.GetTargetByIDRequest{}, - }, - want: &action.GetTargetByIDResponse{ - Target: &action.Target{ - Details: &object.Details{ - ResourceOwner: Tester.Instance.InstanceID(), - }, - Endpoint: "https://example.com", - TargetType: &action.Target_RestAsync{ - RestAsync: &action.SetRESTAsync{}, - }, - Timeout: durationpb.New(10 * time.Second), - }, - }, - }, - { - name: "get, webhook interruptOnError, ok", - args: args{ - ctx: CTX, - dep: func(ctx context.Context, request *action.GetTargetByIDRequest, response *action.GetTargetByIDResponse) error { - name := fmt.Sprint(time.Now().UnixNano() + 1) - resp := Tester.CreateTarget(ctx, t, name, "https://example.com", domain.TargetTypeWebhook, true) - request.TargetId = resp.GetId() - - response.Target.TargetId = resp.GetId() - response.Target.Name = name - response.Target.Details.ResourceOwner = resp.GetDetails().GetResourceOwner() - response.Target.Details.ChangeDate = resp.GetDetails().GetChangeDate() - response.Target.Details.Sequence = resp.GetDetails().GetSequence() - return nil - }, - req: &action.GetTargetByIDRequest{}, - }, - want: &action.GetTargetByIDResponse{ - Target: &action.Target{ - Details: &object.Details{ - ResourceOwner: Tester.Instance.InstanceID(), - }, - Endpoint: "https://example.com", - TargetType: &action.Target_RestWebhook{ - RestWebhook: &action.SetRESTWebhook{ - InterruptOnError: true, - }, - }, - Timeout: durationpb.New(10 * time.Second), - }, - }, - }, - { - name: "get, call, ok", - args: args{ - ctx: CTX, - dep: func(ctx context.Context, request *action.GetTargetByIDRequest, response *action.GetTargetByIDResponse) error { - name := fmt.Sprint(time.Now().UnixNano() + 1) - resp := Tester.CreateTarget(ctx, t, name, "https://example.com", domain.TargetTypeCall, false) - request.TargetId = resp.GetId() - - response.Target.TargetId = resp.GetId() - response.Target.Name = name - response.Target.Details.ResourceOwner = resp.GetDetails().GetResourceOwner() - response.Target.Details.ChangeDate = resp.GetDetails().GetChangeDate() - response.Target.Details.Sequence = resp.GetDetails().GetSequence() - return nil - }, - req: &action.GetTargetByIDRequest{}, - }, - want: &action.GetTargetByIDResponse{ - Target: &action.Target{ - Details: &object.Details{ - ResourceOwner: Tester.Instance.InstanceID(), - }, - Endpoint: "https://example.com", - TargetType: &action.Target_RestCall{ - RestCall: &action.SetRESTCall{ - InterruptOnError: false, - }, - }, - Timeout: durationpb.New(10 * time.Second), - }, - }, - }, - { - name: "get, call interruptOnError, ok", - args: args{ - ctx: CTX, - dep: func(ctx context.Context, request *action.GetTargetByIDRequest, response *action.GetTargetByIDResponse) error { - name := fmt.Sprint(time.Now().UnixNano() + 1) - resp := Tester.CreateTarget(ctx, t, name, "https://example.com", domain.TargetTypeCall, true) - request.TargetId = resp.GetId() - - response.Target.TargetId = resp.GetId() - response.Target.Name = name - response.Target.Details.ResourceOwner = resp.GetDetails().GetResourceOwner() - response.Target.Details.ChangeDate = resp.GetDetails().GetChangeDate() - response.Target.Details.Sequence = resp.GetDetails().GetSequence() - return nil - }, - req: &action.GetTargetByIDRequest{}, - }, - want: &action.GetTargetByIDResponse{ - Target: &action.Target{ - Details: &object.Details{ - ResourceOwner: Tester.Instance.InstanceID(), - }, - Endpoint: "https://example.com", - TargetType: &action.Target_RestCall{ - RestCall: &action.SetRESTCall{ - InterruptOnError: true, - }, - }, - Timeout: durationpb.New(10 * time.Second), - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if tt.args.dep != nil { - err := tt.args.dep(tt.args.ctx, tt.args.req, tt.want) - require.NoError(t, err) - } - - retryDuration := 5 * time.Second - if ctxDeadline, ok := CTX.Deadline(); ok { - retryDuration = time.Until(ctxDeadline) - } - - require.EventuallyWithT(t, func(ttt *assert.CollectT) { - got, getErr := Client.GetTargetByID(tt.args.ctx, tt.args.req) - if tt.wantErr { - assert.Error(ttt, getErr, "Error: "+getErr.Error()) - } else { - assert.NoError(ttt, getErr) - - integration.AssertDetails(t, tt.want.GetTarget(), got.GetTarget()) - - assert.Equal(t, tt.want.Target, got.Target) - } - - }, retryDuration, time.Millisecond*100, "timeout waiting for expected execution result") - }) - } -} - -func TestServer_ListTargets(t *testing.T) { - ensureFeatureEnabled(t) - type args struct { - ctx context.Context - dep func(context.Context, *action.ListTargetsRequest, *action.ListTargetsResponse) error - req *action.ListTargetsRequest - } - tests := []struct { - name string - args args - want *action.ListTargetsResponse - wantErr bool - }{ - { - name: "missing permission", - args: args{ - ctx: Tester.WithAuthorization(context.Background(), integration.OrgOwner), - req: &action.ListTargetsRequest{}, - }, - wantErr: true, - }, - { - name: "list, not found", - args: args{ - ctx: CTX, - req: &action.ListTargetsRequest{ - Queries: []*action.TargetSearchQuery{ - {Query: &action.TargetSearchQuery_InTargetIdsQuery{ - InTargetIdsQuery: &action.InTargetIDsQuery{ - TargetIds: []string{"notfound"}, - }, - }, - }, - }, - }, - }, - want: &action.ListTargetsResponse{ - Details: &object.ListDetails{ - TotalResult: 0, - }, - Result: []*action.Target{}, - }, - }, - { - name: "list single id", - args: args{ - ctx: CTX, - dep: func(ctx context.Context, request *action.ListTargetsRequest, response *action.ListTargetsResponse) error { - name := fmt.Sprint(time.Now().UnixNano() + 1) - resp := Tester.CreateTarget(ctx, t, name, "https://example.com", domain.TargetTypeWebhook, false) - request.Queries[0].Query = &action.TargetSearchQuery_InTargetIdsQuery{ - InTargetIdsQuery: &action.InTargetIDsQuery{ - TargetIds: []string{resp.GetId()}, - }, - } - response.Details.Timestamp = resp.GetDetails().GetChangeDate() - //response.Details.ProcessedSequence = resp.GetDetails().GetSequence() - - response.Result[0].Details.ChangeDate = resp.GetDetails().GetChangeDate() - response.Result[0].Details.Sequence = resp.GetDetails().GetSequence() - response.Result[0].TargetId = resp.GetId() - response.Result[0].Name = name - return nil - }, - req: &action.ListTargetsRequest{ - Queries: []*action.TargetSearchQuery{{}}, - }, - }, - want: &action.ListTargetsResponse{ - Details: &object.ListDetails{ - TotalResult: 1, - }, - Result: []*action.Target{ - { - Details: &object.Details{ - ResourceOwner: Tester.Instance.InstanceID(), - }, - Endpoint: "https://example.com", - TargetType: &action.Target_RestWebhook{ - RestWebhook: &action.SetRESTWebhook{ - InterruptOnError: false, - }, - }, - Timeout: durationpb.New(10 * time.Second), - }, - }, - }, - }, { - name: "list single name", - args: args{ - ctx: CTX, - dep: func(ctx context.Context, request *action.ListTargetsRequest, response *action.ListTargetsResponse) error { - name := fmt.Sprint(time.Now().UnixNano() + 1) - resp := Tester.CreateTarget(ctx, t, name, "https://example.com", domain.TargetTypeWebhook, false) - request.Queries[0].Query = &action.TargetSearchQuery_TargetNameQuery{ - TargetNameQuery: &action.TargetNameQuery{ - TargetName: name, - }, - } - response.Details.Timestamp = resp.GetDetails().GetChangeDate() - response.Details.ProcessedSequence = resp.GetDetails().GetSequence() - - response.Result[0].Details.ChangeDate = resp.GetDetails().GetChangeDate() - response.Result[0].Details.Sequence = resp.GetDetails().GetSequence() - response.Result[0].TargetId = resp.GetId() - response.Result[0].Name = name - return nil - }, - req: &action.ListTargetsRequest{ - Queries: []*action.TargetSearchQuery{{}}, - }, - }, - want: &action.ListTargetsResponse{ - Details: &object.ListDetails{ - TotalResult: 1, - }, - Result: []*action.Target{ - { - Details: &object.Details{ - ResourceOwner: Tester.Instance.InstanceID(), - }, - Endpoint: "https://example.com", - TargetType: &action.Target_RestWebhook{ - RestWebhook: &action.SetRESTWebhook{ - InterruptOnError: false, - }, - }, - Timeout: durationpb.New(10 * time.Second), - }, - }, - }, - }, - { - name: "list multiple id", - args: args{ - ctx: CTX, - dep: func(ctx context.Context, request *action.ListTargetsRequest, response *action.ListTargetsResponse) error { - name1 := fmt.Sprint(time.Now().UnixNano() + 1) - name2 := fmt.Sprint(time.Now().UnixNano() + 3) - name3 := fmt.Sprint(time.Now().UnixNano() + 5) - resp1 := Tester.CreateTarget(ctx, t, name1, "https://example.com", domain.TargetTypeWebhook, false) - resp2 := Tester.CreateTarget(ctx, t, name2, "https://example.com", domain.TargetTypeCall, true) - resp3 := Tester.CreateTarget(ctx, t, name3, "https://example.com", domain.TargetTypeAsync, false) - request.Queries[0].Query = &action.TargetSearchQuery_InTargetIdsQuery{ - InTargetIdsQuery: &action.InTargetIDsQuery{ - TargetIds: []string{resp1.GetId(), resp2.GetId(), resp3.GetId()}, - }, - } - response.Details.Timestamp = resp3.GetDetails().GetChangeDate() - response.Details.ProcessedSequence = resp3.GetDetails().GetSequence() - - response.Result[0].Details.ChangeDate = resp1.GetDetails().GetChangeDate() - response.Result[0].Details.Sequence = resp1.GetDetails().GetSequence() - response.Result[0].TargetId = resp1.GetId() - response.Result[0].Name = name1 - response.Result[1].Details.ChangeDate = resp2.GetDetails().GetChangeDate() - response.Result[1].Details.Sequence = resp2.GetDetails().GetSequence() - response.Result[1].TargetId = resp2.GetId() - response.Result[1].Name = name2 - response.Result[2].Details.ChangeDate = resp3.GetDetails().GetChangeDate() - response.Result[2].Details.Sequence = resp3.GetDetails().GetSequence() - response.Result[2].TargetId = resp3.GetId() - response.Result[2].Name = name3 - return nil - }, - req: &action.ListTargetsRequest{ - Queries: []*action.TargetSearchQuery{{}}, - }, - }, - want: &action.ListTargetsResponse{ - Details: &object.ListDetails{ - TotalResult: 3, - }, - Result: []*action.Target{ - { - Details: &object.Details{ - ResourceOwner: Tester.Instance.InstanceID(), - }, - Endpoint: "https://example.com", - TargetType: &action.Target_RestWebhook{ - RestWebhook: &action.SetRESTWebhook{ - InterruptOnError: false, - }, - }, - Timeout: durationpb.New(10 * time.Second), - }, - { - Details: &object.Details{ - ResourceOwner: Tester.Instance.InstanceID(), - }, - Endpoint: "https://example.com", - TargetType: &action.Target_RestCall{ - RestCall: &action.SetRESTCall{ - InterruptOnError: true, - }, - }, - Timeout: durationpb.New(10 * time.Second), - }, - { - Details: &object.Details{ - ResourceOwner: Tester.Instance.InstanceID(), - }, - Endpoint: "https://example.com", - TargetType: &action.Target_RestAsync{ - RestAsync: &action.SetRESTAsync{}, - }, - Timeout: durationpb.New(10 * time.Second), - }, - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if tt.args.dep != nil { - err := tt.args.dep(tt.args.ctx, tt.args.req, tt.want) - require.NoError(t, err) - } - - retryDuration := 5 * time.Second - if ctxDeadline, ok := CTX.Deadline(); ok { - retryDuration = time.Until(ctxDeadline) - } - - require.EventuallyWithT(t, func(ttt *assert.CollectT) { - got, listErr := Client.ListTargets(tt.args.ctx, tt.args.req) - if tt.wantErr { - assert.Error(ttt, listErr, "Error: "+listErr.Error()) - } else { - assert.NoError(ttt, listErr) - } - if listErr != nil { - return - } - // always first check length, otherwise its failed anyway - assert.Len(ttt, got.Result, len(tt.want.Result)) - for i := range tt.want.Result { - assert.Contains(ttt, got.Result, tt.want.Result[i]) - } - integration.AssertListDetails(t, tt.want, got) - }, retryDuration, time.Millisecond*100, "timeout waiting for expected execution result") - }) - } -} - -func TestServer_ListExecutions(t *testing.T) { - ensureFeatureEnabled(t) - targetResp := Tester.CreateTarget(CTX, t, "", "https://example.com", domain.TargetTypeWebhook, false) - - type args struct { - ctx context.Context - dep func(context.Context, *action.ListExecutionsRequest, *action.ListExecutionsResponse) error - req *action.ListExecutionsRequest - } - tests := []struct { - name string - args args - want *action.ListExecutionsResponse - wantErr bool - }{ - { - name: "missing permission", - args: args{ - ctx: Tester.WithAuthorization(context.Background(), integration.OrgOwner), - req: &action.ListExecutionsRequest{}, - }, - wantErr: true, - }, - { - name: "list request single condition", - args: args{ - ctx: CTX, - dep: func(ctx context.Context, request *action.ListExecutionsRequest, response *action.ListExecutionsResponse) error { - cond := request.Queries[0].GetInConditionsQuery().GetConditions()[0] - resp := Tester.SetExecution(ctx, t, cond, executionTargetsSingleTarget(targetResp.GetId())) - - response.Details.Timestamp = resp.GetDetails().GetChangeDate() - // response.Details.ProcessedSequence = resp.GetDetails().GetSequence() - - // Set expected response with used values for SetExecution - response.Result[0].Details.ChangeDate = resp.GetDetails().GetChangeDate() - response.Result[0].Details.Sequence = resp.GetDetails().GetSequence() - response.Result[0].Condition = cond - return nil - }, - req: &action.ListExecutionsRequest{ - Queries: []*action.SearchQuery{{ - Query: &action.SearchQuery_InConditionsQuery{ - InConditionsQuery: &action.InConditionsQuery{ - Conditions: []*action.Condition{{ - ConditionType: &action.Condition_Request{ - Request: &action.RequestExecution{ - Condition: &action.RequestExecution_Method{ - Method: "/zitadel.session.v2.SessionService/GetSession", - }, - }, - }, - }}, - }, - }, - }}, - }, - }, - want: &action.ListExecutionsResponse{ - Details: &object.ListDetails{ - TotalResult: 1, - }, - Result: []*action.Execution{ - { - Details: &object.Details{ - ResourceOwner: Tester.Instance.InstanceID(), - }, - Condition: &action.Condition{ - ConditionType: &action.Condition_Request{ - Request: &action.RequestExecution{ - Condition: &action.RequestExecution_Method{ - Method: "/zitadel.session.v2.SessionService/GetSession", - }, - }, - }, - }, - Targets: executionTargetsSingleTarget(targetResp.GetId()), - }, - }, - }, - }, - { - name: "list request single target", - args: args{ - ctx: CTX, - dep: func(ctx context.Context, request *action.ListExecutionsRequest, response *action.ListExecutionsResponse) error { - target := Tester.CreateTarget(CTX, t, "", "https://example.com", domain.TargetTypeWebhook, false) - // add target as query to the request - request.Queries[0] = &action.SearchQuery{ - Query: &action.SearchQuery_TargetQuery{ - TargetQuery: &action.TargetQuery{ - TargetId: target.GetId(), - }, - }, - } - cond := &action.Condition{ - ConditionType: &action.Condition_Request{ - Request: &action.RequestExecution{ - Condition: &action.RequestExecution_Method{ - Method: "/zitadel.management.v1.ManagementService/UpdateAction", - }, - }, - }, - } - targets := executionTargetsSingleTarget(target.GetId()) - resp := Tester.SetExecution(ctx, t, cond, targets) - - response.Details.Timestamp = resp.GetDetails().GetChangeDate() - response.Details.ProcessedSequence = resp.GetDetails().GetSequence() - - response.Result[0].Details.ChangeDate = resp.GetDetails().GetChangeDate() - response.Result[0].Details.Sequence = resp.GetDetails().GetSequence() - response.Result[0].Condition = cond - response.Result[0].Targets = targets - return nil - }, - req: &action.ListExecutionsRequest{ - Queries: []*action.SearchQuery{{}}, - }, - }, - want: &action.ListExecutionsResponse{ - Details: &object.ListDetails{ - TotalResult: 1, - }, - Result: []*action.Execution{ - { - Details: &object.Details{ - ResourceOwner: Tester.Instance.InstanceID(), - }, - Condition: &action.Condition{}, - Targets: executionTargetsSingleTarget(""), - }, - }, - }, - }, { - name: "list request single include", - args: args{ - ctx: CTX, - dep: func(ctx context.Context, request *action.ListExecutionsRequest, response *action.ListExecutionsResponse) error { - cond := &action.Condition{ - ConditionType: &action.Condition_Request{ - Request: &action.RequestExecution{ - Condition: &action.RequestExecution_Method{ - Method: "/zitadel.management.v1.ManagementService/GetAction", - }, - }, - }, - } - Tester.SetExecution(ctx, t, cond, executionTargetsSingleTarget(targetResp.GetId())) - request.Queries[0].GetIncludeQuery().Include = cond - - includeCond := &action.Condition{ - ConditionType: &action.Condition_Request{ - Request: &action.RequestExecution{ - Condition: &action.RequestExecution_Method{ - Method: "/zitadel.management.v1.ManagementService/ListActions", - }, - }, - }, - } - includeTargets := executionTargetsSingleInclude(cond) - resp2 := Tester.SetExecution(ctx, t, includeCond, includeTargets) - - response.Details.Timestamp = resp2.GetDetails().GetChangeDate() - response.Details.ProcessedSequence = resp2.GetDetails().GetSequence() - - response.Result[0].Details.ChangeDate = resp2.GetDetails().GetChangeDate() - response.Result[0].Details.Sequence = resp2.GetDetails().GetSequence() - response.Result[0].Condition = includeCond - response.Result[0].Targets = includeTargets - return nil - }, - req: &action.ListExecutionsRequest{ - Queries: []*action.SearchQuery{{ - Query: &action.SearchQuery_IncludeQuery{ - IncludeQuery: &action.IncludeQuery{}, - }, - }}, - }, - }, - want: &action.ListExecutionsResponse{ - Details: &object.ListDetails{ - TotalResult: 1, - }, - Result: []*action.Execution{ - { - Details: &object.Details{ - ResourceOwner: Tester.Instance.InstanceID(), - }, - }, - }, - }, - }, - { - name: "list multiple conditions", - args: args{ - ctx: CTX, - dep: func(ctx context.Context, request *action.ListExecutionsRequest, response *action.ListExecutionsResponse) error { - - cond1 := request.Queries[0].GetInConditionsQuery().GetConditions()[0] - targets1 := executionTargetsSingleTarget(targetResp.GetId()) - resp1 := Tester.SetExecution(ctx, t, cond1, targets1) - response.Result[0].Details.ChangeDate = resp1.GetDetails().GetChangeDate() - response.Result[0].Details.Sequence = resp1.GetDetails().GetSequence() - response.Result[0].Condition = cond1 - response.Result[0].Targets = targets1 - - cond2 := request.Queries[0].GetInConditionsQuery().GetConditions()[1] - targets2 := executionTargetsSingleTarget(targetResp.GetId()) - resp2 := Tester.SetExecution(ctx, t, cond2, targets2) - response.Result[1].Details.ChangeDate = resp2.GetDetails().GetChangeDate() - response.Result[1].Details.Sequence = resp2.GetDetails().GetSequence() - response.Result[1].Condition = cond2 - response.Result[1].Targets = targets2 - - cond3 := request.Queries[0].GetInConditionsQuery().GetConditions()[2] - targets3 := executionTargetsSingleTarget(targetResp.GetId()) - resp3 := Tester.SetExecution(ctx, t, cond3, targets3) - response.Result[2].Details.ChangeDate = resp3.GetDetails().GetChangeDate() - response.Result[2].Details.Sequence = resp3.GetDetails().GetSequence() - response.Result[2].Condition = cond3 - response.Result[2].Targets = targets3 - - response.Details.Timestamp = resp3.GetDetails().GetChangeDate() - response.Details.ProcessedSequence = resp3.GetDetails().GetSequence() - return nil - }, - req: &action.ListExecutionsRequest{ - Queries: []*action.SearchQuery{{ - Query: &action.SearchQuery_InConditionsQuery{ - InConditionsQuery: &action.InConditionsQuery{ - Conditions: []*action.Condition{ - { - ConditionType: &action.Condition_Request{ - Request: &action.RequestExecution{ - Condition: &action.RequestExecution_Method{ - Method: "/zitadel.session.v2.SessionService/GetSession", - }, - }, - }, - }, - { - ConditionType: &action.Condition_Request{ - Request: &action.RequestExecution{ - Condition: &action.RequestExecution_Method{ - Method: "/zitadel.session.v2.SessionService/CreateSession", - }, - }, - }, - }, - { - ConditionType: &action.Condition_Request{ - Request: &action.RequestExecution{ - Condition: &action.RequestExecution_Method{ - Method: "/zitadel.session.v2.SessionService/SetSession", - }, - }, - }, - }, - }, - }, - }, - }}, - }, - }, - want: &action.ListExecutionsResponse{ - Details: &object.ListDetails{ - TotalResult: 3, - }, - Result: []*action.Execution{ - { - Details: &object.Details{ - ResourceOwner: Tester.Instance.InstanceID(), - }, - }, { - Details: &object.Details{ - ResourceOwner: Tester.Instance.InstanceID(), - }, - }, { - Details: &object.Details{ - ResourceOwner: Tester.Instance.InstanceID(), - }, - }, - }, - }, - }, - { - name: "list multiple conditions all types", - args: args{ - ctx: CTX, - dep: func(ctx context.Context, request *action.ListExecutionsRequest, response *action.ListExecutionsResponse) error { - targets := executionTargetsSingleTarget(targetResp.GetId()) - for i, cond := range request.Queries[0].GetInConditionsQuery().GetConditions() { - resp := Tester.SetExecution(ctx, t, cond, targets) - response.Result[i].Details.ChangeDate = resp.GetDetails().GetChangeDate() - response.Result[i].Details.Sequence = resp.GetDetails().GetSequence() - response.Result[i].Condition = cond - response.Result[i].Targets = targets - - // filled with info of last sequence - response.Details.Timestamp = resp.GetDetails().GetChangeDate() - response.Details.ProcessedSequence = resp.GetDetails().GetSequence() - } - - return nil - }, - req: &action.ListExecutionsRequest{ - Queries: []*action.SearchQuery{{ - Query: &action.SearchQuery_InConditionsQuery{ - InConditionsQuery: &action.InConditionsQuery{ - Conditions: []*action.Condition{ - {ConditionType: &action.Condition_Request{Request: &action.RequestExecution{Condition: &action.RequestExecution_Method{Method: "/zitadel.session.v2.SessionService/GetSession"}}}}, - {ConditionType: &action.Condition_Request{Request: &action.RequestExecution{Condition: &action.RequestExecution_Service{Service: "zitadel.session.v2.SessionService"}}}}, - {ConditionType: &action.Condition_Request{Request: &action.RequestExecution{Condition: &action.RequestExecution_All{All: true}}}}, - {ConditionType: &action.Condition_Response{Response: &action.ResponseExecution{Condition: &action.ResponseExecution_Method{Method: "/zitadel.session.v2.SessionService/GetSession"}}}}, - {ConditionType: &action.Condition_Response{Response: &action.ResponseExecution{Condition: &action.ResponseExecution_Service{Service: "zitadel.session.v2.SessionService"}}}}, - {ConditionType: &action.Condition_Response{Response: &action.ResponseExecution{Condition: &action.ResponseExecution_All{All: true}}}}, - {ConditionType: &action.Condition_Event{Event: &action.EventExecution{Condition: &action.EventExecution_Event{Event: "user.added"}}}}, - {ConditionType: &action.Condition_Event{Event: &action.EventExecution{Condition: &action.EventExecution_Group{Group: "user"}}}}, - {ConditionType: &action.Condition_Event{Event: &action.EventExecution{Condition: &action.EventExecution_All{All: true}}}}, - {ConditionType: &action.Condition_Function{Function: &action.FunctionExecution{Name: "Action.Flow.Type.ExternalAuthentication.Action.TriggerType.PostAuthentication"}}}, - }, - }, - }, - }}, - }, - }, - want: &action.ListExecutionsResponse{ - Details: &object.ListDetails{ - TotalResult: 10, - }, - Result: []*action.Execution{ - {Details: &object.Details{ResourceOwner: Tester.Instance.InstanceID()}}, - {Details: &object.Details{ResourceOwner: Tester.Instance.InstanceID()}}, - {Details: &object.Details{ResourceOwner: Tester.Instance.InstanceID()}}, - {Details: &object.Details{ResourceOwner: Tester.Instance.InstanceID()}}, - {Details: &object.Details{ResourceOwner: Tester.Instance.InstanceID()}}, - {Details: &object.Details{ResourceOwner: Tester.Instance.InstanceID()}}, - {Details: &object.Details{ResourceOwner: Tester.Instance.InstanceID()}}, - {Details: &object.Details{ResourceOwner: Tester.Instance.InstanceID()}}, - {Details: &object.Details{ResourceOwner: Tester.Instance.InstanceID()}}, - {Details: &object.Details{ResourceOwner: Tester.Instance.InstanceID()}}, - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if tt.args.dep != nil { - err := tt.args.dep(tt.args.ctx, tt.args.req, tt.want) - require.NoError(t, err) - } - - retryDuration := 5 * time.Second - if ctxDeadline, ok := CTX.Deadline(); ok { - retryDuration = time.Until(ctxDeadline) - } - - require.EventuallyWithT(t, func(ttt *assert.CollectT) { - got, listErr := Client.ListExecutions(tt.args.ctx, tt.args.req) - if tt.wantErr { - assert.Error(t, listErr, "Error: "+listErr.Error()) - } else { - assert.NoError(t, listErr) - } - if listErr != nil { - return - } - // always first check length, otherwise its failed anyway - assert.Len(t, got.Result, len(tt.want.Result)) - for i := range tt.want.Result { - // as not sorted, all elements have to be checked - // workaround as oneof elements can only be checked with assert.EqualExportedValues() - if j, found := containExecution(got.Result, tt.want.Result[i]); found { - assert.EqualExportedValues(t, tt.want.Result[i], got.Result[j]) - } - } - integration.AssertListDetails(t, tt.want, got) - }, retryDuration, time.Millisecond*100, "timeout waiting for expected execution result") - }) - } -} - -func containExecution(executionList []*action.Execution, execution *action.Execution) (int, bool) { - for i, exec := range executionList { - if reflect.DeepEqual(exec.Details, execution.Details) { - return i, true - } - } - return 0, false -} diff --git a/internal/api/grpc/action/v3alpha/target.go b/internal/api/grpc/action/v3alpha/target.go deleted file mode 100644 index c57d5b607f..0000000000 --- a/internal/api/grpc/action/v3alpha/target.go +++ /dev/null @@ -1,112 +0,0 @@ -package action - -import ( - "context" - - "github.com/muhlemmer/gu" - - "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/grpc/object/v2" - "github.com/zitadel/zitadel/internal/command" - "github.com/zitadel/zitadel/internal/domain" - "github.com/zitadel/zitadel/internal/eventstore/v1/models" - action "github.com/zitadel/zitadel/pkg/grpc/action/v3alpha" -) - -func (s *Server) CreateTarget(ctx context.Context, req *action.CreateTargetRequest) (*action.CreateTargetResponse, error) { - if err := checkExecutionEnabled(ctx); err != nil { - return nil, err - } - - add := createTargetToCommand(req) - details, err := s.command.AddTarget(ctx, add, authz.GetInstance(ctx).InstanceID()) - if err != nil { - return nil, err - } - return &action.CreateTargetResponse{ - Id: add.AggregateID, - Details: object.DomainToDetailsPb(details), - }, nil -} - -func (s *Server) UpdateTarget(ctx context.Context, req *action.UpdateTargetRequest) (*action.UpdateTargetResponse, error) { - if err := checkExecutionEnabled(ctx); err != nil { - return nil, err - } - - details, err := s.command.ChangeTarget(ctx, updateTargetToCommand(req), authz.GetInstance(ctx).InstanceID()) - if err != nil { - return nil, err - } - return &action.UpdateTargetResponse{ - Details: object.DomainToDetailsPb(details), - }, nil -} - -func (s *Server) DeleteTarget(ctx context.Context, req *action.DeleteTargetRequest) (*action.DeleteTargetResponse, error) { - if err := checkExecutionEnabled(ctx); err != nil { - return nil, err - } - - details, err := s.command.DeleteTarget(ctx, req.GetTargetId(), authz.GetInstance(ctx).InstanceID()) - if err != nil { - return nil, err - } - return &action.DeleteTargetResponse{ - Details: object.DomainToDetailsPb(details), - }, nil -} - -func createTargetToCommand(req *action.CreateTargetRequest) *command.AddTarget { - var ( - targetType domain.TargetType - interruptOnError bool - ) - switch t := req.GetTargetType().(type) { - case *action.CreateTargetRequest_RestWebhook: - targetType = domain.TargetTypeWebhook - interruptOnError = t.RestWebhook.InterruptOnError - case *action.CreateTargetRequest_RestCall: - targetType = domain.TargetTypeCall - interruptOnError = t.RestCall.InterruptOnError - case *action.CreateTargetRequest_RestAsync: - targetType = domain.TargetTypeAsync - } - return &command.AddTarget{ - Name: req.GetName(), - TargetType: targetType, - Endpoint: req.GetEndpoint(), - Timeout: req.GetTimeout().AsDuration(), - InterruptOnError: interruptOnError, - } -} - -func updateTargetToCommand(req *action.UpdateTargetRequest) *command.ChangeTarget { - if req == nil { - return nil - } - target := &command.ChangeTarget{ - ObjectRoot: models.ObjectRoot{ - AggregateID: req.GetTargetId(), - }, - Name: req.Name, - Endpoint: req.Endpoint, - } - if req.TargetType != nil { - switch t := req.GetTargetType().(type) { - case *action.UpdateTargetRequest_RestWebhook: - target.TargetType = gu.Ptr(domain.TargetTypeWebhook) - target.InterruptOnError = gu.Ptr(t.RestWebhook.InterruptOnError) - case *action.UpdateTargetRequest_RestCall: - target.TargetType = gu.Ptr(domain.TargetTypeCall) - target.InterruptOnError = gu.Ptr(t.RestCall.InterruptOnError) - case *action.UpdateTargetRequest_RestAsync: - target.TargetType = gu.Ptr(domain.TargetTypeAsync) - target.InterruptOnError = gu.Ptr(false) - } - } - if req.Timeout != nil { - target.Timeout = gu.Ptr(req.GetTimeout().AsDuration()) - } - return target -} diff --git a/internal/api/grpc/action/v3alpha/target_integration_test.go b/internal/api/grpc/action/v3alpha/target_integration_test.go deleted file mode 100644 index 539f3c6d35..0000000000 --- a/internal/api/grpc/action/v3alpha/target_integration_test.go +++ /dev/null @@ -1,423 +0,0 @@ -//go:build integration - -package action_test - -import ( - "context" - "fmt" - "testing" - "time" - - "github.com/muhlemmer/gu" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "google.golang.org/protobuf/types/known/durationpb" - "google.golang.org/protobuf/types/known/timestamppb" - - "github.com/zitadel/zitadel/internal/domain" - "github.com/zitadel/zitadel/internal/integration" - action "github.com/zitadel/zitadel/pkg/grpc/action/v3alpha" - "github.com/zitadel/zitadel/pkg/grpc/object/v2" -) - -func TestServer_CreateTarget(t *testing.T) { - ensureFeatureEnabled(t) - tests := []struct { - name string - ctx context.Context - req *action.CreateTargetRequest - want *action.CreateTargetResponse - wantErr bool - }{ - { - name: "missing permission", - ctx: Tester.WithAuthorization(context.Background(), integration.OrgOwner), - req: &action.CreateTargetRequest{ - Name: fmt.Sprint(time.Now().UnixNano() + 1), - }, - wantErr: true, - }, - { - name: "empty name", - ctx: CTX, - req: &action.CreateTargetRequest{ - Name: "", - }, - wantErr: true, - }, - { - name: "empty type", - ctx: CTX, - req: &action.CreateTargetRequest{ - Name: fmt.Sprint(time.Now().UnixNano() + 1), - TargetType: nil, - }, - wantErr: true, - }, - { - name: "empty webhook url", - ctx: CTX, - req: &action.CreateTargetRequest{ - Name: fmt.Sprint(time.Now().UnixNano() + 1), - TargetType: &action.CreateTargetRequest_RestWebhook{ - RestWebhook: &action.SetRESTWebhook{}, - }, - }, - wantErr: true, - }, - { - name: "empty request response url", - ctx: CTX, - req: &action.CreateTargetRequest{ - Name: fmt.Sprint(time.Now().UnixNano() + 1), - TargetType: &action.CreateTargetRequest_RestCall{ - RestCall: &action.SetRESTCall{}, - }, - }, - wantErr: true, - }, - { - name: "empty timeout", - ctx: CTX, - req: &action.CreateTargetRequest{ - Name: fmt.Sprint(time.Now().UnixNano() + 1), - Endpoint: "https://example.com", - TargetType: &action.CreateTargetRequest_RestWebhook{ - RestWebhook: &action.SetRESTWebhook{}, - }, - Timeout: nil, - }, - wantErr: true, - }, - { - name: "async, ok", - ctx: CTX, - req: &action.CreateTargetRequest{ - Name: fmt.Sprint(time.Now().UnixNano() + 1), - Endpoint: "https://example.com", - TargetType: &action.CreateTargetRequest_RestAsync{ - RestAsync: &action.SetRESTAsync{}, - }, - Timeout: durationpb.New(10 * time.Second), - }, - want: &action.CreateTargetResponse{ - Details: &object.Details{ - ChangeDate: timestamppb.Now(), - ResourceOwner: Tester.Instance.InstanceID(), - }, - }, - }, - { - name: "webhook, ok", - ctx: CTX, - req: &action.CreateTargetRequest{ - Name: fmt.Sprint(time.Now().UnixNano() + 1), - Endpoint: "https://example.com", - TargetType: &action.CreateTargetRequest_RestWebhook{ - RestWebhook: &action.SetRESTWebhook{ - InterruptOnError: false, - }, - }, - Timeout: durationpb.New(10 * time.Second), - }, - want: &action.CreateTargetResponse{ - Details: &object.Details{ - ChangeDate: timestamppb.Now(), - ResourceOwner: Tester.Instance.InstanceID(), - }, - }, - }, - { - name: "webhook, interrupt on error, ok", - ctx: CTX, - req: &action.CreateTargetRequest{ - Name: fmt.Sprint(time.Now().UnixNano() + 1), - Endpoint: "https://example.com", - TargetType: &action.CreateTargetRequest_RestWebhook{ - RestWebhook: &action.SetRESTWebhook{ - InterruptOnError: true, - }, - }, - Timeout: durationpb.New(10 * time.Second), - }, - want: &action.CreateTargetResponse{ - Details: &object.Details{ - ChangeDate: timestamppb.Now(), - ResourceOwner: Tester.Instance.InstanceID(), - }, - }, - }, - { - name: "call, ok", - ctx: CTX, - req: &action.CreateTargetRequest{ - Name: fmt.Sprint(time.Now().UnixNano() + 1), - Endpoint: "https://example.com", - TargetType: &action.CreateTargetRequest_RestCall{ - RestCall: &action.SetRESTCall{ - InterruptOnError: false, - }, - }, - Timeout: durationpb.New(10 * time.Second), - }, - want: &action.CreateTargetResponse{ - Details: &object.Details{ - ChangeDate: timestamppb.Now(), - ResourceOwner: Tester.Instance.InstanceID(), - }, - }, - }, - - { - name: "call, interruptOnError, ok", - ctx: CTX, - req: &action.CreateTargetRequest{ - Name: fmt.Sprint(time.Now().UnixNano() + 1), - Endpoint: "https://example.com", - TargetType: &action.CreateTargetRequest_RestCall{ - RestCall: &action.SetRESTCall{ - InterruptOnError: true, - }, - }, - Timeout: durationpb.New(10 * time.Second), - }, - want: &action.CreateTargetResponse{ - Details: &object.Details{ - ChangeDate: timestamppb.Now(), - ResourceOwner: Tester.Instance.InstanceID(), - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := Client.CreateTarget(tt.ctx, tt.req) - if tt.wantErr { - require.Error(t, err) - return - } - require.NoError(t, err) - - integration.AssertDetails(t, tt.want, got) - assert.NotEmpty(t, got.GetId()) - }) - } -} - -func TestServer_UpdateTarget(t *testing.T) { - ensureFeatureEnabled(t) - type args struct { - ctx context.Context - req *action.UpdateTargetRequest - } - tests := []struct { - name string - prepare func(request *action.UpdateTargetRequest) error - args args - want *action.UpdateTargetResponse - wantErr bool - }{ - { - name: "missing permission", - prepare: func(request *action.UpdateTargetRequest) error { - targetID := Tester.CreateTarget(CTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetId() - request.TargetId = targetID - return nil - }, - args: args{ - ctx: Tester.WithAuthorization(context.Background(), integration.OrgOwner), - req: &action.UpdateTargetRequest{ - Name: gu.Ptr(fmt.Sprint(time.Now().UnixNano() + 1)), - }, - }, - wantErr: true, - }, - { - name: "not existing", - prepare: func(request *action.UpdateTargetRequest) error { - request.TargetId = "notexisting" - return nil - }, - args: args{ - ctx: CTX, - req: &action.UpdateTargetRequest{ - Name: gu.Ptr(fmt.Sprint(time.Now().UnixNano() + 1)), - }, - }, - wantErr: true, - }, - { - name: "change name, ok", - prepare: func(request *action.UpdateTargetRequest) error { - targetID := Tester.CreateTarget(CTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetId() - request.TargetId = targetID - return nil - }, - args: args{ - ctx: CTX, - req: &action.UpdateTargetRequest{ - Name: gu.Ptr(fmt.Sprint(time.Now().UnixNano() + 1)), - }, - }, - want: &action.UpdateTargetResponse{ - Details: &object.Details{ - ChangeDate: timestamppb.Now(), - ResourceOwner: Tester.Instance.InstanceID(), - }, - }, - }, - { - name: "change type, ok", - prepare: func(request *action.UpdateTargetRequest) error { - targetID := Tester.CreateTarget(CTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetId() - request.TargetId = targetID - return nil - }, - args: args{ - ctx: CTX, - req: &action.UpdateTargetRequest{ - TargetType: &action.UpdateTargetRequest_RestCall{ - RestCall: &action.SetRESTCall{ - InterruptOnError: true, - }, - }, - }, - }, - want: &action.UpdateTargetResponse{ - Details: &object.Details{ - ChangeDate: timestamppb.Now(), - ResourceOwner: Tester.Instance.InstanceID(), - }, - }, - }, - { - name: "change url, ok", - prepare: func(request *action.UpdateTargetRequest) error { - targetID := Tester.CreateTarget(CTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetId() - request.TargetId = targetID - return nil - }, - args: args{ - ctx: CTX, - req: &action.UpdateTargetRequest{ - Endpoint: gu.Ptr("https://example.com/hooks/new"), - }, - }, - want: &action.UpdateTargetResponse{ - Details: &object.Details{ - ChangeDate: timestamppb.Now(), - ResourceOwner: Tester.Instance.InstanceID(), - }, - }, - }, - { - name: "change timeout, ok", - prepare: func(request *action.UpdateTargetRequest) error { - targetID := Tester.CreateTarget(CTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetId() - request.TargetId = targetID - return nil - }, - args: args{ - ctx: CTX, - req: &action.UpdateTargetRequest{ - Timeout: durationpb.New(20 * time.Second), - }, - }, - want: &action.UpdateTargetResponse{ - Details: &object.Details{ - ChangeDate: timestamppb.Now(), - ResourceOwner: Tester.Instance.InstanceID(), - }, - }, - }, - { - name: "change type async, ok", - prepare: func(request *action.UpdateTargetRequest) error { - targetID := Tester.CreateTarget(CTX, t, "", "https://example.com", domain.TargetTypeAsync, false).GetId() - request.TargetId = targetID - return nil - }, - args: args{ - ctx: CTX, - req: &action.UpdateTargetRequest{ - TargetType: &action.UpdateTargetRequest_RestAsync{ - RestAsync: &action.SetRESTAsync{}, - }, - }, - }, - want: &action.UpdateTargetResponse{ - Details: &object.Details{ - ChangeDate: timestamppb.Now(), - ResourceOwner: Tester.Instance.InstanceID(), - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - err := tt.prepare(tt.args.req) - require.NoError(t, err) - - got, err := Client.UpdateTarget(tt.args.ctx, tt.args.req) - if tt.wantErr { - require.Error(t, err) - return - } - require.NoError(t, err) - integration.AssertDetails(t, tt.want, got) - }) - } -} - -func TestServer_DeleteTarget(t *testing.T) { - ensureFeatureEnabled(t) - target := Tester.CreateTarget(CTX, t, "", "https://example.com", domain.TargetTypeWebhook, false) - tests := []struct { - name string - ctx context.Context - req *action.DeleteTargetRequest - want *action.DeleteTargetResponse - wantErr bool - }{ - { - name: "missing permission", - ctx: Tester.WithAuthorization(context.Background(), integration.OrgOwner), - req: &action.DeleteTargetRequest{ - TargetId: target.GetId(), - }, - wantErr: true, - }, - { - name: "empty id", - ctx: CTX, - req: &action.DeleteTargetRequest{ - TargetId: "", - }, - wantErr: true, - }, - { - name: "delete target", - ctx: CTX, - req: &action.DeleteTargetRequest{ - TargetId: target.GetId(), - }, - want: &action.DeleteTargetResponse{ - Details: &object.Details{ - ChangeDate: timestamppb.Now(), - ResourceOwner: Tester.Instance.InstanceID(), - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := Client.DeleteTarget(tt.ctx, tt.req) - if tt.wantErr { - require.Error(t, err) - return - } - require.NoError(t, err) - integration.AssertDetails(t, tt.want, got) - }) - } -} diff --git a/internal/api/grpc/action/v3alpha/execution.go b/internal/api/grpc/resources/action/v3alpha/execution.go similarity index 64% rename from internal/api/grpc/action/v3alpha/execution.go rename to internal/api/grpc/resources/action/v3alpha/execution.go index 58a36cff22..668b6b5261 100644 --- a/internal/api/grpc/action/v3alpha/execution.go +++ b/internal/api/grpc/resources/action/v3alpha/execution.go @@ -4,38 +4,22 @@ import ( "context" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/grpc/object/v2" + settings_object "github.com/zitadel/zitadel/internal/api/grpc/settings/object/v3alpha" "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/repository/execution" - action "github.com/zitadel/zitadel/pkg/grpc/action/v3alpha" + "github.com/zitadel/zitadel/internal/zerrors" + object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha" + action "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha" ) -func (s *Server) ListExecutionFunctions(_ context.Context, _ *action.ListExecutionFunctionsRequest) (*action.ListExecutionFunctionsResponse, error) { - return &action.ListExecutionFunctionsResponse{ - Functions: s.ListActionFunctions(), - }, nil -} - -func (s *Server) ListExecutionMethods(_ context.Context, _ *action.ListExecutionMethodsRequest) (*action.ListExecutionMethodsResponse, error) { - return &action.ListExecutionMethodsResponse{ - Methods: s.ListGRPCMethods(), - }, nil -} - -func (s *Server) ListExecutionServices(_ context.Context, _ *action.ListExecutionServicesRequest) (*action.ListExecutionServicesResponse, error) { - return &action.ListExecutionServicesResponse{ - Services: s.ListGRPCServices(), - }, nil -} - func (s *Server) SetExecution(ctx context.Context, req *action.SetExecutionRequest) (*action.SetExecutionResponse, error) { if err := checkExecutionEnabled(ctx); err != nil { return nil, err } - - targets := make([]*execution.Target, len(req.Targets)) - for i, target := range req.Targets { + reqTargets := req.GetExecution().GetTargets() + targets := make([]*execution.Target, len(reqTargets)) + for i, target := range reqTargets { switch t := target.GetType().(type) { case *action.ExecutionTargetType_Include: include, err := conditionToInclude(t.Include) @@ -50,36 +34,32 @@ func (s *Server) SetExecution(ctx context.Context, req *action.SetExecutionReque set := &command.SetExecution{ Targets: targets, } - + owner := &object.Owner{ + Type: object.OwnerType_OWNER_TYPE_INSTANCE, + Id: authz.GetInstance(ctx).InstanceID(), + } var err error var details *domain.ObjectDetails switch t := req.GetCondition().GetConditionType().(type) { case *action.Condition_Request: cond := executionConditionFromRequest(t.Request) - details, err = s.command.SetExecutionRequest(ctx, cond, set, authz.GetInstance(ctx).InstanceID()) - if err != nil { - return nil, err - } + details, err = s.command.SetExecutionRequest(ctx, cond, set, owner.Id) case *action.Condition_Response: cond := executionConditionFromResponse(t.Response) - details, err = s.command.SetExecutionResponse(ctx, cond, set, authz.GetInstance(ctx).InstanceID()) - if err != nil { - return nil, err - } + details, err = s.command.SetExecutionResponse(ctx, cond, set, owner.Id) case *action.Condition_Event: cond := executionConditionFromEvent(t.Event) - details, err = s.command.SetExecutionEvent(ctx, cond, set, authz.GetInstance(ctx).InstanceID()) - if err != nil { - return nil, err - } + details, err = s.command.SetExecutionEvent(ctx, cond, set, owner.Id) case *action.Condition_Function: - details, err = s.command.SetExecutionFunction(ctx, command.ExecutionFunctionCondition(t.Function.GetName()), set, authz.GetInstance(ctx).InstanceID()) - if err != nil { - return nil, err - } + details, err = s.command.SetExecutionFunction(ctx, command.ExecutionFunctionCondition(t.Function.GetName()), set, owner.Id) + default: + err = zerrors.ThrowInvalidArgument(nil, "ACTION-5r5Ju", "Errors.Execution.ConditionInvalid") + } + if err != nil { + return nil, err } return &action.SetExecutionResponse{ - Details: object.DomainToDetailsPb(details), + Details: settings_object.DomainToDetailsPb(details, owner), }, nil } @@ -109,44 +89,26 @@ func conditionToInclude(cond *action.Condition) (string, error) { return "", err } return cond.ID(), nil + default: + return "", zerrors.ThrowInvalidArgument(nil, "ACTION-9BBob", "Errors.Execution.ConditionInvalid") } - return "", nil } -func (s *Server) DeleteExecution(ctx context.Context, req *action.DeleteExecutionRequest) (*action.DeleteExecutionResponse, error) { - if err := checkExecutionEnabled(ctx); err != nil { - return nil, err - } +func (s *Server) ListExecutionFunctions(_ context.Context, _ *action.ListExecutionFunctionsRequest) (*action.ListExecutionFunctionsResponse, error) { + return &action.ListExecutionFunctionsResponse{ + Functions: s.ListActionFunctions(), + }, nil +} - var err error - var details *domain.ObjectDetails - switch t := req.GetCondition().GetConditionType().(type) { - case *action.Condition_Request: - cond := executionConditionFromRequest(t.Request) - details, err = s.command.DeleteExecutionRequest(ctx, cond, authz.GetInstance(ctx).InstanceID()) - if err != nil { - return nil, err - } - case *action.Condition_Response: - cond := executionConditionFromResponse(t.Response) - details, err = s.command.DeleteExecutionResponse(ctx, cond, authz.GetInstance(ctx).InstanceID()) - if err != nil { - return nil, err - } - case *action.Condition_Event: - cond := executionConditionFromEvent(t.Event) - details, err = s.command.DeleteExecutionEvent(ctx, cond, authz.GetInstance(ctx).InstanceID()) - if err != nil { - return nil, err - } - case *action.Condition_Function: - details, err = s.command.DeleteExecutionFunction(ctx, command.ExecutionFunctionCondition(t.Function.GetName()), authz.GetInstance(ctx).InstanceID()) - if err != nil { - return nil, err - } - } - return &action.DeleteExecutionResponse{ - Details: object.DomainToDetailsPb(details), +func (s *Server) ListExecutionMethods(_ context.Context, _ *action.ListExecutionMethodsRequest) (*action.ListExecutionMethodsResponse, error) { + return &action.ListExecutionMethodsResponse{ + Methods: s.ListGRPCMethods(), + }, nil +} + +func (s *Server) ListExecutionServices(_ context.Context, _ *action.ListExecutionServicesRequest) (*action.ListExecutionServicesResponse, error) { + return &action.ListExecutionServicesResponse{ + Services: s.ListGRPCServices(), }, nil } diff --git a/internal/api/grpc/resources/action/v3alpha/execution_integration_test.go b/internal/api/grpc/resources/action/v3alpha/execution_integration_test.go new file mode 100644 index 0000000000..9713a3c578 --- /dev/null +++ b/internal/api/grpc/resources/action/v3alpha/execution_integration_test.go @@ -0,0 +1,805 @@ +//go:build integration + +package action_test + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/integration" + object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha" + action "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha" + settings_object "github.com/zitadel/zitadel/pkg/grpc/settings/object/v3alpha" +) + +func executionTargetsSingleTarget(id string) []*action.ExecutionTargetType { + return []*action.ExecutionTargetType{{Type: &action.ExecutionTargetType_Target{Target: id}}} +} + +func executionTargetsSingleInclude(include *action.Condition) []*action.ExecutionTargetType { + return []*action.ExecutionTargetType{{Type: &action.ExecutionTargetType_Include{Include: include}}} +} + +func TestServer_SetExecution_Request(t *testing.T) { + ensureFeatureEnabled(t) + targetResp := Tester.CreateTarget(CTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false) + + tests := []struct { + name string + ctx context.Context + req *action.SetExecutionRequest + want *action.SetExecutionResponse + wantErr bool + }{ + { + name: "missing permission", + ctx: Tester.WithAuthorization(context.Background(), integration.OrgOwner), + req: &action.SetExecutionRequest{ + Condition: &action.Condition{ + ConditionType: &action.Condition_Request{ + Request: &action.RequestExecution{ + Condition: &action.RequestExecution_All{All: true}, + }, + }, + }, + }, + wantErr: true, + }, + { + name: "no condition, error", + ctx: CTX, + req: &action.SetExecutionRequest{ + Condition: &action.Condition{ + ConditionType: &action.Condition_Request{ + Request: &action.RequestExecution{}, + }, + }, + Execution: &action.Execution{ + Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()), + }, + }, + wantErr: true, + }, + { + name: "method, not existing", + ctx: CTX, + req: &action.SetExecutionRequest{ + Condition: &action.Condition{ + ConditionType: &action.Condition_Request{ + Request: &action.RequestExecution{ + Condition: &action.RequestExecution_Method{ + Method: "/zitadel.session.v2beta.NotExistingService/List", + }, + }, + }, + }, + Execution: &action.Execution{ + Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()), + }, + }, + wantErr: true, + }, + { + name: "method, ok", + ctx: CTX, + req: &action.SetExecutionRequest{ + Condition: &action.Condition{ + ConditionType: &action.Condition_Request{ + Request: &action.RequestExecution{ + Condition: &action.RequestExecution_Method{ + Method: "/zitadel.session.v2beta.SessionService/ListSessions", + }, + }, + }, + }, + Execution: &action.Execution{ + Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()), + }, + }, + want: &action.SetExecutionResponse{ + Details: &settings_object.Details{ + ChangeDate: timestamppb.Now(), + Owner: &object.Owner{ + Type: object.OwnerType_OWNER_TYPE_INSTANCE, + Id: Tester.Instance.InstanceID(), + }, + }, + }, + }, + { + name: "service, not existing", + ctx: CTX, + req: &action.SetExecutionRequest{ + Condition: &action.Condition{ + ConditionType: &action.Condition_Request{ + Request: &action.RequestExecution{ + Condition: &action.RequestExecution_Service{ + Service: "NotExistingService", + }, + }, + }, + }, + Execution: &action.Execution{ + Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()), + }, + }, + wantErr: true, + }, + { + name: "service, ok", + ctx: CTX, + req: &action.SetExecutionRequest{ + Condition: &action.Condition{ + ConditionType: &action.Condition_Request{ + Request: &action.RequestExecution{ + Condition: &action.RequestExecution_Service{ + Service: "zitadel.session.v2beta.SessionService", + }, + }, + }, + }, + Execution: &action.Execution{ + Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()), + }, + }, + want: &action.SetExecutionResponse{ + Details: &settings_object.Details{ + ChangeDate: timestamppb.Now(), + Owner: &object.Owner{ + Type: object.OwnerType_OWNER_TYPE_INSTANCE, + Id: Tester.Instance.InstanceID(), + }, + }, + }, + }, + { + name: "all, ok", + ctx: CTX, + req: &action.SetExecutionRequest{ + Condition: &action.Condition{ + ConditionType: &action.Condition_Request{ + Request: &action.RequestExecution{ + Condition: &action.RequestExecution_All{ + All: true, + }, + }, + }, + }, + Execution: &action.Execution{ + Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()), + }, + }, + want: &action.SetExecutionResponse{ + Details: &settings_object.Details{ + ChangeDate: timestamppb.Now(), + Owner: &object.Owner{ + Type: object.OwnerType_OWNER_TYPE_INSTANCE, + Id: Tester.Instance.InstanceID(), + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // We want to have the same response no matter how often we call the function + Client.SetExecution(tt.ctx, tt.req) + got, err := Client.SetExecution(tt.ctx, tt.req) + if tt.wantErr { + require.Error(t, err) + return + } + + require.NoError(t, err) + + integration.AssertSettingsDetails(t, tt.want.Details, got.Details) + + // cleanup to not impact other requests + Tester.DeleteExecution(tt.ctx, t, tt.req.GetCondition()) + }) + } +} + +func TestServer_SetExecution_Request_Include(t *testing.T) { + ensureFeatureEnabled(t) + targetResp := Tester.CreateTarget(CTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false) + executionCond := &action.Condition{ + ConditionType: &action.Condition_Request{ + Request: &action.RequestExecution{ + Condition: &action.RequestExecution_All{ + All: true, + }, + }, + }, + } + Tester.SetExecution(CTX, t, + executionCond, + executionTargetsSingleTarget(targetResp.GetDetails().GetId()), + ) + + circularExecutionService := &action.Condition{ + ConditionType: &action.Condition_Request{ + Request: &action.RequestExecution{ + Condition: &action.RequestExecution_Service{ + Service: "zitadel.session.v2beta.SessionService", + }, + }, + }, + } + Tester.SetExecution(CTX, t, + circularExecutionService, + executionTargetsSingleInclude(executionCond), + ) + circularExecutionMethod := &action.Condition{ + ConditionType: &action.Condition_Request{ + Request: &action.RequestExecution{ + Condition: &action.RequestExecution_Method{ + Method: "/zitadel.session.v2beta.SessionService/ListSessions", + }, + }, + }, + } + Tester.SetExecution(CTX, t, + circularExecutionMethod, + executionTargetsSingleInclude(circularExecutionService), + ) + + tests := []struct { + name string + ctx context.Context + req *action.SetExecutionRequest + want *action.SetExecutionResponse + wantErr bool + }{ + { + name: "method, circular error", + ctx: CTX, + req: &action.SetExecutionRequest{ + Condition: circularExecutionService, + Execution: &action.Execution{ + Targets: executionTargetsSingleInclude(circularExecutionMethod), + }, + }, + wantErr: true, + }, + { + name: "method, ok", + ctx: CTX, + req: &action.SetExecutionRequest{ + Condition: &action.Condition{ + ConditionType: &action.Condition_Request{ + Request: &action.RequestExecution{ + Condition: &action.RequestExecution_Method{ + Method: "/zitadel.session.v2beta.SessionService/ListSessions", + }, + }, + }, + }, + Execution: &action.Execution{ + + Targets: executionTargetsSingleInclude(executionCond), + }, + }, + want: &action.SetExecutionResponse{ + Details: &settings_object.Details{ + ChangeDate: timestamppb.Now(), + Owner: &object.Owner{ + Type: object.OwnerType_OWNER_TYPE_INSTANCE, + Id: Tester.Instance.InstanceID(), + }, + }, + }, + }, + { + name: "service, ok", + ctx: CTX, + req: &action.SetExecutionRequest{ + Condition: &action.Condition{ + ConditionType: &action.Condition_Request{ + Request: &action.RequestExecution{ + Condition: &action.RequestExecution_Service{ + Service: "zitadel.session.v2beta.SessionService", + }, + }, + }, + }, + Execution: &action.Execution{ + + Targets: executionTargetsSingleInclude(executionCond), + }, + }, + want: &action.SetExecutionResponse{ + Details: &settings_object.Details{ + ChangeDate: timestamppb.Now(), + Owner: &object.Owner{ + Type: object.OwnerType_OWNER_TYPE_INSTANCE, + Id: Tester.Instance.InstanceID(), + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // We want to have the same response no matter how often we call the function + Client.SetExecution(tt.ctx, tt.req) + got, err := Client.SetExecution(tt.ctx, tt.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + + integration.AssertSettingsDetails(t, tt.want.Details, got.Details) + + // cleanup to not impact other requests + Tester.DeleteExecution(tt.ctx, t, tt.req.GetCondition()) + }) + } +} + +func TestServer_SetExecution_Response(t *testing.T) { + ensureFeatureEnabled(t) + targetResp := Tester.CreateTarget(CTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false) + + tests := []struct { + name string + ctx context.Context + req *action.SetExecutionRequest + want *action.SetExecutionResponse + wantErr bool + }{ + { + name: "missing permission", + ctx: Tester.WithAuthorization(context.Background(), integration.OrgOwner), + req: &action.SetExecutionRequest{ + Condition: &action.Condition{ + ConditionType: &action.Condition_Response{ + Response: &action.ResponseExecution{ + Condition: &action.ResponseExecution_All{All: true}, + }, + }, + }, + }, + wantErr: true, + }, + { + name: "no condition, error", + ctx: CTX, + req: &action.SetExecutionRequest{ + Condition: &action.Condition{ + ConditionType: &action.Condition_Response{ + Response: &action.ResponseExecution{}, + }, + }, + Execution: &action.Execution{ + Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()), + }, + }, + wantErr: true, + }, + { + name: "method, not existing", + ctx: CTX, + req: &action.SetExecutionRequest{ + Condition: &action.Condition{ + ConditionType: &action.Condition_Response{ + Response: &action.ResponseExecution{ + Condition: &action.ResponseExecution_Method{ + Method: "/zitadel.session.v2beta.NotExistingService/List", + }, + }, + }, + }, + Execution: &action.Execution{ + Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()), + }, + }, + wantErr: true, + }, + { + name: "method, ok", + ctx: CTX, + req: &action.SetExecutionRequest{ + Condition: &action.Condition{ + ConditionType: &action.Condition_Response{ + Response: &action.ResponseExecution{ + Condition: &action.ResponseExecution_Method{ + Method: "/zitadel.session.v2beta.SessionService/ListSessions", + }, + }, + }, + }, + Execution: &action.Execution{ + Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()), + }, + }, + want: &action.SetExecutionResponse{ + Details: &settings_object.Details{ + ChangeDate: timestamppb.Now(), + Owner: &object.Owner{ + Type: object.OwnerType_OWNER_TYPE_INSTANCE, + Id: Tester.Instance.InstanceID(), + }, + }, + }, + }, + { + name: "service, not existing", + ctx: CTX, + req: &action.SetExecutionRequest{ + Condition: &action.Condition{ + ConditionType: &action.Condition_Response{ + Response: &action.ResponseExecution{ + Condition: &action.ResponseExecution_Service{ + Service: "NotExistingService", + }, + }, + }, + }, + Execution: &action.Execution{ + Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()), + }, + }, + wantErr: true, + }, + { + name: "service, ok", + ctx: CTX, + req: &action.SetExecutionRequest{ + Condition: &action.Condition{ + ConditionType: &action.Condition_Response{ + Response: &action.ResponseExecution{ + Condition: &action.ResponseExecution_Service{ + Service: "zitadel.session.v2beta.SessionService", + }, + }, + }, + }, + Execution: &action.Execution{ + Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()), + }, + }, + want: &action.SetExecutionResponse{ + Details: &settings_object.Details{ + ChangeDate: timestamppb.Now(), + Owner: &object.Owner{ + Type: object.OwnerType_OWNER_TYPE_INSTANCE, + Id: Tester.Instance.InstanceID(), + }, + }, + }, + }, + { + name: "all, ok", + ctx: CTX, + req: &action.SetExecutionRequest{ + Condition: &action.Condition{ + ConditionType: &action.Condition_Response{ + Response: &action.ResponseExecution{ + Condition: &action.ResponseExecution_All{ + All: true, + }, + }, + }, + }, + Execution: &action.Execution{ + Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()), + }, + }, + want: &action.SetExecutionResponse{ + Details: &settings_object.Details{ + ChangeDate: timestamppb.Now(), + Owner: &object.Owner{ + Type: object.OwnerType_OWNER_TYPE_INSTANCE, + Id: Tester.Instance.InstanceID(), + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // We want to have the same response no matter how often we call the function + Client.SetExecution(tt.ctx, tt.req) + got, err := Client.SetExecution(tt.ctx, tt.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + + integration.AssertSettingsDetails(t, tt.want.Details, got.Details) + + // cleanup to not impact other requests + Tester.DeleteExecution(tt.ctx, t, tt.req.GetCondition()) + }) + } +} + +func TestServer_SetExecution_Event(t *testing.T) { + ensureFeatureEnabled(t) + targetResp := Tester.CreateTarget(CTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false) + + tests := []struct { + name string + ctx context.Context + req *action.SetExecutionRequest + want *action.SetExecutionResponse + wantErr bool + }{ + { + name: "missing permission", + ctx: Tester.WithAuthorization(context.Background(), integration.OrgOwner), + req: &action.SetExecutionRequest{ + Condition: &action.Condition{ + ConditionType: &action.Condition_Event{ + Event: &action.EventExecution{ + Condition: &action.EventExecution_All{ + All: true, + }, + }, + }, + }, + }, + wantErr: true, + }, + { + name: "no condition, error", + ctx: CTX, + req: &action.SetExecutionRequest{ + Condition: &action.Condition{ + ConditionType: &action.Condition_Event{ + Event: &action.EventExecution{}, + }, + }, + Execution: &action.Execution{ + Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()), + }, + }, + wantErr: true, + }, + /* + //TODO event existing check + + { + name: "event, not existing", + ctx: CTX, + req: &action.SetExecutionRequest{ + Condition: &action.Condition{ + ConditionType: &action.Condition_Event{ + Event: &action.EventExecution{ + Condition: &action.EventExecution_Event{ + Event: "xxx", + }, + }, + }, + }, + Targets: []string{targetResp.GetId()}, + }, + wantErr: true, + }, + */ + { + name: "event, ok", + ctx: CTX, + req: &action.SetExecutionRequest{ + Condition: &action.Condition{ + ConditionType: &action.Condition_Event{ + Event: &action.EventExecution{ + Condition: &action.EventExecution_Event{ + Event: "xxx", + }, + }, + }, + }, + Execution: &action.Execution{ + Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()), + }, + }, + want: &action.SetExecutionResponse{ + Details: &settings_object.Details{ + ChangeDate: timestamppb.Now(), + Owner: &object.Owner{ + Type: object.OwnerType_OWNER_TYPE_INSTANCE, + Id: Tester.Instance.InstanceID(), + }, + }, + }, + }, + /* + // TODO: + + { + name: "group, not existing", + ctx: CTX, + req: &action.SetExecutionRequest{ + Condition: &action.Condition{ + ConditionType: &action.Condition_Event{ + Event: &action.EventExecution{ + Condition: &action.EventExecution_Group{ + Group: "xxx", + }, + }, + }, + }, + Targets: []string{targetResp.GetId()}, + }, + wantErr: true, + }, + */ + { + name: "group, ok", + ctx: CTX, + req: &action.SetExecutionRequest{ + Condition: &action.Condition{ + ConditionType: &action.Condition_Event{ + Event: &action.EventExecution{ + Condition: &action.EventExecution_Group{ + Group: "xxx", + }, + }, + }, + }, + Execution: &action.Execution{ + Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()), + }, + }, + want: &action.SetExecutionResponse{ + Details: &settings_object.Details{ + ChangeDate: timestamppb.Now(), + Owner: &object.Owner{ + Type: object.OwnerType_OWNER_TYPE_INSTANCE, + Id: Tester.Instance.InstanceID(), + }, + }, + }, + }, + { + name: "all, ok", + ctx: CTX, + req: &action.SetExecutionRequest{ + Condition: &action.Condition{ + ConditionType: &action.Condition_Event{ + Event: &action.EventExecution{ + Condition: &action.EventExecution_All{ + All: true, + }, + }, + }, + }, + Execution: &action.Execution{ + Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()), + }, + }, + want: &action.SetExecutionResponse{ + Details: &settings_object.Details{ + ChangeDate: timestamppb.Now(), + Owner: &object.Owner{ + Type: object.OwnerType_OWNER_TYPE_INSTANCE, + Id: Tester.Instance.InstanceID(), + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // We want to have the same response no matter how often we call the function + Client.SetExecution(tt.ctx, tt.req) + got, err := Client.SetExecution(tt.ctx, tt.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + + integration.AssertSettingsDetails(t, tt.want.Details, got.Details) + + // cleanup to not impact other requests + Tester.DeleteExecution(tt.ctx, t, tt.req.GetCondition()) + }) + } +} + +func TestServer_SetExecution_Function(t *testing.T) { + ensureFeatureEnabled(t) + targetResp := Tester.CreateTarget(CTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false) + + tests := []struct { + name string + ctx context.Context + req *action.SetExecutionRequest + want *action.SetExecutionResponse + wantErr bool + }{ + { + name: "missing permission", + ctx: Tester.WithAuthorization(context.Background(), integration.OrgOwner), + req: &action.SetExecutionRequest{ + Condition: &action.Condition{ + ConditionType: &action.Condition_Response{ + Response: &action.ResponseExecution{ + Condition: &action.ResponseExecution_All{All: true}, + }, + }, + }, + }, + wantErr: true, + }, + { + name: "no condition, error", + ctx: CTX, + req: &action.SetExecutionRequest{ + Condition: &action.Condition{ + ConditionType: &action.Condition_Response{ + Response: &action.ResponseExecution{}, + }, + }, + Execution: &action.Execution{ + Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()), + }, + }, + wantErr: true, + }, + { + name: "function, not existing", + ctx: CTX, + req: &action.SetExecutionRequest{ + Condition: &action.Condition{ + ConditionType: &action.Condition_Function{ + Function: &action.FunctionExecution{Name: "xxx"}, + }, + }, + Execution: &action.Execution{ + Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()), + }, + }, + wantErr: true, + }, + { + name: "function, ok", + ctx: CTX, + req: &action.SetExecutionRequest{ + Condition: &action.Condition{ + ConditionType: &action.Condition_Function{ + Function: &action.FunctionExecution{Name: "Action.Flow.Type.ExternalAuthentication.Action.TriggerType.PostAuthentication"}, + }, + }, + Execution: &action.Execution{ + Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()), + }, + }, + want: &action.SetExecutionResponse{ + Details: &settings_object.Details{ + ChangeDate: timestamppb.Now(), + Owner: &object.Owner{ + Type: object.OwnerType_OWNER_TYPE_INSTANCE, + Id: Tester.Instance.InstanceID(), + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // We want to have the same response no matter how often we call the function + Client.SetExecution(tt.ctx, tt.req) + got, err := Client.SetExecution(tt.ctx, tt.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + + integration.AssertSettingsDetails(t, tt.want.Details, got.Details) + + // cleanup to not impact other requests + Tester.DeleteExecution(tt.ctx, t, tt.req.GetCondition()) + }) + } +} diff --git a/internal/api/grpc/action/v3alpha/server.go b/internal/api/grpc/resources/action/v3alpha/server.go similarity index 77% rename from internal/api/grpc/action/v3alpha/server.go rename to internal/api/grpc/resources/action/v3alpha/server.go index 952a555d24..57d0761fd2 100644 --- a/internal/api/grpc/action/v3alpha/server.go +++ b/internal/api/grpc/resources/action/v3alpha/server.go @@ -10,13 +10,13 @@ import ( "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/query" "github.com/zitadel/zitadel/internal/zerrors" - action "github.com/zitadel/zitadel/pkg/grpc/action/v3alpha" + action "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha" ) -var _ action.ActionServiceServer = (*Server)(nil) +var _ action.ZITADELActionsServer = (*Server)(nil) type Server struct { - action.UnimplementedActionServiceServer + action.UnimplementedZITADELActionsServer command *command.Commands query *query.Queries ListActionFunctions func() []string @@ -43,23 +43,23 @@ func CreateServer( } func (s *Server) RegisterServer(grpcServer *grpc.Server) { - action.RegisterActionServiceServer(grpcServer, s) + action.RegisterZITADELActionsServer(grpcServer, s) } func (s *Server) AppName() string { - return action.ActionService_ServiceDesc.ServiceName + return action.ZITADELActions_ServiceDesc.ServiceName } func (s *Server) MethodPrefix() string { - return action.ActionService_ServiceDesc.ServiceName + return action.ZITADELActions_ServiceDesc.ServiceName } func (s *Server) AuthMethods() authz.MethodMapping { - return action.ActionService_AuthMethods + return action.ZITADELActions_AuthMethods } func (s *Server) RegisterGateway() server.RegisterGatewayFunc { - return action.RegisterActionServiceHandler + return action.RegisterZITADELActionsHandler } func checkExecutionEnabled(ctx context.Context) error { diff --git a/internal/api/grpc/action/v3alpha/server_integration_test.go b/internal/api/grpc/resources/action/v3alpha/server_integration_test.go similarity index 90% rename from internal/api/grpc/action/v3alpha/server_integration_test.go rename to internal/api/grpc/resources/action/v3alpha/server_integration_test.go index e97605e1f0..483ed2bd3f 100644 --- a/internal/api/grpc/action/v3alpha/server_integration_test.go +++ b/internal/api/grpc/resources/action/v3alpha/server_integration_test.go @@ -13,14 +13,14 @@ import ( "github.com/stretchr/testify/require" "github.com/zitadel/zitadel/internal/integration" - action "github.com/zitadel/zitadel/pkg/grpc/action/v3alpha" - "github.com/zitadel/zitadel/pkg/grpc/feature/v2" + feature "github.com/zitadel/zitadel/pkg/grpc/feature/v2" + action "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha" ) var ( CTX context.Context Tester *integration.Tester - Client action.ActionServiceClient + Client action.ZITADELActionsClient ) func TestMain(m *testing.M) { diff --git a/internal/api/grpc/resources/action/v3alpha/target.go b/internal/api/grpc/resources/action/v3alpha/target.go new file mode 100644 index 0000000000..5d33dac911 --- /dev/null +++ b/internal/api/grpc/resources/action/v3alpha/target.go @@ -0,0 +1,121 @@ +package action + +import ( + "context" + + "github.com/muhlemmer/gu" + + "github.com/zitadel/zitadel/internal/api/authz" + resource_object "github.com/zitadel/zitadel/internal/api/grpc/resources/object/v3alpha" + "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/eventstore/v1/models" + object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha" + action "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha" +) + +func (s *Server) CreateTarget(ctx context.Context, req *action.CreateTargetRequest) (*action.CreateTargetResponse, error) { + if err := checkExecutionEnabled(ctx); err != nil { + return nil, err + } + add := createTargetToCommand(req) + instance := targetOwnerInstance(ctx) + details, err := s.command.AddTarget(ctx, add, instance.Id) + if err != nil { + return nil, err + } + return &action.CreateTargetResponse{ + Details: resource_object.DomainToDetailsPb(details, instance, add.AggregateID), + }, nil +} + +func (s *Server) PatchTarget(ctx context.Context, req *action.PatchTargetRequest) (*action.PatchTargetResponse, error) { + if err := checkExecutionEnabled(ctx); err != nil { + return nil, err + } + instance := targetOwnerInstance(ctx) + details, err := s.command.ChangeTarget(ctx, patchTargetToCommand(req), instance.Id) + if err != nil { + return nil, err + } + return &action.PatchTargetResponse{ + Details: resource_object.DomainToDetailsPb(details, instance, req.GetId()), + }, nil +} + +func (s *Server) DeleteTarget(ctx context.Context, req *action.DeleteTargetRequest) (*action.DeleteTargetResponse, error) { + if err := checkExecutionEnabled(ctx); err != nil { + return nil, err + } + instance := targetOwnerInstance(ctx) + details, err := s.command.DeleteTarget(ctx, req.GetId(), instance.Id) + if err != nil { + return nil, err + } + return &action.DeleteTargetResponse{ + Details: resource_object.DomainToDetailsPb(details, instance, req.GetId()), + }, nil +} + +func createTargetToCommand(req *action.CreateTargetRequest) *command.AddTarget { + reqTarget := req.GetTarget() + var ( + targetType domain.TargetType + interruptOnError bool + ) + switch t := reqTarget.GetTargetType().(type) { + case *action.Target_RestWebhook: + targetType = domain.TargetTypeWebhook + interruptOnError = t.RestWebhook.InterruptOnError + case *action.Target_RestCall: + targetType = domain.TargetTypeCall + interruptOnError = t.RestCall.InterruptOnError + case *action.Target_RestAsync: + targetType = domain.TargetTypeAsync + } + return &command.AddTarget{ + Name: reqTarget.GetName(), + TargetType: targetType, + Endpoint: reqTarget.GetEndpoint(), + Timeout: reqTarget.GetTimeout().AsDuration(), + InterruptOnError: interruptOnError, + } +} + +func patchTargetToCommand(req *action.PatchTargetRequest) *command.ChangeTarget { + reqTarget := req.GetTarget() + if reqTarget == nil { + return nil + } + target := &command.ChangeTarget{ + ObjectRoot: models.ObjectRoot{ + AggregateID: req.GetId(), + }, + Name: reqTarget.Name, + Endpoint: reqTarget.Endpoint, + } + if reqTarget.TargetType != nil { + switch t := reqTarget.GetTargetType().(type) { + case *action.PatchTarget_RestWebhook: + target.TargetType = gu.Ptr(domain.TargetTypeWebhook) + target.InterruptOnError = gu.Ptr(t.RestWebhook.InterruptOnError) + case *action.PatchTarget_RestCall: + target.TargetType = gu.Ptr(domain.TargetTypeCall) + target.InterruptOnError = gu.Ptr(t.RestCall.InterruptOnError) + case *action.PatchTarget_RestAsync: + target.TargetType = gu.Ptr(domain.TargetTypeAsync) + target.InterruptOnError = gu.Ptr(false) + } + } + if reqTarget.Timeout != nil { + target.Timeout = gu.Ptr(reqTarget.GetTimeout().AsDuration()) + } + return target +} + +func targetOwnerInstance(ctx context.Context) *object.Owner { + return &object.Owner{ + Type: object.OwnerType_OWNER_TYPE_INSTANCE, + Id: authz.GetInstance(ctx).InstanceID(), + } +} diff --git a/internal/api/grpc/resources/action/v3alpha/target_integration_test.go b/internal/api/grpc/resources/action/v3alpha/target_integration_test.go new file mode 100644 index 0000000000..bda54bf862 --- /dev/null +++ b/internal/api/grpc/resources/action/v3alpha/target_integration_test.go @@ -0,0 +1,447 @@ +//go:build integration + +package action_test + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/muhlemmer/gu" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/durationpb" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/integration" + object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha" + action "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha" + resource_object "github.com/zitadel/zitadel/pkg/grpc/resources/object/v3alpha" +) + +func TestServer_CreateTarget(t *testing.T) { + ensureFeatureEnabled(t) + tests := []struct { + name string + ctx context.Context + req *action.Target + want *resource_object.Details + wantErr bool + }{ + { + name: "missing permission", + ctx: Tester.WithAuthorization(context.Background(), integration.OrgOwner), + req: &action.Target{ + Name: fmt.Sprint(time.Now().UnixNano() + 1), + }, + wantErr: true, + }, + { + name: "empty name", + ctx: CTX, + req: &action.Target{ + Name: "", + }, + wantErr: true, + }, + { + name: "empty type", + ctx: CTX, + req: &action.Target{ + Name: fmt.Sprint(time.Now().UnixNano() + 1), + TargetType: nil, + }, + wantErr: true, + }, + { + name: "empty webhook url", + ctx: CTX, + req: &action.Target{ + Name: fmt.Sprint(time.Now().UnixNano() + 1), + TargetType: &action.Target_RestWebhook{ + RestWebhook: &action.SetRESTWebhook{}, + }, + }, + wantErr: true, + }, + { + name: "empty request response url", + ctx: CTX, + req: &action.Target{ + Name: fmt.Sprint(time.Now().UnixNano() + 1), + TargetType: &action.Target_RestCall{ + RestCall: &action.SetRESTCall{}, + }, + }, + wantErr: true, + }, + { + name: "empty timeout", + ctx: CTX, + req: &action.Target{ + Name: fmt.Sprint(time.Now().UnixNano() + 1), + Endpoint: "https://example.com", + TargetType: &action.Target_RestWebhook{ + RestWebhook: &action.SetRESTWebhook{}, + }, + Timeout: nil, + }, + wantErr: true, + }, + { + name: "async, ok", + ctx: CTX, + req: &action.Target{ + Name: fmt.Sprint(time.Now().UnixNano() + 1), + Endpoint: "https://example.com", + TargetType: &action.Target_RestAsync{ + RestAsync: &action.SetRESTAsync{}, + }, + Timeout: durationpb.New(10 * time.Second), + }, + want: &resource_object.Details{ + ChangeDate: timestamppb.Now(), + Owner: &object.Owner{ + Type: object.OwnerType_OWNER_TYPE_INSTANCE, + Id: Tester.Instance.InstanceID(), + }, + }, + }, + { + name: "webhook, ok", + ctx: CTX, + req: &action.Target{ + Name: fmt.Sprint(time.Now().UnixNano() + 1), + Endpoint: "https://example.com", + TargetType: &action.Target_RestWebhook{ + RestWebhook: &action.SetRESTWebhook{ + InterruptOnError: false, + }, + }, + Timeout: durationpb.New(10 * time.Second), + }, + want: &resource_object.Details{ + ChangeDate: timestamppb.Now(), + Owner: &object.Owner{ + Type: object.OwnerType_OWNER_TYPE_INSTANCE, + Id: Tester.Instance.InstanceID(), + }, + }, + }, + { + name: "webhook, interrupt on error, ok", + ctx: CTX, + req: &action.Target{ + Name: fmt.Sprint(time.Now().UnixNano() + 1), + Endpoint: "https://example.com", + TargetType: &action.Target_RestWebhook{ + RestWebhook: &action.SetRESTWebhook{ + InterruptOnError: true, + }, + }, + Timeout: durationpb.New(10 * time.Second), + }, + want: &resource_object.Details{ + ChangeDate: timestamppb.Now(), + Owner: &object.Owner{ + Type: object.OwnerType_OWNER_TYPE_INSTANCE, + Id: Tester.Instance.InstanceID(), + }, + }, + }, + { + name: "call, ok", + ctx: CTX, + req: &action.Target{ + Name: fmt.Sprint(time.Now().UnixNano() + 1), + Endpoint: "https://example.com", + TargetType: &action.Target_RestCall{ + RestCall: &action.SetRESTCall{ + InterruptOnError: false, + }, + }, + Timeout: durationpb.New(10 * time.Second), + }, + want: &resource_object.Details{ + ChangeDate: timestamppb.Now(), + Owner: &object.Owner{ + Type: object.OwnerType_OWNER_TYPE_INSTANCE, + Id: Tester.Instance.InstanceID(), + }, + }, + }, + + { + name: "call, interruptOnError, ok", + ctx: CTX, + req: &action.Target{ + Name: fmt.Sprint(time.Now().UnixNano() + 1), + Endpoint: "https://example.com", + TargetType: &action.Target_RestCall{ + RestCall: &action.SetRESTCall{ + InterruptOnError: true, + }, + }, + Timeout: durationpb.New(10 * time.Second), + }, + want: &resource_object.Details{ + ChangeDate: timestamppb.Now(), + Owner: &object.Owner{ + Type: object.OwnerType_OWNER_TYPE_INSTANCE, + Id: Tester.Instance.InstanceID(), + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Client.CreateTarget(tt.ctx, &action.CreateTargetRequest{Target: tt.req}) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + integration.AssertResourceDetails(t, tt.want, got.Details) + }) + } +} + +func TestServer_PatchTarget(t *testing.T) { + ensureFeatureEnabled(t) + type args struct { + ctx context.Context + req *action.PatchTargetRequest + } + tests := []struct { + name string + prepare func(request *action.PatchTargetRequest) error + args args + want *resource_object.Details + wantErr bool + }{ + { + name: "missing permission", + prepare: func(request *action.PatchTargetRequest) error { + targetID := Tester.CreateTarget(CTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetDetails().GetId() + request.Id = targetID + return nil + }, + args: args{ + ctx: Tester.WithAuthorization(context.Background(), integration.OrgOwner), + req: &action.PatchTargetRequest{ + Target: &action.PatchTarget{ + Name: gu.Ptr(fmt.Sprint(time.Now().UnixNano() + 1)), + }, + }, + }, + wantErr: true, + }, + { + name: "not existing", + prepare: func(request *action.PatchTargetRequest) error { + request.Id = "notexisting" + return nil + }, + args: args{ + ctx: CTX, + req: &action.PatchTargetRequest{ + Target: &action.PatchTarget{ + Name: gu.Ptr(fmt.Sprint(time.Now().UnixNano() + 1)), + }, + }, + }, + wantErr: true, + }, + { + name: "change name, ok", + prepare: func(request *action.PatchTargetRequest) error { + targetID := Tester.CreateTarget(CTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetDetails().GetId() + request.Id = targetID + return nil + }, + args: args{ + ctx: CTX, + req: &action.PatchTargetRequest{ + Target: &action.PatchTarget{ + Name: gu.Ptr(fmt.Sprint(time.Now().UnixNano() + 1)), + }, + }, + }, + want: &resource_object.Details{ + ChangeDate: timestamppb.Now(), + Owner: &object.Owner{ + Type: object.OwnerType_OWNER_TYPE_INSTANCE, + Id: Tester.Instance.InstanceID(), + }, + }, + }, + { + name: "change type, ok", + prepare: func(request *action.PatchTargetRequest) error { + targetID := Tester.CreateTarget(CTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetDetails().GetId() + request.Id = targetID + return nil + }, + args: args{ + ctx: CTX, + req: &action.PatchTargetRequest{ + Target: &action.PatchTarget{ + TargetType: &action.PatchTarget_RestCall{ + RestCall: &action.SetRESTCall{ + InterruptOnError: true, + }, + }, + }, + }, + }, + want: &resource_object.Details{ + ChangeDate: timestamppb.Now(), + Owner: &object.Owner{ + Type: object.OwnerType_OWNER_TYPE_INSTANCE, + Id: Tester.Instance.InstanceID(), + }, + }, + }, + { + name: "change url, ok", + prepare: func(request *action.PatchTargetRequest) error { + targetID := Tester.CreateTarget(CTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetDetails().GetId() + request.Id = targetID + return nil + }, + args: args{ + ctx: CTX, + req: &action.PatchTargetRequest{ + Target: &action.PatchTarget{ + Endpoint: gu.Ptr("https://example.com/hooks/new"), + }, + }, + }, + want: &resource_object.Details{ + ChangeDate: timestamppb.Now(), + Owner: &object.Owner{ + Type: object.OwnerType_OWNER_TYPE_INSTANCE, + Id: Tester.Instance.InstanceID(), + }, + }, + }, + { + name: "change timeout, ok", + prepare: func(request *action.PatchTargetRequest) error { + targetID := Tester.CreateTarget(CTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetDetails().GetId() + request.Id = targetID + return nil + }, + args: args{ + ctx: CTX, + req: &action.PatchTargetRequest{ + Target: &action.PatchTarget{ + Timeout: durationpb.New(20 * time.Second), + }, + }, + }, + want: &resource_object.Details{ + ChangeDate: timestamppb.Now(), + Owner: &object.Owner{ + Type: object.OwnerType_OWNER_TYPE_INSTANCE, + Id: Tester.Instance.InstanceID(), + }, + }, + }, + { + name: "change type async, ok", + prepare: func(request *action.PatchTargetRequest) error { + targetID := Tester.CreateTarget(CTX, t, "", "https://example.com", domain.TargetTypeAsync, false).GetDetails().GetId() + request.Id = targetID + return nil + }, + args: args{ + ctx: CTX, + req: &action.PatchTargetRequest{ + Target: &action.PatchTarget{ + TargetType: &action.PatchTarget_RestAsync{ + RestAsync: &action.SetRESTAsync{}, + }, + }, + }, + }, + want: &resource_object.Details{ + ChangeDate: timestamppb.Now(), + Owner: &object.Owner{ + Type: object.OwnerType_OWNER_TYPE_INSTANCE, + Id: Tester.Instance.InstanceID(), + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.prepare(tt.args.req) + require.NoError(t, err) + // We want to have the same response no matter how often we call the function + Client.PatchTarget(tt.args.ctx, tt.args.req) + got, err := Client.PatchTarget(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + integration.AssertResourceDetails(t, tt.want, got.Details) + }) + } +} + +func TestServer_DeleteTarget(t *testing.T) { + ensureFeatureEnabled(t) + target := Tester.CreateTarget(CTX, t, "", "https://example.com", domain.TargetTypeWebhook, false) + tests := []struct { + name string + ctx context.Context + req *action.DeleteTargetRequest + want *resource_object.Details + wantErr bool + }{ + { + name: "missing permission", + ctx: Tester.WithAuthorization(context.Background(), integration.OrgOwner), + req: &action.DeleteTargetRequest{ + Id: target.GetDetails().GetId(), + }, + wantErr: true, + }, + { + name: "empty id", + ctx: CTX, + req: &action.DeleteTargetRequest{ + Id: "", + }, + wantErr: true, + }, + { + name: "delete target", + ctx: CTX, + req: &action.DeleteTargetRequest{ + Id: target.GetDetails().GetId(), + }, + want: &resource_object.Details{ + ChangeDate: timestamppb.Now(), + Owner: &object.Owner{ + Type: object.OwnerType_OWNER_TYPE_INSTANCE, + Id: Tester.Instance.InstanceID(), + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Client.DeleteTarget(tt.ctx, tt.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + integration.AssertResourceDetails(t, tt.want, got.Details) + }) + } +} diff --git a/internal/api/grpc/action/v3alpha/target_test.go b/internal/api/grpc/resources/action/v3alpha/target_test.go similarity index 83% rename from internal/api/grpc/action/v3alpha/target_test.go rename to internal/api/grpc/resources/action/v3alpha/target_test.go index 23e33ad9be..f4e0d02e3b 100644 --- a/internal/api/grpc/action/v3alpha/target_test.go +++ b/internal/api/grpc/resources/action/v3alpha/target_test.go @@ -10,12 +10,12 @@ import ( "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/domain" - action "github.com/zitadel/zitadel/pkg/grpc/action/v3alpha" + action "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha" ) func Test_createTargetToCommand(t *testing.T) { type args struct { - req *action.CreateTargetRequest + req *action.Target } tests := []struct { name string @@ -34,10 +34,10 @@ func Test_createTargetToCommand(t *testing.T) { }, { name: "all fields (webhook)", - args: args{&action.CreateTargetRequest{ + args: args{&action.Target{ Name: "target 1", Endpoint: "https://example.com/hooks/1", - TargetType: &action.CreateTargetRequest_RestWebhook{ + TargetType: &action.Target_RestWebhook{ RestWebhook: &action.SetRESTWebhook{}, }, Timeout: durationpb.New(10 * time.Second), @@ -52,10 +52,10 @@ func Test_createTargetToCommand(t *testing.T) { }, { name: "all fields (async)", - args: args{&action.CreateTargetRequest{ + args: args{&action.Target{ Name: "target 1", Endpoint: "https://example.com/hooks/1", - TargetType: &action.CreateTargetRequest_RestAsync{ + TargetType: &action.Target_RestAsync{ RestAsync: &action.SetRESTAsync{}, }, Timeout: durationpb.New(10 * time.Second), @@ -70,10 +70,10 @@ func Test_createTargetToCommand(t *testing.T) { }, { name: "all fields (interrupting response)", - args: args{&action.CreateTargetRequest{ + args: args{&action.Target{ Name: "target 1", Endpoint: "https://example.com/hooks/1", - TargetType: &action.CreateTargetRequest_RestCall{ + TargetType: &action.Target_RestCall{ RestCall: &action.SetRESTCall{ InterruptOnError: true, }, @@ -91,7 +91,7 @@ func Test_createTargetToCommand(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got := createTargetToCommand(tt.args.req) + got := createTargetToCommand(&action.CreateTargetRequest{Target: tt.args.req}) assert.Equal(t, tt.want, got) }) } @@ -99,7 +99,7 @@ func Test_createTargetToCommand(t *testing.T) { func Test_updateTargetToCommand(t *testing.T) { type args struct { - req *action.UpdateTargetRequest + req *action.PatchTarget } tests := []struct { name string @@ -113,7 +113,7 @@ func Test_updateTargetToCommand(t *testing.T) { }, { name: "all fields nil", - args: args{&action.UpdateTargetRequest{ + args: args{&action.PatchTarget{ Name: nil, TargetType: nil, Timeout: nil, @@ -128,7 +128,7 @@ func Test_updateTargetToCommand(t *testing.T) { }, { name: "all fields empty", - args: args{&action.UpdateTargetRequest{ + args: args{&action.PatchTarget{ Name: gu.Ptr(""), TargetType: nil, Timeout: durationpb.New(0), @@ -143,10 +143,10 @@ func Test_updateTargetToCommand(t *testing.T) { }, { name: "all fields (webhook)", - args: args{&action.UpdateTargetRequest{ + args: args{&action.PatchTarget{ Name: gu.Ptr("target 1"), Endpoint: gu.Ptr("https://example.com/hooks/1"), - TargetType: &action.UpdateTargetRequest_RestWebhook{ + TargetType: &action.PatchTarget_RestWebhook{ RestWebhook: &action.SetRESTWebhook{ InterruptOnError: false, }, @@ -163,10 +163,10 @@ func Test_updateTargetToCommand(t *testing.T) { }, { name: "all fields (webhook interrupt)", - args: args{&action.UpdateTargetRequest{ + args: args{&action.PatchTarget{ Name: gu.Ptr("target 1"), Endpoint: gu.Ptr("https://example.com/hooks/1"), - TargetType: &action.UpdateTargetRequest_RestWebhook{ + TargetType: &action.PatchTarget_RestWebhook{ RestWebhook: &action.SetRESTWebhook{ InterruptOnError: true, }, @@ -183,10 +183,10 @@ func Test_updateTargetToCommand(t *testing.T) { }, { name: "all fields (async)", - args: args{&action.UpdateTargetRequest{ + args: args{&action.PatchTarget{ Name: gu.Ptr("target 1"), Endpoint: gu.Ptr("https://example.com/hooks/1"), - TargetType: &action.UpdateTargetRequest_RestAsync{ + TargetType: &action.PatchTarget_RestAsync{ RestAsync: &action.SetRESTAsync{}, }, Timeout: durationpb.New(10 * time.Second), @@ -201,10 +201,10 @@ func Test_updateTargetToCommand(t *testing.T) { }, { name: "all fields (interrupting response)", - args: args{&action.UpdateTargetRequest{ + args: args{&action.PatchTarget{ Name: gu.Ptr("target 1"), Endpoint: gu.Ptr("https://example.com/hooks/1"), - TargetType: &action.UpdateTargetRequest_RestCall{ + TargetType: &action.PatchTarget_RestCall{ RestCall: &action.SetRESTCall{ InterruptOnError: true, }, @@ -222,7 +222,7 @@ func Test_updateTargetToCommand(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got := updateTargetToCommand(tt.args.req) + got := patchTargetToCommand(&action.PatchTargetRequest{Target: tt.args.req}) assert.Equal(t, tt.want, got) }) } diff --git a/internal/api/grpc/resources/object/v3alpha/converter.go b/internal/api/grpc/resources/object/v3alpha/converter.go new file mode 100644 index 0000000000..41f81b595f --- /dev/null +++ b/internal/api/grpc/resources/object/v3alpha/converter.go @@ -0,0 +1,21 @@ +package object + +import ( + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/domain" + object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha" + resources_object "github.com/zitadel/zitadel/pkg/grpc/resources/object/v3alpha" +) + +func DomainToDetailsPb(objectDetail *domain.ObjectDetails, owner *object.Owner, id string) *resources_object.Details { + details := &resources_object.Details{ + Id: id, + Sequence: objectDetail.Sequence, + Owner: owner, + } + if !objectDetail.EventDate.IsZero() { + details.ChangeDate = timestamppb.New(objectDetail.EventDate) + } + return details +} diff --git a/internal/api/grpc/server/middleware/execution_interceptor.go b/internal/api/grpc/server/middleware/execution_interceptor.go index ec4eee17d2..c309827d94 100644 --- a/internal/api/grpc/server/middleware/execution_interceptor.go +++ b/internal/api/grpc/server/middleware/execution_interceptor.go @@ -14,6 +14,7 @@ import ( "github.com/zitadel/zitadel/internal/query" exec_repo "github.com/zitadel/zitadel/internal/repository/execution" "github.com/zitadel/zitadel/internal/telemetry/tracing" + "github.com/zitadel/zitadel/internal/zerrors" ) func ExecutionHandler(queries *query.Queries) grpc.UnaryServerInterceptor { @@ -143,6 +144,9 @@ func (c *ContextInfoRequest) GetHTTPRequestBody() []byte { } func (c *ContextInfoRequest) SetHTTPResponseBody(resp []byte) error { + if !json.Valid(resp) { + return zerrors.ThrowPreconditionFailed(nil, "ACTION-4m9s2", "Errors.Execution.ResponseIsNotValidJSON") + } return json.Unmarshal(resp, c.Request) } diff --git a/internal/api/grpc/settings/object/v3alpha/converter.go b/internal/api/grpc/settings/object/v3alpha/converter.go new file mode 100644 index 0000000000..c11c14ea63 --- /dev/null +++ b/internal/api/grpc/settings/object/v3alpha/converter.go @@ -0,0 +1,20 @@ +package object + +import ( + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/domain" + object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha" + settings_object "github.com/zitadel/zitadel/pkg/grpc/settings/object/v3alpha" +) + +func DomainToDetailsPb(objectDetail *domain.ObjectDetails, owner *object.Owner) *settings_object.Details { + details := &settings_object.Details{ + Sequence: objectDetail.Sequence, + Owner: owner, + } + if !objectDetail.EventDate.IsZero() { + details.ChangeDate = timestamppb.New(objectDetail.EventDate) + } + return details +} diff --git a/internal/command/action_v2_execution.go b/internal/command/action_v2_execution.go index 7fb08a4a32..6e0dda4ef2 100644 --- a/internal/command/action_v2_execution.go +++ b/internal/command/action_v2_execution.go @@ -60,6 +60,11 @@ func (c *Commands) SetExecutionRequest(ctx context.Context, cond *ExecutionAPICo if err := cond.IsValid(); err != nil { return nil, err } + for _, target := range set.Targets { + if err = target.Validate(); err != nil { + return nil, err + } + } if err := cond.Existing(c); err != nil { return nil, err } @@ -73,6 +78,11 @@ func (c *Commands) SetExecutionResponse(ctx context.Context, cond *ExecutionAPIC if err := cond.IsValid(); err != nil { return nil, err } + for _, target := range set.Targets { + if err = target.Validate(); err != nil { + return nil, err + } + } if err := cond.Existing(c); err != nil { return nil, err } @@ -106,9 +116,19 @@ func (c *Commands) SetExecutionFunction(ctx context.Context, cond ExecutionFunct if err := cond.IsValid(); err != nil { return nil, err } + for _, target := range set.Targets { + if err = target.Validate(); err != nil { + return nil, err + } + } if err := cond.Existing(c); err != nil { return nil, err } + for _, target := range set.Targets { + if err = target.Validate(); err != nil { + return nil, err + } + } if set.AggregateID == "" { set.AggregateID = cond.ID() } @@ -165,6 +185,11 @@ func (c *Commands) SetExecutionEvent(ctx context.Context, cond *ExecutionEventCo if err := cond.IsValid(); err != nil { return nil, err } + for _, target := range set.Targets { + if err = target.Validate(); err != nil { + return nil, err + } + } if err := cond.Existing(c); err != nil { return nil, err } @@ -200,13 +225,6 @@ func (t SetExecution) GetTargets() []string { return targets } -func (e *SetExecution) IsValid() error { - if len(e.Targets) == 0 { - return zerrors.ThrowInvalidArgument(nil, "COMMAND-56bteot2uj", "Errors.Execution.NoTargets") - } - return nil -} - func (e *SetExecution) Existing(c *Commands, ctx context.Context, resourceOwner string) error { targets := e.GetTargets() if len(targets) > 0 && !c.existsTargetsByIDs(ctx, targets, resourceOwner) { @@ -225,16 +243,17 @@ func (c *Commands) setExecution(ctx context.Context, set *SetExecution, resource if resourceOwner == "" || set.AggregateID == "" { return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-gg3a6ol4om", "Errors.IDMissing") } - if err := set.IsValid(); err != nil { + wm, err := c.getExecutionWriteModelByID(ctx, set.AggregateID, resourceOwner) + if err != nil { return nil, err } - - wm := NewExecutionWriteModel(set.AggregateID, resourceOwner) // Check if targets and includes for execution are existing + if wm.ExecutionTargetsEqual(set.Targets) { + return writeModelToObjectDetails(&wm.WriteModel), err + } if err := set.Existing(c, ctx, resourceOwner); err != nil { return nil, err } - if err := c.pushAppendAndReduce(ctx, wm, execution.NewSetEventV2( ctx, ExecutionAggregateFromWriteModel(&wm.WriteModel), @@ -245,55 +264,6 @@ func (c *Commands) setExecution(ctx context.Context, set *SetExecution, resource return writeModelToObjectDetails(&wm.WriteModel), nil } -func (c *Commands) DeleteExecutionRequest(ctx context.Context, cond *ExecutionAPICondition, resourceOwner string) (_ *domain.ObjectDetails, err error) { - if err := cond.IsValid(); err != nil { - return nil, err - } - return c.deleteExecution(ctx, cond.ID(domain.ExecutionTypeRequest), resourceOwner) -} - -func (c *Commands) DeleteExecutionResponse(ctx context.Context, cond *ExecutionAPICondition, resourceOwner string) (_ *domain.ObjectDetails, err error) { - if err := cond.IsValid(); err != nil { - return nil, err - } - return c.deleteExecution(ctx, cond.ID(domain.ExecutionTypeResponse), resourceOwner) -} - -func (c *Commands) DeleteExecutionFunction(ctx context.Context, cond ExecutionFunctionCondition, resourceOwner string) (_ *domain.ObjectDetails, err error) { - if err := cond.IsValid(); err != nil { - return nil, err - } - return c.deleteExecution(ctx, cond.ID(), resourceOwner) -} - -func (c *Commands) DeleteExecutionEvent(ctx context.Context, cond *ExecutionEventCondition, resourceOwner string) (_ *domain.ObjectDetails, err error) { - if err := cond.IsValid(); err != nil { - return nil, err - } - return c.deleteExecution(ctx, cond.ID(), resourceOwner) -} - -func (c *Commands) deleteExecution(ctx context.Context, aggID string, resourceOwner string) (_ *domain.ObjectDetails, err error) { - if resourceOwner == "" || aggID == "" { - return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-cnic97c0g3", "Errors.IDMissing") - } - - wm, err := c.getExecutionWriteModelByID(ctx, aggID, resourceOwner) - if err != nil { - return nil, err - } - if !wm.Exists() { - return nil, zerrors.ThrowNotFound(nil, "COMMAND-suq2upd3rt", "Errors.Execution.NotFound") - } - if err := c.pushAppendAndReduce(ctx, wm, execution.NewRemovedEvent( - ctx, - ExecutionAggregateFromWriteModel(&wm.WriteModel), - )); err != nil { - return nil, err - } - return writeModelToObjectDetails(&wm.WriteModel), nil -} - func (c *Commands) existsExecutionsByIDs(ctx context.Context, ids []string, resourceOwner string) bool { wm := NewExecutionsExistWriteModel(ids, resourceOwner) err := c.eventstore.FilterToQueryReducer(ctx, wm) diff --git a/internal/command/action_v2_execution_model.go b/internal/command/action_v2_execution_model.go index 30cab0f56e..5e678ed4d7 100644 --- a/internal/command/action_v2_execution_model.go +++ b/internal/command/action_v2_execution_model.go @@ -16,6 +16,18 @@ type ExecutionWriteModel struct { ExecutionTargets []*execution.Target } +func (e *ExecutionWriteModel) ExecutionTargetsEqual(targets []*execution.Target) bool { + if len(e.ExecutionTargets) != len(targets) { + return false + } + for i := range e.ExecutionTargets { + if e.ExecutionTargets[i].Type != targets[i].Type || e.ExecutionTargets[i].Target != targets[i].Target { + return false + } + } + return true +} + func (e *ExecutionWriteModel) IncludeList() []string { includes := make([]string, 0) for i := range e.ExecutionTargets { diff --git a/internal/command/action_v2_execution_test.go b/internal/command/action_v2_execution_test.go index 5a9c0ecb1d..eb6cd21c31 100644 --- a/internal/command/action_v2_execution_test.go +++ b/internal/command/action_v2_execution_test.go @@ -90,26 +90,6 @@ func TestCommands_SetExecutionRequest(t *testing.T) { err: zerrors.IsErrorInvalidArgument, }, }, - { - "empty executionType, error", - fields{ - eventstore: expectEventstore(), - grpcMethodExists: existsMock(true), - }, - args{ - ctx: context.Background(), - cond: &ExecutionAPICondition{ - "notvalid", - "", - false, - }, - set: &SetExecution{}, - resourceOwner: "instance", - }, - res{ - err: zerrors.IsErrorInvalidArgument, - }, - }, { "empty target, error", fields{ @@ -123,7 +103,7 @@ func TestCommands_SetExecutionRequest(t *testing.T) { "", false, }, - set: &SetExecution{}, + set: &SetExecution{Targets: []*execution.Target{{}}}, resourceOwner: "instance", }, res{ @@ -182,6 +162,7 @@ func TestCommands_SetExecutionRequest(t *testing.T) { "push ok, method target", fields{ eventstore: expectEventstore( + expectFilter(), // execution doesn't exist yet expectFilter( eventFromEventPusher( target.NewAddedEvent(context.Background(), @@ -229,6 +210,7 @@ func TestCommands_SetExecutionRequest(t *testing.T) { "push ok, service target", fields{ eventstore: expectEventstore( + expectFilter(), // execution doesn't exist yet expectFilter( eventFromEventPusher( target.NewAddedEvent(context.Background(), @@ -276,6 +258,7 @@ func TestCommands_SetExecutionRequest(t *testing.T) { "push ok, all target", fields{ eventstore: expectEventstore( + expectFilter(), // execution doesn't exist yet expectFilter( eventFromEventPusher( target.NewAddedEvent(context.Background(), @@ -322,7 +305,8 @@ func TestCommands_SetExecutionRequest(t *testing.T) { "push not found, method include", fields{ eventstore: expectEventstore( - expectFilter(), + expectFilter(), // execution doesn't exist yet + expectFilter(), // target doesn't exist ), grpcMethodExists: existsMock(true), }, @@ -348,6 +332,7 @@ func TestCommands_SetExecutionRequest(t *testing.T) { "push ok, method include", fields{ eventstore: expectEventstore( + expectFilter(), // execution doesn't exist yet expectFilter( eventFromEventPusher( execution.NewSetEventV2(context.Background(), @@ -403,7 +388,8 @@ func TestCommands_SetExecutionRequest(t *testing.T) { "push not found, service include", fields{ eventstore: expectEventstore( - expectFilter(), + expectFilter(), // execution doesn't exist yet + expectFilter(), // target doesn't exist ), grpcServiceExists: existsMock(true), }, @@ -429,6 +415,7 @@ func TestCommands_SetExecutionRequest(t *testing.T) { "push ok, service include", fields{ eventstore: expectEventstore( + expectFilter(), // execution doesn't exist yet expectFilter( eventFromEventPusher( execution.NewSetEventV2(context.Background(), @@ -484,7 +471,8 @@ func TestCommands_SetExecutionRequest(t *testing.T) { "push not found, all include", fields{ eventstore: expectEventstore( - expectFilter(), + expectFilter(), // execution doesn't exist yet + expectFilter(), // target doesn't exist ), }, args{ @@ -509,6 +497,7 @@ func TestCommands_SetExecutionRequest(t *testing.T) { "push ok, all include", fields{ eventstore: expectEventstore( + expectFilter(), // execution doesn't exist yet expectFilter( eventFromEventPusher( execution.NewSetEventV2(context.Background(), @@ -559,6 +548,83 @@ func TestCommands_SetExecutionRequest(t *testing.T) { }, }, }, + { + "push ok, remove all targets", + fields{ + eventstore: expectEventstore( + expectFilter( // execution has targets + eventFromEventPusher( + execution.NewSetEventV2(context.Background(), + execution.NewAggregate("request", "instance"), + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, + ), + ), + ), + expectPush( + execution.NewSetEventV2(context.Background(), + execution.NewAggregate("request", "instance"), + []*execution.Target{}, + ), + ), + ), + }, + args{ + ctx: context.Background(), + cond: &ExecutionAPICondition{ + "", + "", + true, + }, + set: &SetExecution{ + Targets: []*execution.Target{}, + }, + resourceOwner: "instance", + }, + res{ + details: &domain.ObjectDetails{ + ResourceOwner: "instance", + }, + }, + }, + { + "push ok, unchanged execution", + fields{ + eventstore: expectEventstore( + expectFilter( // execution has targets + eventFromEventPusher( + execution.NewSetEventV2(context.Background(), + execution.NewAggregate("request", "instance"), + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, + ), + ), + ), + ), + }, + args{ + ctx: context.Background(), + cond: &ExecutionAPICondition{ + "", + "", + true, + }, + set: &SetExecution{ + Targets: []*execution.Target{{ + Type: domain.ExecutionTargetTypeTarget, + Target: "target", + }}, + }, + resourceOwner: "instance", + }, + res{ + details: &domain.ObjectDetails{ + ResourceOwner: "instance", + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -652,26 +718,6 @@ func TestCommands_SetExecutionResponse(t *testing.T) { err: zerrors.IsErrorInvalidArgument, }, }, - { - "empty executionType, error", - fields{ - eventstore: expectEventstore(), - grpcMethodExists: existsMock(true), - }, - args{ - ctx: context.Background(), - cond: &ExecutionAPICondition{ - "notvalid", - "", - false, - }, - set: &SetExecution{}, - resourceOwner: "instance", - }, - res{ - err: zerrors.IsErrorInvalidArgument, - }, - }, { "empty target, error", fields{ @@ -685,7 +731,7 @@ func TestCommands_SetExecutionResponse(t *testing.T) { "", false, }, - set: &SetExecution{}, + set: &SetExecution{Targets: []*execution.Target{{}}}, resourceOwner: "instance", }, res{ @@ -696,6 +742,7 @@ func TestCommands_SetExecutionResponse(t *testing.T) { "push failed, error", fields{ eventstore: expectEventstore( + expectFilter(), // execution doesn't exist yet expectFilter( target.NewAddedEvent(context.Background(), target.NewAggregate("target", "instance"), @@ -788,6 +835,7 @@ func TestCommands_SetExecutionResponse(t *testing.T) { "push ok, method target", fields{ eventstore: expectEventstore( + expectFilter(), // execution doesn't exist yet expectFilter( eventFromEventPusher( target.NewAddedEvent(context.Background(), @@ -835,6 +883,7 @@ func TestCommands_SetExecutionResponse(t *testing.T) { "push ok, service target", fields{ eventstore: expectEventstore( + expectFilter(), // execution doesn't exist yet expectFilter( targetAddEvent("target", "instance"), ), @@ -873,6 +922,7 @@ func TestCommands_SetExecutionResponse(t *testing.T) { "push ok, all target", fields{ eventstore: expectEventstore( + expectFilter(), // execution doesn't exist yet expectFilter( targetAddEvent("target", "instance"), ), @@ -906,6 +956,83 @@ func TestCommands_SetExecutionResponse(t *testing.T) { }, }, }, + { + "push ok, remove all targets", + fields{ + eventstore: expectEventstore( + expectFilter( // execution has targets + eventFromEventPusher( + execution.NewSetEventV2(context.Background(), + execution.NewAggregate("response", "instance"), + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, + ), + ), + ), + expectPush( + execution.NewSetEventV2(context.Background(), + execution.NewAggregate("response", "instance"), + []*execution.Target{}, + ), + ), + ), + }, + args{ + ctx: context.Background(), + cond: &ExecutionAPICondition{ + "", + "", + true, + }, + set: &SetExecution{ + Targets: []*execution.Target{}, + }, + resourceOwner: "instance", + }, + res{ + details: &domain.ObjectDetails{ + ResourceOwner: "instance", + }, + }, + }, + { + "push ok, unchanged execution", + fields{ + eventstore: expectEventstore( + expectFilter( // execution has targets + eventFromEventPusher( + execution.NewSetEventV2(context.Background(), + execution.NewAggregate("response", "instance"), + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, + ), + ), + ), + ), + }, + args{ + ctx: context.Background(), + cond: &ExecutionAPICondition{ + "", + "", + true, + }, + set: &SetExecution{ + Targets: []*execution.Target{{ + Type: domain.ExecutionTargetTypeTarget, + Target: "target", + }}, + }, + resourceOwner: "instance", + }, + res{ + details: &domain.ObjectDetails{ + ResourceOwner: "instance", + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -1012,7 +1139,7 @@ func TestCommands_SetExecutionEvent(t *testing.T) { "", false, }, - set: &SetExecution{}, + set: &SetExecution{Targets: []*execution.Target{{Target: "target"}}}, resourceOwner: "instance", }, res{ @@ -1032,7 +1159,7 @@ func TestCommands_SetExecutionEvent(t *testing.T) { "", false, }, - set: &SetExecution{}, + set: &SetExecution{Targets: []*execution.Target{{}}}, resourceOwner: "instance", }, res{ @@ -1043,6 +1170,7 @@ func TestCommands_SetExecutionEvent(t *testing.T) { "push failed, error", fields{ eventstore: expectEventstore( + expectFilter(), // execution doesn't exist yet expectFilter( targetAddEvent("target", "instance"), ), @@ -1128,6 +1256,7 @@ func TestCommands_SetExecutionEvent(t *testing.T) { "push ok, event target", fields{ eventstore: expectEventstore( + expectFilter(), // execution doesn't exist yet expectFilter( targetAddEvent("target", "instance"), ), @@ -1166,6 +1295,7 @@ func TestCommands_SetExecutionEvent(t *testing.T) { "push ok, group target", fields{ eventstore: expectEventstore( + expectFilter(), // execution doesn't exist yet expectFilter( targetAddEvent("target", "instance"), ), @@ -1204,6 +1334,7 @@ func TestCommands_SetExecutionEvent(t *testing.T) { "push ok, all target", fields{ eventstore: expectEventstore( + expectFilter(), // execution doesn't exist yet expectFilter( targetAddEvent("target", "instance"), ), @@ -1237,6 +1368,83 @@ func TestCommands_SetExecutionEvent(t *testing.T) { }, }, }, + { + "push ok, remove all targets", + fields{ + eventstore: expectEventstore( + expectFilter( // execution has targets + eventFromEventPusher( + execution.NewSetEventV2(context.Background(), + execution.NewAggregate("event", "instance"), + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, + ), + ), + ), + expectPush( + execution.NewSetEventV2(context.Background(), + execution.NewAggregate("event", "instance"), + []*execution.Target{}, + ), + ), + ), + }, + args{ + ctx: context.Background(), + cond: &ExecutionEventCondition{ + "", + "", + true, + }, + set: &SetExecution{ + Targets: []*execution.Target{}, + }, + resourceOwner: "instance", + }, + res{ + details: &domain.ObjectDetails{ + ResourceOwner: "instance", + }, + }, + }, + { + "push ok, unchanged execution", + fields{ + eventstore: expectEventstore( + expectFilter( // execution has targets + eventFromEventPusher( + execution.NewSetEventV2(context.Background(), + execution.NewAggregate("event", "instance"), + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, + ), + ), + ), + ), + }, + args{ + ctx: context.Background(), + cond: &ExecutionEventCondition{ + "", + "", + true, + }, + set: &SetExecution{ + Targets: []*execution.Target{{ + Type: domain.ExecutionTargetTypeTarget, + Target: "target", + }}, + }, + resourceOwner: "instance", + }, + res{ + details: &domain.ObjectDetails{ + ResourceOwner: "instance", + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -1311,22 +1519,6 @@ func TestCommands_SetExecutionFunction(t *testing.T) { err: zerrors.IsErrorInvalidArgument, }, }, - { - "empty executionType, error", - fields{ - eventstore: expectEventstore(), - actionFunctionExists: existsMock(true), - }, - args{ - ctx: context.Background(), - cond: "function", - set: &SetExecution{}, - resourceOwner: "instance", - }, - res{ - err: zerrors.IsErrorInvalidArgument, - }, - }, { "empty target, error", fields{ @@ -1336,7 +1528,7 @@ func TestCommands_SetExecutionFunction(t *testing.T) { args{ ctx: context.Background(), cond: "function", - set: &SetExecution{}, + set: &SetExecution{Targets: []*execution.Target{{}}}, resourceOwner: "instance", }, res{ @@ -1347,6 +1539,7 @@ func TestCommands_SetExecutionFunction(t *testing.T) { "push failed, error", fields{ eventstore: expectEventstore( + expectFilter(), // execution doesn't exist yet expectFilter( targetAddEvent("target", "instance"), ), @@ -1379,7 +1572,8 @@ func TestCommands_SetExecutionFunction(t *testing.T) { "push error, function target", fields{ eventstore: expectEventstore( - expectFilter(), + expectFilter(), // execution doesn't exist yet + expectFilter(), // target doesn't exist ), actionFunctionExists: existsMock(true), }, @@ -1421,6 +1615,7 @@ func TestCommands_SetExecutionFunction(t *testing.T) { "push ok, function target", fields{ eventstore: expectEventstore( + expectFilter(), // execution doesn't exist yet expectFilter( targetAddEvent("target", "instance"), ), @@ -1451,6 +1646,77 @@ func TestCommands_SetExecutionFunction(t *testing.T) { }, }, }, + { + "push ok, remove all targets", + fields{ + actionFunctionExists: existsMock(true), + eventstore: expectEventstore( + expectFilter( // execution has targets + eventFromEventPusher( + execution.NewSetEventV2(context.Background(), + execution.NewAggregate("function/function", "instance"), + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, + ), + ), + ), + expectPush( + execution.NewSetEventV2(context.Background(), + execution.NewAggregate("function/function", "instance"), + []*execution.Target{}, + ), + ), + ), + }, + args{ + ctx: context.Background(), + cond: "function", + set: &SetExecution{ + Targets: []*execution.Target{}, + }, + resourceOwner: "instance", + }, + res{ + details: &domain.ObjectDetails{ + ResourceOwner: "instance", + }, + }, + }, + { + "push ok, unchanged execution", + fields{ + actionFunctionExists: existsMock(true), + eventstore: expectEventstore( + expectFilter( // execution has targets + eventFromEventPusher( + execution.NewSetEventV2(context.Background(), + execution.NewAggregate("function/function", "instance"), + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, + ), + ), + ), + ), + }, + args{ + ctx: context.Background(), + cond: "function", + set: &SetExecution{ + Targets: []*execution.Target{{ + Type: domain.ExecutionTargetTypeTarget, + Target: "target", + }}, + }, + resourceOwner: "instance", + }, + res{ + details: &domain.ObjectDetails{ + ResourceOwner: "instance", + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -1472,938 +1738,6 @@ func TestCommands_SetExecutionFunction(t *testing.T) { } } -func TestCommands_DeleteExecutionRequest(t *testing.T) { - type fields struct { - eventstore func(t *testing.T) *eventstore.Eventstore - } - type args struct { - ctx context.Context - cond *ExecutionAPICondition - resourceOwner string - } - type res struct { - details *domain.ObjectDetails - err func(error) bool - } - tests := []struct { - name string - fields fields - args args - res res - }{ - { - "no resourceowner, error", - fields{ - eventstore: expectEventstore(), - }, - args{ - ctx: context.Background(), - cond: &ExecutionAPICondition{}, - resourceOwner: "", - }, - res{ - err: zerrors.IsErrorInvalidArgument, - }, - }, - { - "no cond, error", - fields{ - eventstore: expectEventstore(), - }, - args{ - ctx: context.Background(), - cond: &ExecutionAPICondition{}, - resourceOwner: "instance", - }, - res{ - err: zerrors.IsErrorInvalidArgument, - }, - }, - { - "no valid cond, error", - fields{ - eventstore: expectEventstore(), - }, - args{ - ctx: context.Background(), - cond: &ExecutionAPICondition{ - "notvalid", - "notvalid", - false, - }, - resourceOwner: "instance", - }, - res{ - err: zerrors.IsErrorInvalidArgument, - }, - }, - { - "push failed, error", - fields{ - eventstore: expectEventstore( - expectFilter( - eventFromEventPusher( - execution.NewSetEventV2(context.Background(), - execution.NewAggregate("request/valid", "instance"), - []*execution.Target{ - {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, - }, - ), - ), - ), - expectPushFailed( - zerrors.ThrowPreconditionFailed(nil, "id", "name already exists"), - execution.NewRemovedEvent(context.Background(), - execution.NewAggregate("request/valid", "instance"), - ), - ), - ), - }, - args{ - ctx: context.Background(), - cond: &ExecutionAPICondition{ - "valid", - "", - false, - }, - resourceOwner: "instance", - }, - res{ - err: zerrors.IsPreconditionFailed, - }, - }, - { - "not found, error", - fields{ - eventstore: expectEventstore( - expectFilter(), - ), - }, - args{ - ctx: context.Background(), - cond: &ExecutionAPICondition{ - "method", - "", - false, - }, - resourceOwner: "instance", - }, - res{ - err: zerrors.IsNotFound, - }, - }, - { - "push ok, method target", - fields{ - eventstore: expectEventstore( - expectFilter( - eventFromEventPusher( - execution.NewSetEventV2(context.Background(), - execution.NewAggregate("request/method", "instance"), - []*execution.Target{ - {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, - }, - ), - ), - ), - expectPush( - execution.NewRemovedEvent(context.Background(), - execution.NewAggregate("request/method", "instance"), - ), - ), - ), - }, - args{ - ctx: context.Background(), - cond: &ExecutionAPICondition{ - "method", - "", - false, - }, - resourceOwner: "instance", - }, - res{ - details: &domain.ObjectDetails{ - ResourceOwner: "instance", - }, - }, - }, - { - "push ok, service target", - fields{ - eventstore: expectEventstore( - expectFilter( - eventFromEventPusher( - execution.NewSetEventV2(context.Background(), - execution.NewAggregate("request/service", "instance"), - []*execution.Target{ - {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, - }, - ), - ), - ), - expectPush( - execution.NewRemovedEvent(context.Background(), - execution.NewAggregate("request/service", "instance"), - ), - ), - ), - }, - args{ - ctx: context.Background(), - cond: &ExecutionAPICondition{ - "", - "service", - false, - }, - resourceOwner: "instance", - }, - res{ - details: &domain.ObjectDetails{ - ResourceOwner: "instance", - }, - }, - }, - { - "push ok, all target", - fields{ - eventstore: expectEventstore( - expectFilter( - eventFromEventPusher( - execution.NewSetEventV2(context.Background(), - execution.NewAggregate("request", "instance"), - []*execution.Target{ - {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, - }, - ), - ), - ), - expectPush( - execution.NewRemovedEvent(context.Background(), - execution.NewAggregate("request", "instance"), - ), - ), - ), - }, - args{ - ctx: context.Background(), - cond: &ExecutionAPICondition{ - "", - "", - true, - }, - resourceOwner: "instance", - }, - res{ - details: &domain.ObjectDetails{ - ResourceOwner: "instance", - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - c := &Commands{ - eventstore: tt.fields.eventstore(t), - } - details, err := c.DeleteExecutionRequest(tt.args.ctx, tt.args.cond, 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) - } - if tt.res.err == nil { - assert.Equal(t, tt.res.details, details) - } - }) - } -} - -func TestCommands_DeleteExecutionResponse(t *testing.T) { - type fields struct { - eventstore func(t *testing.T) *eventstore.Eventstore - } - type args struct { - ctx context.Context - cond *ExecutionAPICondition - resourceOwner string - } - type res struct { - details *domain.ObjectDetails - err func(error) bool - } - tests := []struct { - name string - fields fields - args args - res res - }{ - { - "no resourceowner, error", - fields{ - eventstore: expectEventstore(), - }, - args{ - ctx: context.Background(), - cond: &ExecutionAPICondition{}, - resourceOwner: "", - }, - res{ - err: zerrors.IsErrorInvalidArgument, - }, - }, - { - "no cond, error", - fields{ - eventstore: expectEventstore(), - }, - args{ - ctx: context.Background(), - cond: &ExecutionAPICondition{}, - resourceOwner: "instance", - }, - res{ - err: zerrors.IsErrorInvalidArgument, - }, - }, - { - "no valid cond, error", - fields{ - eventstore: expectEventstore(), - }, - args{ - ctx: context.Background(), - cond: &ExecutionAPICondition{ - "notvalid", - "notvalid", - false, - }, - resourceOwner: "instance", - }, - res{ - err: zerrors.IsErrorInvalidArgument, - }, - }, - { - "push failed, error", - fields{ - eventstore: expectEventstore( - expectFilter( - eventFromEventPusher( - execution.NewSetEventV2(context.Background(), - execution.NewAggregate("response/valid", "instance"), - []*execution.Target{ - {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, - }, - ), - ), - ), - expectPushFailed( - zerrors.ThrowPreconditionFailed(nil, "id", "name already exists"), - execution.NewRemovedEvent(context.Background(), - execution.NewAggregate("response/valid", "instance"), - ), - ), - ), - }, - args{ - ctx: context.Background(), - cond: &ExecutionAPICondition{ - "valid", - "", - false, - }, - resourceOwner: "instance", - }, - res{ - err: zerrors.IsPreconditionFailed, - }, - }, - { - "not found, error", - fields{ - eventstore: expectEventstore( - expectFilter(), - ), - }, - args{ - ctx: context.Background(), - cond: &ExecutionAPICondition{ - "method", - "", - false, - }, - resourceOwner: "instance", - }, - res{ - err: zerrors.IsNotFound, - }, - }, - { - "push ok, method target", - fields{ - eventstore: expectEventstore( - expectFilter( - eventFromEventPusher( - execution.NewSetEventV2(context.Background(), - execution.NewAggregate("response/method", "instance"), - []*execution.Target{ - {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, - }, - ), - ), - ), - expectPush( - execution.NewRemovedEvent(context.Background(), - execution.NewAggregate("response/method", "instance"), - ), - ), - ), - }, - args{ - ctx: context.Background(), - cond: &ExecutionAPICondition{ - "method", - "", - false, - }, - resourceOwner: "instance", - }, - res{ - details: &domain.ObjectDetails{ - ResourceOwner: "instance", - }, - }, - }, - { - "push ok, service target", - fields{ - eventstore: expectEventstore( - expectFilter( - eventFromEventPusher( - execution.NewSetEventV2(context.Background(), - execution.NewAggregate("response/service", "instance"), - []*execution.Target{ - {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, - }, - ), - ), - ), - expectPush( - execution.NewRemovedEvent(context.Background(), - execution.NewAggregate("response/service", "instance"), - ), - ), - ), - }, - args{ - ctx: context.Background(), - cond: &ExecutionAPICondition{ - "", - "service", - false, - }, - resourceOwner: "instance", - }, - res{ - details: &domain.ObjectDetails{ - ResourceOwner: "instance", - }, - }, - }, - { - "push ok, all target", - fields{ - eventstore: expectEventstore( - expectFilter( - eventFromEventPusher( - execution.NewSetEventV2(context.Background(), - execution.NewAggregate("response", "instance"), - []*execution.Target{ - {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, - }, - ), - ), - ), - expectPush( - execution.NewRemovedEvent(context.Background(), - execution.NewAggregate("response", "instance"), - ), - ), - ), - }, - args{ - ctx: context.Background(), - cond: &ExecutionAPICondition{ - "", - "", - true, - }, - resourceOwner: "instance", - }, - res{ - details: &domain.ObjectDetails{ - ResourceOwner: "instance", - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - c := &Commands{ - eventstore: tt.fields.eventstore(t), - } - details, err := c.DeleteExecutionResponse(tt.args.ctx, tt.args.cond, 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) - } - if tt.res.err == nil { - assert.Equal(t, tt.res.details, details) - } - }) - } -} - -func TestCommands_DeleteExecutionEvent(t *testing.T) { - type fields struct { - eventstore func(t *testing.T) *eventstore.Eventstore - } - type args struct { - ctx context.Context - cond *ExecutionEventCondition - resourceOwner string - } - type res struct { - details *domain.ObjectDetails - err func(error) bool - } - tests := []struct { - name string - fields fields - args args - res res - }{ - { - "no resourceowner, error", - fields{ - eventstore: expectEventstore(), - }, - args{ - ctx: context.Background(), - cond: &ExecutionEventCondition{}, - resourceOwner: "", - }, - res{ - err: zerrors.IsErrorInvalidArgument, - }, - }, - { - "no cond, error", - fields{ - eventstore: expectEventstore(), - }, - args{ - ctx: context.Background(), - cond: &ExecutionEventCondition{}, - resourceOwner: "instance", - }, - res{ - err: zerrors.IsErrorInvalidArgument, - }, - }, - { - "push failed, error", - fields{ - eventstore: expectEventstore( - expectFilter( - eventFromEventPusher( - execution.NewSetEventV2(context.Background(), - execution.NewAggregate("event/valid", "instance"), - []*execution.Target{ - {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, - }, - ), - ), - ), - expectPushFailed( - zerrors.ThrowPreconditionFailed(nil, "id", "name already exists"), - execution.NewRemovedEvent(context.Background(), - execution.NewAggregate("event/valid", "instance"), - ), - ), - ), - }, - args{ - ctx: context.Background(), - cond: &ExecutionEventCondition{ - "valid", - "", - false, - }, - resourceOwner: "instance", - }, - res{ - err: zerrors.IsPreconditionFailed, - }, - }, - { - "push error, not existing", - fields{ - eventstore: expectEventstore( - expectFilter(), - ), - }, - args{ - ctx: context.Background(), - cond: &ExecutionEventCondition{ - "valid", - "", - false, - }, - resourceOwner: "instance", - }, - res{ - err: zerrors.IsNotFound, - }, - }, - { - "push error, event", - fields{ - eventstore: expectEventstore( - expectFilter(), - ), - }, - args{ - ctx: context.Background(), - cond: &ExecutionEventCondition{ - "valid", - "", - false, - }, - resourceOwner: "instance", - }, - res{ - err: zerrors.IsNotFound, - }, - }, - { - "push ok, event", - fields{ - eventstore: expectEventstore( - expectFilter( - eventFromEventPusher( - execution.NewSetEventV2(context.Background(), - execution.NewAggregate("event/valid", "instance"), - []*execution.Target{ - {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, - }, - ), - ), - ), - expectPush( - execution.NewRemovedEvent(context.Background(), - execution.NewAggregate("event/valid", "instance"), - ), - ), - ), - }, - args{ - ctx: context.Background(), - cond: &ExecutionEventCondition{ - "valid", - "", - false, - }, - resourceOwner: "instance", - }, - res{ - details: &domain.ObjectDetails{ - ResourceOwner: "instance", - }, - }, - }, - { - "push error, group", - fields{ - eventstore: expectEventstore( - expectFilter(), - ), - }, - args{ - ctx: context.Background(), - cond: &ExecutionEventCondition{ - "", - "valid", - false, - }, - resourceOwner: "instance", - }, - res{ - err: zerrors.IsNotFound, - }, - }, - { - "push ok, group", - fields{ - eventstore: expectEventstore( - expectFilter( - eventFromEventPusher( - execution.NewSetEventV2(context.Background(), - execution.NewAggregate("event/group", "instance"), - []*execution.Target{ - {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, - }, - ), - ), - ), - expectPush( - execution.NewRemovedEvent(context.Background(), - execution.NewAggregate("event/group.*", "instance"), - ), - ), - ), - }, - args{ - ctx: context.Background(), - cond: &ExecutionEventCondition{ - "", - "group", - false, - }, - resourceOwner: "instance", - }, - res{ - details: &domain.ObjectDetails{ - ResourceOwner: "instance", - }, - }, - }, - { - "push error, all", - fields{ - eventstore: expectEventstore( - expectFilter(), - ), - }, - args{ - ctx: context.Background(), - cond: &ExecutionEventCondition{ - "", - "", - true, - }, - resourceOwner: "instance", - }, - res{ - err: zerrors.IsNotFound, - }, - }, - { - "push ok, all", - fields{ - eventstore: expectEventstore( - expectFilter( - eventFromEventPusher( - execution.NewSetEventV2(context.Background(), - execution.NewAggregate("event", "instance"), - []*execution.Target{ - {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, - }, - ), - ), - ), - expectPush( - execution.NewRemovedEvent(context.Background(), - execution.NewAggregate("event", "instance"), - ), - ), - ), - }, - args{ - ctx: context.Background(), - cond: &ExecutionEventCondition{ - "", - "", - true, - }, - resourceOwner: "instance", - }, - res{ - details: &domain.ObjectDetails{ - ResourceOwner: "instance", - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - c := &Commands{ - eventstore: tt.fields.eventstore(t), - } - details, err := c.DeleteExecutionEvent(tt.args.ctx, tt.args.cond, 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) - } - if tt.res.err == nil { - assert.Equal(t, tt.res.details, details) - } - }) - } -} - -func TestCommands_DeleteExecutionFunction(t *testing.T) { - type fields struct { - eventstore func(t *testing.T) *eventstore.Eventstore - } - type args struct { - ctx context.Context - cond ExecutionFunctionCondition - resourceOwner string - } - type res struct { - details *domain.ObjectDetails - err func(error) bool - } - tests := []struct { - name string - fields fields - args args - res res - }{ - { - "no resourceowner, error", - fields{ - eventstore: expectEventstore(), - }, - args{ - ctx: context.Background(), - cond: "", - resourceOwner: "", - }, - res{ - err: zerrors.IsErrorInvalidArgument, - }, - }, - { - "no cond, error", - fields{ - eventstore: expectEventstore(), - }, - args{ - ctx: context.Background(), - cond: "", - resourceOwner: "instance", - }, - res{ - err: zerrors.IsErrorInvalidArgument, - }, - }, - { - "push failed, error", - fields{ - eventstore: expectEventstore( - expectFilter( - eventFromEventPusher( - execution.NewSetEventV2(context.Background(), - execution.NewAggregate("function/function", "instance"), - []*execution.Target{ - {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, - }, - ), - ), - ), - expectPushFailed( - zerrors.ThrowPreconditionFailed(nil, "id", "name already exists"), - execution.NewRemovedEvent(context.Background(), - execution.NewAggregate("function/function", "instance"), - ), - ), - ), - }, - args{ - ctx: context.Background(), - cond: "function", - resourceOwner: "instance", - }, - res{ - err: zerrors.IsPreconditionFailed, - }, - }, - { - "push error, not existing", - fields{ - eventstore: expectEventstore( - expectFilter(), - ), - }, - args{ - ctx: context.Background(), - cond: "function", - resourceOwner: "instance", - }, - res{ - err: zerrors.IsNotFound, - }, - }, - { - "push ok, function", - fields{ - eventstore: expectEventstore( - expectFilter( - eventFromEventPusher( - execution.NewSetEventV2(context.Background(), - execution.NewAggregate("function/function", "instance"), - []*execution.Target{ - {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, - }, - ), - ), - ), - expectPush( - execution.NewRemovedEvent(context.Background(), - execution.NewAggregate("function/function", "instance"), - ), - ), - ), - }, - args{ - ctx: context.Background(), - cond: "function", - resourceOwner: "instance", - }, - res{ - details: &domain.ObjectDetails{ - ResourceOwner: "instance", - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - c := &Commands{ - eventstore: tt.fields.eventstore(t), - } - details, err := c.DeleteExecutionFunction(tt.args.ctx, tt.args.cond, 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) - } - if tt.res.err == nil { - assert.Equal(t, tt.res.details, details) - } - }) - } -} - func mockExecutionIncludesCache(cache map[string][]string) includeCacheFunc { return func(ctx context.Context, id string, resourceOwner string) ([]string, error) { included, ok := cache[id] diff --git a/internal/execution/execution.go b/internal/execution/execution.go index abb2153fc2..c493673e90 100644 --- a/internal/execution/execution.go +++ b/internal/execution/execution.go @@ -46,7 +46,7 @@ func CallTargets( } if len(resp) > 0 { // error in unmarshalling - if err := info.SetHTTPResponseBody(resp); err != nil { + if err := info.SetHTTPResponseBody(resp); err != nil && target.IsInterruptOnError() { return nil, err } } @@ -73,10 +73,10 @@ func CallTarget( return nil, webhook(ctx, target.GetEndpoint(), target.GetTimeout(), info.GetHTTPRequestBody()) // get request, return response and error case domain.TargetTypeCall: - return call(ctx, target.GetEndpoint(), target.GetTimeout(), info.GetHTTPRequestBody()) + return Call(ctx, target.GetEndpoint(), target.GetTimeout(), info.GetHTTPRequestBody()) case domain.TargetTypeAsync: go func(target Target, info ContextInfoRequest) { - if _, err := call(ctx, target.GetEndpoint(), target.GetTimeout(), info.GetHTTPRequestBody()); err != nil { + if _, err := Call(ctx, target.GetEndpoint(), target.GetTimeout(), info.GetHTTPRequestBody()); err != nil { logging.WithFields("target", target.GetTargetID()).OnError(err).Info(err) } }(target, info) @@ -88,12 +88,12 @@ func CallTarget( // webhook call a webhook, ignore the response but return the errror func webhook(ctx context.Context, url string, timeout time.Duration, body []byte) error { - _, err := call(ctx, url, timeout, body) + _, err := Call(ctx, url, timeout, body) return err } -// call function to do a post HTTP request to a desired url with timeout -func call(ctx context.Context, url string, timeout time.Duration, body []byte) (_ []byte, err error) { +// Call function to do a post HTTP request to a desired url with timeout +func Call(ctx context.Context, url string, timeout time.Duration, body []byte) (_ []byte, err error) { ctx, cancel := context.WithTimeout(ctx, timeout) ctx, span := tracing.NewSpan(ctx) defer func() { diff --git a/internal/execution/execution_test.go b/internal/execution/execution_test.go index 2d891148df..4a68ff5ac8 100644 --- a/internal/execution/execution_test.go +++ b/internal/execution/execution_test.go @@ -1,8 +1,8 @@ -package execution +package execution_test import ( "context" - "encoding/json" + "fmt" "io" "net/http" "net/http/httptest" @@ -12,37 +12,11 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/zitadel/zitadel/internal/api/grpc/server/middleware" "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/execution" ) -var _ Target = &mockTarget{} - -type mockTarget struct { - InstanceID string - ExecutionID string - TargetID string - TargetType domain.TargetType - Endpoint string - Timeout time.Duration - InterruptOnError bool -} - -func (e *mockTarget) GetTargetID() string { - return e.TargetID -} -func (e *mockTarget) IsInterruptOnError() bool { - return e.InterruptOnError -} -func (e *mockTarget) GetEndpoint() string { - return e.Endpoint -} -func (e *mockTarget) GetTargetType() domain.TargetType { - return e.TargetType -} -func (e *mockTarget) GetTimeout() time.Duration { - return e.Timeout -} - func Test_Call(t *testing.T) { type args struct { ctx context.Context @@ -110,12 +84,14 @@ func Test_Call(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - respBody, err := testServerCall(t, - tt.args.method, - tt.args.body, - tt.args.sleep, - tt.args.statusCode, - tt.args.respBody, + respBody, err := testServer(t, + &callTestServer{ + method: tt.args.method, + expectBody: tt.args.body, + timeout: tt.args.sleep, + statusCode: tt.args.statusCode, + respondBody: tt.args.respBody, + }, testCall(tt.args.ctx, tt.args.timeout, tt.args.body), ) if tt.res.wantErr { @@ -129,98 +105,12 @@ func Test_Call(t *testing.T) { } } -func testCall(ctx context.Context, timeout time.Duration, body []byte) func(string) ([]byte, error) { - return func(url string) ([]byte, error) { - return call(ctx, url, timeout, body) - } -} - -func testCallTarget(ctx context.Context, - target *mockTarget, - info ContextInfoRequest, -) func(string) ([]byte, error) { - return func(url string) (r []byte, err error) { - target.Endpoint = url - return CallTarget(ctx, target, info) - } -} - -func testServerCall( - t *testing.T, - method string, - body []byte, - timeout time.Duration, - statusCode int, - respBody []byte, - call func(string) ([]byte, error), -) ([]byte, error) { - handler := func(w http.ResponseWriter, r *http.Request) { - checkRequest(t, r, method, body) - - if statusCode != http.StatusOK { - http.Error(w, "error", statusCode) - return - } - - time.Sleep(timeout) - - w.Header().Set("Content-Type", "application/json") - if _, err := io.WriteString(w, string(respBody)); err != nil { - http.Error(w, "error", http.StatusInternalServerError) - return - } - } - - server := httptest.NewServer(http.HandlerFunc(handler)) - defer server.Close() - - return call(server.URL) -} - -func checkRequest(t *testing.T, sent *http.Request, method string, expectedBody []byte) { - sentBody, err := io.ReadAll(sent.Body) - require.NoError(t, err) - require.Equal(t, expectedBody, sentBody) - require.Equal(t, method, sent.Method) -} - -var _ ContextInfoRequest = &mockContextInfoRequest{} - -type request struct { - Request string `json:"request"` -} - -type mockContextInfoRequest struct { - Request *request `json:"request"` -} - -func newMockContextInfoRequest(s string) *mockContextInfoRequest { - return &mockContextInfoRequest{&request{s}} -} - -func (c *mockContextInfoRequest) GetHTTPRequestBody() []byte { - data, _ := json.Marshal(c) - return data -} - -func (c *mockContextInfoRequest) GetContent() []byte { - data, _ := json.Marshal(c.Request) - return data -} - func Test_CallTarget(t *testing.T) { type args struct { ctx context.Context + info *middleware.ContextInfoRequest + server *callTestServer target *mockTarget - sleep time.Duration - - info ContextInfoRequest - - method string - body []byte - - respBody []byte - statusCode int } type res struct { body []byte @@ -234,16 +124,18 @@ func Test_CallTarget(t *testing.T) { { "unknown targettype, error", args{ - ctx: context.Background(), - sleep: time.Second, - method: http.MethodPost, - info: newMockContextInfoRequest("content1"), + ctx: context.Background(), + info: requestContextInfo1, + server: &callTestServer{ + method: http.MethodPost, + expectBody: []byte("{\"request\":{\"request\":\"content1\"}}"), + respondBody: []byte("{\"request\":\"content2\"}"), + timeout: time.Second, + statusCode: http.StatusInternalServerError, + }, target: &mockTarget{ TargetType: 4, }, - body: []byte("{\"request\":{\"request\":\"content1\"}}"), - respBody: []byte("{\"request\":\"content2\"}"), - statusCode: http.StatusInternalServerError, }, res{ wantErr: true, @@ -252,17 +144,19 @@ func Test_CallTarget(t *testing.T) { { "webhook, error", args{ - ctx: context.Background(), - sleep: time.Second, - method: http.MethodPost, - info: newMockContextInfoRequest("content1"), + ctx: context.Background(), + info: requestContextInfo1, + server: &callTestServer{ + timeout: time.Second, + method: http.MethodPost, + expectBody: []byte("{\"request\":{\"request\":\"content1\"}}"), + respondBody: []byte("{\"request\":\"content2\"}"), + statusCode: http.StatusInternalServerError, + }, target: &mockTarget{ TargetType: domain.TargetTypeWebhook, Timeout: time.Minute, }, - body: []byte("{\"request\":{\"request\":\"content1\"}}"), - respBody: []byte("{\"request\":\"content2\"}"), - statusCode: http.StatusInternalServerError, }, res{ wantErr: true, @@ -271,17 +165,19 @@ func Test_CallTarget(t *testing.T) { { "webhook, ok", args{ - ctx: context.Background(), - sleep: time.Second, - method: http.MethodPost, - info: newMockContextInfoRequest("content1"), + ctx: context.Background(), + info: requestContextInfo1, + server: &callTestServer{ + timeout: time.Second, + method: http.MethodPost, + expectBody: []byte("{\"request\":{\"request\":\"content1\"}}"), + respondBody: []byte("{\"request\":\"content2\"}"), + statusCode: http.StatusOK, + }, target: &mockTarget{ TargetType: domain.TargetTypeWebhook, Timeout: time.Minute, }, - body: []byte("{\"request\":{\"request\":\"content1\"}}"), - respBody: []byte("{\"request\":\"content2\"}"), - statusCode: http.StatusOK, }, res{ body: nil, @@ -290,17 +186,19 @@ func Test_CallTarget(t *testing.T) { { "request response, error", args{ - ctx: context.Background(), - sleep: time.Second, - method: http.MethodPost, - info: newMockContextInfoRequest("content1"), + ctx: context.Background(), + info: requestContextInfo1, + server: &callTestServer{ + timeout: time.Second, + method: http.MethodPost, + expectBody: []byte("{\"request\":{\"request\":\"content1\"}}"), + respondBody: []byte("{\"request\":\"content2\"}"), + statusCode: http.StatusInternalServerError, + }, target: &mockTarget{ TargetType: domain.TargetTypeCall, Timeout: time.Minute, }, - body: []byte("{\"request\":{\"request\":\"content1\"}}"), - respBody: []byte("{\"request\":\"content2\"}"), - statusCode: http.StatusInternalServerError, }, res{ wantErr: true, @@ -309,17 +207,19 @@ func Test_CallTarget(t *testing.T) { { "request response, ok", args{ - ctx: context.Background(), - sleep: time.Second, - method: http.MethodPost, - info: newMockContextInfoRequest("content1"), + ctx: context.Background(), + info: requestContextInfo1, + server: &callTestServer{ + timeout: time.Second, + method: http.MethodPost, + expectBody: []byte("{\"request\":{\"request\":\"content1\"}}"), + respondBody: []byte("{\"request\":\"content2\"}"), + statusCode: http.StatusOK, + }, target: &mockTarget{ TargetType: domain.TargetTypeCall, Timeout: time.Minute, }, - body: []byte("{\"request\":{\"request\":\"content1\"}}"), - respBody: []byte("{\"request\":\"content2\"}"), - statusCode: http.StatusOK, }, res{ body: []byte("{\"request\":\"content2\"}"), @@ -328,14 +228,7 @@ func Test_CallTarget(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - respBody, err := testServerCall(t, - tt.args.method, - tt.args.body, - tt.args.sleep, - tt.args.statusCode, - tt.args.respBody, - testCallTarget(tt.args.ctx, tt.args.target, tt.args.info), - ) + respBody, err := testServer(t, tt.args.server, testCallTarget(tt.args.ctx, tt.args.info, tt.args.target)) if tt.res.wantErr { assert.Error(t, err) } else { @@ -345,3 +238,278 @@ func Test_CallTarget(t *testing.T) { }) } } + +func Test_CallTargets(t *testing.T) { + type args struct { + ctx context.Context + info *middleware.ContextInfoRequest + servers []*callTestServer + targets []*mockTarget + } + type res struct { + ret interface{} + wantErr bool + } + tests := []struct { + name string + args args + res res + }{ + { + "interrupt on status", + args{ + ctx: context.Background(), + info: requestContextInfo1, + servers: []*callTestServer{{ + timeout: time.Second, + method: http.MethodPost, + expectBody: requestContextInfoBody1, + respondBody: requestContextInfoBody2, + statusCode: http.StatusInternalServerError, + }, { + timeout: time.Second, + method: http.MethodPost, + expectBody: requestContextInfoBody1, + respondBody: requestContextInfoBody2, + statusCode: http.StatusInternalServerError, + }}, + targets: []*mockTarget{ + {InterruptOnError: false}, + {InterruptOnError: true}, + }, + }, + res{ + wantErr: true, + }, + }, + { + "continue on status", + args{ + ctx: context.Background(), + info: requestContextInfo1, + servers: []*callTestServer{{ + timeout: time.Second, + method: http.MethodPost, + expectBody: requestContextInfoBody1, + respondBody: requestContextInfoBody2, + statusCode: http.StatusInternalServerError, + }, { + timeout: time.Second, + method: http.MethodPost, + expectBody: requestContextInfoBody1, + respondBody: requestContextInfoBody2, + statusCode: http.StatusInternalServerError, + }}, + targets: []*mockTarget{ + {InterruptOnError: false}, + {InterruptOnError: false}, + }, + }, + res{ + ret: requestContextInfo1.GetContent(), + }, + }, + { + "interrupt on json error", + args{ + ctx: context.Background(), + info: requestContextInfo1, + servers: []*callTestServer{{ + timeout: time.Second, + method: http.MethodPost, + expectBody: requestContextInfoBody1, + respondBody: requestContextInfoBody2, + statusCode: http.StatusOK, + }, { + timeout: time.Second, + method: http.MethodPost, + expectBody: requestContextInfoBody1, + respondBody: []byte("just a string, not json"), + statusCode: http.StatusOK, + }}, + targets: []*mockTarget{ + {InterruptOnError: false}, + {InterruptOnError: true}, + }, + }, + res{ + wantErr: true, + }, + }, + { + "continue on json error", + args{ + ctx: context.Background(), + info: requestContextInfo1, + servers: []*callTestServer{{ + timeout: time.Second, + method: http.MethodPost, + expectBody: requestContextInfoBody1, + respondBody: requestContextInfoBody2, + statusCode: http.StatusOK, + }, { + timeout: time.Second, + method: http.MethodPost, + expectBody: requestContextInfoBody1, + respondBody: []byte("just a string, not json"), + statusCode: http.StatusOK, + }}, + targets: []*mockTarget{ + {InterruptOnError: false}, + {InterruptOnError: false}, + }}, + res{ + ret: requestContextInfo1.GetContent(), + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + respBody, err := testServers(t, + tt.args.servers, + testCallTargets(tt.args.ctx, tt.args.info, tt.args.targets), + ) + if tt.res.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + fmt.Println(respBody) + assert.Equal(t, tt.res.ret, respBody) + }) + } +} + +var _ execution.Target = &mockTarget{} + +type mockTarget struct { + InstanceID string + ExecutionID string + TargetID string + TargetType domain.TargetType + Endpoint string + Timeout time.Duration + InterruptOnError bool +} + +func (e *mockTarget) GetTargetID() string { + return e.TargetID +} +func (e *mockTarget) IsInterruptOnError() bool { + return e.InterruptOnError +} +func (e *mockTarget) GetEndpoint() string { + return e.Endpoint +} +func (e *mockTarget) GetTargetType() domain.TargetType { + return e.TargetType +} +func (e *mockTarget) GetTimeout() time.Duration { + return e.Timeout +} + +type callTestServer struct { + method string + expectBody []byte + timeout time.Duration + statusCode int + respondBody []byte +} + +func testServers( + t *testing.T, + c []*callTestServer, + call func([]string) (interface{}, error), +) (interface{}, error) { + urls := make([]string, len(c)) + for i := range c { + url, close := listen(t, c[i]) + defer close() + urls[i] = url + } + return call(urls) +} + +func testServer( + t *testing.T, + c *callTestServer, + call func(string) ([]byte, error), +) ([]byte, error) { + url, close := listen(t, c) + defer close() + return call(url) +} + +func listen( + t *testing.T, + c *callTestServer, +) (url string, close func()) { + handler := func(w http.ResponseWriter, r *http.Request) { + checkRequest(t, r, c.method, c.expectBody) + + if c.statusCode != http.StatusOK { + http.Error(w, "error", c.statusCode) + return + } + + time.Sleep(c.timeout) + + w.Header().Set("Content-Type", "application/json") + if _, err := io.WriteString(w, string(c.respondBody)); err != nil { + http.Error(w, "error", http.StatusInternalServerError) + return + } + } + server := httptest.NewServer(http.HandlerFunc(handler)) + return server.URL, server.Close +} + +func checkRequest(t *testing.T, sent *http.Request, method string, expectedBody []byte) { + sentBody, err := io.ReadAll(sent.Body) + require.NoError(t, err) + require.Equal(t, expectedBody, sentBody) + require.Equal(t, method, sent.Method) +} + +func testCall(ctx context.Context, timeout time.Duration, body []byte) func(string) ([]byte, error) { + return func(url string) ([]byte, error) { + return execution.Call(ctx, url, timeout, body) + } +} + +func testCallTarget(ctx context.Context, + info *middleware.ContextInfoRequest, + target *mockTarget, +) func(string) ([]byte, error) { + return func(url string) (r []byte, err error) { + target.Endpoint = url + return execution.CallTarget(ctx, target, info) + } +} + +func testCallTargets(ctx context.Context, + info *middleware.ContextInfoRequest, + target []*mockTarget, +) func([]string) (interface{}, error) { + return func(urls []string) (interface{}, error) { + targets := make([]execution.Target, len(target)) + for i, t := range target { + t.Endpoint = urls[i] + targets[i] = t + } + return execution.CallTargets(ctx, targets, info) + } +} + +var requestContextInfo1 = &middleware.ContextInfoRequest{ + Request: &request{ + Request: "content1", + }, +} + +var requestContextInfoBody1 = []byte("{\"request\":{\"request\":\"content1\"}}") +var requestContextInfoBody2 = []byte("{\"request\":{\"request\":\"content2\"}}") + +type request struct { + Request string `json:"request"` +} diff --git a/internal/integration/assert.go b/internal/integration/assert.go index 6928054e8e..682a82ff7f 100644 --- a/internal/integration/assert.go +++ b/internal/integration/assert.go @@ -9,6 +9,9 @@ import ( "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/timestamppb" + + resources_object "github.com/zitadel/zitadel/pkg/grpc/resources/object/v3alpha" + settings_object "github.com/zitadel/zitadel/pkg/grpc/settings/object/v3alpha" ) // Details is the interface that covers both v1 and v2 proto generated object details. @@ -63,6 +66,31 @@ func AssertDetails[D Details, M DetailsMsg[D]](t testing.TB, expected, actual M) assert.Equal(t, wantDetails.GetResourceOwner(), gotDetails.GetResourceOwner()) } +func AssertResourceDetails(t testing.TB, expected *resources_object.Details, actual *resources_object.Details) { + assert.NotZero(t, actual.GetSequence()) + + if expected.GetChangeDate() != nil { + wantChangeDate := time.Now() + gotChangeDate := actual.GetChangeDate().AsTime() + assert.WithinRange(t, gotChangeDate, wantChangeDate.Add(-time.Minute), wantChangeDate.Add(time.Minute)) + } + + assert.Equal(t, expected.GetOwner(), actual.GetOwner()) + assert.NotEmpty(t, actual.GetId()) +} + +func AssertSettingsDetails(t testing.TB, expected *settings_object.Details, actual *settings_object.Details) { + assert.NotZero(t, actual.GetSequence()) + + if expected.GetChangeDate() != nil { + wantChangeDate := time.Now() + gotChangeDate := actual.GetChangeDate().AsTime() + assert.WithinRange(t, gotChangeDate, wantChangeDate.Add(-time.Minute), wantChangeDate.Add(time.Minute)) + } + + assert.Equal(t, expected.GetOwner(), actual.GetOwner()) +} + func AssertListDetails[L ListDetails, D ListDetailsMsg[L]](t testing.TB, expected, actual D) { wantDetails, gotDetails := expected.GetDetails(), actual.GetDetails() var nilDetails L diff --git a/internal/integration/client.go b/internal/integration/client.go index 7cb3af4c7b..3819682618 100644 --- a/internal/integration/client.go +++ b/internal/integration/client.go @@ -25,21 +25,20 @@ import ( openid "github.com/zitadel/zitadel/internal/idp/providers/oidc" "github.com/zitadel/zitadel/internal/idp/providers/saml" "github.com/zitadel/zitadel/internal/repository/idp" - action "github.com/zitadel/zitadel/pkg/grpc/action/v3alpha" "github.com/zitadel/zitadel/pkg/grpc/admin" "github.com/zitadel/zitadel/pkg/grpc/auth" "github.com/zitadel/zitadel/pkg/grpc/feature/v2" feature_v2beta "github.com/zitadel/zitadel/pkg/grpc/feature/v2beta" mgmt "github.com/zitadel/zitadel/pkg/grpc/management" - "github.com/zitadel/zitadel/pkg/grpc/object/v2" + object "github.com/zitadel/zitadel/pkg/grpc/object/v2" oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2" oidc_pb_v2beta "github.com/zitadel/zitadel/pkg/grpc/oidc/v2beta" - "github.com/zitadel/zitadel/pkg/grpc/org/v2" - organisation "github.com/zitadel/zitadel/pkg/grpc/org/v2" + org "github.com/zitadel/zitadel/pkg/grpc/org/v2" org_v2beta "github.com/zitadel/zitadel/pkg/grpc/org/v2beta" - "github.com/zitadel/zitadel/pkg/grpc/session/v2" + action "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha" + session "github.com/zitadel/zitadel/pkg/grpc/session/v2" session_v2beta "github.com/zitadel/zitadel/pkg/grpc/session/v2beta" - "github.com/zitadel/zitadel/pkg/grpc/settings/v2" + settings "github.com/zitadel/zitadel/pkg/grpc/settings/v2" settings_v2beta "github.com/zitadel/zitadel/pkg/grpc/settings/v2beta" "github.com/zitadel/zitadel/pkg/grpc/system" user_pb "github.com/zitadel/zitadel/pkg/grpc/user" @@ -62,9 +61,9 @@ type Client struct { OIDCv2beta oidc_pb_v2beta.OIDCServiceClient OIDCv2 oidc_pb.OIDCServiceClient OrgV2beta org_v2beta.OrganizationServiceClient - OrgV2 organisation.OrganizationServiceClient + OrgV2 org.OrganizationServiceClient System system.SystemServiceClient - ActionV3 action.ActionServiceClient + ActionV3 action.ZITADELActionsClient FeatureV2beta feature_v2beta.FeatureServiceClient FeatureV2 feature.FeatureServiceClient UserSchemaV3 schema.UserSchemaServiceClient @@ -85,9 +84,9 @@ func newClient(cc *grpc.ClientConn) Client { OIDCv2beta: oidc_pb_v2beta.NewOIDCServiceClient(cc), OIDCv2: oidc_pb.NewOIDCServiceClient(cc), OrgV2beta: org_v2beta.NewOrganizationServiceClient(cc), - OrgV2: organisation.NewOrganizationServiceClient(cc), + OrgV2: org.NewOrganizationServiceClient(cc), System: system.NewSystemServiceClient(cc), - ActionV3: action.NewActionServiceClient(cc), + ActionV3: action.NewZITADELActionsClient(cc), FeatureV2beta: feature_v2beta.NewFeatureServiceClient(cc), FeatureV2: feature.NewFeatureServiceClient(cc), UserSchemaV3: schema.NewUserSchemaServiceClient(cc), @@ -627,50 +626,52 @@ func (s *Tester) CreateTarget(ctx context.Context, t *testing.T, name, endpoint if name != "" { nameSet = name } - req := &action.CreateTargetRequest{ + reqTarget := &action.Target{ Name: nameSet, Endpoint: endpoint, Timeout: durationpb.New(10 * time.Second), } switch ty { case domain.TargetTypeWebhook: - req.TargetType = &action.CreateTargetRequest_RestWebhook{ + reqTarget.TargetType = &action.Target_RestWebhook{ RestWebhook: &action.SetRESTWebhook{ InterruptOnError: interrupt, }, } case domain.TargetTypeCall: - req.TargetType = &action.CreateTargetRequest_RestCall{ + reqTarget.TargetType = &action.Target_RestCall{ RestCall: &action.SetRESTCall{ InterruptOnError: interrupt, }, } case domain.TargetTypeAsync: - req.TargetType = &action.CreateTargetRequest_RestAsync{ + reqTarget.TargetType = &action.Target_RestAsync{ RestAsync: &action.SetRESTAsync{}, } } - target, err := s.Client.ActionV3.CreateTarget(ctx, req) - require.NoError(t, err) - return target -} - -func (s *Tester) SetExecution(ctx context.Context, t *testing.T, cond *action.Condition, targets []*action.ExecutionTargetType) *action.SetExecutionResponse { - target, err := s.Client.ActionV3.SetExecution(ctx, &action.SetExecutionRequest{ - Condition: cond, - Targets: targets, - }) + target, err := s.Client.ActionV3.CreateTarget(ctx, &action.CreateTargetRequest{Target: reqTarget}) require.NoError(t, err) return target } func (s *Tester) DeleteExecution(ctx context.Context, t *testing.T, cond *action.Condition) { - _, err := s.Client.ActionV3.DeleteExecution(ctx, &action.DeleteExecutionRequest{ + _, err := s.Client.ActionV3.SetExecution(ctx, &action.SetExecutionRequest{ Condition: cond, }) require.NoError(t, err) } +func (s *Tester) SetExecution(ctx context.Context, t *testing.T, cond *action.Condition, targets []*action.ExecutionTargetType) *action.SetExecutionResponse { + target, err := s.Client.ActionV3.SetExecution(ctx, &action.SetExecutionRequest{ + Condition: cond, + Execution: &action.Execution{ + Targets: targets, + }, + }) + require.NoError(t, err) + return target +} + func (s *Tester) CreateUserSchema(ctx context.Context, t *testing.T) *schema.CreateUserSchemaResponse { return s.CreateUserSchemaWithType(ctx, t, fmt.Sprint(time.Now().UnixNano()+1)) } diff --git a/internal/repository/execution/execution.go b/internal/repository/execution/execution.go index a6851b4495..855d646e08 100644 --- a/internal/repository/execution/execution.go +++ b/internal/repository/execution/execution.go @@ -5,6 +5,7 @@ import ( "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/zerrors" ) const ( @@ -56,6 +57,13 @@ type Target struct { Target string `json:"target"` } +func (t *Target) Validate() error { + if t.Type == domain.ExecutionTargetTypeUnspecified || t.Target == "" { + return zerrors.ThrowInvalidArgument(nil, "COMMAND-hdm4zl1hmd", "Errors.Execution.Invalid") + } + return nil +} + func NewSetEventV2( ctx context.Context, aggregate *eventstore.Aggregate, diff --git a/internal/static/i18n/bg.yaml b/internal/static/i18n/bg.yaml index 007a20bf53..1161a4928d 100644 --- a/internal/static/i18n/bg.yaml +++ b/internal/static/i18n/bg.yaml @@ -580,6 +580,7 @@ Errors: NotFound: Изпълнението не е намерено IncludeNotFound: Включването не е намерено NoTargets: Няма определени цели + ResponseIsNotValidJSON: Отговорът не е валиден JSON UserSchema: NotEnabled: Функцията „Потребителска схема“ не е активирана Type: diff --git a/internal/static/i18n/cs.yaml b/internal/static/i18n/cs.yaml index b119e1bba7..3383021b48 100644 --- a/internal/static/i18n/cs.yaml +++ b/internal/static/i18n/cs.yaml @@ -561,6 +561,7 @@ Errors: NotFound: Provedení nenalezeno IncludeNotFound: Zahrnout nenalezeno NoTargets: Nejsou definovány žádné cíle + ResponseIsNotValidJSON: Odpověď není platný JSON UserSchema: NotEnabled: Funkce "Uživatelské schéma" není povolena Type: diff --git a/internal/static/i18n/de.yaml b/internal/static/i18n/de.yaml index 518e1ec501..e9ebccf3bf 100644 --- a/internal/static/i18n/de.yaml +++ b/internal/static/i18n/de.yaml @@ -563,6 +563,7 @@ Errors: NotFound: Ausführung nicht gefunden IncludeNotFound: Einschließen nicht gefunden NoTargets: Keine Ziele definiert + ResponseIsNotValidJSON: Antwort ist kein gültiges JSON UserSchema: NotEnabled: Funktion Benutzerschema ist nicht aktiviert Type: diff --git a/internal/static/i18n/en.yaml b/internal/static/i18n/en.yaml index 7cedac9fd6..a35cfc043d 100644 --- a/internal/static/i18n/en.yaml +++ b/internal/static/i18n/en.yaml @@ -563,6 +563,7 @@ Errors: NotFound: Execution not found IncludeNotFound: Include not found NoTargets: No targets defined + ResponseIsNotValidJSON: Response is not valid JSON UserSchema: NotEnabled: Feature "User Schema" is not enabled Type: diff --git a/internal/static/i18n/es.yaml b/internal/static/i18n/es.yaml index bae735134e..b4ba11cfaa 100644 --- a/internal/static/i18n/es.yaml +++ b/internal/static/i18n/es.yaml @@ -563,6 +563,7 @@ Errors: NotFound: Ejecución no encontrada IncludeNotFound: Incluir no encontrado NoTargets: No hay objetivos definidos + ResponseIsNotValidJSON: La respuesta no es un JSON válido UserSchema: NotEnabled: La función "Esquema de usuario" no está habilitada Type: diff --git a/internal/static/i18n/fr.yaml b/internal/static/i18n/fr.yaml index 9c8e215e25..e10df340da 100644 --- a/internal/static/i18n/fr.yaml +++ b/internal/static/i18n/fr.yaml @@ -563,6 +563,7 @@ Errors: NotFound: Exécution introuvable IncludeNotFound: Inclure introuvable NoTargets: Aucune cible définie + ResponseIsNotValidJSON: La réponse n'est pas un JSON valide UserSchema: NotEnabled: La fonctionnalité "Schéma utilisateur" n'est pas activée Type: diff --git a/internal/static/i18n/it.yaml b/internal/static/i18n/it.yaml index dcc0fab2f3..a853e28748 100644 --- a/internal/static/i18n/it.yaml +++ b/internal/static/i18n/it.yaml @@ -563,6 +563,7 @@ Errors: NotFound: Esecuzione non trovata IncludeNotFound: Includi non trovato NoTargets: Nessun obiettivo definito + ResponseIsNotValidJSON: La risposta non è un JSON valido UserSchema: NotEnabled: La funzionalità "Schema utente" non è abilitata Type: diff --git a/internal/static/i18n/ja.yaml b/internal/static/i18n/ja.yaml index a4224571ec..725cdcc7ab 100644 --- a/internal/static/i18n/ja.yaml +++ b/internal/static/i18n/ja.yaml @@ -552,6 +552,7 @@ Errors: NotFound: 実行が見つかりませんでした IncludeNotFound: 見つからないものを含める NoTargets: ターゲットが定義されていません + ResponseIsNotValidJSON: 応答は有効な JSON ではありません UserSchema: NotEnabled: 機能「ユーザースキーマ」が有効になっていません Type: diff --git a/internal/static/i18n/mk.yaml b/internal/static/i18n/mk.yaml index 8102a4d557..d7aabafe3d 100644 --- a/internal/static/i18n/mk.yaml +++ b/internal/static/i18n/mk.yaml @@ -562,6 +562,7 @@ Errors: NotFound: Извршувањето не е пронајдено IncludeNotFound: Вклучете не е пронајден NoTargets: Не се дефинирани цели + ResponseIsNotValidJSON: Одговорот не е валиден JSON UserSchema: NotEnabled: Функцијата „Корисничка шема“ не е овозможена Type: diff --git a/internal/static/i18n/nl.yaml b/internal/static/i18n/nl.yaml index 11a9510d0d..fa5e3b6ce5 100644 --- a/internal/static/i18n/nl.yaml +++ b/internal/static/i18n/nl.yaml @@ -563,6 +563,7 @@ Errors: NotFound: Uitvoering niet gevonden IncludeNotFound: Inclusief niet gevonden NoTargets: Geen doelstellingen gedefinieerd + ResponseIsNotValidJSON: Reactie is geen geldige JSON UserSchema: NotEnabled: Functie "Gebruikersschema" is niet ingeschakeld Type: diff --git a/internal/static/i18n/pl.yaml b/internal/static/i18n/pl.yaml index d5d688e021..c6081de5a7 100644 --- a/internal/static/i18n/pl.yaml +++ b/internal/static/i18n/pl.yaml @@ -563,6 +563,7 @@ Errors: NotFound: Nie znaleziono wykonania IncludeNotFound: Nie znaleziono uwzględnienia NoTargets: Nie zdefiniowano celów + ResponseIsNotValidJSON: Odpowiedź nie jest prawidłowym JSON-em UserSchema: NotEnabled: Funkcja „Schemat użytkownika” nie jest włączona Type: diff --git a/internal/static/i18n/pt.yaml b/internal/static/i18n/pt.yaml index 59506fc1d0..e980b4ea21 100644 --- a/internal/static/i18n/pt.yaml +++ b/internal/static/i18n/pt.yaml @@ -558,6 +558,7 @@ Errors: NotFound: Execução não encontrada IncludeNotFound: Incluir não encontrado NoTargets: Nenhuma meta definida + ResponseIsNotValidJSON: A resposta não é um JSON válido UserSchema: NotEnabled: O recurso "Esquema do usuário" não está habilitado Type: diff --git a/internal/static/i18n/ru.yaml b/internal/static/i18n/ru.yaml index 7bde4b3f2b..b2aa62d28f 100644 --- a/internal/static/i18n/ru.yaml +++ b/internal/static/i18n/ru.yaml @@ -552,6 +552,7 @@ Errors: NotFound: Исполнение не найдено IncludeNotFound: Включить не найдено NoTargets: Цели не определены + ResponseIsNotValidJSON: Ответ не является допустимым JSON UserSchema: NotEnabled: Функция «Пользовательская схема» не включена Type: diff --git a/internal/static/i18n/sv.yaml b/internal/static/i18n/sv.yaml index 1540c63a1d..d995df06f5 100644 --- a/internal/static/i18n/sv.yaml +++ b/internal/static/i18n/sv.yaml @@ -563,6 +563,7 @@ Errors: NotFound: Exekveringen hittades inte IncludeNotFound: Inkluderingen hittades inte NoTargets: Inga mål definierade + ResponseIsNotValidJSON: Svaret är inte giltigt JSON UserSchema: NotEnabled: Funktionen "Användarschema" är inte aktiverad Type: diff --git a/internal/static/i18n/zh.yaml b/internal/static/i18n/zh.yaml index 4c78a33cb7..b4fa6e90be 100644 --- a/internal/static/i18n/zh.yaml +++ b/internal/static/i18n/zh.yaml @@ -563,6 +563,7 @@ Errors: NotFound: 未找到执行 IncludeNotFound: 包括未找到的内容 NoTargets: 没有定义目标 + ResponseIsNotValidJSON: 响应不是有效的 JSON UserSchema: NotEnabled: 未启用“用户架构”功能 Type: diff --git a/proto/zitadel/action/v3alpha/action_service.proto b/proto/zitadel/action/v3alpha/action_service.proto deleted file mode 100644 index 938c9e88fc..0000000000 --- a/proto/zitadel/action/v3alpha/action_service.proto +++ /dev/null @@ -1,612 +0,0 @@ -syntax = "proto3"; - -package zitadel.action.v3alpha; - -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/action/v3alpha/target.proto"; -import "zitadel/action/v3alpha/execution.proto"; -import "zitadel/action/v3alpha/query.proto"; -import "zitadel/object/v2/object.proto"; -import "zitadel/protoc_gen_zitadel/v2/options.proto"; - -option go_package = "github.com/zitadel/zitadel/pkg/grpc/action/v3alpha;action"; - -option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { - info: { - title: "Action Service"; - version: "3.0-preview"; - description: "This API is intended to manage custom executions (previously known as actions) in a ZITADEL instance. This project is in preview state. It can AND will continue breaking until the services provide the same functionality as the current actions."; - contact:{ - name: "ZITADEL" - url: "https://zitadel.com" - email: "hi@zitadel.com" - } - license: { - name: "Apache 2.0", - url: "https://github.com/zitadel/zitadel/blob/main/LICENSE"; - }; - }; - 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 ActionService { - - // Create a target - // - // Create a new target, which can be used in executions. - rpc CreateTarget (CreateTargetRequest) returns (CreateTargetResponse) { - option (google.api.http) = { - post: "/v3alpha/targets" - body: "*" - }; - - option (zitadel.protoc_gen_zitadel.v2.options) = { - auth_option: { - permission: "execution.target.write" - } - http_response: { - success_code: 201 - } - }; - - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - responses: { - key: "201"; - value: { - description: "Target successfully created"; - schema: { - json_schema: { - ref: "#/definitions/v3alphaCreateTargetResponse"; - } - } - }; - }; - }; - } - - // Update a target - // - // Update an existing target. - rpc UpdateTarget (UpdateTargetRequest) returns (UpdateTargetResponse) { - option (google.api.http) = { - put: "/v3alpha/targets/{target_id}" - body: "*" - }; - - option (zitadel.protoc_gen_zitadel.v2.options) = { - auth_option: { - permission: "execution.target.write" - } - }; - - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - responses: { - key: "200"; - value: { - description: "Target successfully updated"; - }; - }; - }; - } - - // Delete a target - // - // Delete an existing target. This will remove it from any configured execution as well. - rpc DeleteTarget (DeleteTargetRequest) returns (DeleteTargetResponse) { - option (google.api.http) = { - delete: "/v3alpha/targets/{target_id}" - }; - - option (zitadel.protoc_gen_zitadel.v2.options) = { - auth_option: { - permission: "execution.target.delete" - } - }; - - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - responses: { - key: "200"; - value: { - description: "Target successfully deleted"; - }; - }; - }; - } - - // List targets - // - // List all matching targets. By default, we will return all targets of your instance. - // Make sure to include a limit and sorting for pagination. - rpc ListTargets (ListTargetsRequest) returns (ListTargetsResponse) { - option (google.api.http) = { - post: "/v3alpha/targets/search" - body: "*" - }; - - option (zitadel.protoc_gen_zitadel.v2.options) = { - auth_option: { - permission: "execution.target.read" - } - }; - - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - responses: { - key: "200"; - value: { - description: "A list of all targets matching the query"; - }; - }; - responses: { - key: "400"; - value: { - description: "invalid list query"; - schema: { - json_schema: { - ref: "#/definitions/rpcStatus"; - }; - }; - }; - }; - }; - } - - // Target by ID - // - // Returns the target identified by the requested ID. - rpc GetTargetByID (GetTargetByIDRequest) returns (GetTargetByIDResponse) { - option (google.api.http) = { - get: "/v3alpha/targets/{target_id}" - }; - - option (zitadel.protoc_gen_zitadel.v2.options) = { - auth_option: { - permission: "execution.target.read" - } - }; - - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - responses: { - key: "200" - value: { - description: "Target successfully retrieved"; - } - }; - }; - } - - // Set an execution - // - // Set an execution to call a previously defined target or include the targets of a previously defined execution. - rpc SetExecution (SetExecutionRequest) returns (SetExecutionResponse) { - option (google.api.http) = { - put: "/v3alpha/executions" - body: "*" - }; - - option (zitadel.protoc_gen_zitadel.v2.options) = { - auth_option: { - permission: "execution.write" - } - }; - - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - responses: { - key: "200"; - value: { - description: "Execution successfully set"; - }; - }; - }; - } - - // Delete an execution - // - // Delete an existing execution. - rpc DeleteExecution (DeleteExecutionRequest) returns (DeleteExecutionResponse) { - option (google.api.http) = { - delete: "/v3alpha/executions" - body: "*" - }; - - option (zitadel.protoc_gen_zitadel.v2.options) = { - auth_option: { - permission: "execution.delete" - } - }; - - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - responses: { - key: "200"; - value: { - description: "Execution successfully deleted"; - }; - }; - }; - } - - // List executions - // - // List all matching executions. By default, we will return all executions of your instance. - // Make sure to include a limit and sorting for pagination. - rpc ListExecutions (ListExecutionsRequest) returns (ListExecutionsResponse) { - option (google.api.http) = { - post: "/v3alpha/executions/search" - body: "*" - }; - - option (zitadel.protoc_gen_zitadel.v2.options) = { - auth_option: { - permission: "execution.read" - } - }; - - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - responses: { - key: "200"; - value: { - description: "A list of all executions matching the query"; - }; - }; - responses: { - key: "400"; - value: { - description: "invalid list query"; - schema: { - json_schema: { - ref: "#/definitions/rpcStatus"; - }; - }; - }; - }; - }; - } - - // List all available functions - // - // List all available functions which can be used as condition for executions. - rpc ListExecutionFunctions (ListExecutionFunctionsRequest) returns (ListExecutionFunctionsResponse) { - option (google.api.http) = { - get: "/v3alpha/executions/functions" - }; - - option (zitadel.protoc_gen_zitadel.v2.options) = { - auth_option: { - permission: "execution.read" - } - }; - - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - responses: { - key: "200"; - value: { - description: "List all functions successfully"; - }; - }; - }; - } - // List all available methods - // - // List all available methods which can be used as condition for executions. - rpc ListExecutionMethods (ListExecutionMethodsRequest) returns (ListExecutionMethodsResponse) { - option (google.api.http) = { - get: "/v3alpha/executions/methods" - }; - - option (zitadel.protoc_gen_zitadel.v2.options) = { - auth_option: { - permission: "execution.read" - } - }; - - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - responses: { - key: "200"; - value: { - description: "List all methods successfully"; - }; - }; - }; - } - // List all available service - // - // List all available services which can be used as condition for executions. - rpc ListExecutionServices (ListExecutionServicesRequest) returns (ListExecutionServicesResponse) { - option (google.api.http) = { - get: "/v3alpha/executions/services" - }; - - option (zitadel.protoc_gen_zitadel.v2.options) = { - auth_option: { - permission: "execution.read" - } - }; - - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - responses: { - key: "200"; - value: { - description: "List all services successfully"; - }; - }; - }; - } -} - -message CreateTargetRequest { - // Unique name of the target. - string name = 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: "\"ip_allow_list\""; - } - ]; - // Defines the target type and how the response of the target is treated. - oneof target_type { - option (validate.required) = true; - - SetRESTWebhook rest_webhook = 2; - SetRESTCall rest_call = 3; - SetRESTAsync rest_async = 4; - } - // Timeout defines the duration until ZITADEL cancels the execution. - google.protobuf.Duration timeout = 5 [ - (validate.rules).duration = {gt: {seconds: 0}, required: true}, - (google.api.field_behavior) = REQUIRED, - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"10s\""; - } - ]; - string endpoint = 6 [ - (validate.rules).string = {min_len: 1, max_len: 1000, uri: true}, - (google.api.field_behavior) = REQUIRED, - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - min_length: 1, - max_length: 1000, - example: "\"https://example.com/hooks/ip_check\""; - } - ]; -} - -message CreateTargetResponse { - // ID is the read-only unique identifier of the target. - string id = 1; - // Details provide some base information (such as the last change date) of the target. - zitadel.object.v2.Details details = 2; -} - -message UpdateTargetRequest { - // unique identifier of the target. - string target_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\""; - } - ]; - // Optionally change the unique name of the target. - 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: "\"ip_allow_list\""; - } - ]; - // Optionally change the target type and how the response of the target is treated, - // or its target URL. - oneof target_type { - SetRESTWebhook rest_webhook = 3; - SetRESTCall rest_call = 4; - SetRESTAsync rest_async = 5; - } - // Optionally change the timeout, which defines the duration until ZITADEL cancels the execution. - optional google.protobuf.Duration timeout = 6 [ - (validate.rules).duration = {gt: {seconds: 0}}, - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"10s\""; - } - ]; - - optional string endpoint = 7 [ - (validate.rules).string = {max_len: 1000, uri: true}, - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - max_length: 1000, - example: "\"https://example.com/hooks/ip_check\""; - } - ]; -} - -message UpdateTargetResponse { - // Details provide some base information (such as the last change date) of the target. - zitadel.object.v2.Details details = 1; -} - -message DeleteTargetRequest { - // unique identifier of the target. - string target_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 DeleteTargetResponse { - // Details provide some base information (such as the last change date) of the target. - zitadel.object.v2.Details details = 1; -} - -message ListTargetsRequest { - // list limitations and ordering. - zitadel.object.v2.ListQuery query = 1; - // the field the result is sorted. - zitadel.action.v3alpha.TargetFieldName sorting_column = 2 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"FIELD_NAME_SCHEMA_TYPE\"" - } - ]; - // Define the criteria to query for. - repeated zitadel.action.v3alpha.TargetSearchQuery queries = 3; -} - -message ListTargetsResponse { - // Details provides information about the returned result including total amount found. - zitadel.object.v2.ListDetails details = 1; - // States by which field the results are sorted. - zitadel.action.v3alpha.TargetFieldName sorting_column = 2; - // The result contains the user schemas, which matched the queries. - repeated zitadel.action.v3alpha.Target result = 3; -} - -message GetTargetByIDRequest { - // unique identifier of the target. - string target_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 GetTargetByIDResponse { - zitadel.action.v3alpha.Target target = 1; -} - -message SetExecutionRequest { - // Defines the condition type and content of the condition for execution. - Condition condition = 1; - // Ordered list of targets/includes called during the execution. - repeated zitadel.action.v3alpha.ExecutionTargetType targets = 2; -} - -message SetExecutionResponse { - // Details provide some base information (such as the last change date) of the execution. - zitadel.object.v2.Details details = 2; -} - -message DeleteExecutionRequest { - // Unique identifier of the execution. - Condition condition = 1; -} - -message DeleteExecutionResponse { - // Details provide some base information (such as the last change date) of the execution. - zitadel.object.v2.Details details = 1; -} - -message ListExecutionsRequest { - // list limitations and ordering. - zitadel.object.v2.ListQuery query = 1; - // Define the criteria to query for. - repeated zitadel.action.v3alpha.SearchQuery queries = 2; -} - -message ListExecutionsResponse { - // Details provides information about the returned result including total amount found. - zitadel.object.v2.ListDetails details = 1; - // The result contains the executions, which matched the queries. - repeated zitadel.action.v3alpha.Execution result = 2; -} - -message ListExecutionFunctionsRequest{} -message ListExecutionFunctionsResponse{ - // All available methods - repeated string functions = 1; -} -message ListExecutionMethodsRequest{} -message ListExecutionMethodsResponse{ - // All available methods - repeated string methods = 1; -} - -message ListExecutionServicesRequest{} -message ListExecutionServicesResponse{ - // All available methods - repeated string services = 1; -} \ No newline at end of file diff --git a/proto/zitadel/action/v3alpha/query.proto b/proto/zitadel/action/v3alpha/query.proto deleted file mode 100644 index e32bda1d84..0000000000 --- a/proto/zitadel/action/v3alpha/query.proto +++ /dev/null @@ -1,110 +0,0 @@ -syntax = "proto3"; - -package zitadel.action.v3alpha; - -option go_package = "github.com/zitadel/zitadel/pkg/grpc/action/v3alpha;action"; - -import "google/api/field_behavior.proto"; -import "protoc-gen-openapiv2/options/annotations.proto"; -import "validate/validate.proto"; -import "zitadel/object/v2/object.proto"; -import "zitadel/action/v3alpha/execution.proto"; - -message SearchQuery { - oneof query { - option (validate.required) = true; - - InConditionsQuery in_conditions_query = 1; - ExecutionTypeQuery execution_type_query = 2; - TargetQuery target_query = 3; - IncludeQuery include_query = 4; - } -} - -message InConditionsQuery { - // Defines the conditions to query for. - repeated Condition conditions = 1; -} - -message ExecutionTypeQuery { - // Defines the type to query for. - ExecutionType execution_type = 1; -} - -message TargetQuery { - // Defines the id to query for. - string target_id = 1 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - description: "the id of the targets to include" - example: "\"69629023906488334\""; - } - ]; -} - -message IncludeQuery { - // Defines the include to query for. - Condition include = 1 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - description: "the id of the include" - example: "\"request.zitadel.session.v2.SessionService\""; - } - ]; -} - -message TargetSearchQuery { - oneof query { - option (validate.required) = true; - - TargetNameQuery target_name_query = 1; - InTargetIDsQuery in_target_ids_query = 2; - } -} - -message TargetNameQuery { - // Defines the name of the target to query for. - string target_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.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 InTargetIDsQuery { - // Defines the ids to query for. - repeated string target_ids = 1 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - description: "the ids of the targets to include" - example: "[\"69629023906488334\",\"69622366012355662\"]"; - } - ]; -} - -enum ExecutionType { - EXECUTION_TYPE_UNSPECIFIED = 0; - EXECUTION_TYPE_REQUEST = 1; - EXECUTION_TYPE_RESPONSE = 2; - EXECUTION_TYPE_EVENT = 3; - EXECUTION_TYPE_FUNCTION = 4; -} - -enum TargetFieldName { - FIELD_NAME_UNSPECIFIED = 0; - FIELD_NAME_ID = 1; - FIELD_NAME_CREATION_DATE = 2; - FIELD_NAME_CHANGE_DATE = 3; - FIELD_NAME_NAME = 4; - FIELD_NAME_TARGET_TYPE = 5; - FIELD_NAME_URL = 6; - FIELD_NAME_TIMEOUT = 7; - FIELD_NAME_ASYNC = 8; - FIELD_NAME_INTERRUPT_ON_ERROR = 9; -} diff --git a/proto/zitadel/object/v3alpha/object.proto b/proto/zitadel/object/v3alpha/object.proto new file mode 100644 index 0000000000..fba08fa5b4 --- /dev/null +++ b/proto/zitadel/object/v3alpha/object.proto @@ -0,0 +1,22 @@ +syntax = "proto3"; + +package zitadel.object.v3alpha; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha;object"; + +import "google/protobuf/timestamp.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; +import "validate/validate.proto"; + +enum OwnerType { + OWNER_TYPE_UNSPECIFIED = 0; + OWNER_TYPE_SYSTEM = 1; + OWNER_TYPE_INSTANCE = 2; + OWNER_TYPE_ORG = 3; +} + +message Owner { + OwnerType type = 1; + string id = 2; +} + diff --git a/proto/zitadel/resources/action/v3alpha/action_service.proto b/proto/zitadel/resources/action/v3alpha/action_service.proto new file mode 100644 index 0000000000..a8ae67b95d --- /dev/null +++ b/proto/zitadel/resources/action/v3alpha/action_service.proto @@ -0,0 +1,361 @@ +syntax = "proto3"; + +package zitadel.resources.action.v3alpha; + +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/resources/action/v3alpha/target.proto"; +import "zitadel/resources/action/v3alpha/execution.proto"; +import "zitadel/resources/object/v3alpha/object.proto"; +import "zitadel/settings/object/v3alpha/object.proto"; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha;action"; + +option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { + info: { + title: "Action Service"; + version: "3.0-alpha"; + description: "This API is intended to manage custom executions (previously known as actions) in a ZITADEL instance. It will continue breaking as long as it is in alpha state."; + contact:{ + name: "ZITADEL" + url: "https://zitadel.com" + email: "hi@zitadel.com" + } + license: { + name: "Apache 2.0", + url: "https://github.com/zitadel/zitadel/blob/main/LICENSE"; + }; + }; + 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: "$ZITADEL_DOMAIN"; + base_path: "/resources/v3alpha/actions"; + + 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 ZITADELActions { + + // Create a target + // + // Create a new target, which can be used in executions. + rpc CreateTarget (CreateTargetRequest) returns (CreateTargetResponse) { + option (google.api.http) = { + post: "/targets" + body: "target" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "action.target.write" + } + http_response: { + success_code: 201 + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "201"; + value: { + description: "Target successfully created"; + schema: { + json_schema: { + ref: "#/definitions/CreateTargetResponse"; + } + } + }; + }; + }; + } + + // Patch a target + // + // Patch an existing target. + rpc PatchTarget (PatchTargetRequest) returns (PatchTargetResponse) { + option (google.api.http) = { + patch: "/targets/{id}" + body: "target" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "action.target.write" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "Target successfully updated or left unchanged"; + }; + }; + }; + } + + // Delete a target + // + // Delete an existing target. This will remove it from any configured execution as well. + rpc DeleteTarget (DeleteTargetRequest) returns (DeleteTargetResponse) { + option (google.api.http) = { + delete: "/targets/{id}" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "action.target.delete" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "Target successfully deleted"; + }; + }; + }; + } + + // Sets an execution to call a target or include the targets of another execution. + // + // Setting an empty list of targets will remove all targets from the execution, making it a noop. + rpc SetExecution (SetExecutionRequest) returns (SetExecutionResponse) { + option (google.api.http) = { + put: "/executions" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "action.execution.write" + } + http_response: { + success_code: 201 + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "Execution successfully updated or left unchanged"; + schema: { + json_schema: { + ref: "#/definitions/SetExecutionResponse"; + } + } + }; + }; + }; + } + + // List all available functions + // + // List all available functions which can be used as condition for executions. + rpc ListExecutionFunctions (ListExecutionFunctionsRequest) returns (ListExecutionFunctionsResponse) { + option (google.api.http) = { + get: "/executions/functions" + }; + + 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: "List all functions successfully"; + }; + }; + }; + } + // List all available methods + // + // List all available methods which can be used as condition for executions. + rpc ListExecutionMethods (ListExecutionMethodsRequest) returns (ListExecutionMethodsResponse) { + option (google.api.http) = { + get: "/executions/methods" + }; + + 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: "List all methods successfully"; + }; + }; + }; + } + // List all available service + // + // List all available services which can be used as condition for executions. + rpc ListExecutionServices (ListExecutionServicesRequest) returns (ListExecutionServicesResponse) { + option (google.api.http) = { + get: "/executions/services" + }; + + 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: "List all services successfully"; + }; + }; + }; + } +} + +message CreateTargetRequest { + Target target = 1; +} + +message CreateTargetResponse { + zitadel.resources.object.v3alpha.Details details = 1; +} + +message PatchTargetRequest { + 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\""; + } + ]; + PatchTarget target = 2; +} + +message PatchTargetResponse { + zitadel.resources.object.v3alpha.Details details = 1; +} + +message DeleteTargetRequest { + 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 DeleteTargetResponse { + zitadel.resources.object.v3alpha.Details details = 1; +} + +message SetExecutionRequest { + Condition condition = 1; + Execution execution = 2; +} + +message SetExecutionResponse { + zitadel.settings.object.v3alpha.Details details = 1; +} + +message ListExecutionFunctionsRequest{} +message ListExecutionFunctionsResponse{ + // All available methods + repeated string functions = 1; +} +message ListExecutionMethodsRequest{} +message ListExecutionMethodsResponse{ + // All available methods + repeated string methods = 1; +} + +message ListExecutionServicesRequest{} +message ListExecutionServicesResponse{ + // All available methods + repeated string services = 1; +} diff --git a/proto/zitadel/action/v3alpha/execution.proto b/proto/zitadel/resources/action/v3alpha/execution.proto similarity index 76% rename from proto/zitadel/action/v3alpha/execution.proto rename to proto/zitadel/resources/action/v3alpha/execution.proto index 797f997cd8..f666b4c497 100644 --- a/proto/zitadel/action/v3alpha/execution.proto +++ b/proto/zitadel/resources/action/v3alpha/execution.proto @@ -1,6 +1,6 @@ syntax = "proto3"; -package zitadel.action.v3alpha; +package zitadel.resources.action.v3alpha; import "google/api/annotations.proto"; import "google/api/field_behavior.proto"; @@ -8,21 +8,22 @@ 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"; -option go_package = "github.com/zitadel/zitadel/pkg/grpc/action/v3alpha;action"; +import "zitadel/resources/object/v3alpha/object.proto"; +import "google/protobuf/timestamp.proto"; +import "zitadel/object/v3alpha/object.proto"; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha;action"; message Execution { - Condition Condition = 1; - // Details provide some base information (such as the last change date) of the target. - zitadel.object.v2.Details details = 2; - // List of ordered list of targets/includes called during the execution. - repeated ExecutionTargetType targets = 3; + // Ordered list of targets/includes called during the execution. + repeated ExecutionTargetType targets = 1; } message ExecutionTargetType { oneof type { + option (validate.required) = true; // Unique identifier of existing target to call. string target = 1; // Unique identifier of existing execution to include targets of. @@ -47,8 +48,9 @@ message Condition { } message RequestExecution { - // Condition for the request execution, only one possible. + // Condition for the request execution. Only one is possible. oneof condition{ + option (validate.required) = true; // GRPC-method as condition. string method = 1 [ (validate.rules).string = {min_len: 1, max_len: 1000}, @@ -67,14 +69,15 @@ message RequestExecution { example: "\"zitadel.session.v2.SessionService\""; } ]; - // All calls to any available service and endpoint as condition. - bool all = 3; + // All calls to any available services and methods as condition. + bool all = 3 [(validate.rules).bool = {const: true}]; } } message ResponseExecution { - // Condition for the response execution, only one possible. + // Condition for the response execution. Only one is possible. oneof condition{ + option (validate.required) = true; // GRPC-method as condition. string method = 1 [ (validate.rules).string = {min_len: 1, max_len: 1000}, @@ -93,8 +96,8 @@ message ResponseExecution { example: "\"zitadel.session.v2.SessionService\""; } ]; - // All calls to any available service and endpoint as condition. - bool all = 3; + // All calls to any available services and methods as condition. + bool all = 3 [(validate.rules).bool = {const: true}]; } } @@ -103,9 +106,10 @@ message FunctionExecution { string name = 1 [(validate.rules).string = {min_len: 1, max_len: 1000}]; } -message EventExecution{ - // Condition for the event execution, only one possible. +message EventExecution { + // Condition for the event execution. Only one is possible. oneof condition{ + option (validate.required) = true; // Event name as condition. string event = 1 [ (validate.rules).string = {min_len: 1, max_len: 1000}, @@ -125,7 +129,6 @@ message EventExecution{ } ]; // all events as condition. - bool all = 3; + bool all = 3 [(validate.rules).bool = {const: true}]; } } - diff --git a/proto/zitadel/action/v3alpha/target.proto b/proto/zitadel/resources/action/v3alpha/target.proto similarity index 60% rename from proto/zitadel/action/v3alpha/target.proto rename to proto/zitadel/resources/action/v3alpha/target.proto index bea5a4b756..20843a530b 100644 --- a/proto/zitadel/action/v3alpha/target.proto +++ b/proto/zitadel/resources/action/v3alpha/target.proto @@ -1,6 +1,6 @@ syntax = "proto3"; -package zitadel.action.v3alpha; +package zitadel.resources.action.v3alpha; import "google/api/annotations.proto"; import "google/api/field_behavior.proto"; @@ -8,10 +8,61 @@ 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"; -option go_package = "github.com/zitadel/zitadel/pkg/grpc/action/v3alpha;action"; +option go_package = "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha;action"; + +message Target { + string name = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"ip_allow_list\""; + } + ]; + // Defines the target type and how the response of the target is treated. + oneof target_type { + option (validate.required) = true; + SetRESTWebhook rest_webhook = 2; + SetRESTCall rest_call = 3; + SetRESTAsync rest_async = 4; + } + // Timeout defines the duration until ZITADEL cancels the execution. + google.protobuf.Duration timeout = 5 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"10s\""; + } + ]; + string endpoint = 6 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"https://example.com/hooks/ip_check\""; + } + ]; +} + +message PatchTarget { + optional string name = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"ip_allow_list\""; + } + ]; + // Defines the target type and how the response of the target is treated. + oneof target_type { + SetRESTWebhook rest_webhook = 2; + SetRESTCall rest_call = 3; + SetRESTAsync rest_async = 4; + } + // Timeout defines the duration until ZITADEL cancels the execution. + optional google.protobuf.Duration timeout = 5 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"10s\""; + } + ]; + optional string endpoint = 6 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"https://example.com/hooks/ip_check\""; + } + ]; +} + // Wait for response but response body is ignored, status is checked, call is sent as post. message SetRESTWebhook { @@ -27,39 +78,3 @@ message SetRESTCall { // Call is executed in parallel to others, ZITADEL does not wait until the call is finished. The state is ignored, call is sent as post. message SetRESTAsync {} - -message Target { - // ID is the read-only unique identifier of the target. - string target_id = 1 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"69629012906488334\""; - } - ]; - // Details provide some base information (such as the last change date) of the target. - zitadel.object.v2.Details details = 2; - - // Unique name of the target. - string name = 3 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"ip_allow_list\""; - } - ]; - // Defines the target type and how the response of the target is treated. - oneof target_type { - SetRESTWebhook rest_webhook = 4; - SetRESTCall rest_call = 5; - SetRESTAsync rest_async = 6; - } - // Timeout defines the duration until ZITADEL cancels the execution. - google.protobuf.Duration timeout = 7 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"10s\""; - } - ]; - - string endpoint = 8 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"https://example.com/hooks/ip_check\""; - } - ]; -} \ No newline at end of file diff --git a/proto/zitadel/resources/object/v3alpha/object.proto b/proto/zitadel/resources/object/v3alpha/object.proto new file mode 100644 index 0000000000..65b3ef0c94 --- /dev/null +++ b/proto/zitadel/resources/object/v3alpha/object.proto @@ -0,0 +1,43 @@ +syntax = "proto3"; + +package zitadel.resources.object.v3alpha; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/resources/object/v3alpha;object"; + +import "google/api/field_behavior.proto"; +import "google/protobuf/timestamp.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; +import "validate/validate.proto"; + +import "zitadel/object/v3alpha/object.proto"; + +message Details { + string id = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629012906488334\""; + } + ]; + + //sequence represents the order of events. It's always counting + // + // on read: the sequence of the last event reduced by the projection + // + // on manipulation: the timestamp of the event(s) added by the manipulation + uint64 sequence = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2\""; + } + ]; + //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 = 3; + //resource_owner represents the context an object belongs to + zitadel.object.v3alpha.Owner owner = 4 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629023906488334\""; + } + ]; +} \ No newline at end of file diff --git a/proto/zitadel/settings/object/v3alpha/object.proto b/proto/zitadel/settings/object/v3alpha/object.proto new file mode 100644 index 0000000000..722db643d4 --- /dev/null +++ b/proto/zitadel/settings/object/v3alpha/object.proto @@ -0,0 +1,38 @@ +syntax = "proto3"; + +package zitadel.settings.object.v3alpha; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/settings/object/v3alpha;object"; + +import "google/protobuf/timestamp.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; +import "validate/validate.proto"; +import "google/protobuf/duration.proto"; + +import "zitadel/object/v3alpha/object.proto"; + +message Details { + //sequence represents the order of events. It's always counting + // + // on read: the sequence of the last event reduced by the projection + // + // on manipulation: the timestamp of the event(s) added by the manipulation + uint64 sequence = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2\""; + } + ]; + //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; + //resource_owner represents the context an object belongs to + zitadel.object.v3alpha.Owner owner = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629023906488334\""; + } + ]; +} + From 3d071fc505b506691f14d33ce971226b6f07384d Mon Sep 17 00:00:00 2001 From: Livio Spring Date: Wed, 31 Jul 2024 17:00:38 +0200 Subject: [PATCH 12/39] feat: trusted (instance) domains (#8369) # Which Problems Are Solved ZITADEL currently selects the instance context based on a HTTP header (see https://github.com/zitadel/zitadel/issues/8279#issue-2399959845 and checks it against the list of instance domains. Let's call it instance or API domain. For any context based URL (e.g. OAuth, OIDC, SAML endpoints, links in emails, ...) the requested domain (instance domain) will be used. Let's call it the public domain. In cases of proxied setups, all exposed domains (public domains) require the domain to be managed as instance domain. This can either be done using the "ExternalDomain" in the runtime config or via system API, which requires a validation through CustomerPortal on zitadel.cloud. # How the Problems Are Solved - Two new headers / header list are added: - `InstanceHostHeaders`: an ordered list (first sent wins), which will be used to match the instance. (For backward compatibility: the `HTTP1HostHeader`, `HTTP2HostHeader` and `forwarded`, `x-forwarded-for`, `x-forwarded-host` are checked afterwards as well) - `PublicHostHeaders`: an ordered list (first sent wins), which will be used as public host / domain. This will be checked against a list of trusted domains on the instance. - The middleware intercepts all requests to the API and passes a `DomainCtx` object with the hosts and protocol into the context (previously only a computed `origin` was passed) - HTTP / GRPC server do not longer try to match the headers to instances themself, but use the passed `http.DomainContext` in their interceptors. - The `RequestedHost` and `RequestedDomain` from authz.Instance are removed in favor of the `http.DomainContext` - When authenticating to or signing out from Console UI, the current `http.DomainContext(ctx).Origin` (already checked by instance interceptor for validity) is used to compute and dynamically add a `redirect_uri` and `post_logout_redirect_uri`. - Gateway passes all configured host headers (previously only did `x-zitadel-*`) - Admin API allows to manage trusted domain # Additional Changes None # Additional Context - part of #8279 - open topics: - "single-instance" mode - Console UI --- cmd/defaults.yaml | 8 + cmd/start/config.go | 70 ++++--- cmd/start/start.go | 27 +-- internal/activity/activity.go | 4 +- internal/api/api.go | 14 +- internal/api/assets/asset.go | 4 +- internal/api/authz/instance.go | 22 +- internal/api/authz/instance_test.go | 8 - internal/api/grpc/admin/instance.go | 39 ++++ internal/api/grpc/admin/instance_converter.go | 17 ++ internal/api/grpc/admin/org.go | 3 +- internal/api/grpc/admin/server.go | 5 +- internal/api/grpc/auth/passwordless.go | 3 +- internal/api/grpc/auth/server.go | 5 +- internal/api/grpc/instance/converter.go | 40 ++++ internal/api/grpc/management/information.go | 3 +- internal/api/grpc/management/org.go | 3 +- internal/api/grpc/management/server.go | 5 +- internal/api/grpc/management/user.go | 6 +- internal/api/grpc/oidc/v2/oidc.go | 3 +- internal/api/grpc/oidc/v2beta/oidc.go | 3 +- internal/api/grpc/server/gateway.go | 61 ++---- .../server/middleware/access_interceptor.go | 6 +- .../server/middleware/instance_interceptor.go | 50 +---- .../middleware/instance_interceptor_test.go | 112 ++-------- internal/api/grpc/server/server.go | 3 +- internal/api/grpc/settings/v2/server.go | 3 +- internal/api/grpc/settings/v2beta/server.go | 3 +- internal/api/http/header.go | 17 +- .../api/http/middleware/access_interceptor.go | 5 +- .../http/middleware/instance_interceptor.go | 61 ++---- .../middleware/instance_interceptor_test.go | 125 +++-------- .../api/http/middleware/origin_interceptor.go | 85 +++++--- .../middleware/origin_interceptor_test.go | 83 ++++++-- internal/api/http/request_context.go | 60 ++++++ internal/api/idp/idp.go | 20 +- internal/api/oidc/op.go | 16 +- internal/api/ui/console/console.go | 4 +- internal/api/ui/console/path/paths.go | 7 + .../api/ui/login/external_provider_handler.go | 2 +- internal/api/ui/login/login.go | 6 +- internal/api/ui/login/register_org_handler.go | 4 +- .../eventstore/token_verifier.go | 3 +- internal/command/instance.go | 16 +- internal/command/instance_domain.go | 7 +- internal/command/instance_trusted_domain.go | 50 +++++ .../command/instance_trusted_domain_test.go | 197 ++++++++++++++++++ .../command/instance_trusted_domains_model.go | 54 +++++ internal/command/main_test.go | 8 - internal/command/org.go | 5 +- internal/command/org_domain.go | 4 +- internal/command/org_domain_test.go | 42 ++-- internal/command/org_test.go | 33 +-- internal/command/user.go | 6 +- internal/command/user_human_otp.go | 3 +- internal/command/user_human_otp_test.go | 5 +- internal/command/user_v2_passkey_test.go | 9 +- internal/command/user_v2_u2f_test.go | 5 +- internal/integration/integration.go | 2 +- internal/notification/handlers/origin.go | 25 +-- .../notification/handlers/user_notifier.go | 2 +- internal/notification/types/domain_claimed.go | 2 +- .../types/email_verification_code.go | 2 +- .../types/email_verification_code_test.go | 10 +- internal/notification/types/init_code.go | 2 +- internal/notification/types/otp.go | 6 +- .../notification/types/password_change.go | 2 +- internal/notification/types/password_code.go | 2 +- .../types/passwordless_registration_link.go | 2 +- .../passwordless_registration_link_test.go | 10 +- .../types/phone_verification_code.go | 4 +- internal/notification/types/templateData.go | 2 +- internal/query/instance.go | 36 ++-- internal/query/instance_by_domain.sql | 4 +- internal/query/instance_trusted_domain.go | 142 +++++++++++++ .../query/instance_trusted_domain_test.go | 157 ++++++++++++++ internal/query/oidc_client.go | 6 + .../projection/instance_trusted_domain.go | 102 +++++++++ .../instance_trusted_domain_test.go | 119 +++++++++++ internal/query/projection/projection.go | 3 + internal/repository/instance/eventstore.go | 2 + .../repository/instance/trusted_domain.go | 95 +++++++++ internal/repository/session/session.go | 4 +- internal/repository/user/human.go | 2 +- internal/repository/user/human_email.go | 2 +- internal/repository/user/human_mfa_otp.go | 4 +- .../repository/user/human_mfa_passwordless.go | 2 +- internal/repository/user/human_password.go | 6 +- internal/repository/user/human_phone.go | 2 +- internal/repository/user/user.go | 2 +- internal/webauthn/webauthn.go | 7 +- internal/webauthn/webauthn_test.go | 14 +- proto/zitadel/admin.proto | 94 +++++++++ proto/zitadel/instance.proto | 17 ++ 94 files changed, 1693 insertions(+), 674 deletions(-) create mode 100644 internal/api/http/request_context.go create mode 100644 internal/api/ui/console/path/paths.go create mode 100644 internal/command/instance_trusted_domain.go create mode 100644 internal/command/instance_trusted_domain_test.go create mode 100644 internal/command/instance_trusted_domains_model.go create mode 100644 internal/query/instance_trusted_domain.go create mode 100644 internal/query/instance_trusted_domain_test.go create mode 100644 internal/query/projection/instance_trusted_domain.go create mode 100644 internal/query/projection/instance_trusted_domain_test.go create mode 100644 internal/repository/instance/trusted_domain.go diff --git a/cmd/defaults.yaml b/cmd/defaults.yaml index 58847e2334..7ee106840c 100644 --- a/cmd/defaults.yaml +++ b/cmd/defaults.yaml @@ -83,9 +83,17 @@ TLS: Cert: # ZITADEL_TLS_CERT # Header name of HTTP2 (incl. gRPC) calls from which the instance will be matched +# Deprecated: Use the InstanceHostHeaders instead HTTP2HostHeader: ":authority" # ZITADEL_HTTP2HOSTHEADER # Header name of HTTP1 calls from which the instance will be matched +# Deprecated: Use the InstanceHostHeaders instead HTTP1HostHeader: "host" # ZITADEL_HTTP1HOSTHEADER +# Ordered header name list, which will be used to match the instance +InstanceHostHeaders: # ZITADEL_INSTANCEHOSTHEADERS + - "x-zitadel-instance-host" +# Ordered header name list, which will be used as the public host +PublicHostHeaders: # ZITADEL_PUBLICHOSTHEADERS + - "x-zitadel-public-host" WebAuthNName: ZITADEL # ZITADEL_WEBAUTHNNAME diff --git a/cmd/start/config.go b/cmd/start/config.go index c96386521f..4ac5da13ab 100644 --- a/cmd/start/config.go +++ b/cmd/start/config.go @@ -35,40 +35,42 @@ import ( ) type Config struct { - Log *logging.Config - Port uint16 - ExternalPort uint16 - ExternalDomain string - ExternalSecure bool - TLS network.TLS - HTTP2HostHeader string - HTTP1HostHeader string - WebAuthNName string - Database database.Config - Tracing tracing.Config - Metrics metrics.Config - Projections projection.Config - Auth auth_es.Config - Admin admin_es.Config - UserAgentCookie *middleware.UserAgentCookieConfig - OIDC oidc.Config - SAML saml.Config - Login login.Config - Console console.Config - AssetStorage static_config.AssetStorageConfig - InternalAuthZ internal_authz.Config - SystemDefaults systemdefaults.SystemDefaults - EncryptionKeys *encryption.EncryptionKeyConfig - DefaultInstance command.InstanceSetup - AuditLogRetention time.Duration - SystemAPIUsers map[string]*internal_authz.SystemAPIUser - CustomerPortal string - Machine *id.Config - Actions *actions.Config - Eventstore *eventstore.Config - LogStore *logstore.Configs - Quotas *QuotasConfig - Telemetry *handlers.TelemetryPusherConfig + Log *logging.Config + Port uint16 + ExternalPort uint16 + ExternalDomain string + ExternalSecure bool + TLS network.TLS + InstanceHostHeaders []string + PublicHostHeaders []string + HTTP2HostHeader string + HTTP1HostHeader string + WebAuthNName string + Database database.Config + Tracing tracing.Config + Metrics metrics.Config + Projections projection.Config + Auth auth_es.Config + Admin admin_es.Config + UserAgentCookie *middleware.UserAgentCookieConfig + OIDC oidc.Config + SAML saml.Config + Login login.Config + Console console.Config + AssetStorage static_config.AssetStorageConfig + InternalAuthZ internal_authz.Config + SystemDefaults systemdefaults.SystemDefaults + EncryptionKeys *encryption.EncryptionKeyConfig + DefaultInstance command.InstanceSetup + AuditLogRetention time.Duration + SystemAPIUsers map[string]*internal_authz.SystemAPIUser + CustomerPortal string + Machine *id.Config + Actions *actions.Config + Eventstore *eventstore.Config + LogStore *logstore.Configs + Quotas *QuotasConfig + Telemetry *handlers.TelemetryPusherConfig } type QuotasConfig struct { diff --git a/cmd/start/start.go b/cmd/start/start.go index 6bed1168dc..1a65ea3f24 100644 --- a/cmd/start/start.go +++ b/cmd/start/start.go @@ -59,6 +59,7 @@ import ( "github.com/zitadel/zitadel/internal/api/robots_txt" "github.com/zitadel/zitadel/internal/api/saml" "github.com/zitadel/zitadel/internal/api/ui/console" + "github.com/zitadel/zitadel/internal/api/ui/console/path" "github.com/zitadel/zitadel/internal/api/ui/login" auth_es "github.com/zitadel/zitadel/internal/auth/repository/eventsourcing" "github.com/zitadel/zitadel/internal/authz" @@ -346,7 +347,7 @@ func startAPIs( } oidcPrefixes := []string{"/.well-known/openid-configuration", "/oidc/v1", "/oauth/v2"} // always set the origin in the context if available in the http headers, no matter for what protocol - router.Use(middleware.WithOrigin(config.ExternalSecure)) + router.Use(middleware.WithOrigin(config.ExternalSecure, config.HTTP1HostHeader, config.HTTP2HostHeader, config.InstanceHostHeaders, config.PublicHostHeaders)) systemTokenVerifier, err := internal_authz.StartSystemTokenVerifierFromConfig(http_util.BuildHTTP(config.ExternalDomain, config.ExternalPort, config.ExternalSecure), config.SystemAPIUsers) if err != nil { return nil, err @@ -374,7 +375,7 @@ func startAPIs( http_util.WithMaxAge(int(math.Floor(config.Quotas.Access.ExhaustedCookieMaxAge.Seconds()))), ) limitingAccessInterceptor := middleware.NewAccessInterceptor(accessSvc, exhaustedCookieHandler, &config.Quotas.Access.AccessConfig) - apis, err := api.New(ctx, config.Port, router, queries, verifier, config.InternalAuthZ, tlsConfig, config.HTTP2HostHeader, config.HTTP1HostHeader, config.ExternalDomain, limitingAccessInterceptor) + apis, err := api.New(ctx, config.Port, router, queries, verifier, config.InternalAuthZ, tlsConfig, config.ExternalDomain, append(config.InstanceHostHeaders, config.PublicHostHeaders...), limitingAccessInterceptor) if err != nil { return nil, fmt.Errorf("error creating api %w", err) } @@ -396,25 +397,25 @@ 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.RegisterServer(ctx, admin.CreateServer(config.Database.DatabaseName(), commands, queries, config.SystemDefaults, config.ExternalSecure, keys.User, config.AuditLogRetention), tlsConfig); err != nil { + if err := apis.RegisterServer(ctx, admin.CreateServer(config.Database.DatabaseName(), commands, queries, keys.User, config.AuditLogRetention), tlsConfig); err != nil { return nil, err } - if err := apis.RegisterServer(ctx, management.CreateServer(commands, queries, config.SystemDefaults, keys.User, config.ExternalSecure), tlsConfig); err != nil { + if err := apis.RegisterServer(ctx, management.CreateServer(commands, queries, config.SystemDefaults, keys.User), tlsConfig); err != nil { return nil, err } - if err := apis.RegisterServer(ctx, auth.CreateServer(commands, queries, authRepo, config.SystemDefaults, keys.User, config.ExternalSecure), tlsConfig); err != nil { + if err := apis.RegisterServer(ctx, auth.CreateServer(commands, queries, authRepo, config.SystemDefaults, keys.User), tlsConfig); err != nil { return nil, err } - if err := apis.RegisterService(ctx, user_v2beta.CreateServer(commands, queries, keys.User, keys.IDPConfig, idp.CallbackURL(config.ExternalSecure), idp.SAMLRootURL(config.ExternalSecure), assets.AssetAPI(config.ExternalSecure), permissionCheck)); err != nil { + 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(config.ExternalSecure), idp.SAMLRootURL(config.ExternalSecure), assets.AssetAPI(config.ExternalSecure), permissionCheck)); err != nil { + if err := apis.RegisterService(ctx, user_v2.CreateServer(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)); err != nil { return nil, err } - if err := apis.RegisterService(ctx, settings_v2beta.CreateServer(commands, queries, config.ExternalSecure)); err != nil { + 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 { @@ -426,7 +427,7 @@ func startAPIs( if err := apis.RegisterService(ctx, session_v2.CreateServer(commands, queries)); err != nil { return nil, err } - if err := apis.RegisterService(ctx, settings_v2.CreateServer(commands, queries, config.ExternalSecure)); err != nil { + if err := apis.RegisterService(ctx, settings_v2.CreateServer(commands, queries)); err != nil { return nil, err } if err := apis.RegisterService(ctx, org_v2.CreateServer(commands, queries, permissionCheck)); err != nil { @@ -441,11 +442,11 @@ func startAPIs( if err := apis.RegisterService(ctx, user_schema_v3_alpha.CreateServer(commands, queries)); err != nil { return nil, err } - instanceInterceptor := middleware.InstanceInterceptor(queries, config.HTTP1HostHeader, config.ExternalDomain, login.IgnoreInstanceEndpoints...) + instanceInterceptor := middleware.InstanceInterceptor(queries, config.ExternalDomain, login.IgnoreInstanceEndpoints...) assetsCache := middleware.AssetsCacheInterceptor(config.AssetStorage.Cache.MaxAge, config.AssetStorage.Cache.SharedMaxAge) apis.RegisterHandlerOnPrefix(assets.HandlerPrefix, assets.NewHandler(commands, verifier, config.InternalAuthZ, id.SonyFlakeGenerator(), store, queries, middleware.CallDurationHandler, instanceInterceptor.Handler, assetsCache.Handler, limitingAccessInterceptor.Handle)) - apis.RegisterHandlerOnPrefix(idp.HandlerPrefix, idp.NewHandler(commands, queries, keys.IDPConfig, config.ExternalSecure, instanceInterceptor.Handler)) + apis.RegisterHandlerOnPrefix(idp.HandlerPrefix, idp.NewHandler(commands, queries, keys.IDPConfig, instanceInterceptor.Handler)) userAgentInterceptor, err := middleware.NewUserAgentHandler(config.UserAgentCookie, keys.UserAgentCookieKey, id.SonyFlakeGenerator(), config.ExternalSecure, login.EndpointResources, login.EndpointExternalLoginCallbackFormPost, login.EndpointSAMLACS) if err != nil { @@ -482,8 +483,8 @@ func startAPIs( if err != nil { return nil, fmt.Errorf("unable to start console: %w", err) } - apis.RegisterHandlerOnPrefix(console.HandlerPrefix, c) - consolePath := console.HandlerPrefix + "/" + apis.RegisterHandlerOnPrefix(path.HandlerPrefix, c) + consolePath := path.HandlerPrefix + "/" l, err := login.CreateLogin( config.Login, commands, diff --git a/internal/activity/activity.go b/internal/activity/activity.go index 85095c7593..34fc411d6f 100644 --- a/internal/activity/activity.go +++ b/internal/activity/activity.go @@ -61,7 +61,7 @@ func Trigger(ctx context.Context, orgID, userID string, trigger TriggerMethod, r authz.GetInstance(ctx).InstanceID(), orgID, userID, - http_utils.ComposedOrigin(ctx), + http_utils.DomainContext(ctx).Origin(), // TODO: origin? trigger, ai.Method, ai.Path, @@ -78,7 +78,7 @@ func TriggerGRPCWithContext(ctx context.Context, trigger TriggerMethod) { authz.GetInstance(ctx).InstanceID(), authz.GetCtxData(ctx).OrgID, authz.GetCtxData(ctx).UserID, - http_utils.ComposedOrigin(ctx), + http_utils.DomainContext(ctx).Origin(), // TODO: origin? trigger, ai.Method, ai.Path, diff --git a/internal/api/api.go b/internal/api/api.go index f3ed27c2b0..15d6c5b996 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -32,7 +32,7 @@ type API struct { verifier internal_authz.APITokenVerifier health healthCheck router *mux.Router - http1HostName string + hostHeaders []string grpcGateway *server.Gateway healthServer *health.Server accessInterceptor *http_mw.AccessInterceptor @@ -75,7 +75,8 @@ func New( verifier internal_authz.APITokenVerifier, authZ internal_authz.Config, tlsConfig *tls.Config, - http2HostName, http1HostName, externalDomain string, + externalDomain string, + hostHeaders []string, accessInterceptor *http_mw.AccessInterceptor, ) (_ *API, err error) { api := &API{ @@ -83,13 +84,13 @@ func New( verifier: verifier, health: queries, router: router, - http1HostName: http1HostName, queries: queries, accessInterceptor: accessInterceptor, + hostHeaders: hostHeaders, } - api.grpcServer = server.CreateServer(api.verifier, authZ, queries, http2HostName, externalDomain, tlsConfig, accessInterceptor.AccessService()) - api.grpcGateway, err = server.CreateGateway(ctx, port, http1HostName, accessInterceptor, tlsConfig) + api.grpcServer = server.CreateServer(api.verifier, authZ, queries, externalDomain, tlsConfig, accessInterceptor.AccessService()) + api.grpcGateway, err = server.CreateGateway(ctx, port, hostHeaders, accessInterceptor, tlsConfig) if err != nil { return nil, err } @@ -112,9 +113,8 @@ func (a *API) RegisterServer(ctx context.Context, grpcServer server.WithGatewayP ctx, grpcServer, a.port, - a.http1HostName, + a.hostHeaders, a.accessInterceptor, - a.queries, tlsConfig, ) if err != nil { diff --git a/internal/api/assets/asset.go b/internal/api/assets/asset.go index 5f30c94a29..57ad3710bc 100644 --- a/internal/api/assets/asset.go +++ b/internal/api/assets/asset.go @@ -55,9 +55,9 @@ func (h *Handler) Storage() static.Storage { return h.storage } -func AssetAPI(externalSecure bool) func(context.Context) string { +func AssetAPI() func(context.Context) string { return func(ctx context.Context) string { - return http_util.BuildOrigin(authz.GetInstance(ctx).RequestedHost(), externalSecure) + HandlerPrefix + return http_util.DomainContext(ctx).Origin() + HandlerPrefix } } diff --git a/internal/api/authz/instance.go b/internal/api/authz/instance.go index 18767a3151..9610283a59 100644 --- a/internal/api/authz/instance.go +++ b/internal/api/authz/instance.go @@ -18,8 +18,6 @@ type Instance interface { ProjectID() string ConsoleClientID() string ConsoleApplicationID() string - RequestedDomain() string - RequestedHost() string DefaultLanguage() language.Tag DefaultOrganisationID() string SecurityPolicyAllowedOrigins() []string @@ -30,7 +28,7 @@ type Instance interface { } type InstanceVerifier interface { - InstanceByHost(ctx context.Context, host string) (Instance, error) + InstanceByHost(ctx context.Context, host, publicDomain string) (Instance, error) InstanceByID(ctx context.Context) (Instance, error) } @@ -68,14 +66,6 @@ func (i *instance) ConsoleApplicationID() string { return i.appID } -func (i *instance) RequestedDomain() string { - return i.domain -} - -func (i *instance) RequestedHost() string { - return i.domain -} - func (i *instance) DefaultLanguage() language.Tag { return language.Und } @@ -116,16 +106,6 @@ func WithInstanceID(ctx context.Context, id string) context.Context { return context.WithValue(ctx, instanceKey, &instance{id: id}) } -func WithRequestedDomain(ctx context.Context, domain string) context.Context { - i, ok := ctx.Value(instanceKey).(*instance) - if !ok { - i = new(instance) - } - - i.domain = domain - 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/api/authz/instance_test.go b/internal/api/authz/instance_test.go index c6cac09d4b..f71cbac5d1 100644 --- a/internal/api/authz/instance_test.go +++ b/internal/api/authz/instance_test.go @@ -118,14 +118,6 @@ func (m *mockInstance) DefaultOrganisationID() string { return "orgID" } -func (m *mockInstance) RequestedDomain() string { - return "zitadel.cloud" -} - -func (m *mockInstance) RequestedHost() string { - return "zitadel.cloud:443" -} - func (m *mockInstance) SecurityPolicyAllowedOrigins() []string { return nil } diff --git a/internal/api/grpc/admin/instance.go b/internal/api/grpc/admin/instance.go index f973c8a320..74f1576ae1 100644 --- a/internal/api/grpc/admin/instance.go +++ b/internal/api/grpc/admin/instance.go @@ -37,3 +37,42 @@ func (s *Server) ListInstanceDomains(ctx context.Context, req *admin_pb.ListInst ), }, nil } + +func (s *Server) ListInstanceTrustedDomains(ctx context.Context, req *admin_pb.ListInstanceTrustedDomainsRequest) (*admin_pb.ListInstanceTrustedDomainsResponse, error) { + queries, err := ListInstanceTrustedDomainsRequestToModel(req) + if err != nil { + return nil, err + } + domains, err := s.query.SearchInstanceTrustedDomains(ctx, queries) + if err != nil { + return nil, err + } + return &admin_pb.ListInstanceTrustedDomainsResponse{ + Result: instance_grpc.TrustedDomainsToPb(domains.Domains), + Details: object.ToListDetails( + domains.Count, + domains.Sequence, + domains.LastRun, + ), + }, nil +} + +func (s *Server) AddInstanceTrustedDomain(ctx context.Context, req *admin_pb.AddInstanceTrustedDomainRequest) (*admin_pb.AddInstanceTrustedDomainResponse, error) { + details, err := s.command.AddTrustedDomain(ctx, req.Domain) + if err != nil { + return nil, err + } + return &admin_pb.AddInstanceTrustedDomainResponse{ + Details: object.DomainToAddDetailsPb(details), + }, nil +} + +func (s *Server) RemoveInstanceTrustedDomain(ctx context.Context, req *admin_pb.RemoveInstanceTrustedDomainRequest) (*admin_pb.RemoveInstanceTrustedDomainResponse, error) { + details, err := s.command.RemoveTrustedDomain(ctx, req.Domain) + if err != nil { + return nil, err + } + return &admin_pb.RemoveInstanceTrustedDomainResponse{ + Details: object.DomainToChangeDetailsPb(details), + }, nil +} diff --git a/internal/api/grpc/admin/instance_converter.go b/internal/api/grpc/admin/instance_converter.go index 0443d8bb8e..397845c9e3 100644 --- a/internal/api/grpc/admin/instance_converter.go +++ b/internal/api/grpc/admin/instance_converter.go @@ -39,3 +39,20 @@ func fieldNameToInstanceDomainColumn(fieldName instance.DomainFieldName) query.C return query.Column{} } } + +func ListInstanceTrustedDomainsRequestToModel(req *admin_pb.ListInstanceTrustedDomainsRequest) (*query.InstanceTrustedDomainSearchQueries, error) { + offset, limit, asc := object.ListQueryToModel(req.Query) + queries, err := instance_grpc.TrustedDomainQueriesToModel(req.Queries) + if err != nil { + return nil, err + } + return &query.InstanceTrustedDomainSearchQueries{ + SearchRequest: query.SearchRequest{ + Offset: offset, + Limit: limit, + Asc: asc, + SortingColumn: fieldNameToInstanceDomainColumn(req.SortingColumn), + }, + Queries: queries, + }, nil +} diff --git a/internal/api/grpc/admin/org.go b/internal/api/grpc/admin/org.go index 7d3203970d..81064cebff 100644 --- a/internal/api/grpc/admin/org.go +++ b/internal/api/grpc/admin/org.go @@ -6,6 +6,7 @@ import ( "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/grpc/object" org_grpc "github.com/zitadel/zitadel/internal/api/grpc/org" + http_utils "github.com/zitadel/zitadel/internal/api/http" "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/query" @@ -69,7 +70,7 @@ func (s *Server) ListOrgs(ctx context.Context, req *admin_pb.ListOrgsRequest) (* } func (s *Server) SetUpOrg(ctx context.Context, req *admin_pb.SetUpOrgRequest) (*admin_pb.SetUpOrgResponse, error) { - orgDomain, err := domain.NewIAMDomainName(req.Org.Name, authz.GetInstance(ctx).RequestedDomain()) + orgDomain, err := domain.NewIAMDomainName(req.Org.Name, http_utils.DomainContext(ctx).RequestedDomain()) if err != nil { return nil, err } diff --git a/internal/api/grpc/admin/server.go b/internal/api/grpc/admin/server.go index 96bf208347..639d1a9df8 100644 --- a/internal/api/grpc/admin/server.go +++ b/internal/api/grpc/admin/server.go @@ -11,7 +11,6 @@ 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/query" "github.com/zitadel/zitadel/pkg/grpc/admin" @@ -41,8 +40,6 @@ func CreateServer( database string, command *command.Commands, query *query.Queries, - sd systemdefaults.SystemDefaults, - externalSecure bool, userCodeAlg crypto.EncryptionAlgorithm, auditLogRetention time.Duration, ) *Server { @@ -50,7 +47,7 @@ func CreateServer( database: database, command: command, query: query, - assetsAPIDomain: assets.AssetAPI(externalSecure), + assetsAPIDomain: assets.AssetAPI(), userCodeAlg: userCodeAlg, auditLogRetention: auditLogRetention, } diff --git a/internal/api/grpc/auth/passwordless.go b/internal/api/grpc/auth/passwordless.go index b7b914d2c2..ddc0a331c4 100644 --- a/internal/api/grpc/auth/passwordless.go +++ b/internal/api/grpc/auth/passwordless.go @@ -67,10 +67,9 @@ func (s *Server) AddMyPasswordlessLink(ctx context.Context, _ *auth_pb.AddMyPass if err != nil { return nil, err } - origin := http.BuildOrigin(authz.GetInstance(ctx).RequestedHost(), s.externalSecure) return &auth_pb.AddMyPasswordlessLinkResponse{ Details: object.AddToDetailsPb(initCode.Sequence, initCode.ChangeDate, initCode.ResourceOwner), - Link: initCode.Link(origin + login.HandlerPrefix + login.EndpointPasswordlessRegistration), + Link: initCode.Link(http.DomainContext(ctx).Origin() + login.HandlerPrefix + login.EndpointPasswordlessRegistration), Expiration: durationpb.New(initCode.Expiration), }, nil } diff --git a/internal/api/grpc/auth/server.go b/internal/api/grpc/auth/server.go index 44a6172768..3200acffc0 100644 --- a/internal/api/grpc/auth/server.go +++ b/internal/api/grpc/auth/server.go @@ -31,7 +31,6 @@ type Server struct { defaults systemdefaults.SystemDefaults assetsAPIDomain func(context.Context) string userCodeAlg crypto.EncryptionAlgorithm - externalSecure bool } type Config struct { @@ -43,16 +42,14 @@ func CreateServer(command *command.Commands, authRepo repository.Repository, defaults systemdefaults.SystemDefaults, userCodeAlg crypto.EncryptionAlgorithm, - externalSecure bool, ) *Server { return &Server{ command: command, query: query, repo: authRepo, defaults: defaults, - assetsAPIDomain: assets.AssetAPI(externalSecure), + assetsAPIDomain: assets.AssetAPI(), userCodeAlg: userCodeAlg, - externalSecure: externalSecure, } } diff --git a/internal/api/grpc/instance/converter.go b/internal/api/grpc/instance/converter.go index 9bad4a3eff..4094da4a77 100644 --- a/internal/api/grpc/instance/converter.go +++ b/internal/api/grpc/instance/converter.go @@ -115,3 +115,43 @@ func DomainToPb(d *query.InstanceDomain) *instance_pb.Domain { ), } } + +func TrustedDomainQueriesToModel(queries []*instance_pb.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_pb.TrustedDomainSearchQuery) (query.SearchQuery, error) { + switch q := searchQuery.Query.(type) { + case *instance_pb.TrustedDomainSearchQuery_DomainQuery: + return query.NewInstanceTrustedDomainDomainSearchQuery(object.TextMethodToQuery(q.DomainQuery.Method), q.DomainQuery.Domain) + default: + return nil, zerrors.ThrowInvalidArgument(nil, "INST-Ags42", "List.Query.Invalid") + } +} + +func TrustedDomainsToPb(domains []*query.InstanceTrustedDomain) []*instance_pb.TrustedDomain { + d := make([]*instance_pb.TrustedDomain, len(domains)) + for i, domain := range domains { + d[i] = TrustedDomainToPb(domain) + } + return d +} + +func TrustedDomainToPb(d *query.InstanceTrustedDomain) *instance_pb.TrustedDomain { + return &instance_pb.TrustedDomain{ + Domain: d.Domain, + Details: object.ToViewDetailsPb( + d.Sequence, + d.CreationDate, + d.ChangeDate, + d.InstanceID, + ), + } +} diff --git a/internal/api/grpc/management/information.go b/internal/api/grpc/management/information.go index d18e115bfa..8b769f6820 100644 --- a/internal/api/grpc/management/information.go +++ b/internal/api/grpc/management/information.go @@ -5,7 +5,6 @@ import ( "github.com/zitadel/oidc/v3/pkg/oidc" - "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/http" mgmt_pb "github.com/zitadel/zitadel/pkg/grpc/management" ) @@ -15,7 +14,7 @@ func (s *Server) Healthz(context.Context, *mgmt_pb.HealthzRequest) (*mgmt_pb.Hea } func (s *Server) GetOIDCInformation(ctx context.Context, _ *mgmt_pb.GetOIDCInformationRequest) (*mgmt_pb.GetOIDCInformationResponse, error) { - issuer := http.BuildOrigin(authz.GetInstance(ctx).RequestedHost(), s.externalSecure) + issuer := http.DomainContext(ctx).Origin() return &mgmt_pb.GetOIDCInformationResponse{ Issuer: issuer, DiscoveryEndpoint: issuer + oidc.DiscoveryEndpoint, diff --git a/internal/api/grpc/management/org.go b/internal/api/grpc/management/org.go index aed1394d65..91ef8e3b84 100644 --- a/internal/api/grpc/management/org.go +++ b/internal/api/grpc/management/org.go @@ -11,6 +11,7 @@ import ( obj_grpc "github.com/zitadel/zitadel/internal/api/grpc/object" org_grpc "github.com/zitadel/zitadel/internal/api/grpc/org" policy_grpc "github.com/zitadel/zitadel/internal/api/grpc/policy" + http_utils "github.com/zitadel/zitadel/internal/api/http" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/eventstore/v1/models" @@ -73,7 +74,7 @@ func (s *Server) ListOrgChanges(ctx context.Context, req *mgmt_pb.ListOrgChanges } func (s *Server) AddOrg(ctx context.Context, req *mgmt_pb.AddOrgRequest) (*mgmt_pb.AddOrgResponse, error) { - orgDomain, err := domain.NewIAMDomainName(req.Name, authz.GetInstance(ctx).RequestedDomain()) + orgDomain, err := domain.NewIAMDomainName(req.Name, http_utils.DomainContext(ctx).RequestedDomain()) if err != nil { return nil, err } diff --git a/internal/api/grpc/management/server.go b/internal/api/grpc/management/server.go index cdefabeab9..06a3381fa1 100644 --- a/internal/api/grpc/management/server.go +++ b/internal/api/grpc/management/server.go @@ -28,7 +28,6 @@ type Server struct { systemDefaults systemdefaults.SystemDefaults assetAPIPrefix func(context.Context) string userCodeAlg crypto.EncryptionAlgorithm - externalSecure bool } func CreateServer( @@ -36,15 +35,13 @@ func CreateServer( query *query.Queries, sd systemdefaults.SystemDefaults, userCodeAlg crypto.EncryptionAlgorithm, - externalSecure bool, ) *Server { return &Server{ command: command, query: query, systemDefaults: sd, - assetAPIPrefix: assets.AssetAPI(externalSecure), + assetAPIPrefix: assets.AssetAPI(), userCodeAlg: userCodeAlg, - externalSecure: externalSecure, } } diff --git a/internal/api/grpc/management/user.go b/internal/api/grpc/management/user.go index d3cab62961..64d99ea786 100644 --- a/internal/api/grpc/management/user.go +++ b/internal/api/grpc/management/user.go @@ -286,9 +286,8 @@ func (s *Server) ImportHumanUser(ctx context.Context, req *mgmt_pb.ImportHumanUs ), } if code != nil { - origin := http.BuildOrigin(authz.GetInstance(ctx).RequestedHost(), s.externalSecure) resp.PasswordlessRegistration = &mgmt_pb.ImportHumanUserResponse_PasswordlessRegistration{ - Link: code.Link(origin + login.HandlerPrefix + login.EndpointPasswordlessRegistration), + Link: code.Link(http.DomainContext(ctx).Origin() + login.HandlerPrefix + login.EndpointPasswordlessRegistration), Lifetime: durationpb.New(code.Expiration), Expiration: durationpb.New(code.Expiration), } @@ -691,10 +690,9 @@ func (s *Server) AddPasswordlessRegistration(ctx context.Context, req *mgmt_pb.A if err != nil { return nil, err } - origin := http.BuildOrigin(authz.GetInstance(ctx).RequestedHost(), s.externalSecure) return &mgmt_pb.AddPasswordlessRegistrationResponse{ Details: obj_grpc.AddToDetailsPb(initCode.Sequence, initCode.ChangeDate, initCode.ResourceOwner), - Link: initCode.Link(origin + login.HandlerPrefix + login.EndpointPasswordlessRegistration), + Link: initCode.Link(http.DomainContext(ctx).Origin() + login.HandlerPrefix + login.EndpointPasswordlessRegistration), Expiration: durationpb.New(initCode.Expiration), }, nil } diff --git a/internal/api/grpc/oidc/v2/oidc.go b/internal/api/grpc/oidc/v2/oidc.go index d84edd1c2f..826c198fad 100644 --- a/internal/api/grpc/oidc/v2/oidc.go +++ b/internal/api/grpc/oidc/v2/oidc.go @@ -8,7 +8,6 @@ import ( "google.golang.org/protobuf/types/known/durationpb" "google.golang.org/protobuf/types/known/timestamppb" - "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/grpc/object/v2" "github.com/zitadel/zitadel/internal/api/http" "github.com/zitadel/zitadel/internal/api/oidc" @@ -107,7 +106,7 @@ func (s *Server) linkSessionToAuthRequest(ctx context.Context, authRequestID str return nil, err } authReq := &oidc.AuthRequestV2{CurrentAuthRequest: aar} - ctx = op.ContextWithIssuer(ctx, http.BuildOrigin(authz.GetInstance(ctx).RequestedHost(), s.externalSecure)) + ctx = op.ContextWithIssuer(ctx, http.DomainContext(ctx).Origin()) var callback string if aar.ResponseType == domain.OIDCResponseTypeCode { callback, err = oidc.CreateCodeCallbackURL(ctx, authReq, s.op.Provider()) diff --git a/internal/api/grpc/oidc/v2beta/oidc.go b/internal/api/grpc/oidc/v2beta/oidc.go index d504e411f0..04ffdbb348 100644 --- a/internal/api/grpc/oidc/v2beta/oidc.go +++ b/internal/api/grpc/oidc/v2beta/oidc.go @@ -8,7 +8,6 @@ import ( "google.golang.org/protobuf/types/known/durationpb" "google.golang.org/protobuf/types/known/timestamppb" - "github.com/zitadel/zitadel/internal/api/authz" object "github.com/zitadel/zitadel/internal/api/grpc/object/v2beta" "github.com/zitadel/zitadel/internal/api/http" "github.com/zitadel/zitadel/internal/api/oidc" @@ -107,7 +106,7 @@ func (s *Server) linkSessionToAuthRequest(ctx context.Context, authRequestID str return nil, err } authReq := &oidc.AuthRequestV2{CurrentAuthRequest: aar} - ctx = op.ContextWithIssuer(ctx, http.BuildOrigin(authz.GetInstance(ctx).RequestedHost(), s.externalSecure)) + ctx = op.ContextWithIssuer(ctx, http.DomainContext(ctx).Origin()) var callback string if aar.ResponseType == domain.OIDCResponseTypeCode { callback, err = oidc.CreateCodeCallbackURL(ctx, authReq, s.op.Provider()) diff --git a/internal/api/grpc/server/gateway.go b/internal/api/grpc/server/gateway.go index 25b4c6996a..3e41be94ae 100644 --- a/internal/api/grpc/server/gateway.go +++ b/internal/api/grpc/server/gateway.go @@ -5,6 +5,7 @@ import ( "crypto/tls" "fmt" "net/http" + "slices" "strings" "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" @@ -19,10 +20,8 @@ import ( "google.golang.org/protobuf/proto" client_middleware "github.com/zitadel/zitadel/internal/api/grpc/client/middleware" - "github.com/zitadel/zitadel/internal/api/grpc/server/middleware" http_utils "github.com/zitadel/zitadel/internal/api/http" http_mw "github.com/zitadel/zitadel/internal/api/http/middleware" - "github.com/zitadel/zitadel/internal/query" "github.com/zitadel/zitadel/internal/telemetry/metrics" ) @@ -57,26 +56,29 @@ var ( }, ) - serveMuxOptions = []runtime.ServeMuxOption{ - runtime.WithMarshalerOption(jsonMarshaler.ContentType(nil), jsonMarshaler), - runtime.WithMarshalerOption(mimeWildcard, jsonMarshaler), - runtime.WithMarshalerOption(runtime.MIMEWildcard, jsonMarshaler), - runtime.WithIncomingHeaderMatcher(headerMatcher), - runtime.WithOutgoingHeaderMatcher(runtime.DefaultHeaderMatcher), - runtime.WithForwardResponseOption(responseForwarder), - runtime.WithRoutingErrorHandler(httpErrorHandler), + serveMuxOptions = func(hostHeaders []string) []runtime.ServeMuxOption { + return []runtime.ServeMuxOption{ + runtime.WithMarshalerOption(jsonMarshaler.ContentType(nil), jsonMarshaler), + runtime.WithMarshalerOption(mimeWildcard, jsonMarshaler), + runtime.WithMarshalerOption(runtime.MIMEWildcard, jsonMarshaler), + runtime.WithIncomingHeaderMatcher(headerMatcher(hostHeaders)), + runtime.WithOutgoingHeaderMatcher(runtime.DefaultHeaderMatcher), + runtime.WithForwardResponseOption(responseForwarder), + runtime.WithRoutingErrorHandler(httpErrorHandler), + } } - headerMatcher = runtime.HeaderMatcherFunc( - func(header string) (string, bool) { + headerMatcher = func(hostHeaders []string) runtime.HeaderMatcherFunc { + customHeaders = slices.Compact(append(customHeaders, hostHeaders...)) + return func(header string) (string, bool) { for _, customHeader := range customHeaders { if strings.HasPrefix(strings.ToLower(header), customHeader) { return header, true } } return runtime.DefaultHeaderMatcher(header) - }, - ) + } + } responseForwarder = func(ctx context.Context, w http.ResponseWriter, resp proto.Message) error { t, ok := resp.(CustomHTTPResponse) @@ -90,14 +92,12 @@ var ( type Gateway struct { mux *runtime.ServeMux - http1HostName string connection *grpc.ClientConn accessInterceptor *http_mw.AccessInterceptor - queries *query.Queries } func (g *Gateway) Handler() http.Handler { - return addInterceptors(g.mux, g.http1HostName, g.accessInterceptor, g.queries) + return addInterceptors(g.mux, g.accessInterceptor) } type CustomHTTPResponse interface { @@ -110,12 +110,11 @@ func CreateGatewayWithPrefix( ctx context.Context, g WithGatewayPrefix, port uint16, - http1HostName string, + hostHeaders []string, accessInterceptor *http_mw.AccessInterceptor, - queries *query.Queries, tlsConfig *tls.Config, ) (http.Handler, string, error) { - runtimeMux := runtime.NewServeMux(serveMuxOptions...) + runtimeMux := runtime.NewServeMux(serveMuxOptions(hostHeaders)...) opts := []grpc.DialOption{ grpc.WithTransportCredentials(grpcCredentials(tlsConfig)), grpc.WithChainUnaryInterceptor( @@ -131,13 +130,13 @@ func CreateGatewayWithPrefix( if err != nil { return nil, "", fmt.Errorf("failed to register grpc gateway: %w", err) } - return addInterceptors(runtimeMux, http1HostName, accessInterceptor, queries), g.GatewayPathPrefix(), nil + return addInterceptors(runtimeMux, accessInterceptor), g.GatewayPathPrefix(), nil } func CreateGateway( ctx context.Context, port uint16, - http1HostName string, + hostHeaders []string, accessInterceptor *http_mw.AccessInterceptor, tlsConfig *tls.Config, ) (*Gateway, error) { @@ -153,10 +152,9 @@ func CreateGateway( if err != nil { return nil, err } - runtimeMux := runtime.NewServeMux(append(serveMuxOptions, runtime.WithHealthzEndpoint(healthpb.NewHealthClient(connection)))...) + runtimeMux := runtime.NewServeMux(append(serveMuxOptions(hostHeaders), runtime.WithHealthzEndpoint(healthpb.NewHealthClient(connection)))...) return &Gateway{ mux: runtimeMux, - http1HostName: http1HostName, connection: connection, accessInterceptor: accessInterceptor, }, nil @@ -195,12 +193,9 @@ func dial(ctx context.Context, port uint16, opts []grpc.DialOption) (*grpc.Clien func addInterceptors( handler http.Handler, - http1HostName string, accessInterceptor *http_mw.AccessInterceptor, - queries *query.Queries, ) http.Handler { handler = http_mw.CallDurationHandler(handler) - handler = http1Host(handler, http1HostName) handler = http_mw.CORSInterceptor(handler) handler = http_mw.RobotsTagHandler(handler) handler = http_mw.DefaultTelemetryHandler(handler) @@ -215,18 +210,6 @@ func addInterceptors( return handler } -func http1Host(next http.Handler, http1HostName string) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - host, err := http_mw.HostFromRequest(r, http1HostName) - if err != nil { - http.Error(w, err.Error(), http.StatusForbidden) - return - } - r.Header.Set(middleware.HTTP1Host, host) - next.ServeHTTP(w, r) - }) -} - func exhaustedCookieInterceptor( next http.Handler, accessInterceptor *http_mw.AccessInterceptor, diff --git a/internal/api/grpc/server/middleware/access_interceptor.go b/internal/api/grpc/server/middleware/access_interceptor.go index 719e03da78..100264c3f5 100644 --- a/internal/api/grpc/server/middleware/access_interceptor.go +++ b/internal/api/grpc/server/middleware/access_interceptor.go @@ -9,6 +9,7 @@ import ( "google.golang.org/grpc/status" "github.com/zitadel/zitadel/internal/api/authz" + http_util "github.com/zitadel/zitadel/internal/api/http" "github.com/zitadel/zitadel/internal/logstore" "github.com/zitadel/zitadel/internal/logstore/record" "github.com/zitadel/zitadel/internal/telemetry/tracing" @@ -35,6 +36,7 @@ func AccessStorageInterceptor(svc *logstore.Service[*record.AccessLog]) grpc.Una resMd, _ := metadata.FromOutgoingContext(ctx) instance := authz.GetInstance(ctx) + domainCtx := http_util.DomainContext(ctx) r := &record.AccessLog{ LogDate: time.Now(), @@ -45,8 +47,8 @@ func AccessStorageInterceptor(svc *logstore.Service[*record.AccessLog]) grpc.Una ResponseHeaders: resMd, InstanceID: instance.InstanceID(), ProjectID: instance.ProjectID(), - RequestedDomain: instance.RequestedDomain(), - RequestedHost: instance.RequestedHost(), + RequestedDomain: domainCtx.RequestedDomain(), + RequestedHost: domainCtx.RequestedHost(), } svc.Handle(interceptorCtx, r) diff --git a/internal/api/grpc/server/middleware/instance_interceptor.go b/internal/api/grpc/server/middleware/instance_interceptor.go index ba637d59fa..07ae1ba277 100644 --- a/internal/api/grpc/server/middleware/instance_interceptor.go +++ b/internal/api/grpc/server/middleware/instance_interceptor.go @@ -10,7 +10,6 @@ import ( "golang.org/x/text/language" "google.golang.org/grpc" "google.golang.org/grpc/codes" - "google.golang.org/grpc/metadata" "google.golang.org/grpc/status" "github.com/zitadel/zitadel/internal/api/authz" @@ -24,15 +23,15 @@ const ( HTTP1Host = "x-zitadel-http1-host" ) -func InstanceInterceptor(verifier authz.InstanceVerifier, headerName, externalDomain string, explicitInstanceIdServices ...string) grpc.UnaryServerInterceptor { +func InstanceInterceptor(verifier authz.InstanceVerifier, externalDomain string, explicitInstanceIdServices ...string) grpc.UnaryServerInterceptor { translator, err := i18n.NewZitadelTranslator(language.English) logging.OnError(err).Panic("unable to get translator") return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { - return setInstance(ctx, req, info, handler, verifier, headerName, externalDomain, translator, explicitInstanceIdServices...) + return setInstance(ctx, req, info, handler, verifier, externalDomain, translator, explicitInstanceIdServices...) } } -func setInstance(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler, verifier authz.InstanceVerifier, headerName, externalDomain string, translator *i18n.Translator, idFromRequestsServices ...string) (_ interface{}, err error) { +func setInstance(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler, verifier authz.InstanceVerifier, externalDomain string, translator *i18n.Translator, idFromRequestsServices ...string) (_ interface{}, err error) { interceptorCtx, span := tracing.NewServerInterceptorSpan(ctx) defer func() { span.EndWithError(err) }() for _, service := range idFromRequestsServices { @@ -56,14 +55,15 @@ func setInstance(ctx context.Context, req interface{}, info *grpc.UnaryServerInf return handler(authz.WithInstance(ctx, instance), req) } } - host, err := hostFromContext(interceptorCtx, headerName) - if err != nil { - return nil, status.Error(codes.NotFound, err.Error()) + requestContext := zitadel_http.DomainContext(ctx) + if requestContext.InstanceHost == "" { + logging.WithFields("origin", requestContext.Origin(), "externalDomain", externalDomain).WithError(err).Error("unable to set instance") + return nil, status.Error(codes.NotFound, "no instanceHost specified") } - instance, err := verifier.InstanceByHost(interceptorCtx, host) + instance, err := verifier.InstanceByHost(interceptorCtx, requestContext.InstanceHost, requestContext.PublicHost) if err != nil { - origin := zitadel_http.ComposedOrigin(ctx) - logging.WithFields("origin", origin, "externalDomain", externalDomain).WithError(err).Error("unable to set instance") + origin := zitadel_http.DomainContext(ctx) + logging.WithFields("origin", requestContext.Origin(), "externalDomain", externalDomain).WithError(err).Error("unable to set instance") zErr := new(zerrors.ZitadelError) if errors.As(err, &zErr) { zErr.SetMessage(translator.LocalizeFromCtx(ctx, zErr.GetMessage(), nil)) @@ -75,33 +75,3 @@ func setInstance(ctx context.Context, req interface{}, info *grpc.UnaryServerInf span.End() return handler(authz.WithInstance(ctx, instance), req) } - -func hostFromContext(ctx context.Context, headerName string) (string, error) { - md, ok := metadata.FromIncomingContext(ctx) - if !ok { - return "", fmt.Errorf("cannot read metadata") - } - host, ok := md[HTTP1Host] - if ok && len(host) == 1 { - if !isAllowedToSendHTTP1Header(md) { - return "", fmt.Errorf("no valid host header") - } - return host[0], nil - } - host, ok = md[headerName] - if !ok { - return "", fmt.Errorf("cannot find header: %v", headerName) - } - if len(host) != 1 { - return "", fmt.Errorf("invalid host header: %v", host) - } - return host[0], nil -} - -// isAllowedToSendHTTP1Header check if the gRPC call was sent to `localhost` -// this is only possible when calling the server directly running on localhost -// or through the gRPC gateway -func isAllowedToSendHTTP1Header(md metadata.MD) bool { - authority, ok := md[":authority"] - return ok && len(authority) == 1 && strings.Split(authority[0], ":")[0] == "localhost" -} diff --git a/internal/api/grpc/server/middleware/instance_interceptor_test.go b/internal/api/grpc/server/middleware/instance_interceptor_test.go index 132ced4e61..211444a707 100644 --- a/internal/api/grpc/server/middleware/instance_interceptor_test.go +++ b/internal/api/grpc/server/middleware/instance_interceptor_test.go @@ -9,82 +9,19 @@ import ( "golang.org/x/text/language" "google.golang.org/grpc" - "google.golang.org/grpc/metadata" "github.com/zitadel/zitadel/internal/api/authz" + http_util "github.com/zitadel/zitadel/internal/api/http" "github.com/zitadel/zitadel/internal/feature" ) -func Test_hostNameFromContext(t *testing.T) { - type args struct { - ctx context.Context - headerName string - } - type res struct { - want string - err bool - } - tests := []struct { - name string - args args - res res - }{ - { - "empty context, error", - args{ - ctx: context.Background(), - headerName: "header", - }, - res{ - want: "", - err: true, - }, - }, - { - "header not found", - args{ - ctx: metadata.NewIncomingContext(context.Background(), nil), - headerName: "header", - }, - res{ - want: "", - err: true, - }, - }, - { - "header not found", - args{ - ctx: metadata.NewIncomingContext(context.Background(), metadata.Pairs("header", "value")), - headerName: "header", - }, - res{ - want: "value", - err: false, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := hostFromContext(tt.args.ctx, tt.args.headerName) - if (err != nil) != tt.res.err { - t.Errorf("hostFromContext() error = %v, wantErr %v", err, tt.res.err) - return - } - if got != tt.res.want { - t.Errorf("hostFromContext() got = %v, want %v", got, tt.res.want) - } - }) - } -} - func Test_setInstance(t *testing.T) { type args struct { - ctx context.Context - req interface{} - info *grpc.UnaryServerInfo - handler grpc.UnaryHandler - verifier authz.InstanceVerifier - headerName string + ctx context.Context + req interface{} + info *grpc.UnaryServerInfo + handler grpc.UnaryHandler + verifier authz.InstanceVerifier } type res struct { want interface{} @@ -108,10 +45,9 @@ func Test_setInstance(t *testing.T) { { "invalid host, error", args{ - ctx: metadata.NewIncomingContext(context.Background(), metadata.Pairs("header", "host2")), - req: &mockRequest{}, - verifier: &mockInstanceVerifier{"host"}, - headerName: "header", + ctx: http_util.WithDomainContext(context.Background(), &http_util.DomainCtx{InstanceHost: "host2"}), + req: &mockRequest{}, + verifier: &mockInstanceVerifier{instanceHost: "host"}, }, res{ want: nil, @@ -121,10 +57,9 @@ func Test_setInstance(t *testing.T) { { "valid host", args{ - ctx: metadata.NewIncomingContext(context.Background(), metadata.Pairs("header", "host")), - req: &mockRequest{}, - verifier: &mockInstanceVerifier{"host"}, - headerName: "header", + ctx: http_util.WithDomainContext(context.Background(), &http_util.DomainCtx{InstanceHost: "host"}), + req: &mockRequest{}, + verifier: &mockInstanceVerifier{instanceHost: "host"}, handler: func(ctx context.Context, req interface{}) (interface{}, error) { return req, nil }, @@ -137,7 +72,7 @@ func Test_setInstance(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := setInstance(tt.args.ctx, tt.args.req, tt.args.info, tt.args.handler, tt.args.verifier, tt.args.headerName, "", nil) + got, err := setInstance(tt.args.ctx, tt.args.req, tt.args.info, tt.args.handler, tt.args.verifier, "", nil) if (err != nil) != tt.res.err { t.Errorf("setInstance() error = %v, wantErr %v", err, tt.res.err) return @@ -152,11 +87,18 @@ func Test_setInstance(t *testing.T) { type mockRequest struct{} type mockInstanceVerifier struct { - host string + instanceHost string + publicHost string } -func (m *mockInstanceVerifier) InstanceByHost(_ context.Context, host string) (authz.Instance, error) { - if host != m.host { +func (m *mockInstanceVerifier) InstanceByHost(_ context.Context, instanceHost, publicHost string) (authz.Instance, error) { + if instanceHost != m.instanceHost { + return nil, fmt.Errorf("invalid host") + } + if publicHost == "" { + return &mockInstance{}, nil + } + if publicHost != instanceHost && publicHost != m.publicHost { return nil, fmt.Errorf("invalid host") } return &mockInstance{}, nil @@ -198,14 +140,6 @@ func (m *mockInstance) DefaultOrganisationID() string { return "orgID" } -func (m *mockInstance) RequestedDomain() string { - return "localhost" -} - -func (m *mockInstance) RequestedHost() string { - return "localhost:8080" -} - func (m *mockInstance) SecurityPolicyAllowedOrigins() []string { return nil } diff --git a/internal/api/grpc/server/server.go b/internal/api/grpc/server/server.go index ef4c271bf5..5408ae257f 100644 --- a/internal/api/grpc/server/server.go +++ b/internal/api/grpc/server/server.go @@ -38,7 +38,6 @@ func CreateServer( verifier authz.APITokenVerifier, authConfig authz.Config, queries *query.Queries, - hostHeaderName string, externalDomain string, tlsConfig *tls.Config, accessSvc *logstore.Service[*record.AccessLog], @@ -51,7 +50,7 @@ func CreateServer( middleware.DefaultTracingServer(), middleware.MetricsHandler(metricTypes, grpc_api.Probes...), middleware.NoCacheInterceptor(), - middleware.InstanceInterceptor(queries, hostHeaderName, externalDomain, system_pb.SystemService_ServiceDesc.ServiceName, healthpb.Health_ServiceDesc.ServiceName), + middleware.InstanceInterceptor(queries, externalDomain, system_pb.SystemService_ServiceDesc.ServiceName, healthpb.Health_ServiceDesc.ServiceName), middleware.AccessStorageInterceptor(accessSvc), middleware.ErrorHandler(), middleware.LimitsInterceptor(system_pb.SystemService_ServiceDesc.ServiceName), diff --git a/internal/api/grpc/settings/v2/server.go b/internal/api/grpc/settings/v2/server.go index 0391d01188..9cae50824f 100644 --- a/internal/api/grpc/settings/v2/server.go +++ b/internal/api/grpc/settings/v2/server.go @@ -27,12 +27,11 @@ type Config struct{} func CreateServer( command *command.Commands, query *query.Queries, - externalSecure bool, ) *Server { return &Server{ command: command, query: query, - assetsAPIDomain: assets.AssetAPI(externalSecure), + assetsAPIDomain: assets.AssetAPI(), } } diff --git a/internal/api/grpc/settings/v2beta/server.go b/internal/api/grpc/settings/v2beta/server.go index f001549595..24c8f7774a 100644 --- a/internal/api/grpc/settings/v2beta/server.go +++ b/internal/api/grpc/settings/v2beta/server.go @@ -27,12 +27,11 @@ type Config struct{} func CreateServer( command *command.Commands, query *query.Queries, - externalSecure bool, ) *Server { return &Server{ command: command, query: query, - assetsAPIDomain: assets.AssetAPI(externalSecure), + assetsAPIDomain: assets.AssetAPI(), } } diff --git a/internal/api/http/header.go b/internal/api/http/header.go index 14ae3dfb68..91726c4338 100644 --- a/internal/api/http/header.go +++ b/internal/api/http/header.go @@ -20,6 +20,9 @@ const ( Pragma = "pragma" UserAgentHeader = "user-agent" ForwardedFor = "x-forwarded-for" + ForwardedHost = "x-forwarded-host" + ForwardedProto = "x-forwarded-proto" + Forwarded = "forwarded" XUserAgent = "x-user-agent" XGrpcWeb = "x-grpc-web" XRequestedWith = "x-requested-with" @@ -45,7 +48,7 @@ type key int const ( httpHeaders key = iota remoteAddr - origin + domainCtx ) func CopyHeadersToContext(h http.Handler) http.Handler { @@ -70,18 +73,6 @@ func OriginHeader(ctx context.Context) string { return headers.Get(Origin) } -func ComposedOrigin(ctx context.Context) string { - o, ok := ctx.Value(origin).(string) - if !ok { - return "" - } - return o -} - -func WithComposedOrigin(ctx context.Context, composed string) context.Context { - return context.WithValue(ctx, origin, composed) -} - func RemoteIPFromCtx(ctx context.Context) string { ctxHeaders, ok := HeadersFromCtx(ctx) if !ok { diff --git a/internal/api/http/middleware/access_interceptor.go b/internal/api/http/middleware/access_interceptor.go index f1dd0182f0..f1a935d95c 100644 --- a/internal/api/http/middleware/access_interceptor.go +++ b/internal/api/http/middleware/access_interceptor.go @@ -163,6 +163,7 @@ func (a *AccessInterceptor) writeLog(ctx context.Context, wrappedWriter *statusR logging.WithError(err).WithField("url", requestURL).Warning("failed to unescape request url") } instance := authz.GetInstance(ctx) + domainCtx := http_utils.DomainContext(ctx) a.logstoreSvc.Handle(ctx, &record.AccessLog{ LogDate: time.Now(), Protocol: record.HTTP, @@ -172,8 +173,8 @@ func (a *AccessInterceptor) writeLog(ctx context.Context, wrappedWriter *statusR ResponseHeaders: writer.Header(), InstanceID: instance.InstanceID(), ProjectID: instance.ProjectID(), - RequestedDomain: instance.RequestedDomain(), - RequestedHost: instance.RequestedHost(), + RequestedDomain: domainCtx.RequestedDomain(), + RequestedHost: domainCtx.RequestedHost(), NotCountable: notCountable, }) } diff --git a/internal/api/http/middleware/instance_interceptor.go b/internal/api/http/middleware/instance_interceptor.go index 2117b98d30..facb2ceec0 100644 --- a/internal/api/http/middleware/instance_interceptor.go +++ b/internal/api/http/middleware/instance_interceptor.go @@ -5,7 +5,6 @@ import ( "errors" "fmt" "net/http" - "net/url" "strings" "github.com/zitadel/logging" @@ -19,16 +18,15 @@ import ( ) type instanceInterceptor struct { - verifier authz.InstanceVerifier - headerName, externalDomain string - ignoredPrefixes []string - translator *i18n.Translator + verifier authz.InstanceVerifier + externalDomain string + ignoredPrefixes []string + translator *i18n.Translator } -func InstanceInterceptor(verifier authz.InstanceVerifier, headerName, externalDomain string, ignoredPrefixes ...string) *instanceInterceptor { +func InstanceInterceptor(verifier authz.InstanceVerifier, externalDomain string, ignoredPrefixes ...string) *instanceInterceptor { return &instanceInterceptor{ verifier: verifier, - headerName: headerName, externalDomain: externalDomain, ignoredPrefixes: ignoredPrefixes, translator: newZitadelTranslator(), @@ -54,10 +52,10 @@ func (a *instanceInterceptor) handleInstance(w http.ResponseWriter, r *http.Requ return } } - ctx, err := setInstance(r, a.verifier, a.headerName) + ctx, err := setInstance(r, a.verifier) if err != nil { - origin := zitadel_http.ComposedOrigin(r.Context()) - logging.WithFields("origin", origin, "externalDomain", a.externalDomain).WithError(err).Error("unable to set instance") + origin := zitadel_http.DomainContext(r.Context()) + logging.WithFields("origin", origin.Origin(), "externalDomain", a.externalDomain).WithError(err).Error("unable to set instance") zErr := new(zerrors.ZitadelError) if errors.As(err, &zErr) { zErr.SetMessage(a.translator.LocalizeFromRequest(r, zErr.GetMessage(), nil)) @@ -71,18 +69,16 @@ func (a *instanceInterceptor) handleInstance(w http.ResponseWriter, r *http.Requ next.ServeHTTP(w, r) } -func setInstance(r *http.Request, verifier authz.InstanceVerifier, headerName string) (_ context.Context, err error) { +func setInstance(r *http.Request, verifier authz.InstanceVerifier) (_ context.Context, err error) { ctx := r.Context() authCtx, span := tracing.NewServerInterceptorSpan(ctx) defer func() { span.EndWithError(err) }() - host, err := HostFromRequest(r, headerName) - - if err != nil { + requestContext := zitadel_http.DomainContext(ctx) + if requestContext.InstanceHost == "" { return nil, zerrors.ThrowNotFound(err, "INST-zWq7X", "Errors.IAM.NotFound") } - - instance, err := verifier.InstanceByHost(authCtx, host) + instance, err := verifier.InstanceByHost(authCtx, requestContext.InstanceHost, requestContext.PublicHost) if err != nil { return nil, err } @@ -90,39 +86,6 @@ func setInstance(r *http.Request, verifier authz.InstanceVerifier, headerName st return authz.WithInstance(ctx, instance), nil } -func HostFromRequest(r *http.Request, headerName string) (host string, err error) { - if headerName != "host" { - return hostFromSpecialHeader(r, headerName) - } - return hostFromOrigin(r.Context()) -} - -func hostFromSpecialHeader(r *http.Request, headerName string) (host string, err error) { - host = r.Header.Get(headerName) - if host == "" { - return "", fmt.Errorf("host header `%s` not found", headerName) - } - return host, nil -} - -func hostFromOrigin(ctx context.Context) (host string, err error) { - defer func() { - if err != nil { - err = fmt.Errorf("invalid origin: %w", err) - } - }() - origin := zitadel_http.ComposedOrigin(ctx) - u, err := url.Parse(origin) - if err != nil { - return "", err - } - host = u.Host - if host == "" { - err = errors.New("empty host") - } - return host, err -} - func newZitadelTranslator() *i18n.Translator { translator, err := i18n.NewZitadelTranslator(language.English) logging.OnError(err).Panic("unable to get translator") diff --git a/internal/api/http/middleware/instance_interceptor_test.go b/internal/api/http/middleware/instance_interceptor_test.go index 4bb0276913..7a2d7aeb26 100644 --- a/internal/api/http/middleware/instance_interceptor_test.go +++ b/internal/api/http/middleware/instance_interceptor_test.go @@ -19,8 +19,7 @@ import ( func Test_instanceInterceptor_Handler(t *testing.T) { type fields struct { - verifier authz.InstanceVerifier - headerName string + verifier authz.InstanceVerifier } type args struct { request *http.Request @@ -38,8 +37,7 @@ func Test_instanceInterceptor_Handler(t *testing.T) { { "setInstance error", fields{ - verifier: &mockInstanceVerifier{}, - headerName: "header", + verifier: &mockInstanceVerifier{}, }, args{ request: httptest.NewRequest("", "/url", nil), @@ -52,19 +50,18 @@ func Test_instanceInterceptor_Handler(t *testing.T) { { "setInstance ok", fields{ - verifier: &mockInstanceVerifier{"host"}, - headerName: "header", + verifier: &mockInstanceVerifier{instanceHost: "host"}, }, args{ request: func() *http.Request { r := httptest.NewRequest("", "/url", nil) - r.Header.Set("header", "host") + r = r.WithContext(zitadel_http.WithDomainContext(r.Context(), &zitadel_http.DomainCtx{InstanceHost: "host"})) return r }(), }, res{ statusCode: 200, - context: authz.WithInstance(context.Background(), &mockInstance{}), + context: authz.WithInstance(zitadel_http.WithDomainContext(context.Background(), &zitadel_http.DomainCtx{InstanceHost: "host"}), &mockInstance{}), }, }, } @@ -72,7 +69,6 @@ func Test_instanceInterceptor_Handler(t *testing.T) { t.Run(tt.name, func(t *testing.T) { a := &instanceInterceptor{ verifier: tt.fields.verifier, - headerName: tt.fields.headerName, translator: newZitadelTranslator(), } next := &testHandler{} @@ -87,8 +83,7 @@ func Test_instanceInterceptor_Handler(t *testing.T) { func Test_instanceInterceptor_HandlerFunc(t *testing.T) { type fields struct { - verifier authz.InstanceVerifier - headerName string + verifier authz.InstanceVerifier } type args struct { request *http.Request @@ -106,8 +101,7 @@ func Test_instanceInterceptor_HandlerFunc(t *testing.T) { { "setInstance error", fields{ - verifier: &mockInstanceVerifier{}, - headerName: "header", + verifier: &mockInstanceVerifier{}, }, args{ request: httptest.NewRequest("", "/url", nil), @@ -120,19 +114,18 @@ func Test_instanceInterceptor_HandlerFunc(t *testing.T) { { "setInstance ok", fields{ - verifier: &mockInstanceVerifier{"host"}, - headerName: "header", + verifier: &mockInstanceVerifier{instanceHost: "host"}, }, args{ request: func() *http.Request { r := httptest.NewRequest("", "/url", nil) - r.Header.Set("header", "host") + r = r.WithContext(zitadel_http.WithDomainContext(r.Context(), &zitadel_http.DomainCtx{InstanceHost: "host"})) return r }(), }, res{ statusCode: 200, - context: authz.WithInstance(context.Background(), &mockInstance{}), + context: authz.WithInstance(zitadel_http.WithDomainContext(context.Background(), &zitadel_http.DomainCtx{InstanceHost: "host"}), &mockInstance{}), }, }, } @@ -140,7 +133,6 @@ func Test_instanceInterceptor_HandlerFunc(t *testing.T) { t.Run(tt.name, func(t *testing.T) { a := &instanceInterceptor{ verifier: tt.fields.verifier, - headerName: tt.fields.headerName, translator: newZitadelTranslator(), } next := &testHandler{} @@ -155,9 +147,8 @@ func Test_instanceInterceptor_HandlerFunc(t *testing.T) { func Test_setInstance(t *testing.T) { type args struct { - r *http.Request - verifier authz.InstanceVerifier - headerName string + r *http.Request + verifier authz.InstanceVerifier } type res struct { want context.Context @@ -169,14 +160,13 @@ func Test_setInstance(t *testing.T) { res res }{ { - "special host header not found, error", + "no domain context, not found error", args{ r: func() *http.Request { r := httptest.NewRequest("", "/url", nil) return r }(), - verifier: &mockInstanceVerifier{}, - headerName: "", + verifier: &mockInstanceVerifier{}, }, res{ want: nil, @@ -184,77 +174,27 @@ func Test_setInstance(t *testing.T) { }, }, { - "special host header invalid, error", + "instanceHost found, ok", args{ r: func() *http.Request { r := httptest.NewRequest("", "/url", nil) - r.Header.Set("header", "host2") - return r + return r.WithContext(zitadel_http.WithDomainContext(r.Context(), &zitadel_http.DomainCtx{InstanceHost: "host", Protocol: "https"})) }(), - verifier: &mockInstanceVerifier{"host"}, - headerName: "header", + verifier: &mockInstanceVerifier{instanceHost: "host"}, }, res{ - want: nil, - err: true, - }, - }, - { - "special host header valid, ok", - args{ - r: func() *http.Request { - r := httptest.NewRequest("", "/url", nil) - r.Header.Set("header", "host") - return r - }(), - verifier: &mockInstanceVerifier{"host"}, - headerName: "header", - }, - res{ - want: authz.WithInstance(context.Background(), &mockInstance{}), + want: authz.WithInstance(zitadel_http.WithDomainContext(context.Background(), &zitadel_http.DomainCtx{InstanceHost: "host", Protocol: "https"}), &mockInstance{}), err: false, }, }, { - "host from origin if header is not special, ok", + "instanceHost not found, error", args{ r: func() *http.Request { r := httptest.NewRequest("", "/url", nil) - r.Header.Set("host", "fromrequest") - return r.WithContext(zitadel_http.WithComposedOrigin(r.Context(), "https://fromorigin:9999")) + return r.WithContext(zitadel_http.WithDomainContext(r.Context(), &zitadel_http.DomainCtx{InstanceHost: "fromorigin:9999", Protocol: "https"})) }(), - verifier: &mockInstanceVerifier{"fromorigin:9999"}, - headerName: "host", - }, - res{ - want: authz.WithInstance(zitadel_http.WithComposedOrigin(context.Background(), "https://fromorigin:9999"), &mockInstance{}), - err: false, - }, - }, - { - "host from origin, instance not found", - args{ - r: func() *http.Request { - r := httptest.NewRequest("", "/url", nil) - return r.WithContext(zitadel_http.WithComposedOrigin(r.Context(), "https://fromorigin:9999")) - }(), - verifier: &mockInstanceVerifier{"unknowndomain"}, - headerName: "host", - }, - res{ - want: nil, - err: true, - }, - }, - { - "host from origin invalid, err", - args{ - r: func() *http.Request { - r := httptest.NewRequest("", "/url", nil) - return r.WithContext(zitadel_http.WithComposedOrigin(r.Context(), "https://from origin:9999")) - }(), - verifier: &mockInstanceVerifier{"from origin"}, - headerName: "host", + verifier: &mockInstanceVerifier{instanceHost: "unknowndomain"}, }, res{ want: nil, @@ -264,7 +204,7 @@ func Test_setInstance(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := setInstance(tt.args.r, tt.args.verifier, tt.args.headerName) + got, err := setInstance(tt.args.r, tt.args.verifier) if (err != nil) != tt.res.err { t.Errorf("setInstance() error = %v, wantErr %v", err, tt.res.err) return @@ -285,11 +225,18 @@ func (t *testHandler) ServeHTTP(_ http.ResponseWriter, r *http.Request) { } type mockInstanceVerifier struct { - host string + instanceHost string + publicHost string } -func (m *mockInstanceVerifier) InstanceByHost(_ context.Context, host string) (authz.Instance, error) { - if host != m.host { +func (m *mockInstanceVerifier) InstanceByHost(_ context.Context, instanceHost, publicHost string) (authz.Instance, error) { + if instanceHost != m.instanceHost { + return nil, fmt.Errorf("invalid host") + } + if publicHost == "" { + return &mockInstance{}, nil + } + if publicHost != instanceHost && publicHost != m.publicHost { return nil, fmt.Errorf("invalid host") } return &mockInstance{}, nil @@ -333,14 +280,6 @@ func (m *mockInstance) DefaultOrganisationID() string { return "orgID" } -func (m *mockInstance) RequestedDomain() string { - return "zitadel.cloud" -} - -func (m *mockInstance) RequestedHost() string { - return "zitadel.cloud:443" -} - func (m *mockInstance) SecurityPolicyAllowedOrigins() []string { return nil } diff --git a/internal/api/http/middleware/origin_interceptor.go b/internal/api/http/middleware/origin_interceptor.go index 02a67ad05d..bbec9dc14d 100644 --- a/internal/api/http/middleware/origin_interceptor.go +++ b/internal/api/http/middleware/origin_interceptor.go @@ -1,53 +1,82 @@ package middleware import ( - "fmt" "net/http" + "slices" "github.com/gorilla/mux" "github.com/muhlemmer/httpforwarded" - "github.com/zitadel/logging" http_util "github.com/zitadel/zitadel/internal/api/http" ) -func WithOrigin(fallBackToHttps bool) mux.MiddlewareFunc { +func WithOrigin(fallBackToHttps 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 := composeOrigin(r, fallBackToHttps) - if !http_util.IsOrigin(origin) { - logging.Debugf("extracted origin is not valid: %s", origin) - next.ServeHTTP(w, r) - return - } - next.ServeHTTP(w, r.WithContext(http_util.WithComposedOrigin(r.Context(), origin))) + origin := composeDomainContext( + r, + fallBackToHttps, + // 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.ForwardedFor, http_util.ForwardedHost, http_util.ForwardedProto)), + publicDomainHeaders, + ) + next.ServeHTTP(w, r.WithContext(http_util.WithDomainContext(r.Context(), origin))) }) } } -func composeOrigin(r *http.Request, fallBackToHttps bool) string { - var proto, host string - fwd, fwdErr := httpforwarded.ParseFromRequest(r) - if fwdErr == nil { - proto = oldestForwardedValue(fwd, "proto") - host = oldestForwardedValue(fwd, "host") +func composeDomainContext(r *http.Request, fallBackToHttps bool, instanceDomainHeaders, publicDomainHeaders []string) *http_util.DomainCtx { + instanceHost, instanceProto := hostFromRequest(r, instanceDomainHeaders) + publicHost, publicProto := hostFromRequest(r, publicDomainHeaders) + if publicProto == "" { + publicProto = instanceProto } - if proto == "" { - proto = r.Header.Get("X-Forwarded-Proto") - } - if host == "" { - host = r.Header.Get("X-Forwarded-Host") - } - if proto == "" { - proto = "http" + if publicProto == "" { + publicProto = "http" if fallBackToHttps { - proto = "https" + publicProto = "https" } } - if host == "" { - host = r.Host + if instanceHost == "" { + instanceHost = r.Host } - return fmt.Sprintf("%s://%s", proto, host) + return &http_util.DomainCtx{ + InstanceHost: instanceHost, + Protocol: publicProto, + PublicHost: publicHost, + } +} + +func hostFromRequest(r *http.Request, headers []string) (host, proto string) { + var hostFromHeader, protoFromHeader string + for _, header := range headers { + switch http.CanonicalHeaderKey(header) { + case http.CanonicalHeaderKey(http_util.Forwarded), + http.CanonicalHeaderKey(http_util.ForwardedFor): + hostFromHeader, protoFromHeader = hostFromForwarded(r.Header.Values(header)) + case http.CanonicalHeaderKey(http_util.ForwardedHost): + hostFromHeader = r.Header.Get(header) + case http.CanonicalHeaderKey(http_util.ForwardedProto): + protoFromHeader = r.Header.Get(header) + default: + hostFromHeader = r.Header.Get(header) + } + if host == "" { + host = hostFromHeader + } + if proto == "" { + proto = protoFromHeader + } + } + return host, proto +} + +func hostFromForwarded(values []string) (string, string) { + fwd, fwdErr := httpforwarded.Parse(values) + if fwdErr == nil { + return oldestForwardedValue(fwd, "host"), oldestForwardedValue(fwd, "proto") + } + return "", "" } func oldestForwardedValue(forwarded map[string][]string, key string) string { diff --git a/internal/api/http/middleware/origin_interceptor_test.go b/internal/api/http/middleware/origin_interceptor_test.go index 31b2136b58..989e4d48b3 100644 --- a/internal/api/http/middleware/origin_interceptor_test.go +++ b/internal/api/http/middleware/origin_interceptor_test.go @@ -5,6 +5,8 @@ import ( "testing" "github.com/stretchr/testify/assert" + + http_util "github.com/zitadel/zitadel/internal/api/http" ) func Test_composeOrigin(t *testing.T) { @@ -15,10 +17,13 @@ func Test_composeOrigin(t *testing.T) { tests := []struct { name string args args - want string + want *http_util.DomainCtx }{{ name: "no proxy headers", - want: "http://host.header", + want: &http_util.DomainCtx{ + InstanceHost: "host.header", + Protocol: "http", + }, }, { name: "forwarded proto", args: args{ @@ -27,7 +32,10 @@ func Test_composeOrigin(t *testing.T) { }, fallBackToHttps: false, }, - want: "https://host.header", + want: &http_util.DomainCtx{ + InstanceHost: "host.header", + Protocol: "https", + }, }, { name: "forwarded host", args: args{ @@ -36,7 +44,10 @@ func Test_composeOrigin(t *testing.T) { }, fallBackToHttps: false, }, - want: "http://forwarded.host", + want: &http_util.DomainCtx{ + InstanceHost: "forwarded.host", + Protocol: "http", + }, }, { name: "forwarded proto and host", args: args{ @@ -45,7 +56,10 @@ func Test_composeOrigin(t *testing.T) { }, fallBackToHttps: false, }, - want: "https://forwarded.host", + want: &http_util.DomainCtx{ + InstanceHost: "forwarded.host", + Protocol: "https", + }, }, { name: "forwarded proto and host with multiple complete entries", args: args{ @@ -54,7 +68,10 @@ func Test_composeOrigin(t *testing.T) { }, fallBackToHttps: false, }, - want: "https://forwarded.host", + want: &http_util.DomainCtx{ + InstanceHost: "forwarded.host", + Protocol: "https", + }, }, { name: "forwarded proto and host with multiple incomplete entries", args: args{ @@ -63,7 +80,10 @@ func Test_composeOrigin(t *testing.T) { }, fallBackToHttps: false, }, - want: "https://forwarded.host", + want: &http_util.DomainCtx{ + InstanceHost: "forwarded.host", + Protocol: "https", + }, }, { name: "forwarded proto and host with incomplete entries in different values", args: args{ @@ -72,7 +92,10 @@ func Test_composeOrigin(t *testing.T) { }, fallBackToHttps: true, }, - want: "http://forwarded.host", + want: &http_util.DomainCtx{ + InstanceHost: "forwarded.host", + Protocol: "http", + }, }, { name: "x-forwarded-proto https", args: args{ @@ -81,7 +104,10 @@ func Test_composeOrigin(t *testing.T) { }, fallBackToHttps: false, }, - want: "https://host.header", + want: &http_util.DomainCtx{ + InstanceHost: "host.header", + Protocol: "https", + }, }, { name: "x-forwarded-proto http", args: args{ @@ -90,19 +116,28 @@ func Test_composeOrigin(t *testing.T) { }, fallBackToHttps: true, }, - want: "http://host.header", + want: &http_util.DomainCtx{ + InstanceHost: "host.header", + Protocol: "http", + }, }, { name: "fallback to http", args: args{ fallBackToHttps: false, }, - want: "http://host.header", + want: &http_util.DomainCtx{ + InstanceHost: "host.header", + Protocol: "http", + }, }, { name: "fallback to https", args: args{ fallBackToHttps: true, }, - want: "https://host.header", + want: &http_util.DomainCtx{ + InstanceHost: "host.header", + Protocol: "https", + }, }, { name: "x-forwarded-host", args: args{ @@ -111,7 +146,10 @@ func Test_composeOrigin(t *testing.T) { }, fallBackToHttps: false, }, - want: "http://x-forwarded.host", + want: &http_util.DomainCtx{ + InstanceHost: "x-forwarded.host", + Protocol: "http", + }, }, { name: "x-forwarded-proto and x-forwarded-host", args: args{ @@ -121,7 +159,10 @@ func Test_composeOrigin(t *testing.T) { }, fallBackToHttps: false, }, - want: "https://x-forwarded.host", + want: &http_util.DomainCtx{ + InstanceHost: "x-forwarded.host", + Protocol: "https", + }, }, { name: "forwarded host and x-forwarded-host", args: args{ @@ -131,7 +172,10 @@ func Test_composeOrigin(t *testing.T) { }, fallBackToHttps: false, }, - want: "http://forwarded.host", + want: &http_util.DomainCtx{ + InstanceHost: "forwarded.host", + Protocol: "http", + }, }, { name: "forwarded host and x-forwarded-proto", args: args{ @@ -141,17 +185,22 @@ func Test_composeOrigin(t *testing.T) { }, fallBackToHttps: false, }, - want: "https://forwarded.host", + want: &http_util.DomainCtx{ + InstanceHost: "forwarded.host", + Protocol: "https", + }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assert.Equalf(t, tt.want, composeOrigin( + assert.Equalf(t, tt.want, composeDomainContext( &http.Request{ Host: "host.header", Header: tt.args.h, }, tt.args.fallBackToHttps, + []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) }) } diff --git a/internal/api/http/request_context.go b/internal/api/http/request_context.go new file mode 100644 index 0000000000..9ed345ed88 --- /dev/null +++ b/internal/api/http/request_context.go @@ -0,0 +1,60 @@ +package http + +import ( + "context" + "fmt" + "strings" +) + +type DomainCtx struct { + InstanceHost string + PublicHost string + Protocol string +} + +// RequestedHost returns the host (hostname[:port]) for which the request was handled. +// The instance host is returned if not public host was set. +func (r *DomainCtx) RequestedHost() string { + if r.PublicHost != "" { + return r.PublicHost + } + return r.InstanceHost +} + +// RequestedDomain returns the domain (hostname) for which the request was handled. +// The instance domain is returned if not public host / domain was set. +func (r *DomainCtx) RequestedDomain() string { + return strings.Split(r.RequestedHost(), ":")[0] +} + +// Origin returns the origin (protocol://hostname[:port]) for which the request was handled. +// The instance host is used if not public host was set. +func (r *DomainCtx) Origin() string { + host := r.PublicHost + if host == "" { + host = r.InstanceHost + } + return fmt.Sprintf("%s://%s", r.Protocol, host) +} + +func DomainContext(ctx context.Context) *DomainCtx { + o, ok := ctx.Value(domainCtx).(*DomainCtx) + if !ok { + return &DomainCtx{} + } + return o +} + +func WithDomainContext(ctx context.Context, domainContext *DomainCtx) context.Context { + return context.WithValue(ctx, domainCtx, domainContext) +} + +func WithRequestedHost(ctx context.Context, host string) context.Context { + i, ok := ctx.Value(domainCtx).(*DomainCtx) + if !ok { + i = new(DomainCtx) + } + + i.PublicHost = host + return context.WithValue(ctx, domainCtx, i) +} diff --git a/internal/api/idp/idp.go b/internal/api/idp/idp.go index 0209e0337d..b058ba5c2a 100644 --- a/internal/api/idp/idp.go +++ b/internal/api/idp/idp.go @@ -13,7 +13,6 @@ import ( "github.com/gorilla/mux" "github.com/zitadel/logging" - "github.com/zitadel/zitadel/internal/api/authz" http_utils "github.com/zitadel/zitadel/internal/api/http" "github.com/zitadel/zitadel/internal/api/ui/login" "github.com/zitadel/zitadel/internal/command" @@ -79,21 +78,21 @@ type externalSAMLIDPCallbackData struct { } // CallbackURL generates the instance specific URL to the IDP callback handler -func CallbackURL(externalSecure bool) func(ctx context.Context) string { +func CallbackURL() func(ctx context.Context) string { return func(ctx context.Context) string { - return http_utils.BuildOrigin(authz.GetInstance(ctx).RequestedHost(), externalSecure) + HandlerPrefix + callbackPath + return http_utils.DomainContext(ctx).Origin() + HandlerPrefix + callbackPath } } -func SAMLRootURL(externalSecure bool) func(ctx context.Context, idpID string) string { +func SAMLRootURL() func(ctx context.Context, idpID string) string { return func(ctx context.Context, idpID string) string { - return http_utils.BuildOrigin(authz.GetInstance(ctx).RequestedHost(), externalSecure) + HandlerPrefix + "/" + idpID + "/" + return http_utils.DomainContext(ctx).Origin() + HandlerPrefix + "/" + idpID + "/" } } -func LoginSAMLRootURL(externalSecure bool) func(ctx context.Context) string { +func LoginSAMLRootURL() func(ctx context.Context) string { return func(ctx context.Context) string { - return http_utils.BuildOrigin(authz.GetInstance(ctx).RequestedHost(), externalSecure) + login.HandlerPrefix + login.EndpointSAMLACS + return http_utils.DomainContext(ctx).Origin() + login.HandlerPrefix + login.EndpointSAMLACS } } @@ -101,7 +100,6 @@ func NewHandler( commands *command.Commands, queries *query.Queries, encryptionAlgorithm crypto.EncryptionAlgorithm, - externalSecure bool, instanceInterceptor func(next http.Handler) http.Handler, ) http.Handler { h := &Handler{ @@ -109,9 +107,9 @@ func NewHandler( queries: queries, parser: form.NewParser(), encryptionAlgorithm: encryptionAlgorithm, - callbackURL: CallbackURL(externalSecure), - samlRootURL: SAMLRootURL(externalSecure), - loginSAMLRootURL: LoginSAMLRootURL(externalSecure), + callbackURL: CallbackURL(), + samlRootURL: SAMLRootURL(), + loginSAMLRootURL: LoginSAMLRootURL(), } router := mux.NewRouter() diff --git a/internal/api/oidc/op.go b/internal/api/oidc/op.go index 24e201e620..67bc1765a5 100644 --- a/internal/api/oidc/op.go +++ b/internal/api/oidc/op.go @@ -100,7 +100,7 @@ func NewServer( if err != nil { return nil, zerrors.ThrowInternal(err, "OIDC-EGrqd", "cannot create op config: %w") } - storage := newStorage(config, command, query, repo, encryptionAlg, es, projections, externalSecure) + storage := newStorage(config, command, query, repo, encryptionAlg, es, projections) keyCache := newPublicKeyCache(ctx, config.PublicKeyCacheMaxAge, query.GetPublicKeyByID) accessTokenKeySet := newOidcKeySet(keyCache, withKeyExpiryCheck(true)) idTokenHintKeySet := newOidcKeySet(keyCache) @@ -115,7 +115,7 @@ func NewServer( provider, err := op.NewProvider( opConfig, storage, - op.IssuerFromForwardedOrHost("", op.WithIssuerFromCustomHeaders("forwarded", "x-zitadel-forwarded")), + IssuerFromContext, options..., ) if err != nil { @@ -142,7 +142,7 @@ func NewServer( signingKeyAlgorithm: config.SigningKeyAlgorithm, encAlg: encryptionAlg, opCrypto: op.NewAESCrypto(opConfig.CryptoKey), - assetAPIPrefix: assets.AssetAPI(externalSecure), + assetAPIPrefix: assets.AssetAPI(), } metricTypes := []metrics.MetricType{metrics.MetricTypeRequestCount, metrics.MetricTypeStatusCode, metrics.MetricTypeTotalCount} server.Handler = op.RegisterLegacyServer(server, @@ -162,6 +162,12 @@ func NewServer( return server, nil } +func IssuerFromContext(_ bool) (op.IssuerFromRequest, error) { + return func(r *http.Request) string { + return http_utils.DomainContext(r.Context()).Origin() + }, nil +} + func publicAuthPathPrefixes(endpoints *EndpointConfig) []string { authURL := op.DefaultEndpoints.Authorization.Relative() keysURL := op.DefaultEndpoints.JwksURI.Relative() @@ -194,7 +200,7 @@ func createOPConfig(config Config, defaultLogoutRedirectURI string, cryptoKey [] return opConfig, nil } -func newStorage(config Config, command *command.Commands, query *query.Queries, repo repository.Repository, encAlg crypto.EncryptionAlgorithm, es *eventstore.Eventstore, db *database.DB, externalSecure bool) *OPStorage { +func newStorage(config Config, command *command.Commands, query *query.Queries, repo repository.Repository, encAlg crypto.EncryptionAlgorithm, es *eventstore.Eventstore, db *database.DB) *OPStorage { return &OPStorage{ repo: repo, command: command, @@ -210,7 +216,7 @@ func newStorage(config Config, command *command.Commands, query *query.Queries, defaultRefreshTokenExpiration: config.DefaultRefreshTokenExpiration, encAlg: encAlg, locker: crdb.NewLocker(db.DB, locksTable, signingKey), - assetAPIPrefix: assets.AssetAPI(externalSecure), + assetAPIPrefix: assets.AssetAPI(), } } diff --git a/internal/api/ui/console/console.go b/internal/api/ui/console/console.go index 9e28dae70a..515f26db9b 100644 --- a/internal/api/ui/console/console.go +++ b/internal/api/ui/console/console.go @@ -21,6 +21,7 @@ import ( "github.com/zitadel/zitadel/internal/api/authz" http_util "github.com/zitadel/zitadel/internal/api/http" "github.com/zitadel/zitadel/internal/api/http/middleware" + console_path "github.com/zitadel/zitadel/internal/api/ui/console/path" ) type Config struct { @@ -40,7 +41,6 @@ var ( const ( envRequestPath = "/assets/environment.json" - HandlerPrefix = "/ui/console" ) var ( @@ -56,7 +56,7 @@ var ( ) func LoginHintLink(origin, username string) string { - return origin + HandlerPrefix + "?login_hint=" + username + return origin + console_path.HandlerPrefix + "?login_hint=" + username } func (i *spaHandler) Open(name string) (http.File, error) { diff --git a/internal/api/ui/console/path/paths.go b/internal/api/ui/console/path/paths.go new file mode 100644 index 0000000000..8890c516cd --- /dev/null +++ b/internal/api/ui/console/path/paths.go @@ -0,0 +1,7 @@ +package path + +const ( + HandlerPrefix = "/ui/console" + RedirectPath = HandlerPrefix + "/auth/callback" + PostLogoutPath = HandlerPrefix + "/signedout" +) diff --git a/internal/api/ui/login/external_provider_handler.go b/internal/api/ui/login/external_provider_handler.go index ab0d98b997..b8c6a70abf 100644 --- a/internal/api/ui/login/external_provider_handler.go +++ b/internal/api/ui/login/external_provider_handler.go @@ -1042,7 +1042,7 @@ func (l *Login) samlProvider(ctx context.Context, identityProvider *query.IDPTem opts = append(opts, saml.WithTransientMappingAttributeName(identityProvider.SAMLIDPTemplate.TransientMappingAttributeName)) } opts = append(opts, - saml.WithEntityID(http_utils.BuildOrigin(authz.GetInstance(ctx).RequestedHost(), l.externalSecure)+"/idps/"+identityProvider.ID+"/saml/metadata"), + saml.WithEntityID(http_utils.DomainContext(ctx).Origin()+"/idps/"+identityProvider.ID+"/saml/metadata"), saml.WithCustomRequestTracker( requesttracker.New( func(ctx context.Context, authRequestID, samlRequestID string) error { diff --git a/internal/api/ui/login/login.go b/internal/api/ui/login/login.go index 5c431f1f24..bda1ecac59 100644 --- a/internal/api/ui/login/login.go +++ b/internal/api/ui/login/login.go @@ -129,7 +129,7 @@ func createCSRFInterceptor(cookieName string, csrfCookieKey []byte, externalSecu sameSiteMode = csrf.SameSiteNoneMode // ... and since SameSite none requires the secure flag, we'll set it for TLS and for localhost // (regardless of the TLS / externalSecure settings) - secureOnly = externalSecure || instance.RequestedDomain() == "localhost" + secureOnly = externalSecure || http_utils.DomainContext(r.Context()).RequestedDomain() == "localhost" } csrf.Protect(csrfCookieKey, csrf.Secure(secureOnly), @@ -163,7 +163,7 @@ func (l *Login) Handler() http.Handler { } func (l *Login) getClaimedUserIDsOfOrgDomain(ctx context.Context, orgName string) ([]string, error) { - orgDomain, err := domain.NewIAMDomainName(orgName, authz.GetInstance(ctx).RequestedDomain()) + orgDomain, err := domain.NewIAMDomainName(orgName, http_utils.DomainContext(ctx).RequestedDomain()) if err != nil { return nil, err } @@ -199,5 +199,5 @@ func setUserContext(ctx context.Context, userID, resourceOwner string) context.C } func (l *Login) baseURL(ctx context.Context) string { - return http_utils.BuildOrigin(authz.GetInstance(ctx).RequestedHost(), l.externalSecure) + HandlerPrefix + return http_utils.DomainContext(ctx).Origin() + HandlerPrefix } diff --git a/internal/api/ui/login/register_org_handler.go b/internal/api/ui/login/register_org_handler.go index 0243a37569..acb032d8f1 100644 --- a/internal/api/ui/login/register_org_handler.go +++ b/internal/api/ui/login/register_org_handler.go @@ -3,7 +3,7 @@ package login import ( "net/http" - "github.com/zitadel/zitadel/internal/api/authz" + http_util "github.com/zitadel/zitadel/internal/api/http" "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/zerrors" @@ -128,7 +128,7 @@ func (l *Login) renderRegisterOrg(w http.ResponseWriter, r *http.Request, authRe orgPolicy, _ := l.getDefaultDomainPolicy(r) if orgPolicy != nil { data.UserLoginMustBeDomain = orgPolicy.UserLoginMustBeDomain - data.IamDomain = authz.GetInstance(r.Context()).RequestedDomain() + data.IamDomain = http_util.DomainContext(r.Context()).RequestedDomain() } if authRequest == nil { diff --git a/internal/authz/repository/eventsourcing/eventstore/token_verifier.go b/internal/authz/repository/eventsourcing/eventstore/token_verifier.go index 4d8823913d..c9286d61dd 100644 --- a/internal/authz/repository/eventsourcing/eventstore/token_verifier.go +++ b/internal/authz/repository/eventsourcing/eventstore/token_verifier.go @@ -297,8 +297,7 @@ func (repo *TokenVerifierRepo) getTokenIDAndSubject(ctx context.Context, accessT func (repo *TokenVerifierRepo) jwtTokenVerifier(ctx context.Context) *op.AccessTokenVerifier { keySet := &openIDKeySet{repo.Query} - issuer := http_util.BuildOrigin(authz.GetInstance(ctx).RequestedHost(), repo.ExternalSecure) - return op.NewAccessTokenVerifier(issuer, keySet) + return op.NewAccessTokenVerifier(http_util.DomainContext(ctx).Origin(), keySet) } func (repo *TokenVerifierRepo) decryptAccessToken(token string) (string, error) { diff --git a/internal/command/instance.go b/internal/command/instance.go index b5fb700459..4d3e1d2528 100644 --- a/internal/command/instance.go +++ b/internal/command/instance.go @@ -7,7 +7,7 @@ import ( "golang.org/x/text/language" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/ui/console" + "github.com/zitadel/zitadel/internal/api/http" "github.com/zitadel/zitadel/internal/command/preparation" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" @@ -26,13 +26,11 @@ import ( ) const ( - zitadelProjectName = "ZITADEL" - mgmtAppName = "Management-API" - adminAppName = "Admin-API" - authAppName = "Auth-API" - consoleAppName = "Console" - consoleRedirectPath = console.HandlerPrefix + "/auth/callback" - consolePostLogoutPath = console.HandlerPrefix + "/signedout" + zitadelProjectName = "ZITADEL" + mgmtAppName = "Management-API" + adminAppName = "Admin-API" + authAppName = "Auth-API" + consoleAppName = "Console" ) type InstanceSetup struct { @@ -233,7 +231,7 @@ func (c *Commands) SetUpInstance(ctx context.Context, setup *InstanceSetup) (str func contextWithInstanceSetupInfo(ctx context.Context, instanceID, projectID, consoleAppID, externalDomain string) context.Context { return authz.WithConsole( authz.SetCtxData( - authz.WithRequestedDomain( + http.WithRequestedHost( authz.WithInstanceID( ctx, instanceID), diff --git a/internal/command/instance_domain.go b/internal/command/instance_domain.go index 43e1aebbf8..8eacee359e 100644 --- a/internal/command/instance_domain.go +++ b/internal/command/instance_domain.go @@ -7,6 +7,7 @@ import ( "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/http" + "github.com/zitadel/zitadel/internal/api/ui/console/path" "github.com/zitadel/zitadel/internal/command/preparation" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" @@ -74,7 +75,7 @@ func (c *Commands) RemoveInstanceDomain(ctx context.Context, instanceDomain stri } func (c *Commands) addGeneratedInstanceDomain(ctx context.Context, a *instance.Aggregate, instanceName string) ([]preparation.Validation, error) { - domain, err := c.GenerateDomain(instanceName, authz.GetInstance(ctx).RequestedDomain()) + domain, err := c.GenerateDomain(instanceName, http.DomainContext(ctx).RequestedDomain()) if err != nil { return nil, err } @@ -143,12 +144,12 @@ func (c *Commands) updateConsoleRedirectURIs(ctx context.Context, filter prepara if !appWriteModel.State.Exists() { return nil, nil } - redirectURI := http.BuildHTTP(instanceDomain, c.externalPort, c.externalSecure) + consoleRedirectPath + redirectURI := http.BuildHTTP(instanceDomain, c.externalPort, c.externalSecure) + path.RedirectPath changes := make([]project.OIDCConfigChanges, 0, 2) if !containsURI(appWriteModel.RedirectUris, redirectURI) { changes = append(changes, project.ChangeRedirectURIs(append(appWriteModel.RedirectUris, redirectURI))) } - postLogoutRedirectURI := http.BuildHTTP(instanceDomain, c.externalPort, c.externalSecure) + consolePostLogoutPath + postLogoutRedirectURI := http.BuildHTTP(instanceDomain, c.externalPort, c.externalSecure) + path.PostLogoutPath if !containsURI(appWriteModel.PostLogoutRedirectUris, postLogoutRedirectURI) { changes = append(changes, project.ChangePostLogoutRedirectURIs(append(appWriteModel.PostLogoutRedirectUris, postLogoutRedirectURI))) } diff --git a/internal/command/instance_trusted_domain.go b/internal/command/instance_trusted_domain.go new file mode 100644 index 0000000000..f404e6665a --- /dev/null +++ b/internal/command/instance_trusted_domain.go @@ -0,0 +1,50 @@ +package command + +import ( + "context" + "slices" + "strings" + + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/repository/instance" + "github.com/zitadel/zitadel/internal/zerrors" +) + +func (c *Commands) AddTrustedDomain(ctx context.Context, trustedDomain string) (*domain.ObjectDetails, error) { + trustedDomain = strings.TrimSpace(trustedDomain) + if trustedDomain == "" || len(trustedDomain) > 253 { + return nil, zerrors.ThrowInvalidArgument(nil, "COMMA-Stk21", "Errors.Invalid.Argument") + } + if !allowDomainRunes.MatchString(trustedDomain) { + return nil, zerrors.ThrowInvalidArgument(nil, "COMMA-S3v3w", "Errors.Instance.Domain.InvalidCharacter") + } + model := NewInstanceTrustedDomainsWriteModel(ctx) + err := c.eventstore.FilterToQueryReducer(ctx, model) + if err != nil { + return nil, err + } + if slices.Contains(model.Domains, trustedDomain) { + return nil, zerrors.ThrowPreconditionFailed(nil, "COMMA-hg42a", "Errors.Instance.Domain.AlreadyExists") + } + err = c.pushAppendAndReduce(ctx, model, instance.NewTrustedDomainAddedEvent(ctx, InstanceAggregateFromWriteModel(&model.WriteModel), trustedDomain)) + if err != nil { + return nil, err + } + return writeModelToObjectDetails(&model.WriteModel), nil +} + +func (c *Commands) RemoveTrustedDomain(ctx context.Context, trustedDomain string) (*domain.ObjectDetails, error) { + model := NewInstanceTrustedDomainsWriteModel(ctx) + err := c.eventstore.FilterToQueryReducer(ctx, model) + if err != nil { + return nil, err + } + if !slices.Contains(model.Domains, trustedDomain) { + return nil, zerrors.ThrowNotFound(nil, "COMMA-de3z9", "Errors.Instance.Domain.NotFound") + } + err = c.pushAppendAndReduce(ctx, model, instance.NewTrustedDomainRemovedEvent(ctx, InstanceAggregateFromWriteModel(&model.WriteModel), trustedDomain)) + if err != nil { + return nil, err + } + return writeModelToObjectDetails(&model.WriteModel), nil +} diff --git a/internal/command/instance_trusted_domain_test.go b/internal/command/instance_trusted_domain_test.go new file mode 100644 index 0000000000..f4f0001c7c --- /dev/null +++ b/internal/command/instance_trusted_domain_test.go @@ -0,0 +1,197 @@ +package command + +import ( + "context" + "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/repository/instance" + "github.com/zitadel/zitadel/internal/zerrors" +) + +func TestCommands_AddTrustedDomain(t *testing.T) { + type fields struct { + eventstore func(*testing.T) *eventstore.Eventstore + } + type args struct { + ctx context.Context + trustedDomain string + } + type want struct { + details *domain.ObjectDetails + err error + } + tests := []struct { + name string + fields fields + args args + want want + }{ + { + name: "empty domain, error", + fields: fields{ + eventstore: expectEventstore(), + }, + args: args{ + ctx: authz.WithInstanceID(context.Background(), "instanceID"), + trustedDomain: "", + }, + want: want{ + err: zerrors.ThrowInvalidArgument(nil, "COMMA-Stk21", "Errors.Invalid.Argument"), + }, + }, + { + name: "invalid domain (length), error", + fields: fields{ + eventstore: expectEventstore(), + }, + args: args{ + ctx: authz.WithInstanceID(context.Background(), "instanceID"), + trustedDomain: "my-very-endleeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeess-looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo0ooooooooooooooooooooooong.domain.com", + }, + want: want{ + err: zerrors.ThrowInvalidArgument(nil, "COMMA-Stk21", "Errors.Invalid.Argument"), + }, + }, + { + name: "invalid domain (chars), error", + fields: fields{ + eventstore: expectEventstore(), + }, + args: args{ + ctx: authz.WithInstanceID(context.Background(), "instanceID"), + trustedDomain: "&.com", + }, + want: want{ + err: zerrors.ThrowInvalidArgument(nil, "COMMA-S3v3w", "Errors.Instance.Domain.InvalidCharacter"), + }, + }, + { + name: "domain already exists, error", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + instance.NewTrustedDomainAddedEvent(context.Background(), + &instance.NewAggregate("instanceID").Aggregate, "domain.com"), + ), + ), + ), + }, + args: args{ + ctx: authz.WithInstanceID(context.Background(), "instanceID"), + trustedDomain: "domain.com", + }, + want: want{ + err: zerrors.ThrowPreconditionFailed(nil, "COMMA-hg42a", "Errors.Instance.Domain.AlreadyExists"), + }, + }, + { + name: "domain add ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter(), + expectPush( + instance.NewTrustedDomainAddedEvent(context.Background(), + &instance.NewAggregate("instanceID").Aggregate, "domain.com"), + ), + ), + }, + args: args{ + ctx: authz.WithInstanceID(context.Background(), "instanceID"), + trustedDomain: "domain.com", + }, + want: want{ + details: &domain.ObjectDetails{ + ResourceOwner: "instanceID", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &Commands{ + eventstore: tt.fields.eventstore(t), + } + got, err := c.AddTrustedDomain(tt.args.ctx, tt.args.trustedDomain) + assert.ErrorIs(t, err, tt.want.err) + assert.Equal(t, tt.want.details, got) + }) + } +} + +func TestCommands_RemoveTrustedDomain(t *testing.T) { + type fields struct { + eventstore func(*testing.T) *eventstore.Eventstore + } + type args struct { + ctx context.Context + trustedDomain string + } + type want struct { + details *domain.ObjectDetails + err error + } + tests := []struct { + name string + fields fields + args args + want want + }{ + { + name: "domain does not exists, error", + fields: fields{ + eventstore: expectEventstore( + expectFilter(), + ), + }, + args: args{ + ctx: authz.WithInstanceID(context.Background(), "instanceID"), + trustedDomain: "domain.com", + }, + want: want{ + err: zerrors.ThrowNotFound(nil, "COMMA-de3z9", "Errors.Instance.Domain.NotFound"), + }, + }, + { + name: "domain remove ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + instance.NewTrustedDomainAddedEvent(context.Background(), + &instance.NewAggregate("instanceID").Aggregate, "domain.com"), + ), + ), + expectPush( + instance.NewTrustedDomainRemovedEvent(context.Background(), + &instance.NewAggregate("instanceID").Aggregate, "domain.com"), + ), + ), + }, + args: args{ + ctx: authz.WithInstanceID(context.Background(), "instanceID"), + trustedDomain: "domain.com", + }, + want: want{ + details: &domain.ObjectDetails{ + ResourceOwner: "instanceID", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &Commands{ + eventstore: tt.fields.eventstore(t), + } + got, err := c.RemoveTrustedDomain(tt.args.ctx, tt.args.trustedDomain) + assert.ErrorIs(t, err, tt.want.err) + assert.Equal(t, tt.want.details, got) + }) + } +} diff --git a/internal/command/instance_trusted_domains_model.go b/internal/command/instance_trusted_domains_model.go new file mode 100644 index 0000000000..ddfc0a110c --- /dev/null +++ b/internal/command/instance_trusted_domains_model.go @@ -0,0 +1,54 @@ +package command + +import ( + "context" + "slices" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/repository/instance" +) + +type InstanceTrustedDomainsWriteModel struct { + eventstore.WriteModel + + Domains []string +} + +func NewInstanceTrustedDomainsWriteModel(ctx context.Context) *InstanceTrustedDomainsWriteModel { + instanceID := authz.GetInstance(ctx).InstanceID() + return &InstanceTrustedDomainsWriteModel{ + WriteModel: eventstore.WriteModel{ + AggregateID: instanceID, + ResourceOwner: instanceID, + InstanceID: instanceID, + }, + } +} + +func (wm *InstanceTrustedDomainsWriteModel) Reduce() error { + for _, event := range wm.Events { + switch e := event.(type) { + case *instance.TrustedDomainAddedEvent: + wm.Domains = append(wm.Domains, e.Domain) + case *instance.TrustedDomainRemovedEvent: + wm.Domains = slices.DeleteFunc(wm.Domains, func(domain string) bool { + return domain == e.Domain + }) + } + } + return wm.WriteModel.Reduce() +} + +func (wm *InstanceTrustedDomainsWriteModel) Query() *eventstore.SearchQueryBuilder { + return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). + ResourceOwner(wm.ResourceOwner). + AddQuery(). + AggregateTypes(instance.AggregateType). + AggregateIDs(wm.AggregateID). + EventTypes( + instance.TrustedDomainAddedEventType, + instance.TrustedDomainRemovedEventType, + ). + Builder() +} diff --git a/internal/command/main_test.go b/internal/command/main_test.go index a5991dd16f..e75392309f 100644 --- a/internal/command/main_test.go +++ b/internal/command/main_test.go @@ -208,14 +208,6 @@ func (m *mockInstance) DefaultOrganisationID() string { return "defaultOrgID" } -func (m *mockInstance) RequestedDomain() string { - return "zitadel.cloud" -} - -func (m *mockInstance) RequestedHost() string { - return "zitadel.cloud:443" -} - func (m *mockInstance) SecurityPolicyAllowedOrigins() []string { return nil } diff --git a/internal/command/org.go b/internal/command/org.go index e603531b3f..261a571cb2 100644 --- a/internal/command/org.go +++ b/internal/command/org.go @@ -5,6 +5,7 @@ import ( "strings" "github.com/zitadel/zitadel/internal/api/authz" + http_util "github.com/zitadel/zitadel/internal/api/http" "github.com/zitadel/zitadel/internal/command/preparation" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" @@ -239,7 +240,7 @@ func AddOrgCommand(ctx context.Context, a *org.Aggregate, name string) preparati if name = strings.TrimSpace(name); name == "" { return nil, zerrors.ThrowInvalidArgument(nil, "ORG-mruNY", "Errors.Invalid.Argument") } - defaultDomain, err := domain.NewIAMDomainName(name, authz.GetInstance(ctx).RequestedDomain()) + defaultDomain, err := domain.NewIAMDomainName(name, http_util.DomainContext(ctx).RequestedDomain()) if err != nil { return nil, err } @@ -708,7 +709,7 @@ func (c *Commands) addOrgWithID(ctx context.Context, organisation *domain.Org, o } organisation.AggregateID = orgID - organisation.AddIAMDomain(authz.GetInstance(ctx).RequestedDomain()) + organisation.AddIAMDomain(http_util.DomainContext(ctx).RequestedDomain()) addedOrg := NewOrgWriteModel(organisation.AggregateID) orgAgg := OrgAggregateFromWriteModel(&addedOrg.WriteModel) diff --git a/internal/command/org_domain.go b/internal/command/org_domain.go index 20091aeb8b..929e0ab75b 100644 --- a/internal/command/org_domain.go +++ b/internal/command/org_domain.go @@ -323,7 +323,7 @@ func (c *Commands) changeDefaultDomain(ctx context.Context, orgID, newName strin if err != nil { return nil, err } - iamDomain := authz.GetInstance(ctx).RequestedDomain() + iamDomain := http_utils.DomainContext(ctx).RequestedDomain() defaultDomain, _ := domain.NewIAMDomainName(orgDomains.OrgName, iamDomain) isPrimary := defaultDomain == orgDomains.PrimaryDomain orgAgg := OrgAggregateFromWriteModel(&orgDomains.WriteModel) @@ -356,7 +356,7 @@ func (c *Commands) removeCustomDomains(ctx context.Context, orgID string) ([]eve return nil, err } hasDefault := false - defaultDomain, _ := domain.NewIAMDomainName(orgDomains.OrgName, authz.GetInstance(ctx).RequestedDomain()) + defaultDomain, _ := domain.NewIAMDomainName(orgDomains.OrgName, http_utils.DomainContext(ctx).RequestedDomain()) isPrimary := defaultDomain == orgDomains.PrimaryDomain orgAgg := OrgAggregateFromWriteModel(&orgDomains.WriteModel) events := make([]eventstore.Command, 0, len(orgDomains.Domains)) diff --git a/internal/command/org_domain_test.go b/internal/command/org_domain_test.go index 8af38098c5..80ec082846 100644 --- a/internal/command/org_domain_test.go +++ b/internal/command/org_domain_test.go @@ -8,7 +8,6 @@ import ( "go.uber.org/mock/gomock" "golang.org/x/text/language" - "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/http" "github.com/zitadel/zitadel/internal/command/preparation" "github.com/zitadel/zitadel/internal/crypto" @@ -105,7 +104,7 @@ func TestAddDomain(t *testing.T) { Commands: []eventstore.Command{ org.NewDomainAddedEvent(context.Background(), &agg.Aggregate, "domain"), org.NewDomainVerifiedEvent(context.Background(), &agg.Aggregate, "domain"), - user.NewDomainClaimedEvent(context.Background(), &user.NewAggregate("userID1", "org2").Aggregate, "newID@temporary.domain", "username", false), + user.NewDomainClaimedEvent(http.WithRequestedHost(context.Background(), "domain"), &user.NewAggregate("userID1", "org2").Aggregate, "newID@temporary.domain", "username", false), }, }, }, @@ -132,7 +131,7 @@ func TestAddDomain(t *testing.T) { t.Run(tt.name, func(t *testing.T) { AssertValidation( t, - authz.WithRequestedDomain(context.Background(), "domain"), + http.WithRequestedHost(context.Background(), "domain"), (&Commands{idGenerator: tt.args.idGenerator}).prepareAddOrgDomain(tt.args.a, tt.args.domain, tt.args.claimedUserIDs), tt.args.filter, tt.want, @@ -673,7 +672,7 @@ func TestCommandSide_GenerateOrgDomainValidation(t *testing.T) { func TestCommandSide_ValidateOrgDomain(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(*testing.T) *eventstore.Eventstore idGenerator id.Generator secretGenerator crypto.Generator alg crypto.EncryptionAlgorithm @@ -697,9 +696,7 @@ func TestCommandSide_ValidateOrgDomain(t *testing.T) { { name: "invalid domain, error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), }, args: args{ ctx: context.Background(), @@ -716,9 +713,7 @@ func TestCommandSide_ValidateOrgDomain(t *testing.T) { { name: "missing aggregateid, error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), }, args: args{ ctx: context.Background(), @@ -733,8 +728,7 @@ func TestCommandSide_ValidateOrgDomain(t *testing.T) { { name: "domain not exists, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( org.NewOrgAddedEvent(context.Background(), @@ -762,8 +756,7 @@ func TestCommandSide_ValidateOrgDomain(t *testing.T) { { name: "domain already verified, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( org.NewOrgAddedEvent(context.Background(), @@ -803,8 +796,7 @@ func TestCommandSide_ValidateOrgDomain(t *testing.T) { { name: "no code existing, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( org.NewOrgAddedEvent(context.Background(), @@ -838,8 +830,7 @@ func TestCommandSide_ValidateOrgDomain(t *testing.T) { { name: "invalid domain verification, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( org.NewOrgAddedEvent(context.Background(), @@ -894,8 +885,7 @@ func TestCommandSide_ValidateOrgDomain(t *testing.T) { { name: "domain verification, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( org.NewOrgAddedEvent(context.Background(), @@ -952,8 +942,7 @@ func TestCommandSide_ValidateOrgDomain(t *testing.T) { { name: "domain verification, claimed users not found, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( org.NewOrgAddedEvent(context.Background(), @@ -1012,8 +1001,7 @@ func TestCommandSide_ValidateOrgDomain(t *testing.T) { { name: "domain verification, claimed users, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( org.NewOrgAddedEvent(context.Background(), @@ -1067,7 +1055,7 @@ func TestCommandSide_ValidateOrgDomain(t *testing.T) { &org.NewAggregate("org1").Aggregate, "domain.ch", ), - user.NewDomainClaimedEvent(context.Background(), + user.NewDomainClaimedEvent(http.WithRequestedHost(context.Background(), "zitadel.ch"), &user.NewAggregate("user1", "org2").Aggregate, "tempid@temporary.zitadel.ch", "username@domain.ch", @@ -1100,13 +1088,13 @@ func TestCommandSide_ValidateOrgDomain(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), domainVerificationGenerator: tt.fields.secretGenerator, domainVerificationAlg: tt.fields.alg, domainVerificationValidator: tt.fields.domainValidationFunc, idGenerator: tt.fields.idGenerator, } - got, err := r.ValidateOrgDomain(authz.WithRequestedDomain(tt.args.ctx, "zitadel.ch"), tt.args.domain, tt.args.claimedUserIDs) + got, err := r.ValidateOrgDomain(http.WithRequestedHost(tt.args.ctx, "zitadel.ch"), tt.args.domain, tt.args.claimedUserIDs) if tt.res.err == nil { assert.NoError(t, err) } diff --git a/internal/command/org_test.go b/internal/command/org_test.go index 5c69034b5c..cc6a384f21 100644 --- a/internal/command/org_test.go +++ b/internal/command/org_test.go @@ -11,6 +11,7 @@ import ( "golang.org/x/text/language" "github.com/zitadel/zitadel/internal/api/authz" + http_util "github.com/zitadel/zitadel/internal/api/http" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" @@ -65,7 +66,7 @@ func TestAddOrg(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - AssertValidation(t, context.Background(), AddOrgCommand(authz.WithRequestedDomain(context.Background(), "localhost"), tt.args.a, tt.args.name), nil, tt.want) + AssertValidation(t, context.Background(), AddOrgCommand(http_util.WithRequestedHost(context.Background(), "localhost"), tt.args.a, tt.args.name), nil, tt.want) }) } } @@ -229,7 +230,7 @@ func TestCommandSide_AddOrg(t *testing.T) { }, }, args: args{ - ctx: authz.WithRequestedDomain(context.Background(), "iam-domain"), + ctx: http_util.WithRequestedHost(context.Background(), "iam-domain"), name: "Org", userID: "user1", resourceOwner: "org1", @@ -297,7 +298,7 @@ func TestCommandSide_AddOrg(t *testing.T) { }, }, args: args{ - ctx: authz.WithRequestedDomain(context.Background(), "iam-domain"), + ctx: http_util.WithRequestedHost(context.Background(), "iam-domain"), name: "Org", userID: "user1", resourceOwner: "org1", @@ -360,7 +361,7 @@ func TestCommandSide_AddOrg(t *testing.T) { }, }, args: args{ - ctx: authz.WithRequestedDomain(context.Background(), "iam-domain"), + ctx: http_util.WithRequestedHost(context.Background(), "iam-domain"), name: "Org", userID: "user1", resourceOwner: "org1", @@ -431,7 +432,7 @@ func TestCommandSide_AddOrg(t *testing.T) { }, }, args: args{ - ctx: authz.WithRequestedDomain(context.Background(), "iam-domain"), + ctx: http_util.WithRequestedHost(context.Background(), "iam-domain"), name: " Org ", userID: "user1", resourceOwner: "org1", @@ -551,7 +552,7 @@ func TestCommandSide_ChangeOrg(t *testing.T) { ), }, args: args{ - ctx: authz.WithRequestedDomain(context.Background(), "zitadel.ch"), + ctx: http_util.WithRequestedHost(context.Background(), "zitadel.ch"), orgID: "org1", name: " org ", }, @@ -581,7 +582,7 @@ func TestCommandSide_ChangeOrg(t *testing.T) { ), }, args: args{ - ctx: authz.WithRequestedDomain(context.Background(), "zitadel.ch"), + ctx: http_util.WithRequestedHost(context.Background(), "zitadel.ch"), orgID: "org1", name: "neworg", }, @@ -635,7 +636,7 @@ func TestCommandSide_ChangeOrg(t *testing.T) { ), }, args: args{ - ctx: authz.WithRequestedDomain(context.Background(), "zitadel.ch"), + ctx: http_util.WithRequestedHost(context.Background(), "zitadel.ch"), orgID: "org1", name: "neworg", }, @@ -695,7 +696,7 @@ func TestCommandSide_ChangeOrg(t *testing.T) { ), }, args: args{ - ctx: authz.WithRequestedDomain(context.Background(), "zitadel.ch"), + ctx: http_util.WithRequestedHost(context.Background(), "zitadel.ch"), orgID: "org1", name: "neworg", }, @@ -1286,7 +1287,7 @@ func TestCommandSide_SetUpOrg(t *testing.T) { idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "orgID"), }, args: args{ - ctx: authz.WithRequestedDomain(context.Background(), "iam-domain"), + ctx: http_util.WithRequestedHost(context.Background(), "iam-domain"), setupOrg: &OrgSetup{ Name: "", }, @@ -1304,7 +1305,7 @@ func TestCommandSide_SetUpOrg(t *testing.T) { idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "orgID"), }, args: args{ - ctx: authz.WithRequestedDomain(context.Background(), "iam-domain"), + ctx: http_util.WithRequestedHost(context.Background(), "iam-domain"), setupOrg: &OrgSetup{ Name: "Org", Admins: []*OrgSetupAdmin{ @@ -1325,7 +1326,7 @@ func TestCommandSide_SetUpOrg(t *testing.T) { idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "orgID", "userID"), }, args: args{ - ctx: authz.WithRequestedDomain(context.Background(), "iam-domain"), + ctx: http_util.WithRequestedHost(context.Background(), "iam-domain"), setupOrg: &OrgSetup{ Name: "Org", Admins: []*OrgSetupAdmin{ @@ -1418,7 +1419,7 @@ func TestCommandSide_SetUpOrg(t *testing.T) { ), eventFromEventPusher( user.NewHumanInitialCodeAddedEvent( - context.Background(), + http_util.WithRequestedHost(context.Background(), "iam-domain"), &user.NewAggregate("userID", "orgID").Aggregate, &crypto.CryptoValue{ CryptoType: crypto.TypeEncryption, @@ -1441,7 +1442,7 @@ func TestCommandSide_SetUpOrg(t *testing.T) { newCode: mockEncryptedCode("userinit", time.Hour), }, args: args{ - ctx: authz.WithRequestedDomain(context.Background(), "iam-domain"), + ctx: http_util.WithRequestedHost(context.Background(), "iam-domain"), setupOrg: &OrgSetup{ Name: "Org", Admins: []*OrgSetupAdmin{ @@ -1521,7 +1522,7 @@ func TestCommandSide_SetUpOrg(t *testing.T) { idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "orgID"), }, args: args{ - ctx: authz.WithRequestedDomain(context.Background(), "iam-domain"), + ctx: http_util.WithRequestedHost(context.Background(), "iam-domain"), setupOrg: &OrgSetup{ Name: "Org", Admins: []*OrgSetupAdmin{ @@ -1621,7 +1622,7 @@ func TestCommandSide_SetUpOrg(t *testing.T) { keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), }, args: args{ - ctx: authz.WithRequestedDomain(context.Background(), "iam-domain"), + ctx: http_util.WithRequestedHost(context.Background(), "iam-domain"), setupOrg: &OrgSetup{ Name: "Org", Admins: []*OrgSetupAdmin{ diff --git a/internal/command/user.go b/internal/command/user.go index 6d8d10bca3..bb3c75775a 100644 --- a/internal/command/user.go +++ b/internal/command/user.go @@ -7,7 +7,7 @@ import ( "github.com/zitadel/logging" - "github.com/zitadel/zitadel/internal/api/authz" + http_util "github.com/zitadel/zitadel/internal/api/http" "github.com/zitadel/zitadel/internal/command/preparation" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" @@ -277,7 +277,7 @@ func (c *Commands) userDomainClaimed(ctx context.Context, userID string) (events user.NewDomainClaimedEvent( ctx, userAgg, - fmt.Sprintf("%s@temporary.%s", id, authz.GetInstance(ctx).RequestedDomain()), + fmt.Sprintf("%s@temporary.%s", id, http_util.DomainContext(ctx).RequestedDomain()), existingUser.UserName, domainPolicy.UserLoginMustBeDomain), }, changedUserGrant, nil @@ -305,7 +305,7 @@ func (c *Commands) prepareUserDomainClaimed(ctx context.Context, filter preparat return user.NewDomainClaimedEvent( ctx, userAgg, - fmt.Sprintf("%s@temporary.%s", id, authz.GetInstance(ctx).RequestedDomain()), + fmt.Sprintf("%s@temporary.%s", id, http_util.DomainContext(ctx).RequestedDomain()), userWriteModel.UserName, domainPolicy.UserLoginMustBeDomain), nil } diff --git a/internal/command/user_human_otp.go b/internal/command/user_human_otp.go index 8ecd517519..7f587cf9e5 100644 --- a/internal/command/user_human_otp.go +++ b/internal/command/user_human_otp.go @@ -8,6 +8,7 @@ import ( "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/api/authz" + http_util "github.com/zitadel/zitadel/internal/api/http" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" @@ -107,7 +108,7 @@ func (c *Commands) createHumanTOTP(ctx context.Context, userID, resourceOwner st } issuer := c.multifactors.OTP.Issuer if issuer == "" { - issuer = authz.GetInstance(ctx).RequestedDomain() + issuer = http_util.DomainContext(ctx).RequestedDomain() } key, err := domain.NewTOTPKey(issuer, accountName) if err != nil { diff --git a/internal/command/user_human_otp_test.go b/internal/command/user_human_otp_test.go index 806d24dd47..94a47f6dae 100644 --- a/internal/command/user_human_otp_test.go +++ b/internal/command/user_human_otp_test.go @@ -14,6 +14,7 @@ import ( "golang.org/x/text/language" "github.com/zitadel/zitadel/internal/api/authz" + http_util "github.com/zitadel/zitadel/internal/api/http" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" @@ -534,7 +535,7 @@ func TestCommands_createHumanTOTP(t *testing.T) { ), }, args: args{ - ctx: authz.WithRequestedDomain(authz.NewMockContext("instanceID", "org1", "user1"), "zitadel.com"), + ctx: http_util.WithRequestedHost(authz.NewMockContext("instanceID", "org1", "user1"), "zitadel.com"), resourceOwner: "org1", userID: "user1", }, @@ -583,7 +584,7 @@ func TestCommands_createHumanTOTP(t *testing.T) { checkPermission: newMockPermissionCheckAllowed(), }, args: args{ - ctx: authz.WithRequestedDomain(authz.NewMockContext("instanceID", "org1", "user1"), "zitadel.com"), + ctx: http_util.WithRequestedHost(authz.NewMockContext("instanceID", "org1", "user1"), "zitadel.com"), resourceOwner: "org1", userID: "user2", }, diff --git a/internal/command/user_v2_passkey_test.go b/internal/command/user_v2_passkey_test.go index 7b1343b862..5a12cb9dc6 100644 --- a/internal/command/user_v2_passkey_test.go +++ b/internal/command/user_v2_passkey_test.go @@ -12,6 +12,7 @@ import ( "golang.org/x/text/language" "github.com/zitadel/zitadel/internal/api/authz" + http_util "github.com/zitadel/zitadel/internal/api/http" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" @@ -25,7 +26,7 @@ import ( func TestCommands_RegisterUserPasskey(t *testing.T) { ctx := authz.NewMockContextWithPermissions("instance1", "org1", "user1", nil) - ctx = authz.WithRequestedDomain(ctx, "example.com") + ctx = http_util.WithRequestedHost(ctx, "example.com") webauthnConfig := &webauthn_helper.Config{ DisplayName: "test", @@ -129,7 +130,7 @@ func TestCommands_RegisterUserPasskey(t *testing.T) { } func TestCommands_RegisterUserPasskeyWithCode(t *testing.T) { - ctx := authz.WithRequestedDomain(context.Background(), "example.com") + ctx := http_util.WithRequestedHost(context.Background(), "example.com") webauthnConfig := &webauthn_helper.Config{ DisplayName: "test", ExternalSecure: true, @@ -231,7 +232,7 @@ func TestCommands_RegisterUserPasskeyWithCode(t *testing.T) { } func TestCommands_verifyUserPasskeyCode(t *testing.T) { - ctx := authz.WithRequestedDomain(context.Background(), "example.com") + ctx := http_util.WithRequestedHost(context.Background(), "example.com") alg := crypto.CreateMockEncryptionAlg(gomock.NewController(t)) es := eventstoreExpect(t, expectFilter(eventFromEventPusher(testSecretGeneratorAddedEvent(domain.SecretGeneratorTypePasswordlessInitCode))), @@ -339,7 +340,7 @@ func TestCommands_verifyUserPasskeyCode(t *testing.T) { } func TestCommands_pushUserPasskey(t *testing.T) { - ctx := authz.WithRequestedDomain(authz.NewMockContext("instance1", "org1", "user1"), "example.com") + ctx := http_util.WithRequestedHost(authz.NewMockContext("instance1", "org1", "user1"), "example.com") webauthnConfig := &webauthn_helper.Config{ DisplayName: "test", ExternalSecure: true, diff --git a/internal/command/user_v2_u2f_test.go b/internal/command/user_v2_u2f_test.go index b2a5788d00..4d23ac80b9 100644 --- a/internal/command/user_v2_u2f_test.go +++ b/internal/command/user_v2_u2f_test.go @@ -9,6 +9,7 @@ import ( "golang.org/x/text/language" "github.com/zitadel/zitadel/internal/api/authz" + http_util "github.com/zitadel/zitadel/internal/api/http" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/id" @@ -21,7 +22,7 @@ import ( func TestCommands_RegisterUserU2F(t *testing.T) { ctx := authz.NewMockContextWithPermissions("instance1", "org1", "user1", nil) - ctx = authz.WithRequestedDomain(ctx, "example.com") + ctx = http_util.WithRequestedHost(ctx, "example.com") webauthnConfig := &webauthn_helper.Config{ DisplayName: "test", @@ -143,7 +144,7 @@ func TestCommands_RegisterUserU2F(t *testing.T) { } func TestCommands_pushUserU2F(t *testing.T) { - ctx := authz.WithRequestedDomain(authz.NewMockContext("instance1", "org1", "user1"), "example.com") + ctx := http_util.WithRequestedHost(authz.NewMockContext("instance1", "org1", "user1"), "example.com") webauthnConfig := &webauthn_helper.Config{ DisplayName: "test", ExternalSecure: true, diff --git a/internal/integration/integration.go b/internal/integration/integration.go index ebca10d3ed..807f976a32 100644 --- a/internal/integration/integration.go +++ b/internal/integration/integration.go @@ -205,7 +205,7 @@ func (s *Tester) createLoginClient(ctx context.Context) { func (s *Tester) createMachineUser(ctx context.Context, username string, userType UserType) (context.Context, *query.User) { var err error - s.Instance, err = s.Queries.InstanceByHost(ctx, s.Host()) + s.Instance, err = s.Queries.InstanceByHost(ctx, s.Host(), "") logging.OnError(err).Fatal("query instance") ctx = authz.WithInstance(ctx, s.Instance) diff --git a/internal/notification/handlers/origin.go b/internal/notification/handlers/origin.go index 0d12ac3035..8846f5e2dc 100644 --- a/internal/notification/handlers/origin.go +++ b/internal/notification/handlers/origin.go @@ -6,7 +6,6 @@ import ( "github.com/zitadel/logging" - "github.com/zitadel/zitadel/internal/api/authz" http_utils "github.com/zitadel/zitadel/internal/api/http" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/query" @@ -27,11 +26,7 @@ func (n *NotificationQueries) Origin(ctx context.Context, e eventstore.Event) (c origin = originEvent.TriggerOrigin() } if origin != "" { - originURL, err := url.Parse(origin) - if err != nil { - return ctx, err - } - return enrichCtx(ctx, originURL.Hostname(), origin), nil + return enrichCtx(ctx, origin) } primary, err := query.NewInstanceDomainPrimarySearchQuery(true) if err != nil { @@ -48,13 +43,19 @@ func (n *NotificationQueries) Origin(ctx context.Context, e eventstore.Event) (c } return enrichCtx( ctx, - domains.Domains[0].Domain, http_utils.BuildHTTP(domains.Domains[0].Domain, n.externalPort, n.externalSecure), - ), nil + ) } -func enrichCtx(ctx context.Context, host, origin string) context.Context { - ctx = authz.WithRequestedDomain(ctx, host) - ctx = http_utils.WithComposedOrigin(ctx, origin) - return ctx +func enrichCtx(ctx context.Context, origin string) (context.Context, error) { + u, err := url.Parse(origin) + if err != nil { + return nil, err + } + ctx = http_utils.WithDomainContext(ctx, &http_utils.DomainCtx{ + InstanceHost: u.Host, + PublicHost: u.Host, + Protocol: u.Scheme, + }) + return ctx, nil } diff --git a/internal/notification/handlers/user_notifier.go b/internal/notification/handlers/user_notifier.go index fd3acc84aa..066796ae3b 100644 --- a/internal/notification/handlers/user_notifier.go +++ b/internal/notification/handlers/user_notifier.go @@ -491,7 +491,7 @@ func (u *userNotifier) reduceOTPEmail( if err != nil { return nil, err } - url, err := urlTmpl(plainCode, http_util.ComposedOrigin(ctx), notifyUser) + url, err := urlTmpl(plainCode, http_util.DomainContext(ctx).Origin(), notifyUser) if err != nil { return nil, err } diff --git a/internal/notification/types/domain_claimed.go b/internal/notification/types/domain_claimed.go index 975cc74db4..433728392b 100644 --- a/internal/notification/types/domain_claimed.go +++ b/internal/notification/types/domain_claimed.go @@ -11,7 +11,7 @@ import ( ) func (notify Notify) SendDomainClaimed(ctx context.Context, user *query.NotifyUser, username string) error { - url := login.LoginLink(http_utils.ComposedOrigin(ctx), user.ResourceOwner) + url := login.LoginLink(http_utils.DomainContext(ctx).Origin(), user.ResourceOwner) index := strings.LastIndex(user.LastEmail, "@") args := make(map[string]interface{}) args["TempUsername"] = username diff --git a/internal/notification/types/email_verification_code.go b/internal/notification/types/email_verification_code.go index 912e846a92..4ff59137b1 100644 --- a/internal/notification/types/email_verification_code.go +++ b/internal/notification/types/email_verification_code.go @@ -13,7 +13,7 @@ import ( func (notify Notify) SendEmailVerificationCode(ctx context.Context, user *query.NotifyUser, code string, urlTmpl, authRequestID string) error { var url string if urlTmpl == "" { - url = login.MailVerificationLink(http_utils.ComposedOrigin(ctx), user.ID, code, user.ResourceOwner, authRequestID) + url = login.MailVerificationLink(http_utils.DomainContext(ctx).Origin(), user.ID, code, user.ResourceOwner, authRequestID) } else { var buf strings.Builder if err := domain.RenderConfirmURLTemplate(&buf, urlTmpl, user.ID, code, user.ResourceOwner); err != nil { diff --git a/internal/notification/types/email_verification_code_test.go b/internal/notification/types/email_verification_code_test.go index 3f538b3fb6..2196e25b0c 100644 --- a/internal/notification/types/email_verification_code_test.go +++ b/internal/notification/types/email_verification_code_test.go @@ -16,7 +16,7 @@ import ( func TestNotify_SendEmailVerificationCode(t *testing.T) { type args struct { user *query.NotifyUser - origin string + origin *http_utils.DomainCtx code string urlTmpl string authRequestID string @@ -34,7 +34,7 @@ func TestNotify_SendEmailVerificationCode(t *testing.T) { ID: "user1", ResourceOwner: "org1", }, - origin: "https://example.com", + origin: &http_utils.DomainCtx{InstanceHost: "example.com", Protocol: "https"}, code: "123", urlTmpl: "", authRequestID: "authRequestID", @@ -53,7 +53,7 @@ func TestNotify_SendEmailVerificationCode(t *testing.T) { ID: "user1", ResourceOwner: "org1", }, - origin: "https://example.com", + origin: &http_utils.DomainCtx{InstanceHost: "example.com", Protocol: "https"}, code: "123", urlTmpl: "{{", authRequestID: "authRequestID", @@ -68,7 +68,7 @@ func TestNotify_SendEmailVerificationCode(t *testing.T) { ID: "user1", ResourceOwner: "org1", }, - origin: "https://example.com", + origin: &http_utils.DomainCtx{InstanceHost: "example.com", Protocol: "https"}, code: "123", urlTmpl: "https://example.com/email/verify?userID={{.UserID}}&code={{.Code}}&orgID={{.OrgID}}", authRequestID: "authRequestID", @@ -84,7 +84,7 @@ func TestNotify_SendEmailVerificationCode(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, notify := mockNotify() - err := notify.SendEmailVerificationCode(http_utils.WithComposedOrigin(context.Background(), tt.args.origin), tt.args.user, tt.args.code, tt.args.urlTmpl, tt.args.authRequestID) + err := notify.SendEmailVerificationCode(http_utils.WithDomainContext(context.Background(), tt.args.origin), tt.args.user, tt.args.code, tt.args.urlTmpl, tt.args.authRequestID) require.ErrorIs(t, err, tt.wantErr) assert.Equal(t, tt.want, got) }) diff --git a/internal/notification/types/init_code.go b/internal/notification/types/init_code.go index 11ea75ab27..3e38cc284b 100644 --- a/internal/notification/types/init_code.go +++ b/internal/notification/types/init_code.go @@ -10,7 +10,7 @@ import ( ) func (notify Notify) SendUserInitCode(ctx context.Context, user *query.NotifyUser, code, authRequestID string) error { - url := login.InitUserLink(http_utils.ComposedOrigin(ctx), user.ID, user.PreferredLoginName, code, user.ResourceOwner, user.PasswordSet, authRequestID) + url := login.InitUserLink(http_utils.DomainContext(ctx).Origin(), user.ID, user.PreferredLoginName, code, user.ResourceOwner, user.PasswordSet, authRequestID) args := make(map[string]interface{}) args["Code"] = code return notify(url, args, domain.InitCodeMessageType, true) diff --git a/internal/notification/types/otp.go b/internal/notification/types/otp.go index 91befa23fc..3242b2da3d 100644 --- a/internal/notification/types/otp.go +++ b/internal/notification/types/otp.go @@ -4,7 +4,6 @@ import ( "context" "time" - "github.com/zitadel/zitadel/internal/api/authz" http_utils "github.com/zitadel/zitadel/internal/api/http" "github.com/zitadel/zitadel/internal/domain" ) @@ -20,10 +19,11 @@ func (notify Notify) SendOTPEmailCode(ctx context.Context, url, code string, exp } func otpArgs(ctx context.Context, code string, expiry time.Duration) map[string]interface{} { + domainCtx := http_utils.DomainContext(ctx) args := make(map[string]interface{}) args["OTP"] = code - args["Origin"] = http_utils.ComposedOrigin(ctx) - args["Domain"] = authz.GetInstance(ctx).RequestedDomain() + args["Origin"] = domainCtx.Origin() + args["Domain"] = domainCtx.RequestedDomain() args["Expiry"] = expiry return args } diff --git a/internal/notification/types/password_change.go b/internal/notification/types/password_change.go index 61483f0471..8536ac4c04 100644 --- a/internal/notification/types/password_change.go +++ b/internal/notification/types/password_change.go @@ -10,7 +10,7 @@ import ( ) func (notify Notify) SendPasswordChange(ctx context.Context, user *query.NotifyUser) error { - url := console.LoginHintLink(http_utils.ComposedOrigin(ctx), user.PreferredLoginName) + url := console.LoginHintLink(http_utils.DomainContext(ctx).Origin(), user.PreferredLoginName) args := make(map[string]interface{}) return notify(url, args, domain.PasswordChangeMessageType, true) } diff --git a/internal/notification/types/password_code.go b/internal/notification/types/password_code.go index c5616e6e24..40ffee3e6d 100644 --- a/internal/notification/types/password_code.go +++ b/internal/notification/types/password_code.go @@ -13,7 +13,7 @@ import ( func (notify Notify) SendPasswordCode(ctx context.Context, user *query.NotifyUser, code, urlTmpl, authRequestID string) error { var url string if urlTmpl == "" { - url = login.InitPasswordLink(http_utils.ComposedOrigin(ctx), user.ID, code, user.ResourceOwner, authRequestID) + url = login.InitPasswordLink(http_utils.DomainContext(ctx).Origin(), user.ID, code, user.ResourceOwner, authRequestID) } else { var buf strings.Builder if err := domain.RenderConfirmURLTemplate(&buf, urlTmpl, user.ID, code, user.ResourceOwner); err != nil { diff --git a/internal/notification/types/passwordless_registration_link.go b/internal/notification/types/passwordless_registration_link.go index 940aeaec0a..64af1a9797 100644 --- a/internal/notification/types/passwordless_registration_link.go +++ b/internal/notification/types/passwordless_registration_link.go @@ -13,7 +13,7 @@ import ( func (notify Notify) SendPasswordlessRegistrationLink(ctx context.Context, user *query.NotifyUser, code, codeID, urlTmpl string) error { var url string if urlTmpl == "" { - url = domain.PasswordlessInitCodeLink(http_utils.ComposedOrigin(ctx)+login.HandlerPrefix+login.EndpointPasswordlessRegistration, user.ID, user.ResourceOwner, codeID, code) + url = domain.PasswordlessInitCodeLink(http_utils.DomainContext(ctx).Origin()+login.HandlerPrefix+login.EndpointPasswordlessRegistration, user.ID, user.ResourceOwner, codeID, code) } else { var buf strings.Builder if err := domain.RenderPasskeyURLTemplate(&buf, urlTmpl, user.ID, user.ResourceOwner, codeID, code); err != nil { diff --git a/internal/notification/types/passwordless_registration_link_test.go b/internal/notification/types/passwordless_registration_link_test.go index 95fb75603f..0a04b7a0fe 100644 --- a/internal/notification/types/passwordless_registration_link_test.go +++ b/internal/notification/types/passwordless_registration_link_test.go @@ -16,7 +16,7 @@ import ( func TestNotify_SendPasswordlessRegistrationLink(t *testing.T) { type args struct { user *query.NotifyUser - origin string + origin *http_utils.DomainCtx code string codeID string urlTmpl string @@ -34,7 +34,7 @@ func TestNotify_SendPasswordlessRegistrationLink(t *testing.T) { ID: "user1", ResourceOwner: "org1", }, - origin: "https://example.com", + origin: &http_utils.DomainCtx{InstanceHost: "example.com", Protocol: "https"}, code: "123", codeID: "456", urlTmpl: "", @@ -52,7 +52,7 @@ func TestNotify_SendPasswordlessRegistrationLink(t *testing.T) { ID: "user1", ResourceOwner: "org1", }, - origin: "https://example.com", + origin: &http_utils.DomainCtx{InstanceHost: "example.com", Protocol: "https"}, code: "123", codeID: "456", urlTmpl: "{{", @@ -67,7 +67,7 @@ func TestNotify_SendPasswordlessRegistrationLink(t *testing.T) { ID: "user1", ResourceOwner: "org1", }, - origin: "https://example.com", + origin: &http_utils.DomainCtx{InstanceHost: "example.com", Protocol: "https"}, code: "123", codeID: "456", urlTmpl: "https://example.com/passkey/register?userID={{.UserID}}&orgID={{.OrgID}}&codeID={{.CodeID}}&code={{.Code}}", @@ -82,7 +82,7 @@ func TestNotify_SendPasswordlessRegistrationLink(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, notify := mockNotify() - err := notify.SendPasswordlessRegistrationLink(http_utils.WithComposedOrigin(context.Background(), tt.args.origin), tt.args.user, tt.args.code, tt.args.codeID, tt.args.urlTmpl) + err := notify.SendPasswordlessRegistrationLink(http_utils.WithDomainContext(context.Background(), tt.args.origin), tt.args.user, tt.args.code, tt.args.codeID, tt.args.urlTmpl) require.ErrorIs(t, err, tt.wantErr) assert.Equal(t, tt.want, got) }) diff --git a/internal/notification/types/phone_verification_code.go b/internal/notification/types/phone_verification_code.go index 4b583c1e4f..461b85749c 100644 --- a/internal/notification/types/phone_verification_code.go +++ b/internal/notification/types/phone_verification_code.go @@ -3,13 +3,13 @@ package types import ( "context" - "github.com/zitadel/zitadel/internal/api/authz" + http_util "github.com/zitadel/zitadel/internal/api/http" "github.com/zitadel/zitadel/internal/domain" ) func (notify Notify) SendPhoneVerificationCode(ctx context.Context, code string) error { args := make(map[string]interface{}) args["Code"] = code - args["Domain"] = authz.GetInstance(ctx).RequestedDomain() + args["Domain"] = http_util.DomainContext(ctx).RequestedDomain() return notify("", args, domain.VerifyPhoneMessageType, true) } diff --git a/internal/notification/types/templateData.go b/internal/notification/types/templateData.go index 23b7deee70..4a410c0276 100644 --- a/internal/notification/types/templateData.go +++ b/internal/notification/types/templateData.go @@ -13,7 +13,7 @@ import ( ) func GetTemplateData(ctx context.Context, translator *i18n.Translator, translateArgs map[string]interface{}, href, msgType, lang string, policy *query.LabelPolicy) templates.TemplateData { - assetsPrefix := http_util.ComposedOrigin(ctx) + assets.HandlerPrefix + assetsPrefix := http_util.DomainContext(ctx).Origin() + assets.HandlerPrefix templateData := templates.TemplateData{ URL: href, PrimaryColor: templates.DefaultPrimaryColor, diff --git a/internal/query/instance.go b/internal/query/instance.go index 8bc83b2d47..d547ae538c 100644 --- a/internal/query/instance.go +++ b/internal/query/instance.go @@ -195,19 +195,24 @@ var ( instanceByIDQuery string ) -func (q *Queries) InstanceByHost(ctx context.Context, host string) (_ authz.Instance, err error) { +func (q *Queries) InstanceByHost(ctx context.Context, instanceHost, publicHost string) (_ authz.Instance, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { if err != nil { - err = fmt.Errorf("unable to get instance by host %s: %w", host, err) + err = fmt.Errorf("unable to get instance by host: instanceHost %s, publicHost %s: %w", instanceHost, publicHost, err) } span.EndWithError(err) }() - domain := strings.Split(host, ":")[0] // remove possible port - instance, scan := scanAuthzInstance(host, domain) - err = q.client.QueryRowContext(ctx, scan, instanceByDomainQuery, domain) - logging.OnError(err).WithField("host", host).WithField("domain", domain).Warn("instance by host") + instanceDomain := strings.Split(instanceHost, ":")[0] // remove possible port + publicDomain := strings.Split(publicHost, ":")[0] // remove possible port + instance, scan := scanAuthzInstance() + // in case public domain is the same as the instance domain, we do not need to check it + // and can empty it for the check + if instanceDomain == publicDomain { + publicDomain = "" + } + err = q.client.QueryRowContext(ctx, scan, instanceByDomainQuery, instanceDomain, publicDomain) return instance, err } @@ -216,7 +221,7 @@ func (q *Queries) InstanceByID(ctx context.Context) (_ authz.Instance, err error defer func() { span.EndWithError(err) }() instanceID := authz.GetInstance(ctx).InstanceID() - instance, scan := scanAuthzInstance("", "") + instance, scan := scanAuthzInstance() err = q.client.QueryRowContext(ctx, scan, instanceByIDQuery, instanceID) logging.OnError(err).WithField("instance_id", instanceID).Warn("instance by ID") return instance, err @@ -421,8 +426,6 @@ type authzInstance struct { iamProjectID string consoleID string consoleAppID string - host string - domain string defaultLang language.Tag defaultOrgID string csp csp @@ -453,14 +456,6 @@ func (i *authzInstance) ConsoleApplicationID() string { return i.consoleAppID } -func (i *authzInstance) RequestedDomain() string { - return strings.Split(i.host, ":")[0] -} - -func (i *authzInstance) RequestedHost() string { - return i.host -} - func (i *authzInstance) DefaultLanguage() language.Tag { return i.defaultLang } @@ -492,11 +487,8 @@ func (i *authzInstance) Features() feature.Features { return i.features } -func scanAuthzInstance(host, domain string) (*authzInstance, func(row *sql.Row) error) { - instance := &authzInstance{ - host: host, - domain: domain, - } +func scanAuthzInstance() (*authzInstance, func(row *sql.Row) error) { + instance := &authzInstance{} return instance, func(row *sql.Row) error { var ( lang string diff --git a/internal/query/instance_by_domain.sql b/internal/query/instance_by_domain.sql index 0c914df77b..0d0aeeb4f5 100644 --- a/internal/query/instance_by_domain.sql +++ b/internal/query/instance_by_domain.sql @@ -30,6 +30,8 @@ select f.features from domain d join projections.instances i on i.id = d.instance_id +left join projections.instance_trusted_domains td on i.id = td.instance_id left join projections.security_policies2 s on i.id = s.instance_id left join projections.limits l on i.id = l.instance_id -left join features f on i.id = f.instance_id; +left join features f on i.id = f.instance_id +where case when $2 = '' then true else td.domain = $2 end; diff --git a/internal/query/instance_trusted_domain.go b/internal/query/instance_trusted_domain.go new file mode 100644 index 0000000000..2847c3969a --- /dev/null +++ b/internal/query/instance_trusted_domain.go @@ -0,0 +1,142 @@ +package query + +import ( + "context" + "database/sql" + "time" + + sq "github.com/Masterminds/squirrel" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/api/call" + "github.com/zitadel/zitadel/internal/query/projection" + "github.com/zitadel/zitadel/internal/telemetry/tracing" + "github.com/zitadel/zitadel/internal/zerrors" +) + +type InstanceTrustedDomain struct { + CreationDate time.Time + ChangeDate time.Time + Sequence uint64 + Domain string + InstanceID string +} + +type InstanceTrustedDomains struct { + SearchResponse + Domains []*InstanceTrustedDomain +} + +type InstanceTrustedDomainSearchQueries struct { + SearchRequest + Queries []SearchQuery +} + +func (q *InstanceTrustedDomainSearchQueries) toQuery(query sq.SelectBuilder) sq.SelectBuilder { + query = q.SearchRequest.toQuery(query) + for _, q := range q.Queries { + query = q.toQuery(query) + } + return query +} + +func NewInstanceTrustedDomainDomainSearchQuery(method TextComparison, value string) (SearchQuery, error) { + return NewTextQuery(InstanceTrustedDomainDomainCol, value, method) +} + +func (q *Queries) SearchInstanceTrustedDomains(ctx context.Context, queries *InstanceTrustedDomainSearchQueries) (domains *InstanceTrustedDomains, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + query, scan := prepareInstanceTrustedDomainsQuery(ctx, q.client) + stmt, args, err := queries.toQuery(query). + Where(sq.Eq{ + InstanceTrustedDomainInstanceIDCol.identifier(): authz.GetInstance(ctx).InstanceID(), + }).ToSql() + if err != nil { + return nil, zerrors.ThrowInvalidArgument(err, "QUERY-SGrt4", "Errors.Query.SQLStatement") + } + + return q.queryInstanceTrustedDomains(ctx, stmt, scan, args...) +} + +func (q *Queries) queryInstanceTrustedDomains(ctx context.Context, stmt string, scan func(*sql.Rows) (*InstanceTrustedDomains, error), args ...interface{}) (domains *InstanceTrustedDomains, err error) { + err = q.client.QueryContext(ctx, func(rows *sql.Rows) error { + domains, err = scan(rows) + return err + }, stmt, args...) + if err != nil { + return nil, err + } + domains.State, err = q.latestState(ctx, instanceDomainsTable) + return domains, err +} + +func prepareInstanceTrustedDomainsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*InstanceTrustedDomains, error)) { + return sq.Select( + InstanceTrustedDomainCreationDateCol.identifier(), + InstanceTrustedDomainChangeDateCol.identifier(), + InstanceTrustedDomainSequenceCol.identifier(), + InstanceTrustedDomainDomainCol.identifier(), + InstanceTrustedDomainInstanceIDCol.identifier(), + countColumn.identifier(), + ).From(instanceTrustedDomainsTable.identifier() + db.Timetravel(call.Took(ctx))). + PlaceholderFormat(sq.Dollar), + func(rows *sql.Rows) (*InstanceTrustedDomains, error) { + domains := make([]*InstanceTrustedDomain, 0) + var count uint64 + for rows.Next() { + domain := new(InstanceTrustedDomain) + err := rows.Scan( + &domain.CreationDate, + &domain.ChangeDate, + &domain.Sequence, + &domain.Domain, + &domain.InstanceID, + &count, + ) + if err != nil { + return nil, err + } + domains = append(domains, domain) + } + + if err := rows.Close(); err != nil { + return nil, zerrors.ThrowInternal(err, "QUERY-SDg4h", "Errors.Query.CloseRows") + } + + return &InstanceTrustedDomains{ + Domains: domains, + SearchResponse: SearchResponse{ + Count: count, + }, + }, nil + } +} + +var ( + instanceTrustedDomainsTable = table{ + name: projection.InstanceTrustedDomainTable, + instanceIDCol: projection.InstanceTrustedDomainInstanceIDCol, + } + InstanceTrustedDomainCreationDateCol = Column{ + name: projection.InstanceTrustedDomainCreationDateCol, + table: instanceTrustedDomainsTable, + } + InstanceTrustedDomainChangeDateCol = Column{ + name: projection.InstanceTrustedDomainChangeDateCol, + table: instanceTrustedDomainsTable, + } + InstanceTrustedDomainSequenceCol = Column{ + name: projection.InstanceTrustedDomainSequenceCol, + table: instanceTrustedDomainsTable, + } + InstanceTrustedDomainDomainCol = Column{ + name: projection.InstanceTrustedDomainDomainCol, + table: instanceTrustedDomainsTable, + } + InstanceTrustedDomainInstanceIDCol = Column{ + name: projection.InstanceTrustedDomainInstanceIDCol, + table: instanceTrustedDomainsTable, + } +) diff --git a/internal/query/instance_trusted_domain_test.go b/internal/query/instance_trusted_domain_test.go new file mode 100644 index 0000000000..6e3eea027e --- /dev/null +++ b/internal/query/instance_trusted_domain_test.go @@ -0,0 +1,157 @@ +package query + +import ( + "database/sql" + "database/sql/driver" + "errors" + "fmt" + "regexp" + "testing" +) + +var ( + prepareInstanceTrustedDomainsStmt = `SELECT projections.instance_trusted_domains.creation_date,` + + ` projections.instance_trusted_domains.change_date,` + + ` projections.instance_trusted_domains.sequence,` + + ` projections.instance_trusted_domains.domain,` + + ` projections.instance_trusted_domains.instance_id,` + + ` COUNT(*) OVER ()` + + ` FROM projections.instance_trusted_domains` + + ` AS OF SYSTEM TIME '-1 ms'` + prepareInstanceTrustedDomainsCols = []string{ + "creation_date", + "change_date", + "sequence", + "domain", + "instance_id", + "count", + } +) + +func Test_InstanceTrustedDomainPrepares(t *testing.T) { + type want struct { + sqlExpectations sqlExpectation + err checkErr + } + tests := []struct { + name string + prepare interface{} + want want + object interface{} + }{ + { + name: "prepareInstanceTrustedDomainsQuery no result", + prepare: prepareInstanceTrustedDomainsQuery, + want: want{ + sqlExpectations: mockQueries( + regexp.QuoteMeta(prepareInstanceTrustedDomainsStmt), + nil, + nil, + ), + }, + object: &InstanceTrustedDomains{Domains: []*InstanceTrustedDomain{}}, + }, + { + name: "prepareInstanceTrustedDomainsQuery one result", + prepare: prepareInstanceTrustedDomainsQuery, + want: want{ + sqlExpectations: mockQueries( + regexp.QuoteMeta(prepareInstanceTrustedDomainsStmt), + prepareInstanceTrustedDomainsCols, + [][]driver.Value{ + { + testNow, + testNow, + uint64(20211109), + "zitadel.ch", + "inst-id", + }, + }, + ), + }, + object: &InstanceTrustedDomains{ + SearchResponse: SearchResponse{ + Count: 1, + }, + Domains: []*InstanceTrustedDomain{ + { + CreationDate: testNow, + ChangeDate: testNow, + Sequence: 20211109, + Domain: "zitadel.ch", + InstanceID: "inst-id", + }, + }, + }, + }, + { + name: "prepareInstanceTrustedDomainsQuery multiple result", + prepare: prepareInstanceTrustedDomainsQuery, + want: want{ + sqlExpectations: mockQueries( + regexp.QuoteMeta(prepareInstanceTrustedDomainsStmt), + prepareInstanceTrustedDomainsCols, + [][]driver.Value{ + { + testNow, + testNow, + uint64(20211109), + "zitadel.ch", + "inst-id", + }, + { + testNow, + testNow, + uint64(20211109), + "zitadel.com", + "inst-id", + }, + }, + ), + }, + object: &InstanceTrustedDomains{ + SearchResponse: SearchResponse{ + Count: 2, + }, + Domains: []*InstanceTrustedDomain{ + { + CreationDate: testNow, + ChangeDate: testNow, + Sequence: 20211109, + Domain: "zitadel.ch", + InstanceID: "inst-id", + }, + { + CreationDate: testNow, + ChangeDate: testNow, + Sequence: 20211109, + Domain: "zitadel.com", + InstanceID: "inst-id", + }, + }, + }, + }, + { + name: "prepareInstanceTrustedDomainsQuery sql err", + prepare: prepareInstanceTrustedDomainsQuery, + want: want{ + sqlExpectations: mockQueryErr( + regexp.QuoteMeta(prepareInstanceTrustedDomainsStmt), + 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: (*Domains)(nil), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + }) + } +} diff --git a/internal/query/oidc_client.go b/internal/query/oidc_client.go index 6669b398b5..d4364248bb 100644 --- a/internal/query/oidc_client.go +++ b/internal/query/oidc_client.go @@ -8,6 +8,8 @@ import ( "time" "github.com/zitadel/zitadel/internal/api/authz" + http_util "github.com/zitadel/zitadel/internal/api/http" + "github.com/zitadel/zitadel/internal/api/ui/console/path" "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/telemetry/tracing" @@ -56,5 +58,9 @@ func (q *Queries) GetOIDCClientByID(ctx context.Context, clientID string, getKey if err != nil { return nil, zerrors.ThrowInternal(err, "QUERY-ieR7R", "Errors.Internal") } + if authz.GetInstance(ctx).ConsoleClientID() == clientID { + client.RedirectURIs = append(client.RedirectURIs, http_util.DomainContext(ctx).Origin()+path.RedirectPath) + client.PostLogoutRedirectURIs = append(client.PostLogoutRedirectURIs, http_util.DomainContext(ctx).Origin()+path.PostLogoutPath) + } return client, err } diff --git a/internal/query/projection/instance_trusted_domain.go b/internal/query/projection/instance_trusted_domain.go new file mode 100644 index 0000000000..b8c881c171 --- /dev/null +++ b/internal/query/projection/instance_trusted_domain.go @@ -0,0 +1,102 @@ +package projection + +import ( + "context" + + "github.com/zitadel/zitadel/internal/eventstore" + old_handler "github.com/zitadel/zitadel/internal/eventstore/handler" + "github.com/zitadel/zitadel/internal/eventstore/handler/v2" + "github.com/zitadel/zitadel/internal/repository/instance" +) + +const ( + InstanceTrustedDomainTable = "projections.instance_trusted_domains" + + InstanceTrustedDomainInstanceIDCol = "instance_id" + InstanceTrustedDomainCreationDateCol = "creation_date" + InstanceTrustedDomainChangeDateCol = "change_date" + InstanceTrustedDomainSequenceCol = "sequence" + InstanceTrustedDomainDomainCol = "domain" +) + +type instanceTrustedDomainProjection struct{} + +func newInstanceTrustedDomainProjection(ctx context.Context, config handler.Config) *handler.Handler { + return handler.NewHandler(ctx, &config, new(instanceTrustedDomainProjection)) +} + +func (*instanceTrustedDomainProjection) Name() string { + return InstanceTrustedDomainTable +} + +func (*instanceTrustedDomainProjection) Init() *old_handler.Check { + return handler.NewTableCheck( + handler.NewTable([]*handler.InitColumn{ + handler.NewColumn(InstanceTrustedDomainInstanceIDCol, handler.ColumnTypeText), + handler.NewColumn(InstanceTrustedDomainCreationDateCol, handler.ColumnTypeTimestamp), + handler.NewColumn(InstanceTrustedDomainChangeDateCol, handler.ColumnTypeTimestamp), + handler.NewColumn(InstanceTrustedDomainSequenceCol, handler.ColumnTypeInt64), + handler.NewColumn(InstanceTrustedDomainDomainCol, handler.ColumnTypeText), + }, + handler.NewPrimaryKey(InstanceTrustedDomainInstanceIDCol, InstanceTrustedDomainDomainCol), + handler.WithIndex( + handler.NewIndex("instance_trusted_domain", []string{InstanceTrustedDomainDomainCol}, + handler.WithInclude(InstanceTrustedDomainCreationDateCol, InstanceTrustedDomainChangeDateCol, InstanceTrustedDomainSequenceCol), + ), + ), + ), + ) +} + +func (p *instanceTrustedDomainProjection) Reducers() []handler.AggregateReducer { + return []handler.AggregateReducer{ + { + Aggregate: instance.AggregateType, + EventReducers: []handler.EventReducer{ + { + Event: instance.TrustedDomainAddedEventType, + Reduce: p.reduceDomainAdded, + }, + { + Event: instance.TrustedDomainRemovedEventType, + Reduce: p.reduceDomainRemoved, + }, + { + Event: instance.InstanceRemovedEventType, + Reduce: reduceInstanceRemovedHelper(InstanceTrustedDomainInstanceIDCol), + }, + }, + }, + } +} + +func (p *instanceTrustedDomainProjection) reduceDomainAdded(event eventstore.Event) (*handler.Statement, error) { + e, err := assertEvent[*instance.TrustedDomainAddedEvent](event) + if err != nil { + return nil, err + } + return handler.NewCreateStatement( + e, + []handler.Column{ + handler.NewCol(InstanceTrustedDomainCreationDateCol, e.CreatedAt()), + handler.NewCol(InstanceTrustedDomainChangeDateCol, e.CreatedAt()), + handler.NewCol(InstanceTrustedDomainSequenceCol, e.Sequence()), + handler.NewCol(InstanceTrustedDomainDomainCol, e.Domain), + handler.NewCol(InstanceTrustedDomainInstanceIDCol, e.Aggregate().ID), + }, + ), nil +} + +func (p *instanceTrustedDomainProjection) reduceDomainRemoved(event eventstore.Event) (*handler.Statement, error) { + e, err := assertEvent[*instance.TrustedDomainRemovedEvent](event) + if err != nil { + return nil, err + } + return handler.NewDeleteStatement( + e, + []handler.Condition{ + handler.NewCond(InstanceTrustedDomainDomainCol, e.Domain), + handler.NewCond(InstanceTrustedDomainInstanceIDCol, e.Aggregate().ID), + }, + ), nil +} diff --git a/internal/query/projection/instance_trusted_domain_test.go b/internal/query/projection/instance_trusted_domain_test.go new file mode 100644 index 0000000000..d06bb2e2e5 --- /dev/null +++ b/internal/query/projection/instance_trusted_domain_test.go @@ -0,0 +1,119 @@ +package projection + +import ( + "testing" + + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/eventstore/handler/v2" + "github.com/zitadel/zitadel/internal/repository/instance" + "github.com/zitadel/zitadel/internal/zerrors" +) + +func TestInstanceTrustedDomainProjection_reduces(t *testing.T) { + type args struct { + event func(t *testing.T) eventstore.Event + } + tests := []struct { + name string + args args + reduce func(event eventstore.Event) (*handler.Statement, error) + want wantReduce + }{ + { + name: "reduceDomainAdded", + args: args{ + event: getEvent( + testEvent( + instance.TrustedDomainAddedEventType, + instance.AggregateType, + []byte(`{"domain": "domain.new"}`), + ), eventstore.GenericEventMapper[instance.TrustedDomainAddedEvent]), + }, + reduce: (&instanceTrustedDomainProjection{}).reduceDomainAdded, + want: wantReduce{ + aggregateType: eventstore.AggregateType("instance"), + sequence: 15, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "INSERT INTO projections.instance_trusted_domains (creation_date, change_date, sequence, domain, instance_id) VALUES ($1, $2, $3, $4, $5)", + expectedArgs: []interface{}{ + anyArg{}, + anyArg{}, + uint64(15), + "domain.new", + "agg-id", + }, + }, + }, + }, + }, + }, + { + name: "reduceDomainRemoved", + args: args{ + event: getEvent( + testEvent( + instance.TrustedDomainRemovedEventType, + instance.AggregateType, + []byte(`{"domain": "domain.new"}`), + ), eventstore.GenericEventMapper[instance.TrustedDomainRemovedEvent]), + }, + reduce: (&instanceTrustedDomainProjection{}).reduceDomainRemoved, + want: wantReduce{ + aggregateType: eventstore.AggregateType("instance"), + sequence: 15, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "DELETE FROM projections.instance_trusted_domains WHERE (domain = $1) AND (instance_id = $2)", + expectedArgs: []interface{}{ + "domain.new", + "agg-id", + }, + }, + }, + }, + }, + }, + { + name: "instance reduceInstanceRemoved", + args: args{ + event: getEvent( + testEvent( + instance.InstanceRemovedEventType, + instance.AggregateType, + nil, + ), instance.InstanceRemovedEventMapper), + }, + reduce: reduceInstanceRemovedHelper(InstanceTrustedDomainInstanceIDCol), + want: wantReduce{ + aggregateType: eventstore.AggregateType("instance"), + sequence: 15, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "DELETE FROM projections.instance_trusted_domains WHERE (instance_id = $1)", + expectedArgs: []interface{}{ + "agg-id", + }, + }, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + event := baseEvent(t) + got, err := tt.reduce(event) + if ok := zerrors.IsErrorInvalidArgument(err); !ok { + t.Errorf("no wrong event mapping: %v, got: %v", err, got) + } + + event = tt.args.event(t) + got, err = tt.reduce(event) + assertReduce(t, got, err, InstanceTrustedDomainTable, tt.want) + }) + } +} diff --git a/internal/query/projection/projection.go b/internal/query/projection/projection.go index 0cc3abeb04..a7776d24af 100644 --- a/internal/query/projection/projection.go +++ b/internal/query/projection/projection.go @@ -45,6 +45,7 @@ var ( LoginNameProjection *handler.Handler OrgMemberProjection *handler.Handler InstanceDomainProjection *handler.Handler + InstanceTrustedDomainProjection *handler.Handler InstanceMemberProjection *handler.Handler ProjectMemberProjection *handler.Handler ProjectGrantMemberProjection *handler.Handler @@ -132,6 +133,7 @@ func Create(ctx context.Context, sqlClient *database.DB, es handler.EventStore, LoginNameProjection = newLoginNameProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["login_names"])) OrgMemberProjection = newOrgMemberProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["org_members"])) InstanceDomainProjection = newInstanceDomainProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["instance_domains"])) + InstanceTrustedDomainProjection = newInstanceTrustedDomainProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["instance_trusted_domains"])) InstanceMemberProjection = newInstanceMemberProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["iam_members"])) ProjectMemberProjection = newProjectMemberProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["project_members"])) ProjectGrantMemberProjection = newProjectGrantMemberProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["project_grant_members"])) @@ -260,6 +262,7 @@ func newProjectionsList() { LoginNameProjection, OrgMemberProjection, InstanceDomainProjection, + InstanceTrustedDomainProjection, InstanceMemberProjection, ProjectMemberProjection, ProjectGrantMemberProjection, diff --git a/internal/repository/instance/eventstore.go b/internal/repository/instance/eventstore.go index d17dd2aa25..16b7e3967e 100644 --- a/internal/repository/instance/eventstore.go +++ b/internal/repository/instance/eventstore.go @@ -121,4 +121,6 @@ func init() { eventstore.RegisterFilterEventMapper(AggregateType, InstanceRemovedEventType, InstanceRemovedEventMapper) eventstore.RegisterFilterEventMapper(AggregateType, NotificationPolicyAddedEventType, NotificationPolicyAddedEventMapper) eventstore.RegisterFilterEventMapper(AggregateType, NotificationPolicyChangedEventType, NotificationPolicyChangedEventMapper) + eventstore.RegisterFilterEventMapper(AggregateType, TrustedDomainAddedEventType, eventstore.GenericEventMapper[TrustedDomainAddedEvent]) + eventstore.RegisterFilterEventMapper(AggregateType, TrustedDomainRemovedEventType, eventstore.GenericEventMapper[TrustedDomainRemovedEvent]) } diff --git a/internal/repository/instance/trusted_domain.go b/internal/repository/instance/trusted_domain.go new file mode 100644 index 0000000000..29d356d9e9 --- /dev/null +++ b/internal/repository/instance/trusted_domain.go @@ -0,0 +1,95 @@ +package instance + +import ( + "context" + + "github.com/zitadel/zitadel/internal/eventstore" +) + +const ( + trustedDomainPrefix = "trusted_domains." + UniqueTrustedDomain = "trusted_domain" + TrustedDomainAddedEventType = instanceEventTypePrefix + trustedDomainPrefix + "added" + TrustedDomainRemovedEventType = instanceEventTypePrefix + trustedDomainPrefix + "removed" +) + +func NewAddTrustedDomainUniqueConstraint(trustedDomain string) *eventstore.UniqueConstraint { + return eventstore.NewAddEventUniqueConstraint( + UniqueTrustedDomain, + trustedDomain, + "Errors.Instance.Domain.AlreadyExists") +} + +func NewRemoveTrustedDomainUniqueConstraint(trustedDomain string) *eventstore.UniqueConstraint { + return eventstore.NewRemoveUniqueConstraint( + UniqueTrustedDomain, + trustedDomain) +} + +type TrustedDomainAddedEvent struct { + eventstore.BaseEvent `json:"-"` + + Domain string `json:"domain"` +} + +func (e *TrustedDomainAddedEvent) SetBaseEvent(event *eventstore.BaseEvent) { + e.BaseEvent = *event +} + +func NewTrustedDomainAddedEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, + trustedDomain string, +) *TrustedDomainAddedEvent { + event := &TrustedDomainAddedEvent{ + BaseEvent: *eventstore.NewBaseEventForPush( + ctx, + aggregate, + TrustedDomainAddedEventType, + ), + Domain: trustedDomain, + } + return event +} + +func (e *TrustedDomainAddedEvent) Payload() interface{} { + return e +} + +func (e *TrustedDomainAddedEvent) UniqueConstraints() []*eventstore.UniqueConstraint { + return []*eventstore.UniqueConstraint{NewAddTrustedDomainUniqueConstraint(e.Domain)} +} + +type TrustedDomainRemovedEvent struct { + eventstore.BaseEvent `json:"-"` + + Domain string `json:"domain"` +} + +func (e *TrustedDomainRemovedEvent) SetBaseEvent(event *eventstore.BaseEvent) { + e.BaseEvent = *event +} + +func NewTrustedDomainRemovedEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, + trustedDomain string, +) *TrustedDomainRemovedEvent { + event := &TrustedDomainRemovedEvent{ + BaseEvent: *eventstore.NewBaseEventForPush( + ctx, + aggregate, + TrustedDomainRemovedEventType, + ), + Domain: trustedDomain, + } + return event +} + +func (e *TrustedDomainRemovedEvent) Payload() interface{} { + return e +} + +func (e *TrustedDomainRemovedEvent) UniqueConstraints() []*eventstore.UniqueConstraint { + return []*eventstore.UniqueConstraint{NewRemoveTrustedDomainUniqueConstraint(e.Domain)} +} diff --git a/internal/repository/session/session.go b/internal/repository/session/session.go index b8768c6396..3e9b727f5a 100644 --- a/internal/repository/session/session.go +++ b/internal/repository/session/session.go @@ -358,7 +358,7 @@ func NewOTPSMSChallengedEvent( Code: code, Expiry: expiry, CodeReturned: codeReturned, - TriggeredAtOrigin: http.ComposedOrigin(ctx), + TriggeredAtOrigin: http.DomainContext(ctx).Origin(), } } @@ -468,7 +468,7 @@ func NewOTPEmailChallengedEvent( Expiry: expiry, ReturnCode: returnCode, URLTmpl: urlTmpl, - TriggeredAtOrigin: http.ComposedOrigin(ctx), + TriggeredAtOrigin: http.DomainContext(ctx).Origin(), } } diff --git a/internal/repository/user/human.go b/internal/repository/user/human.go index ab5b76d8ae..e9fd49a359 100644 --- a/internal/repository/user/human.go +++ b/internal/repository/user/human.go @@ -278,7 +278,7 @@ func NewHumanInitialCodeAddedEvent( ), Code: code, Expiry: expiry, - TriggeredAtOrigin: http.ComposedOrigin(ctx), + TriggeredAtOrigin: http.DomainContext(ctx).Origin(), AuthRequestID: authRequestID, } } diff --git a/internal/repository/user/human_email.go b/internal/repository/user/human_email.go index 942f101912..34e490323e 100644 --- a/internal/repository/user/human_email.go +++ b/internal/repository/user/human_email.go @@ -171,7 +171,7 @@ func NewHumanEmailCodeAddedEventV2( Expiry: expiry, URLTemplate: urlTemplate, CodeReturned: codeReturned, - TriggeredAtOrigin: http.ComposedOrigin(ctx), + TriggeredAtOrigin: http.DomainContext(ctx).Origin(), AuthRequestID: authRequestID, } } diff --git a/internal/repository/user/human_mfa_otp.go b/internal/repository/user/human_mfa_otp.go index 1eb74f3403..f0f3762c81 100644 --- a/internal/repository/user/human_mfa_otp.go +++ b/internal/repository/user/human_mfa_otp.go @@ -314,7 +314,7 @@ func NewHumanOTPSMSCodeAddedEvent( ), Code: code, Expiry: expiry, - TriggeredAtOrigin: http.ComposedOrigin(ctx), + TriggeredAtOrigin: http.DomainContext(ctx).Origin(), AuthRequestInfo: info, } } @@ -515,7 +515,7 @@ func NewHumanOTPEmailCodeAddedEvent( Code: code, Expiry: expiry, AuthRequestInfo: info, - TriggeredAtOrigin: http.ComposedOrigin(ctx), + TriggeredAtOrigin: http.DomainContext(ctx).Origin(), } } diff --git a/internal/repository/user/human_mfa_passwordless.go b/internal/repository/user/human_mfa_passwordless.go index 603c657497..52264f8d65 100644 --- a/internal/repository/user/human_mfa_passwordless.go +++ b/internal/repository/user/human_mfa_passwordless.go @@ -356,7 +356,7 @@ func NewHumanPasswordlessInitCodeRequestedEvent( Expiry: expiry, URLTemplate: urlTmpl, CodeReturned: codeReturned, - TriggeredAtOrigin: http.ComposedOrigin(ctx), + TriggeredAtOrigin: http.DomainContext(ctx).Origin(), } } diff --git a/internal/repository/user/human_password.go b/internal/repository/user/human_password.go index bf7ba278cd..c425c144b2 100644 --- a/internal/repository/user/human_password.go +++ b/internal/repository/user/human_password.go @@ -62,7 +62,7 @@ func NewHumanPasswordChangedEvent( EncodedHash: encodeHash, ChangeRequired: changeRequired, UserAgentID: userAgentID, - TriggeredAtOrigin: http.ComposedOrigin(ctx), + TriggeredAtOrigin: http.DomainContext(ctx).Origin(), } } @@ -120,7 +120,7 @@ func NewHumanPasswordCodeAddedEvent( Code: code, Expiry: expiry, NotificationType: notificationType, - TriggeredAtOrigin: http.ComposedOrigin(ctx), + TriggeredAtOrigin: http.DomainContext(ctx).Origin(), AuthRequestID: authRequestID, } } @@ -145,7 +145,7 @@ func NewHumanPasswordCodeAddedEventV2( NotificationType: notificationType, URLTemplate: urlTemplate, CodeReturned: codeReturned, - TriggeredAtOrigin: http.ComposedOrigin(ctx), + TriggeredAtOrigin: http.DomainContext(ctx).Origin(), } } diff --git a/internal/repository/user/human_phone.go b/internal/repository/user/human_phone.go index 2b3a1f24d3..5655b1b3d3 100644 --- a/internal/repository/user/human_phone.go +++ b/internal/repository/user/human_phone.go @@ -190,7 +190,7 @@ func NewHumanPhoneCodeAddedEventV2( Code: code, Expiry: expiry, CodeReturned: codeReturned, - TriggeredAtOrigin: http.ComposedOrigin(ctx), + TriggeredAtOrigin: http.DomainContext(ctx).Origin(), } } diff --git a/internal/repository/user/user.go b/internal/repository/user/user.go index a736cb4d2a..e8faddb645 100644 --- a/internal/repository/user/user.go +++ b/internal/repository/user/user.go @@ -430,7 +430,7 @@ func NewDomainClaimedEvent( UserName: userName, oldUserName: oldUserName, userLoginMustBeDomain: userLoginMustBeDomain, - TriggeredAtOrigin: http.ComposedOrigin(ctx), + TriggeredAtOrigin: http.DomainContext(ctx).Origin(), } } diff --git a/internal/webauthn/webauthn.go b/internal/webauthn/webauthn.go index bb13d28bd9..5938f885bc 100644 --- a/internal/webauthn/webauthn.go +++ b/internal/webauthn/webauthn.go @@ -10,7 +10,6 @@ import ( "github.com/go-webauthn/webauthn/webauthn" "github.com/zitadel/logging" - "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/http" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/zerrors" @@ -195,11 +194,11 @@ func (w *Config) serverFromContext(ctx context.Context, id, origin string) (*web } func (w *Config) configFromContext(ctx context.Context) *webauthn.Config { - instance := authz.GetInstance(ctx) + domainCtx := http.DomainContext(ctx) return &webauthn.Config{ RPDisplayName: w.DisplayName, - RPID: instance.RequestedDomain(), - RPOrigins: []string{http.BuildOrigin(instance.RequestedHost(), w.ExternalSecure)}, + RPID: domainCtx.RequestedDomain(), + RPOrigins: []string{domainCtx.Origin()}, } } diff --git a/internal/webauthn/webauthn_test.go b/internal/webauthn/webauthn_test.go index b7c4276aec..b020f802c1 100644 --- a/internal/webauthn/webauthn_test.go +++ b/internal/webauthn/webauthn_test.go @@ -8,7 +8,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/zitadel/zitadel/internal/api/authz" + http_util "github.com/zitadel/zitadel/internal/api/http" "github.com/zitadel/zitadel/internal/zerrors" ) @@ -31,7 +31,11 @@ func TestConfig_serverFromContext(t *testing.T) { }, { name: "success from ctx", - args: args{authz.WithRequestedDomain(context.Background(), "example.com"), "", ""}, + args: args{ + ctx: http_util.WithDomainContext(context.Background(), &http_util.DomainCtx{InstanceHost: "example.com", Protocol: "https"}), + id: "", + origin: "", + }, want: &webauthn.WebAuthn{ Config: &webauthn.Config{ RPDisplayName: "DisplayName", @@ -42,7 +46,11 @@ func TestConfig_serverFromContext(t *testing.T) { }, { name: "success from id", - args: args{authz.WithRequestedDomain(context.Background(), "example.com"), "external.com", "https://external.com"}, + args: args{ + ctx: http_util.WithDomainContext(context.Background(), &http_util.DomainCtx{InstanceHost: "example.com", Protocol: "https"}), + id: "external.com", + origin: "https://external.com", + }, want: &webauthn.WebAuthn{ Config: &webauthn.Config{ RPDisplayName: "DisplayName", diff --git a/proto/zitadel/admin.proto b/proto/zitadel/admin.proto index 8fa9b03226..9fe6ff6434 100644 --- a/proto/zitadel/admin.proto +++ b/proto/zitadel/admin.proto @@ -323,6 +323,54 @@ service AdminService { }; } + rpc ListInstanceTrustedDomains(ListInstanceTrustedDomainsRequest) returns (ListInstanceTrustedDomainsResponse) { + option (google.api.http) = { + post: "/trusted_domains/_search"; + }; + + option (zitadel.v1.auth_option) = { + permission: "iam.read"; + }; + + 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." + }; + } + + rpc AddInstanceTrustedDomain(AddInstanceTrustedDomainRequest) returns (AddInstanceTrustedDomainResponse) { + option (google.api.http) = { + post: "/trusted_domains"; + }; + + option (zitadel.v1.auth_option) = { + permission: "iam.write"; + }; + + 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." + }; + } + + rpc RemoveInstanceTrustedDomain(RemoveInstanceTrustedDomainRequest) returns (RemoveInstanceTrustedDomainResponse) { + option (google.api.http) = { + delete: "/trusted_domains/{domain}"; + }; + + option (zitadel.v1.auth_option) = { + permission: "iam.write"; + }; + + 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." + }; + } + rpc ListSecretGenerators(ListSecretGeneratorsRequest) returns (ListSecretGeneratorsResponse) { option (google.api.http) = { post: "/secretgenerators/_search" @@ -4062,6 +4110,52 @@ message ListInstanceDomainsResponse { repeated zitadel.instance.v1.Domain result = 3; } +message ListInstanceTrustedDomainsRequest { + zitadel.v1.ListQuery query = 1; + // the field the result is sorted + zitadel.instance.v1.DomainFieldName sorting_column = 2; + //criteria the client is looking for + repeated zitadel.instance.v1.TrustedDomainSearchQuery queries = 3; +} + +message ListInstanceTrustedDomainsResponse { + zitadel.v1.ListDetails details = 1; + zitadel.instance.v1.DomainFieldName sorting_column = 2; + repeated zitadel.instance.v1.TrustedDomain result = 3; +} + +message AddInstanceTrustedDomainRequest { + string domain = 1 [ + (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 AddInstanceTrustedDomainResponse { + zitadel.v1.ObjectDetails details = 1; +} + +message RemoveInstanceTrustedDomainRequest { + string domain = 1 [ + (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 RemoveInstanceTrustedDomainResponse { + zitadel.v1.ObjectDetails details = 1; +} + message ListSecretGeneratorsRequest { //list limitations and ordering zitadel.v1.ListQuery query = 1; diff --git a/proto/zitadel/instance.proto b/proto/zitadel/instance.proto index e6fdbd3411..c5ef206ee9 100644 --- a/proto/zitadel/instance.proto +++ b/proto/zitadel/instance.proto @@ -163,3 +163,20 @@ enum DomainFieldName { DOMAIN_FIELD_NAME_GENERATED = 3; DOMAIN_FIELD_NAME_CREATION_DATE = 4; } + +message TrustedDomain { + zitadel.v1.ObjectDetails details = 1; + string domain = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"zitadel.com\"" + } + ]; +} + +message TrustedDomainSearchQuery { + oneof query { + option (validate.required) = true; + + DomainQuery domain_query = 1; + } +} From 4e3fd305abe87ec08d021770c9111a1054e356a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Fri, 2 Aug 2024 11:38:37 +0300 Subject: [PATCH 13/39] fix(crypto): reject decrypted strings with non-UTF8 characters. (#8374) # Which Problems Are Solved We noticed logging where 500: Internal Server errors were returned from the token endpoint, mostly for the `refresh_token` grant. The error was thrown by the database as it received non-UTF8 strings for token IDs Zitadel uses symmetric encryption for opaque tokens, including refresh tokens. Encrypted values are base64 encoded. It appeared to be possible to send garbage base64 to the token endpoint, which will pass decryption and string-splitting. In those cases the resulting ID is not a valid UTF-8 string. Invalid non-UTF8 strings are now rejected during token decryption. # How the Problems Are Solved - `AESCrypto.DecryptString()` checks if the decrypted bytes only contain valid UTF-8 characters before converting them into a string. - `AESCrypto.Decrypt()` is unmodified and still allows decryption on non-UTF8 byte strings. - `FromRefreshToken` now uses `DecryptString` instead of `Decrypt` # Additional Changes - Unit tests added for `FromRefreshToken` and `AESCrypto.DecryptString()`. - Fuzz tests added for `FromRefreshToken` and `AESCrypto.DecryptString()`. This was to pinpoint the problem - Testdata with values that resulted in invalid strings are committed. In the pipeline this results in the Fuzz tests to execute as regular unit-test cases. As we don't use the `-fuzz` flag in the pipeline no further fuzzing is performed. # Additional Context - Closes #7765 - https://go.dev/doc/tutorial/fuzz --- internal/command/oidc_session.go | 2 +- internal/crypto/aes.go | 11 +- internal/crypto/aes_test.go | 109 +++++++++++++-- internal/crypto/crypto.go | 5 + .../8d609af8fa2eb76f | 2 + internal/domain/refresh_token.go | 8 +- internal/domain/refresh_token_test.go | 129 ++++++++++++++++++ .../FuzzFromRefreshToken/576e811604c701eb | 2 + internal/zerrors/invalid_argument.go | 13 +- 9 files changed, 262 insertions(+), 19 deletions(-) create mode 100644 internal/crypto/testdata/fuzz/FuzzAESCrypto_DecryptString/8d609af8fa2eb76f create mode 100644 internal/domain/refresh_token_test.go create mode 100644 internal/domain/testdata/fuzz/FuzzFromRefreshToken/576e811604c701eb diff --git a/internal/command/oidc_session.go b/internal/command/oidc_session.go index fc384d0d30..1fe82198bf 100644 --- a/internal/command/oidc_session.go +++ b/internal/command/oidc_session.go @@ -293,7 +293,7 @@ func (c *Commands) decryptRefreshToken(refreshToken string) (sessionID, refreshT } decrypted, err := c.keyAlgorithm.DecryptString(decoded, c.keyAlgorithm.EncryptionKeyID()) if err != nil { - return "", "", err + return "", "", zerrors.ThrowInvalidArgument(err, "OIDCS-Jei0i", "Errors.User.RefreshToken.Invalid") } return parseRefreshToken(decrypted) } diff --git a/internal/crypto/aes.go b/internal/crypto/aes.go index e943c2ca8e..f57a78fb85 100644 --- a/internal/crypto/aes.go +++ b/internal/crypto/aes.go @@ -6,6 +6,7 @@ import ( "crypto/rand" "encoding/base64" "io" + "unicode/utf8" "github.com/zitadel/zitadel/internal/zerrors" ) @@ -46,15 +47,17 @@ func (a *AESCrypto) Decrypt(value []byte, keyID string) ([]byte, error) { return DecryptAES(value, key) } +// DecryptString decrypts the value using the key identified by keyID. +// When the decrypted value contains non-UTF8 characters an error is returned. func (a *AESCrypto) DecryptString(value []byte, keyID string) (string, error) { - key, err := a.decryptionKey(keyID) + b, err := a.Decrypt(value, keyID) if err != nil { return "", err } - b, err := DecryptAES(value, key) - if err != nil { - return "", err + if !utf8.Valid(b) { + return "", zerrors.ThrowPreconditionFailed(err, "CRYPT-hiCh0", "non-UTF-8 in decrypted string") } + return string(b), nil } diff --git a/internal/crypto/aes_test.go b/internal/crypto/aes_test.go index 5731f320eb..128fd6c4dc 100644 --- a/internal/crypto/aes_test.go +++ b/internal/crypto/aes_test.go @@ -1,18 +1,109 @@ package crypto import ( + "context" + "errors" "testing" + "unicode/utf8" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/zitadel/zitadel/internal/zerrors" ) -// TODO: refactor test style -func TestDecrypt_OK(t *testing.T) { - encryptedpw, err := EncryptAESString("ThisIsMySecretPw", "passphrasewhichneedstobe32bytes!") - assert.NoError(t, err) - - decryptedpw, err := DecryptAESString(encryptedpw, "passphrasewhichneedstobe32bytes!") - assert.NoError(t, err) - - assert.Equal(t, "ThisIsMySecretPw", decryptedpw) +type mockKeyStorage struct { + keys Keys +} + +func (s *mockKeyStorage) ReadKeys() (Keys, error) { + return s.keys, nil +} + +func (s *mockKeyStorage) ReadKey(id string) (*Key, error) { + return &Key{ + ID: id, + Value: s.keys[id], + }, nil +} + +func (*mockKeyStorage) CreateKeys(context.Context, ...*Key) error { + return errors.New("mockKeyStorage.CreateKeys not implemented") +} + +func newTestAESCrypto(t testing.TB) *AESCrypto { + keyConfig := &KeyConfig{ + EncryptionKeyID: "keyID", + DecryptionKeyIDs: []string{"keyID"}, + } + keys := Keys{"keyID": "ThisKeyNeedsToHave32Characters!!"} + aesCrypto, err := NewAESCrypto(keyConfig, &mockKeyStorage{keys: keys}) + require.NoError(t, err) + return aesCrypto +} + +func TestAESCrypto_DecryptString(t *testing.T) { + aesCrypto := newTestAESCrypto(t) + const input = "SecretData" + crypted, err := aesCrypto.Encrypt([]byte(input)) + require.NoError(t, err) + + type args struct { + value []byte + keyID string + } + tests := []struct { + name string + args args + want string + wantErr error + }{ + { + name: "unknown key id error", + args: args{ + value: crypted, + keyID: "foo", + }, + wantErr: zerrors.ThrowNotFound(nil, "CRYPT-nkj1s", "unknown key id"), + }, + { + name: "ok", + args: args{ + value: crypted, + keyID: "keyID", + }, + want: input, + }, + } + for _, tt := range tests { + got, err := aesCrypto.DecryptString(tt.args.value, tt.args.keyID) + require.ErrorIs(t, err, tt.wantErr) + assert.Equal(t, tt.want, got) + } +} + +func FuzzAESCrypto_DecryptString(f *testing.F) { + aesCrypto := newTestAESCrypto(f) + tests := []string{ + " ", + "SecretData", + "FooBar", + "HelloWorld", + } + for _, input := range tests { + tc, err := aesCrypto.Encrypt([]byte(input)) + require.NoError(f, err) + f.Add(tc) + } + f.Fuzz(func(t *testing.T, value []byte) { + got, err := aesCrypto.DecryptString(value, "keyID") + if errors.Is(err, zerrors.ThrowPreconditionFailed(nil, "CRYPT-23kH1", "cipher text block too short")) { + return + } + if errors.Is(err, zerrors.ThrowPreconditionFailed(nil, "CRYPT-hiCh0", "non-UTF-8 in decrypted string")) { + return + } + require.NoError(t, err) + assert.True(t, utf8.ValidString(got), "result is not valid UTF-8") + }) } diff --git a/internal/crypto/crypto.go b/internal/crypto/crypto.go index 2e8e4a71b0..a74f97a054 100644 --- a/internal/crypto/crypto.go +++ b/internal/crypto/crypto.go @@ -19,6 +19,9 @@ type EncryptionAlgorithm interface { DecryptionKeyIDs() []string Encrypt(value []byte) ([]byte, error) Decrypt(hashed []byte, keyID string) ([]byte, error) + + // DecryptString decrypts the value using the key identified by keyID. + // When the decrypted value contains non-UTF8 characters an error is returned. DecryptString(hashed []byte, keyID string) (string, error) } @@ -72,6 +75,8 @@ func Decrypt(value *CryptoValue, alg EncryptionAlgorithm) ([]byte, error) { return alg.Decrypt(value.Crypted, value.KeyID) } +// DecryptString decrypts the value using the key identified by keyID. +// When the decrypted value contains non-UTF8 characters an error is returned. func DecryptString(value *CryptoValue, alg EncryptionAlgorithm) (string, error) { if err := checkEncryptionAlgorithm(value, alg); err != nil { return "", err diff --git a/internal/crypto/testdata/fuzz/FuzzAESCrypto_DecryptString/8d609af8fa2eb76f b/internal/crypto/testdata/fuzz/FuzzAESCrypto_DecryptString/8d609af8fa2eb76f new file mode 100644 index 0000000000..233de8fb25 --- /dev/null +++ b/internal/crypto/testdata/fuzz/FuzzAESCrypto_DecryptString/8d609af8fa2eb76f @@ -0,0 +1,2 @@ +go test fuzz v1 +[]byte("0010120C001010070") diff --git a/internal/domain/refresh_token.go b/internal/domain/refresh_token.go index 6f2d883df5..25ab32f45b 100644 --- a/internal/domain/refresh_token.go +++ b/internal/domain/refresh_token.go @@ -25,13 +25,13 @@ func FromRefreshToken(refreshToken string, algorithm crypto.EncryptionAlgorithm) if err != nil { return "", "", "", zerrors.ThrowInvalidArgument(err, "DOMAIN-BGDhn", "Errors.User.RefreshToken.Invalid") } - decrypted, err := algorithm.Decrypt(decoded, algorithm.EncryptionKeyID()) + decrypted, err := algorithm.DecryptString(decoded, algorithm.EncryptionKeyID()) if err != nil { - return "", "", "", err + return "", "", "", zerrors.ThrowInvalidArgument(err, "DOMAIN-rie9A", "Errors.User.RefreshToken.Invalid") } - split := strings.Split(string(decrypted), ":") + split := strings.Split(decrypted, ":") if len(split) != 3 { - return "", "", "", zerrors.ThrowInvalidArgument(nil, "DOMAIN-BGDhn", "Errors.User.RefreshToken.Invalid") + return "", "", "", zerrors.ThrowInvalidArgument(nil, "DOMAIN-Se8oh", "Errors.User.RefreshToken.Invalid") } return split[0], split[1], split[2], nil } diff --git a/internal/domain/refresh_token_test.go b/internal/domain/refresh_token_test.go new file mode 100644 index 0000000000..e2719bd238 --- /dev/null +++ b/internal/domain/refresh_token_test.go @@ -0,0 +1,129 @@ +package domain + +import ( + "encoding/base64" + "errors" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/net/context" + + "github.com/zitadel/zitadel/internal/crypto" + "github.com/zitadel/zitadel/internal/zerrors" +) + +type mockKeyStorage struct { + keys crypto.Keys +} + +func (s *mockKeyStorage) ReadKeys() (crypto.Keys, error) { + return s.keys, nil +} + +func (s *mockKeyStorage) ReadKey(id string) (*crypto.Key, error) { + return &crypto.Key{ + ID: id, + Value: s.keys[id], + }, nil +} + +func (*mockKeyStorage) CreateKeys(context.Context, ...*crypto.Key) error { + return errors.New("mockKeyStorage.CreateKeys not implemented") +} + +func TestFromRefreshToken(t *testing.T) { + const ( + userID = "userID" + tokenID = "tokenID" + ) + + keyConfig := &crypto.KeyConfig{ + EncryptionKeyID: "keyID", + DecryptionKeyIDs: []string{"keyID"}, + } + keys := crypto.Keys{"keyID": "ThisKeyNeedsToHave32Characters!!"} + algorithm, err := crypto.NewAESCrypto(keyConfig, &mockKeyStorage{keys: keys}) + require.NoError(t, err) + + refreshToken, err := NewRefreshToken(userID, tokenID, algorithm) + require.NoError(t, err) + + invalidRefreshToken, err := algorithm.Encrypt([]byte(userID + ":" + tokenID)) + require.NoError(t, err) + + type args struct { + refreshToken string + algorithm crypto.EncryptionAlgorithm + } + tests := []struct { + name string + args args + wantUserID string + wantTokenID string + wantToken string + wantErr error + }{ + { + name: "invalid base64", + args: args{"~~~", algorithm}, + wantErr: zerrors.ThrowInvalidArgument(nil, "DOMAIN-BGDhn", "Errors.User.RefreshToken.Invalid"), + }, + { + name: "short cipher text", + args: args{"DEADBEEF", algorithm}, + wantErr: zerrors.ThrowInvalidArgument(err, "DOMAIN-rie9A", "Errors.User.RefreshToken.Invalid"), + }, + { + name: "incorrect amount of segments", + args: args{base64.RawURLEncoding.EncodeToString(invalidRefreshToken), algorithm}, + wantErr: zerrors.ThrowInvalidArgument(nil, "DOMAIN-Se8oh", "Errors.User.RefreshToken.Invalid"), + }, + { + name: "success", + args: args{refreshToken, algorithm}, + wantUserID: userID, + wantTokenID: tokenID, + wantToken: tokenID, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotUserID, gotTokenID, gotToken, err := FromRefreshToken(tt.args.refreshToken, tt.args.algorithm) + require.ErrorIs(t, err, tt.wantErr) + assert.Equal(t, tt.wantUserID, gotUserID) + assert.Equal(t, tt.wantTokenID, gotTokenID) + assert.Equal(t, tt.wantToken, gotToken) + }) + } +} + +// Fuzz test invalid inputs. None of the inputs should result in a success. +func FuzzFromRefreshToken(f *testing.F) { + keyConfig := &crypto.KeyConfig{ + EncryptionKeyID: "keyID", + DecryptionKeyIDs: []string{"keyID"}, + } + keys := crypto.Keys{"keyID": "ThisKeyNeedsToHave32Characters!!"} + algorithm, err := crypto.NewAESCrypto(keyConfig, &mockKeyStorage{keys: keys}) + require.NoError(f, err) + + invalidRefreshToken, err := algorithm.Encrypt([]byte("userID:tokenID")) + require.NoError(f, err) + + tests := []string{ + "~~~", // invalid base64 + "DEADBEEF", // short cipher text + base64.RawURLEncoding.EncodeToString(invalidRefreshToken), // incorrect amount of segments + } + for _, tc := range tests { + f.Add(tc) + } + + f.Fuzz(func(t *testing.T, refreshToken string) { + gotUserID, gotTokenID, gotToken, err := FromRefreshToken(refreshToken, algorithm) + target := zerrors.InvalidArgumentError{ZitadelError: new(zerrors.ZitadelError)} + t.Log(gotUserID, gotTokenID, gotToken) + require.ErrorAs(t, err, &target) + }) +} diff --git a/internal/domain/testdata/fuzz/FuzzFromRefreshToken/576e811604c701eb b/internal/domain/testdata/fuzz/FuzzFromRefreshToken/576e811604c701eb new file mode 100644 index 0000000000..0e9296b076 --- /dev/null +++ b/internal/domain/testdata/fuzz/FuzzFromRefreshToken/576e811604c701eb @@ -0,0 +1,2 @@ +go test fuzz v1 +string("0000050000000000000000000000000") diff --git a/internal/zerrors/invalid_argument.go b/internal/zerrors/invalid_argument.go index b2a33fc860..e97519e660 100644 --- a/internal/zerrors/invalid_argument.go +++ b/internal/zerrors/invalid_argument.go @@ -1,6 +1,8 @@ package zerrors -import "fmt" +import ( + "fmt" +) var ( _ InvalidArgument = (*InvalidArgumentError)(nil) @@ -39,6 +41,15 @@ func (err *InvalidArgumentError) Is(target error) bool { return err.ZitadelError.Is(t.ZitadelError) } +func (err *InvalidArgumentError) As(target any) bool { + targetErr, ok := target.(*InvalidArgumentError) + if !ok { + return false + } + *targetErr = *err + return true +} + func (err *InvalidArgumentError) Unwrap() error { return err.ZitadelError } From 1c7c550d605c4237d5fe0304eb4842d65ed864af Mon Sep 17 00:00:00 2001 From: Fuzzbizz Date: Mon, 5 Aug 2024 12:40:29 +0200 Subject: [PATCH 14/39] fix: singular/plural wording (#8381) Simple language fix --- docs/docs/guides/solution-scenarios/b2c.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/guides/solution-scenarios/b2c.mdx b/docs/docs/guides/solution-scenarios/b2c.mdx index 2daacfd82c..ab350fe60d 100644 --- a/docs/docs/guides/solution-scenarios/b2c.mdx +++ b/docs/docs/guides/solution-scenarios/b2c.mdx @@ -52,7 +52,7 @@ When planning your application consider the following questions about User Authe - Do you offer Login via Identity Provider? - Which languages do you have to provide? -When looking at this questions, you may have to admit that building an Identity Management System is much more complex and complicated than you thought initially and implementing if yourself may be too much work. +When looking at these questions, you may have to admit that building an Identity Management System is much more complex and complicated than you thought initially and implementing if yourself may be too much work. Particularly because you should focus building your applications. ### Federation From 31ecbe04ec77b18abcbe2bb4feaaa0d9802b9b27 Mon Sep 17 00:00:00 2001 From: Nico Schett <52858351+schettn@users.noreply.github.com> Date: Mon, 5 Aug 2024 15:56:39 +0200 Subject: [PATCH 15/39] docs: update custom-domain.md (#8367) Co-authored-by: Fabi --- docs/docs/self-hosting/manage/custom-domain.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/self-hosting/manage/custom-domain.md b/docs/docs/self-hosting/manage/custom-domain.md index 65186d558b..233c505a9d 100644 --- a/docs/docs/self-hosting/manage/custom-domain.md +++ b/docs/docs/self-hosting/manage/custom-domain.md @@ -56,7 +56,7 @@ Be aware that you won't automatically have the organizations context when you ac ## Generated Subdomains ZITADEL creates random subdomains for [each new virtual instance](/concepts/structure/instance#multiple-virtual-instances). -You can immediately access the ZITADEL Console an APIs using these subdomains without further actions. +You can immediately access the ZITADEL Console and APIs using these subdomains without further actions. ## More Information From 0f6003f9a1fef5dde6933d7129dd58ca1999bdc4 Mon Sep 17 00:00:00 2001 From: Benjamin Roedell Date: Tue, 6 Aug 2024 03:22:57 -0400 Subject: [PATCH 16/39] docs: Clarify third party apps NOT use embedded view (#8322) # Which Problems Are Solved The text appears to contradict the statement in the page on oauth.net. # How the Problems Are Solved The text has been updated to reflect the statement in the page on oauth.net. # Additional Changes None # Additional Context The page [OAUTH2.0 for mobile and native apps](https://oauth.net/2/native-apps/) linked just above the text that was changed states: > It describes things like not allowing the third-party application to open an embedded web view which is more susceptible to phishing attacks, as well as platform-specific recommendations on how to do so. Co-authored-by: Max Peintner --- docs/docs/examples/login/flutter.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/docs/examples/login/flutter.md b/docs/docs/examples/login/flutter.md index c144ccd0c8..87822a01d6 100644 --- a/docs/docs/examples/login/flutter.md +++ b/docs/docs/examples/login/flutter.md @@ -65,8 +65,8 @@ The [RFC 8252 specification](https://tools.ietf.org/html/rfc8252) defines how Basically, there are two major points in this specification: 1. It recommends to use [PKCE](https://oauth.net/2/pkce/) -2. It does not allow third party apps to open the browser for the login process, - the app must open the login page within the embedded browser view +2. It does not allow third party apps to use an embedded web view for the login process, + the app must open the login page within the default browser First install [http](https://pub.dev/packages/http) a library for making HTTP calls, then [`flutter_web_auth_2`](https://pub.dev/packages/flutter_web_auth_2) and a secure storage to store the auth / refresh tokens [flutter_secure_storage](https://pub.dev/packages/flutter_secure_storage). From 646ffe7a26a99616b95ba7908a4bf6d0c78a0d94 Mon Sep 17 00:00:00 2001 From: Silvan Date: Tue, 6 Aug 2024 13:27:28 +0200 Subject: [PATCH 17/39] fix(fields): await running queries during trigger (#8391) # Which Problems Are Solved During triggering of the fields table WriteTooOld errors can occure when using cockroachdb. # How the Problems Are Solved The statements exclusively lock the projection before they start to insert data by using `FOR UPDATE`. --- internal/eventstore/handler/v2/field_handler.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/internal/eventstore/handler/v2/field_handler.go b/internal/eventstore/handler/v2/field_handler.go index 95ca6dfee6..40e4496e42 100644 --- a/internal/eventstore/handler/v2/field_handler.go +++ b/internal/eventstore/handler/v2/field_handler.go @@ -113,6 +113,8 @@ func (h *FieldHandler) processEvents(ctx context.Context, config *triggerConfig) } }() + // always await currently running transactions + config.awaitRunning = true currentState, err := h.currentState(ctx, tx, config) if err != nil { if errors.Is(err, errJustUpdated) { From eb834c9a35ee6ef38a05811eda6afa654d4fac35 Mon Sep 17 00:00:00 2001 From: Elio Bischof Date: Tue, 6 Aug 2024 15:07:55 +0200 Subject: [PATCH 18/39] fix: update oidc lib (#8393) # Which Problems Are Solved OIDC redirects have wrong headers # How the Problems Are Solved This is fixed with https://github.com/zitadel/oidc/pull/632. This change updates the OIDC lib to a fixed version. --- go.mod | 12 ++++++------ go.sum | 24 ++++++++++++------------ 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/go.mod b/go.mod index 1185ffd963..0d17be8be0 100644 --- a/go.mod +++ b/go.mod @@ -22,7 +22,7 @@ require ( github.com/envoyproxy/protoc-gen-validate v1.0.4 github.com/fatih/color v1.17.0 github.com/gabriel-vasile/mimetype v1.4.4 - github.com/go-jose/go-jose/v4 v4.0.2 + github.com/go-jose/go-jose/v4 v4.0.4 github.com/go-ldap/ldap/v3 v3.4.8 github.com/go-webauthn/webauthn v0.10.2 github.com/gorilla/csrf v1.7.2 @@ -58,7 +58,7 @@ require ( github.com/superseriousbusiness/exifremove v0.0.0-20210330092427-6acd27eac203 github.com/ttacon/libphonenumber v1.2.1 github.com/zitadel/logging v0.6.0 - github.com/zitadel/oidc/v3 v3.25.1 + github.com/zitadel/oidc/v3 v3.26.1 github.com/zitadel/passwap v0.6.0 github.com/zitadel/saml v0.1.3 github.com/zitadel/schema v1.3.0 @@ -73,10 +73,10 @@ require ( go.opentelemetry.io/otel/sdk/metric v1.28.0 go.opentelemetry.io/otel/trace v1.28.0 go.uber.org/mock v0.4.0 - golang.org/x/crypto v0.24.0 + golang.org/x/crypto v0.25.0 golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 golang.org/x/net v0.26.0 - golang.org/x/oauth2 v0.21.0 + golang.org/x/oauth2 v0.22.0 golang.org/x/sync v0.7.0 golang.org/x/text v0.16.0 google.golang.org/api v0.187.0 @@ -92,7 +92,7 @@ require ( github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.48.0 // indirect github.com/bmatcuk/doublestar/v4 v4.6.1 // indirect github.com/crewjam/httperr v0.2.0 // indirect - github.com/go-chi/chi/v5 v5.0.12 // indirect + github.com/go-chi/chi/v5 v5.1.0 // indirect github.com/go-ini/ini v1.67.0 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect @@ -198,7 +198,7 @@ require ( go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 // indirect go.opentelemetry.io/proto/otlp v1.3.1 // indirect - golang.org/x/sys v0.21.0 + golang.org/x/sys v0.22.0 gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 629064c288..ab2038280b 100644 --- a/go.sum +++ b/go.sum @@ -200,8 +200,8 @@ github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= github.com/go-asn1-ber/asn1-ber v1.5.5 h1:MNHlNMBDgEKD4TcKr36vQN68BA00aDfjIt3/bD50WnA= github.com/go-asn1-ber/asn1-ber v1.5.5/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= -github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s= -github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw= +github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= github.com/go-errors/errors v1.0.2/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs= github.com/go-errors/errors v1.1.1/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs= @@ -210,8 +210,8 @@ 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.2 h1:R3l3kkBds16bO7ZFAEEcofK0MkrAJt3jlJznWZG0nvk= -github.com/go-jose/go-jose/v4 v4.0.2/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY= +github.com/go-jose/go-jose/v4 v4.0.4 h1:VsjPI33J0SB9vQM6PLmNjoHqMQNGPiZ0rHL7Ni7Q6/E= +github.com/go-jose/go-jose/v4 v4.0.4/go.mod h1:NKb5HO1EZccyMpiZNbdUw/14tiXNyUJh188dfnMCAfc= 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= @@ -721,8 +721,8 @@ 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/logging v0.6.0 h1:t5Nnt//r+m2ZhhoTmoPX+c96pbMarqJvW1Vq6xFTank= github.com/zitadel/logging v0.6.0/go.mod h1:Y4CyAXHpl3Mig6JOszcV5Rqqsojj+3n7y2F591Mp/ow= -github.com/zitadel/oidc/v3 v3.25.1 h1:mkGimTWzbb8wARUewIqr6LhTPZnZeL6WOeXWy+iz1aI= -github.com/zitadel/oidc/v3 v3.25.1/go.mod h1:UDwD+PRFbUBzabyPd9JORrakty3/wec7VpKZYi9Ahh0= +github.com/zitadel/oidc/v3 v3.26.1 h1:/4wi2gxHByI9YYEjqcwEUx5GjsfDk8reudNP1Cp5Hgo= +github.com/zitadel/oidc/v3 v3.26.1/go.mod h1:ZwBEqSviCpJVZiYashzo53bEGRGXi7amE5Q8PpQg9IM= github.com/zitadel/passwap v0.6.0 h1:m9F3epFC0VkBXu25rihSLGyHvWiNlCzU5kk8RoI+SXQ= github.com/zitadel/passwap v0.6.0/go.mod h1:kqAiJ4I4eZvm3Y6oAk6hlEqlZZOkjMHraGXF90GG7LI= github.com/zitadel/saml v0.1.3 h1:LI4DOCVyyU1qKPkzs3vrGcA5J3H4pH3+CL9zr9ShkpM= @@ -791,8 +791,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= -golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= -golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= +golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= 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= @@ -858,8 +858,8 @@ golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= 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.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs= -golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA= +golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= 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= @@ -913,8 +913,8 @@ golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= +golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 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= From a91e344a6266a9cab4db78c4bd28ddbceb1c01ea Mon Sep 17 00:00:00 2001 From: Nico Schett <52858351+schettn@users.noreply.github.com> Date: Wed, 7 Aug 2024 13:16:57 +0200 Subject: [PATCH 19/39] docs: update pylon.mdx (#8399) # Which Problems Are Solved Add a link to the Pylon website. --- docs/docs/examples/secure-api/pylon.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/docs/examples/secure-api/pylon.mdx b/docs/docs/examples/secure-api/pylon.mdx index a607d12cd8..81f66999af 100644 --- a/docs/docs/examples/secure-api/pylon.mdx +++ b/docs/docs/examples/secure-api/pylon.mdx @@ -8,7 +8,7 @@ import ServiceuserJWT from "../imports/_serviceuser_jwt.mdx"; import ServiceuserRole from "../imports/_serviceuser_role.mdx"; import SetupPylon from "../imports/_setup_pylon.mdx"; -This integration guide demonstrates the recommended way to incorporate ZITADEL into your Pylon service. +This integration guide demonstrates the recommended way to incorporate ZITADEL into your [Pylon](https://pylon.cronit.io) service. It explains how to check the token validity in the API and how to check for permissions. By the end of this guide, your application will have three different endpoint which are public, private(valid token) and private-scoped(valid token with specific role). @@ -301,4 +301,4 @@ curl -H "Authorization: Bearer $TOKEN" -X GET http://localhost:3000/api/me/messa Congratulations! You have successfully integrated your Pylon with ZITADEL! -If you get stuck, consider checking out their [documentation](https://pylon.cronit.io/). If you face issues, contact Pylon or raise an issue on [GitHub](https://github.com/getcronit/pylon/issues). +If you get stuck, consider checking out their [documentation](https://pylon.cronit.io). If you face issues, contact Pylon or raise an issue on [GitHub](https://github.com/getcronit/pylon/issues). From 5adebd552f29b2e93da1986bf400837a0b7a7569 Mon Sep 17 00:00:00 2001 From: Silvan Date: Thu, 8 Aug 2024 09:53:55 +0200 Subject: [PATCH 20/39] test(e2e): wait before select org (#8403) # Which Problems Are Solved The e2e tests fail because the organization selection is too fast. # How the Problems Are Solved Wait until console has loaded properly. # Additional Context - The tests still use the wrong browser, #8404 describes the problem - closes https://github.com/zitadel/zitadel/issues/8378 --- e2e/cypress/e2e/organization/organizations.cy.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/e2e/cypress/e2e/organization/organizations.cy.ts b/e2e/cypress/e2e/organization/organizations.cy.ts index 37e29e7f48..773ac956ee 100644 --- a/e2e/cypress/e2e/organization/organizations.cy.ts +++ b/e2e/cypress/e2e/organization/organizations.cy.ts @@ -25,8 +25,9 @@ describe('organizations', () => { it('should delete an org', () => { cy.visit(orgsPath); + cy.wait(2000); cy.contains('tr', newOrg).click(); - cy.wait(3000); + cy.wait(1000); cy.get('[data-e2e="actions"]').click(); cy.get('[data-e2e="delete"]', { timeout: 1000 }).should('be.visible').click(); cy.get('[data-e2e="confirm-dialog-input"]').focus().clear().type(newOrg); From 7cb814c3ebdeb3828b54aad75f1fdafdd96b424e Mon Sep 17 00:00:00 2001 From: Silvan Date: Thu, 8 Aug 2024 10:05:48 +0200 Subject: [PATCH 21/39] docs: add office hour #4 (#8398) Announces office hours `login UI deepdive` --- MEETING_SCHEDULE.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/MEETING_SCHEDULE.md b/MEETING_SCHEDULE.md index e13d2c6deb..671da36fa1 100644 --- a/MEETING_SCHEDULE.md +++ b/MEETING_SCHEDULE.md @@ -3,6 +3,31 @@ Dear community! We're excited to announce bi-weekly office hours. +## #4 Login UI deepdive + +Dear community, + +We are back from the summer pause with interesting topics. +We will showcase the new Login UI, provide insights into the application's architecture, session API, packages, OIDC middleware, customization options and settings, and offer a look ahead at upcoming features. + +## What to expect: + +* **Architecture of the Login UI**: Explore how server-side and client-side components interact within the new Login UI and NextJS framework. +* **Session API**: Understand the workings of the Session API +* **OIDC middleware configuration**: Learn how OIDC functions with the new Login UI and the necessary steps for a complete flow. +* **Customization Options / Settings**: Discover how to personalize the login and which ZITADEL settings are implemented. +* **Outlook**: Gain insights into future features +* **Q&A** + +## Details: + +* **Target Audience**: Developers using ZITADEL / Contributors +* **Topic**: New login UI and repo +* **Duration**: about 1 hour +* **When**: Wednesday 14th of August 19:00 UTC +* **Platform**: ZITADEL Discrod Server (Join us here: https://discord.gg/zitadel-927474939156643850?event=1270661421952274442) + + ## #2 New Resources and Settings APIs **Shape the future of ZITADEL Let's redesign the API for a better developer experience!** From c6b405ca96519925b5a39242ac503a2b4d1c1b91 Mon Sep 17 00:00:00 2001 From: Silvan Date: Thu, 8 Aug 2024 11:26:22 +0200 Subject: [PATCH 22/39] chore(stable): update to v2.53.9 (#8405) # Which Problems Are Solved Update stable to latest 2.53 --- release-channels.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/release-channels.yaml b/release-channels.yaml index 4fd5200d60..e58f35267a 100644 --- a/release-channels.yaml +++ b/release-channels.yaml @@ -1 +1 @@ -stable: "v2.52.2" +stable: "v2.53.9" From 523d73f6740073a4b4dddf687008add92aba59fb Mon Sep 17 00:00:00 2001 From: Silvan Date: Fri, 9 Aug 2024 11:24:28 +0200 Subject: [PATCH 23/39] fix(fields): use read commit isolation level in trigger (#8410) # Which Problems Are Solved If the processing time of serializable transactions in the fields handler take too long, the next iteration can fail. # How the Problems Are Solved Changed the isolation level of the current states query to Read Commited --- internal/eventstore/handler/v2/field_handler.go | 2 +- internal/eventstore/v3/field.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/eventstore/handler/v2/field_handler.go b/internal/eventstore/handler/v2/field_handler.go index 40e4496e42..8b71f32519 100644 --- a/internal/eventstore/handler/v2/field_handler.go +++ b/internal/eventstore/handler/v2/field_handler.go @@ -97,7 +97,7 @@ func (h *FieldHandler) processEvents(ctx context.Context, config *triggerConfig) defer cancel() } - tx, err := h.client.BeginTx(txCtx, nil) + tx, err := h.client.BeginTx(txCtx, &sql.TxOptions{Isolation: sql.LevelReadCommitted}) if err != nil { return false, err } diff --git a/internal/eventstore/v3/field.go b/internal/eventstore/v3/field.go index 1298e59e42..17037f8bcc 100644 --- a/internal/eventstore/v3/field.go +++ b/internal/eventstore/v3/field.go @@ -28,7 +28,7 @@ func (es *Eventstore) FillFields(ctx context.Context, events ...eventstore.FillF ctx, span := tracing.NewSpan(ctx) defer span.End() - tx, err := es.client.BeginTx(ctx, nil) + tx, err := es.client.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelReadCommitted}) if err != nil { return err } From 2e7235ebf2ade66d39c6f17958618ca73ad99a59 Mon Sep 17 00:00:00 2001 From: Fabi Date: Fri, 9 Aug 2024 15:41:40 +0200 Subject: [PATCH 24/39] fix: change pr template to not link to existing issues and prs (#8412) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Which Problems Are Solved In the PR template we have added some ideas about additional context, but we link to existing prs and issues as an example. So everytime someone doesn't change the description when creating the issue, its a mention to that issue or pr. # How the Problems Are Solved replace with non existing values ![Uploading image.png…]() --- .github/pull_request_template.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 25969473d2..265902feff 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -21,7 +21,7 @@ For example: 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 #123 -- Discussion #456 -- Follow-up for PR #789 -- https://discord.com/channels/123/456 \ No newline at end of file +- Closes #xxx +- Discussion #xxx +- Follow-up for PR #xxx +- https://discord.com/channels/xxx/xxx From 3f25e36fbd634a5c45adfeb6d60d4332de4b3a10 Mon Sep 17 00:00:00 2001 From: Livio Spring Date: Mon, 12 Aug 2024 11:55:07 +0200 Subject: [PATCH 25/39] fix: provide device auth config (#8419) # Which Problems Are Solved There was no default configuration for `DeviceAuth`, which makes it impossible to override by environment variables. Additionally, a custom `CharAmount` value would overwrite also the `DashInterval`. # How the Problems Are Solved - added to defaults.yaml - fixed customization # Additional Changes None. # Additional Context - noticed during a customer request --- cmd/defaults.yaml | 7 +++++++ internal/api/oidc/device_auth.go | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/cmd/defaults.yaml b/cmd/defaults.yaml index 7ee106840c..f757bb4589 100644 --- a/cmd/defaults.yaml +++ b/cmd/defaults.yaml @@ -358,6 +358,13 @@ OIDC: Path: /oauth/v2/keys # ZITADEL_OIDC_CUSTOMENDPOINTS_KEYS_PATH DeviceAuth: Path: /oauth/v2/device_authorization # ZITADEL_OIDC_CUSTOMENDPOINTS_DEVICEAUTH_PATH + DeviceAuth: + Lifetime: 5m # ZITADEL_OIDC_DEVICEAUTH_LIFETIME + PollInterval: 5s # ZITADEL_OIDC_DEVICEAUTH_POLLINTERVAL + UserCode: + CharSet: "BCDFGHJKLMNPQRSTVWXZ" # ZITADEL_OIDC_DEVICEAUTH_USERCODE_CHARSET + CharAmount: 8 # ZITADEL_OIDC_DEVICEAUTH_USERCODE_CHARARMOUNT + DashInterval: 4 # ZITADEL_OIDC_DEVICEAUTH_USERCODE_DASHINTERVAL DefaultLoginURLV2: "/login?authRequest=" # ZITADEL_OIDC_DEFAULTLOGINURLV2 DefaultLogoutURLV2: "/logout?post_logout_redirect=" # ZITADEL_OIDC_DEFAULTLOGOUTURLV2 PublicKeyCacheMaxAge: 24h # ZITADEL_OIDC_PUBLICKEYCACHEMAXAGE diff --git a/internal/api/oidc/device_auth.go b/internal/api/oidc/device_auth.go index 0fe5a7d8c7..a10cba499d 100644 --- a/internal/api/oidc/device_auth.go +++ b/internal/api/oidc/device_auth.go @@ -60,7 +60,7 @@ func (c *DeviceAuthorizationConfig) toOPConfig() op.DeviceAuthorizationConfig { out.UserCode.CharAmount = c.UserCode.CharAmount } if c.UserCode.DashInterval != 0 { - out.UserCode.DashInterval = c.UserCode.CharAmount + out.UserCode.DashInterval = c.UserCode.DashInterval } return out } From cd3ffbd3eb48afeefe50e6f5d1d20445d8cd09d3 Mon Sep 17 00:00:00 2001 From: Silvan Date: Mon, 12 Aug 2024 12:33:45 +0200 Subject: [PATCH 26/39] fix(mirror): use correct statements on push (#8414) # Which Problems Are Solved The mirror command used the wrong position to filter for events if different database technologies for source and destination were used. # How the Problems Are Solved The statements which diverge are stored on the client so that different technologies can use different statements. # Additional Context - https://discord.com/channels/927474939156643850/1256396896243552347 --- cmd/mirror/event_store.go | 2 +- internal/v2/eventstore/postgres/push.go | 6 +++--- internal/v2/eventstore/postgres/push_test.go | 5 ++++- internal/v2/eventstore/postgres/storage.go | 19 ++++++++++--------- 4 files changed, 18 insertions(+), 14 deletions(-) diff --git a/cmd/mirror/event_store.go b/cmd/mirror/event_store.go index 358f878d77..23145bdc37 100644 --- a/cmd/mirror/event_store.go +++ b/cmd/mirror/event_store.go @@ -44,7 +44,7 @@ Migrate only copies events2 and unique constraints`, } func copyEventstore(ctx context.Context, config *Migration) { - sourceClient, err := db.Connect(config.Source, false, dialect.DBPurposeQuery) + sourceClient, err := db.Connect(config.Source, false, dialect.DBPurposeEventPusher) logging.OnError(err).Fatal("unable to connect to source database") defer sourceClient.Close() diff --git a/internal/v2/eventstore/postgres/push.go b/internal/v2/eventstore/postgres/push.go index 0f4c29316c..09f663a086 100644 --- a/internal/v2/eventstore/postgres/push.go +++ b/internal/v2/eventstore/postgres/push.go @@ -71,7 +71,7 @@ func (s *Storage) Push(ctx context.Context, intent *eventstore.PushIntent) (err return err } - return push(ctx, tx, intent, commands) + return s.push(ctx, tx, intent, commands) }) } @@ -144,7 +144,7 @@ func lockAggregates(ctx context.Context, tx *sql.Tx, intent *eventstore.PushInte return res, nil } -func push(ctx context.Context, tx *sql.Tx, reducer eventstore.Reducer, commands []*command) (err error) { +func (s *Storage) push(ctx context.Context, tx *sql.Tx, reducer eventstore.Reducer, commands []*command) (err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() @@ -171,7 +171,7 @@ func push(ctx context.Context, tx *sql.Tx, reducer eventstore.Reducer, commands cmd.position.InPositionOrder, ) - stmt.WriteString(pushPositionStmt) + stmt.WriteString(s.pushPositionStmt) stmt.WriteString(`)`) } stmt.WriteString(` RETURNING created_at, "position"`) diff --git a/internal/v2/eventstore/postgres/push_test.go b/internal/v2/eventstore/postgres/push_test.go index 641b36680d..91fdc1fcd7 100644 --- a/internal/v2/eventstore/postgres/push_test.go +++ b/internal/v2/eventstore/postgres/push_test.go @@ -1297,7 +1297,10 @@ func Test_push(t *testing.T) { t.Errorf("unexpected error in begin: %v", err) t.FailNow() } - err = push(context.Background(), tx, tt.args.reducer, tt.args.commands) + s := Storage{ + pushPositionStmt: initPushStmt("postgres"), + } + err = s.push(context.Background(), tx, tt.args.reducer, tt.args.commands) tt.want.assertErr(t, err) dbMock.Assert(t) if tt.args.reducer != nil { diff --git a/internal/v2/eventstore/postgres/storage.go b/internal/v2/eventstore/postgres/storage.go index c983cf83f7..3a703a7d17 100644 --- a/internal/v2/eventstore/postgres/storage.go +++ b/internal/v2/eventstore/postgres/storage.go @@ -12,13 +12,12 @@ import ( var ( _ eventstore.Pusher = (*Storage)(nil) _ eventstore.Querier = (*Storage)(nil) - - pushPositionStmt string ) type Storage struct { - client *database.DB - config *Config + client *database.DB + config *Config + pushPositionStmt string } type Config struct { @@ -28,19 +27,21 @@ type Config struct { func New(client *database.DB, config *Config) *Storage { initPushStmt(client.Type()) return &Storage{ - client: client, - config: config, + client: client, + config: config, + pushPositionStmt: initPushStmt(client.Type()), } } -func initPushStmt(typ string) { +func initPushStmt(typ string) string { switch typ { case "cockroach": - pushPositionStmt = ", hlc_to_timestamp(cluster_logical_timestamp()), cluster_logical_timestamp()" + return ", hlc_to_timestamp(cluster_logical_timestamp()), cluster_logical_timestamp()" case "postgres": - pushPositionStmt = ", statement_timestamp(), EXTRACT(EPOCH FROM clock_timestamp())" + return ", statement_timestamp(), EXTRACT(EPOCH FROM clock_timestamp())" default: logging.WithFields("database_type", typ).Panic("position statement for type not implemented") + return "" } } From 18c3f574a9d6dcb37cd9f54fa16f799893c389ae Mon Sep 17 00:00:00 2001 From: Fabi Date: Mon, 12 Aug 2024 13:58:49 +0200 Subject: [PATCH 27/39] docs: fix broken links (#8421) # Which Problems Are Solved ^Since publishing the new V2 GA APi, we have a lot of broken links in our docs # How the Problems Are Solved replace api links with v2 links --- docs/docs/apis/introduction.mdx | 10 +++---- .../integrate/login-ui/_list-mfa-options.mdx | 2 +- .../guides/integrate/login-ui/_logout.mdx | 2 +- .../integrate/login-ui/_select-account.mdx | 2 +- .../login-ui/_update_session_webauthn.mdx | 2 +- .../integrate/login-ui/external-login.mdx | 8 +++--- docs/docs/guides/integrate/login-ui/mfa.mdx | 26 +++++++++---------- .../integrate/login-ui/oidc-standard.mdx | 8 +++--- .../guides/integrate/login-ui/passkey.mdx | 8 +++--- .../integrate/login-ui/password-reset.mdx | 4 +-- .../integrate/login-ui/session-validation.mdx | 4 +-- .../integrate/login-ui/typescript-repo.mdx | 4 +-- .../integrate/login-ui/username-password.mdx | 12 ++++----- .../guides/integrate/login/login-users.mdx | 2 +- .../guides/integrate/onboarding/end-users.mdx | 2 +- 15 files changed, 48 insertions(+), 48 deletions(-) diff --git a/docs/docs/apis/introduction.mdx b/docs/docs/apis/introduction.mdx index 443896d508..4facd30ea6 100644 --- a/docs/docs/apis/introduction.mdx +++ b/docs/docs/apis/introduction.mdx @@ -21,7 +21,7 @@ Please refer to our guides how to [authenticate users](/docs/guides/integrate/lo For user authentication on devices with limited accessibility (eg, SmartTV, Smart Watch etc.) use the [device authorization grant](/docs/guides/integrate/login/oidc/device-authorization). -Additionally, you can use the [session API](../apis/resources/session_service/) to authenticate users, for example by building a [custom login UI](/docs/guides/integrate/login-ui/). +Additionally, you can use the [session API](../apis/resources/session_service_v2/) to authenticate users, for example by building a [custom login UI](/docs/guides/integrate/login-ui/). ### Authenticate service users and machines @@ -45,7 +45,7 @@ The [OIDC Playground](/docs/apis/openidoauth/authrequest) is for testing OpenID ### Custom -ZITADEL allows to authenticate users by creating a session with the [Session API](/docs/apis/resources/session_service) or get OIDC authentication request details with the [OIDC service API](/docs/apis/resources/oidc_service). +ZITADEL allows to authenticate users by creating a session with the [Session API](/docs/apis/resources/session_service_v2) or get OIDC authentication request details with the [OIDC service API](/docs/apis/resources/oidc_service). 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) @@ -54,9 +54,9 @@ Refer to our guide to learn how to [build your own login UI](/docs/guides/integr ZITADEL provides APIs for each [core resource](/docs/apis/v2): -- [User](/docs/apis/resources/user_service) -- [Session](/docs/apis/resources/session_service) -- [Settings](/docs/apis/resources/settings_service) +- [User](/docs/apis/resources/user_service_v2) +- [Session](/docs/apis/resources/session_service_v2) +- [Settings](/docs/apis/resources/settings_service_v2) :::info We are migrating to a resource-based API approach. diff --git a/docs/docs/guides/integrate/login-ui/_list-mfa-options.mdx b/docs/docs/guides/integrate/login-ui/_list-mfa-options.mdx index 0872c1cfce..8b62002c3e 100644 --- a/docs/docs/guides/integrate/login-ui/_list-mfa-options.mdx +++ b/docs/docs/guides/integrate/login-ui/_list-mfa-options.mdx @@ -2,7 +2,7 @@ Your user has successfully authenticated, and now you ask them if they want to s When the user starts the configuration, first you want to show them the possible methods. You can either list it implicitly or call the settings service from ZITADEL to get what is configured on the login settings. -More detailed information about the API: [Get Login Settings Documentation](/apis/resources/settings_service/settings-service-get-login-settings) +More detailed information about the API: [Get Login Settings Documentation](/apis/resources/settings_service_v2/settings-service-get-login-settings) Request Example: diff --git a/docs/docs/guides/integrate/login-ui/_logout.mdx b/docs/docs/guides/integrate/login-ui/_logout.mdx index 73e89b84cc..c4d55fd0f9 100644 --- a/docs/docs/guides/integrate/login-ui/_logout.mdx +++ b/docs/docs/guides/integrate/login-ui/_logout.mdx @@ -1,5 +1,5 @@ When your user is done using your application and clicks on the logout button, you have to send a request to the terminate session endpoint. -[Terminate Session Documentation](/docs/apis/resources/session_service/session-service-delete-session) +[Terminate Session Documentation](/docs/apis/resources/session_service_v2/session-service-delete-session) Sessions can be terminated by either: - the authenticated user diff --git a/docs/docs/guides/integrate/login-ui/_select-account.mdx b/docs/docs/guides/integrate/login-ui/_select-account.mdx index 786ecfb713..44df859426 100644 --- a/docs/docs/guides/integrate/login-ui/_select-account.mdx +++ b/docs/docs/guides/integrate/login-ui/_select-account.mdx @@ -3,7 +3,7 @@ If you want to build your own select account/account picker, you have to cache t We recommend storing a list of the session Ids with the corresponding session token in a cookie. The list of session IDs can be sent in the “search sessions” request to get a detailed list of sessions for the account selection. -[Search Sessions Documentation](/docs/apis/resources/session_service/session-service-list-sessions) +[Search Sessions Documentation](/docs/apis/resources/session_service_v2/session-service-list-sessions) ### Request diff --git a/docs/docs/guides/integrate/login-ui/_update_session_webauthn.mdx b/docs/docs/guides/integrate/login-ui/_update_session_webauthn.mdx index 9388687bf9..7a00d006a8 100644 --- a/docs/docs/guides/integrate/login-ui/_update_session_webauthn.mdx +++ b/docs/docs/guides/integrate/login-ui/_update_session_webauthn.mdx @@ -2,7 +2,7 @@ Now that you have successfully authenticated in the browser, you can update the session of the user. Fill the webAuthN checks with the credential assertion data you get from the browser. -More detailed information about the API: [Update Session Documentation](/apis/resources/session_service/session-service-set-session) +More detailed information about the API: [Update Session Documentation](/apis/resources/session_service_v2/session-service-set-session) Example Request: diff --git a/docs/docs/guides/integrate/login-ui/external-login.mdx b/docs/docs/guides/integrate/login-ui/external-login.mdx index b9f847935a..dde966d388 100644 --- a/docs/docs/guides/integrate/login-ui/external-login.mdx +++ b/docs/docs/guides/integrate/login-ui/external-login.mdx @@ -20,7 +20,7 @@ Send the following two URLs in the request body: 2. ErrorURL: Page that should be shown when an error happens during the authentication In the response, you will get an authentication URL of the provider you like. -[Start Identity Provider Intent Documentation](/docs/apis/resources/user_service/user-service-start-identity-provider-intent) +[Start Identity Provider Intent Documentation](/docs/apis/resources/user_service_v2/user-service-start-identity-provider-intent) ### Request @@ -68,7 +68,7 @@ After the user has successfully authenticated, a redirect to the ZITADEL backend ZITADEL will take the information of the provider. After this, a redirect will be made to either the success page in case of a successful login or to the error page in case of a failure will be performed. In the parameters, you will provide the IDP intentID, a token, and optionally, if a user could be found, a user ID. To get the information of the provider, make a request to ZITADEL. -[Retrieve Identity Provider Intent Documentation](/docs/apis/resources/user_service/user-service-retrieve-identity-provider-intent) +[Retrieve Identity Provider Intent Documentation](/docs/apis/resources/user_service_v2/user-service-retrieve-identity-provider-intent) ### Request ```bash @@ -155,7 +155,7 @@ Fill the IdP links in the create user request to add a user with an external log The idpId is the ID of the provider in ZITADEL, the idpExternalId is the ID of the user in the external identity provider; usually, this is sent in the “sub”. The display name is used to list the linkings on the users. -[Create User API Documentation](/docs/apis/resources/user_service/user-service-add-human-user) +[Create User API Documentation](/docs/apis/resources/user_service_v2/user-service-add-human-user) #### Request ```bash @@ -193,7 +193,7 @@ curl --request POST \ If you didn't get a user ID in the parameters to your success page, you know that there is no existing user in ZITADEL with that provider and you can register a new user (read previous section), or link it to an existing account. If you want to link/connect to an existing account you can perform the add identity provider link request. -[Add IDP Link to existing user documentation](/docs/apis/resources/user_service/user-service-add-idp-link) +[Add IDP Link to existing user documentation](/docs/apis/resources/user_service_v2/user-service-add-idp-link) #### Request ```bash diff --git a/docs/docs/guides/integrate/login-ui/mfa.mdx b/docs/docs/guides/integrate/login-ui/mfa.mdx index accf27398d..cea51870a5 100644 --- a/docs/docs/guides/integrate/login-ui/mfa.mdx +++ b/docs/docs/guides/integrate/login-ui/mfa.mdx @@ -35,7 +35,7 @@ To show the user the QR to register TOTP with his Authenticator App like Google/ Generate the QR Code with the URI from the response. For users that do not have a QR Code reader make sure to also show the secret, to enable manual configuration. -More detailed information about the API: [Start TOTP Registration Documentation](/apis/resources/user_service/user-service-register-totp) +More detailed information about the API: [Start TOTP Registration Documentation](/apis/resources/user_service_v2/user-service-register-totp) Request Example: @@ -67,7 +67,7 @@ Response Example: When the user has added the account to the authenticator app, the code from the App has to be entered to finish the registration. This code has to be sent to the verify endpoint in ZITADEL. -More detailed information about the API: [Verify TOTP Documentation](/apis/resources/user_service/user-service-verify-totp-registration) +More detailed information about the API: [Verify TOTP Documentation](/apis/resources/user_service_v2/user-service-verify-totp-registration) Request Example: @@ -93,7 +93,7 @@ curl --request POST \ To be able to check the TOTP you need a session with a checked user. This can either happen before the TOTP check or at the same time. In this example we do two separate requests. So the first step is to create a new Sessions. -More detailed information about the API: [Create new session Documentation](/apis/resources/session_service/session-service-create-session) +More detailed information about the API: [Create new session Documentation](/apis/resources/session_service_v2/session-service-create-session) Example Request @@ -131,7 +131,7 @@ Example Response Now you can show the code field to the user, where the code needs to be entered from the Authenticator App. With that code you have to update the existing session with a totp check. -More detailed information about the API: [Update session Documentation](/apis/resources/session_service/session-service-set-session) +More detailed information about the API: [Update session Documentation](/apis/resources/session_service_v2/session-service-set-session) Example Request ```bash @@ -169,7 +169,7 @@ With an empty returnCode object in the request, ZITADEL will not send the code, If you don't want the user to verify the phone number, you can also create it directly as verified, by sending the isVerified attribute. -More detailed information about the API: [Add phone](/apis/resources/user_service/user-service-set-phone) +More detailed information about the API: [Add phone](/apis/resources/user_service_v2/user-service-set-phone) Example Request: @@ -190,7 +190,7 @@ curl --request POST \ The next step is to show a screen, so the user is able to enter the code for verifying the phone number. Send a verify phone request with the code in the body. -More detailed information about the API: [Verify phone](/apis/resources/user_service/user-service-verify-phone) +More detailed information about the API: [Verify phone](/apis/resources/user_service_v2/user-service-verify-phone) Example Request: ```bash @@ -208,7 +208,7 @@ curl --request POST \ Now that the user has a verified phone number you can enable SMS OTP on the user. -More detailed information about the API: [Add OTP SMS for a user](/apis/resources/user_service/user-service-add-otpsms) +More detailed information about the API: [Add OTP SMS for a user](/apis/resources/user_service_v2/user-service-add-otpsms) Example Request: ```bash @@ -231,7 +231,7 @@ To be able to check the SMS Code you need a session with a checked user. When creating the session you can already start the sms challenge, this will only be executed if the user check was successful. You can tell the challenge, if the code should be returned (returnCode: true) or if ZITADEL should send it (returnCode: false). -More detailed information about the API: [Create new session Documentation](/apis/resources/session_service/session-service-create-session) +More detailed information about the API: [Create new session Documentation](/apis/resources/session_service_v2/session-service-create-session) Example Request @@ -296,7 +296,7 @@ For the Email second factor the already verified email address will be taken. As the user already has a verified E-Mail address you can enable E-Mail OTP on the user. -More detailed information about the API: [Add OTP Email for a user](/apis/resources/user_service/user-service-add-otp-email) +More detailed information about the API: [Add OTP Email for a user](/apis/resources/user_service_v2/user-service-add-otp-email) Example Request: ```bash @@ -319,7 +319,7 @@ To be able to check the Email Code you need a session with a checked user. When creating the session you can already start the sms challenge, this will only be executed if the user check was successful. You can tell the challenge, if the code should be returned (returnCode: true) or if ZITADEL should send it (returnCode: false). -More detailed information about the API: [Create new session Documentation](/apis/resources/session_service/session-service-create-session) +More detailed information about the API: [Create new session Documentation](/apis/resources/session_service_v2/session-service-create-session) Example Request @@ -380,7 +380,7 @@ curl --request PATCH \ The user has selected to setup Universal Second Factor (U2F). To be able to authenticate in the browser you have to start the u2f registration within ZITADEL. -More detailed information about the API: [Start U2F Registration Documentation](/apis/resources/user_service/user-service-register-u-2-f) +More detailed information about the API: [Start U2F Registration Documentation](/apis/resources/user_service_v2/user-service-register-u-2-f) Request Example: @@ -450,7 +450,7 @@ Include the public key credential you got from the browser in your request. You can give the U2F a name, which makes it easier for the user to identify the registered authentication methods. Example: Google Pixel, iCloud Keychain, Yubikey, etc -More detailed information about the API: [Verify U2F Documentation](/apis/resources/user_service/user-service-verify-u-2-f-registration) +More detailed information about the API: [Verify U2F Documentation](/apis/resources/user_service_v2/user-service-verify-u-2-f-registration) Example Request: @@ -491,7 +491,7 @@ In the creat session request you can check for the user and directly initiate th For U2F you can choose between "USER_VERIFICATION_REQUIREMENT_PREFERRED" and "USER_VERIFICATION_REQUIREMENT_DISCOURAGED" for the challenge. Best practice is using discouraged, as this doesn't require the user to enter a PIN. With `preferred` the user might be prompted for the PIN, but it is not necessary. -More detailed information about the API: [Create new session Documentation](/apis/resources/session_service/session-service-create-session) +More detailed information about the API: [Create new session Documentation](/apis/resources/session_service_v2/session-service-create-session) Example Request diff --git a/docs/docs/guides/integrate/login-ui/oidc-standard.mdx b/docs/docs/guides/integrate/login-ui/oidc-standard.mdx index 2f5ba563df..e9bdfb7cbf 100644 --- a/docs/docs/guides/integrate/login-ui/oidc-standard.mdx +++ b/docs/docs/guides/integrate/login-ui/oidc-standard.mdx @@ -51,7 +51,7 @@ The endpoint will redirect you to the domain of your UI on the path /login and a ### Get Auth Request by ID With the ID from the redirect before you will now be able to get the information of the auth request. -[Get Auth Request By ID Documentation](/docs/apis/resources/oidc_service/oidc-service-get-auth-request) +[Get Auth Request By ID Documentation](/docs/apis/resources/oidc_service_v2/oidc-service-get-auth-request) ```bash curl --request GET \ @@ -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/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/session-service-set-session) the user-session. Read the following resources for more information about the different checks: - [Username and Password](./username-password) @@ -95,7 +95,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 [Finalize Auth Request Documentation](/docs/apis/resources/oidc_service/oidc-service-create-callback) +Read more about the [Finalize Auth Request Documentation](/docs/apis/resources/oidc_service_v2/oidc-service-create-callback) Make sure that the authorization header is from the same account that you originally sent in the client id header ```x-zitadel-login-client: ``` on the authorize endpoint. ```bash @@ -145,4 +145,4 @@ In case the ZITADEL backend is not able to determine which session to terminate ```/logout?post_logout_redirect=``` -Prompt the user to select a session, terminate it using the [corresponding endpoint](/docs/apis/resources/session_service/session-service-delete-session) and send the user to the `post_logout_redirect` URL. +Prompt the user to select a session, terminate it using the [corresponding endpoint](/docs/apis/resources/session_service_v2/session-service-delete-session) and send the user to the `post_logout_redirect` URL. diff --git a/docs/docs/guides/integrate/login-ui/passkey.mdx b/docs/docs/guides/integrate/login-ui/passkey.mdx index 112ba207e8..9260b90f74 100644 --- a/docs/docs/guides/integrate/login-ui/passkey.mdx +++ b/docs/docs/guides/integrate/login-ui/passkey.mdx @@ -27,7 +27,7 @@ When you want to send a link to your user, that enables them to register a new p If you asked ZITADEL to send the link to the user please make sure to populate the link with the needed values that point towards your registration UI. -More detailed information about the API: [Send Registration Link Documentation](/apis/resources/user_service/user-service-create-passkey-registration-link) +More detailed information about the API: [Send Registration Link Documentation](/apis/resources/user_service_v2/user-service-create-passkey-registration-link) Request Example: Send either the sendLink or the returnCode (empty message) in the request body, depending on the use case you have. @@ -74,7 +74,7 @@ By specifying the authenticator type you can choose if the passkey should be cro The API response will provide you the public key credential options, this will be used by the browser to obtain a signed challenge. -More detailed information about the API: [Start Passkey Registration Documentation](/apis/resources/user_service/user-service-register-passkey) +More detailed information about the API: [Start Passkey Registration Documentation](/apis/resources/user_service_v2/user-service-register-passkey) Request Example: The code only has to be filled if the user did get a registration code. @@ -179,7 +179,7 @@ Include the public key credential you got from the browser in your request. You can give the Passkey a name, which makes it easier for the user to identify the registered authentication methods. Example: Google Pixel, iCloud Keychain, Yubikey, etc -More detailed information about the API: [Verify Passkey Registration Documentation](/apis/resources/user_service/user-service-verify-passkey-registration) +More detailed information about the API: [Verify Passkey Registration Documentation](/apis/resources/user_service_v2/user-service-verify-passkey-registration) Example Request: @@ -218,7 +218,7 @@ First step is to ask the user for his username and create a new session with the When creating the new session make sure to include the challenge for passkey, resp. webAuthN with a required user verification and the domain of your login UI. The response will include the public key credential request options for the passkey in the challenges. -More detailed information about the API: [Create Session Documentation](/apis/resources/session_service/session-service-create-session) +More detailed information about the API: [Create Session Documentation](/apis/resources/session_service_v2/session-service-create-session) Example Request: ```bash diff --git a/docs/docs/guides/integrate/login-ui/password-reset.mdx b/docs/docs/guides/integrate/login-ui/password-reset.mdx index b5caae38b1..fd5028b2c2 100644 --- a/docs/docs/guides/integrate/login-ui/password-reset.mdx +++ b/docs/docs/guides/integrate/login-ui/password-reset.mdx @@ -16,7 +16,7 @@ The goal is to send the user a verification code, which can be entered to verify There are two possible ways: You can either let ZITADEL send the notification with the verification code, or you can ask ZITADEL for returning the code and send the email by yourself. -[Request Password Reset Documentation](/apis/resources/user_service/user-service-password-reset) +[Request Password Reset Documentation](/apis/resources/user_service_v2/user-service-password-reset) ### ZITADEL sends the verification message @@ -84,7 +84,7 @@ From a user experience perspective it is nice to prefill the verification code, As soon as the user has typed the new password, you can send the change password request. The change password request allows you to set a new password for the user. -[Change Password Documentation](/apis/resources/user_service/user-service-set-password) +[Change Password Documentation](/apis/resources/user_service_v2/user-service-set-password) :::note This request can be used in the password reset flow as well as to let your user change the password manually. diff --git a/docs/docs/guides/integrate/login-ui/session-validation.mdx b/docs/docs/guides/integrate/login-ui/session-validation.mdx index e7aa20957d..969f8f546e 100644 --- a/docs/docs/guides/integrate/login-ui/session-validation.mdx +++ b/docs/docs/guides/integrate/login-ui/session-validation.mdx @@ -46,8 +46,8 @@ If a user successfully authenticated using username and password, a session coul Your application would then need to check whether these `factors` are enough, esp. if the `verifiedAt` of both are within an acceptable time range. -To get the current state of the session, you can call the [GetSession endpoint](/apis/resources/session_service/session-service-get-session), -resp. you can get several by [searching sessions](/apis/resources/session_service/session-service-list-sessions). +To get the current state of the session, you can call the [GetSession endpoint](/apis/resources/session_service_v2/session-service-get-session), +resp. you can get several by [searching sessions](/apis/resources/session_service_v2/session-service-list-sessions). ## Expiration diff --git a/docs/docs/guides/integrate/login-ui/typescript-repo.mdx b/docs/docs/guides/integrate/login-ui/typescript-repo.mdx index 9979fcea7f..6986cf9bb7 100644 --- a/docs/docs/guides/integrate/login-ui/typescript-repo.mdx +++ b/docs/docs/guides/integrate/login-ui/typescript-repo.mdx @@ -43,14 +43,14 @@ _Note that the illustration is just a representation of the architecture and may The flow starts by the `/authorize` endpoint which is intercepted by the login and forwarded to ZITADEL. ZITADEL interprets the request and specifically redirects back to the login app. It does so by redirecting to `/login`. -The login is then able to load an [AuthRequest](/docs/apis/resources/oidc_service/oidc-service-get-auth-request#get-oidc-auth-request-details). +The login is then able to load an [AuthRequest](/docs/apis/resources/oidc_service_v2/oidc-service-get-auth-request#get-oidc-auth-request-details). The Auth Request defines how users proceed to authenticate. If no special prompts or scopes are set, the login brings up the `/loginname` page. The /loginname page allows to enter loginname or email of a user. User discovery is implemented at /api/loginname and if the user is found, they will be redirected to the available authentication method page. Right after the user is found, a session is created and set as cookie. This cookie is then hydrated with more information once the users continues. The OIDC Auth request is always passed in the url to have a context to the ongoing authentication flow. -If enough user information is retrieved and the user is authenticated according to the policies, the flow is finalized by [requesting a the callback url](/docs/apis/resources/oidc_service/oidc-service-create-callback) for the auth request and the user is redirected back to the application. +If enough user information is retrieved and the user is authenticated according to the policies, the flow is finalized by [requesting a the callback url](/docs/apis/resources/oidc_service_v2/oidc-service-create-callback) for the auth request and the user is redirected back to the application. The application can then request a token calling the /token endpoint of the login which is proxied to the ZITADEL API. ### Implemented features diff --git a/docs/docs/guides/integrate/login-ui/username-password.mdx b/docs/docs/guides/integrate/login-ui/username-password.mdx index d6b913ea77..8f796e48c9 100644 --- a/docs/docs/guides/integrate/login-ui/username-password.mdx +++ b/docs/docs/guides/integrate/login-ui/username-password.mdx @@ -10,7 +10,7 @@ sidebar_label: Username and Password ## Register First, we create a new user with a username and password. In the example below we add a user with profile data, a verified email address, and a password. -[Create User Documentation](/apis/resources/user_service/user-service-add-human-user) +[Create User Documentation](/apis/resources/user_service_v2/user-service-add-human-user) ### Custom Fields @@ -88,8 +88,8 @@ Return Code: To check what is allowed on your instance, call the settings service for more information. The following requests can be useful for registration: -- [Get Login Settings](/apis/resources/settings_service/settings-service-get-login-settings) To find out which authentication possibilities are enabled (password, identity provider, etc.) -- [Get Password Complexity Settings](/apis/resources/settings_service/settings-service-get-password-complexity-settings) to find out how the password should look like (length, characters, etc.) +- [Get Login Settings](/apis/resources/settings_service_v2/settings-service-get-login-settings) To find out which authentication possibilities are enabled (password, identity provider, etc.) +- [Get Password Complexity Settings](/apis/resources/settings_service_v2/settings-service-get-password-complexity-settings) to find out how the password should look like (length, characters, etc.) ## Create Session with User Check @@ -103,9 +103,9 @@ The create and update session endpoints will always return a session ID and an o If you do not rely on the OIDC standard you can directly use the token. Send it to the Get Session Endpoint to find out how the user has authenticated. -- [Create new session Documentation](/apis/resources/session_service/session-service-create-session) -- [Update an existing session Documentation](/apis/resources/session_service/session-service-set-session) -- [Get Session Documentation](/apis/resources/session_service/session-service-get-session) +- [Create new session Documentation](/apis/resources/session_service_v2/session-service-create-session) +- [Update an existing session Documentation](/apis/resources/session_service_v2/session-service-set-session) +- [Get Session Documentation](/apis/resources/session_service_v2/session-service-get-session) ### Request diff --git a/docs/docs/guides/integrate/login/login-users.mdx b/docs/docs/guides/integrate/login/login-users.mdx index eccbcb11c7..82c9dbe5a4 100644 --- a/docs/docs/guides/integrate/login/login-users.mdx +++ b/docs/docs/guides/integrate/login/login-users.mdx @@ -55,7 +55,7 @@ There are more [differences between SAML and OIDC](https://zitadel.com/blog/saml ### ZITADEL's Session API -ZITADEL's [Session API](/docs/apis/resources/session_service) provides developers with a straightforward method to manage user sessions within their applications. +ZITADEL's [Session API](/docs/apis/resources/session_service_v2) provides developers with a straightforward method to manage user sessions within their applications. The Session API is not an industry-standard and can be used instead of OpenID Connect or SAML to authenticate users by [building your own custom login user interface](/docs/guides/integrate/login-ui). #### Tokens in the Session API diff --git a/docs/docs/guides/integrate/onboarding/end-users.mdx b/docs/docs/guides/integrate/onboarding/end-users.mdx index f1cf726ab4..4c720be041 100644 --- a/docs/docs/guides/integrate/onboarding/end-users.mdx +++ b/docs/docs/guides/integrate/onboarding/end-users.mdx @@ -119,7 +119,7 @@ You can find all the guides here: [Build your own login UI](/docs/guides/integra #### Custom fields -The [create user request](/docs/apis/resources/user_service/user-service-add-human-user) also allows you to add [metadata](/docs/guides/manage/customize/user-metadata) (key, value) to the user. +The [create user request](/docs/apis/resources/user_service_v2/user-service-add-human-user) also allows you to add [metadata](/docs/guides/manage/customize/user-metadata) (key, value) to the user. This gives you the possibility to collect additional data from your users during the registration process and store it directly to the user in ZITADEL. Those metadata can also directly be included in the [token](/docs/guides/manage/customize/user-metadata#use-tokens-to-get-user-metadata) of the user. We recommend storing business relevant data in the database of your application, and only authentication and authorization relevant data in ZITADEL to follow the separation of concern pattern. From 042c438813855fa506b2bb1bbda3efa41ccdf475 Mon Sep 17 00:00:00 2001 From: Elio Bischof Date: Mon, 12 Aug 2024 22:32:01 +0200 Subject: [PATCH 28/39] feat(v3alpha): read actions (#8357) # Which Problems Are Solved The current v3alpha actions APIs don't exactly adhere to the [new resources API design](https://zitadel.com/docs/apis/v3#standard-resources). # How the Problems Are Solved - **Improved ID access**: The aggregate ID is added to the resource details object, so accessing resource IDs and constructing proto messages for resources is easier - **Explicit Instances**: Optionally, the instance can be explicitly given in each request - **Pagination**: A default search limit and a max search limit are added to the defaults.yaml. They apply to the new v3 APIs (currently only actions). The search query defaults are changed to ascending by creation date, because this makes the pagination results the most deterministic. The creation date is also added to the object details. The bug with updated creation dates is fixed for executions and targets. - **Removed Sequences**: Removed Sequence from object details and ProcessedSequence from search details # Additional Changes Object details IDs are checked in unit test only if an empty ID is expected. Centralizing the details check also makes this internal object more flexible for future evolutions. # Additional Context - Closes #8169 - Depends on https://github.com/zitadel/zitadel/pull/8225 --------- Co-authored-by: Silvan Co-authored-by: Stefan Benz <46600784+stebenz@users.noreply.github.com> --- cmd/defaults.yaml | 10 +- cmd/start/start.go | 2 +- docs/docs/apis/_v3_action_service.proto | 4 +- docs/docs/apis/_v3_idp_service.proto | 2 +- internal/api/authz/instance.go | 2 +- .../resources/action/v3alpha/execution.go | 19 +- .../v3alpha/execution_integration_test.go | 185 ++-- .../execution_target_integration_test.go | 335 +++++++ .../grpc/resources/action/v3alpha/query.go | 410 ++++++++ .../action/v3alpha/query_integration_test.go | 898 ++++++++++++++++++ .../grpc/resources/action/v3alpha/server.go | 6 +- .../action/v3alpha/server_integration_test.go | 25 +- .../grpc/resources/action/v3alpha/target.go | 31 +- .../action/v3alpha/target_integration_test.go | 113 +-- .../resources/object/v3alpha/converter.go | 70 +- .../server/middleware/instance_interceptor.go | 62 +- .../middleware/instance_interceptor_test.go | 146 ++- internal/api/grpc/server/server.go | 1 - internal/api/grpc/system/instance.go | 7 - .../middleware/instance_interceptor_test.go | 2 +- internal/command/action_v2_execution_test.go | 331 ++++++- internal/command/action_v2_target.go | 2 - internal/command/action_v2_target_test.go | 12 +- internal/command/auth_request_test.go | 4 +- internal/command/converter.go | 1 + internal/command/device_auth_test.go | 6 +- internal/command/idp_intent_test.go | 2 +- .../instance_custom_login_text_test.go | 2 +- .../instance_custom_message_text_test.go | 2 +- .../instance_debug_notification_file_test.go | 6 +- .../instance_debug_notification_log_test.go | 6 +- internal/command/instance_domain_test.go | 6 +- internal/command/instance_features_test.go | 2 +- internal/command/instance_idp_test.go | 50 +- internal/command/instance_member_test.go | 2 +- .../command/instance_oidc_settings_test.go | 4 +- .../command/instance_policy_domain_test.go | 4 +- .../command/instance_policy_label_test.go | 22 +- .../command/instance_policy_login_test.go | 12 +- .../instance_policy_notification_test.go | 4 +- .../instance_policy_password_age_test.go | 2 +- ...nstance_policy_password_complexity_test.go | 2 +- .../instance_policy_password_lockout_test.go | 2 +- .../command/instance_policy_privacy_test.go | 2 +- internal/command/instance_settings_test.go | 6 +- internal/command/instance_test.go | 4 +- .../command/instance_trusted_domain_test.go | 4 +- internal/command/limits_test.go | 11 +- internal/command/org_action_test.go | 10 +- .../command/org_custom_login_text_test.go | 2 +- .../command/org_custom_message_text_test.go | 2 +- internal/command/org_domain_test.go | 8 +- internal/command/org_flow_test.go | 4 +- internal/command/org_idp_config_test.go | 2 +- internal/command/org_idp_test.go | 50 +- internal/command/org_member_test.go | 2 +- internal/command/org_metadata_test.go | 6 +- internal/command/org_policy_domain_test.go | 6 +- internal/command/org_policy_label_test.go | 20 +- internal/command/org_policy_login_test.go | 8 +- .../command/org_policy_notification_test.go | 6 +- .../command/org_policy_password_age_test.go | 2 +- .../org_policy_password_complexity_test.go | 2 +- internal/command/org_policy_privacy_test.go | 2 +- internal/command/preparation_test.go | 17 + internal/command/project_application_test.go | 8 +- internal/command/project_grant_member_test.go | 2 +- internal/command/project_grant_test.go | 6 +- internal/command/project_member_test.go | 2 +- internal/command/project_role_test.go | 4 +- internal/command/project_test.go | 6 +- internal/command/quota_test.go | 6 +- internal/command/restrictions_test.go | 2 +- internal/command/session_test.go | 2 +- internal/command/sms_config_test.go | 10 +- internal/command/smtp_test.go | 12 +- internal/command/system_features_test.go | 2 +- internal/command/user_grant_test.go | 6 +- internal/command/user_human_avatar_test.go | 4 +- internal/command/user_human_email_test.go | 4 +- internal/command/user_human_init_test.go | 2 +- internal/command/user_human_otp_test.go | 14 +- internal/command/user_human_password_test.go | 8 +- internal/command/user_human_phone_test.go | 6 +- .../command/user_human_refresh_token_test.go | 2 +- internal/command/user_idp_link_test.go | 2 +- internal/command/user_machine_key_test.go | 2 +- internal/command/user_machine_secret_test.go | 4 +- internal/command/user_machine_test.go | 4 +- internal/command/user_metadata_test.go | 6 +- .../user_personal_access_token_test.go | 4 +- internal/command/user_schema_test.go | 10 +- internal/command/user_test.go | 14 +- internal/command/user_v2_email_test.go | 2 +- internal/command/user_v2_human_test.go | 4 +- internal/command/user_v2_passkey_test.go | 22 +- internal/command/user_v2_password_test.go | 2 +- internal/command/user_v2_test.go | 10 +- .../config/systemdefaults/system_defaults.go | 2 + internal/domain/object.go | 9 +- internal/integration/assert.go | 47 +- internal/integration/client.go | 2 +- internal/integration/integration.go | 20 +- internal/query/execution.go | 22 +- internal/query/execution_test.go | 32 +- internal/query/instance.go | 8 +- internal/query/projection/target.go | 2 +- internal/query/target.go | 17 +- internal/query/target_test.go | 38 +- internal/static/i18n/bg.yaml | 1 + internal/static/i18n/cs.yaml | 1 + internal/static/i18n/de.yaml | 1 + internal/static/i18n/en.yaml | 2 + internal/static/i18n/es.yaml | 1 + internal/static/i18n/fr.yaml | 1 + internal/static/i18n/it.yaml | 1 + internal/static/i18n/ja.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/ru.yaml | 1 + internal/static/i18n/sv.yaml | 1 + internal/static/i18n/zh.yaml | 1 + proto/zitadel/object/v3alpha/object.proto | 7 + .../action/v3alpha/action_service.proto | 219 ++++- .../resources/action/v3alpha/execution.proto | 6 + .../resources/action/v3alpha/query.proto | 117 +++ .../resources/action/v3alpha/target.proto | 29 +- .../resources/object/v3alpha/object.proto | 79 +- 130 files changed, 3253 insertions(+), 605 deletions(-) create mode 100644 internal/api/grpc/resources/action/v3alpha/execution_target_integration_test.go create mode 100644 internal/api/grpc/resources/action/v3alpha/query.go create mode 100644 internal/api/grpc/resources/action/v3alpha/query_integration_test.go create mode 100644 proto/zitadel/resources/action/v3alpha/query.proto diff --git a/cmd/defaults.yaml b/cmd/defaults.yaml index f757bb4589..6afbaddbd7 100644 --- a/cmd/defaults.yaml +++ b/cmd/defaults.yaml @@ -547,6 +547,10 @@ SystemDefaults: PublicKeyLifetime: 30h # ZITADEL_SYSTEMDEFAULTS_KEYCONFIG_PUBLICKEYLIFETIME # 8766h are 1 year CertificateLifetime: 8766h # ZITADEL_SYSTEMDEFAULTS_KEYCONFIG_CERTIFICATELIFETIME + # DefaultQueryLimit limits the number of items that can be queried in a single v3 API search request without explicitly passing a limit. + 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 Actions: HTTP: @@ -1056,8 +1060,10 @@ InternalAuthZ: - "events.read" - "milestones.read" - "session.delete" + - "action.target.read" - "action.target.write" - "action.target.delete" + - "action.execution.read" - "action.execution.write" - "userschema.read" - "userschema.write" @@ -1092,8 +1098,8 @@ InternalAuthZ: - "project.grant.member.read" - "events.read" - "milestones.read" - - "execution.target.read" - - "execution.read" + - "action.target.read" + - "action.execution.read" - "userschema.read" - Role: "IAM_ORG_MANAGER" Permissions: diff --git a/cmd/start/start.go b/cmd/start/start.go index 1a65ea3f24..d542e8be62 100644 --- a/cmd/start/start.go +++ b/cmd/start/start.go @@ -436,7 +436,7 @@ func startAPIs( if err := apis.RegisterService(ctx, feature_v2.CreateServer(commands, queries)); err != nil { return nil, err } - if err := apis.RegisterService(ctx, action_v3_alpha.CreateServer(commands, queries, domain.AllFunctions, apis.ListGrpcMethods, apis.ListGrpcServices)); err != nil { + if err := apis.RegisterService(ctx, action_v3_alpha.CreateServer(config.SystemDefaults, commands, queries, domain.AllFunctions, apis.ListGrpcMethods, apis.ListGrpcServices)); err != nil { return nil, err } if err := apis.RegisterService(ctx, user_schema_v3_alpha.CreateServer(commands, queries)); err != nil { diff --git a/docs/docs/apis/_v3_action_service.proto b/docs/docs/apis/_v3_action_service.proto index 5bb2b658c1..1e62ddd7d7 100644 --- a/docs/docs/apis/_v3_action_service.proto +++ b/docs/docs/apis/_v3_action_service.proto @@ -466,7 +466,7 @@ message DeleteTargetResponse { message SearchTargetsRequest { // list limitations and ordering. - zitadel.resources.object.v3alpha.ListQuery query = 2; + zitadel.resources.object.v3alpha.SearchQuery query = 2; // the field the result is sorted. zitadel.resources.action.v3alpha.TargetFieldName sorting_column = 3 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -526,7 +526,7 @@ message DeleteExecutionResponse { message SearchExecutionsRequest { // list limitations and ordering. - zitadel.resources.object.v3alpha.ListQuery query = 1; + zitadel.resources.object.v3alpha.SearchQuery query = 1; // Define the criteria to query for. repeated zitadel.resources.action.v3alpha.ExecutionSearchFilter filters = 2; } diff --git a/docs/docs/apis/_v3_idp_service.proto b/docs/docs/apis/_v3_idp_service.proto index 3a33d407e6..26f734f68d 100644 --- a/docs/docs/apis/_v3_idp_service.proto +++ b/docs/docs/apis/_v3_idp_service.proto @@ -292,7 +292,7 @@ message DeleteIDPResponse { message SearchIDPsRequest { optional zitadel.object.v3alpha.RequestContext ctx = 1; // list limitations and ordering. - zitadel.resources.object.v3alpha.ListQuery query = 2; + zitadel.resources.object.v3alpha.SearchQuery query = 2; // the field the result is sorted. zitadel.resources.idp.v3alpha.IDPFieldName sorting_column = 3 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { diff --git a/internal/api/authz/instance.go b/internal/api/authz/instance.go index 9610283a59..8721a75f3a 100644 --- a/internal/api/authz/instance.go +++ b/internal/api/authz/instance.go @@ -29,7 +29,7 @@ type Instance interface { type InstanceVerifier interface { InstanceByHost(ctx context.Context, host, publicDomain string) (Instance, error) - InstanceByID(ctx context.Context) (Instance, error) + InstanceByID(ctx context.Context, id string) (Instance, error) } type instance struct { diff --git a/internal/api/grpc/resources/action/v3alpha/execution.go b/internal/api/grpc/resources/action/v3alpha/execution.go index 668b6b5261..794827970b 100644 --- a/internal/api/grpc/resources/action/v3alpha/execution.go +++ b/internal/api/grpc/resources/action/v3alpha/execution.go @@ -4,7 +4,7 @@ import ( "context" "github.com/zitadel/zitadel/internal/api/authz" - settings_object "github.com/zitadel/zitadel/internal/api/grpc/settings/object/v3alpha" + resource_object "github.com/zitadel/zitadel/internal/api/grpc/resources/object/v3alpha" "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/repository/execution" @@ -14,7 +14,7 @@ import ( ) func (s *Server) SetExecution(ctx context.Context, req *action.SetExecutionRequest) (*action.SetExecutionResponse, error) { - if err := checkExecutionEnabled(ctx); err != nil { + if err := checkActionsEnabled(ctx); err != nil { return nil, err } reqTargets := req.GetExecution().GetTargets() @@ -34,24 +34,21 @@ func (s *Server) SetExecution(ctx context.Context, req *action.SetExecutionReque set := &command.SetExecution{ Targets: targets, } - owner := &object.Owner{ - Type: object.OwnerType_OWNER_TYPE_INSTANCE, - Id: authz.GetInstance(ctx).InstanceID(), - } var err error var details *domain.ObjectDetails + instanceID := authz.GetInstance(ctx).InstanceID() switch t := req.GetCondition().GetConditionType().(type) { case *action.Condition_Request: cond := executionConditionFromRequest(t.Request) - details, err = s.command.SetExecutionRequest(ctx, cond, set, owner.Id) + details, err = s.command.SetExecutionRequest(ctx, cond, set, instanceID) case *action.Condition_Response: cond := executionConditionFromResponse(t.Response) - details, err = s.command.SetExecutionResponse(ctx, cond, set, owner.Id) + details, err = s.command.SetExecutionResponse(ctx, cond, set, instanceID) case *action.Condition_Event: cond := executionConditionFromEvent(t.Event) - details, err = s.command.SetExecutionEvent(ctx, cond, set, owner.Id) + details, err = s.command.SetExecutionEvent(ctx, cond, set, instanceID) case *action.Condition_Function: - details, err = s.command.SetExecutionFunction(ctx, command.ExecutionFunctionCondition(t.Function.GetName()), set, owner.Id) + details, err = s.command.SetExecutionFunction(ctx, command.ExecutionFunctionCondition(t.Function.GetName()), set, instanceID) default: err = zerrors.ThrowInvalidArgument(nil, "ACTION-5r5Ju", "Errors.Execution.ConditionInvalid") } @@ -59,7 +56,7 @@ func (s *Server) SetExecution(ctx context.Context, req *action.SetExecutionReque return nil, err } return &action.SetExecutionResponse{ - Details: settings_object.DomainToDetailsPb(details, owner), + Details: resource_object.DomainToDetailsPb(details, object.OwnerType_OWNER_TYPE_INSTANCE, instanceID), }, nil } diff --git a/internal/api/grpc/resources/action/v3alpha/execution_integration_test.go b/internal/api/grpc/resources/action/v3alpha/execution_integration_test.go index 9713a3c578..3056d450c6 100644 --- a/internal/api/grpc/resources/action/v3alpha/execution_integration_test.go +++ b/internal/api/grpc/resources/action/v3alpha/execution_integration_test.go @@ -13,7 +13,7 @@ import ( "github.com/zitadel/zitadel/internal/integration" object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha" action "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha" - settings_object "github.com/zitadel/zitadel/pkg/grpc/settings/object/v3alpha" + resource_object "github.com/zitadel/zitadel/pkg/grpc/resources/object/v3alpha" ) func executionTargetsSingleTarget(id string) []*action.ExecutionTargetType { @@ -25,8 +25,9 @@ func executionTargetsSingleInclude(include *action.Condition) []*action.Executio } func TestServer_SetExecution_Request(t *testing.T) { - ensureFeatureEnabled(t) - targetResp := Tester.CreateTarget(CTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false) + _, instanceID, _, isolatedIAMOwnerCTX := Tester.UseIsolatedInstance(t, IAMOwnerCTX, SystemCTX) + ensureFeatureEnabled(t, isolatedIAMOwnerCTX) + targetResp := Tester.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false) tests := []struct { name string @@ -51,7 +52,7 @@ func TestServer_SetExecution_Request(t *testing.T) { }, { name: "no condition, error", - ctx: CTX, + ctx: isolatedIAMOwnerCTX, req: &action.SetExecutionRequest{ Condition: &action.Condition{ ConditionType: &action.Condition_Request{ @@ -66,7 +67,7 @@ func TestServer_SetExecution_Request(t *testing.T) { }, { name: "method, not existing", - ctx: CTX, + ctx: isolatedIAMOwnerCTX, req: &action.SetExecutionRequest{ Condition: &action.Condition{ ConditionType: &action.Condition_Request{ @@ -85,7 +86,7 @@ func TestServer_SetExecution_Request(t *testing.T) { }, { name: "method, ok", - ctx: CTX, + ctx: isolatedIAMOwnerCTX, req: &action.SetExecutionRequest{ Condition: &action.Condition{ ConditionType: &action.Condition_Request{ @@ -101,18 +102,18 @@ func TestServer_SetExecution_Request(t *testing.T) { }, }, want: &action.SetExecutionResponse{ - Details: &settings_object.Details{ - ChangeDate: timestamppb.Now(), + Details: &resource_object.Details{ + Changed: timestamppb.Now(), Owner: &object.Owner{ Type: object.OwnerType_OWNER_TYPE_INSTANCE, - Id: Tester.Instance.InstanceID(), + Id: instanceID, }, }, }, }, { name: "service, not existing", - ctx: CTX, + ctx: isolatedIAMOwnerCTX, req: &action.SetExecutionRequest{ Condition: &action.Condition{ ConditionType: &action.Condition_Request{ @@ -131,7 +132,7 @@ func TestServer_SetExecution_Request(t *testing.T) { }, { name: "service, ok", - ctx: CTX, + ctx: isolatedIAMOwnerCTX, req: &action.SetExecutionRequest{ Condition: &action.Condition{ ConditionType: &action.Condition_Request{ @@ -147,18 +148,18 @@ func TestServer_SetExecution_Request(t *testing.T) { }, }, want: &action.SetExecutionResponse{ - Details: &settings_object.Details{ - ChangeDate: timestamppb.Now(), + Details: &resource_object.Details{ + Changed: timestamppb.Now(), Owner: &object.Owner{ Type: object.OwnerType_OWNER_TYPE_INSTANCE, - Id: Tester.Instance.InstanceID(), + Id: instanceID, }, }, }, }, { name: "all, ok", - ctx: CTX, + ctx: isolatedIAMOwnerCTX, req: &action.SetExecutionRequest{ Condition: &action.Condition{ ConditionType: &action.Condition_Request{ @@ -174,11 +175,11 @@ func TestServer_SetExecution_Request(t *testing.T) { }, }, want: &action.SetExecutionResponse{ - Details: &settings_object.Details{ - ChangeDate: timestamppb.Now(), + Details: &resource_object.Details{ + Changed: timestamppb.Now(), Owner: &object.Owner{ Type: object.OwnerType_OWNER_TYPE_INSTANCE, - Id: Tester.Instance.InstanceID(), + Id: instanceID, }, }, }, @@ -187,8 +188,8 @@ func TestServer_SetExecution_Request(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // We want to have the same response no matter how often we call the function - Client.SetExecution(tt.ctx, tt.req) - got, err := Client.SetExecution(tt.ctx, tt.req) + Tester.Client.ActionV3.SetExecution(tt.ctx, tt.req) + got, err := Tester.Client.ActionV3.SetExecution(tt.ctx, tt.req) if tt.wantErr { require.Error(t, err) return @@ -196,7 +197,7 @@ func TestServer_SetExecution_Request(t *testing.T) { require.NoError(t, err) - integration.AssertSettingsDetails(t, tt.want.Details, got.Details) + integration.AssertResourceDetails(t, tt.want.Details, got.Details) // cleanup to not impact other requests Tester.DeleteExecution(tt.ctx, t, tt.req.GetCondition()) @@ -205,8 +206,9 @@ func TestServer_SetExecution_Request(t *testing.T) { } func TestServer_SetExecution_Request_Include(t *testing.T) { - ensureFeatureEnabled(t) - targetResp := Tester.CreateTarget(CTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false) + _, instanceID, _, isolatedIAMOwnerCTX := Tester.UseIsolatedInstance(t, IAMOwnerCTX, SystemCTX) + ensureFeatureEnabled(t, isolatedIAMOwnerCTX) + targetResp := Tester.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false) executionCond := &action.Condition{ ConditionType: &action.Condition_Request{ Request: &action.RequestExecution{ @@ -216,7 +218,7 @@ func TestServer_SetExecution_Request_Include(t *testing.T) { }, }, } - Tester.SetExecution(CTX, t, + Tester.SetExecution(isolatedIAMOwnerCTX, t, executionCond, executionTargetsSingleTarget(targetResp.GetDetails().GetId()), ) @@ -230,7 +232,7 @@ func TestServer_SetExecution_Request_Include(t *testing.T) { }, }, } - Tester.SetExecution(CTX, t, + Tester.SetExecution(isolatedIAMOwnerCTX, t, circularExecutionService, executionTargetsSingleInclude(executionCond), ) @@ -243,7 +245,7 @@ func TestServer_SetExecution_Request_Include(t *testing.T) { }, }, } - Tester.SetExecution(CTX, t, + Tester.SetExecution(isolatedIAMOwnerCTX, t, circularExecutionMethod, executionTargetsSingleInclude(circularExecutionService), ) @@ -257,7 +259,7 @@ func TestServer_SetExecution_Request_Include(t *testing.T) { }{ { name: "method, circular error", - ctx: CTX, + ctx: isolatedIAMOwnerCTX, req: &action.SetExecutionRequest{ Condition: circularExecutionService, Execution: &action.Execution{ @@ -268,7 +270,7 @@ func TestServer_SetExecution_Request_Include(t *testing.T) { }, { name: "method, ok", - ctx: CTX, + ctx: isolatedIAMOwnerCTX, req: &action.SetExecutionRequest{ Condition: &action.Condition{ ConditionType: &action.Condition_Request{ @@ -280,23 +282,22 @@ func TestServer_SetExecution_Request_Include(t *testing.T) { }, }, Execution: &action.Execution{ - Targets: executionTargetsSingleInclude(executionCond), }, }, want: &action.SetExecutionResponse{ - Details: &settings_object.Details{ - ChangeDate: timestamppb.Now(), + Details: &resource_object.Details{ + Changed: timestamppb.Now(), Owner: &object.Owner{ Type: object.OwnerType_OWNER_TYPE_INSTANCE, - Id: Tester.Instance.InstanceID(), + Id: instanceID, }, }, }, }, { name: "service, ok", - ctx: CTX, + ctx: isolatedIAMOwnerCTX, req: &action.SetExecutionRequest{ Condition: &action.Condition{ ConditionType: &action.Condition_Request{ @@ -308,16 +309,15 @@ func TestServer_SetExecution_Request_Include(t *testing.T) { }, }, Execution: &action.Execution{ - Targets: executionTargetsSingleInclude(executionCond), }, }, want: &action.SetExecutionResponse{ - Details: &settings_object.Details{ - ChangeDate: timestamppb.Now(), + Details: &resource_object.Details{ + Changed: timestamppb.Now(), Owner: &object.Owner{ Type: object.OwnerType_OWNER_TYPE_INSTANCE, - Id: Tester.Instance.InstanceID(), + Id: instanceID, }, }, }, @@ -326,15 +326,15 @@ func TestServer_SetExecution_Request_Include(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // We want to have the same response no matter how often we call the function - Client.SetExecution(tt.ctx, tt.req) - got, err := Client.SetExecution(tt.ctx, tt.req) + Tester.Client.ActionV3.SetExecution(tt.ctx, tt.req) + got, err := Tester.Client.ActionV3.SetExecution(tt.ctx, tt.req) if tt.wantErr { require.Error(t, err) return } require.NoError(t, err) - integration.AssertSettingsDetails(t, tt.want.Details, got.Details) + integration.AssertResourceDetails(t, tt.want.Details, got.Details) // cleanup to not impact other requests Tester.DeleteExecution(tt.ctx, t, tt.req.GetCondition()) @@ -343,8 +343,9 @@ func TestServer_SetExecution_Request_Include(t *testing.T) { } func TestServer_SetExecution_Response(t *testing.T) { - ensureFeatureEnabled(t) - targetResp := Tester.CreateTarget(CTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false) + _, instanceID, _, isolatedIAMOwnerCTX := Tester.UseIsolatedInstance(t, IAMOwnerCTX, SystemCTX) + ensureFeatureEnabled(t, isolatedIAMOwnerCTX) + targetResp := Tester.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false) tests := []struct { name string @@ -369,7 +370,7 @@ func TestServer_SetExecution_Response(t *testing.T) { }, { name: "no condition, error", - ctx: CTX, + ctx: isolatedIAMOwnerCTX, req: &action.SetExecutionRequest{ Condition: &action.Condition{ ConditionType: &action.Condition_Response{ @@ -384,7 +385,7 @@ func TestServer_SetExecution_Response(t *testing.T) { }, { name: "method, not existing", - ctx: CTX, + ctx: isolatedIAMOwnerCTX, req: &action.SetExecutionRequest{ Condition: &action.Condition{ ConditionType: &action.Condition_Response{ @@ -403,7 +404,7 @@ func TestServer_SetExecution_Response(t *testing.T) { }, { name: "method, ok", - ctx: CTX, + ctx: isolatedIAMOwnerCTX, req: &action.SetExecutionRequest{ Condition: &action.Condition{ ConditionType: &action.Condition_Response{ @@ -419,18 +420,18 @@ func TestServer_SetExecution_Response(t *testing.T) { }, }, want: &action.SetExecutionResponse{ - Details: &settings_object.Details{ - ChangeDate: timestamppb.Now(), + Details: &resource_object.Details{ + Changed: timestamppb.Now(), Owner: &object.Owner{ Type: object.OwnerType_OWNER_TYPE_INSTANCE, - Id: Tester.Instance.InstanceID(), + Id: instanceID, }, }, }, }, { name: "service, not existing", - ctx: CTX, + ctx: isolatedIAMOwnerCTX, req: &action.SetExecutionRequest{ Condition: &action.Condition{ ConditionType: &action.Condition_Response{ @@ -449,7 +450,7 @@ func TestServer_SetExecution_Response(t *testing.T) { }, { name: "service, ok", - ctx: CTX, + ctx: isolatedIAMOwnerCTX, req: &action.SetExecutionRequest{ Condition: &action.Condition{ ConditionType: &action.Condition_Response{ @@ -465,18 +466,18 @@ func TestServer_SetExecution_Response(t *testing.T) { }, }, want: &action.SetExecutionResponse{ - Details: &settings_object.Details{ - ChangeDate: timestamppb.Now(), + Details: &resource_object.Details{ + Changed: timestamppb.Now(), Owner: &object.Owner{ Type: object.OwnerType_OWNER_TYPE_INSTANCE, - Id: Tester.Instance.InstanceID(), + Id: instanceID, }, }, }, }, { name: "all, ok", - ctx: CTX, + ctx: isolatedIAMOwnerCTX, req: &action.SetExecutionRequest{ Condition: &action.Condition{ ConditionType: &action.Condition_Response{ @@ -492,11 +493,11 @@ func TestServer_SetExecution_Response(t *testing.T) { }, }, want: &action.SetExecutionResponse{ - Details: &settings_object.Details{ - ChangeDate: timestamppb.Now(), + Details: &resource_object.Details{ + Changed: timestamppb.Now(), Owner: &object.Owner{ Type: object.OwnerType_OWNER_TYPE_INSTANCE, - Id: Tester.Instance.InstanceID(), + Id: instanceID, }, }, }, @@ -505,15 +506,15 @@ func TestServer_SetExecution_Response(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // We want to have the same response no matter how often we call the function - Client.SetExecution(tt.ctx, tt.req) - got, err := Client.SetExecution(tt.ctx, tt.req) + Tester.Client.ActionV3.SetExecution(tt.ctx, tt.req) + got, err := Tester.Client.ActionV3.SetExecution(tt.ctx, tt.req) if tt.wantErr { require.Error(t, err) return } require.NoError(t, err) - integration.AssertSettingsDetails(t, tt.want.Details, got.Details) + integration.AssertResourceDetails(t, tt.want.Details, got.Details) // cleanup to not impact other requests Tester.DeleteExecution(tt.ctx, t, tt.req.GetCondition()) @@ -522,8 +523,9 @@ func TestServer_SetExecution_Response(t *testing.T) { } func TestServer_SetExecution_Event(t *testing.T) { - ensureFeatureEnabled(t) - targetResp := Tester.CreateTarget(CTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false) + _, instanceID, _, isolatedIAMOwnerCTX := Tester.UseIsolatedInstance(t, IAMOwnerCTX, SystemCTX) + ensureFeatureEnabled(t, isolatedIAMOwnerCTX) + targetResp := Tester.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false) tests := []struct { name string @@ -550,7 +552,7 @@ func TestServer_SetExecution_Event(t *testing.T) { }, { name: "no condition, error", - ctx: CTX, + ctx: isolatedIAMOwnerCTX, req: &action.SetExecutionRequest{ Condition: &action.Condition{ ConditionType: &action.Condition_Event{ @@ -568,7 +570,7 @@ func TestServer_SetExecution_Event(t *testing.T) { { name: "event, not existing", - ctx: CTX, + ctx: isolatedIAMOwnerCTX, req: &action.SetExecutionRequest{ Condition: &action.Condition{ ConditionType: &action.Condition_Event{ @@ -586,7 +588,7 @@ func TestServer_SetExecution_Event(t *testing.T) { */ { name: "event, ok", - ctx: CTX, + ctx: isolatedIAMOwnerCTX, req: &action.SetExecutionRequest{ Condition: &action.Condition{ ConditionType: &action.Condition_Event{ @@ -602,11 +604,11 @@ func TestServer_SetExecution_Event(t *testing.T) { }, }, want: &action.SetExecutionResponse{ - Details: &settings_object.Details{ - ChangeDate: timestamppb.Now(), + Details: &resource_object.Details{ + Changed: timestamppb.Now(), Owner: &object.Owner{ Type: object.OwnerType_OWNER_TYPE_INSTANCE, - Id: Tester.Instance.InstanceID(), + Id: instanceID, }, }, }, @@ -616,7 +618,7 @@ func TestServer_SetExecution_Event(t *testing.T) { { name: "group, not existing", - ctx: CTX, + ctx: isolatedIAMOwnerCTX, req: &action.SetExecutionRequest{ Condition: &action.Condition{ ConditionType: &action.Condition_Event{ @@ -634,7 +636,7 @@ func TestServer_SetExecution_Event(t *testing.T) { */ { name: "group, ok", - ctx: CTX, + ctx: isolatedIAMOwnerCTX, req: &action.SetExecutionRequest{ Condition: &action.Condition{ ConditionType: &action.Condition_Event{ @@ -650,18 +652,18 @@ func TestServer_SetExecution_Event(t *testing.T) { }, }, want: &action.SetExecutionResponse{ - Details: &settings_object.Details{ - ChangeDate: timestamppb.Now(), + Details: &resource_object.Details{ + Changed: timestamppb.Now(), Owner: &object.Owner{ Type: object.OwnerType_OWNER_TYPE_INSTANCE, - Id: Tester.Instance.InstanceID(), + Id: instanceID, }, }, }, }, { name: "all, ok", - ctx: CTX, + ctx: isolatedIAMOwnerCTX, req: &action.SetExecutionRequest{ Condition: &action.Condition{ ConditionType: &action.Condition_Event{ @@ -677,11 +679,11 @@ func TestServer_SetExecution_Event(t *testing.T) { }, }, want: &action.SetExecutionResponse{ - Details: &settings_object.Details{ - ChangeDate: timestamppb.Now(), + Details: &resource_object.Details{ + Changed: timestamppb.Now(), Owner: &object.Owner{ Type: object.OwnerType_OWNER_TYPE_INSTANCE, - Id: Tester.Instance.InstanceID(), + Id: instanceID, }, }, }, @@ -690,15 +692,15 @@ func TestServer_SetExecution_Event(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // We want to have the same response no matter how often we call the function - Client.SetExecution(tt.ctx, tt.req) - got, err := Client.SetExecution(tt.ctx, tt.req) + Tester.Client.ActionV3.SetExecution(tt.ctx, tt.req) + got, err := Tester.Client.ActionV3.SetExecution(tt.ctx, tt.req) if tt.wantErr { require.Error(t, err) return } require.NoError(t, err) - integration.AssertSettingsDetails(t, tt.want.Details, got.Details) + integration.AssertResourceDetails(t, tt.want.Details, got.Details) // cleanup to not impact other requests Tester.DeleteExecution(tt.ctx, t, tt.req.GetCondition()) @@ -707,8 +709,9 @@ func TestServer_SetExecution_Event(t *testing.T) { } func TestServer_SetExecution_Function(t *testing.T) { - ensureFeatureEnabled(t) - targetResp := Tester.CreateTarget(CTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false) + _, instanceID, _, isolatedIAMOwnerCTX := Tester.UseIsolatedInstance(t, IAMOwnerCTX, SystemCTX) + ensureFeatureEnabled(t, isolatedIAMOwnerCTX) + targetResp := Tester.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false) tests := []struct { name string @@ -733,7 +736,7 @@ func TestServer_SetExecution_Function(t *testing.T) { }, { name: "no condition, error", - ctx: CTX, + ctx: isolatedIAMOwnerCTX, req: &action.SetExecutionRequest{ Condition: &action.Condition{ ConditionType: &action.Condition_Response{ @@ -748,7 +751,7 @@ func TestServer_SetExecution_Function(t *testing.T) { }, { name: "function, not existing", - ctx: CTX, + ctx: isolatedIAMOwnerCTX, req: &action.SetExecutionRequest{ Condition: &action.Condition{ ConditionType: &action.Condition_Function{ @@ -763,7 +766,7 @@ func TestServer_SetExecution_Function(t *testing.T) { }, { name: "function, ok", - ctx: CTX, + ctx: isolatedIAMOwnerCTX, req: &action.SetExecutionRequest{ Condition: &action.Condition{ ConditionType: &action.Condition_Function{ @@ -775,11 +778,11 @@ func TestServer_SetExecution_Function(t *testing.T) { }, }, want: &action.SetExecutionResponse{ - Details: &settings_object.Details{ - ChangeDate: timestamppb.Now(), + Details: &resource_object.Details{ + Changed: timestamppb.Now(), Owner: &object.Owner{ Type: object.OwnerType_OWNER_TYPE_INSTANCE, - Id: Tester.Instance.InstanceID(), + Id: instanceID, }, }, }, @@ -788,15 +791,15 @@ func TestServer_SetExecution_Function(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // We want to have the same response no matter how often we call the function - Client.SetExecution(tt.ctx, tt.req) - got, err := Client.SetExecution(tt.ctx, tt.req) + Tester.Client.ActionV3.SetExecution(tt.ctx, tt.req) + got, err := Tester.Client.ActionV3.SetExecution(tt.ctx, tt.req) if tt.wantErr { require.Error(t, err) return } require.NoError(t, err) - integration.AssertSettingsDetails(t, tt.want.Details, got.Details) + integration.AssertResourceDetails(t, tt.want.Details, got.Details) // cleanup to not impact other requests Tester.DeleteExecution(tt.ctx, t, tt.req.GetCondition()) diff --git a/internal/api/grpc/resources/action/v3alpha/execution_target_integration_test.go b/internal/api/grpc/resources/action/v3alpha/execution_target_integration_test.go new file mode 100644 index 0000000000..0fe042eb08 --- /dev/null +++ b/internal/api/grpc/resources/action/v3alpha/execution_target_integration_test.go @@ -0,0 +1,335 @@ +//go:build integration + +package action_test + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/http/httptest" + "reflect" + "testing" + "time" + + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/durationpb" + + "github.com/zitadel/zitadel/internal/api/grpc/server/middleware" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/integration" + object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha" + action "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha" + resource_object "github.com/zitadel/zitadel/pkg/grpc/resources/object/v3alpha" +) + +func TestServer_ExecutionTarget(t *testing.T) { + _, instanceID, _, isolatedIAMOwnerCTX := Tester.UseIsolatedInstance(t, IAMOwnerCTX, SystemCTX) + ensureFeatureEnabled(t, isolatedIAMOwnerCTX) + + fullMethod := "/zitadel.resources.action.v3alpha.ZITADELActions/GetTarget" + + tests := []struct { + name string + ctx context.Context + dep func(context.Context, *action.GetTargetRequest, *action.GetTargetResponse) (func(), error) + clean func(context.Context) + req *action.GetTargetRequest + want *action.GetTargetResponse + wantErr bool + }{ + { + name: "GetTarget, request and response, ok", + ctx: isolatedIAMOwnerCTX, + dep: func(ctx context.Context, request *action.GetTargetRequest, response *action.GetTargetResponse) (func(), error) { + + orgID := Tester.Organisation.ID + projectID := "" + userID := Tester.Users.Get(instanceID, integration.IAMOwner).ID + + // create target for target changes + targetCreatedName := fmt.Sprint("GetTarget", time.Now().UnixNano()+1) + targetCreatedURL := "https://nonexistent" + + targetCreated := Tester.CreateTarget(ctx, t, targetCreatedName, targetCreatedURL, domain.TargetTypeCall, false) + + // request received by target + wantRequest := &middleware.ContextInfoRequest{FullMethod: fullMethod, InstanceID: instanceID, OrgID: orgID, ProjectID: projectID, UserID: userID, Request: request} + changedRequest := &action.GetTargetRequest{Id: targetCreated.GetDetails().GetId()} + // replace original request with different targetID + urlRequest, closeRequest := testServerCall(wantRequest, 0, http.StatusOK, changedRequest) + targetRequest := Tester.CreateTarget(ctx, t, "", urlRequest, domain.TargetTypeCall, false) + Tester.SetExecution(ctx, t, conditionRequestFullMethod(fullMethod), executionTargetsSingleTarget(targetRequest.GetDetails().GetId())) + + // expected response from the GetTarget + expectedResponse := &action.GetTargetResponse{ + Target: &action.GetTarget{ + Config: &action.Target{ + Name: targetCreatedName, + Endpoint: targetCreatedURL, + TargetType: &action.Target_RestCall{ + RestCall: &action.SetRESTCall{ + InterruptOnError: false, + }, + }, + Timeout: durationpb.New(10 * time.Second), + }, + Details: targetCreated.GetDetails(), + }, + } + // has to be set separately because of the pointers + response.Target = &action.GetTarget{ + Details: targetCreated.GetDetails(), + Config: &action.Target{ + Name: targetCreatedName, + TargetType: &action.Target_RestCall{ + RestCall: &action.SetRESTCall{ + InterruptOnError: false, + }, + }, + Timeout: durationpb.New(10 * time.Second), + Endpoint: targetCreatedURL, + }, + } + + // content for partial update + changedResponse := &action.GetTargetResponse{ + Target: &action.GetTarget{ + Details: &resource_object.Details{ + Id: targetCreated.GetDetails().GetId(), + }, + }, + } + + // response received by target + wantResponse := &middleware.ContextInfoResponse{ + FullMethod: fullMethod, + InstanceID: instanceID, + OrgID: orgID, + ProjectID: projectID, + UserID: userID, + Request: changedRequest, + Response: expectedResponse, + } + // after request with different targetID, return changed response + targetResponseURL, closeResponse := testServerCall(wantResponse, 0, http.StatusOK, changedResponse) + targetResponse := Tester.CreateTarget(ctx, t, "", targetResponseURL, domain.TargetTypeCall, false) + Tester.SetExecution(ctx, t, conditionResponseFullMethod(fullMethod), executionTargetsSingleTarget(targetResponse.GetDetails().GetId())) + + return func() { + closeRequest() + closeResponse() + }, nil + }, + clean: func(ctx context.Context) { + Tester.DeleteExecution(ctx, t, conditionRequestFullMethod(fullMethod)) + Tester.DeleteExecution(ctx, t, conditionResponseFullMethod(fullMethod)) + }, + req: &action.GetTargetRequest{ + Id: "something", + }, + want: &action.GetTargetResponse{ + Target: &action.GetTarget{ + Details: &resource_object.Details{ + Id: "changed", + Owner: &object.Owner{ + Type: object.OwnerType_OWNER_TYPE_INSTANCE, + Id: instanceID, + }, + }, + }, + }, + }, + { + name: "GetTarget, request, interrupt", + ctx: isolatedIAMOwnerCTX, + dep: func(ctx context.Context, request *action.GetTargetRequest, response *action.GetTargetResponse) (func(), error) { + + fullMethod := "/zitadel.resources.action.v3alpha.ZITADELActions/GetTarget" + orgID := Tester.Organisation.ID + projectID := "" + userID := Tester.Users.Get(instanceID, integration.IAMOwner).ID + + // request received by target + wantRequest := &middleware.ContextInfoRequest{FullMethod: fullMethod, InstanceID: instanceID, OrgID: orgID, ProjectID: projectID, UserID: userID, Request: request} + urlRequest, closeRequest := testServerCall(wantRequest, 0, http.StatusInternalServerError, &action.GetTargetRequest{Id: "notchanged"}) + + targetRequest := Tester.CreateTarget(ctx, t, "", urlRequest, domain.TargetTypeCall, true) + Tester.SetExecution(ctx, t, conditionRequestFullMethod(fullMethod), executionTargetsSingleTarget(targetRequest.GetDetails().GetId())) + // GetTarget with used target + request.Id = targetRequest.GetDetails().GetId() + + return func() { + closeRequest() + }, nil + }, + clean: func(ctx context.Context) { + Tester.DeleteExecution(ctx, t, conditionRequestFullMethod(fullMethod)) + }, + req: &action.GetTargetRequest{}, + wantErr: true, + }, + { + name: "GetTarget, response, interrupt", + ctx: isolatedIAMOwnerCTX, + dep: func(ctx context.Context, request *action.GetTargetRequest, response *action.GetTargetResponse) (func(), error) { + + fullMethod := "/zitadel.resources.action.v3alpha.ZITADELActions/GetTarget" + orgID := Tester.Organisation.ID + projectID := "" + userID := Tester.Users.Get(instanceID, integration.IAMOwner).ID + + // create target for target changes + targetCreatedName := fmt.Sprint("GetTarget", time.Now().UnixNano()+1) + targetCreatedURL := "https://nonexistent" + + targetCreated := Tester.CreateTarget(ctx, t, targetCreatedName, targetCreatedURL, domain.TargetTypeCall, false) + + // GetTarget with used target + request.Id = targetCreated.GetDetails().GetId() + + // expected response from the GetTarget + expectedResponse := &action.GetTargetResponse{ + Target: &action.GetTarget{ + Details: targetCreated.GetDetails(), + Config: &action.Target{ + Name: targetCreatedName, + Endpoint: targetCreatedURL, + TargetType: &action.Target_RestCall{ + RestCall: &action.SetRESTCall{ + InterruptOnError: false, + }, + }, + Timeout: durationpb.New(10 * time.Second), + }, + }, + } + // content for partial update + changedResponse := &action.GetTargetResponse{ + Target: &action.GetTarget{ + Details: &resource_object.Details{ + Id: "changed", + }, + }, + } + + // response received by target + wantResponse := &middleware.ContextInfoResponse{ + FullMethod: fullMethod, + InstanceID: instanceID, + OrgID: orgID, + ProjectID: projectID, + UserID: userID, + Request: request, + Response: expectedResponse, + } + // after request with different targetID, return changed response + targetResponseURL, closeResponse := testServerCall(wantResponse, 0, http.StatusInternalServerError, changedResponse) + targetResponse := Tester.CreateTarget(ctx, t, "", targetResponseURL, domain.TargetTypeCall, true) + Tester.SetExecution(ctx, t, conditionResponseFullMethod(fullMethod), executionTargetsSingleTarget(targetResponse.GetDetails().GetId())) + + return func() { + closeResponse() + }, nil + }, + clean: func(ctx context.Context) { + Tester.DeleteExecution(ctx, t, conditionResponseFullMethod(fullMethod)) + }, + req: &action.GetTargetRequest{}, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.dep != nil { + close, err := tt.dep(tt.ctx, tt.req, tt.want) + require.NoError(t, err) + defer close() + } + + got, err := Tester.Client.ActionV3.GetTarget(tt.ctx, tt.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + + integration.AssertResourceDetails(t, tt.want.GetTarget().GetDetails(), got.GetTarget().GetDetails()) + require.Equal(t, tt.want.GetTarget().GetConfig(), got.GetTarget().GetConfig()) + if tt.clean != nil { + tt.clean(tt.ctx) + } + }) + } +} + +func conditionRequestFullMethod(fullMethod string) *action.Condition { + return &action.Condition{ + ConditionType: &action.Condition_Request{ + Request: &action.RequestExecution{ + Condition: &action.RequestExecution_Method{ + Method: fullMethod, + }, + }, + }, + } +} + +func conditionResponseFullMethod(fullMethod string) *action.Condition { + return &action.Condition{ + ConditionType: &action.Condition_Response{ + Response: &action.ResponseExecution{ + Condition: &action.ResponseExecution_Method{ + Method: fullMethod, + }, + }, + }, + } +} + +func testServerCall( + reqBody interface{}, + sleep time.Duration, + statusCode int, + respBody interface{}, +) (string, func()) { + handler := func(w http.ResponseWriter, r *http.Request) { + data, err := json.Marshal(reqBody) + if err != nil { + http.Error(w, "error, marshall: "+err.Error(), http.StatusInternalServerError) + return + } + + sentBody, err := io.ReadAll(r.Body) + if err != nil { + http.Error(w, "error, read body: "+err.Error(), http.StatusInternalServerError) + return + } + if !reflect.DeepEqual(data, sentBody) { + http.Error(w, "error, equal:\n"+string(data)+"\nsent:\n"+string(sentBody), http.StatusInternalServerError) + return + } + if statusCode != http.StatusOK { + http.Error(w, "error, statusCode", statusCode) + return + } + + time.Sleep(sleep) + + w.Header().Set("Content-Type", "application/json") + resp, err := json.Marshal(respBody) + if err != nil { + http.Error(w, "error", http.StatusInternalServerError) + return + } + if _, err := io.WriteString(w, string(resp)); err != nil { + http.Error(w, "error", http.StatusInternalServerError) + return + } + } + + server := httptest.NewServer(http.HandlerFunc(handler)) + + return server.URL, server.Close +} diff --git a/internal/api/grpc/resources/action/v3alpha/query.go b/internal/api/grpc/resources/action/v3alpha/query.go new file mode 100644 index 0000000000..ec7ed8b9c8 --- /dev/null +++ b/internal/api/grpc/resources/action/v3alpha/query.go @@ -0,0 +1,410 @@ +package action + +import ( + "context" + "strings" + + "google.golang.org/protobuf/types/known/durationpb" + + resource_object "github.com/zitadel/zitadel/internal/api/grpc/resources/object/v3alpha" + "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/query" + "github.com/zitadel/zitadel/internal/zerrors" + object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha" + action "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha" +) + +const ( + conditionIDAllSegmentCount = 0 + conditionIDRequestResponseServiceSegmentCount = 1 + conditionIDRequestResponseMethodSegmentCount = 2 + conditionIDEventGroupSegmentCount = 1 +) + +func (s *Server) GetTarget(ctx context.Context, req *action.GetTargetRequest) (*action.GetTargetResponse, error) { + if err := checkActionsEnabled(ctx); err != nil { + return nil, err + } + + resp, err := s.query.GetTargetByID(ctx, req.GetId()) + if err != nil { + return nil, err + } + return &action.GetTargetResponse{ + Target: targetToPb(resp), + }, nil +} + +type InstanceContext interface { + GetInstanceId() string + GetInstanceDomain() string +} + +type Context interface { + GetOwner() InstanceContext +} + +func (s *Server) SearchTargets(ctx context.Context, req *action.SearchTargetsRequest) (*action.SearchTargetsResponse, error) { + if err := checkActionsEnabled(ctx); err != nil { + return nil, err + } + queries, err := s.searchTargetsRequestToModel(req) + if err != nil { + return nil, err + } + resp, err := s.query.SearchTargets(ctx, queries) + if err != nil { + return nil, err + } + return &action.SearchTargetsResponse{ + Result: targetsToPb(resp.Targets), + Details: resource_object.ToSearchDetailsPb(queries.SearchRequest, resp.SearchResponse), + }, nil +} + +func (s *Server) SearchExecutions(ctx context.Context, req *action.SearchExecutionsRequest) (*action.SearchExecutionsResponse, error) { + if err := checkActionsEnabled(ctx); err != nil { + return nil, err + } + queries, err := s.searchExecutionsRequestToModel(req) + if err != nil { + return nil, err + } + resp, err := s.query.SearchExecutions(ctx, queries) + if err != nil { + return nil, err + } + return &action.SearchExecutionsResponse{ + Result: executionsToPb(resp.Executions), + Details: resource_object.ToSearchDetailsPb(queries.SearchRequest, resp.SearchResponse), + }, nil +} + +func targetsToPb(targets []*query.Target) []*action.GetTarget { + t := make([]*action.GetTarget, len(targets)) + for i, target := range targets { + t[i] = targetToPb(target) + } + return t +} + +func targetToPb(t *query.Target) *action.GetTarget { + target := &action.GetTarget{ + Details: resource_object.DomainToDetailsPb(&t.ObjectDetails, object.OwnerType_OWNER_TYPE_INSTANCE, t.ResourceOwner), + Config: &action.Target{ + Name: t.Name, + Timeout: durationpb.New(t.Timeout), + Endpoint: t.Endpoint, + }, + } + switch t.TargetType { + case domain.TargetTypeWebhook: + target.Config.TargetType = &action.Target_RestWebhook{RestWebhook: &action.SetRESTWebhook{InterruptOnError: t.InterruptOnError}} + case domain.TargetTypeCall: + target.Config.TargetType = &action.Target_RestCall{RestCall: &action.SetRESTCall{InterruptOnError: t.InterruptOnError}} + case domain.TargetTypeAsync: + target.Config.TargetType = &action.Target_RestAsync{RestAsync: &action.SetRESTAsync{}} + default: + target.Config.TargetType = nil + } + return target +} + +func (s *Server) searchTargetsRequestToModel(req *action.SearchTargetsRequest) (*query.TargetSearchQueries, error) { + offset, limit, asc, err := resource_object.SearchQueryPbToQuery(s.systemDefaults, req.Query) + if err != nil { + return nil, err + } + queries, err := targetQueriesToQuery(req.Filters) + if err != nil { + return nil, err + } + return &query.TargetSearchQueries{ + SearchRequest: query.SearchRequest{ + Offset: offset, + Limit: limit, + Asc: asc, + SortingColumn: targetFieldNameToSortingColumn(req.SortingColumn), + }, + Queries: queries, + }, nil +} + +func targetQueriesToQuery(queries []*action.TargetSearchFilter) (_ []query.SearchQuery, err error) { + q := make([]query.SearchQuery, len(queries)) + for i, qry := range queries { + q[i], err = targetQueryToQuery(qry) + if err != nil { + return nil, err + } + } + return q, nil +} + +func targetQueryToQuery(filter *action.TargetSearchFilter) (query.SearchQuery, error) { + switch q := filter.Filter.(type) { + case *action.TargetSearchFilter_TargetNameFilter: + return targetNameQueryToQuery(q.TargetNameFilter) + case *action.TargetSearchFilter_InTargetIdsFilter: + return targetInTargetIdsQueryToQuery(q.InTargetIdsFilter) + default: + return nil, zerrors.ThrowInvalidArgument(nil, "GRPC-vR9nC", "List.Query.Invalid") + } +} + +func targetNameQueryToQuery(q *action.TargetNameFilter) (query.SearchQuery, error) { + return query.NewTargetNameSearchQuery(resource_object.TextMethodPbToQuery(q.Method), q.GetTargetName()) +} + +func targetInTargetIdsQueryToQuery(q *action.InTargetIDsFilter) (query.SearchQuery, error) { + return query.NewTargetInIDsSearchQuery(q.GetTargetIds()) +} + +// targetFieldNameToSortingColumn defaults to the creation date because this ensures deterministic pagination +func targetFieldNameToSortingColumn(field *action.TargetFieldName) query.Column { + if field == nil { + return query.TargetColumnCreationDate + } + switch *field { + case action.TargetFieldName_TARGET_FIELD_NAME_UNSPECIFIED: + return query.TargetColumnID + case action.TargetFieldName_TARGET_FIELD_NAME_ID: + return query.TargetColumnID + case action.TargetFieldName_TARGET_FIELD_NAME_CREATED_DATE: + return query.TargetColumnCreationDate + case action.TargetFieldName_TARGET_FIELD_NAME_CHANGED_DATE: + return query.TargetColumnChangeDate + case action.TargetFieldName_TARGET_FIELD_NAME_NAME: + return query.TargetColumnName + case action.TargetFieldName_TARGET_FIELD_NAME_TARGET_TYPE: + return query.TargetColumnTargetType + case action.TargetFieldName_TARGET_FIELD_NAME_URL: + return query.TargetColumnURL + case action.TargetFieldName_TARGET_FIELD_NAME_TIMEOUT: + return query.TargetColumnTimeout + case action.TargetFieldName_TARGET_FIELD_NAME_INTERRUPT_ON_ERROR: + return query.TargetColumnInterruptOnError + default: + return query.TargetColumnCreationDate + } +} + +// executionFieldNameToSortingColumn defaults to the creation date because this ensures deterministic pagination +func executionFieldNameToSortingColumn(field *action.ExecutionFieldName) query.Column { + if field == nil { + return query.ExecutionColumnCreationDate + } + switch *field { + case action.ExecutionFieldName_EXECUTION_FIELD_NAME_UNSPECIFIED: + return query.ExecutionColumnID + case action.ExecutionFieldName_EXECUTION_FIELD_NAME_ID: + return query.ExecutionColumnID + case action.ExecutionFieldName_EXECUTION_FIELD_NAME_CREATED_DATE: + return query.ExecutionColumnCreationDate + case action.ExecutionFieldName_EXECUTION_FIELD_NAME_CHANGED_DATE: + return query.ExecutionColumnChangeDate + default: + return query.ExecutionColumnCreationDate + } +} + +func (s *Server) searchExecutionsRequestToModel(req *action.SearchExecutionsRequest) (*query.ExecutionSearchQueries, error) { + offset, limit, asc, err := resource_object.SearchQueryPbToQuery(s.systemDefaults, req.Query) + if err != nil { + return nil, err + } + queries, err := executionQueriesToQuery(req.Filters) + if err != nil { + return nil, err + } + return &query.ExecutionSearchQueries{ + SearchRequest: query.SearchRequest{ + Offset: offset, + Limit: limit, + Asc: asc, + SortingColumn: executionFieldNameToSortingColumn(req.SortingColumn), + }, + Queries: queries, + }, nil +} + +func executionQueriesToQuery(queries []*action.ExecutionSearchFilter) (_ []query.SearchQuery, err error) { + q := make([]query.SearchQuery, len(queries)) + for i, query := range queries { + q[i], err = executionQueryToQuery(query) + if err != nil { + return nil, err + } + } + return q, nil +} + +func executionQueryToQuery(searchQuery *action.ExecutionSearchFilter) (query.SearchQuery, error) { + switch q := searchQuery.Filter.(type) { + case *action.ExecutionSearchFilter_InConditionsFilter: + return inConditionsQueryToQuery(q.InConditionsFilter) + case *action.ExecutionSearchFilter_ExecutionTypeFilter: + return executionTypeToQuery(q.ExecutionTypeFilter) + case *action.ExecutionSearchFilter_IncludeFilter: + include, err := conditionToInclude(q.IncludeFilter.GetInclude()) + if err != nil { + return nil, err + } + return query.NewIncludeSearchQuery(include) + case *action.ExecutionSearchFilter_TargetFilter: + return query.NewTargetSearchQuery(q.TargetFilter.GetTargetId()) + default: + return nil, zerrors.ThrowInvalidArgument(nil, "GRPC-vR9nC", "List.Query.Invalid") + } +} + +func executionTypeToQuery(q *action.ExecutionTypeFilter) (query.SearchQuery, error) { + switch q.ExecutionType { + case action.ExecutionType_EXECUTION_TYPE_UNSPECIFIED: + return query.NewExecutionTypeSearchQuery(domain.ExecutionTypeUnspecified) + case action.ExecutionType_EXECUTION_TYPE_REQUEST: + return query.NewExecutionTypeSearchQuery(domain.ExecutionTypeRequest) + case action.ExecutionType_EXECUTION_TYPE_RESPONSE: + return query.NewExecutionTypeSearchQuery(domain.ExecutionTypeResponse) + case action.ExecutionType_EXECUTION_TYPE_EVENT: + return query.NewExecutionTypeSearchQuery(domain.ExecutionTypeEvent) + case action.ExecutionType_EXECUTION_TYPE_FUNCTION: + return query.NewExecutionTypeSearchQuery(domain.ExecutionTypeFunction) + default: + return query.NewExecutionTypeSearchQuery(domain.ExecutionTypeUnspecified) + } +} + +func inConditionsQueryToQuery(q *action.InConditionsFilter) (query.SearchQuery, error) { + values := make([]string, len(q.GetConditions())) + for i, condition := range q.GetConditions() { + id, err := conditionToID(condition) + if err != nil { + return nil, err + } + values[i] = id + } + return query.NewExecutionInIDsSearchQuery(values) +} + +func conditionToID(q *action.Condition) (string, error) { + switch t := q.GetConditionType().(type) { + case *action.Condition_Request: + cond := &command.ExecutionAPICondition{ + Method: t.Request.GetMethod(), + Service: t.Request.GetService(), + All: t.Request.GetAll(), + } + return cond.ID(domain.ExecutionTypeRequest), nil + case *action.Condition_Response: + cond := &command.ExecutionAPICondition{ + Method: t.Response.GetMethod(), + Service: t.Response.GetService(), + All: t.Response.GetAll(), + } + return cond.ID(domain.ExecutionTypeResponse), nil + case *action.Condition_Event: + cond := &command.ExecutionEventCondition{ + Event: t.Event.GetEvent(), + Group: t.Event.GetGroup(), + All: t.Event.GetAll(), + } + return cond.ID(), nil + case *action.Condition_Function: + return command.ExecutionFunctionCondition(t.Function.GetName()).ID(), nil + default: + return "", zerrors.ThrowInvalidArgument(nil, "GRPC-vR9nC", "List.Query.Invalid") + } +} + +func executionsToPb(executions []*query.Execution) []*action.GetExecution { + e := make([]*action.GetExecution, len(executions)) + for i, execution := range executions { + e[i] = executionToPb(execution) + } + return e +} + +func executionToPb(e *query.Execution) *action.GetExecution { + targets := make([]*action.ExecutionTargetType, len(e.Targets)) + for i := range e.Targets { + switch e.Targets[i].Type { + case domain.ExecutionTargetTypeInclude: + targets[i] = &action.ExecutionTargetType{Type: &action.ExecutionTargetType_Include{Include: executionIDToCondition(e.Targets[i].Target)}} + case domain.ExecutionTargetTypeTarget: + targets[i] = &action.ExecutionTargetType{Type: &action.ExecutionTargetType_Target{Target: e.Targets[i].Target}} + case domain.ExecutionTargetTypeUnspecified: + continue + default: + continue + } + } + + return &action.GetExecution{ + Details: resource_object.DomainToDetailsPb(&e.ObjectDetails, object.OwnerType_OWNER_TYPE_INSTANCE, e.ResourceOwner), + Execution: &action.Execution{ + Targets: targets, + }, + } +} + +func executionIDToCondition(include string) *action.Condition { + if strings.HasPrefix(include, domain.ExecutionTypeRequest.String()) { + return includeRequestToCondition(strings.TrimPrefix(include, domain.ExecutionTypeRequest.String())) + } + if strings.HasPrefix(include, domain.ExecutionTypeResponse.String()) { + return includeResponseToCondition(strings.TrimPrefix(include, domain.ExecutionTypeResponse.String())) + } + if strings.HasPrefix(include, domain.ExecutionTypeEvent.String()) { + return includeEventToCondition(strings.TrimPrefix(include, domain.ExecutionTypeEvent.String())) + } + if strings.HasPrefix(include, domain.ExecutionTypeFunction.String()) { + return includeFunctionToCondition(strings.TrimPrefix(include, domain.ExecutionTypeFunction.String())) + } + return nil +} + +func includeRequestToCondition(id string) *action.Condition { + switch strings.Count(id, "/") { + case conditionIDRequestResponseMethodSegmentCount: + return &action.Condition{ConditionType: &action.Condition_Request{Request: &action.RequestExecution{Condition: &action.RequestExecution_Method{Method: id}}}} + case conditionIDRequestResponseServiceSegmentCount: + return &action.Condition{ConditionType: &action.Condition_Request{Request: &action.RequestExecution{Condition: &action.RequestExecution_Service{Service: strings.TrimPrefix(id, "/")}}}} + case conditionIDAllSegmentCount: + return &action.Condition{ConditionType: &action.Condition_Request{Request: &action.RequestExecution{Condition: &action.RequestExecution_All{All: true}}}} + default: + return nil + } +} +func includeResponseToCondition(id string) *action.Condition { + switch strings.Count(id, "/") { + case conditionIDRequestResponseMethodSegmentCount: + return &action.Condition{ConditionType: &action.Condition_Response{Response: &action.ResponseExecution{Condition: &action.ResponseExecution_Method{Method: id}}}} + case conditionIDRequestResponseServiceSegmentCount: + return &action.Condition{ConditionType: &action.Condition_Response{Response: &action.ResponseExecution{Condition: &action.ResponseExecution_Service{Service: strings.TrimPrefix(id, "/")}}}} + case conditionIDAllSegmentCount: + return &action.Condition{ConditionType: &action.Condition_Response{Response: &action.ResponseExecution{Condition: &action.ResponseExecution_All{All: true}}}} + default: + return nil + } +} + +func includeEventToCondition(id string) *action.Condition { + switch strings.Count(id, "/") { + case conditionIDEventGroupSegmentCount: + if strings.HasSuffix(id, command.EventGroupSuffix) { + return &action.Condition{ConditionType: &action.Condition_Event{Event: &action.EventExecution{Condition: &action.EventExecution_Group{Group: strings.TrimSuffix(strings.TrimPrefix(id, "/"), command.EventGroupSuffix)}}}} + } else { + return &action.Condition{ConditionType: &action.Condition_Event{Event: &action.EventExecution{Condition: &action.EventExecution_Event{Event: strings.TrimPrefix(id, "/")}}}} + } + case conditionIDAllSegmentCount: + return &action.Condition{ConditionType: &action.Condition_Event{Event: &action.EventExecution{Condition: &action.EventExecution_All{All: true}}}} + default: + return nil + } +} + +func includeFunctionToCondition(id string) *action.Condition { + return &action.Condition{ConditionType: &action.Condition_Function{Function: &action.FunctionExecution{Name: strings.TrimPrefix(id, "/")}}} +} diff --git a/internal/api/grpc/resources/action/v3alpha/query_integration_test.go b/internal/api/grpc/resources/action/v3alpha/query_integration_test.go new file mode 100644 index 0000000000..b4f7578286 --- /dev/null +++ b/internal/api/grpc/resources/action/v3alpha/query_integration_test.go @@ -0,0 +1,898 @@ +//go:build integration + +package action_test + +import ( + "context" + "fmt" + "reflect" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/durationpb" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/integration" + object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha" + action "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha" + resource_object "github.com/zitadel/zitadel/pkg/grpc/resources/object/v3alpha" +) + +func TestServer_GetTarget(t *testing.T) { + _, _, _, isolatedIAMOwnerCTX := Tester.UseIsolatedInstance(t, IAMOwnerCTX, SystemCTX) + ensureFeatureEnabled(t, isolatedIAMOwnerCTX) + type args struct { + ctx context.Context + dep func(context.Context, *action.GetTargetRequest, *action.GetTargetResponse) error + req *action.GetTargetRequest + } + tests := []struct { + name string + args args + want *action.GetTargetResponse + wantErr bool + }{ + { + name: "missing permission", + args: args{ + ctx: Tester.WithAuthorization(context.Background(), integration.OrgOwner), + req: &action.GetTargetRequest{}, + }, + wantErr: true, + }, + { + name: "not found", + args: args{ + ctx: isolatedIAMOwnerCTX, + req: &action.GetTargetRequest{Id: "notexisting"}, + }, + wantErr: true, + }, + { + name: "get, ok", + args: args{ + ctx: isolatedIAMOwnerCTX, + dep: func(ctx context.Context, request *action.GetTargetRequest, response *action.GetTargetResponse) error { + name := fmt.Sprint(time.Now().UnixNano() + 1) + resp := Tester.CreateTarget(ctx, t, name, "https://example.com", domain.TargetTypeWebhook, false) + request.Id = resp.GetDetails().GetId() + response.Target.Config.Name = name + response.Target.Details = resp.GetDetails() + return nil + }, + req: &action.GetTargetRequest{}, + }, + want: &action.GetTargetResponse{ + Target: &action.GetTarget{ + Details: &resource_object.Details{ + Created: timestamppb.Now(), + Changed: timestamppb.Now(), + }, + Config: &action.Target{ + Endpoint: "https://example.com", + TargetType: &action.Target_RestWebhook{ + RestWebhook: &action.SetRESTWebhook{}, + }, + Timeout: durationpb.New(10 * time.Second), + }, + }, + }, + }, + { + name: "get, async, ok", + args: args{ + ctx: isolatedIAMOwnerCTX, + dep: func(ctx context.Context, request *action.GetTargetRequest, response *action.GetTargetResponse) error { + name := fmt.Sprint(time.Now().UnixNano() + 1) + resp := Tester.CreateTarget(ctx, t, name, "https://example.com", domain.TargetTypeAsync, false) + request.Id = resp.GetDetails().GetId() + response.Target.Config.Name = name + response.Target.Details = resp.GetDetails() + return nil + }, + req: &action.GetTargetRequest{}, + }, + want: &action.GetTargetResponse{ + Target: &action.GetTarget{ + Details: &resource_object.Details{ + Created: timestamppb.Now(), + Changed: timestamppb.Now(), + }, + Config: &action.Target{ + Endpoint: "https://example.com", + TargetType: &action.Target_RestAsync{ + RestAsync: &action.SetRESTAsync{}, + }, + Timeout: durationpb.New(10 * time.Second), + }, + }, + }, + }, + { + name: "get, webhook interruptOnError, ok", + args: args{ + ctx: isolatedIAMOwnerCTX, + dep: func(ctx context.Context, request *action.GetTargetRequest, response *action.GetTargetResponse) error { + name := fmt.Sprint(time.Now().UnixNano() + 1) + resp := Tester.CreateTarget(ctx, t, name, "https://example.com", domain.TargetTypeWebhook, true) + request.Id = resp.GetDetails().GetId() + response.Target.Config.Name = name + response.Target.Details = resp.GetDetails() + return nil + }, + req: &action.GetTargetRequest{}, + }, + want: &action.GetTargetResponse{ + Target: &action.GetTarget{ + Details: &resource_object.Details{ + Created: timestamppb.Now(), + Changed: timestamppb.Now(), + }, + Config: &action.Target{ + Endpoint: "https://example.com", + TargetType: &action.Target_RestWebhook{ + RestWebhook: &action.SetRESTWebhook{ + InterruptOnError: true, + }, + }, + Timeout: durationpb.New(10 * time.Second), + }, + }, + }, + }, + { + name: "get, call, ok", + args: args{ + ctx: isolatedIAMOwnerCTX, + dep: func(ctx context.Context, request *action.GetTargetRequest, response *action.GetTargetResponse) error { + name := fmt.Sprint(time.Now().UnixNano() + 1) + resp := Tester.CreateTarget(ctx, t, name, "https://example.com", domain.TargetTypeCall, false) + request.Id = resp.GetDetails().GetId() + response.Target.Config.Name = name + response.Target.Details = resp.GetDetails() + return nil + }, + req: &action.GetTargetRequest{}, + }, + want: &action.GetTargetResponse{ + Target: &action.GetTarget{ + Details: &resource_object.Details{ + Created: timestamppb.Now(), + Changed: timestamppb.Now(), + }, + Config: &action.Target{ + Endpoint: "https://example.com", + TargetType: &action.Target_RestCall{ + RestCall: &action.SetRESTCall{ + InterruptOnError: false, + }, + }, + Timeout: durationpb.New(10 * time.Second), + }, + }, + }, + }, + { + name: "get, call interruptOnError, ok", + args: args{ + ctx: isolatedIAMOwnerCTX, + dep: func(ctx context.Context, request *action.GetTargetRequest, response *action.GetTargetResponse) error { + name := fmt.Sprint(time.Now().UnixNano() + 1) + resp := Tester.CreateTarget(ctx, t, name, "https://example.com", domain.TargetTypeCall, true) + request.Id = resp.GetDetails().GetId() + response.Target.Config.Name = name + response.Target.Details = resp.GetDetails() + return nil + }, + req: &action.GetTargetRequest{}, + }, + want: &action.GetTargetResponse{ + Target: &action.GetTarget{ + Details: &resource_object.Details{ + Created: timestamppb.Now(), + Changed: timestamppb.Now(), + }, + Config: &action.Target{ + Endpoint: "https://example.com", + TargetType: &action.Target_RestCall{ + RestCall: &action.SetRESTCall{ + InterruptOnError: true, + }, + }, + Timeout: durationpb.New(10 * time.Second), + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.args.dep != nil { + err := tt.args.dep(tt.args.ctx, tt.args.req, tt.want) + require.NoError(t, err) + } + got, getErr := Tester.Client.ActionV3.GetTarget(tt.args.ctx, tt.args.req) + if tt.wantErr { + assert.Error(t, getErr, "Error: "+getErr.Error()) + } else { + assert.NoError(t, getErr) + wantTarget := tt.want.GetTarget() + gotTarget := got.GetTarget() + integration.AssertResourceDetails(t, wantTarget.GetDetails(), gotTarget.GetDetails()) + assert.Equal(t, wantTarget.GetConfig(), gotTarget.GetConfig()) + } + }) + } +} + +func TestServer_ListTargets(t *testing.T) { + _, instanceID, _, isolatedIAMOwnerCTX := Tester.UseIsolatedInstance(t, IAMOwnerCTX, SystemCTX) + ensureFeatureEnabled(t, isolatedIAMOwnerCTX) + type args struct { + ctx context.Context + dep func(context.Context, *action.SearchTargetsRequest, *action.SearchTargetsResponse) error + req *action.SearchTargetsRequest + } + tests := []struct { + name string + args args + want *action.SearchTargetsResponse + wantErr bool + }{ + { + name: "missing permission", + args: args{ + ctx: Tester.WithAuthorization(context.Background(), integration.OrgOwner), + req: &action.SearchTargetsRequest{}, + }, + wantErr: true, + }, + { + name: "list, not found", + args: args{ + ctx: isolatedIAMOwnerCTX, + req: &action.SearchTargetsRequest{ + Filters: []*action.TargetSearchFilter{ + {Filter: &action.TargetSearchFilter_InTargetIdsFilter{ + InTargetIdsFilter: &action.InTargetIDsFilter{ + TargetIds: []string{"notfound"}, + }, + }, + }, + }, + }, + }, + want: &action.SearchTargetsResponse{ + Details: &resource_object.ListDetails{ + TotalResult: 0, + AppliedLimit: 100, + }, + Result: []*action.GetTarget{}, + }, + }, + { + name: "list single id", + args: args{ + ctx: isolatedIAMOwnerCTX, + dep: func(ctx context.Context, request *action.SearchTargetsRequest, response *action.SearchTargetsResponse) error { + name := fmt.Sprint(time.Now().UnixNano() + 1) + resp := Tester.CreateTarget(ctx, t, name, "https://example.com", domain.TargetTypeWebhook, false) + request.Filters[0].Filter = &action.TargetSearchFilter_InTargetIdsFilter{ + InTargetIdsFilter: &action.InTargetIDsFilter{ + TargetIds: []string{resp.GetDetails().GetId()}, + }, + } + response.Details.Timestamp = resp.GetDetails().GetChanged() + + response.Result[0].Details = resp.GetDetails() + response.Result[0].Config.Name = name + return nil + }, + req: &action.SearchTargetsRequest{ + Filters: []*action.TargetSearchFilter{{}}, + }, + }, + want: &action.SearchTargetsResponse{ + Details: &resource_object.ListDetails{ + TotalResult: 1, + AppliedLimit: 100, + }, + Result: []*action.GetTarget{ + { + Details: &resource_object.Details{ + Created: timestamppb.Now(), + Changed: timestamppb.Now(), + }, + Config: &action.Target{ + Endpoint: "https://example.com", + TargetType: &action.Target_RestWebhook{ + RestWebhook: &action.SetRESTWebhook{ + InterruptOnError: false, + }, + }, + Timeout: durationpb.New(10 * time.Second), + }, + }, + }, + }, + }, { + name: "list single name", + args: args{ + ctx: isolatedIAMOwnerCTX, + dep: func(ctx context.Context, request *action.SearchTargetsRequest, response *action.SearchTargetsResponse) error { + name := fmt.Sprint(time.Now().UnixNano() + 1) + resp := Tester.CreateTarget(ctx, t, name, "https://example.com", domain.TargetTypeWebhook, false) + request.Filters[0].Filter = &action.TargetSearchFilter_TargetNameFilter{ + TargetNameFilter: &action.TargetNameFilter{ + TargetName: name, + }, + } + response.Details.Timestamp = resp.GetDetails().GetChanged() + + response.Result[0].Details = resp.GetDetails() + response.Result[0].Config.Name = name + return nil + }, + req: &action.SearchTargetsRequest{ + Filters: []*action.TargetSearchFilter{{}}, + }, + }, + want: &action.SearchTargetsResponse{ + Details: &resource_object.ListDetails{ + TotalResult: 1, + AppliedLimit: 100, + }, + Result: []*action.GetTarget{ + { + Details: &resource_object.Details{ + Created: timestamppb.Now(), + Changed: timestamppb.Now(), + Owner: &object.Owner{ + Type: object.OwnerType_OWNER_TYPE_INSTANCE, + Id: instanceID, + }, + }, + Config: &action.Target{ + Endpoint: "https://example.com", + TargetType: &action.Target_RestWebhook{ + RestWebhook: &action.SetRESTWebhook{ + InterruptOnError: false, + }, + }, + Timeout: durationpb.New(10 * time.Second), + }, + }, + }, + }, + }, + { + name: "list multiple id", + args: args{ + ctx: isolatedIAMOwnerCTX, + dep: func(ctx context.Context, request *action.SearchTargetsRequest, response *action.SearchTargetsResponse) error { + name1 := fmt.Sprint(time.Now().UnixNano() + 1) + name2 := fmt.Sprint(time.Now().UnixNano() + 3) + name3 := fmt.Sprint(time.Now().UnixNano() + 5) + resp1 := Tester.CreateTarget(ctx, t, name1, "https://example.com", domain.TargetTypeWebhook, false) + resp2 := Tester.CreateTarget(ctx, t, name2, "https://example.com", domain.TargetTypeCall, true) + resp3 := Tester.CreateTarget(ctx, t, name3, "https://example.com", domain.TargetTypeAsync, false) + request.Filters[0].Filter = &action.TargetSearchFilter_InTargetIdsFilter{ + InTargetIdsFilter: &action.InTargetIDsFilter{ + TargetIds: []string{resp1.GetDetails().GetId(), resp2.GetDetails().GetId(), resp3.GetDetails().GetId()}, + }, + } + response.Details.Timestamp = resp3.GetDetails().GetChanged() + + response.Result[0].Details = resp1.GetDetails() + response.Result[0].Config.Name = name1 + response.Result[1].Details = resp2.GetDetails() + response.Result[1].Config.Name = name2 + response.Result[2].Details = resp3.GetDetails() + response.Result[2].Config.Name = name3 + return nil + }, + req: &action.SearchTargetsRequest{ + Filters: []*action.TargetSearchFilter{{}}, + }, + }, + want: &action.SearchTargetsResponse{ + Details: &resource_object.ListDetails{ + TotalResult: 3, + AppliedLimit: 100, + }, + Result: []*action.GetTarget{ + { + Details: &resource_object.Details{ + Created: timestamppb.Now(), + Changed: timestamppb.Now(), + Owner: &object.Owner{ + Type: object.OwnerType_OWNER_TYPE_INSTANCE, + Id: instanceID, + }, + }, + Config: &action.Target{ + Endpoint: "https://example.com", + TargetType: &action.Target_RestWebhook{ + RestWebhook: &action.SetRESTWebhook{ + InterruptOnError: false, + }, + }, + Timeout: durationpb.New(10 * time.Second), + }, + }, + { + Details: &resource_object.Details{ + Created: timestamppb.Now(), + Changed: timestamppb.Now(), + Owner: &object.Owner{ + Type: object.OwnerType_OWNER_TYPE_INSTANCE, + Id: instanceID, + }, + }, + Config: &action.Target{ + Endpoint: "https://example.com", + TargetType: &action.Target_RestCall{ + RestCall: &action.SetRESTCall{ + InterruptOnError: true, + }, + }, + Timeout: durationpb.New(10 * time.Second), + }, + }, + { + Details: &resource_object.Details{ + Created: timestamppb.Now(), + Changed: timestamppb.Now(), + Owner: &object.Owner{ + Type: object.OwnerType_OWNER_TYPE_INSTANCE, + Id: instanceID, + }, + }, + Config: &action.Target{ + Endpoint: "https://example.com", + TargetType: &action.Target_RestAsync{ + RestAsync: &action.SetRESTAsync{}, + }, + Timeout: durationpb.New(10 * time.Second), + }, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.args.dep != nil { + err := tt.args.dep(tt.args.ctx, tt.args.req, tt.want) + require.NoError(t, err) + } + + retryDuration := 5 * time.Second + if ctxDeadline, ok := isolatedIAMOwnerCTX.Deadline(); ok { + retryDuration = time.Until(ctxDeadline) + } + + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + got, listErr := Tester.Client.ActionV3.SearchTargets(tt.args.ctx, tt.args.req) + if tt.wantErr { + assert.Error(ttt, listErr, "Error: "+listErr.Error()) + } else { + assert.NoError(ttt, listErr) + } + if listErr != nil { + return + } + // always first check length, otherwise its failed anyway + assert.Len(ttt, got.Result, len(tt.want.Result)) + for i := range tt.want.Result { + integration.AssertResourceDetails(t, tt.want.Result[i].GetDetails(), got.Result[i].GetDetails()) + assert.Equal(ttt, tt.want.Result[i].GetConfig(), got.Result[i].GetConfig()) + } + integration.AssertResourceListDetails(t, tt.want, got) + }, retryDuration, time.Millisecond*100, "timeout waiting for expected execution result") + }) + } +} + +func TestServer_SearchExecutions(t *testing.T) { + _, instanceID, _, isolatedIAMOwnerCTX := Tester.UseIsolatedInstance(t, IAMOwnerCTX, SystemCTX) + ensureFeatureEnabled(t, isolatedIAMOwnerCTX) + targetResp := Tester.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeWebhook, false) + + type args struct { + ctx context.Context + dep func(context.Context, *action.SearchExecutionsRequest, *action.SearchExecutionsResponse) error + req *action.SearchExecutionsRequest + } + tests := []struct { + name string + args args + want *action.SearchExecutionsResponse + wantErr bool + }{ + { + name: "missing permission", + args: args{ + ctx: Tester.WithAuthorization(context.Background(), integration.OrgOwner), + req: &action.SearchExecutionsRequest{}, + }, + wantErr: true, + }, + { + name: "list request single condition", + args: args{ + ctx: isolatedIAMOwnerCTX, + dep: func(ctx context.Context, request *action.SearchExecutionsRequest, response *action.SearchExecutionsResponse) error { + cond := request.Filters[0].GetInConditionsFilter().GetConditions()[0] + resp := Tester.SetExecution(ctx, t, cond, executionTargetsSingleTarget(targetResp.GetDetails().GetId())) + + response.Details.Timestamp = resp.GetDetails().GetChanged() + // Set expected response with used values for SetExecution + response.Result[0].Details = resp.GetDetails() + response.Result[0].Condition = cond + return nil + }, + req: &action.SearchExecutionsRequest{ + Filters: []*action.ExecutionSearchFilter{{ + Filter: &action.ExecutionSearchFilter_InConditionsFilter{ + InConditionsFilter: &action.InConditionsFilter{ + Conditions: []*action.Condition{{ + ConditionType: &action.Condition_Request{ + Request: &action.RequestExecution{ + Condition: &action.RequestExecution_Method{ + Method: "/zitadel.session.v2.SessionService/GetSession", + }, + }, + }, + }}, + }, + }, + }}, + }, + }, + want: &action.SearchExecutionsResponse{ + Details: &resource_object.ListDetails{ + TotalResult: 1, + AppliedLimit: 100, + }, + Result: []*action.GetExecution{ + { + Details: &resource_object.Details{ + Created: timestamppb.Now(), + Changed: timestamppb.Now(), + }, + Condition: &action.Condition{ + ConditionType: &action.Condition_Request{ + Request: &action.RequestExecution{ + Condition: &action.RequestExecution_Method{ + Method: "/zitadel.session.v2.SessionService/GetSession", + }, + }, + }, + }, + Execution: &action.Execution{ + Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()), + }, + }, + }, + }, + }, + { + name: "list request single target", + args: args{ + ctx: isolatedIAMOwnerCTX, + dep: func(ctx context.Context, request *action.SearchExecutionsRequest, response *action.SearchExecutionsResponse) error { + target := Tester.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeWebhook, false) + // add target as Filter to the request + request.Filters[0] = &action.ExecutionSearchFilter{ + Filter: &action.ExecutionSearchFilter_TargetFilter{ + TargetFilter: &action.TargetFilter{ + TargetId: target.GetDetails().GetId(), + }, + }, + } + cond := &action.Condition{ + ConditionType: &action.Condition_Request{ + Request: &action.RequestExecution{ + Condition: &action.RequestExecution_Method{ + Method: "/zitadel.management.v1.ManagementService/UpdateAction", + }, + }, + }, + } + targets := executionTargetsSingleTarget(target.GetDetails().GetId()) + resp := Tester.SetExecution(ctx, t, cond, targets) + + response.Details.Timestamp = resp.GetDetails().GetChanged() + + response.Result[0].Details = resp.GetDetails() + response.Result[0].Condition = cond + response.Result[0].Execution.Targets = targets + return nil + }, + req: &action.SearchExecutionsRequest{ + Filters: []*action.ExecutionSearchFilter{{}}, + }, + }, + want: &action.SearchExecutionsResponse{ + Details: &resource_object.ListDetails{ + TotalResult: 1, + AppliedLimit: 100, + }, + Result: []*action.GetExecution{ + { + Details: &resource_object.Details{ + Created: timestamppb.Now(), + Changed: timestamppb.Now(), + }, + Condition: &action.Condition{}, + Execution: &action.Execution{ + Targets: executionTargetsSingleTarget(""), + }, + }, + }, + }, + }, { + name: "list request single include", + args: args{ + ctx: isolatedIAMOwnerCTX, + dep: func(ctx context.Context, request *action.SearchExecutionsRequest, response *action.SearchExecutionsResponse) error { + cond := &action.Condition{ + ConditionType: &action.Condition_Request{ + Request: &action.RequestExecution{ + Condition: &action.RequestExecution_Method{ + Method: "/zitadel.management.v1.ManagementService/GetAction", + }, + }, + }, + } + Tester.SetExecution(ctx, t, cond, executionTargetsSingleTarget(targetResp.GetDetails().GetId())) + request.Filters[0].GetIncludeFilter().Include = cond + + includeCond := &action.Condition{ + ConditionType: &action.Condition_Request{ + Request: &action.RequestExecution{ + Condition: &action.RequestExecution_Method{ + Method: "/zitadel.management.v1.ManagementService/ListActions", + }, + }, + }, + } + includeTargets := executionTargetsSingleInclude(cond) + resp2 := Tester.SetExecution(ctx, t, includeCond, includeTargets) + + response.Details.Timestamp = resp2.GetDetails().GetChanged() + + response.Result[0].Details = resp2.GetDetails() + response.Result[0].Condition = includeCond + response.Result[0].Execution = &action.Execution{ + Targets: includeTargets, + } + return nil + }, + req: &action.SearchExecutionsRequest{ + Filters: []*action.ExecutionSearchFilter{{ + Filter: &action.ExecutionSearchFilter_IncludeFilter{ + IncludeFilter: &action.IncludeFilter{}, + }, + }}, + }, + }, + want: &action.SearchExecutionsResponse{ + Details: &resource_object.ListDetails{ + TotalResult: 1, + AppliedLimit: 100, + }, + Result: []*action.GetExecution{ + { + Details: &resource_object.Details{ + Created: timestamppb.Now(), + Changed: timestamppb.Now(), + }, + }, + }, + }, + }, + { + name: "list multiple conditions", + args: args{ + ctx: isolatedIAMOwnerCTX, + dep: func(ctx context.Context, request *action.SearchExecutionsRequest, response *action.SearchExecutionsResponse) error { + + cond1 := request.Filters[0].GetInConditionsFilter().GetConditions()[0] + targets1 := executionTargetsSingleTarget(targetResp.GetDetails().GetId()) + resp1 := Tester.SetExecution(ctx, t, cond1, targets1) + response.Result[0].Details = resp1.GetDetails() + response.Result[0].Condition = cond1 + response.Result[0].Execution = &action.Execution{ + Targets: targets1, + } + + cond2 := request.Filters[0].GetInConditionsFilter().GetConditions()[1] + targets2 := executionTargetsSingleTarget(targetResp.GetDetails().GetId()) + resp2 := Tester.SetExecution(ctx, t, cond2, targets2) + response.Result[1].Details = resp2.GetDetails() + response.Result[1].Condition = cond2 + response.Result[1].Execution = &action.Execution{ + Targets: targets2, + } + + cond3 := request.Filters[0].GetInConditionsFilter().GetConditions()[2] + targets3 := executionTargetsSingleTarget(targetResp.GetDetails().GetId()) + resp3 := Tester.SetExecution(ctx, t, cond3, targets3) + response.Result[2].Details = resp3.GetDetails() + response.Result[2].Condition = cond3 + response.Result[2].Execution = &action.Execution{ + Targets: targets3, + } + response.Details.Timestamp = resp3.GetDetails().GetChanged() + return nil + }, + req: &action.SearchExecutionsRequest{ + Filters: []*action.ExecutionSearchFilter{{ + Filter: &action.ExecutionSearchFilter_InConditionsFilter{ + InConditionsFilter: &action.InConditionsFilter{ + Conditions: []*action.Condition{ + { + ConditionType: &action.Condition_Request{ + Request: &action.RequestExecution{ + Condition: &action.RequestExecution_Method{ + Method: "/zitadel.session.v2.SessionService/GetSession", + }, + }, + }, + }, + { + ConditionType: &action.Condition_Request{ + Request: &action.RequestExecution{ + Condition: &action.RequestExecution_Method{ + Method: "/zitadel.session.v2.SessionService/CreateSession", + }, + }, + }, + }, + { + ConditionType: &action.Condition_Request{ + Request: &action.RequestExecution{ + Condition: &action.RequestExecution_Method{ + Method: "/zitadel.session.v2.SessionService/SetSession", + }, + }, + }, + }, + }, + }, + }, + }}, + }, + }, + want: &action.SearchExecutionsResponse{ + Details: &resource_object.ListDetails{ + TotalResult: 3, + AppliedLimit: 100, + }, + Result: []*action.GetExecution{ + { + Details: &resource_object.Details{ + Owner: &object.Owner{Type: object.OwnerType_OWNER_TYPE_INSTANCE, Id: instanceID}, + }, + }, { + Details: &resource_object.Details{ + Owner: &object.Owner{Type: object.OwnerType_OWNER_TYPE_INSTANCE, Id: instanceID}, + }, + }, { + Details: &resource_object.Details{ + Owner: &object.Owner{Type: object.OwnerType_OWNER_TYPE_INSTANCE, Id: instanceID}, + }, + }, + }, + }, + }, + { + name: "list multiple conditions all types", + args: args{ + ctx: isolatedIAMOwnerCTX, + dep: func(ctx context.Context, request *action.SearchExecutionsRequest, response *action.SearchExecutionsResponse) error { + targets := executionTargetsSingleTarget(targetResp.GetDetails().GetId()) + for i, cond := range request.Filters[0].GetInConditionsFilter().GetConditions() { + resp := Tester.SetExecution(ctx, t, cond, targets) + response.Result[i].Details = resp.GetDetails() + response.Result[i].Condition = cond + response.Result[i].Execution = &action.Execution{ + Targets: targets, + } + // filled with info of last sequence + response.Details.Timestamp = resp.GetDetails().GetChanged() + } + + return nil + }, + req: &action.SearchExecutionsRequest{ + Filters: []*action.ExecutionSearchFilter{{ + Filter: &action.ExecutionSearchFilter_InConditionsFilter{ + InConditionsFilter: &action.InConditionsFilter{ + Conditions: []*action.Condition{ + {ConditionType: &action.Condition_Request{Request: &action.RequestExecution{Condition: &action.RequestExecution_Method{Method: "/zitadel.session.v2.SessionService/GetSession"}}}}, + {ConditionType: &action.Condition_Request{Request: &action.RequestExecution{Condition: &action.RequestExecution_Service{Service: "zitadel.session.v2.SessionService"}}}}, + {ConditionType: &action.Condition_Request{Request: &action.RequestExecution{Condition: &action.RequestExecution_All{All: true}}}}, + {ConditionType: &action.Condition_Response{Response: &action.ResponseExecution{Condition: &action.ResponseExecution_Method{Method: "/zitadel.session.v2.SessionService/GetSession"}}}}, + {ConditionType: &action.Condition_Response{Response: &action.ResponseExecution{Condition: &action.ResponseExecution_Service{Service: "zitadel.session.v2.SessionService"}}}}, + {ConditionType: &action.Condition_Response{Response: &action.ResponseExecution{Condition: &action.ResponseExecution_All{All: true}}}}, + {ConditionType: &action.Condition_Event{Event: &action.EventExecution{Condition: &action.EventExecution_Event{Event: "user.added"}}}}, + {ConditionType: &action.Condition_Event{Event: &action.EventExecution{Condition: &action.EventExecution_Group{Group: "user"}}}}, + {ConditionType: &action.Condition_Event{Event: &action.EventExecution{Condition: &action.EventExecution_All{All: true}}}}, + {ConditionType: &action.Condition_Function{Function: &action.FunctionExecution{Name: "Action.Flow.Type.ExternalAuthentication.Action.TriggerType.PostAuthentication"}}}, + }, + }, + }, + }}, + }, + }, + want: &action.SearchExecutionsResponse{ + Details: &resource_object.ListDetails{ + TotalResult: 10, + AppliedLimit: 100, + }, + Result: []*action.GetExecution{ + {Details: &resource_object.Details{Owner: &object.Owner{Type: object.OwnerType_OWNER_TYPE_INSTANCE, Id: instanceID}}}, + {Details: &resource_object.Details{Owner: &object.Owner{Type: object.OwnerType_OWNER_TYPE_INSTANCE, Id: instanceID}}}, + {Details: &resource_object.Details{Owner: &object.Owner{Type: object.OwnerType_OWNER_TYPE_INSTANCE, Id: instanceID}}}, + {Details: &resource_object.Details{Owner: &object.Owner{Type: object.OwnerType_OWNER_TYPE_INSTANCE, Id: instanceID}}}, + {Details: &resource_object.Details{Owner: &object.Owner{Type: object.OwnerType_OWNER_TYPE_INSTANCE, Id: instanceID}}}, + {Details: &resource_object.Details{Owner: &object.Owner{Type: object.OwnerType_OWNER_TYPE_INSTANCE, Id: instanceID}}}, + {Details: &resource_object.Details{Owner: &object.Owner{Type: object.OwnerType_OWNER_TYPE_INSTANCE, Id: instanceID}}}, + {Details: &resource_object.Details{Owner: &object.Owner{Type: object.OwnerType_OWNER_TYPE_INSTANCE, Id: instanceID}}}, + {Details: &resource_object.Details{Owner: &object.Owner{Type: object.OwnerType_OWNER_TYPE_INSTANCE, Id: instanceID}}}, + {Details: &resource_object.Details{Owner: &object.Owner{Type: object.OwnerType_OWNER_TYPE_INSTANCE, Id: instanceID}}}, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.args.dep != nil { + err := tt.args.dep(tt.args.ctx, tt.args.req, tt.want) + require.NoError(t, err) + } + + retryDuration := 5 * time.Second + if ctxDeadline, ok := isolatedIAMOwnerCTX.Deadline(); ok { + retryDuration = time.Until(ctxDeadline) + } + + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + got, listErr := Tester.Client.ActionV3.SearchExecutions(tt.args.ctx, tt.args.req) + if tt.wantErr { + assert.Error(t, listErr, "Error: "+listErr.Error()) + } else { + assert.NoError(t, listErr) + } + if listErr != nil { + return + } + // always first check length, otherwise its failed anyway + assert.Len(t, got.Result, len(tt.want.Result)) + for i := range tt.want.Result { + // as not sorted, all elements have to be checked + // workaround as oneof elements can only be checked with assert.EqualExportedValues() + if j, found := containExecution(got.Result, tt.want.Result[i]); found { + assert.EqualExportedValues(t, tt.want.Result[i], got.Result[j]) + } + } + integration.AssertResourceListDetails(t, tt.want, got) + }, retryDuration, time.Millisecond*100, "timeout waiting for expected execution result") + }) + } +} + +func containExecution(executionList []*action.GetExecution, execution *action.GetExecution) (int, bool) { + for i, exec := range executionList { + if reflect.DeepEqual(exec.Details, execution.Details) { + return i, true + } + } + return 0, false +} diff --git a/internal/api/grpc/resources/action/v3alpha/server.go b/internal/api/grpc/resources/action/v3alpha/server.go index 57d0761fd2..b80c60d668 100644 --- a/internal/api/grpc/resources/action/v3alpha/server.go +++ b/internal/api/grpc/resources/action/v3alpha/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/query" "github.com/zitadel/zitadel/internal/zerrors" action "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha" @@ -17,6 +18,7 @@ var _ action.ZITADELActionsServer = (*Server)(nil) type Server struct { action.UnimplementedZITADELActionsServer + systemDefaults systemdefaults.SystemDefaults command *command.Commands query *query.Queries ListActionFunctions func() []string @@ -27,6 +29,7 @@ type Server struct { type Config struct{} func CreateServer( + systemDefaults systemdefaults.SystemDefaults, command *command.Commands, query *query.Queries, listActionFunctions func() []string, @@ -34,6 +37,7 @@ func CreateServer( listGRPCServices func() []string, ) *Server { return &Server{ + systemDefaults: systemDefaults, command: command, query: query, ListActionFunctions: listActionFunctions, @@ -62,7 +66,7 @@ func (s *Server) RegisterGateway() server.RegisterGatewayFunc { return action.RegisterZITADELActionsHandler } -func checkExecutionEnabled(ctx context.Context) error { +func checkActionsEnabled(ctx context.Context) error { if authz.GetInstance(ctx).Features().Actions { return nil } diff --git a/internal/api/grpc/resources/action/v3alpha/server_integration_test.go b/internal/api/grpc/resources/action/v3alpha/server_integration_test.go index 483ed2bd3f..e286ea94b0 100644 --- a/internal/api/grpc/resources/action/v3alpha/server_integration_test.go +++ b/internal/api/grpc/resources/action/v3alpha/server_integration_test.go @@ -13,49 +13,48 @@ import ( "github.com/stretchr/testify/require" "github.com/zitadel/zitadel/internal/integration" - feature "github.com/zitadel/zitadel/pkg/grpc/feature/v2" - action "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha" + "github.com/zitadel/zitadel/pkg/grpc/feature/v2" ) var ( - CTX context.Context - Tester *integration.Tester - Client action.ZITADELActionsClient + IAMOwnerCTX, SystemCTX context.Context + Tester *integration.Tester ) func TestMain(m *testing.M) { os.Exit(func() int { - ctx, errCtx, cancel := integration.Contexts(5 * time.Minute) + ctx, _, cancel := integration.Contexts(5 * time.Minute) defer cancel() Tester = integration.NewTester(ctx) defer Tester.Done() - Client = Tester.Client.ActionV3 - CTX, _ = Tester.WithAuthorization(ctx, integration.IAMOwner), errCtx + IAMOwnerCTX = Tester.WithAuthorization(ctx, integration.IAMOwner) + SystemCTX = Tester.WithAuthorization(ctx, integration.SystemUser) + return m.Run() }()) } -func ensureFeatureEnabled(t *testing.T) { - f, err := Tester.Client.FeatureV2.GetInstanceFeatures(CTX, &feature.GetInstanceFeaturesRequest{ +func ensureFeatureEnabled(t *testing.T, iamOwnerCTX context.Context) { + f, err := Tester.Client.FeatureV2.GetInstanceFeatures(iamOwnerCTX, &feature.GetInstanceFeaturesRequest{ Inheritance: true, }) require.NoError(t, err) if f.Actions.GetEnabled() { return } - _, err = Tester.Client.FeatureV2.SetInstanceFeatures(CTX, &feature.SetInstanceFeaturesRequest{ + _, err = Tester.Client.FeatureV2.SetInstanceFeatures(iamOwnerCTX, &feature.SetInstanceFeaturesRequest{ Actions: gu.Ptr(true), }) require.NoError(t, err) retryDuration := time.Minute - if ctxDeadline, ok := CTX.Deadline(); ok { + if ctxDeadline, ok := iamOwnerCTX.Deadline(); ok { retryDuration = time.Until(ctxDeadline) } require.EventuallyWithT(t, func(ttt *assert.CollectT) { - f, err := Tester.Client.FeatureV2.GetInstanceFeatures(CTX, &feature.GetInstanceFeaturesRequest{ + f, err := Tester.Client.FeatureV2.GetInstanceFeatures(iamOwnerCTX, &feature.GetInstanceFeaturesRequest{ Inheritance: true, }) require.NoError(ttt, err) diff --git a/internal/api/grpc/resources/action/v3alpha/target.go b/internal/api/grpc/resources/action/v3alpha/target.go index 5d33dac911..031cd99477 100644 --- a/internal/api/grpc/resources/action/v3alpha/target.go +++ b/internal/api/grpc/resources/action/v3alpha/target.go @@ -15,45 +15,45 @@ import ( ) func (s *Server) CreateTarget(ctx context.Context, req *action.CreateTargetRequest) (*action.CreateTargetResponse, error) { - if err := checkExecutionEnabled(ctx); err != nil { + if err := checkActionsEnabled(ctx); err != nil { return nil, err } add := createTargetToCommand(req) - instance := targetOwnerInstance(ctx) - details, err := s.command.AddTarget(ctx, add, instance.Id) + instanceID := authz.GetInstance(ctx).InstanceID() + details, err := s.command.AddTarget(ctx, add, instanceID) if err != nil { return nil, err } return &action.CreateTargetResponse{ - Details: resource_object.DomainToDetailsPb(details, instance, add.AggregateID), + Details: resource_object.DomainToDetailsPb(details, object.OwnerType_OWNER_TYPE_INSTANCE, instanceID), }, nil } func (s *Server) PatchTarget(ctx context.Context, req *action.PatchTargetRequest) (*action.PatchTargetResponse, error) { - if err := checkExecutionEnabled(ctx); err != nil { + if err := checkActionsEnabled(ctx); err != nil { return nil, err } - instance := targetOwnerInstance(ctx) - details, err := s.command.ChangeTarget(ctx, patchTargetToCommand(req), instance.Id) + instanceID := authz.GetInstance(ctx).InstanceID() + details, err := s.command.ChangeTarget(ctx, patchTargetToCommand(req), instanceID) if err != nil { return nil, err } return &action.PatchTargetResponse{ - Details: resource_object.DomainToDetailsPb(details, instance, req.GetId()), + Details: resource_object.DomainToDetailsPb(details, object.OwnerType_OWNER_TYPE_INSTANCE, instanceID), }, nil } func (s *Server) DeleteTarget(ctx context.Context, req *action.DeleteTargetRequest) (*action.DeleteTargetResponse, error) { - if err := checkExecutionEnabled(ctx); err != nil { + if err := checkActionsEnabled(ctx); err != nil { return nil, err } - instance := targetOwnerInstance(ctx) - details, err := s.command.DeleteTarget(ctx, req.GetId(), instance.Id) + instanceID := authz.GetInstance(ctx).InstanceID() + details, err := s.command.DeleteTarget(ctx, req.GetId(), instanceID) if err != nil { return nil, err } return &action.DeleteTargetResponse{ - Details: resource_object.DomainToDetailsPb(details, instance, req.GetId()), + Details: resource_object.DomainToDetailsPb(details, object.OwnerType_OWNER_TYPE_INSTANCE, instanceID), }, nil } @@ -112,10 +112,3 @@ func patchTargetToCommand(req *action.PatchTargetRequest) *command.ChangeTarget } return target } - -func targetOwnerInstance(ctx context.Context) *object.Owner { - return &object.Owner{ - Type: object.OwnerType_OWNER_TYPE_INSTANCE, - Id: authz.GetInstance(ctx).InstanceID(), - } -} diff --git a/internal/api/grpc/resources/action/v3alpha/target_integration_test.go b/internal/api/grpc/resources/action/v3alpha/target_integration_test.go index bda54bf862..c94c080674 100644 --- a/internal/api/grpc/resources/action/v3alpha/target_integration_test.go +++ b/internal/api/grpc/resources/action/v3alpha/target_integration_test.go @@ -5,6 +5,7 @@ package action_test import ( "context" "fmt" + object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha" "testing" "time" @@ -15,13 +16,13 @@ import ( "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/integration" - object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha" action "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha" resource_object "github.com/zitadel/zitadel/pkg/grpc/resources/object/v3alpha" ) func TestServer_CreateTarget(t *testing.T) { - ensureFeatureEnabled(t) + _, instanceID, _, isolatedIAMOwnerCTX := Tester.UseIsolatedInstance(t, IAMOwnerCTX, SystemCTX) + ensureFeatureEnabled(t, isolatedIAMOwnerCTX) tests := []struct { name string ctx context.Context @@ -39,7 +40,7 @@ func TestServer_CreateTarget(t *testing.T) { }, { name: "empty name", - ctx: CTX, + ctx: isolatedIAMOwnerCTX, req: &action.Target{ Name: "", }, @@ -47,7 +48,7 @@ func TestServer_CreateTarget(t *testing.T) { }, { name: "empty type", - ctx: CTX, + ctx: isolatedIAMOwnerCTX, req: &action.Target{ Name: fmt.Sprint(time.Now().UnixNano() + 1), TargetType: nil, @@ -56,7 +57,7 @@ func TestServer_CreateTarget(t *testing.T) { }, { name: "empty webhook url", - ctx: CTX, + ctx: isolatedIAMOwnerCTX, req: &action.Target{ Name: fmt.Sprint(time.Now().UnixNano() + 1), TargetType: &action.Target_RestWebhook{ @@ -67,7 +68,7 @@ func TestServer_CreateTarget(t *testing.T) { }, { name: "empty request response url", - ctx: CTX, + ctx: isolatedIAMOwnerCTX, req: &action.Target{ Name: fmt.Sprint(time.Now().UnixNano() + 1), TargetType: &action.Target_RestCall{ @@ -78,7 +79,7 @@ func TestServer_CreateTarget(t *testing.T) { }, { name: "empty timeout", - ctx: CTX, + ctx: isolatedIAMOwnerCTX, req: &action.Target{ Name: fmt.Sprint(time.Now().UnixNano() + 1), Endpoint: "https://example.com", @@ -91,7 +92,7 @@ func TestServer_CreateTarget(t *testing.T) { }, { name: "async, ok", - ctx: CTX, + ctx: isolatedIAMOwnerCTX, req: &action.Target{ Name: fmt.Sprint(time.Now().UnixNano() + 1), Endpoint: "https://example.com", @@ -101,16 +102,16 @@ func TestServer_CreateTarget(t *testing.T) { Timeout: durationpb.New(10 * time.Second), }, want: &resource_object.Details{ - ChangeDate: timestamppb.Now(), + Changed: timestamppb.Now(), Owner: &object.Owner{ Type: object.OwnerType_OWNER_TYPE_INSTANCE, - Id: Tester.Instance.InstanceID(), + Id: instanceID, }, }, }, { name: "webhook, ok", - ctx: CTX, + ctx: isolatedIAMOwnerCTX, req: &action.Target{ Name: fmt.Sprint(time.Now().UnixNano() + 1), Endpoint: "https://example.com", @@ -122,16 +123,16 @@ func TestServer_CreateTarget(t *testing.T) { Timeout: durationpb.New(10 * time.Second), }, want: &resource_object.Details{ - ChangeDate: timestamppb.Now(), + Changed: timestamppb.Now(), Owner: &object.Owner{ Type: object.OwnerType_OWNER_TYPE_INSTANCE, - Id: Tester.Instance.InstanceID(), + Id: instanceID, }, }, }, { name: "webhook, interrupt on error, ok", - ctx: CTX, + ctx: isolatedIAMOwnerCTX, req: &action.Target{ Name: fmt.Sprint(time.Now().UnixNano() + 1), Endpoint: "https://example.com", @@ -143,16 +144,16 @@ func TestServer_CreateTarget(t *testing.T) { Timeout: durationpb.New(10 * time.Second), }, want: &resource_object.Details{ - ChangeDate: timestamppb.Now(), + Changed: timestamppb.Now(), Owner: &object.Owner{ Type: object.OwnerType_OWNER_TYPE_INSTANCE, - Id: Tester.Instance.InstanceID(), + Id: instanceID, }, }, }, { name: "call, ok", - ctx: CTX, + ctx: isolatedIAMOwnerCTX, req: &action.Target{ Name: fmt.Sprint(time.Now().UnixNano() + 1), Endpoint: "https://example.com", @@ -164,17 +165,17 @@ func TestServer_CreateTarget(t *testing.T) { Timeout: durationpb.New(10 * time.Second), }, want: &resource_object.Details{ - ChangeDate: timestamppb.Now(), + Changed: timestamppb.Now(), Owner: &object.Owner{ Type: object.OwnerType_OWNER_TYPE_INSTANCE, - Id: Tester.Instance.InstanceID(), + Id: instanceID, }, }, }, { name: "call, interruptOnError, ok", - ctx: CTX, + ctx: isolatedIAMOwnerCTX, req: &action.Target{ Name: fmt.Sprint(time.Now().UnixNano() + 1), Endpoint: "https://example.com", @@ -186,17 +187,17 @@ func TestServer_CreateTarget(t *testing.T) { Timeout: durationpb.New(10 * time.Second), }, want: &resource_object.Details{ - ChangeDate: timestamppb.Now(), + Changed: timestamppb.Now(), Owner: &object.Owner{ Type: object.OwnerType_OWNER_TYPE_INSTANCE, - Id: Tester.Instance.InstanceID(), + Id: instanceID, }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := Client.CreateTarget(tt.ctx, &action.CreateTargetRequest{Target: tt.req}) + got, err := Tester.Client.ActionV3.CreateTarget(tt.ctx, &action.CreateTargetRequest{Target: tt.req}) if tt.wantErr { require.Error(t, err) return @@ -208,7 +209,8 @@ func TestServer_CreateTarget(t *testing.T) { } func TestServer_PatchTarget(t *testing.T) { - ensureFeatureEnabled(t) + _, instanceID, _, isolatedIAMOwnerCTX := Tester.UseIsolatedInstance(t, IAMOwnerCTX, SystemCTX) + ensureFeatureEnabled(t, isolatedIAMOwnerCTX) type args struct { ctx context.Context req *action.PatchTargetRequest @@ -223,7 +225,7 @@ func TestServer_PatchTarget(t *testing.T) { { name: "missing permission", prepare: func(request *action.PatchTargetRequest) error { - targetID := Tester.CreateTarget(CTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetDetails().GetId() + targetID := Tester.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetDetails().GetId() request.Id = targetID return nil }, @@ -244,7 +246,7 @@ func TestServer_PatchTarget(t *testing.T) { return nil }, args: args{ - ctx: CTX, + ctx: isolatedIAMOwnerCTX, req: &action.PatchTargetRequest{ Target: &action.PatchTarget{ Name: gu.Ptr(fmt.Sprint(time.Now().UnixNano() + 1)), @@ -256,12 +258,12 @@ func TestServer_PatchTarget(t *testing.T) { { name: "change name, ok", prepare: func(request *action.PatchTargetRequest) error { - targetID := Tester.CreateTarget(CTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetDetails().GetId() + targetID := Tester.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetDetails().GetId() request.Id = targetID return nil }, args: args{ - ctx: CTX, + ctx: isolatedIAMOwnerCTX, req: &action.PatchTargetRequest{ Target: &action.PatchTarget{ Name: gu.Ptr(fmt.Sprint(time.Now().UnixNano() + 1)), @@ -269,22 +271,22 @@ func TestServer_PatchTarget(t *testing.T) { }, }, want: &resource_object.Details{ - ChangeDate: timestamppb.Now(), + Changed: timestamppb.Now(), Owner: &object.Owner{ Type: object.OwnerType_OWNER_TYPE_INSTANCE, - Id: Tester.Instance.InstanceID(), + Id: instanceID, }, }, }, { name: "change type, ok", prepare: func(request *action.PatchTargetRequest) error { - targetID := Tester.CreateTarget(CTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetDetails().GetId() + targetID := Tester.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetDetails().GetId() request.Id = targetID return nil }, args: args{ - ctx: CTX, + ctx: isolatedIAMOwnerCTX, req: &action.PatchTargetRequest{ Target: &action.PatchTarget{ TargetType: &action.PatchTarget_RestCall{ @@ -296,22 +298,22 @@ func TestServer_PatchTarget(t *testing.T) { }, }, want: &resource_object.Details{ - ChangeDate: timestamppb.Now(), + Changed: timestamppb.Now(), Owner: &object.Owner{ Type: object.OwnerType_OWNER_TYPE_INSTANCE, - Id: Tester.Instance.InstanceID(), + Id: instanceID, }, }, }, { name: "change url, ok", prepare: func(request *action.PatchTargetRequest) error { - targetID := Tester.CreateTarget(CTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetDetails().GetId() + targetID := Tester.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetDetails().GetId() request.Id = targetID return nil }, args: args{ - ctx: CTX, + ctx: isolatedIAMOwnerCTX, req: &action.PatchTargetRequest{ Target: &action.PatchTarget{ Endpoint: gu.Ptr("https://example.com/hooks/new"), @@ -319,22 +321,22 @@ func TestServer_PatchTarget(t *testing.T) { }, }, want: &resource_object.Details{ - ChangeDate: timestamppb.Now(), + Changed: timestamppb.Now(), Owner: &object.Owner{ Type: object.OwnerType_OWNER_TYPE_INSTANCE, - Id: Tester.Instance.InstanceID(), + Id: instanceID, }, }, }, { name: "change timeout, ok", prepare: func(request *action.PatchTargetRequest) error { - targetID := Tester.CreateTarget(CTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetDetails().GetId() + targetID := Tester.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetDetails().GetId() request.Id = targetID return nil }, args: args{ - ctx: CTX, + ctx: isolatedIAMOwnerCTX, req: &action.PatchTargetRequest{ Target: &action.PatchTarget{ Timeout: durationpb.New(20 * time.Second), @@ -342,22 +344,22 @@ func TestServer_PatchTarget(t *testing.T) { }, }, want: &resource_object.Details{ - ChangeDate: timestamppb.Now(), + Changed: timestamppb.Now(), Owner: &object.Owner{ Type: object.OwnerType_OWNER_TYPE_INSTANCE, - Id: Tester.Instance.InstanceID(), + Id: instanceID, }, }, }, { name: "change type async, ok", prepare: func(request *action.PatchTargetRequest) error { - targetID := Tester.CreateTarget(CTX, t, "", "https://example.com", domain.TargetTypeAsync, false).GetDetails().GetId() + targetID := Tester.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeAsync, false).GetDetails().GetId() request.Id = targetID return nil }, args: args{ - ctx: CTX, + ctx: isolatedIAMOwnerCTX, req: &action.PatchTargetRequest{ Target: &action.PatchTarget{ TargetType: &action.PatchTarget_RestAsync{ @@ -367,10 +369,10 @@ func TestServer_PatchTarget(t *testing.T) { }, }, want: &resource_object.Details{ - ChangeDate: timestamppb.Now(), + Changed: timestamppb.Now(), Owner: &object.Owner{ Type: object.OwnerType_OWNER_TYPE_INSTANCE, - Id: Tester.Instance.InstanceID(), + Id: instanceID, }, }, }, @@ -380,8 +382,8 @@ func TestServer_PatchTarget(t *testing.T) { err := tt.prepare(tt.args.req) require.NoError(t, err) // We want to have the same response no matter how often we call the function - Client.PatchTarget(tt.args.ctx, tt.args.req) - got, err := Client.PatchTarget(tt.args.ctx, tt.args.req) + Tester.Client.ActionV3.PatchTarget(tt.args.ctx, tt.args.req) + got, err := Tester.Client.ActionV3.PatchTarget(tt.args.ctx, tt.args.req) if tt.wantErr { require.Error(t, err) return @@ -393,8 +395,9 @@ func TestServer_PatchTarget(t *testing.T) { } func TestServer_DeleteTarget(t *testing.T) { - ensureFeatureEnabled(t) - target := Tester.CreateTarget(CTX, t, "", "https://example.com", domain.TargetTypeWebhook, false) + _, instanceID, _, isolatedIAMOwnerCTX := Tester.UseIsolatedInstance(t, IAMOwnerCTX, SystemCTX) + ensureFeatureEnabled(t, isolatedIAMOwnerCTX) + target := Tester.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeWebhook, false) tests := []struct { name string ctx context.Context @@ -412,7 +415,7 @@ func TestServer_DeleteTarget(t *testing.T) { }, { name: "empty id", - ctx: CTX, + ctx: isolatedIAMOwnerCTX, req: &action.DeleteTargetRequest{ Id: "", }, @@ -420,22 +423,22 @@ func TestServer_DeleteTarget(t *testing.T) { }, { name: "delete target", - ctx: CTX, + ctx: isolatedIAMOwnerCTX, req: &action.DeleteTargetRequest{ Id: target.GetDetails().GetId(), }, want: &resource_object.Details{ - ChangeDate: timestamppb.Now(), + Changed: timestamppb.Now(), Owner: &object.Owner{ Type: object.OwnerType_OWNER_TYPE_INSTANCE, - Id: Tester.Instance.InstanceID(), + Id: instanceID, }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := Client.DeleteTarget(tt.ctx, tt.req) + got, err := Tester.Client.ActionV3.DeleteTarget(tt.ctx, tt.req) if tt.wantErr { require.Error(t, err) return diff --git a/internal/api/grpc/resources/object/v3alpha/converter.go b/internal/api/grpc/resources/object/v3alpha/converter.go index 41f81b595f..c2dc7bcc6d 100644 --- a/internal/api/grpc/resources/object/v3alpha/converter.go +++ b/internal/api/grpc/resources/object/v3alpha/converter.go @@ -1,21 +1,77 @@ package object import ( + "fmt" + "google.golang.org/protobuf/types/known/timestamppb" + "github.com/zitadel/zitadel/internal/config/systemdefaults" "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/query" + "github.com/zitadel/zitadel/internal/zerrors" object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha" - resources_object "github.com/zitadel/zitadel/pkg/grpc/resources/object/v3alpha" + resource_object "github.com/zitadel/zitadel/pkg/grpc/resources/object/v3alpha" ) -func DomainToDetailsPb(objectDetail *domain.ObjectDetails, owner *object.Owner, id string) *resources_object.Details { - details := &resources_object.Details{ - Id: id, - Sequence: objectDetail.Sequence, - Owner: owner, +func DomainToDetailsPb(objectDetail *domain.ObjectDetails, ownerType object.OwnerType, ownerId string) *resource_object.Details { + details := &resource_object.Details{ + Id: objectDetail.ID, + Owner: &object.Owner{ + Type: ownerType, + Id: ownerId, + }, } if !objectDetail.EventDate.IsZero() { - details.ChangeDate = timestamppb.New(objectDetail.EventDate) + details.Changed = timestamppb.New(objectDetail.EventDate) + } + if !objectDetail.CreationDate.IsZero() { + details.Created = timestamppb.New(objectDetail.CreationDate) } return details } + +func ToSearchDetailsPb(request query.SearchRequest, response query.SearchResponse) *resource_object.ListDetails { + details := &resource_object.ListDetails{ + AppliedLimit: request.Limit, + TotalResult: response.Count, + Timestamp: timestamppb.New(response.EventCreatedAt), + } + + return details +} + +func TextMethodPbToQuery(method resource_object.TextFilterMethod) query.TextComparison { + switch method { + case resource_object.TextFilterMethod_TEXT_FILTER_METHOD_EQUALS: + return query.TextEquals + case resource_object.TextFilterMethod_TEXT_FILTER_METHOD_EQUALS_IGNORE_CASE: + return query.TextEqualsIgnoreCase + case resource_object.TextFilterMethod_TEXT_FILTER_METHOD_STARTS_WITH: + return query.TextStartsWith + case resource_object.TextFilterMethod_TEXT_FILTER_METHOD_STARTS_WITH_IGNORE_CASE: + return query.TextStartsWithIgnoreCase + case resource_object.TextFilterMethod_TEXT_FILTER_METHOD_CONTAINS: + return query.TextContains + default: + return -1 + } +} + +func SearchQueryPbToQuery(defaults systemdefaults.SystemDefaults, query *resource_object.SearchQuery) (offset, limit uint64, asc bool, err error) { + limit = defaults.DefaultQueryLimit + asc = true + if query == nil { + return 0, limit, asc, nil + } + offset = query.Offset + if query.Desc { + asc = false + } + 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 +} diff --git a/internal/api/grpc/server/middleware/instance_interceptor.go b/internal/api/grpc/server/middleware/instance_interceptor.go index 07ae1ba277..6232ed4cd9 100644 --- a/internal/api/grpc/server/middleware/instance_interceptor.go +++ b/internal/api/grpc/server/middleware/instance_interceptor.go @@ -17,6 +17,7 @@ import ( "github.com/zitadel/zitadel/internal/i18n" "github.com/zitadel/zitadel/internal/telemetry/tracing" "github.com/zitadel/zitadel/internal/zerrors" + object_v3 "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha" ) const ( @@ -34,33 +35,67 @@ func InstanceInterceptor(verifier authz.InstanceVerifier, externalDomain string, func setInstance(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler, verifier authz.InstanceVerifier, externalDomain string, translator *i18n.Translator, idFromRequestsServices ...string) (_ interface{}, err error) { interceptorCtx, span := tracing.NewServerInterceptorSpan(ctx) defer func() { span.EndWithError(err) }() + for _, service := range idFromRequestsServices { if !strings.HasPrefix(service, "/") { service = "/" + service } if strings.HasPrefix(info.FullMethod, service) { - withInstanceIDProperty, ok := req.(interface{ GetInstanceId() string }) + withInstanceIDProperty, ok := req.(interface { + GetInstanceId() string + }) if !ok { return handler(ctx, req) } - ctx = authz.WithInstanceID(ctx, withInstanceIDProperty.GetInstanceId()) - instance, err := verifier.InstanceByID(ctx) - if err != nil { - notFoundErr := new(zerrors.NotFoundError) - if errors.As(err, ¬FoundErr) { - notFoundErr.Message = translator.LocalizeFromCtx(ctx, notFoundErr.GetMessage(), nil) - } - return nil, status.Error(codes.NotFound, err.Error()) - } - return handler(authz.WithInstance(ctx, instance), req) + return addInstanceByID(interceptorCtx, req, handler, verifier, translator, withInstanceIDProperty.GetInstanceId()) } } + explicitInstanceRequest, ok := req.(interface { + GetInstance() *object_v3.Instance + }) + if ok { + instance := explicitInstanceRequest.GetInstance() + if id := instance.GetId(); id != "" { + return addInstanceByID(interceptorCtx, req, handler, verifier, translator, id) + } + if domain := instance.GetDomain(); domain != "" { + return addInstanceByDomain(interceptorCtx, req, handler, verifier, translator, domain) + } + } + return addInstanceByRequestedHost(interceptorCtx, req, handler, verifier, translator, externalDomain) +} + +func addInstanceByID(ctx context.Context, req interface{}, handler grpc.UnaryHandler, verifier authz.InstanceVerifier, translator *i18n.Translator, id string) (interface{}, error) { + instance, err := verifier.InstanceByID(ctx, id) + if err != nil { + notFoundErr := new(zerrors.ZitadelError) + if errors.As(err, ¬FoundErr) { + notFoundErr.Message = translator.LocalizeFromCtx(ctx, notFoundErr.GetMessage(), nil) + } + return nil, status.Error(codes.NotFound, fmt.Errorf("unable to set instance using id %s: %w", id, notFoundErr).Error()) + } + return handler(authz.WithInstance(ctx, instance), req) +} + +func addInstanceByDomain(ctx context.Context, req interface{}, handler grpc.UnaryHandler, verifier authz.InstanceVerifier, translator *i18n.Translator, domain string) (interface{}, error) { + instance, err := verifier.InstanceByHost(ctx, domain, "") + if err != nil { + notFoundErr := new(zerrors.NotFoundError) + if errors.As(err, ¬FoundErr) { + notFoundErr.Message = translator.LocalizeFromCtx(ctx, notFoundErr.GetMessage(), nil) + } + return nil, status.Error(codes.NotFound, fmt.Errorf("unable to set instance using domain %s: %w", domain, notFoundErr).Error()) + } + return handler(authz.WithInstance(ctx, instance), req) +} + +func addInstanceByRequestedHost(ctx context.Context, req interface{}, handler grpc.UnaryHandler, verifier authz.InstanceVerifier, translator *i18n.Translator, externalDomain string) (interface{}, error) { requestContext := zitadel_http.DomainContext(ctx) if requestContext.InstanceHost == "" { - logging.WithFields("origin", requestContext.Origin(), "externalDomain", externalDomain).WithError(err).Error("unable to set instance") + logging.WithFields("origin", requestContext.Origin(), "externalDomain", externalDomain).Error("unable to set instance") return nil, status.Error(codes.NotFound, "no instanceHost specified") } - instance, err := verifier.InstanceByHost(interceptorCtx, requestContext.InstanceHost, requestContext.PublicHost) + instance, err := verifier.InstanceByHost(ctx, requestContext.InstanceHost, requestContext.PublicHost) if err != nil { origin := zitadel_http.DomainContext(ctx) logging.WithFields("origin", requestContext.Origin(), "externalDomain", externalDomain).WithError(err).Error("unable to set instance") @@ -72,6 +107,5 @@ func setInstance(ctx context.Context, req interface{}, info *grpc.UnaryServerInf } return nil, status.Error(codes.NotFound, fmt.Sprintf("unable to set instance using origin %s (ExternalDomain is %s)", origin, externalDomain)) } - span.End() return handler(authz.WithInstance(ctx, instance), req) } diff --git a/internal/api/grpc/server/middleware/instance_interceptor_test.go b/internal/api/grpc/server/middleware/instance_interceptor_test.go index 211444a707..cc71de75f7 100644 --- a/internal/api/grpc/server/middleware/instance_interceptor_test.go +++ b/internal/api/grpc/server/middleware/instance_interceptor_test.go @@ -13,6 +13,7 @@ import ( "github.com/zitadel/zitadel/internal/api/authz" http_util "github.com/zitadel/zitadel/internal/api/http" "github.com/zitadel/zitadel/internal/feature" + object_v3 "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha" ) func Test_setInstance(t *testing.T) { @@ -69,6 +70,135 @@ func Test_setInstance(t *testing.T) { err: false, }, }, + { + "explicit instance unset, hostname not found, error", + args{ + ctx: http_util.WithDomainContext(context.Background(), &http_util.DomainCtx{InstanceHost: "host2"}), + req: &mockRequestWithExplicitInstance{}, + verifier: &mockInstanceVerifier{instanceHost: "host"}, + }, + res{ + want: nil, + err: true, + }, + }, + { + "explicit instance unset, invalid host, error", + args{ + ctx: http_util.WithDomainContext(context.Background(), &http_util.DomainCtx{InstanceHost: "host2"}), + req: &mockRequestWithExplicitInstance{}, + verifier: &mockInstanceVerifier{instanceHost: "host"}, + }, + res{ + want: nil, + err: true, + }, + }, + { + "explicit instance unset, valid host", + args{ + ctx: http_util.WithDomainContext(context.Background(), &http_util.DomainCtx{InstanceHost: "host"}), + req: &mockRequestWithExplicitInstance{}, + verifier: &mockInstanceVerifier{instanceHost: "host"}, + handler: func(ctx context.Context, req interface{}) (interface{}, error) { + return req, nil + }, + }, + res{ + want: &mockRequestWithExplicitInstance{}, + err: false, + }, + }, + { + name: "explicit instance set, id not found, error", + args: args{ + ctx: context.Background(), + req: &mockRequestWithExplicitInstance{ + instance: object_v3.Instance{ + Property: &object_v3.Instance_Id{ + Id: "not existing instance id", + }, + }, + }, + verifier: &mockInstanceVerifier{id: "existing instance id"}, + }, + res: res{ + want: nil, + err: true, + }, + }, + { + name: "explicit instance set, id found, ok", + args: args{ + ctx: context.Background(), + req: &mockRequestWithExplicitInstance{ + instance: object_v3.Instance{ + Property: &object_v3.Instance_Id{ + Id: "existing instance id", + }, + }, + }, + verifier: &mockInstanceVerifier{id: "existing instance id"}, + handler: func(ctx context.Context, req interface{}) (interface{}, error) { + return req, nil + }, + }, + res: res{ + want: &mockRequestWithExplicitInstance{ + instance: object_v3.Instance{ + Property: &object_v3.Instance_Id{ + Id: "existing instance id", + }, + }, + }, + err: false, + }, + }, + { + name: "explicit instance set, domain not found, error", + args: args{ + ctx: context.Background(), + req: &mockRequestWithExplicitInstance{ + instance: object_v3.Instance{ + Property: &object_v3.Instance_Domain{ + Domain: "not existing instance domain", + }, + }, + }, + verifier: &mockInstanceVerifier{instanceHost: "existing instance domain"}, + }, + res: res{ + want: nil, + err: true, + }, + }, + { + name: "explicit instance set, domain found, ok", + args: args{ + ctx: context.Background(), + req: &mockRequestWithExplicitInstance{ + instance: object_v3.Instance{ + Property: &object_v3.Instance_Domain{ + Domain: "existing instance domain", + }, + }, + }, + verifier: &mockInstanceVerifier{instanceHost: "existing instance domain"}, + handler: func(ctx context.Context, req interface{}) (interface{}, error) { + return req, nil + }, + }, + res: res{ + want: &mockRequestWithExplicitInstance{ + instance: object_v3.Instance{ + Property: &object_v3.Instance_Domain{ + Domain: "existing instance domain", + }, + }, + }, + err: false, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -86,7 +216,16 @@ func Test_setInstance(t *testing.T) { type mockRequest struct{} +type mockRequestWithExplicitInstance struct { + instance object_v3.Instance +} + +func (m *mockRequestWithExplicitInstance) GetInstance() *object_v3.Instance { + return &m.instance +} + type mockInstanceVerifier struct { + id string instanceHost string publicHost string } @@ -104,7 +243,12 @@ func (m *mockInstanceVerifier) InstanceByHost(_ context.Context, instanceHost, p return &mockInstance{}, nil } -func (m *mockInstanceVerifier) InstanceByID(context.Context) (authz.Instance, error) { return nil, nil } +func (m *mockInstanceVerifier) InstanceByID(_ context.Context, id string) (authz.Instance, error) { + if id != m.id { + return nil, fmt.Errorf("not found") + } + return &mockInstance{}, nil +} type mockInstance struct{} diff --git a/internal/api/grpc/server/server.go b/internal/api/grpc/server/server.go index 5408ae257f..6b17e929bd 100644 --- a/internal/api/grpc/server/server.go +++ b/internal/api/grpc/server/server.go @@ -2,7 +2,6 @@ package server import ( "crypto/tls" - grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware" "google.golang.org/grpc" "google.golang.org/grpc/credentials" diff --git a/internal/api/grpc/system/instance.go b/internal/api/grpc/system/instance.go index d684c37e86..a5dd7b81bc 100644 --- a/internal/api/grpc/system/instance.go +++ b/internal/api/grpc/system/instance.go @@ -3,7 +3,6 @@ package system import ( "context" - "github.com/zitadel/zitadel/internal/api/authz" instance_grpc "github.com/zitadel/zitadel/internal/api/grpc/instance" "github.com/zitadel/zitadel/internal/api/grpc/member" "github.com/zitadel/zitadel/internal/api/grpc/object" @@ -151,12 +150,6 @@ func (s *Server) ListDomains(ctx context.Context, req *system_pb.ListDomainsRequ } func (s *Server) AddDomain(ctx context.Context, req *system_pb.AddDomainRequest) (*system_pb.AddDomainResponse, error) { - instance, err := s.query.InstanceByID(ctx) - if err != nil { - return nil, err - } - ctx = authz.WithInstance(ctx, instance) - details, err := s.command.AddInstanceDomain(ctx, req.Domain) if err != nil { return nil, err diff --git a/internal/api/http/middleware/instance_interceptor_test.go b/internal/api/http/middleware/instance_interceptor_test.go index 7a2d7aeb26..51c0fb9a10 100644 --- a/internal/api/http/middleware/instance_interceptor_test.go +++ b/internal/api/http/middleware/instance_interceptor_test.go @@ -242,7 +242,7 @@ func (m *mockInstanceVerifier) InstanceByHost(_ context.Context, instanceHost, p return &mockInstance{}, nil } -func (m *mockInstanceVerifier) InstanceByID(context.Context) (authz.Instance, error) { +func (m *mockInstanceVerifier) InstanceByID(context.Context, string) (authz.Instance, error) { return nil, nil } diff --git a/internal/command/action_v2_execution_test.go b/internal/command/action_v2_execution_test.go index eb6cd21c31..be05929695 100644 --- a/internal/command/action_v2_execution_test.go +++ b/internal/command/action_v2_execution_test.go @@ -203,6 +203,7 @@ func TestCommands_SetExecutionRequest(t *testing.T) { res{ details: &domain.ObjectDetails{ ResourceOwner: "instance", + ID: "request/method", }, }, }, @@ -251,6 +252,7 @@ func TestCommands_SetExecutionRequest(t *testing.T) { res{ details: &domain.ObjectDetails{ ResourceOwner: "instance", + ID: "request/service", }, }, }, @@ -298,6 +300,7 @@ func TestCommands_SetExecutionRequest(t *testing.T) { res{ details: &domain.ObjectDetails{ ResourceOwner: "instance", + ID: "request", }, }, }, @@ -381,6 +384,7 @@ func TestCommands_SetExecutionRequest(t *testing.T) { res{ details: &domain.ObjectDetails{ ResourceOwner: "instance", + ID: "request/method", }, }, }, @@ -464,6 +468,7 @@ func TestCommands_SetExecutionRequest(t *testing.T) { res{ details: &domain.ObjectDetails{ ResourceOwner: "instance", + ID: "request/service", }, }, }, @@ -545,6 +550,86 @@ func TestCommands_SetExecutionRequest(t *testing.T) { res{ details: &domain.ObjectDetails{ ResourceOwner: "instance", + ID: "request", + }, + }, + }, + { + "push ok, remove all targets", + fields{ + eventstore: expectEventstore( + expectFilter( // execution has targets + eventFromEventPusher( + execution.NewSetEventV2(context.Background(), + execution.NewAggregate("request", "instance"), + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, + ), + ), + ), + expectPush( + execution.NewSetEventV2(context.Background(), + execution.NewAggregate("request", "instance"), + []*execution.Target{}, + ), + ), + ), + }, + args{ + ctx: context.Background(), + cond: &ExecutionAPICondition{ + "", + "", + true, + }, + set: &SetExecution{ + Targets: []*execution.Target{}, + }, + resourceOwner: "instance", + }, + res{ + details: &domain.ObjectDetails{ + ResourceOwner: "instance", + ID: "request", + }, + }, + }, + { + "push ok, unchanged execution", + fields{ + eventstore: expectEventstore( + expectFilter( // execution has targets + eventFromEventPusher( + execution.NewSetEventV2(context.Background(), + execution.NewAggregate("request", "instance"), + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, + ), + ), + ), + ), + }, + args{ + ctx: context.Background(), + cond: &ExecutionAPICondition{ + "", + "", + true, + }, + set: &SetExecution{ + Targets: []*execution.Target{{ + Type: domain.ExecutionTargetTypeTarget, + Target: "target", + }}, + }, + resourceOwner: "instance", + }, + res{ + details: &domain.ObjectDetails{ + ResourceOwner: "instance", + ID: "request", }, }, }, @@ -641,7 +726,7 @@ func TestCommands_SetExecutionRequest(t *testing.T) { t.Errorf("got wrong err: %v ", err) } if tt.res.err == nil { - assert.Equal(t, tt.res.details, details) + assertObjectDetails(t, tt.res.details, details) } }) } @@ -876,6 +961,7 @@ func TestCommands_SetExecutionResponse(t *testing.T) { res{ details: &domain.ObjectDetails{ ResourceOwner: "instance", + ID: "response/method", }, }, }, @@ -915,6 +1001,7 @@ func TestCommands_SetExecutionResponse(t *testing.T) { res{ details: &domain.ObjectDetails{ ResourceOwner: "instance", + ID: "response/service", }, }, }, @@ -953,6 +1040,86 @@ func TestCommands_SetExecutionResponse(t *testing.T) { res{ details: &domain.ObjectDetails{ ResourceOwner: "instance", + ID: "response", + }, + }, + }, + { + "push ok, remove all targets", + fields{ + eventstore: expectEventstore( + expectFilter( // execution has targets + eventFromEventPusher( + execution.NewSetEventV2(context.Background(), + execution.NewAggregate("response", "instance"), + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, + ), + ), + ), + expectPush( + execution.NewSetEventV2(context.Background(), + execution.NewAggregate("response", "instance"), + []*execution.Target{}, + ), + ), + ), + }, + args{ + ctx: context.Background(), + cond: &ExecutionAPICondition{ + "", + "", + true, + }, + set: &SetExecution{ + Targets: []*execution.Target{}, + }, + resourceOwner: "instance", + }, + res{ + details: &domain.ObjectDetails{ + ResourceOwner: "instance", + ID: "response", + }, + }, + }, + { + "push ok, unchanged execution", + fields{ + eventstore: expectEventstore( + expectFilter( // execution has targets + eventFromEventPusher( + execution.NewSetEventV2(context.Background(), + execution.NewAggregate("response", "instance"), + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, + ), + ), + ), + ), + }, + args{ + ctx: context.Background(), + cond: &ExecutionAPICondition{ + "", + "", + true, + }, + set: &SetExecution{ + Targets: []*execution.Target{{ + Type: domain.ExecutionTargetTypeTarget, + Target: "target", + }}, + }, + resourceOwner: "instance", + }, + res{ + details: &domain.ObjectDetails{ + ResourceOwner: "instance", + ID: "response", }, }, }, @@ -1049,7 +1216,7 @@ func TestCommands_SetExecutionResponse(t *testing.T) { t.Errorf("got wrong err: %v ", err) } if tt.res.err == nil { - assert.Equal(t, tt.res.details, details) + assertObjectDetails(t, tt.res.details, details) } }) } @@ -1288,6 +1455,7 @@ func TestCommands_SetExecutionEvent(t *testing.T) { res{ details: &domain.ObjectDetails{ ResourceOwner: "instance", + ID: "event/event", }, }, }, @@ -1327,6 +1495,7 @@ func TestCommands_SetExecutionEvent(t *testing.T) { res{ details: &domain.ObjectDetails{ ResourceOwner: "instance", + ID: "event/group.*", }, }, }, @@ -1365,6 +1534,86 @@ func TestCommands_SetExecutionEvent(t *testing.T) { res{ details: &domain.ObjectDetails{ ResourceOwner: "instance", + ID: "event", + }, + }, + }, + { + "push ok, remove all targets", + fields{ + eventstore: expectEventstore( + expectFilter( // execution has targets + eventFromEventPusher( + execution.NewSetEventV2(context.Background(), + execution.NewAggregate("event", "instance"), + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, + ), + ), + ), + expectPush( + execution.NewSetEventV2(context.Background(), + execution.NewAggregate("event", "instance"), + []*execution.Target{}, + ), + ), + ), + }, + args{ + ctx: context.Background(), + cond: &ExecutionEventCondition{ + "", + "", + true, + }, + set: &SetExecution{ + Targets: []*execution.Target{}, + }, + resourceOwner: "instance", + }, + res{ + details: &domain.ObjectDetails{ + ResourceOwner: "instance", + ID: "event", + }, + }, + }, + { + "push ok, unchanged execution", + fields{ + eventstore: expectEventstore( + expectFilter( // execution has targets + eventFromEventPusher( + execution.NewSetEventV2(context.Background(), + execution.NewAggregate("event", "instance"), + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, + ), + ), + ), + ), + }, + args{ + ctx: context.Background(), + cond: &ExecutionEventCondition{ + "", + "", + true, + }, + set: &SetExecution{ + Targets: []*execution.Target{{ + Type: domain.ExecutionTargetTypeTarget, + Target: "target", + }}, + }, + resourceOwner: "instance", + }, + res{ + details: &domain.ObjectDetails{ + ResourceOwner: "instance", + ID: "event", }, }, }, @@ -1461,7 +1710,7 @@ func TestCommands_SetExecutionEvent(t *testing.T) { t.Errorf("got wrong err: %v ", err) } if tt.res.err == nil { - assert.Equal(t, tt.res.details, details) + assertObjectDetails(t, tt.res.details, details) } }) } @@ -1643,6 +1892,80 @@ func TestCommands_SetExecutionFunction(t *testing.T) { res{ details: &domain.ObjectDetails{ ResourceOwner: "instance", + ID: "function/function", + }, + }, + }, + { + "push ok, remove all targets", + fields{ + actionFunctionExists: existsMock(true), + eventstore: expectEventstore( + expectFilter( // execution has targets + eventFromEventPusher( + execution.NewSetEventV2(context.Background(), + execution.NewAggregate("function/function", "instance"), + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, + ), + ), + ), + expectPush( + execution.NewSetEventV2(context.Background(), + execution.NewAggregate("function/function", "instance"), + []*execution.Target{}, + ), + ), + ), + }, + args{ + ctx: context.Background(), + cond: "function", + set: &SetExecution{ + Targets: []*execution.Target{}, + }, + resourceOwner: "instance", + }, + res{ + details: &domain.ObjectDetails{ + ResourceOwner: "instance", + ID: "function/function", + }, + }, + }, + { + "push ok, unchanged execution", + fields{ + actionFunctionExists: existsMock(true), + eventstore: expectEventstore( + expectFilter( // execution has targets + eventFromEventPusher( + execution.NewSetEventV2(context.Background(), + execution.NewAggregate("function/function", "instance"), + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, + ), + ), + ), + ), + }, + args{ + ctx: context.Background(), + cond: "function", + set: &SetExecution{ + Targets: []*execution.Target{{ + Type: domain.ExecutionTargetTypeTarget, + Target: "target", + }}, + }, + resourceOwner: "instance", + }, + res{ + details: &domain.ObjectDetails{ + ResourceOwner: "instance", + ID: "function/function", }, }, }, @@ -1732,7 +2055,7 @@ func TestCommands_SetExecutionFunction(t *testing.T) { t.Errorf("got wrong err: %v ", err) } if tt.res.err == nil { - assert.Equal(t, tt.res.details, details) + assertObjectDetails(t, tt.res.details, details) } }) } diff --git a/internal/command/action_v2_target.go b/internal/command/action_v2_target.go index 913bfb2299..d1f06b79b2 100644 --- a/internal/command/action_v2_target.go +++ b/internal/command/action_v2_target.go @@ -113,7 +113,6 @@ func (c *Commands) ChangeTarget(ctx context.Context, change *ChangeTarget, resou if err := change.IsValid(); err != nil { return nil, err } - existing, err := c.getTargetWriteModelByID(ctx, change.AggregateID, resourceOwner) if err != nil { return nil, err @@ -121,7 +120,6 @@ func (c *Commands) ChangeTarget(ctx context.Context, change *ChangeTarget, resou if !existing.State.Exists() { return nil, zerrors.ThrowNotFound(nil, "COMMAND-xj14f2cccn", "Errors.Target.NotFound") } - changedEvent := existing.NewChangedEvent( ctx, TargetAggregateFromWriteModel(&existing.WriteModel), diff --git a/internal/command/action_v2_target_test.go b/internal/command/action_v2_target_test.go index ef60baae49..12f76c4629 100644 --- a/internal/command/action_v2_target_test.go +++ b/internal/command/action_v2_target_test.go @@ -202,6 +202,7 @@ func TestCommands_AddTarget(t *testing.T) { id: "id1", details: &domain.ObjectDetails{ ResourceOwner: "instance", + ID: "id1", }, }, }, @@ -235,6 +236,7 @@ func TestCommands_AddTarget(t *testing.T) { id: "id1", details: &domain.ObjectDetails{ ResourceOwner: "instance", + ID: "id1", }, }, }, @@ -254,7 +256,7 @@ func TestCommands_AddTarget(t *testing.T) { } if tt.res.err == nil { assert.Equal(t, tt.res.id, tt.args.add.AggregateID) - assert.Equal(t, tt.res.details, details) + assertObjectDetails(t, tt.res.details, details) } }) } @@ -416,6 +418,7 @@ func TestCommands_ChangeTarget(t *testing.T) { res{ details: &domain.ObjectDetails{ ResourceOwner: "instance", + ID: "id1", }, }, }, @@ -485,6 +488,7 @@ func TestCommands_ChangeTarget(t *testing.T) { res{ details: &domain.ObjectDetails{ ResourceOwner: "instance", + ID: "id1", }, }, }, @@ -528,6 +532,7 @@ func TestCommands_ChangeTarget(t *testing.T) { res{ details: &domain.ObjectDetails{ ResourceOwner: "instance", + ID: "id1", }, }, }, @@ -545,7 +550,7 @@ func TestCommands_ChangeTarget(t *testing.T) { t.Errorf("got wrong err: %v ", err) } if tt.res.err == nil { - assert.Equal(t, tt.res.details, details) + assertObjectDetails(t, tt.res.details, details) } }) } @@ -625,6 +630,7 @@ func TestCommands_DeleteTarget(t *testing.T) { res{ details: &domain.ObjectDetails{ ResourceOwner: "instance", + ID: "id1", }, }, }, @@ -642,7 +648,7 @@ func TestCommands_DeleteTarget(t *testing.T) { t.Errorf("got wrong err: %v ", err) } if tt.res.err == nil { - assert.Equal(t, tt.res.details, details) + assertObjectDetails(t, tt.res.details, details) } }) } diff --git a/internal/command/auth_request_test.go b/internal/command/auth_request_test.go index a3e1db3a28..d097e4f381 100644 --- a/internal/command/auth_request_test.go +++ b/internal/command/auth_request_test.go @@ -629,7 +629,7 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) { } details, got, err := c.LinkSessionToAuthRequest(tt.args.ctx, tt.args.id, tt.args.sessionID, tt.args.sessionToken, tt.args.checkLoginClient) require.ErrorIs(t, err, tt.res.wantErr) - assert.Equal(t, tt.res.details, details) + assertObjectDetails(t, tt.res.details, details) if err == nil { assert.WithinRange(t, got.AuthTime, testNow, testNow) got.AuthTime = time.Time{} @@ -739,7 +739,7 @@ func TestCommands_FailAuthRequest(t *testing.T) { } details, got, err := c.FailAuthRequest(tt.args.ctx, tt.args.id, tt.args.reason) require.ErrorIs(t, err, tt.res.wantErr) - assert.Equal(t, tt.res.details, details) + assertObjectDetails(t, tt.res.details, details) assert.Equal(t, tt.res.authReq, got) }) } diff --git a/internal/command/converter.go b/internal/command/converter.go index 0761f5e19f..e4309a54c1 100644 --- a/internal/command/converter.go +++ b/internal/command/converter.go @@ -10,6 +10,7 @@ func writeModelToObjectDetails(writeModel *eventstore.WriteModel) *domain.Object Sequence: writeModel.ProcessedSequence, ResourceOwner: writeModel.ResourceOwner, EventDate: writeModel.ChangeDate, + ID: writeModel.AggregateID, } } diff --git a/internal/command/device_auth_test.go b/internal/command/device_auth_test.go index 3191e8dce9..2988d058c0 100644 --- a/internal/command/device_auth_test.go +++ b/internal/command/device_auth_test.go @@ -115,7 +115,7 @@ func TestCommands_AddDeviceAuth(t *testing.T) { } gotDetails, err := c.AddDeviceAuth(tt.args.ctx, tt.args.clientID, tt.args.deviceCode, tt.args.userCode, tt.args.expires, tt.args.scopes, tt.args.audience, tt.args.needRefreshToken) require.ErrorIs(t, err, tt.wantErr) - assert.Equal(t, tt.wantDetails, gotDetails) + assertObjectDetails(t, tt.wantDetails, gotDetails) }) } } @@ -254,7 +254,7 @@ func TestCommands_ApproveDeviceAuth(t *testing.T) { } gotDetails, err := c.ApproveDeviceAuth(tt.args.ctx, tt.args.id, tt.args.userID, tt.args.userOrgID, tt.args.authMethods, tt.args.authTime, tt.args.preferredLanguage, tt.args.userAgent) require.ErrorIs(t, err, tt.wantErr) - assert.Equal(t, gotDetails, tt.wantDetails) + assertObjectDetails(t, tt.wantDetails, gotDetails) }) } } @@ -376,7 +376,7 @@ func TestCommands_CancelDeviceAuth(t *testing.T) { } gotDetails, err := c.CancelDeviceAuth(tt.args.ctx, tt.args.id, tt.args.reason) require.ErrorIs(t, err, tt.wantErr) - assert.Equal(t, gotDetails, tt.wantDetails) + assertObjectDetails(t, tt.wantDetails, gotDetails) }) } } diff --git a/internal/command/idp_intent_test.go b/internal/command/idp_intent_test.go index efcc9e0ddc..832e2e9902 100644 --- a/internal/command/idp_intent_test.go +++ b/internal/command/idp_intent_test.go @@ -333,7 +333,7 @@ func TestCommands_CreateIntent(t *testing.T) { } else { assert.Equal(t, tt.res.intentID, "") } - assert.Equal(t, tt.res.details, details) + assertObjectDetails(t, tt.res.details, details) }) } } diff --git a/internal/command/instance_custom_login_text_test.go b/internal/command/instance_custom_login_text_test.go index aa2bf33659..9e460a2f75 100644 --- a/internal/command/instance_custom_login_text_test.go +++ b/internal/command/instance_custom_login_text_test.go @@ -6864,7 +6864,7 @@ func TestCommandSide_SetCustomIAMLoginText(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) } }) } diff --git a/internal/command/instance_custom_message_text_test.go b/internal/command/instance_custom_message_text_test.go index 76f463ae02..a579350d79 100644 --- a/internal/command/instance_custom_message_text_test.go +++ b/internal/command/instance_custom_message_text_test.go @@ -191,7 +191,7 @@ func TestCommandSide_SetDefaultMessageText(t *testing.T) { t.Errorf("got wrong err: %v ", err) t.FailNow() } - assert.Equal(t, tt.res.want, got) + assertObjectDetails(t, tt.res.want, got) }) } } diff --git a/internal/command/instance_debug_notification_file_test.go b/internal/command/instance_debug_notification_file_test.go index 73e8b6bc26..c86d95453b 100644 --- a/internal/command/instance_debug_notification_file_test.go +++ b/internal/command/instance_debug_notification_file_test.go @@ -100,7 +100,7 @@ func TestCommandSide_AddDefaultDebugNotificationProviderFile(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) } }) } @@ -242,7 +242,7 @@ func TestCommandSide_ChangeDebugNotificationProviderFile(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) } }) } @@ -323,7 +323,7 @@ func TestCommandSide_RemoveDebugNotificationProviderFile(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) } }) } diff --git a/internal/command/instance_debug_notification_log_test.go b/internal/command/instance_debug_notification_log_test.go index bac9186023..9190064f60 100644 --- a/internal/command/instance_debug_notification_log_test.go +++ b/internal/command/instance_debug_notification_log_test.go @@ -128,7 +128,7 @@ func TestCommandSide_AddDefaultDebugNotificationProviderLog(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) } }) } @@ -277,7 +277,7 @@ func TestCommandSide_ChangeDebugNotificationProviderLog(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) } }) } @@ -359,7 +359,7 @@ func TestCommandSide_RemoveDebugNotificationProviderLog(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) } }) } diff --git a/internal/command/instance_domain_test.go b/internal/command/instance_domain_test.go index aa844cefa3..3f5e73aedd 100644 --- a/internal/command/instance_domain_test.go +++ b/internal/command/instance_domain_test.go @@ -198,7 +198,7 @@ func TestCommandSide_AddInstanceDomain(t *testing.T) { t.Errorf("got wrong err: %v ", err) return } - assert.Equal(t, tt.res.want, got) + assertObjectDetails(t, tt.res.want, got) }) } } @@ -299,7 +299,7 @@ func TestCommandSide_SetPrimaryInstanceDomain(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) } }) } @@ -425,7 +425,7 @@ func TestCommandSide_RemoveInstanceDomain(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) } }) } diff --git a/internal/command/instance_features_test.go b/internal/command/instance_features_test.go index 4765fc2832..6ca59b00e4 100644 --- a/internal/command/instance_features_test.go +++ b/internal/command/instance_features_test.go @@ -363,7 +363,7 @@ func TestCommands_ResetInstanceFeatures(t *testing.T) { } got, err := c.ResetInstanceFeatures(ctx) require.ErrorIs(t, err, tt.wantErr) - assert.Equal(t, tt.want, got) + assertObjectDetails(t, tt.want, got) }) } } diff --git a/internal/command/instance_idp_test.go b/internal/command/instance_idp_test.go index 0f7998a632..c6181af9f1 100644 --- a/internal/command/instance_idp_test.go +++ b/internal/command/instance_idp_test.go @@ -301,7 +301,7 @@ func TestCommandSide_AddInstanceGenericOAuthIDP(t *testing.T) { } if tt.res.err == nil { assert.Equal(t, tt.res.id, id) - assert.Equal(t, tt.res.want, got) + assertObjectDetails(t, tt.res.want, got) } }) } @@ -619,7 +619,7 @@ func TestCommandSide_UpdateInstanceGenericOAuthIDP(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) } }) } @@ -829,7 +829,7 @@ func TestCommandSide_AddInstanceGenericOIDCIDP(t *testing.T) { } if tt.res.err == nil { assert.Equal(t, tt.res.id, id) - assert.Equal(t, tt.res.want, got) + assertObjectDetails(t, tt.res.want, got) } }) } @@ -1070,7 +1070,7 @@ func TestCommandSide_UpdateInstanceGenericOIDCIDP(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) } }) } @@ -1312,7 +1312,7 @@ func TestCommandSide_MigrateInstanceGenericOIDCToAzureADProvider(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) } }) } @@ -1520,7 +1520,7 @@ func TestCommandSide_MigrateInstanceOIDCToGoogleIDP(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) } }) } @@ -1709,7 +1709,7 @@ func TestCommandSide_AddInstanceAzureADIDP(t *testing.T) { } if tt.res.err == nil { assert.Equal(t, tt.res.id, id) - assert.Equal(t, tt.res.want, got) + assertObjectDetails(t, tt.res.want, got) } }) } @@ -1929,7 +1929,7 @@ func TestCommandSide_UpdateInstanceAzureADIDP(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) } }) } @@ -2092,7 +2092,7 @@ func TestCommandSide_AddInstanceGitHubIDP(t *testing.T) { } if tt.res.err == nil { assert.Equal(t, tt.res.id, id) - assert.Equal(t, tt.res.want, got) + assertObjectDetails(t, tt.res.want, got) } }) } @@ -2284,7 +2284,7 @@ func TestCommandSide_UpdateInstanceGitHubIDP(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) } }) } @@ -2542,7 +2542,7 @@ func TestCommandSide_AddInstanceGitHubEnterpriseIDP(t *testing.T) { } if tt.res.err == nil { assert.Equal(t, tt.res.id, id) - assert.Equal(t, tt.res.want, got) + assertObjectDetails(t, tt.res.want, got) } }) } @@ -2832,7 +2832,7 @@ func TestCommandSide_UpdateInstanceGitHubEnterpriseIDP(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) } }) } @@ -2994,7 +2994,7 @@ func TestCommandSide_AddInstanceGitLabIDP(t *testing.T) { } if tt.res.err == nil { assert.Equal(t, tt.res.id, id) - assert.Equal(t, tt.res.want, got) + assertObjectDetails(t, tt.res.want, got) } }) } @@ -3184,7 +3184,7 @@ func TestCommandSide_UpdateInstanceGitLabIDP(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) } }) } @@ -3391,7 +3391,7 @@ func TestCommandSide_AddInstanceGitLabSelfHostedIDP(t *testing.T) { } if tt.res.err == nil { assert.Equal(t, tt.res.id, id) - assert.Equal(t, tt.res.want, got) + assertObjectDetails(t, tt.res.want, got) } }) } @@ -3628,7 +3628,7 @@ func TestCommandSide_UpdateInstanceGitLabSelfHostedIDP(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) } }) } @@ -3790,7 +3790,7 @@ func TestCommandSide_AddInstanceGoogleIDP(t *testing.T) { } if tt.res.err == nil { assert.Equal(t, tt.res.id, id) - assert.Equal(t, tt.res.want, got) + assertObjectDetails(t, tt.res.want, got) } }) } @@ -3980,7 +3980,7 @@ func TestCommandSide_UpdateInstanceGoogleIDP(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) } }) } @@ -4329,7 +4329,7 @@ func TestCommandSide_AddInstanceLDAPIDP(t *testing.T) { } if tt.res.err == nil { assert.Equal(t, tt.res.id, id) - assert.Equal(t, tt.res.want, got) + assertObjectDetails(t, tt.res.want, got) } }) } @@ -4717,7 +4717,7 @@ func TestCommandSide_UpdateInstanceLDAPIDP(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) } }) } @@ -4926,7 +4926,7 @@ func TestCommandSide_AddInstanceAppleIDP(t *testing.T) { } if tt.res.err == nil { assert.Equal(t, tt.res.id, id) - assert.Equal(t, tt.res.want, got) + assertObjectDetails(t, tt.res.want, got) } }) } @@ -5165,7 +5165,7 @@ func TestCommandSide_UpdateInstanceAppleIDP(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) } }) } @@ -5342,7 +5342,7 @@ func TestCommandSide_AddInstanceSAMLIDP(t *testing.T) { } if tt.res.err == nil { assert.Equal(t, tt.res.id, id) - assert.Equal(t, tt.res.want, got) + assertObjectDetails(t, tt.res.want, got) } }) } @@ -5561,7 +5561,7 @@ func TestCommandSide_UpdateInstanceGenericSAMLIDP(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) } }) } @@ -5687,7 +5687,7 @@ func TestCommandSide_RegenerateInstanceSAMLProviderCertificate(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) } }) } diff --git a/internal/command/instance_member_test.go b/internal/command/instance_member_test.go index f520bc3240..8d254c56bc 100644 --- a/internal/command/instance_member_test.go +++ b/internal/command/instance_member_test.go @@ -529,7 +529,7 @@ func TestCommandSide_RemoveIAMMember(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) } }) } diff --git a/internal/command/instance_oidc_settings_test.go b/internal/command/instance_oidc_settings_test.go index 353efa5813..f2e52f49fe 100644 --- a/internal/command/instance_oidc_settings_test.go +++ b/internal/command/instance_oidc_settings_test.go @@ -190,7 +190,7 @@ func TestCommandSide_AddOIDCConfig(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) } }) } @@ -403,7 +403,7 @@ func TestCommandSide_ChangeOIDCConfig(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) } }) } diff --git a/internal/command/instance_policy_domain_test.go b/internal/command/instance_policy_domain_test.go index e2b5c2ccce..745d4a7efe 100644 --- a/internal/command/instance_policy_domain_test.go +++ b/internal/command/instance_policy_domain_test.go @@ -106,7 +106,7 @@ func TestCommandSide_AddDefaultDomainPolicy(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) } }) } @@ -340,7 +340,7 @@ func TestCommandSide_ChangeDefaultDomainPolicy(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) } }) } diff --git a/internal/command/instance_policy_label_test.go b/internal/command/instance_policy_label_test.go index 3720fba094..636f24975d 100644 --- a/internal/command/instance_policy_label_test.go +++ b/internal/command/instance_policy_label_test.go @@ -291,7 +291,7 @@ func TestCommandSide_ActivateDefaultLabelPolicy(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) } }) } @@ -446,7 +446,7 @@ func TestCommandSide_AddLogoDefaultLabelPolicy(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) } }) } @@ -588,7 +588,7 @@ func TestCommandSide_RemoveLogoDefaultLabelPolicy(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) } }) } @@ -743,7 +743,7 @@ func TestCommandSide_AddIconDefaultLabelPolicy(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) } }) } @@ -845,7 +845,7 @@ func TestCommandSide_RemoveIconDefaultLabelPolicy(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) } }) } @@ -1003,7 +1003,7 @@ func TestCommandSide_AddLogoDarkDefaultLabelPolicy(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) } }) } @@ -1145,7 +1145,7 @@ func TestCommandSide_RemoveLogoDarkDefaultLabelPolicy(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) } }) } @@ -1300,7 +1300,7 @@ func TestCommandSide_AddIconDarkDefaultLabelPolicy(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) } }) } @@ -1442,7 +1442,7 @@ func TestCommandSide_RemoveIconDarkDefaultLabelPolicy(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) } }) } @@ -1597,7 +1597,7 @@ func TestCommandSide_AddFontDefaultLabelPolicy(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) } }) } @@ -1739,7 +1739,7 @@ func TestCommandSide_RemoveFontDefaultLabelPolicy(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) } }) } diff --git a/internal/command/instance_policy_login_test.go b/internal/command/instance_policy_login_test.go index a3f9a936b8..500b3b4ab2 100644 --- a/internal/command/instance_policy_login_test.go +++ b/internal/command/instance_policy_login_test.go @@ -204,7 +204,7 @@ func TestCommandSide_ChangeDefaultLoginPolicy(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) } }) } @@ -726,7 +726,7 @@ func TestCommandSide_RemoveIDPProviderDefaultLoginPolicy(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) } }) } @@ -901,7 +901,7 @@ func TestCommandSide_AddSecondFactorDefaultLoginPolicy(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) } }) } @@ -1170,7 +1170,7 @@ func TestCommandSide_RemoveSecondFactorDefaultLoginPolicy(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) } }) } @@ -1269,7 +1269,7 @@ func TestCommandSide_AddMultiFactorDefaultLoginPolicy(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) } }) } @@ -1397,7 +1397,7 @@ func TestCommandSide_RemoveMultiFactorDefaultLoginPolicy(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) } }) } diff --git a/internal/command/instance_policy_notification_test.go b/internal/command/instance_policy_notification_test.go index f864e42751..1423ab1472 100644 --- a/internal/command/instance_policy_notification_test.go +++ b/internal/command/instance_policy_notification_test.go @@ -120,7 +120,7 @@ func TestCommandSide_AddDefaultNotificationPolicy(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) } }) } @@ -231,7 +231,7 @@ func TestCommandSide_ChangeDefaultNotificationPolicy(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) } }) } diff --git a/internal/command/instance_policy_password_age_test.go b/internal/command/instance_policy_password_age_test.go index 6380325edd..57efa222f9 100644 --- a/internal/command/instance_policy_password_age_test.go +++ b/internal/command/instance_policy_password_age_test.go @@ -99,7 +99,7 @@ func TestCommandSide_AddDefaultPasswordAgePolicy(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) } }) } diff --git a/internal/command/instance_policy_password_complexity_test.go b/internal/command/instance_policy_password_complexity_test.go index 0c10c0c7f7..25d8dc0489 100644 --- a/internal/command/instance_policy_password_complexity_test.go +++ b/internal/command/instance_policy_password_complexity_test.go @@ -127,7 +127,7 @@ func TestCommandSide_AddDefaultPasswordComplexityPolicy(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) } }) } diff --git a/internal/command/instance_policy_password_lockout_test.go b/internal/command/instance_policy_password_lockout_test.go index 866399caec..19af09f236 100644 --- a/internal/command/instance_policy_password_lockout_test.go +++ b/internal/command/instance_policy_password_lockout_test.go @@ -103,7 +103,7 @@ func TestCommandSide_AddDefaultLockoutPolicy(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) } }) } diff --git a/internal/command/instance_policy_privacy_test.go b/internal/command/instance_policy_privacy_test.go index ed09ba0f53..18d384d721 100644 --- a/internal/command/instance_policy_privacy_test.go +++ b/internal/command/instance_policy_privacy_test.go @@ -178,7 +178,7 @@ func TestCommandSide_AddDefaultPrivacyPolicy(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) } }) } diff --git a/internal/command/instance_settings_test.go b/internal/command/instance_settings_test.go index ddb82f709b..b52db33fc4 100644 --- a/internal/command/instance_settings_test.go +++ b/internal/command/instance_settings_test.go @@ -140,7 +140,7 @@ func TestCommandSide_AddSecretGenerator(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) } }) } @@ -376,7 +376,7 @@ func TestCommandSide_ChangeSecretGenerator(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) } }) } @@ -518,7 +518,7 @@ func TestCommandSide_RemoveSecretGenerator(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) } }) } diff --git a/internal/command/instance_test.go b/internal/command/instance_test.go index 2fbed1845e..b35e226383 100644 --- a/internal/command/instance_test.go +++ b/internal/command/instance_test.go @@ -1370,7 +1370,7 @@ func TestCommandSide_UpdateInstance(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) } }) } @@ -1507,7 +1507,7 @@ func TestCommandSide_RemoveInstance(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) } }) } diff --git a/internal/command/instance_trusted_domain_test.go b/internal/command/instance_trusted_domain_test.go index f4f0001c7c..3caef90f01 100644 --- a/internal/command/instance_trusted_domain_test.go +++ b/internal/command/instance_trusted_domain_test.go @@ -119,7 +119,7 @@ func TestCommands_AddTrustedDomain(t *testing.T) { } got, err := c.AddTrustedDomain(tt.args.ctx, tt.args.trustedDomain) assert.ErrorIs(t, err, tt.want.err) - assert.Equal(t, tt.want.details, got) + assertObjectDetails(t, tt.want.details, got) }) } } @@ -191,7 +191,7 @@ func TestCommands_RemoveTrustedDomain(t *testing.T) { } got, err := c.RemoveTrustedDomain(tt.args.ctx, tt.args.trustedDomain) assert.ErrorIs(t, err, tt.want.err) - assert.Equal(t, tt.want.details, got) + assertObjectDetails(t, tt.want.details, got) }) } } diff --git a/internal/command/limits_test.go b/internal/command/limits_test.go index 53eeab0b87..543f84fa5d 100644 --- a/internal/command/limits_test.go +++ b/internal/command/limits_test.go @@ -223,7 +223,7 @@ func TestLimits_SetLimits(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) } }) } @@ -626,8 +626,11 @@ func TestLimits_SetLimitsBulk(t *testing.T) { t.Errorf("got wrong err: %v ", err) } if tt.res.err == nil { - assert.Equal(t, tt.res.want, gotDetails) - assert.Equal(t, tt.res.wantTarget, gotTargetDetails) + assertObjectDetails(t, tt.res.want, gotDetails) + assert.Len(t, gotTargetDetails, len(tt.res.wantTarget)) + for i, want := range tt.res.wantTarget { + assertObjectDetails(t, want, gotTargetDetails[i]) + } } }) } @@ -748,7 +751,7 @@ func TestLimits_ResetLimits(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) } }) } diff --git a/internal/command/org_action_test.go b/internal/command/org_action_test.go index e3f1a8afc8..94e9bfba29 100644 --- a/internal/command/org_action_test.go +++ b/internal/command/org_action_test.go @@ -129,7 +129,7 @@ func TestCommands_AddAction(t *testing.T) { } if tt.res.err == nil { assert.Equal(t, tt.res.id, id) - assert.Equal(t, tt.res.details, details) + assertObjectDetails(t, tt.res.details, details) } }) } @@ -330,7 +330,7 @@ func TestCommands_ChangeAction(t *testing.T) { t.Errorf("got wrong err: %v ", err) } if tt.res.err == nil { - assert.Equal(t, tt.res.details, details) + assertObjectDetails(t, tt.res.details, details) } }) } @@ -463,7 +463,7 @@ func TestCommands_DeactivateAction(t *testing.T) { t.Errorf("got wrong err: %v ", err) } if tt.res.err == nil { - assert.Equal(t, tt.res.details, details) + assertObjectDetails(t, tt.res.details, details) } }) } @@ -596,7 +596,7 @@ func TestCommands_ReactivateAction(t *testing.T) { t.Errorf("got wrong err: %v ", err) } if tt.res.err == nil { - assert.Equal(t, tt.res.details, details) + assertObjectDetails(t, tt.res.details, details) } }) } @@ -742,7 +742,7 @@ func TestCommands_DeleteAction(t *testing.T) { t.Errorf("got wrong err: %v ", err) } if tt.res.err == nil { - assert.Equal(t, tt.res.details, details) + assertObjectDetails(t, tt.res.details, details) } }) } diff --git a/internal/command/org_custom_login_text_test.go b/internal/command/org_custom_login_text_test.go index dbef76c4ac..0e4be6ceec 100644 --- a/internal/command/org_custom_login_text_test.go +++ b/internal/command/org_custom_login_text_test.go @@ -6632,7 +6632,7 @@ func TestCommandSide_SetCustomOrgLoginText(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) } }) } diff --git a/internal/command/org_custom_message_text_test.go b/internal/command/org_custom_message_text_test.go index ffa8d87aed..f102b10935 100644 --- a/internal/command/org_custom_message_text_test.go +++ b/internal/command/org_custom_message_text_test.go @@ -356,7 +356,7 @@ func TestCommandSide_SetCustomMessageText(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) } }) } diff --git a/internal/command/org_domain_test.go b/internal/command/org_domain_test.go index 80ec082846..df79710955 100644 --- a/internal/command/org_domain_test.go +++ b/internal/command/org_domain_test.go @@ -396,7 +396,7 @@ func TestCommandSide_AddOrgDomain(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) } }) } @@ -1102,7 +1102,7 @@ func TestCommandSide_ValidateOrgDomain(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) } }) } @@ -1287,7 +1287,7 @@ func TestCommandSide_SetPrimaryDomain(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) } }) } @@ -1526,7 +1526,7 @@ func TestCommandSide_RemoveOrgDomain(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) } }) } diff --git a/internal/command/org_flow_test.go b/internal/command/org_flow_test.go index 36961bc6e5..4936306299 100644 --- a/internal/command/org_flow_test.go +++ b/internal/command/org_flow_test.go @@ -112,7 +112,7 @@ func TestCommands_ClearFlow(t *testing.T) { t.Errorf("got wrong err: %v ", err) } if tt.res.err == nil { - assert.Equal(t, tt.res.details, details) + assertObjectDetails(t, tt.res.details, details) } }) } @@ -276,7 +276,7 @@ func TestCommands_SetTriggerActions(t *testing.T) { t.Errorf("got wrong err: %v ", err) } if tt.res.err == nil { - assert.Equal(t, tt.res.details, details) + assertObjectDetails(t, tt.res.details, details) } }) } diff --git a/internal/command/org_idp_config_test.go b/internal/command/org_idp_config_test.go index eb38ca962c..b2d7d2ba4c 100644 --- a/internal/command/org_idp_config_test.go +++ b/internal/command/org_idp_config_test.go @@ -652,7 +652,7 @@ func TestCommands_RemoveIDPConfig(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) } }) } diff --git a/internal/command/org_idp_test.go b/internal/command/org_idp_test.go index bcda24df2d..e1fa43e879 100644 --- a/internal/command/org_idp_test.go +++ b/internal/command/org_idp_test.go @@ -310,7 +310,7 @@ func TestCommandSide_AddOrgGenericOAuthIDP(t *testing.T) { } if tt.res.err == nil { assert.Equal(t, tt.res.id, id) - assert.Equal(t, tt.res.want, got) + assertObjectDetails(t, tt.res.want, got) } }) } @@ -639,7 +639,7 @@ func TestCommandSide_UpdateOrgGenericOAuthIDP(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) } }) } @@ -856,7 +856,7 @@ func TestCommandSide_AddOrgGenericOIDCIDP(t *testing.T) { } if tt.res.err == nil { assert.Equal(t, tt.res.id, id) - assert.Equal(t, tt.res.want, got) + assertObjectDetails(t, tt.res.want, got) } }) } @@ -1105,7 +1105,7 @@ func TestCommandSide_UpdateOrgGenericOIDCIDP(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) } }) } @@ -1350,7 +1350,7 @@ func TestCommandSide_MigrateOrgGenericOIDCToAzureADProvider(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) } }) } @@ -1566,7 +1566,7 @@ func TestCommandSide_MigrateOrgOIDCToGoogleIDP(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) } }) } @@ -1761,7 +1761,7 @@ func TestCommandSide_AddOrgAzureADIDP(t *testing.T) { } if tt.res.err == nil { assert.Equal(t, tt.res.id, id) - assert.Equal(t, tt.res.want, got) + assertObjectDetails(t, tt.res.want, got) } }) } @@ -1988,7 +1988,7 @@ func TestCommandSide_UpdateOrgAzureADIDP(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) } }) } @@ -2156,7 +2156,7 @@ func TestCommandSide_AddOrgGitHubIDP(t *testing.T) { } if tt.res.err == nil { assert.Equal(t, tt.res.id, id) - assert.Equal(t, tt.res.want, got) + assertObjectDetails(t, tt.res.want, got) } }) } @@ -2354,7 +2354,7 @@ func TestCommandSide_UpdateOrgGitHubIDP(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) } }) } @@ -2621,7 +2621,7 @@ func TestCommandSide_AddOrgGitHubEnterpriseIDP(t *testing.T) { } if tt.res.err == nil { assert.Equal(t, tt.res.id, id) - assert.Equal(t, tt.res.want, got) + assertObjectDetails(t, tt.res.want, got) } }) } @@ -2921,7 +2921,7 @@ func TestCommandSide_UpdateOrgGitHubEnterpriseIDP(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) } }) } @@ -3088,7 +3088,7 @@ func TestCommandSide_AddOrgGitLabIDP(t *testing.T) { } if tt.res.err == nil { assert.Equal(t, tt.res.id, id) - assert.Equal(t, tt.res.want, got) + assertObjectDetails(t, tt.res.want, got) } }) } @@ -3284,7 +3284,7 @@ func TestCommandSide_UpdateOrgGitLabIDP(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) } }) } @@ -3498,7 +3498,7 @@ func TestCommandSide_AddOrgGitLabSelfHostedIDP(t *testing.T) { } if tt.res.err == nil { assert.Equal(t, tt.res.id, id) - assert.Equal(t, tt.res.want, got) + assertObjectDetails(t, tt.res.want, got) } }) } @@ -3743,7 +3743,7 @@ func TestCommandSide_UpdateOrgGitLabSelfHostedIDP(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) } }) } @@ -3910,7 +3910,7 @@ func TestCommandSide_AddOrgGoogleIDP(t *testing.T) { } if tt.res.err == nil { assert.Equal(t, tt.res.id, id) - assert.Equal(t, tt.res.want, got) + assertObjectDetails(t, tt.res.want, got) } }) } @@ -4106,7 +4106,7 @@ func TestCommandSide_UpdateOrgGoogleIDP(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) } }) } @@ -4466,7 +4466,7 @@ func TestCommandSide_AddOrgLDAPIDP(t *testing.T) { } if tt.res.err == nil { assert.Equal(t, tt.res.id, id) - assert.Equal(t, tt.res.want, got) + assertObjectDetails(t, tt.res.want, got) } }) } @@ -4865,7 +4865,7 @@ func TestCommandSide_UpdateOrgLDAPIDP(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) } }) } @@ -5081,7 +5081,7 @@ func TestCommandSide_AddOrgAppleIDP(t *testing.T) { } if tt.res.err == nil { assert.Equal(t, tt.res.id, id) - assert.Equal(t, tt.res.want, got) + assertObjectDetails(t, tt.res.want, got) } }) } @@ -5328,7 +5328,7 @@ func TestCommandSide_UpdateOrgAppleIDP(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) } }) } @@ -5513,7 +5513,7 @@ func TestCommandSide_AddOrgSAMLIDP(t *testing.T) { } if tt.res.err == nil { assert.Equal(t, tt.res.id, id) - assert.Equal(t, tt.res.want, got) + assertObjectDetails(t, tt.res.want, got) } }) } @@ -5741,7 +5741,7 @@ func TestCommandSide_UpdateOrgSAMLIDP(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) } }) } @@ -5873,7 +5873,7 @@ func TestCommandSide_RegenerateOrgSAMLProviderCertificate(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) } }) } diff --git a/internal/command/org_member_test.go b/internal/command/org_member_test.go index 2c49f3bd73..4e2e926b52 100644 --- a/internal/command/org_member_test.go +++ b/internal/command/org_member_test.go @@ -841,7 +841,7 @@ func TestCommandSide_RemoveOrgMember(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) } }) } diff --git a/internal/command/org_metadata_test.go b/internal/command/org_metadata_test.go index e52f2480b6..667263007b 100644 --- a/internal/command/org_metadata_test.go +++ b/internal/command/org_metadata_test.go @@ -280,7 +280,7 @@ func TestCommandSide_BulkSetOrgMetadata(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) } }) } @@ -420,7 +420,7 @@ func TestCommandSide_OrgRemoveMetadata(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) } }) } @@ -618,7 +618,7 @@ func TestCommandSide_BulkRemoveOrgMetadata(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) } }) } diff --git a/internal/command/org_policy_domain_test.go b/internal/command/org_policy_domain_test.go index 1cb4c080a6..24020d8026 100644 --- a/internal/command/org_policy_domain_test.go +++ b/internal/command/org_policy_domain_test.go @@ -247,7 +247,7 @@ func TestCommandSide_AddDomainPolicy(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) } }) } @@ -461,7 +461,7 @@ func TestCommandSide_ChangeDomainPolicy(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) } }) } @@ -648,7 +648,7 @@ func TestCommandSide_RemoveDomainPolicy(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) } }) } diff --git a/internal/command/org_policy_label_test.go b/internal/command/org_policy_label_test.go index 8fee5e8909..c5227cc26b 100644 --- a/internal/command/org_policy_label_test.go +++ b/internal/command/org_policy_label_test.go @@ -773,7 +773,7 @@ func TestCommandSide_AddLogoLabelPolicy(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) } }) } @@ -896,7 +896,7 @@ func TestCommandSide_RemoveLogoLabelPolicy(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) } }) } @@ -1078,7 +1078,7 @@ func TestCommandSide_AddIconLabelPolicy(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) } }) } @@ -1198,7 +1198,7 @@ func TestCommandSide_RemoveIconLabelPolicy(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) } }) } @@ -1380,7 +1380,7 @@ func TestCommandSide_AddLogoDarkLabelPolicy(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) } }) } @@ -1503,7 +1503,7 @@ func TestCommandSide_RemoveLogoDarkLabelPolicy(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) } }) } @@ -1685,7 +1685,7 @@ func TestCommandSide_AddIconDarkLabelPolicy(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) } }) } @@ -1803,7 +1803,7 @@ func TestCommandSide_RemoveIconDarkLabelPolicy(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) } }) } @@ -1977,7 +1977,7 @@ func TestCommandSide_AddFontLabelPolicy(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) } }) } @@ -2095,7 +2095,7 @@ func TestCommandSide_RemoveFontLabelPolicy(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) } }) } diff --git a/internal/command/org_policy_login_test.go b/internal/command/org_policy_login_test.go index 62b418d029..0d1b9f5324 100644 --- a/internal/command/org_policy_login_test.go +++ b/internal/command/org_policy_login_test.go @@ -483,7 +483,7 @@ func TestCommandSide_AddLoginPolicy(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) } }) } @@ -690,7 +690,7 @@ func TestCommandSide_ChangeLoginPolicy(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) } }) } @@ -803,7 +803,7 @@ func TestCommandSide_RemoveLoginPolicy(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) } }) } @@ -1393,7 +1393,7 @@ func TestCommandSide_RemoveIDPProviderLoginPolicy(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) } }) } diff --git a/internal/command/org_policy_notification_test.go b/internal/command/org_policy_notification_test.go index 5f8f4f49c4..17af5dd270 100644 --- a/internal/command/org_policy_notification_test.go +++ b/internal/command/org_policy_notification_test.go @@ -136,7 +136,7 @@ func TestCommandSide_AddNotificationPolicy(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) } }) } @@ -260,7 +260,7 @@ func TestCommandSide_ChangeNotificationPolicy(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) } }) } @@ -357,7 +357,7 @@ func TestCommandSide_RemoveNotificationPolicy(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) } }) } diff --git a/internal/command/org_policy_password_age_test.go b/internal/command/org_policy_password_age_test.go index 6100533871..aa76b1b3ad 100644 --- a/internal/command/org_policy_password_age_test.go +++ b/internal/command/org_policy_password_age_test.go @@ -368,7 +368,7 @@ func TestCommandSide_RemovePasswordAgePolicy(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) } }) } diff --git a/internal/command/org_policy_password_complexity_test.go b/internal/command/org_policy_password_complexity_test.go index 955b3bedf6..a1443413d3 100644 --- a/internal/command/org_policy_password_complexity_test.go +++ b/internal/command/org_policy_password_complexity_test.go @@ -395,7 +395,7 @@ func TestCommandSide_RemovePasswordComplexityPolicy(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) } }) } diff --git a/internal/command/org_policy_privacy_test.go b/internal/command/org_policy_privacy_test.go index 147d4d3e56..d12a8bcf42 100644 --- a/internal/command/org_policy_privacy_test.go +++ b/internal/command/org_policy_privacy_test.go @@ -582,7 +582,7 @@ func TestCommandSide_RemovePrivacyPolicy(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) } }) } diff --git a/internal/command/preparation_test.go b/internal/command/preparation_test.go index 9ffd815b39..24ed57792c 100644 --- a/internal/command/preparation_test.go +++ b/internal/command/preparation_test.go @@ -7,7 +7,10 @@ import ( "reflect" "testing" + "github.com/stretchr/testify/assert" + "github.com/zitadel/zitadel/internal/command/preparation" + "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" ) @@ -89,3 +92,17 @@ func (mf *MultiFilter) Filter() preparation.FilterToQueryReducer { return mf.filters[mf.count-1](ctx, queryFactory) } } + +func assertObjectDetails(t *testing.T, want, got *domain.ObjectDetails) { + if want == nil { + assert.Nil(t, got) + return + } + assert.Equal(t, got.CreationDate, want.CreationDate) + assert.Equal(t, got.EventDate, want.EventDate) + assert.Equal(t, got.ResourceOwner, want.ResourceOwner) + assert.Equal(t, got.Sequence, want.Sequence) + if want.ID != "" { + assert.Equal(t, got.ID, want.ID) + } +} diff --git a/internal/command/project_application_test.go b/internal/command/project_application_test.go index 66671a5a51..ae2c6c39b0 100644 --- a/internal/command/project_application_test.go +++ b/internal/command/project_application_test.go @@ -190,7 +190,7 @@ func TestCommandSide_ChangeApplication(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) } }) } @@ -342,7 +342,7 @@ func TestCommandSide_DeactivateApplication(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) } }) } @@ -494,7 +494,7 @@ func TestCommandSide_ReactivateApplication(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 +670,7 @@ func TestCommandSide_RemoveApplication(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) } }) } diff --git a/internal/command/project_grant_member_test.go b/internal/command/project_grant_member_test.go index 5535cc9367..189d1b9911 100644 --- a/internal/command/project_grant_member_test.go +++ b/internal/command/project_grant_member_test.go @@ -575,7 +575,7 @@ func TestCommandSide_RemoveProjectGrantMember(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) } }) } diff --git a/internal/command/project_grant_test.go b/internal/command/project_grant_test.go index 7450723185..42c6875f06 100644 --- a/internal/command/project_grant_test.go +++ b/internal/command/project_grant_test.go @@ -926,7 +926,7 @@ func TestCommandSide_DeactivateProjectGrant(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) } }) } @@ -1123,7 +1123,7 @@ func TestCommandSide_ReactivateProjectGrant(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) } }) } @@ -1386,7 +1386,7 @@ func TestCommandSide_RemoveProjectGrant(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) } }) } diff --git a/internal/command/project_member_test.go b/internal/command/project_member_test.go index 358ee4382f..88b52f63f8 100644 --- a/internal/command/project_member_test.go +++ b/internal/command/project_member_test.go @@ -607,7 +607,7 @@ func TestCommandSide_RemoveProjectMember(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) } }) } diff --git a/internal/command/project_role_test.go b/internal/command/project_role_test.go index bd8c85aef7..2dc7ee35a6 100644 --- a/internal/command/project_role_test.go +++ b/internal/command/project_role_test.go @@ -423,7 +423,7 @@ func TestCommandSide_BulkAddProjectRole(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) } }) } @@ -997,7 +997,7 @@ func TestCommandSide_RemoveProjectRole(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) } }) } diff --git a/internal/command/project_test.go b/internal/command/project_test.go index ca8d2b2ddf..299ff344fc 100644 --- a/internal/command/project_test.go +++ b/internal/command/project_test.go @@ -606,7 +606,7 @@ func TestCommandSide_DeactivateProject(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) } }) } @@ -782,7 +782,7 @@ func TestCommandSide_ReactivateProject(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) } }) } @@ -1066,7 +1066,7 @@ func TestCommandSide_RemoveProject(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) } }) } diff --git a/internal/command/quota_test.go b/internal/command/quota_test.go index 14d5d50794..5436885c55 100644 --- a/internal/command/quota_test.go +++ b/internal/command/quota_test.go @@ -275,7 +275,7 @@ func TestQuota_AddQuota(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) } }) } @@ -541,7 +541,7 @@ func TestQuota_SetQuota(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) } }) } @@ -678,7 +678,7 @@ func TestQuota_RemoveQuota(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) } }) } diff --git a/internal/command/restrictions_test.go b/internal/command/restrictions_test.go index 4dcbc479ed..2f1b26d9fd 100644 --- a/internal/command/restrictions_test.go +++ b/internal/command/restrictions_test.go @@ -232,7 +232,7 @@ func TestSetRestrictions(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) } }) } diff --git a/internal/command/session_test.go b/internal/command/session_test.go index 46cbaeafc3..5fba985148 100644 --- a/internal/command/session_test.go +++ b/internal/command/session_test.go @@ -1464,7 +1464,7 @@ func TestCommands_TerminateSession(t *testing.T) { } got, err := c.TerminateSession(tt.args.ctx, tt.args.sessionID, tt.args.sessionToken) require.ErrorIs(t, err, tt.res.err) - assert.Equal(t, tt.res.want, got) + assertObjectDetails(t, tt.res.want, got) }) } } diff --git a/internal/command/sms_config_test.go b/internal/command/sms_config_test.go index b79ab25519..8d96751944 100644 --- a/internal/command/sms_config_test.go +++ b/internal/command/sms_config_test.go @@ -94,7 +94,7 @@ func TestCommandSide_AddSMSConfigTwilio(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) } }) } @@ -253,7 +253,7 @@ func TestCommandSide_ChangeSMSConfigTwilio(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) } }) } @@ -360,7 +360,7 @@ func TestCommandSide_ActivateSMSConfigTwilio(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) } }) } @@ -474,7 +474,7 @@ func TestCommandSide_DeactivateSMSConfigTwilio(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) } }) } @@ -581,7 +581,7 @@ func TestCommandSide_RemoveSMSConfig(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) } }) } diff --git a/internal/command/smtp_test.go b/internal/command/smtp_test.go index bbcbd0b365..b6bb7d98a7 100644 --- a/internal/command/smtp_test.go +++ b/internal/command/smtp_test.go @@ -332,7 +332,7 @@ func TestCommandSide_AddSMTPConfig(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) } }) } @@ -736,7 +736,7 @@ func TestCommandSide_ChangeSMTPConfig(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) } }) } @@ -852,7 +852,7 @@ func TestCommandSide_ChangeSMTPConfigPassword(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) } }) } @@ -968,7 +968,7 @@ func TestCommandSide_ActivateSMTPConfig(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) } }) } @@ -1091,7 +1091,7 @@ func TestCommandSide_DeactivateSMTPConfig(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) } }) } @@ -1190,7 +1190,7 @@ func TestCommandSide_RemoveSMTPConfig(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) } }) } diff --git a/internal/command/system_features_test.go b/internal/command/system_features_test.go index 83d8c8550b..d6d0d6b5c2 100644 --- a/internal/command/system_features_test.go +++ b/internal/command/system_features_test.go @@ -340,7 +340,7 @@ func TestCommands_ResetSystemFeatures(t *testing.T) { } got, err := c.ResetSystemFeatures(context.Background()) require.ErrorIs(t, err, tt.wantErr) - assert.Equal(t, tt.want, got) + assertObjectDetails(t, tt.want, got) }) } } diff --git a/internal/command/user_grant_test.go b/internal/command/user_grant_test.go index a073c94e19..a5fafde836 100644 --- a/internal/command/user_grant_test.go +++ b/internal/command/user_grant_test.go @@ -1417,7 +1417,7 @@ func TestCommandSide_DeactivateUserGrant(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) } }) } @@ -1625,7 +1625,7 @@ func TestCommandSide_ReactivateUserGrant(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) } }) } @@ -1827,7 +1827,7 @@ func TestCommandSide_RemoveUserGrant(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) } }) } diff --git a/internal/command/user_human_avatar_test.go b/internal/command/user_human_avatar_test.go index 70552e7e50..61c1f01bca 100644 --- a/internal/command/user_human_avatar_test.go +++ b/internal/command/user_human_avatar_test.go @@ -192,7 +192,7 @@ func TestCommandSide_AddHumanAvatar(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) } }) } @@ -355,7 +355,7 @@ func TestCommandSide_RemoveHumanAvatar(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) } }) } diff --git a/internal/command/user_human_email_test.go b/internal/command/user_human_email_test.go index fca5ba2606..e18413594f 100644 --- a/internal/command/user_human_email_test.go +++ b/internal/command/user_human_email_test.go @@ -688,7 +688,7 @@ func TestCommandSide_VerifyHumanEmail(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) } }) } @@ -949,7 +949,7 @@ func TestCommandSide_CreateVerificationCodeHumanEmail(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) } }) } diff --git a/internal/command/user_human_init_test.go b/internal/command/user_human_init_test.go index 382d2892b4..0a0b6060fc 100644 --- a/internal/command/user_human_init_test.go +++ b/internal/command/user_human_init_test.go @@ -394,7 +394,7 @@ func TestCommandSide_ResendInitialMail(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) } }) } diff --git a/internal/command/user_human_otp_test.go b/internal/command/user_human_otp_test.go index 94a47f6dae..197d222518 100644 --- a/internal/command/user_human_otp_test.go +++ b/internal/command/user_human_otp_test.go @@ -962,7 +962,7 @@ func TestCommandSide_RemoveHumanTOTP(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) } }) } @@ -1179,7 +1179,7 @@ func TestCommandSide_AddHumanOTPSMS(t *testing.T) { } got, err := r.AddHumanOTPSMS(tt.args.ctx, tt.args.userID, tt.args.resourceOwner) assert.ErrorIs(t, err, tt.res.err) - assert.Equal(t, tt.res.want, got) + assertObjectDetails(t, tt.res.want, got) }) } } @@ -1306,7 +1306,7 @@ func TestCommandSide_AddHumanOTPSMSWithCheckSucceeded(t *testing.T) { } got, err := r.AddHumanOTPSMSWithCheckSucceeded(tt.args.ctx, tt.args.userID, tt.args.resourceOwner, tt.args.authRequest) assert.ErrorIs(t, err, tt.res.err) - assert.Equal(t, tt.res.want, got) + assertObjectDetails(t, tt.res.want, got) }) } } @@ -1421,7 +1421,7 @@ func TestCommandSide_RemoveHumanOTPSMS(t *testing.T) { } got, err := r.RemoveHumanOTPSMS(tt.args.ctx, tt.args.userID, tt.args.resourceOwner) assert.ErrorIs(t, err, tt.res.err) - assert.Equal(t, tt.res.want, got) + assertObjectDetails(t, tt.res.want, got) }) } } @@ -2334,7 +2334,7 @@ func TestCommandSide_AddHumanOTPEmail(t *testing.T) { } got, err := r.AddHumanOTPEmail(tt.args.ctx, tt.args.userID, tt.args.resourceOwner) assert.ErrorIs(t, err, tt.res.err) - assert.Equal(t, tt.res.want, got) + assertObjectDetails(t, tt.res.want, got) }) } } @@ -2461,7 +2461,7 @@ func TestCommandSide_AddHumanOTPEmailWithCheckSucceeded(t *testing.T) { } got, err := r.AddHumanOTPEmailWithCheckSucceeded(tt.args.ctx, tt.args.userID, tt.args.resourceOwner, tt.args.authRequest) assert.ErrorIs(t, err, tt.res.err) - assert.Equal(t, tt.res.want, got) + assertObjectDetails(t, tt.res.want, got) }) } } @@ -2576,7 +2576,7 @@ func TestCommandSide_RemoveHumanOTPEmail(t *testing.T) { } got, err := r.RemoveHumanOTPEmail(tt.args.ctx, tt.args.userID, tt.args.resourceOwner) assert.ErrorIs(t, err, tt.res.err) - assert.Equal(t, tt.res.want, got) + assertObjectDetails(t, tt.res.want, got) }) } } diff --git a/internal/command/user_human_password_test.go b/internal/command/user_human_password_test.go index 0326967498..1eb886828b 100644 --- a/internal/command/user_human_password_test.go +++ b/internal/command/user_human_password_test.go @@ -254,7 +254,7 @@ func TestCommandSide_SetOneTimePassword(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) } }) } @@ -657,7 +657,7 @@ func TestCommandSide_SetPasswordWithVerifyCode(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) } }) } @@ -1102,7 +1102,7 @@ func TestCommandSide_ChangePassword(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) } }) } @@ -1336,7 +1336,7 @@ func TestCommandSide_RequestSetPassword(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) } }) } diff --git a/internal/command/user_human_phone_test.go b/internal/command/user_human_phone_test.go index ae9784cc0c..9ee3fcbdd2 100644 --- a/internal/command/user_human_phone_test.go +++ b/internal/command/user_human_phone_test.go @@ -609,7 +609,7 @@ func TestCommandSide_VerifyHumanPhone(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) } }) } @@ -791,7 +791,7 @@ func TestCommandSide_CreateVerificationCodeHumanPhone(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) } }) } @@ -1047,7 +1047,7 @@ func TestCommandSide_RemoveHumanPhone(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) } }) } diff --git a/internal/command/user_human_refresh_token_test.go b/internal/command/user_human_refresh_token_test.go index c3b992535e..f70afe3ee5 100644 --- a/internal/command/user_human_refresh_token_test.go +++ b/internal/command/user_human_refresh_token_test.go @@ -157,7 +157,7 @@ func TestCommands_RevokeRefreshToken(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) } }) } diff --git a/internal/command/user_idp_link_test.go b/internal/command/user_idp_link_test.go index 67d1c005ef..62b30f5fce 100644 --- a/internal/command/user_idp_link_test.go +++ b/internal/command/user_idp_link_test.go @@ -728,7 +728,7 @@ func TestCommandSide_RemoveUserIDPLink(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) } }) } diff --git a/internal/command/user_machine_key_test.go b/internal/command/user_machine_key_test.go index 9e50f81e7c..5437026c0b 100644 --- a/internal/command/user_machine_key_test.go +++ b/internal/command/user_machine_key_test.go @@ -267,7 +267,7 @@ func TestCommands_AddMachineKey(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) receivedKey := len(tt.args.key.PrivateKey) > 0 assert.Equal(t, tt.res.key, receivedKey) } diff --git a/internal/command/user_machine_secret_test.go b/internal/command/user_machine_secret_test.go index ee58b42d82..4c6d16960c 100644 --- a/internal/command/user_machine_secret_test.go +++ b/internal/command/user_machine_secret_test.go @@ -139,7 +139,7 @@ func TestCommandSide_GenerateMachineSecret(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) assert.Equal(t, tt.args.set.ClientSecret, tt.res.secret.ClientSecret) } }) @@ -297,7 +297,7 @@ func TestCommandSide_RemoveMachineSecret(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) } }) } diff --git a/internal/command/user_machine_test.go b/internal/command/user_machine_test.go index d6b93bb94f..c7b4b8caf4 100644 --- a/internal/command/user_machine_test.go +++ b/internal/command/user_machine_test.go @@ -209,7 +209,7 @@ func TestCommandSide_AddMachine(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) } }) } @@ -384,7 +384,7 @@ func TestCommandSide_ChangeMachine(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) } }) } diff --git a/internal/command/user_metadata_test.go b/internal/command/user_metadata_test.go index 812ef529da..b3ffa7b823 100644 --- a/internal/command/user_metadata_test.go +++ b/internal/command/user_metadata_test.go @@ -322,7 +322,7 @@ func TestCommandSide_BulkSetUserMetadata(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) } }) } @@ -483,7 +483,7 @@ func TestCommandSide_UserRemoveMetadata(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) } }) } @@ -711,7 +711,7 @@ func TestCommandSide_BulkRemoveUserMetadata(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) } }) } diff --git a/internal/command/user_personal_access_token_test.go b/internal/command/user_personal_access_token_test.go index 335a59e701..2341d52b62 100644 --- a/internal/command/user_personal_access_token_test.go +++ b/internal/command/user_personal_access_token_test.go @@ -270,7 +270,7 @@ func TestCommands_AddPersonalAccessToken(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) assert.Equal(t, tt.res.token, tt.args.pat.Token) } }) @@ -368,7 +368,7 @@ func TestCommands_RemovePersonalAccessToken(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) } }) } diff --git a/internal/command/user_schema_test.go b/internal/command/user_schema_test.go index 717620b44f..084963116a 100644 --- a/internal/command/user_schema_test.go +++ b/internal/command/user_schema_test.go @@ -281,7 +281,7 @@ func TestCommands_CreateUserSchema(t *testing.T) { } gotID, gotDetails, err := c.CreateUserSchema(tt.args.ctx, tt.args.userSchema) assert.Equal(t, tt.res.id, gotID) - assert.Equal(t, tt.res.details, gotDetails) + assertObjectDetails(t, tt.res.details, gotDetails) assert.ErrorIs(t, err, tt.res.err) }) } @@ -620,7 +620,7 @@ func TestCommands_UpdateUserSchema(t *testing.T) { } got, err := c.UpdateUserSchema(tt.args.ctx, tt.args.userSchema) assert.ErrorIs(t, err, tt.res.err) - assert.Equal(t, tt.res.details, got) + assertObjectDetails(t, tt.res.details, got) }) } } @@ -713,7 +713,7 @@ func TestCommands_DeactivateUserSchema(t *testing.T) { } got, err := c.DeactivateUserSchema(tt.args.ctx, tt.args.id, tt.args.resourceOwner) assert.ErrorIs(t, err, tt.res.err) - assert.Equal(t, tt.res.details, got) + assertObjectDetails(t, tt.res.details, got) }) } } @@ -812,7 +812,7 @@ func TestCommands_ReactivateUserSchema(t *testing.T) { } got, err := c.ReactivateUserSchema(tt.args.ctx, tt.args.id, tt.args.resourceOwner) assert.ErrorIs(t, err, tt.res.err) - assert.Equal(t, tt.res.details, got) + assertObjectDetails(t, tt.res.details, got) }) } } @@ -906,7 +906,7 @@ func TestCommands_DeleteUserSchema(t *testing.T) { } got, err := c.DeleteUserSchema(tt.args.ctx, tt.args.id, tt.args.resourceOwner) assert.ErrorIs(t, err, tt.res.err) - assert.Equal(t, tt.res.details, got) + assertObjectDetails(t, tt.res.details, got) }) } } diff --git a/internal/command/user_test.go b/internal/command/user_test.go index 5de57a153a..9abae187c1 100644 --- a/internal/command/user_test.go +++ b/internal/command/user_test.go @@ -498,7 +498,7 @@ func TestCommandSide_UsernameChange(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) } }) } @@ -648,7 +648,7 @@ func TestCommandSide_DeactivateUser(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) } }) } @@ -797,7 +797,7 @@ func TestCommandSide_ReactivateUser(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) } }) } @@ -947,7 +947,7 @@ func TestCommandSide_LockUser(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) } }) } @@ -1096,7 +1096,7 @@ func TestCommandSide_UnlockUser(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) } }) } @@ -1427,7 +1427,7 @@ func TestCommandSide_RemoveUser(t *testing.T) { t.Errorf("got wrong err: %v ", err) return } - assert.Equal(t, tt.res.want, got) + assertObjectDetails(t, tt.res.want, got) }) } } @@ -1562,7 +1562,7 @@ func TestCommands_RevokeAccessToken(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) } }) } diff --git a/internal/command/user_v2_email_test.go b/internal/command/user_v2_email_test.go index 5a7b4fb2ac..79a53705f8 100644 --- a/internal/command/user_v2_email_test.go +++ b/internal/command/user_v2_email_test.go @@ -1831,7 +1831,7 @@ func TestCommands_verifyUserEmailWithGenerator(t *testing.T) { } got, err := c.verifyUserEmailWithGenerator(context.Background(), tt.args.userID, tt.args.code, GetMockSecretGenerator(t)) require.ErrorIs(t, err, tt.wantErr) - assert.Equal(t, got, tt.want) + assertObjectDetails(t, tt.want, got) }) } } diff --git a/internal/command/user_v2_human_test.go b/internal/command/user_v2_human_test.go index 997124e1f1..37cd2837e7 100644 --- a/internal/command/user_v2_human_test.go +++ b/internal/command/user_v2_human_test.go @@ -1569,7 +1569,7 @@ func TestCommandSide_AddUserHuman(t *testing.T) { return } if tt.res.err == nil { - assert.Equal(t, tt.res.want, tt.args.human.Details) + assertObjectDetails(t, tt.res.want, tt.args.human.Details) assert.Equal(t, tt.res.wantID, tt.args.human.ID) assert.Equal(t, tt.res.wantEmailCode, gu.Value(tt.args.human.EmailCode)) } @@ -2945,7 +2945,7 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) { return } if tt.res.err == nil { - assert.Equal(t, tt.res.want, tt.args.human.Details) + assertObjectDetails(t, tt.res.want, tt.args.human.Details) assert.Equal(t, tt.res.wantEmailCode, tt.args.human.EmailCode) assert.Equal(t, tt.res.wantPhoneCode, tt.args.human.PhoneCode) } diff --git a/internal/command/user_v2_passkey_test.go b/internal/command/user_v2_passkey_test.go index 5a12cb9dc6..aa2ded6d7a 100644 --- a/internal/command/user_v2_passkey_test.go +++ b/internal/command/user_v2_passkey_test.go @@ -537,7 +537,7 @@ func TestCommands_AddUserPasskeyCode(t *testing.T) { } got, err := c.AddUserPasskeyCode(context.Background(), tt.args.userID, tt.args.resourceOwner, alg) require.ErrorIs(t, err, tt.wantErr) - assert.Equal(t, tt.want, got) + assertObjectDetails(t, tt.want, got) }) } } @@ -645,7 +645,7 @@ func TestCommands_AddUserPasskeyCodeURLTemplate(t *testing.T) { } got, err := c.AddUserPasskeyCodeURLTemplate(context.Background(), tt.args.userID, tt.args.resourceOwner, alg, tt.args.urlTmpl) require.ErrorIs(t, err, tt.wantErr) - assert.Equal(t, tt.want, got) + assertObjectDetails(t, tt.want, got) }) } } @@ -736,8 +736,13 @@ func TestCommands_AddUserPasskeyCodeReturn(t *testing.T) { idGenerator: tt.fields.idGenerator, } got, err := c.AddUserPasskeyCodeReturn(context.Background(), tt.args.userID, tt.args.resourceOwner, alg) - require.ErrorIs(t, err, tt.wantErr) - assert.Equal(t, tt.want, got) + if tt.wantErr != nil { + require.ErrorIs(t, err, tt.wantErr) + return + } + assert.Equal(t, tt.want.CodeID, got.CodeID) + assert.Equal(t, tt.want.Code, got.Code) + assertObjectDetails(t, tt.want.ObjectDetails, got.ObjectDetails) }) } } @@ -896,8 +901,13 @@ func TestCommands_addUserPasskeyCode(t *testing.T) { idGenerator: tt.fields.idGenerator, } got, err := c.addUserPasskeyCode(context.Background(), tt.args.userID, tt.args.resourceOwner, alg, "", false) - require.ErrorIs(t, err, tt.wantErr) - assert.Equal(t, tt.want, got) + if tt.wantErr != nil { + require.ErrorIs(t, err, tt.wantErr) + return + } + assert.Equal(t, tt.want.CodeID, got.CodeID) + assert.Equal(t, tt.want.Code, got.Code) + assertObjectDetails(t, tt.want.ObjectDetails, got.ObjectDetails) }) } } diff --git a/internal/command/user_v2_password_test.go b/internal/command/user_v2_password_test.go index a870376251..2575e9442c 100644 --- a/internal/command/user_v2_password_test.go +++ b/internal/command/user_v2_password_test.go @@ -601,7 +601,7 @@ func TestCommands_requestPasswordReset(t *testing.T) { } got, gotPlainCode, err := c.requestPasswordReset(tt.args.ctx, tt.args.userID, tt.args.returnCode, tt.args.urlTmpl, tt.args.notificationType) require.ErrorIs(t, err, tt.res.err) - assert.Equal(t, tt.res.details, got) + assertObjectDetails(t, tt.res.details, got) assert.Equal(t, tt.res.code, gotPlainCode) }) } diff --git a/internal/command/user_v2_test.go b/internal/command/user_v2_test.go index 0ee9fe6946..e156c92f08 100644 --- a/internal/command/user_v2_test.go +++ b/internal/command/user_v2_test.go @@ -263,7 +263,7 @@ func TestCommandSide_LockUserV2(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) } }) } @@ -516,7 +516,7 @@ func TestCommandSide_UnlockUserV2(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) } }) } @@ -819,7 +819,7 @@ func TestCommandSide_DeactivateUserV2(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) } }) } @@ -1074,7 +1074,7 @@ func TestCommandSide_ReactivateUserV2(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) } }) } @@ -1366,7 +1366,7 @@ func TestCommandSide_RemoveUserV2(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) } }) } diff --git a/internal/config/systemdefaults/system_defaults.go b/internal/config/systemdefaults/system_defaults.go index 1d89115c8e..f6d39befe7 100644 --- a/internal/config/systemdefaults/system_defaults.go +++ b/internal/config/systemdefaults/system_defaults.go @@ -14,6 +14,8 @@ type SystemDefaults struct { DomainVerification DomainVerification Notifications Notifications KeyConfig KeyConfig + DefaultQueryLimit uint64 + MaxQueryLimit uint64 } type SecretGenerators struct { diff --git a/internal/domain/object.go b/internal/domain/object.go index a9e90a6ca9..9ebffae584 100644 --- a/internal/domain/object.go +++ b/internal/domain/object.go @@ -5,7 +5,12 @@ import ( ) type ObjectDetails struct { - Sequence uint64 - EventDate time.Time + Sequence uint64 + // EventDate is the date of the last event that changed the object + EventDate time.Time + // CreationDate is the date of the first event that created the object + CreationDate time.Time ResourceOwner string + // ID is the Aggregate ID of the resource + ID string } diff --git a/internal/integration/assert.go b/internal/integration/assert.go index 682a82ff7f..b3e63fd29e 100644 --- a/internal/integration/assert.go +++ b/internal/integration/assert.go @@ -11,7 +11,6 @@ import ( "google.golang.org/protobuf/types/known/timestamppb" resources_object "github.com/zitadel/zitadel/pkg/grpc/resources/object/v3alpha" - settings_object "github.com/zitadel/zitadel/pkg/grpc/settings/object/v3alpha" ) // Details is the interface that covers both v1 and v2 proto generated object details. @@ -37,6 +36,10 @@ type ListDetailsMsg[L ListDetails] interface { GetDetails() L } +type ResourceListDetailsMsg interface { + GetDetails() *resources_object.ListDetails +} + // AssertDetails asserts values in a message's object Details, // if the object Details in expected is a non-nil value. // It targets API v2 messages that have the `GetDetails()` method. @@ -67,28 +70,21 @@ func AssertDetails[D Details, M DetailsMsg[D]](t testing.TB, expected, actual M) } func AssertResourceDetails(t testing.TB, expected *resources_object.Details, actual *resources_object.Details) { - assert.NotZero(t, actual.GetSequence()) - - if expected.GetChangeDate() != nil { + if expected.GetChanged() != nil { wantChangeDate := time.Now() - gotChangeDate := actual.GetChangeDate().AsTime() + gotChangeDate := actual.GetChanged().AsTime() assert.WithinRange(t, gotChangeDate, wantChangeDate.Add(-time.Minute), wantChangeDate.Add(time.Minute)) } - + if expected.GetCreated() != nil { + wantCreatedDate := time.Now() + gotCreatedDate := actual.GetCreated().AsTime() + assert.WithinRange(t, gotCreatedDate, wantCreatedDate.Add(-time.Minute), wantCreatedDate.Add(time.Minute)) + } assert.Equal(t, expected.GetOwner(), actual.GetOwner()) assert.NotEmpty(t, actual.GetId()) -} - -func AssertSettingsDetails(t testing.TB, expected *settings_object.Details, actual *settings_object.Details) { - assert.NotZero(t, actual.GetSequence()) - - if expected.GetChangeDate() != nil { - wantChangeDate := time.Now() - gotChangeDate := actual.GetChangeDate().AsTime() - assert.WithinRange(t, gotChangeDate, wantChangeDate.Add(-time.Minute), wantChangeDate.Add(time.Minute)) + if expected.GetId() != "" { + assert.Equal(t, expected.GetId(), actual.GetId()) } - - assert.Equal(t, expected.GetOwner(), actual.GetOwner()) } func AssertListDetails[L ListDetails, D ListDetailsMsg[L]](t testing.TB, expected, actual D) { @@ -107,6 +103,23 @@ func AssertListDetails[L ListDetails, D ListDetailsMsg[L]](t testing.TB, expecte } } +func AssertResourceListDetails[D ResourceListDetailsMsg](t testing.TB, expected, actual D) { + wantDetails, gotDetails := expected.GetDetails(), actual.GetDetails() + if wantDetails == nil { + assert.Nil(t, gotDetails) + return + } + + assert.Equal(t, wantDetails.GetTotalResult(), gotDetails.GetTotalResult()) + assert.Equal(t, wantDetails.GetAppliedLimit(), gotDetails.GetAppliedLimit()) + + if wantDetails.GetTimestamp() != nil { + gotCD := gotDetails.GetTimestamp().AsTime() + wantCD := time.Now() + assert.WithinRange(t, gotCD, wantCD.Add(-time.Minute), wantCD.Add(time.Minute)) + } +} + // EqualProto is inspired by [assert.Equal], only that it tests equality of a proto message. // A message diff is printed on the error test log if the messages are not equal. // diff --git a/internal/integration/client.go b/internal/integration/client.go index 3819682618..9e5dc63dde 100644 --- a/internal/integration/client.go +++ b/internal/integration/client.go @@ -137,7 +137,7 @@ func (t *Tester) UseIsolatedInstance(tt *testing.T, iamOwnerCtx, systemCtx conte }) assert.NoError(collectT, importErr) }, 2*time.Minute, 100*time.Millisecond, "instance not ready") - return primaryDomain, instanceId, adminUser.GetUserId(), newCtx + return primaryDomain, instanceId, adminUser.GetUserId(), t.updateInstanceAndOrg(newCtx, fmt.Sprintf("%s:%d", primaryDomain, t.Config.ExternalPort)) } func (s *Tester) CreateHumanUser(ctx context.Context) *user.AddHumanUserResponse { diff --git a/internal/integration/integration.go b/internal/integration/integration.go index 807f976a32..18836be8fc 100644 --- a/internal/integration/integration.go +++ b/internal/integration/integration.go @@ -204,14 +204,7 @@ func (s *Tester) createLoginClient(ctx context.Context) { func (s *Tester) createMachineUser(ctx context.Context, username string, userType UserType) (context.Context, *query.User) { var err error - - s.Instance, err = s.Queries.InstanceByHost(ctx, s.Host(), "") - logging.OnError(err).Fatal("query instance") - ctx = authz.WithInstance(ctx, s.Instance) - - s.Organisation, err = s.Queries.OrgByID(ctx, true, s.Instance.DefaultOrganisationID()) - logging.OnError(err).Fatal("query organisation") - + ctx = s.updateInstanceAndOrg(ctx, s.Host()) usernameQuery, err := query.NewUserUsernameSearchQuery(username, query.TextEquals) logging.OnError(err).Fatal("user query") user, err := s.Queries.GetUser(ctx, true, usernameQuery) @@ -427,3 +420,14 @@ func runQuotaServer(ctx context.Context, bodies chan []byte) (*httptest.Server, mockServer.Start() return mockServer, nil } + +func (s *Tester) updateInstanceAndOrg(ctx context.Context, domain string) context.Context { + var err error + s.Instance, err = s.Queries.InstanceByHost(ctx, domain, "") + logging.OnError(err).Fatal("query instance") + ctx = authz.WithInstance(ctx, s.Instance) + + s.Organisation, err = s.Queries.OrgByID(ctx, true, s.Instance.DefaultOrganisationID()) + logging.OnError(err).Fatal("query organisation") + return ctx +} diff --git a/internal/query/execution.go b/internal/query/execution.go index ff501f8201..5ce5e36a94 100644 --- a/internal/query/execution.go +++ b/internal/query/execution.go @@ -28,6 +28,10 @@ var ( name: projection.ExecutionIDCol, table: executionTable, } + ExecutionColumnCreationDate = Column{ + name: projection.ExecutionCreationDateCol, + table: executionTable, + } ExecutionColumnChangeDate = Column{ name: projection.ExecutionChangeDateCol, table: executionTable, @@ -36,11 +40,6 @@ var ( name: projection.ExecutionInstanceIDCol, table: executionTable, } - ExecutionColumnSequence = Column{ - name: projection.ExecutionSequenceCol, - table: executionTable, - } - executionTargetsTable = table{ name: projection.ExecutionTable + "_" + projection.ExecutionTargetSuffix, instanceIDCol: projection.ExecutionTargetInstanceIDCol, @@ -79,7 +78,6 @@ func (e *Executions) SetState(s *State) { } type Execution struct { - ID string domain.ObjectDetails Targets []*exec.Target @@ -210,12 +208,12 @@ func (q *Queries) TargetsByExecutionIDs(ctx context.Context, ids1, ids2 []string return execution, err } -func prepareExecutionQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(row *sql.Row) (*Execution, error)) { +func prepareExecutionQuery(context.Context, prepareDatabase) (sq.SelectBuilder, func(row *sql.Row) (*Execution, error)) { return sq.Select( ExecutionColumnInstanceID.identifier(), ExecutionColumnID.identifier(), + ExecutionColumnCreationDate.identifier(), ExecutionColumnChangeDate.identifier(), - ExecutionColumnSequence.identifier(), executionTargetsListCol.identifier(), ).From(executionTable.identifier()). Join("(" + executionTargetsQuery + ") AS " + executionTargetsTableAlias.alias + " ON " + @@ -226,12 +224,12 @@ func prepareExecutionQuery(ctx context.Context, db prepareDatabase) (sq.SelectBu scanExecution } -func prepareExecutionsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(rows *sql.Rows) (*Executions, error)) { +func prepareExecutionsQuery(context.Context, prepareDatabase) (sq.SelectBuilder, func(rows *sql.Rows) (*Executions, error)) { return sq.Select( ExecutionColumnInstanceID.identifier(), ExecutionColumnID.identifier(), + ExecutionColumnCreationDate.identifier(), ExecutionColumnChangeDate.identifier(), - ExecutionColumnSequence.identifier(), executionTargetsListCol.identifier(), countColumn.identifier(), ).From(executionTable.identifier()). @@ -256,8 +254,8 @@ func scanExecution(row *sql.Row) (*Execution, error) { err := row.Scan( &execution.ResourceOwner, &execution.ID, + &execution.CreationDate, &execution.EventDate, - &execution.Sequence, &targets, ) if err != nil { @@ -315,8 +313,8 @@ func scanExecutions(rows *sql.Rows) (*Executions, error) { err := rows.Scan( &execution.ResourceOwner, &execution.ID, + &execution.CreationDate, &execution.EventDate, - &execution.Sequence, &targets, &count, ) diff --git a/internal/query/execution_test.go b/internal/query/execution_test.go index b989d539a0..ee6bdc4d96 100644 --- a/internal/query/execution_test.go +++ b/internal/query/execution_test.go @@ -16,8 +16,8 @@ import ( var ( prepareExecutionsStmt = `SELECT projections.executions1.instance_id,` + ` projections.executions1.id,` + + ` projections.executions1.creation_date,` + ` projections.executions1.change_date,` + - ` projections.executions1.sequence,` + ` execution_targets.targets,` + ` COUNT(*) OVER ()` + ` FROM projections.executions1` + @@ -32,16 +32,16 @@ var ( prepareExecutionsCols = []string{ "instance_id", "id", + "creation_date", "change_date", - "sequence", "targets", "count", } prepareExecutionStmt = `SELECT projections.executions1.instance_id,` + ` projections.executions1.id,` + + ` projections.executions1.creation_date,` + ` projections.executions1.change_date,` + - ` projections.executions1.sequence,` + ` execution_targets.targets` + ` FROM projections.executions1` + ` JOIN (` + @@ -55,8 +55,8 @@ var ( prepareExecutionCols = []string{ "instance_id", "id", + "creation_date", "change_date", - "sequence", "targets", } ) @@ -96,7 +96,7 @@ func Test_ExecutionPrepares(t *testing.T) { "ro", "id", testNow, - uint64(20211109), + testNow, []byte(`[{"position" : 1, "target" : "target"}, {"position" : 2, "include" : "include"}]`), }, }, @@ -108,11 +108,11 @@ func Test_ExecutionPrepares(t *testing.T) { }, Executions: []*Execution{ { - ID: "id", ObjectDetails: domain.ObjectDetails{ EventDate: testNow, + CreationDate: testNow, ResourceOwner: "ro", - Sequence: 20211109, + ID: "id", }, Targets: []*exec.Target{ {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, @@ -134,14 +134,14 @@ func Test_ExecutionPrepares(t *testing.T) { "ro", "id-1", testNow, - uint64(20211109), + testNow, []byte(`[{"position" : 1, "target" : "target"}, {"position" : 2, "include" : "include"}]`), }, { "ro", "id-2", testNow, - uint64(20211110), + testNow, []byte(`[{"position" : 2, "target" : "target"}, {"position" : 1, "include" : "include"}]`), }, }, @@ -153,11 +153,11 @@ func Test_ExecutionPrepares(t *testing.T) { }, Executions: []*Execution{ { - ID: "id-1", ObjectDetails: domain.ObjectDetails{ + ID: "id-1", EventDate: testNow, + CreationDate: testNow, ResourceOwner: "ro", - Sequence: 20211109, }, Targets: []*exec.Target{ {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, @@ -165,11 +165,11 @@ func Test_ExecutionPrepares(t *testing.T) { }, }, { - ID: "id-2", ObjectDetails: domain.ObjectDetails{ + ID: "id-2", EventDate: testNow, + CreationDate: testNow, ResourceOwner: "ro", - Sequence: 20211110, }, Targets: []*exec.Target{ {Type: domain.ExecutionTargetTypeInclude, Target: "include"}, @@ -225,17 +225,17 @@ func Test_ExecutionPrepares(t *testing.T) { "ro", "id", testNow, - uint64(20211109), + testNow, []byte(`[{"position" : 1, "target" : "target"}, {"position" : 2, "include" : "include"}]`), }, ), }, object: &Execution{ - ID: "id", ObjectDetails: domain.ObjectDetails{ + ID: "id", EventDate: testNow, + CreationDate: testNow, ResourceOwner: "ro", - Sequence: 20211109, }, Targets: []*exec.Target{ {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, diff --git a/internal/query/instance.go b/internal/query/instance.go index d547ae538c..fb60946d31 100644 --- a/internal/query/instance.go +++ b/internal/query/instance.go @@ -216,14 +216,12 @@ func (q *Queries) InstanceByHost(ctx context.Context, instanceHost, publicHost s return instance, err } -func (q *Queries) InstanceByID(ctx context.Context) (_ authz.Instance, err error) { +func (q *Queries) InstanceByID(ctx context.Context, id string) (_ authz.Instance, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - - instanceID := authz.GetInstance(ctx).InstanceID() instance, scan := scanAuthzInstance() - err = q.client.QueryRowContext(ctx, scan, instanceByIDQuery, instanceID) - logging.OnError(err).WithField("instance_id", instanceID).Warn("instance by ID") + err = q.client.QueryRowContext(ctx, scan, instanceByIDQuery, id) + logging.OnError(err).WithField("instance_id", id).Warn("instance by ID") return instance, err } diff --git a/internal/query/projection/target.go b/internal/query/projection/target.go index 7b7c46d257..d39a75b6dc 100644 --- a/internal/query/projection/target.go +++ b/internal/query/projection/target.go @@ -97,7 +97,7 @@ func (p *targetProjection) reduceTargetAdded(event eventstore.Event) (*handler.S handler.NewCol(TargetInstanceIDCol, e.Aggregate().InstanceID), handler.NewCol(TargetResourceOwnerCol, e.Aggregate().ResourceOwner), handler.NewCol(TargetIDCol, e.Aggregate().ID), - handler.NewCol(TargetCreationDateCol, e.CreationDate()), + handler.NewCol(TargetCreationDateCol, handler.OnlySetValueOnInsert(TargetTable, e.CreationDate())), handler.NewCol(TargetChangeDateCol, e.CreationDate()), handler.NewCol(TargetSequenceCol, e.Sequence()), handler.NewCol(TargetNameCol, e.Name), diff --git a/internal/query/target.go b/internal/query/target.go index c5d8f893ad..8d926a699b 100644 --- a/internal/query/target.go +++ b/internal/query/target.go @@ -39,10 +39,6 @@ var ( name: projection.TargetInstanceIDCol, table: targetTable, } - TargetColumnSequence = Column{ - name: projection.TargetSequenceCol, - table: targetTable, - } TargetColumnName = Column{ name: projection.TargetNameCol, table: targetTable, @@ -75,7 +71,6 @@ func (t *Targets) SetState(s *State) { } type Target struct { - ID string domain.ObjectDetails Name string @@ -123,12 +118,12 @@ func NewTargetInIDsSearchQuery(values []string) (SearchQuery, error) { return NewInTextQuery(TargetColumnID, values) } -func prepareTargetsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(rows *sql.Rows) (*Targets, error)) { +func prepareTargetsQuery(context.Context, prepareDatabase) (sq.SelectBuilder, func(rows *sql.Rows) (*Targets, error)) { return sq.Select( TargetColumnID.identifier(), + TargetColumnCreationDate.identifier(), TargetColumnChangeDate.identifier(), TargetColumnResourceOwner.identifier(), - TargetColumnSequence.identifier(), TargetColumnName.identifier(), TargetColumnTargetType.identifier(), TargetColumnTimeout.identifier(), @@ -144,9 +139,9 @@ func prepareTargetsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuil target := new(Target) err := rows.Scan( &target.ID, + &target.CreationDate, &target.EventDate, &target.ResourceOwner, - &target.Sequence, &target.Name, &target.TargetType, &target.Timeout, @@ -173,12 +168,12 @@ func prepareTargetsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuil } } -func prepareTargetQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(row *sql.Row) (*Target, error)) { +func prepareTargetQuery(context.Context, prepareDatabase) (sq.SelectBuilder, func(row *sql.Row) (*Target, error)) { return sq.Select( TargetColumnID.identifier(), + TargetColumnCreationDate.identifier(), TargetColumnChangeDate.identifier(), TargetColumnResourceOwner.identifier(), - TargetColumnSequence.identifier(), TargetColumnName.identifier(), TargetColumnTargetType.identifier(), TargetColumnTimeout.identifier(), @@ -190,9 +185,9 @@ func prepareTargetQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuild target := new(Target) err := row.Scan( &target.ID, + &target.CreationDate, &target.EventDate, &target.ResourceOwner, - &target.Sequence, &target.Name, &target.TargetType, &target.Timeout, diff --git a/internal/query/target_test.go b/internal/query/target_test.go index 61a61e9e45..1b6edd1ad7 100644 --- a/internal/query/target_test.go +++ b/internal/query/target_test.go @@ -15,9 +15,9 @@ import ( var ( prepareTargetsStmt = `SELECT projections.targets1.id,` + + ` projections.targets1.creation_date,` + ` projections.targets1.change_date,` + ` projections.targets1.resource_owner,` + - ` projections.targets1.sequence,` + ` projections.targets1.name,` + ` projections.targets1.target_type,` + ` projections.targets1.timeout,` + @@ -27,9 +27,9 @@ var ( ` FROM projections.targets1` prepareTargetsCols = []string{ "id", + "creation_date", "change_date", "resource_owner", - "sequence", "name", "target_type", "timeout", @@ -39,9 +39,9 @@ var ( } prepareTargetStmt = `SELECT projections.targets1.id,` + + ` projections.targets1.creation_date,` + ` projections.targets1.change_date,` + ` projections.targets1.resource_owner,` + - ` projections.targets1.sequence,` + ` projections.targets1.name,` + ` projections.targets1.target_type,` + ` projections.targets1.timeout,` + @@ -50,9 +50,9 @@ var ( ` FROM projections.targets1` prepareTargetCols = []string{ "id", + "creation_date", "change_date", "resource_owner", - "sequence", "name", "target_type", "timeout", @@ -95,8 +95,8 @@ func Test_TargetPrepares(t *testing.T) { { "id", testNow, + testNow, "ro", - uint64(20211109), "target-name", domain.TargetTypeWebhook, 1 * time.Second, @@ -112,11 +112,11 @@ func Test_TargetPrepares(t *testing.T) { }, Targets: []*Target{ { - ID: "id", ObjectDetails: domain.ObjectDetails{ + ID: "id", EventDate: testNow, + CreationDate: testNow, ResourceOwner: "ro", - Sequence: 20211109, }, Name: "target-name", TargetType: domain.TargetTypeWebhook, @@ -138,8 +138,8 @@ func Test_TargetPrepares(t *testing.T) { { "id-1", testNow, + testNow, "ro", - uint64(20211109), "target-name1", domain.TargetTypeWebhook, 1 * time.Second, @@ -149,8 +149,8 @@ func Test_TargetPrepares(t *testing.T) { { "id-2", testNow, + testNow, "ro", - uint64(20211110), "target-name2", domain.TargetTypeWebhook, 1 * time.Second, @@ -160,8 +160,8 @@ func Test_TargetPrepares(t *testing.T) { { "id-3", testNow, + testNow, "ro", - uint64(20211110), "target-name3", domain.TargetTypeAsync, 1 * time.Second, @@ -177,11 +177,11 @@ func Test_TargetPrepares(t *testing.T) { }, Targets: []*Target{ { - ID: "id-1", ObjectDetails: domain.ObjectDetails{ + ID: "id-1", EventDate: testNow, + CreationDate: testNow, ResourceOwner: "ro", - Sequence: 20211109, }, Name: "target-name1", TargetType: domain.TargetTypeWebhook, @@ -190,11 +190,11 @@ func Test_TargetPrepares(t *testing.T) { InterruptOnError: true, }, { - ID: "id-2", ObjectDetails: domain.ObjectDetails{ + ID: "id-2", EventDate: testNow, + CreationDate: testNow, ResourceOwner: "ro", - Sequence: 20211110, }, Name: "target-name2", TargetType: domain.TargetTypeWebhook, @@ -203,11 +203,11 @@ func Test_TargetPrepares(t *testing.T) { InterruptOnError: false, }, { - ID: "id-3", ObjectDetails: domain.ObjectDetails{ + ID: "id-3", EventDate: testNow, + CreationDate: testNow, ResourceOwner: "ro", - Sequence: 20211110, }, Name: "target-name3", TargetType: domain.TargetTypeAsync, @@ -263,8 +263,8 @@ func Test_TargetPrepares(t *testing.T) { []driver.Value{ "id", testNow, + testNow, "ro", - uint64(20211109), "target-name", domain.TargetTypeWebhook, 1 * time.Second, @@ -274,11 +274,11 @@ func Test_TargetPrepares(t *testing.T) { ), }, object: &Target{ - ID: "id", ObjectDetails: domain.ObjectDetails{ + ID: "id", EventDate: testNow, + CreationDate: testNow, ResourceOwner: "ro", - Sequence: 20211109, }, Name: "target-name", TargetType: domain.TargetTypeWebhook, diff --git a/internal/static/i18n/bg.yaml b/internal/static/i18n/bg.yaml index 1161a4928d..fd7bfee660 100644 --- a/internal/static/i18n/bg.yaml +++ b/internal/static/i18n/bg.yaml @@ -509,6 +509,7 @@ Errors: SQLStatement: SQL изразът не може да бъде създаден InvalidRequest: Заявката е невалидна TooManyNestingLevels: Твърде много нива на влагане на заявката (макс. 20) + LimitExceeded: Ограничението на заявката е превишено Quota: AlreadyExists: Вече съществува квота за тази единица NotFound: Не е намерена квота за тази единица diff --git a/internal/static/i18n/cs.yaml b/internal/static/i18n/cs.yaml index 3383021b48..986bc6327f 100644 --- a/internal/static/i18n/cs.yaml +++ b/internal/static/i18n/cs.yaml @@ -495,6 +495,7 @@ Errors: SQLStatement: SQL příkaz nemohl být vytvořen InvalidRequest: Požadavek je neplatný TooManyNestingLevels: Příliš mnoho úrovní vnoření dotazů (max. 20) + LimitExceeded: Překročen limit výsledků Quota: AlreadyExists: Kvóta pro tuto jednotku již existuje NotFound: Kvóta pro tuto jednotku nenalezena diff --git a/internal/static/i18n/de.yaml b/internal/static/i18n/de.yaml index e9ebccf3bf..ec7d5976b5 100644 --- a/internal/static/i18n/de.yaml +++ b/internal/static/i18n/de.yaml @@ -495,6 +495,7 @@ Errors: SQLStatement: SQL Statement konnte nicht erstellt werden InvalidRequest: Anfrage ist ungültig TooManyNestingLevels: Zu viele Abfrageverschachtelungsebenen (maximal 20) + LimitExceeded: Limit überschritten Quota: AlreadyExists: Das Kontingent existiert bereits für diese Einheit NotFound: Kontingent für diese Einheit nicht gefunden diff --git a/internal/static/i18n/en.yaml b/internal/static/i18n/en.yaml index a35cfc043d..b1bf5907cf 100644 --- a/internal/static/i18n/en.yaml +++ b/internal/static/i18n/en.yaml @@ -495,6 +495,7 @@ Errors: SQLStatement: SQL Statement could not be created InvalidRequest: Request is invalid TooManyNestingLevels: Too many query nesting levels (Max 20) + LimitExceeded: Limit exceeded Quota: AlreadyExists: Quota already exists for this unit NotFound: Quota not found for this unit @@ -586,6 +587,7 @@ Errors: Impersonation: PolicyDisabled: Impersonation is disabled in the instance security policy + AggregateTypes: action: Action instance: Instance diff --git a/internal/static/i18n/es.yaml b/internal/static/i18n/es.yaml index b4ba11cfaa..cec5cfedd1 100644 --- a/internal/static/i18n/es.yaml +++ b/internal/static/i18n/es.yaml @@ -495,6 +495,7 @@ Errors: SQLStatement: La sentencia SQL no pudo crearse InvalidRequest: La solicitud no es válida TooManyNestingLevels: Demasiados niveles de anidamiento de consultas (máximo 20) + LimitExceeded: Se ha superado el límite de resultados Quota: AlreadyExists: La cuota ya existe para esta unidad NotFound: Cuota no encontrada para esta unidad diff --git a/internal/static/i18n/fr.yaml b/internal/static/i18n/fr.yaml index e10df340da..9871725704 100644 --- a/internal/static/i18n/fr.yaml +++ b/internal/static/i18n/fr.yaml @@ -495,6 +495,7 @@ Errors: SQLStatement: L'instruction SQL n'a pas pu être créée InvalidRequest: La requête n'est pas valide TooManyNestingLevels: Trop de niveaux d'imbrication de requêtes (maximum 20) + LimitExceeded: Limite dépassée Quota: AlreadyExists: Contingent existe déjà pour cette unité NotFound: Contingent non trouvé pour cette unité diff --git a/internal/static/i18n/it.yaml b/internal/static/i18n/it.yaml index a853e28748..73180b982b 100644 --- a/internal/static/i18n/it.yaml +++ b/internal/static/i18n/it.yaml @@ -495,6 +495,7 @@ Errors: SQLStatement: Lo statement SQL non può essere creato InvalidRequest: La richiesta non è valida TooManyNestingLevels: Troppi livelli di nidificazione delle query (massimo 20) + LimitExceeded: Limite superato Quota: AlreadyExists: La quota esiste già per questa unità NotFound: Quota non trovata per questa unità diff --git a/internal/static/i18n/ja.yaml b/internal/static/i18n/ja.yaml index 725cdcc7ab..3b8b4cbb92 100644 --- a/internal/static/i18n/ja.yaml +++ b/internal/static/i18n/ja.yaml @@ -484,6 +484,7 @@ Errors: SQLStatement: SQLステートメントの作成に失敗しました InvalidRequest: 無効なリクエストです TooManyNestingLevels: クエリのネスト レベルが多すぎます (最大 20) + LimitExceeded: 制限を超えました Quota: AlreadyExists: このユニットにはすでにクォータが存在しています NotFound: このユニットにはクォータが見つかりません diff --git a/internal/static/i18n/mk.yaml b/internal/static/i18n/mk.yaml index d7aabafe3d..cec3cbb506 100644 --- a/internal/static/i18n/mk.yaml +++ b/internal/static/i18n/mk.yaml @@ -494,6 +494,7 @@ Errors: SQLStatement: SQL наредбата не може да се креира InvalidRequest: Барањето е невалидно TooManyNestingLevels: Премногу нивоа на вгнездување на барања (макс 20) + LimitExceeded: Превишена граница Quota: AlreadyExists: Веќе постои квота за оваа единица NotFound: Квотата не е пронајдена за оваа единица diff --git a/internal/static/i18n/nl.yaml b/internal/static/i18n/nl.yaml index fa5e3b6ce5..062ee7f5c9 100644 --- a/internal/static/i18n/nl.yaml +++ b/internal/static/i18n/nl.yaml @@ -495,6 +495,7 @@ Errors: SQLStatement: SQL Statement kon niet worden gemaakt InvalidRequest: Verzoek is ongeldig TooManyNestingLevels: Te veel query nesting niveaus (Max 20) + LimitExceeded: Limiet overschreden Quota: AlreadyExists: Quota bestaat al voor deze eenheid NotFound: Quota niet gevonden voor deze eenheid diff --git a/internal/static/i18n/pl.yaml b/internal/static/i18n/pl.yaml index c6081de5a7..4705320d84 100644 --- a/internal/static/i18n/pl.yaml +++ b/internal/static/i18n/pl.yaml @@ -495,6 +495,7 @@ Errors: SQLStatement: Instrukcja SQL nie mogła zostać utworzona InvalidRequest: Żądanie jest nieprawidłowe TooManyNestingLevels: Zbyt wiele poziomów zagnieżdżenia zapytań (maks. 20) + LimitExceeded: Limit przekroczony Quota: AlreadyExists: Limit już istnieje dla tej jednostki NotFound: Nie znaleziono limitu dla tej jednostki diff --git a/internal/static/i18n/pt.yaml b/internal/static/i18n/pt.yaml index e980b4ea21..acb69e2c0b 100644 --- a/internal/static/i18n/pt.yaml +++ b/internal/static/i18n/pt.yaml @@ -494,6 +494,7 @@ Errors: SQLStatement: Não foi possível criar a instrução SQL InvalidRequest: O pedido é inválido TooManyNestingLevels: muitos níveis de aninhamento de consulta (máx. 20) + LimitExceeded: Limite excedido Quota: AlreadyExists: Cota já existe para esta unidade NotFound: Cota não encontrada para esta unidade diff --git a/internal/static/i18n/ru.yaml b/internal/static/i18n/ru.yaml index b2aa62d28f..36918b9c1f 100644 --- a/internal/static/i18n/ru.yaml +++ b/internal/static/i18n/ru.yaml @@ -488,6 +488,7 @@ Errors: SQLStatement: SQL-запрос не может быть создан InvalidRequest: Запрос недействителен TooManyNestingLevels: слишком много уровней вложенности запросов (максимум 20) + LimitExceeded: Превышен лимит Quota: AlreadyExists: Квота для данного объекта уже существует NotFound: Квота для данного объекта не найдена diff --git a/internal/static/i18n/sv.yaml b/internal/static/i18n/sv.yaml index d995df06f5..ee0a6a3b04 100644 --- a/internal/static/i18n/sv.yaml +++ b/internal/static/i18n/sv.yaml @@ -495,6 +495,7 @@ Errors: SQLStatement: SQL-satsen kunde inte skapas InvalidRequest: Begäran är ogiltig TooManyNestingLevels: För många nivåer av frågenästning (Max 20) + LimitExceeded: Gränsen överskreds Quota: AlreadyExists: Kvota finns redan för denna enhet NotFound: Kvota hittades inte för denna enhet diff --git a/internal/static/i18n/zh.yaml b/internal/static/i18n/zh.yaml index b4fa6e90be..9d22c30891 100644 --- a/internal/static/i18n/zh.yaml +++ b/internal/static/i18n/zh.yaml @@ -495,6 +495,7 @@ Errors: SQLStatement: 无法创建 SQL 语句 InvalidRequest: 请求无效 TooManyNestingLevels: 查询嵌套级别过多(最多 20 个) + LimitExceeded: 限制已超出 Quota: AlreadyExists: 这个单位的配额已经存在 NotFound: 没有找到该单位的配额 diff --git a/proto/zitadel/object/v3alpha/object.proto b/proto/zitadel/object/v3alpha/object.proto index fba08fa5b4..5c067ddc1a 100644 --- a/proto/zitadel/object/v3alpha/object.proto +++ b/proto/zitadel/object/v3alpha/object.proto @@ -20,3 +20,10 @@ message Owner { string id = 2; } +message Instance { + oneof property { + option (validate.required) = true; + string id = 1; + string domain = 2; + } +} diff --git a/proto/zitadel/resources/action/v3alpha/action_service.proto b/proto/zitadel/resources/action/v3alpha/action_service.proto index a8ae67b95d..08d57e93e5 100644 --- a/proto/zitadel/resources/action/v3alpha/action_service.proto +++ b/proto/zitadel/resources/action/v3alpha/action_service.proto @@ -10,10 +10,13 @@ import "protoc-gen-openapiv2/options/annotations.proto"; import "validate/validate.proto"; import "zitadel/protoc_gen_zitadel/v2/options.proto"; + import "zitadel/resources/action/v3alpha/target.proto"; import "zitadel/resources/action/v3alpha/execution.proto"; +import "zitadel/resources/action/v3alpha/query.proto"; import "zitadel/resources/object/v3alpha/object.proto"; -import "zitadel/settings/object/v3alpha/object.proto"; +import "zitadel/object/v3alpha/object.proto"; + option go_package = "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha;action"; @@ -44,7 +47,7 @@ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { consumes: "application/grpc-web+proto"; produces: "application/grpc-web+proto"; - host: "$ZITADEL_DOMAIN"; + host: "${ZITADEL_DOMAIN}"; base_path: "/resources/v3alpha/actions"; external_docs: { @@ -57,8 +60,8 @@ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { value: { type: TYPE_OAUTH2; flow: FLOW_ACCESS_CODE; - authorization_url: "$CUSTOM-DOMAIN/oauth/v2/authorize"; - token_url: "$CUSTOM-DOMAIN/oauth/v2/token"; + authorization_url: "${ZITADEL_DOMAIN}/oauth/v2/authorize"; + token_url: "${ZITADEL_DOMAIN}/oauth/v2/token"; scopes: { scope: { key: "openid"; @@ -189,6 +192,67 @@ service ZITADELActions { }; } + // Target by ID + // + // Returns the target identified by the requested ID. + rpc GetTarget (GetTargetRequest) returns (GetTargetResponse) { + option (google.api.http) = { + get: "/targets/{id}" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "action.target.read" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200" + value: { + description: "Target successfully retrieved"; + } + }; + }; + } + + // Search targets + // + // Search all matching targets. By default all targets of the instance are returned. + // Make sure to include a limit and sorting for pagination. + rpc SearchTargets (SearchTargetsRequest) returns (SearchTargetsResponse) { + option (google.api.http) = { + post: "/targets/_search", + body: "filters" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "action.target.read" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "A list of all targets matching the query"; + }; + }; + responses: { + key: "400"; + value: { + description: "invalid list query"; + schema: { + json_schema: { + ref: "#/definitions/rpcStatus"; + }; + }; + }; + }; + }; + } + // Sets an execution to call a target or include the targets of another execution. // // Setting an empty list of targets will remove all targets from the execution, making it a noop. @@ -222,6 +286,43 @@ service ZITADELActions { }; } + // Search executions + // + // Search all matching executions. By default all executions of the instance are returned that have at least one execution target. + // Make sure to include a limit and sorting for pagination. + rpc SearchExecutions (SearchExecutionsRequest) returns (SearchExecutionsResponse) { + option (google.api.http) = { + post: "/executions/_search" + body: "filters" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "action.execution.read" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "A list of all non noop executions matching the query"; + }; + }; + responses: { + key: "400"; + value: { + description: "invalid list query"; + schema: { + json_schema: { + ref: "#/definitions/rpcStatus"; + }; + }; + }; + }; + }; + } + // List all available functions // // List all available functions which can be used as condition for executions. @@ -294,7 +395,16 @@ service ZITADELActions { } message CreateTargetRequest { - Target target = 1; + optional zitadel.object.v3alpha.Instance instance = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"domain from HOST or :authority header\"" + } + ]; + Target target = 2 [ + (validate.rules).message = { + required: true + } + ]; } message CreateTargetResponse { @@ -302,16 +412,24 @@ message CreateTargetResponse { } message PatchTargetRequest { - string id = 1 [ + optional zitadel.object.v3alpha.Instance instance = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"domain from HOST or :authority header\"" + } + ]; + 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: "\"69629026806489455\""; } ]; - PatchTarget target = 2; + PatchTarget target = 3 [ + (validate.rules).message = { + required: true + } + ]; } message PatchTargetResponse { @@ -319,9 +437,13 @@ message PatchTargetResponse { } message DeleteTargetRequest { - string id = 1 [ + optional zitadel.object.v3alpha.Instance instance = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"domain from HOST or :authority header\"" + } + ]; + 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, @@ -334,13 +456,84 @@ message DeleteTargetResponse { zitadel.resources.object.v3alpha.Details details = 1; } +message GetTargetRequest { + optional zitadel.object.v3alpha.Instance instance = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"domain from HOST or :authority header\"" + } + ]; + 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\""; + } + ]; +} + +message GetTargetResponse { + GetTarget target = 1; +} + +message SearchTargetsRequest { + optional zitadel.object.v3alpha.Instance instance = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"domain from HOST or :authority header\"" + } + ]; + // list limitations and ordering. + optional zitadel.resources.object.v3alpha.SearchQuery query = 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 TargetFieldName sorting_column = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"TARGET_FIELD_NAME_CREATION_DATE\"" + } + ]; + // Define the criteria to query for. + repeated TargetSearchFilter filters = 4; +} + +message SearchTargetsResponse { + zitadel.resources.object.v3alpha.ListDetails details = 1; + repeated GetTarget result = 2; +} + message SetExecutionRequest { - Condition condition = 1; - Execution execution = 2; + optional zitadel.object.v3alpha.Instance instance = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"domain from HOST or :authority header\"" + } + ]; + Condition condition = 2; + Execution execution = 3; } message SetExecutionResponse { - zitadel.settings.object.v3alpha.Details details = 1; + zitadel.resources.object.v3alpha.Details details = 1; +} + +message SearchExecutionsRequest { + optional zitadel.object.v3alpha.Instance instance = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"domain from HOST or :authority header\"" + } + ]; + // list limitations and ordering. + optional zitadel.resources.object.v3alpha.SearchQuery query = 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 ExecutionFieldName sorting_column = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"EXECUTION_FIELD_NAME_CREATION_DATE\"" + } + ]; + // Define the criteria to query for. + repeated ExecutionSearchFilter filters = 4; +} + +message SearchExecutionsResponse { + zitadel.resources.object.v3alpha.ListDetails details = 1; + repeated GetExecution result = 2; } message ListExecutionFunctionsRequest{} diff --git a/proto/zitadel/resources/action/v3alpha/execution.proto b/proto/zitadel/resources/action/v3alpha/execution.proto index f666b4c497..375ab02b86 100644 --- a/proto/zitadel/resources/action/v3alpha/execution.proto +++ b/proto/zitadel/resources/action/v3alpha/execution.proto @@ -21,6 +21,12 @@ message Execution { repeated ExecutionTargetType targets = 1; } +message GetExecution { + zitadel.resources.object.v3alpha.Details details = 1; + Condition condition = 2; + Execution execution = 3; +} + message ExecutionTargetType { oneof type { option (validate.required) = true; diff --git a/proto/zitadel/resources/action/v3alpha/query.proto b/proto/zitadel/resources/action/v3alpha/query.proto new file mode 100644 index 0000000000..fb51543085 --- /dev/null +++ b/proto/zitadel/resources/action/v3alpha/query.proto @@ -0,0 +1,117 @@ +syntax = "proto3"; + +package zitadel.resources.action.v3alpha; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha;action"; + +import "google/api/field_behavior.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; +import "validate/validate.proto"; + +import "zitadel/resources/object/v3alpha/object.proto"; +import "zitadel/resources/action/v3alpha/execution.proto"; + +message ExecutionSearchFilter { + oneof filter { + option (validate.required) = true; + + InConditionsFilter in_conditions_filter = 1; + ExecutionTypeFilter execution_type_filter = 2; + TargetFilter target_filter = 3; + IncludeFilter include_filter = 4; + } +} + +message InConditionsFilter { + // Defines the conditions to query for. + repeated Condition conditions = 1; +} + +message ExecutionTypeFilter { + // Defines the type to query for. + ExecutionType execution_type = 1; +} + +message TargetFilter { + // Defines the id to query for. + string target_id = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "the id of the targets to include" + example: "\"69629023906488334\""; + } + ]; +} + +message IncludeFilter { + // Defines the include to query for. + Condition include = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "the id of the include" + example: "\"request.zitadel.session.v2.SessionService\""; + } + ]; +} + +message TargetSearchFilter { + oneof filter { + option (validate.required) = true; + + TargetNameFilter target_name_filter = 1; + InTargetIDsFilter in_target_ids_filter = 2; + } +} + +message TargetNameFilter { + // Defines the name of the target to query for. + string target_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.resources.object.v3alpha.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 InTargetIDsFilter { + // Defines the ids to query for. + repeated string target_ids = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "the ids of the targets to include" + example: "[\"69629023906488334\",\"69622366012355662\"]"; + } + ]; +} + +enum ExecutionType { + EXECUTION_TYPE_UNSPECIFIED = 0; + EXECUTION_TYPE_REQUEST = 1; + EXECUTION_TYPE_RESPONSE = 2; + EXECUTION_TYPE_EVENT = 3; + EXECUTION_TYPE_FUNCTION = 4; +} + +enum TargetFieldName { + TARGET_FIELD_NAME_UNSPECIFIED = 0; + TARGET_FIELD_NAME_ID = 1; + TARGET_FIELD_NAME_CREATED_DATE = 2; + TARGET_FIELD_NAME_CHANGED_DATE = 3; + TARGET_FIELD_NAME_NAME = 4; + TARGET_FIELD_NAME_TARGET_TYPE = 5; + TARGET_FIELD_NAME_URL = 6; + TARGET_FIELD_NAME_TIMEOUT = 7; + TARGET_FIELD_NAME_INTERRUPT_ON_ERROR = 8; +} + +enum ExecutionFieldName { + EXECUTION_FIELD_NAME_UNSPECIFIED = 0; + EXECUTION_FIELD_NAME_ID = 1; + EXECUTION_FIELD_NAME_CREATED_DATE = 2; + EXECUTION_FIELD_NAME_CHANGED_DATE = 3; +} diff --git a/proto/zitadel/resources/action/v3alpha/target.proto b/proto/zitadel/resources/action/v3alpha/target.proto index 20843a530b..cb1ff85883 100644 --- a/proto/zitadel/resources/action/v3alpha/target.proto +++ b/proto/zitadel/resources/action/v3alpha/target.proto @@ -10,12 +10,17 @@ import "protoc-gen-openapiv2/options/annotations.proto"; import "validate/validate.proto"; import "zitadel/protoc_gen_zitadel/v2/options.proto"; +import "zitadel/resources/object/v3alpha/object.proto"; + option go_package = "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha;action"; message Target { string name = 1 [ + (validate.rules).string = {min_len: 1, max_len: 1000}, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { example: "\"ip_allow_list\""; + min_length: 1 + max_length: 1000 } ]; // Defines the target type and how the response of the target is treated. @@ -27,21 +32,34 @@ message Target { } // Timeout defines the duration until ZITADEL cancels the execution. google.protobuf.Duration timeout = 5 [ + (validate.rules).duration = {gte: {}, lte: {seconds: 270}}, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "if the target doesn't respond before this timeout expires, the the connection is closed and the action fails"; example: "\"10s\""; } ]; string endpoint = 6 [ + (validate.rules).string = {min_len: 1, max_len: 1000}, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"https://example.com/hooks/ip_check\""; + example: "\"https://example.com/hooks/ip_check\"" + min_length: 1 + max_length: 1000 } ]; } +message GetTarget { + zitadel.resources.object.v3alpha.Details details = 1; + Target config = 2; +} + message PatchTarget { optional string name = 1 [ + (validate.rules).string = {min_len: 1, max_len: 1000}, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"ip_allow_list\""; + example: "\"ip_allow_list\"" + min_length: 1 + max_length: 1000 } ]; // Defines the target type and how the response of the target is treated. @@ -52,13 +70,18 @@ message PatchTarget { } // Timeout defines the duration until ZITADEL cancels the execution. optional google.protobuf.Duration timeout = 5 [ + (validate.rules).duration = {gte: {}, lte: {seconds: 270}}, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "if the target doesn't respond before this timeout expires, the the connection is closed and the action fails"; example: "\"10s\""; } ]; optional string endpoint = 6 [ + (validate.rules).string = {min_len: 1, max_len: 1000}, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"https://example.com/hooks/ip_check\""; + example: "\"https://example.com/hooks/ip_check\"" + min_length: 1 + max_length: 1000 } ]; } diff --git a/proto/zitadel/resources/object/v3alpha/object.proto b/proto/zitadel/resources/object/v3alpha/object.proto index 65b3ef0c94..17d536b5ec 100644 --- a/proto/zitadel/resources/object/v3alpha/object.proto +++ b/proto/zitadel/resources/object/v3alpha/object.proto @@ -17,27 +17,66 @@ message Details { example: "\"69629012906488334\""; } ]; - - //sequence represents the order of events. It's always counting - // - // on read: the sequence of the last event reduced by the projection - // - // on manipulation: the timestamp of the event(s) added by the manipulation - uint64 sequence = 2 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"2\""; - } - ]; - //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 = 3; - //resource_owner represents the context an object belongs to - zitadel.object.v3alpha.Owner owner = 4 [ + //the timestamp of the first event applied to the object. + google.protobuf.Timestamp created = 3; + //the timestamp of the last event applied to the object. + google.protobuf.Timestamp changed = 4; + //the parent object representing the returned objects context. + zitadel.object.v3alpha.Owner owner = 5 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { example: "\"69629023906488334\""; } ]; -} \ No newline at end of file +} + +message SearchQuery { + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { + json_schema: { + title: "General List Query" + description: "Object unspecific list filters like offset, limit and asc/desc." + } + }; + uint64 offset = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"0\""; + } + ]; + uint32 limit = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "100"; + description: "Maximum amount of events returned. If not configured otherwise, the default is 100, the maximum is 1000. If the limit exceeds the maximum, ZITADEL throws an error."; + } + ]; + // If desc is true, the result is sorted by in descending order. Beware that if desc is true or the sorting column is not the creation date, the pagination results might be inconsistent. + bool desc = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "default is ascending, because together with the creation date sorting column, this returns the most deterministic pagination results."; + } + ]; +} + +message ListDetails { + uint64 applied_limit = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "100"; + } + ]; + uint64 total_result = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2\""; + } + ]; + google.protobuf.Timestamp timestamp = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "the last time the projection got updated" + } + ]; +} + +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; +} From fcda6580ffb86fe9f0651b5639c21a4202d92a5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Tue, 13 Aug 2024 15:52:43 +0300 Subject: [PATCH 29/39] fix(query): print log line on secret generator error (#8424) # Which Problems Are Solved Log some details when a secret generator is not found. This should help us debugging such issue. # How the Problems Are Solved When a secret generator by type query fails, we log the generator type and instance id for which the generator was requested. # Additional Changes - none # Additional Context - Related to https://github.com/zitadel/zitadel/issues/8379 - Also encountered in https://github.com/zitadel/zitadel/pull/8407 --- internal/domain/secret_generator.go | 1 + internal/domain/secretgeneratortype_enumer.go | 114 ++++++++++++++++++ internal/query/secret_generators.go | 5 +- 3 files changed, 119 insertions(+), 1 deletion(-) create mode 100644 internal/domain/secretgeneratortype_enumer.go diff --git a/internal/domain/secret_generator.go b/internal/domain/secret_generator.go index 4a3300a1bf..10afb774a7 100644 --- a/internal/domain/secret_generator.go +++ b/internal/domain/secret_generator.go @@ -1,5 +1,6 @@ package domain +//go:generate enumer -type SecretGeneratorType -transform snake -trimprefix SecretGeneratorType type SecretGeneratorType int32 const ( diff --git a/internal/domain/secretgeneratortype_enumer.go b/internal/domain/secretgeneratortype_enumer.go new file mode 100644 index 0000000000..92e0ead334 --- /dev/null +++ b/internal/domain/secretgeneratortype_enumer.go @@ -0,0 +1,114 @@ +// Code generated by "enumer -type SecretGeneratorType -transform snake -trimprefix SecretGeneratorType"; DO NOT EDIT. + +package domain + +import ( + "fmt" + "strings" +) + +const _SecretGeneratorTypeName = "unspecifiedinit_codeverify_email_codeverify_phone_codeverify_domainpassword_reset_codepasswordless_init_codeapp_secretotpsmsotp_emailsecret_generator_type_count" + +var _SecretGeneratorTypeIndex = [...]uint8{0, 11, 20, 37, 54, 67, 86, 108, 118, 124, 133, 160} + +const _SecretGeneratorTypeLowerName = "unspecifiedinit_codeverify_email_codeverify_phone_codeverify_domainpassword_reset_codepasswordless_init_codeapp_secretotpsmsotp_emailsecret_generator_type_count" + +func (i SecretGeneratorType) String() string { + if i < 0 || i >= SecretGeneratorType(len(_SecretGeneratorTypeIndex)-1) { + return fmt.Sprintf("SecretGeneratorType(%d)", i) + } + return _SecretGeneratorTypeName[_SecretGeneratorTypeIndex[i]:_SecretGeneratorTypeIndex[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 _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[secretGeneratorTypeCount-(10)] +} + +var _SecretGeneratorTypeValues = []SecretGeneratorType{SecretGeneratorTypeUnspecified, SecretGeneratorTypeInitCode, SecretGeneratorTypeVerifyEmailCode, SecretGeneratorTypeVerifyPhoneCode, SecretGeneratorTypeVerifyDomain, SecretGeneratorTypePasswordResetCode, SecretGeneratorTypePasswordlessInitCode, SecretGeneratorTypeAppSecret, SecretGeneratorTypeOTPSMS, SecretGeneratorTypeOTPEmail, secretGeneratorTypeCount} + +var _SecretGeneratorTypeNameToValueMap = map[string]SecretGeneratorType{ + _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:160]: secretGeneratorTypeCount, + _SecretGeneratorTypeLowerName[133:160]: 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:160], +} + +// SecretGeneratorTypeString retrieves an enum value from the enum constants string name. +// Throws an error if the param is not part of the enum. +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) +} + +// SecretGeneratorTypeValues returns all values of the enum +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 { + if i == v { + return true + } + } + return false +} diff --git a/internal/query/secret_generators.go b/internal/query/secret_generators.go index ffd62bd26f..8ee8694d2b 100644 --- a/internal/query/secret_generators.go +++ b/internal/query/secret_generators.go @@ -7,6 +7,7 @@ import ( "time" sq "github.com/Masterminds/squirrel" + "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/call" @@ -125,10 +126,11 @@ func (q *Queries) SecretGeneratorByType(ctx context.Context, generatorType domai ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() + instanceID := authz.GetInstance(ctx).InstanceID() stmt, scan := prepareSecretGeneratorQuery(ctx, q.client) query, args, err := stmt.Where(sq.Eq{ SecretGeneratorColumnGeneratorType.identifier(): generatorType, - SecretGeneratorColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), + SecretGeneratorColumnInstanceID.identifier(): instanceID, }).ToSql() if err != nil { return nil, zerrors.ThrowInternal(err, "QUERY-3k99f", "Errors.Query.SQLStatment") @@ -138,6 +140,7 @@ func (q *Queries) SecretGeneratorByType(ctx context.Context, generatorType domai generator, err = scan(row) return err }, query, args...) + logging.OnError(err).WithField("type", generatorType).WithField("instance_id", instanceID).Error("secret generator by type") return generator, err } From d32e22734f541b28124bf6d13ebf6aeab3b0ad72 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Wed, 14 Aug 2024 13:56:58 +0200 Subject: [PATCH 30/39] docs: update typescript repo (#8394) server package is node package now, idp scope is implemented --------- Co-authored-by: Fabi --- .../guides/integrate/login-ui/typescript-repo.mdx | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/docs/guides/integrate/login-ui/typescript-repo.mdx b/docs/docs/guides/integrate/login-ui/typescript-repo.mdx index 6986cf9bb7..4c864cbc90 100644 --- a/docs/docs/guides/integrate/login-ui/typescript-repo.mdx +++ b/docs/docs/guides/integrate/login-ui/typescript-repo.mdx @@ -9,10 +9,11 @@ The typescript repository contains all TypeScript and JavaScript packages and ap ## Included Apps And Packages - **[login](./typescript-repo#new-login-ui)**: The future login UI used by ZITADEL Cloud, powered by Next.js -- `@zitadel/server`: core components for establishing node client connection, grpc stub -- `@zitadel/client`: core components for establishing web client connection, grpc stub -- `@zitadel/react`: shared React utilities and components built with Tailwind CSS -- `@zitadel/next`: shared Next.js utilities +- `@zitadel/proto`: Typescript implementation of Protocol Buffers, suitable for web browsers and Node.js. +- `@zitadel/client`: Core components for establishing a client connection +- `@zitadel/node`: Core components for establishing a server connection +- `@zitadel/react`: Shared React Utilities and components built with Tailwind CSS +- `@zitadel/next`: Shared Next.js Utilities - `@zitadel/tsconfig`: shared `tsconfig.json`s used throughout the monorepo - `eslint-config-zitadel`: ESLint preset @@ -69,7 +70,7 @@ The application can then request a token calling the /token endpoint of the logi - Scopes - [x] `openid email profile address`` - [x] `offline access` - - [ ] `urn:zitadel:iam:org:idp:id:{idp_id}` + - [x] `urn:zitadel:iam:org:idp:id:{idp_id}` - [x] `urn:zitadel:iam:org:project:id:zitadel:aud` - [x] `urn:zitadel:iam:org:id:{orgid}` - [x] `urn:zitadel:iam:org:domain:primary:{domain}` From e2e1100124a64fd5bb1fba53fa51d9d5ee809704 Mon Sep 17 00:00:00 2001 From: Livio Spring Date: Wed, 14 Aug 2024 15:04:26 +0200 Subject: [PATCH 31/39] feat(idp): provide auto only options (#8420) # Which Problems Are Solved As of now, **automatic creation** and **automatic linking options** were only considered if the corresponding **allowed option** (account creation / linking allowed) was enabled. With this PR, this is no longer needed and allows administrators to address cases, where only an **automatic creation** is allowed, but users themselves should not be allowed to **manually** create new accounts using an identity provider or edit the information during the process. Also, allowing users to only link to the proposed existing account is now possible with an enabled **automatic linking option**, while disabling **account linking allowed**. # How the Problems Are Solved - Check for **automatic** options without the corresponding **allowed** option. - added technical advisory to notify about the possible behavior change # Additional Changes - display the error message on the IdP linking step in the login UI (in case there is one) - display an error in case no option is possible - exchanged deprecated `eventstoreExpect` with `expectEventstore` in touched test files # Additional Context closes https://github.com/zitadel/zitadel/issues/7393 --------- Co-authored-by: Stefan Benz <46600784+stebenz@users.noreply.github.com> --- 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/it.json | 8 +- console/src/assets/i18n/ja.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/ru.json | 8 +- console/src/assets/i18n/sv.json | 8 +- console/src/assets/i18n/zh.json | 8 +- docs/docs/apis/_v3_idp.proto | 8 +- .../identity-providers/introduction.md | 6 +- docs/docs/support/advisory/a10011.md | 29 ++ docs/docs/support/technical_advisory.mdx | 12 + .../api/ui/login/external_provider_handler.go | 11 +- internal/api/ui/login/link_users_handler.go | 3 + internal/api/ui/login/static/i18n/bg.yaml | 1 + internal/api/ui/login/static/i18n/cs.yaml | 1 + internal/api/ui/login/static/i18n/de.yaml | 1 + internal/api/ui/login/static/i18n/en.yaml | 5 +- internal/api/ui/login/static/i18n/es.yaml | 1 + internal/api/ui/login/static/i18n/fr.yaml | 1 + internal/api/ui/login/static/i18n/it.yaml | 1 + internal/api/ui/login/static/i18n/ja.yaml | 1 + internal/api/ui/login/static/i18n/mk.yaml | 1 + internal/api/ui/login/static/i18n/nl.yaml | 1 + internal/api/ui/login/static/i18n/pl.yaml | 1 + internal/api/ui/login/static/i18n/pt.yaml | 1 + internal/api/ui/login/static/i18n/ru.yaml | 1 + internal/api/ui/login/static/i18n/sv.yaml | 1 + internal/api/ui/login/static/i18n/zh.yaml | 1 + .../static/templates/link_users_done.html | 4 + internal/command/user_human_test.go | 439 +++++++++++++++--- internal/command/user_idp_link.go | 5 +- internal/command/user_idp_link_test.go | 296 ++++++++++-- proto/zitadel/idp.proto | 4 +- 41 files changed, 776 insertions(+), 180 deletions(-) create mode 100644 docs/docs/support/advisory/a10011.md diff --git a/console/src/assets/i18n/bg.json b/console/src/assets/i18n/bg.json index 920c5a6047..e8b8ade072 100644 --- a/console/src/assets/i18n/bg.json +++ b/console/src/assets/i18n/bg.json @@ -2058,10 +2058,10 @@ "ISAUTOCREATION_DESC": "Ако е избрано, ще бъде създаден акаунт, ако все още не съществува.", "ISAUTOUPDATE": "Автоматична актуализация", "ISAUTOUPDATE_DESC": "Ако е избрано, акаунтите се актуализират при повторно удостоверяване.", - "ISCREATIONALLOWED": "Създаването на акаунт е разрешено", - "ISCREATIONALLOWED_DESC": "Определя дали могат да се създават акаунти.", - "ISLINKINGALLOWED": "Свързването на акаунти е разрешено", - "ISLINKINGALLOWED_DESC": "Определя дали дадена самоличност може да бъде свързана със съществуващ акаунт.", + "ISCREATIONALLOWED": "Създаването на акаунт е разрешено (ръчно)", + "ISCREATIONALLOWED_DESC": "Определя дали могат да се създават акаунти с помощта на външен акаунт. Деактивирай, ако потребителите не трябва да могат да редактират информация за акаунта, когато автоматичното създаване е активирано.", + "ISLINKINGALLOWED": "Свързването на акаунти е разрешено (ръчно)", + "ISLINKINGALLOWED_DESC": "Определя дали дадена самоличност може да бъде свързана със съществуващ акаунт. Деактивирай, ако потребителите трябва да могат да свързват само предлагания акаунт в случай на активно автоматично свързване.", "AUTOLINKING_DESC": "Определя дали идентичността ще бъде подканена да бъде свързана със съществуващ профил.", "AUTOLINKINGTYPE": { "0": "Изключено", diff --git a/console/src/assets/i18n/cs.json b/console/src/assets/i18n/cs.json index 579ccbf261..8cdbc69e64 100644 --- a/console/src/assets/i18n/cs.json +++ b/console/src/assets/i18n/cs.json @@ -2063,10 +2063,10 @@ "ISAUTOCREATION_DESC": "Pokud je vybráno, účet bude vytvořen, pokud ještě neexistuje.", "ISAUTOUPDATE": "Automatická aktualizace", "ISAUTOUPDATE_DESC": "Pokud je vybráno, účty jsou aktualizovány při opětovné autentizaci.", - "ISCREATIONALLOWED": "Je povoleno vytváření účtu", - "ISCREATIONALLOWED_DESC": "Určuje, zda lze vytvářet účty.", - "ISLINKINGALLOWED": "Je povoleno propojení účtů", - "ISLINKINGALLOWED_DESC": "Určuje, zda lze identitu propojit s existujícím účtem.", + "ISCREATIONALLOWED": "Vytvoření účtu povoleno (ručně)", + "ISCREATIONALLOWED_DESC": "Určuje, zda mohou být účty vytvořeny pomocí externího účtu. Zakázat, pokud uživatelé by neměli být schopni upravovat informace o účtu, když je automatické vytváření povoleno.", + "ISLINKINGALLOWED": "Propojení účtu povoleno (ručně)", + "ISLINKINGALLOWED_DESC": "Určuje, zda identita může být ručně propojena s existujícím účtem. Zakázat, pokud by uživatelé měli být povoleni pouze propojit navrhovaný účet v případě aktivního automatického propojení.", "AUTOLINKING_DESC": "Určuje, zda se bude identita vyzývat k propojení se stávajícím účtem.", "AUTOLINKINGTYPE": { "0": "Vypnuto", diff --git a/console/src/assets/i18n/de.json b/console/src/assets/i18n/de.json index e07c003497..2ffdb74e8f 100644 --- a/console/src/assets/i18n/de.json +++ b/console/src/assets/i18n/de.json @@ -2054,10 +2054,10 @@ "ISAUTOCREATION_DESC": "Legt fest, ob ein Konto erstellt wird, falls es noch nicht existiert.", "ISAUTOUPDATE": "Automatisches Update", "ISAUTOUPDATE_DESC": "Legt fest, ob Konten bei der erneuten Authentifizierung aktualisiert werden.", - "ISCREATIONALLOWED": "Account erstellen erlaubt", - "ISCREATIONALLOWED_DESC": "Legt fest, ob Konten erstellt werden können.", - "ISLINKINGALLOWED": "Account linking erlaubt", - "ISLINKINGALLOWED_DESC": "Legt fest, ob eine Identität mit einem bestehenden Konto verknüpft werden kann.", + "ISCREATIONALLOWED": "Kontoerstellung erlaubt (manuell)", + "ISCREATIONALLOWED_DESC": "Bestimmt, ob Konten mit einem externen Konto erstellt werden können. Deaktiviere, wenn Benutzer Kontoinformationen nicht bearbeiten können sollen, wenn die automatische Erstellung aktiviert ist.", + "ISLINKINGALLOWED": "Kontoverknüpfung erlaubt (manuell)", + "ISLINKINGALLOWED_DESC": "Bestimmt, ob eine Identität manuell mit einem bestehenden Konto verknüpft werden kann. Deaktiviere, wenn Benutzer nur das vorgeschlagene Konto im Falle einer aktiven automatischen Verknüpfung verknüpfen dürfen.", "AUTOLINKING_DESC": "Legt fest, ob eine Identität aufgefordert wird, mit einem vorhandenen Konto verknüpft zu werden.", "AUTOLINKINGTYPE": { "0": "Deaktiviert", diff --git a/console/src/assets/i18n/en.json b/console/src/assets/i18n/en.json index 6fbfa6a6b2..fefbb5bfb5 100644 --- a/console/src/assets/i18n/en.json +++ b/console/src/assets/i18n/en.json @@ -2066,10 +2066,10 @@ "ISAUTOCREATION_DESC": "If selected, an account will be created if it does not exist yet.", "ISAUTOUPDATE": "Automatic update", "ISAUTOUPDATE_DESC": "If selected, accounts are updated on reauthentication.", - "ISCREATIONALLOWED": "Account creation allowed", - "ISCREATIONALLOWED_DESC": "Determines whether accounts can be created.", - "ISLINKINGALLOWED": "Account linking allowed", - "ISLINKINGALLOWED_DESC": "Determines whether an identity can be linked to an existing account.", + "ISCREATIONALLOWED": "Account creation allowed (manually)", + "ISCREATIONALLOWED_DESC": "Determines whether accounts can be created using an external account. Disable if users should not be able to edit account information when auto_creation is enabled.", + "ISLINKINGALLOWED": "Account linking allowed (manually)", + "ISLINKINGALLOWED_DESC": "Determines whether an identity can be manually linked to an existing account. Disable if users should only be allowed to link the proposed account in case of active auto_linking.", "AUTOLINKING_DESC": "Determines whether an identity will be prompted to be linked to an existing account.", "AUTOLINKINGTYPE": { "0": "Disabled", diff --git a/console/src/assets/i18n/es.json b/console/src/assets/i18n/es.json index a252d8db0e..61867dbc76 100644 --- a/console/src/assets/i18n/es.json +++ b/console/src/assets/i18n/es.json @@ -2059,10 +2059,10 @@ "ISAUTOCREATION_DESC": "Si se selecciona, una cuenta se creará si aún no existiera.", "ISAUTOUPDATE": "Actualización automática", "ISAUTOUPDATE_DESC": "Si se selecciona, las cuentas se actualizarán en la reautenticación.", - "ISCREATIONALLOWED": "Creación de cuentas permitida", - "ISCREATIONALLOWED_DESC": "Determina si se pueden crear cuentas.", - "ISLINKINGALLOWED": "Permitida la vinculación de cuentas", - "ISLINKINGALLOWED_DESC": "Determina si una identidad puede vincularse a una cuenta existente.", + "ISCREATIONALLOWED": "Creación de cuenta permitida (manualmente)", + "ISCREATIONALLOWED_DESC": "Determina si las cuentas se pueden crear usando una cuenta externa. Deshabilita si los usuarios no deberían poder editar la información de la cuenta cuando la creación automática está habilitada.", + "ISLINKINGALLOWED": "Enlace de cuenta permitido (manualmente)", + "ISLINKINGALLOWED_DESC": "Determina si una identidad se puede vincular manualmente a una cuenta existente. Deshabilita si los usuarios solo deberían poder vincular la cuenta propuesta en caso de vinculación automática activa.", "AUTOLINKING_DESC": "Determina si se pedirá a una identidad que se vincule a una cuenta existente.", "AUTOLINKINGTYPE": { "0": "Desactivado", diff --git a/console/src/assets/i18n/fr.json b/console/src/assets/i18n/fr.json index a4ff97869e..fb250f06ca 100644 --- a/console/src/assets/i18n/fr.json +++ b/console/src/assets/i18n/fr.json @@ -2058,10 +2058,10 @@ "ISAUTOCREATION_DESC": "Détermine si un compte sera créé s'il n'existe pas déjà.", "ISAUTOUPDATE": "Mise à jour automatique", "ISAUTOUPDATE_DESC": "Si cette option est sélectionnée, les comptes sont mis à jour lors de la réauthentification.", - "ISCREATIONALLOWED": "Création de comptes autorisée", - "ISCREATIONALLOWED_DESC": "Détermine si des comptes peuvent être créés.", - "ISLINKINGALLOWED": "Liaison de comptes autorisée", - "ISLINKINGALLOWED_DESC": "Détermine si une identité peut être liée à un compte existant.", + "ISCREATIONALLOWED": "Création de compte autorisée (manuellement)", + "ISCREATIONALLOWED_DESC": "Détermine si les comptes peuvent être créés à l'aide d'un compte externe. Désactivez si les utilisateurs ne doivent pas pouvoir modifier les informations du compte lorsque la création automatique est activée.", + "ISLINKINGALLOWED": "Liaison de compte autorisée (manuellement)", + "ISLINKINGALLOWED_DESC": "Détermine si une identité peut être liée manuellement à un compte existant. Désactivez si les utilisateurs ne doivent pouvoir lier que le compte proposé en cas de liaison automatique active.", "AUTOLINKING_DESC": "Détermine si une identité sera invitée à être liée à un compte existant.", "AUTOLINKINGTYPE": { "0": "Désactivé", diff --git a/console/src/assets/i18n/it.json b/console/src/assets/i18n/it.json index 4693af6187..96206462c0 100644 --- a/console/src/assets/i18n/it.json +++ b/console/src/assets/i18n/it.json @@ -2058,10 +2058,10 @@ "ISAUTOCREATION_DESC": "Se selezionato, verrà creato un account se non esiste ancora.", "ISAUTOUPDATE": "Aggiornamento automatico", "ISAUTOUPDATE_DESC": "Se selezionato, gli account vengono aggiornati alla riautenticazione.", - "ISCREATIONALLOWED": "Creazione consentita", - "ISCREATIONALLOWED_DESC": "Determina se i conti possono essere creati.", - "ISLINKINGALLOWED": "Collegamento consentito", - "ISLINKINGALLOWED_DESC": "Determina se un'identità può essere collegata a un account esistente.", + "ISCREATIONALLOWED": "Creazione account consentita (manualmente)", + "ISCREATIONALLOWED_DESC": "Determina se gli account possono essere creati utilizzando un account esterno. Disabilita se gli utenti non dovrebbero essere in grado di modificare le informazioni dell'account quando la creazione automatica è abilitata.", + "ISLINKINGALLOWED": "Collegamento account consentito (manualmente)", + "ISLINKINGALLOWED_DESC": "Determina se un'identità può essere collegata manualmente a un account esistente. Disabilita se gli utenti dovrebbero essere autorizzati solo a collegare l'account proposto in caso di collegamento automatico attivo.", "AUTOLINKING_DESC": "Determina se un'identità verrà invitata a essere collegata a un account esistente.", "AUTOLINKINGTYPE": { "0": "Disabilitato", diff --git a/console/src/assets/i18n/ja.json b/console/src/assets/i18n/ja.json index fd823d5243..aa89b52a8b 100644 --- a/console/src/assets/i18n/ja.json +++ b/console/src/assets/i18n/ja.json @@ -2054,10 +2054,10 @@ "ISAUTOCREATION_DESC": "選択した場合、アカウントがまだ存在しない場合はアカウントが作成されます。", "ISAUTOUPDATE": "自動更新", "ISAUTOUPDATE_DESC": "選択した場合、アカウントは再認証時に更新されます。", - "ISCREATIONALLOWED": "アカウント作成を許可", - "ISCREATIONALLOWED_DESC": "アカウントを作成できるかどうかを決めます。", - "ISLINKINGALLOWED": "アカウントリンクを許可", - "ISLINKINGALLOWED_DESC": "IDを既存のアカウントにリンクできるかどうかを決めます。", + "ISCREATIONALLOWED": "アカウント作成を許可 (手動)", + "ISCREATIONALLOWED_DESC": "外部アカウントを使用してアカウントを作成できるかどうかを決定します。 自動作成が有効になっている場合、ユーザーがアカウント情報を編集できないようにするには、無効にします。", + "ISLINKINGALLOWED": "アカウントリンクを許可 (手動)", + "ISLINKINGALLOWED_DESC": "アイデンティティを手動で既存のアカウントにリンクできるかどうかを決定します。 自動リンクがアクティブな場合は、ユーザーが提案されたアカウントのみをリンクできるようにするには、無効にします。", "AUTOLINKING_DESC": "アイデンティティが既存のアカウントにリンクされるように促されるかどうかを決定します。", "AUTOLINKINGTYPE": { "0": "無効", diff --git a/console/src/assets/i18n/mk.json b/console/src/assets/i18n/mk.json index 47b6f76ac9..847e221267 100644 --- a/console/src/assets/i18n/mk.json +++ b/console/src/assets/i18n/mk.json @@ -2061,10 +2061,10 @@ "ISAUTOCREATION_DESC": "Доколку е избрано, корисничка сметка ќе биде креиран ако сè уште не постои.", "ISAUTOUPDATE": "Автоматско ажурирање", "ISAUTOUPDATE_DESC": "Доколку е избрано, корисничките сметки се ажурираат при повторно најавување.", - "ISCREATIONALLOWED": "Дозволено креирање на кориснички сметки", - "ISCREATIONALLOWED_DESC": "Одредува дали може да се креираат кориснички сметки.", - "ISLINKINGALLOWED": "Дозволено поврзување на кориснички сметки", - "ISLINKINGALLOWED_DESC": "Одредува дали може да се поврзе идентитет со постоечка корисничка сметка.", + "ISCREATIONALLOWED": "Создавање на сметка дозволено (рачно)", + "ISCREATIONALLOWED_DESC": "Одредува дали сметките можат да се создадат со користење на надворешна сметка. Деактивирај ако корисниците не треба да можат да уредуваат информации за сметката кога автоматското создавање е овозможено.", + "ISLINKINGALLOWED": "Поврзување на сметка дозволено (рачно)", + "ISLINKINGALLOWED_DESC": "Одредува дали идентитет може да биде рачно поврзан со постоечка сметка. Деактивирај ако корисниците треба да можат да поврзуваат само предложената сметка во случај на активно автоматско поврзување.", "AUTOLINKING_DESC": "Одредува дали ќе се бара идентитетот да биде поврзан со постоечки профил.", "AUTOLINKINGTYPE": { "0": "Исклучено", diff --git a/console/src/assets/i18n/nl.json b/console/src/assets/i18n/nl.json index f40fb493f1..9e494b3dc1 100644 --- a/console/src/assets/i18n/nl.json +++ b/console/src/assets/i18n/nl.json @@ -2066,10 +2066,10 @@ "ISAUTOCREATION_DESC": "Als geselecteerd, wordt een account aangemaakt als het nog niet bestaat.", "ISAUTOUPDATE": "Automatische update", "ISAUTOUPDATE_DESC": "Als geselecteerd, worden accounts bijgewerkt bij opnieuw authenticeren.", - "ISCREATIONALLOWED": "Account creatie toegestaan", - "ISCREATIONALLOWED_DESC": "Bepaalt of accounts kunnen worden aangemaakt.", - "ISLINKINGALLOWED": "Account koppeling toegestaan", - "ISLINKINGALLOWED_DESC": "Bepaalt of een identiteit kan worden gekoppeld aan een bestaand account.", + "ISCREATIONALLOWED": "Accountaanmaak toegestaan (handmatig)", + "ISCREATIONALLOWED_DESC": "Bepaalt of accounts kunnen worden aangemaakt met behulp van een extern account. Schakel uit als gebruikers accountinformatie niet mogen kunnen bewerken wanneer auto-aanmaak is ingeschakeld.", + "ISLINKINGALLOWED": "Accountkoppeling toegestaan (handmatig)", + "ISLINKINGALLOWED_DESC": "Bepaalt of een identiteit handmatig kan worden gekoppeld aan een bestaand account. Schakel uit als gebruikers alleen het voorgestelde account mogen kunnen koppelen in geval van actieve auto-koppeling.", "AUTOLINKING_DESC": "Bepaalt of een identiteit wordt gevraagd om te worden gekoppeld aan een bestaand account.", "AUTOLINKINGTYPE": { "0": "Uitgeschakeld", diff --git a/console/src/assets/i18n/pl.json b/console/src/assets/i18n/pl.json index 7823271790..3caf7f39c1 100644 --- a/console/src/assets/i18n/pl.json +++ b/console/src/assets/i18n/pl.json @@ -2057,10 +2057,10 @@ "ISAUTOCREATION_DESC": "Jeśli zostanie wybrana, konto zostanie utworzone, jeśli jeszcze nie istnieje.", "ISAUTOUPDATE": "Automatyczna aktualizacja", "ISAUTOUPDATE_DESC": "Jeśli zaznaczone, konta są aktualizowane przy ponownym uwierzytelnianiu.", - "ISCREATIONALLOWED": "tworzenie dozwolone", - "ISCREATIONALLOWED_DESC": "Określa, czy można tworzyć konta.", - "ISLINKINGALLOWED": "dozwolone łączenie rachunków", - "ISLINKINGALLOWED_DESC": "Określa, czy tożsamość może być powiązana z istniejącym kontem.", + "ISCREATIONALLOWED": "Tworzenie konta dozwolone (ręcznie)", + "ISCREATIONALLOWED_DESC": "Określa, czy konta mogą być tworzone przy użyciu konta zewnętrznego. Wyłącz, jeśli użytkownicy nie powinni mieć możliwości edytowania informacji o koncie, gdy automatyczne tworzenie jest włączone.", + "ISLINKINGALLOWED": "Łączenie kont dozwolone (ręcznie)", + "ISLINKINGALLOWED_DESC": "Określa, czy tożsamość może być ręcznie połączona z istniejącym kontem. Wyłącz, jeśli użytkownicy powinni mieć możliwość łączenia tylko proponowanego konta w przypadku aktywnego automatycznego łączenia.", "AUTOLINKING_DESC": "Określa, czy tożsamość będzie proszona o połączenie z istniejącym kontem.", "AUTOLINKINGTYPE": { "0": "Wyłączone", diff --git a/console/src/assets/i18n/pt.json b/console/src/assets/i18n/pt.json index ec2752086b..c40f635357 100644 --- a/console/src/assets/i18n/pt.json +++ b/console/src/assets/i18n/pt.json @@ -2059,10 +2059,10 @@ "ISAUTOCREATION_DESC": "Se selecionado, uma conta será criada se ainda não existir.", "ISAUTOUPDATE": "Atualização Automática", "ISAUTOUPDATE_DESC": "Se selecionado, as contas são atualizadas ao serem reautenticadas.", - "ISCREATIONALLOWED": "Criação de Conta Permitida", - "ISCREATIONALLOWED_DESC": "Determina se as contas podem ser criadas.", - "ISLINKINGALLOWED": "Vinculação de Conta Permitida", - "ISLINKINGALLOWED_DESC": "Determina se uma identidade pode ser vinculada a uma conta existente.", + "ISCREATIONALLOWED": "Criação de conta permitida (manualmente)", + "ISCREATIONALLOWED_DESC": "Determina se contas podem ser criadas usando uma conta externa. Desative se os usuários não devem poder editar informações da conta quando a criação automática está ativada.", + "ISLINKINGALLOWED": "Vinculação de conta permitida (manualmente)", + "ISLINKINGALLOWED_DESC": "Determina se uma identidade pode ser vinculada manualmente a uma conta existente. Desative se os usuários só devem poder vincular a conta proposta em caso de vinculação automática ativa.", "AUTOLINKING_DESC": "Determina se uma identidade será solicitada a ser vinculada a uma conta existente.", "AUTOLINKINGTYPE": { "0": "Desativado", diff --git a/console/src/assets/i18n/ru.json b/console/src/assets/i18n/ru.json index d0d17758f1..532aa2ba9d 100644 --- a/console/src/assets/i18n/ru.json +++ b/console/src/assets/i18n/ru.json @@ -2148,10 +2148,10 @@ "ISAUTOCREATION_DESC": "Если этот флажок установлен, учетная запись будет создана, если она еще не существует.", "ISAUTOUPDATE": "Автоматическое обновление", "ISAUTOUPDATE_DESC": "Если этот флажок установлен, учетные записи обновляются при повторной аутентификации.", - "ISCREATIONALLOWED": "Создание учетной записи разрешено", - "ISCREATIONALLOWED_DESC": "Определяет, можно ли создавать учетные записи.", - "ISLINKINGALLOWED": "Привязка аккаунтов разрешена", - "ISLINKINGALLOWED_DESC": "Определяет, можно ли связать личность с существующей учетной записью.", + "ISCREATIONALLOWED": "Создание аккаунта разрешено (вручную)", + "ISCREATIONALLOWED_DESC": "Определяет, можно ли создавать учетные записи с использованием внешней учетной записи. Отключите, если пользователи не должны иметь возможность редактировать информацию об аккаунте при включенном автоматическом создании.", + "ISLINKINGALLOWED": "Связывание аккаунтов разрешено (вручную)", + "ISLINKINGALLOWED_DESC": "Определяет, можно ли вручную связать идентификатор с существующим аккаунтом. Отключите, если пользователям разрешено связывать только предлагаемый аккаунт в случае активного автоматического связывания.", "AUTOLINKING_DESC": "Определяет, будет ли запрошено связать идентификацию с существующим аккаунтом.", "AUTOLINKINGTYPE": { "0": "Отключено", diff --git a/console/src/assets/i18n/sv.json b/console/src/assets/i18n/sv.json index 10b0b86152..13333a4c32 100644 --- a/console/src/assets/i18n/sv.json +++ b/console/src/assets/i18n/sv.json @@ -2070,10 +2070,10 @@ "ISAUTOCREATION_DESC": "Om valt, skapas ett konto om det inte redan finns.", "ISAUTOUPDATE": "Automatisk uppdatering", "ISAUTOUPDATE_DESC": "Om valt, uppdateras konton vid återautentisering.", - "ISCREATIONALLOWED": "Kontoskapande tillåtet", - "ISCREATIONALLOWED_DESC": "Avgör om konton kan skapas.", - "ISLINKINGALLOWED": "Kontolänkning tillåten", - "ISLINKINGALLOWED_DESC": "Avgör om en identitet kan länkas till ett befintligt konto.", + "ISCREATIONALLOWED": "Kontoinrättning tillåten (manuellt)", + "ISCREATIONALLOWED_DESC": "Bestämmer om konton kan skapas med hjälp av ett externt konto. Avaktivera om användare inte ska kunna redigera kontoinformation när automatisk skapelse är aktiverad.", + "ISLINKINGALLOWED": "Kontolänkning tillåten (manuellt)", + "ISLINKINGALLOWED_DESC": "Bestämmer om en identitet kan länkas manuellt till ett befintligt konto. Avaktivera om användare bara ska kunna länka det föreslagna kontot vid aktiv automatisk länkning.", "AUTOLINKING_DESC": "Avgör om en identitet kommer att uppmanas att länkas till ett befintligt konto.", "AUTOLINKINGTYPE": { "0": "Inaktiverad", diff --git a/console/src/assets/i18n/zh.json b/console/src/assets/i18n/zh.json index 27b695e8a4..d273750ad6 100644 --- a/console/src/assets/i18n/zh.json +++ b/console/src/assets/i18n/zh.json @@ -2057,10 +2057,10 @@ "ISAUTOCREATION_DESC": "如果选择了,如果账户还不存在,就会创建一个账户。", "ISAUTOUPDATE": "正在自动更新", "ISAUTOUPDATE_DESC": "如果选择,账户将在重新认证时更新。", - "ISCREATIONALLOWED": "是否允许创作", - "ISCREATIONALLOWED_DESC": "确定是否可以创建账户。", - "ISLINKINGALLOWED": "是否允许连接", - "ISLINKINGALLOWED_DESC": "确定一个身份是否可以与一个现有的账户相联系。", + "ISCREATIONALLOWED": "允许创建账户(手动)", + "ISCREATIONALLOWED_DESC": "确定是否可以使用外部账户创建账户。如果用户在启用自动创建时不应该能够编辑账户信息,请禁用。", + "ISLINKINGALLOWED": "允许链接账户(手动)", + "ISLINKINGALLOWED_DESC": "确定是否可以手动将身份链接到现有账户。如果用户只能在激活自动链接的情况下链接建议的账户,请禁用。", "AUTOLINKING_DESC": "确定是否提示将身份链接到现有帐户。", "AUTOLINKINGTYPE": { "0": "已禁用", diff --git a/docs/docs/apis/_v3_idp.proto b/docs/docs/apis/_v3_idp.proto index 54fb1ede1b..bb2ed741aa 100644 --- a/docs/docs/apis/_v3_idp.proto +++ b/docs/docs/apis/_v3_idp.proto @@ -66,14 +66,14 @@ enum AutoLinkingOption { } message Options { - bool is_linking_allowed = 1 [ + bool is_manual_linking_allowed = 1 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - description: "Enable if users should be able to link an existing ZITADEL user with an external account."; + description: "Enable if users should be able to link an existing ZITADEL user with an external account. Disable if users should only be allowed to link the proposed account in case of active auto_linking."; } ]; - bool is_creation_allowed = 2 [ + bool is_manual_creation_allowed = 2 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - description: "Enable if users should be able to create a new account in ZITADEL when using an external account."; + description: "Enable if users should be able to create a new account in ZITADEL when using an external account. Disable if users should not be able to edit account information when auto_creation is enabled."; } ]; bool is_auto_creation = 3 [ diff --git a/docs/docs/guides/integrate/identity-providers/introduction.md b/docs/docs/guides/integrate/identity-providers/introduction.md index 738f7b312b..628b91a40b 100644 --- a/docs/docs/guides/integrate/identity-providers/introduction.md +++ b/docs/docs/guides/integrate/identity-providers/introduction.md @@ -147,9 +147,11 @@ When configuring external IdP templates in ZITADEL, several common settings enab - **Automatic update**: This feature, when activated, allows ZITADEL to automatically update a user's profile information whenever changes are detected in the user's account on the external IdP. For example, if a user changes their last name in their Google or Microsoft account, ZITADEL will reflect this update in the user's account upon their next login. -- **Account creation allowed**: Determines whether new user accounts can be created in ZITADEL through the external IdP authentication process. Enabling this setting is crucial for allowing users who are new to your application to register and create accounts seamlessly via their existing external IdP accounts. +- **Account creation allowed (manually)**: Determines whether new user accounts can be created in ZITADEL through the external IdP authentication process. Enabling this setting is crucial for allowing users who are new to your application to register and create accounts seamlessly via their existing external IdP accounts. However, if you rely on the **automatic creation** and want to prevent users to manually create their accounts or edit information during the automatic process, you need to disable this option. -- **Account linking allowed**: Enables existing ZITADEL accounts to be linked with identities from external IdPs. It requires that a linkable ZITADEL account already exists for the user attempting to log in with an external IdP. Account linking is beneficial for users who wish to associate multiple login methods with their ZITADEL account, providing flexibility and convenience in how they access your application. +- **Account linking allowed (manually)**: Enables existing ZITADEL accounts to be linked with identities from external IdPs. It requires that a linkable ZITADEL account already exists for the user attempting to log in with an external IdP. Account linking is beneficial for users who wish to associate multiple login methods with their ZITADEL account, providing flexibility and convenience in how they access your application. However, if you rely on an **automatic linking option** and want to prevent users to manually link their accounts, you need to disable this option. + +- **Automatic linking options**: Enables existing ZITADEL accounts to be linked with identities from external IdPs. If not disabled, ZITADEL will check for an existing account with the configured criteria (username or email) and prompt the user to link the account. diff --git a/docs/docs/support/advisory/a10011.md b/docs/docs/support/advisory/a10011.md new file mode 100644 index 0000000000..992e4202c2 --- /dev/null +++ b/docs/docs/support/advisory/a10011.md @@ -0,0 +1,29 @@ +--- +title: Technical Advisory 10011 +--- + +## Date and Version + +Version: 2.60.0 + +Date: TBD + +## Description + +Version 2.60.0 allows more combinations in the identity provider options. As of now, **automatic creation** and **automatic linking options** were only considered if the corresponding **allowed option** (account creation / linking allowed) was enabled. + +Starting with this release, this is no longer needed and allows administrators to address cases, where only an **automatic creation** is allowed, but users themselves should not be allowed to **manually** create new accounts using an identity provider or edit the information during the process. +Also, allowing users to only link to the proposed existing account is now possible with an enabled **automatic linking option**, while disabling **account linking allowed**. + +## Statement + +This change was tracked in the following PR: +[feat(idp): provide auto only options](https://github.com/zitadel/zitadel/pull/8420), which was released in Version [2.60.0](https://github.com/zitadel/zitadel/releases/tag/v2.60.0) + +## Mitigation + +If you previously enabled one of the **automatic** options with the corresponding **allowed** option, be sure that this is the intended behavior. + +## Impact + +Once this update has been released and deployed, the **automatic** options can be activated with the corresponding **allowed** option. diff --git a/docs/docs/support/technical_advisory.mdx b/docs/docs/support/technical_advisory.mdx index e205b87683..a252803fe0 100644 --- a/docs/docs/support/technical_advisory.mdx +++ b/docs/docs/support/technical_advisory.mdx @@ -178,6 +178,18 @@ We understand that these advisories may include breaking changes, and we aim to 2.53.0 2024-05-28 + + + A-10011 + + Identity Provider options: allow "auto" only + Breaking Behavior Change + + Version 2.60.0 allows more combinations in the identity provider options. Due to this there might be unexpected behavior changes. + + 2.53.0 + 2024-05-28 + ## Subscribe to our Mailing List diff --git a/internal/api/ui/login/external_provider_handler.go b/internal/api/ui/login/external_provider_handler.go index b8c6a70abf..a462f06a7d 100644 --- a/internal/api/ui/login/external_provider_handler.go +++ b/internal/api/ui/login/external_provider_handler.go @@ -533,9 +533,10 @@ func (l *Login) externalUserNotExisting(w http.ResponseWriter, r *http.Request, } } - // if auto creation or creation itself is disabled, send the user to the notFoundOption - if !provider.IsCreationAllowed || !provider.IsAutoCreation { - l.renderExternalNotFoundOption(w, r, authReq, orgIAMPolicy, human, idpLink, err) + // if auto creation is disabled, send the user to the notFoundOption + // where they can either link or create an account (based on the available options) + if !provider.IsAutoCreation { + l.renderExternalNotFoundOption(w, r, authReq, orgIAMPolicy, human, idpLink, nil) return } @@ -614,6 +615,10 @@ func (l *Login) renderExternalNotFoundOption(w http.ResponseWriter, r *http.Requ l.renderError(w, r, authReq, err) return } + if !idpTemplate.IsCreationAllowed && !idpTemplate.IsLinkingAllowed { + l.renderError(w, r, authReq, zerrors.ThrowPreconditionFailed(nil, "LOGIN-3kl44", "Errors.User.ExternalIDP.NoOptionAllowed")) + return + } translator := l.getTranslator(r.Context(), authReq) data := externalNotFoundOptionData{ diff --git a/internal/api/ui/login/link_users_handler.go b/internal/api/ui/login/link_users_handler.go index 1952ed1213..c720559084 100644 --- a/internal/api/ui/login/link_users_handler.go +++ b/internal/api/ui/login/link_users_handler.go @@ -19,6 +19,9 @@ func (l *Login) linkUsers(w http.ResponseWriter, r *http.Request, authReq *domai func (l *Login) renderLinkUsersDone(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, err error) { var errType, errMessage string + if err != nil { + errType, errMessage = l.getErrorMessage(r, err) + } translator := l.getTranslator(r.Context(), authReq) data := l.getUserData(r, authReq, translator, "LinkingUsersDone.Title", "LinkingUsersDone.Description", errType, errMessage) l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplLinkUsersDone], data, nil) diff --git a/internal/api/ui/login/static/i18n/bg.yaml b/internal/api/ui/login/static/i18n/bg.yaml index 93df99ab47..91c4ca0fbf 100644 --- a/internal/api/ui/login/static/i18n/bg.yaml +++ b/internal/api/ui/login/static/i18n/bg.yaml @@ -475,6 +475,7 @@ Errors: NoExternalUserData: Не са получени външни потребителски данни CreationNotAllowed: Създаването на нов потребител не е разрешено на този доставчик LinkingNotAllowed: Свързването на потребител не е разрешено на този доставчик + NoOptionAllowed: Нито създаване, нито свързване е разрешено за този доставчик. Моля, свържете се с администратора. GrantRequired: 'Влизането не е възможно. ' ProjectRequired: 'Влизането не е възможно. ' IdentityProvider: diff --git a/internal/api/ui/login/static/i18n/cs.yaml b/internal/api/ui/login/static/i18n/cs.yaml index d9f359bc1d..df53baccce 100644 --- a/internal/api/ui/login/static/i18n/cs.yaml +++ b/internal/api/ui/login/static/i18n/cs.yaml @@ -486,6 +486,7 @@ Errors: NoExternalUserData: Nebyla přijata žádná externí uživatelská data CreationNotAllowed: Vytvoření nového uživatele není na tomto poskytovateli povoleno LinkingNotAllowed: Propojení uživatele není na tomto poskytovateli povoleno + NoOptionAllowed: Ani vytvoření, ani propojení není povoleno pro tohoto poskytovatele. Obraťte se na svého správce. GrantRequired: Přihlášení není možné. Uživatel musí mít alespoň jeden oprávnění na aplikaci. Prosím, kontaktujte svého správce. ProjectRequired: Přihlášení není možné. Organizace uživatele musí být přidělena k projektu. Prosím, kontaktujte svého správce. IdentityProvider: diff --git a/internal/api/ui/login/static/i18n/de.yaml b/internal/api/ui/login/static/i18n/de.yaml index 25002177a9..75dbf3884a 100644 --- a/internal/api/ui/login/static/i18n/de.yaml +++ b/internal/api/ui/login/static/i18n/de.yaml @@ -485,6 +485,7 @@ Errors: NoExternalUserData: Keine externen User-Daten erhalten CreationNotAllowed: Erstellen eines neuen Benutzers mit diesem Provider ist nicht erlaubt LinkingNotAllowed: Verknüpfen eines Benutzers mit diesem Provider ist nicht erlaubt + NoOptionAllowed: Weder Erstellung noch Verknüpfung ist für diesen Provider erlaubt. Bitte wenden Sie sich an Ihren Administrator. GrantRequired: Die Anmeldung an diese Applikation ist nicht möglich. Der Benutzer benötigt mindestens eine Berechtigung an der Applikation. Bitte wende dich an deinen Administrator. ProjectRequired: Die Anmeldung an dieser Applikation ist nicht möglich. Die Organisation des Benutzer benötigt Berechtigung auf das Projekt. Bitte wende dich an deinen Administrator. IdentityProvider: diff --git a/internal/api/ui/login/static/i18n/en.yaml b/internal/api/ui/login/static/i18n/en.yaml index 45a3340bb6..3a41d4c1c4 100644 --- a/internal/api/ui/login/static/i18n/en.yaml +++ b/internal/api/ui/login/static/i18n/en.yaml @@ -484,8 +484,9 @@ Errors: ExternalUserIDEmpty: External User ID is empty UserDisplayNameEmpty: User Display Name is empty NoExternalUserData: No external User Data received - CreationNotAllowed: Creation of a new user is not allowed on this Provider - LinkingNotAllowed: Linking of a user is not allowed on this Provider + CreationNotAllowed: Creation of a new user is not allowed on this provider + LinkingNotAllowed: Linking of a user is not allowed on this provider + NoOptionAllowed: Neither creation of linking is allowed on this provider. Please contact your administrator. GrantRequired: Login not possible. The user is required to have at least one grant on the application. Please contact your administrator. ProjectRequired: Login not possible. The organization of the user must be granted to the project. Please contact your administrator. IdentityProvider: diff --git a/internal/api/ui/login/static/i18n/es.yaml b/internal/api/ui/login/static/i18n/es.yaml index 5d1f444f73..f53c7aedfc 100644 --- a/internal/api/ui/login/static/i18n/es.yaml +++ b/internal/api/ui/login/static/i18n/es.yaml @@ -469,6 +469,7 @@ Errors: NoExternalUserData: No se recibieron datos del usuario externo CreationNotAllowed: La creación de un nuevo usuario no está permitida para este proveedor LinkingNotAllowed: La vinculación de un usuario no está permitida para este proveedor + NoOptionAllowed: Ni la creación ni la vinculación están permitidas en este proveedor. Póngase en contacto con su administrador. GrantRequired: El inicio de sesión no es posible. Se requiere que el usuario tenga al menos una concesión sobre la aplicación. Por favor contacta con tu administrador. ProjectRequired: El inicio de sesión no es posible. La organización del usuario debe tener el acceso concedido para el proyecto. Por favor contacta con tu administrador. IdentityProvider: diff --git a/internal/api/ui/login/static/i18n/fr.yaml b/internal/api/ui/login/static/i18n/fr.yaml index 6288f32cf8..57ce69f171 100644 --- a/internal/api/ui/login/static/i18n/fr.yaml +++ b/internal/api/ui/login/static/i18n/fr.yaml @@ -487,6 +487,7 @@ Errors: NoExternalUserData: Aucune donnée d'utilisateur externe reçue CreationNotAllowed: La création d'un nouvel utilisateur n'est pas autorisée sur ce fournisseur. LinkingNotAllowed: La création d'un lien vers un utilisateur n'est pas autorisée pour ce fournisseur. + NoOptionAllowed: Ni la création ni la liaison sont autorisées pour ce fournisseur. Veuillez contacter votre administrateur. GrantRequired: Connexion impossible. L'utilisateur doit avoir au moins une subvention sur l'application. Veuillez contacter votre administrateur. ProjectRequired: Connexion impossible. L'organisation de l'utilisateur doit être accordée au projet. Veuillez contacter votre administrateur. IdentityProvider: diff --git a/internal/api/ui/login/static/i18n/it.yaml b/internal/api/ui/login/static/i18n/it.yaml index 2547b2a559..16a7e5d88e 100644 --- a/internal/api/ui/login/static/i18n/it.yaml +++ b/internal/api/ui/login/static/i18n/it.yaml @@ -487,6 +487,7 @@ Errors: NoExternalUserData: Nessun dato utente esterno ricevuto CreationNotAllowed: La creazione di un nuovo utente non è consentita su questo provider. LinkingNotAllowed: Il collegamento di un utente non è consentito su questo provider. + NoOptionAllowed: Né la creazione né il collegamento sono consentiti per questo provider. Contattare l'amministratore. GrantRequired: Accesso non possibile. L'utente deve avere almeno una sovvenzione sull'applicazione. Contatta il tuo amministratore. ProjectRequired: Accesso non possibile. L'organizzazione dell'utente deve essere concessa al progetto. Contatta il tuo amministratore. IdentityProvider: diff --git a/internal/api/ui/login/static/i18n/ja.yaml b/internal/api/ui/login/static/i18n/ja.yaml index 2fb5bea727..fb64f89846 100644 --- a/internal/api/ui/login/static/i18n/ja.yaml +++ b/internal/api/ui/login/static/i18n/ja.yaml @@ -450,6 +450,7 @@ Errors: NoExternalUserData: 外部ユーザー情報を取得できません CreationNotAllowed: このプロバイダーでは、新しいユーザーの作成は許可されていません LinkingNotAllowed: このプロバイダーでは、ユーザーのリンクが許可されていません + NoOptionAllowed: このプロバイダーでは作成もリンクも許可されていません。 管理者にお問い合わせください。 GrantRequired: ログインできません。このユーザーは、アプリケーションに少なくとも1つの権限を付与されていることが必要です。管理者にお問い合わせください。 ProjectRequired: ログインできません。ユーザーの組織がプロジェクトに権限を付与されている必要があります。管理者にお問い合わせください。 IdentityProvider: diff --git a/internal/api/ui/login/static/i18n/mk.yaml b/internal/api/ui/login/static/i18n/mk.yaml index cb98565127..b8dc7552c4 100644 --- a/internal/api/ui/login/static/i18n/mk.yaml +++ b/internal/api/ui/login/static/i18n/mk.yaml @@ -487,6 +487,7 @@ Errors: NoExternalUserData: Нема преземени податоци за надворешен корисник CreationNotAllowed: Креирањето на нов корисник не е дозволено на овој провајдер LinkingNotAllowed: Поврзувањето на корисник не е дозволено на овој провајдер + NoOptionAllowed: NНиту создавање, ниту поврзување е дозволено за овој провајдер. Ве молиме контактирајте го вашиот администратор. GrantRequired: Не е можно најавување. Корисникот мора да има барем едно овластување за апликацијата. Ве молиме контактирајте го вашиот администратор. ProjectRequired: Не е можно најавување. Организацијата на корисникот мора да биде доделена на проектот. Ве молиме контактирајте го вашиот администратор. IdentityProvider: diff --git a/internal/api/ui/login/static/i18n/nl.yaml b/internal/api/ui/login/static/i18n/nl.yaml index 7dac59687d..71cfa2f8ef 100644 --- a/internal/api/ui/login/static/i18n/nl.yaml +++ b/internal/api/ui/login/static/i18n/nl.yaml @@ -486,6 +486,7 @@ Errors: NoExternalUserData: Geen externe Gebruiker Data ontvangen CreationNotAllowed: Creatie van een nieuwe gebruiker is niet toegestaan op deze Provider LinkingNotAllowed: Koppeling van een gebruiker is niet toegestaan op deze Provider + NoOptionAllowed: Noch aanmaak noch koppeling is toegestaan voor deze provider. Neem contact op met uw beheerder. GrantRequired: Inloggen niet mogelijk. De gebruiker moet minimaal één grant hebben op de applicatie. Neem contact op met uw beheerder. ProjectRequired: Inloggen niet mogelijk. De organisatie van de gebruiker moet toegekend zijn aan het project. Neem contact op met uw beheerder. IdentityProvider: diff --git a/internal/api/ui/login/static/i18n/pl.yaml b/internal/api/ui/login/static/i18n/pl.yaml index 5fd51bc49f..3ab78afc44 100644 --- a/internal/api/ui/login/static/i18n/pl.yaml +++ b/internal/api/ui/login/static/i18n/pl.yaml @@ -487,6 +487,7 @@ Errors: NoExternalUserData: Nie otrzymano danych użytkownika zewnętrznego CreationNotAllowed: Tworzenie nowego użytkownika nie jest dozwolone w tym Providencie LinkingNotAllowed: Linkowanie użytkownika nie jest dozwolone na tym Providencie + NoOptionAllowed: Ani tworzenie, ani łączenie nie jest dozwolone dla tego dostawcy. Skontaktuj się z administratorem. GrantRequired: Logowanie nie jest możliwe. Użytkownik musi posiadać przynajmniej jedno uprawnienie w aplikacji. Skontaktuj się z administratorem. ProjectRequired: Logowanie nie jest możliwe. Organizacja użytkownika musi zostać udzielona projektowi. Skontaktuj się z administratorem. IdentityProvider: diff --git a/internal/api/ui/login/static/i18n/pt.yaml b/internal/api/ui/login/static/i18n/pt.yaml index 9155e82061..fc1ce68220 100644 --- a/internal/api/ui/login/static/i18n/pt.yaml +++ b/internal/api/ui/login/static/i18n/pt.yaml @@ -483,6 +483,7 @@ Errors: NoExternalUserData: Nenhum dado de usuário externo recebido CreationNotAllowed: A criação de um novo usuário não é permitida neste provedor LinkingNotAllowed: A vinculação de um usuário não é permitida neste provedor + NoOptionAllowed: Nem criação nem vinculação são permitidas neste fornecedor. Contate o seu administrador. GrantRequired: Login não é possível. O usuário precisa ter pelo menos uma permissão no aplicativo. Entre em contato com o administrador. ProjectRequired: Login não é possível. A organização do usuário precisa ser concedida ao projeto. Entre em contato com o administrador. IdentityProvider: diff --git a/internal/api/ui/login/static/i18n/ru.yaml b/internal/api/ui/login/static/i18n/ru.yaml index 0473171b15..6909171385 100644 --- a/internal/api/ui/login/static/i18n/ru.yaml +++ b/internal/api/ui/login/static/i18n/ru.yaml @@ -486,6 +486,7 @@ Errors: NoExternalUserData: Данные внешнего пользователя не получены CreationNotAllowed: Создание нового пользователя для данного провайдера не разрешено LinkingNotAllowed: Привязка пользователя с данным провайдером запрещена + NoOptionAllowed: Ни создание, ни связывание не разрешены для этого провайдера. Пожалуйста, обратитесь к администратору. GrantRequired: Вход невозможен. Пользователь должен иметь хотя бы один допуск в приложении. Пожалуйста, свяжитесь с вашим администратором. ProjectRequired: Вход невозможен. Организация пользователя должна иметь допуск к проекту. Пожалуйста, свяжитесь с вашим администратором. IdentityProvider: diff --git a/internal/api/ui/login/static/i18n/sv.yaml b/internal/api/ui/login/static/i18n/sv.yaml index f2798c8ef3..da9e888f8d 100644 --- a/internal/api/ui/login/static/i18n/sv.yaml +++ b/internal/api/ui/login/static/i18n/sv.yaml @@ -486,6 +486,7 @@ Errors: NoExternalUserData: Det kom ingen användarinformation från det externa kontot CreationNotAllowed: Det är inte tillåtet att skapa nya konton från den här externa leverantören LinkingNotAllowed: Det är inte tillåtet att koppla ihop konton från den här externa leverantören + NoOptionAllowed: Varken skapande eller länkande är tillåtet för denna leverantör. Kontakta administratören. GrantRequired: Det går inte att logga in just nu. Användarkontot har inte tillgång till någonting i tjänsten. Ta kontakt med systemansvarig. ProjectRequired: Det går inte att logga in just nu. Användarkontots organisation har inte tillgång till tjänsten. Ta kontakt med systemansvarig. IdentityProvider: diff --git a/internal/api/ui/login/static/i18n/zh.yaml b/internal/api/ui/login/static/i18n/zh.yaml index 072b45e575..fcec01002e 100644 --- a/internal/api/ui/login/static/i18n/zh.yaml +++ b/internal/api/ui/login/static/i18n/zh.yaml @@ -486,6 +486,7 @@ Errors: NoExternalUserData: 未收到外部用户数据 CreationNotAllowed: 不允许在该供应商上创建新用户 LinkingNotAllowed: 在此提供者上不允许链接一个用户 + NoOptionAllowed: 此提供商不允许创建或链接。请联系您的管理员。 GrantRequired: 无法登录,用户需要在应用程序上拥有至少一项授权,请联系您的管理员。 ProjectRequired: 无法登录,用户的组织必须授予项目,请联系您的管理员。 IdentityProvider: diff --git a/internal/api/ui/login/static/templates/link_users_done.html b/internal/api/ui/login/static/templates/link_users_done.html index beb9073598..a3d2d5a5ab 100644 --- a/internal/api/ui/login/static/templates/link_users_done.html +++ b/internal/api/ui/login/static/templates/link_users_done.html @@ -4,7 +4,9 @@

{{t "LinkingUsersDone.Title"}}

{{ template "user-profile" . }} + {{if not .ErrID}}

{{t "LinkingUsersDone.Description"}}

+ {{end}}
@@ -12,6 +14,8 @@ + {{template "error-message" .}} +
{{t "LinkingUsersDone.CancelButtonText"}} diff --git a/internal/command/user_human_test.go b/internal/command/user_human_test.go index daf037c951..ffde2480c1 100644 --- a/internal/command/user_human_test.go +++ b/internal/command/user_human_test.go @@ -1272,7 +1272,7 @@ func TestCommandSide_AddHuman(t *testing.T) { func TestCommandSide_ImportHuman(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(*testing.T) *eventstore.Eventstore idGenerator id.Generator userPasswordHasher *crypto.Hasher } @@ -1299,9 +1299,7 @@ func TestCommandSide_ImportHuman(t *testing.T) { name: "orgid missing, invalid argument error", given: func(t *testing.T) (fields, args) { return fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), }, args{ ctx: context.Background(), @@ -1327,8 +1325,7 @@ func TestCommandSide_ImportHuman(t *testing.T) { name: "org policy not found, precondition error", given: func(t *testing.T) (fields, args) { return fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), expectFilter(), ), @@ -1357,8 +1354,7 @@ func TestCommandSide_ImportHuman(t *testing.T) { name: "password policy not found, precondition error", given: func(t *testing.T) (fields, args) { return fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( org.NewDomainPolicyAddedEvent(context.Background(), @@ -1397,8 +1393,7 @@ func TestCommandSide_ImportHuman(t *testing.T) { name: "user invalid, invalid argument error", given: func(t *testing.T) (fields, args) { return fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( org.NewDomainPolicyAddedEvent(context.Background(), @@ -1442,8 +1437,7 @@ func TestCommandSide_ImportHuman(t *testing.T) { name: "add human (with password and initial code), ok", given: func(t *testing.T) (fields, args) { return fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( org.NewDomainPolicyAddedEvent(context.Background(), @@ -1529,8 +1523,7 @@ func TestCommandSide_ImportHuman(t *testing.T) { name: "add human email verified password change not required, ok", given: func(t *testing.T) (fields, args) { return fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( org.NewDomainPolicyAddedEvent(context.Background(), @@ -1610,8 +1603,7 @@ func TestCommandSide_ImportHuman(t *testing.T) { name: "add human email verified passwordless only, ok", given: func(t *testing.T) (fields, args) { return fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( org.NewDomainPolicyAddedEvent(context.Background(), @@ -1710,8 +1702,7 @@ func TestCommandSide_ImportHuman(t *testing.T) { name: "add human email verified passwordless and password change not required, ok", given: func(t *testing.T) (fields, args) { return fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( org.NewDomainPolicyAddedEvent(context.Background(), @@ -1814,8 +1805,7 @@ func TestCommandSide_ImportHuman(t *testing.T) { name: "add human (with phone), ok", given: func(t *testing.T) (fields, args) { return fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( org.NewDomainPolicyAddedEvent(context.Background(), @@ -1916,8 +1906,7 @@ func TestCommandSide_ImportHuman(t *testing.T) { name: "add human (with verified phone), ok", given: func(t *testing.T) (fields, args) { return fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( org.NewDomainPolicyAddedEvent(context.Background(), @@ -2012,8 +2001,7 @@ func TestCommandSide_ImportHuman(t *testing.T) { name: "add human (with undefined preferred language), ok", given: func(t *testing.T) (fields, args) { return fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( org.NewDomainPolicyAddedEvent(context.Background(), @@ -2098,8 +2086,7 @@ func TestCommandSide_ImportHuman(t *testing.T) { name: "add human (with unsupported preferred language), ok", given: func(t *testing.T) (fields, args) { return fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( org.NewDomainPolicyAddedEvent(context.Background(), @@ -2185,8 +2172,7 @@ func TestCommandSide_ImportHuman(t *testing.T) { name: "add human (with idp), ok", given: func(t *testing.T) (fields, args) { return fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( org.NewDomainPolicyAddedEvent(context.Background(), @@ -2328,11 +2314,153 @@ func TestCommandSide_ImportHuman(t *testing.T) { }, }, { - name: "add human (with idp, creation not allowed), precondition error", + name: "add human (with idp, no creation allowed), precondition error", given: func(t *testing.T) (fields, args) { return fields{ - eventstore: eventstoreExpect( - t, + 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), + IsAutoCreation: 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(false)}), + }, + ) + return e + }(), + ), + eventFromEventPusher( + org.NewIdentityProviderAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "idpID", + domain.IdentityProviderTypeOrg, + ), + ), + ), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), + userPasswordHasher: mockPasswordHasher("x"), + }, + args{ + ctx: context.Background(), + orgID: "org1", + human: &domain.Human{ + Username: "username", + Profile: &domain.Profile{ + FirstName: "firstname", + LastName: "lastname", + }, + Email: &domain.Email{ + EmailAddress: "email@test.ch", + IsEmailVerified: true, + }, + }, + links: []*domain.UserIDPLink{ + { + IDPConfigID: "idpID", + ExternalUserID: "externalID", + DisplayName: "name", + }, + }, + secretGenerator: GetMockSecretGenerator(t), + } + }, + res: res{ + err: zerrors.IsPreconditionFailed, + }, + }, + { + name: "add human (with idp, manual creation not allowed), ok", + given: func(t *testing.T) (fields, args) { + return fields{ + eventstore: expectEventstore( expectFilter( eventFromEventPusher( org.NewDomainPolicyAddedEvent(context.Background(), @@ -2422,7 +2550,10 @@ func TestCommandSide_ImportHuman(t *testing.T) { &org.NewAggregate("org1").Aggregate, "config1", []idp.OIDCIDPChanges{ - idp.ChangeOIDCOptions(idp.OptionChanges{IsCreationAllowed: gu.Ptr(false)}), + idp.ChangeOIDCOptions(idp.OptionChanges{ + IsCreationAllowed: gu.Ptr(false), + IsAutoCreation: gu.Ptr(true), + }), }, ) return e @@ -2436,6 +2567,17 @@ func TestCommandSide_ImportHuman(t *testing.T) { ), ), ), + 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), + ), ), idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), userPasswordHasher: mockPasswordHasher("x"), @@ -2446,8 +2588,9 @@ func TestCommandSide_ImportHuman(t *testing.T) { human: &domain.Human{ Username: "username", Profile: &domain.Profile{ - FirstName: "firstname", - LastName: "lastname", + FirstName: "firstname", + LastName: "lastname", + PreferredLanguage: AllowedLanguage, }, Email: &domain.Email{ EmailAddress: "email@test.ch", @@ -2465,7 +2608,196 @@ func TestCommandSide_ImportHuman(t *testing.T) { } }, res: res{ - err: zerrors.IsPreconditionFailed, + 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.UserStateActive, + }, + }, + }, + { + name: "add human (with idp, auto creation not allowed), 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), + ), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), + userPasswordHasher: mockPasswordHasher("x"), + }, + args{ + ctx: context.Background(), + orgID: "org1", + 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.UserStateActive, + }, }, }, } @@ -2473,7 +2805,7 @@ func TestCommandSide_ImportHuman(t *testing.T) { t.Run(tt.name, func(t *testing.T) { f, a := tt.given(t) r := &Commands{ - eventstore: f.eventstore, + eventstore: f.eventstore(t), idGenerator: f.idGenerator, userPasswordHasher: f.userPasswordHasher, } @@ -2494,7 +2826,7 @@ func TestCommandSide_ImportHuman(t *testing.T) { func TestCommandSide_HumanMFASkip(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(*testing.T) *eventstore.Eventstore } type ( args struct { @@ -2516,9 +2848,7 @@ func TestCommandSide_HumanMFASkip(t *testing.T) { { name: "userid missing, invalid argument error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), }, args: args{ ctx: context.Background(), @@ -2532,8 +2862,7 @@ func TestCommandSide_HumanMFASkip(t *testing.T) { { name: "user not existing, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), }, @@ -2549,8 +2878,7 @@ func TestCommandSide_HumanMFASkip(t *testing.T) { { name: "skip mfa init, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), @@ -2589,7 +2917,7 @@ func TestCommandSide_HumanMFASkip(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), } err := r.HumanSkipMFAInit(tt.args.ctx, tt.args.userID, tt.args.orgID) if tt.res.err == nil { @@ -2604,7 +2932,7 @@ func TestCommandSide_HumanMFASkip(t *testing.T) { func TestCommandSide_HumanSignOut(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(*testing.T) *eventstore.Eventstore } type ( args struct { @@ -2626,9 +2954,7 @@ func TestCommandSide_HumanSignOut(t *testing.T) { { name: "agentid missing, invalid argument error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), }, args: args{ ctx: context.Background(), @@ -2642,9 +2968,7 @@ func TestCommandSide_HumanSignOut(t *testing.T) { { name: "userids missing, invalid argument error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), }, args: args{ ctx: context.Background(), @@ -2658,8 +2982,7 @@ func TestCommandSide_HumanSignOut(t *testing.T) { { name: "user not existing, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), }, @@ -2673,8 +2996,7 @@ func TestCommandSide_HumanSignOut(t *testing.T) { { name: "human sign out, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), @@ -2713,8 +3035,7 @@ func TestCommandSide_HumanSignOut(t *testing.T) { { name: "human sign out multiple users, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), @@ -2774,7 +3095,7 @@ func TestCommandSide_HumanSignOut(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), } err := r.HumansSignOut(tt.args.ctx, tt.args.agentID, tt.args.userIDs) if tt.res.err == nil { diff --git a/internal/command/user_idp_link.go b/internal/command/user_idp_link.go index 761cebb7d2..432d2e0b90 100644 --- a/internal/command/user_idp_link.go +++ b/internal/command/user_idp_link.go @@ -86,12 +86,13 @@ func (c *Commands) addUserIDPLink(ctx context.Context, human *eventstore.Aggrega if err != nil { return nil, zerrors.ThrowPreconditionFailed(err, "COMMAND-39nfs", "Errors.IDPConfig.NotExisting") } + options := idpWriteModel.GetProviderOptions() // IDP user will either be linked or created on a new user // Therefore we need to either check if linking is allowed or creation: - if linkToExistingUser && !idpWriteModel.GetProviderOptions().IsLinkingAllowed { + if linkToExistingUser && !options.IsLinkingAllowed && options.AutoLinkingOption == domain.AutoLinkingOptionUnspecified { return nil, zerrors.ThrowPreconditionFailed(err, "COMMAND-Sfee2", "Errors.ExternalIDP.LinkingNotAllowed") } - if !linkToExistingUser && !idpWriteModel.GetProviderOptions().IsCreationAllowed { + if !linkToExistingUser && !options.IsCreationAllowed && !options.IsAutoCreation { return nil, zerrors.ThrowPreconditionFailed(err, "COMMAND-SJI3g", "Errors.ExternalIDP.CreationNotAllowed") } return user.NewUserIDPLinkAddedEvent(ctx, human, link.IDPConfigID, link.DisplayName, link.ExternalUserID), nil diff --git a/internal/command/user_idp_link_test.go b/internal/command/user_idp_link_test.go index 62b30f5fce..2ff75fadf4 100644 --- a/internal/command/user_idp_link_test.go +++ b/internal/command/user_idp_link_test.go @@ -20,7 +20,7 @@ import ( func TestCommandSide_BulkAddUserIDPLinks(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(*testing.T) *eventstore.Eventstore } type args struct { ctx context.Context @@ -40,9 +40,7 @@ func TestCommandSide_BulkAddUserIDPLinks(t *testing.T) { { name: "missing userid, invalid argument error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), }, args: args{ ctx: context.Background(), @@ -62,9 +60,7 @@ func TestCommandSide_BulkAddUserIDPLinks(t *testing.T) { { name: "no external idps, invalid argument error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), }, args: args{ ctx: context.Background(), @@ -78,8 +74,7 @@ func TestCommandSide_BulkAddUserIDPLinks(t *testing.T) { { name: "userID doesnt match aggregate id, invalid argument error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanAddedEvent( @@ -120,8 +115,7 @@ func TestCommandSide_BulkAddUserIDPLinks(t *testing.T) { { name: "invalid external idp, invalid argument error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanAddedEvent( @@ -162,8 +156,7 @@ func TestCommandSide_BulkAddUserIDPLinks(t *testing.T) { { name: "config not existing, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanAddedEvent( @@ -203,10 +196,9 @@ func TestCommandSide_BulkAddUserIDPLinks(t *testing.T) { }, }, { - name: "linking not allowed, precondition error", + name: "no linking not allowed, precondition failed", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanAddedEvent( @@ -307,11 +299,236 @@ func TestCommandSide_BulkAddUserIDPLinks(t *testing.T) { err: zerrors.ThrowPreconditionFailed(nil, "COMMAND-Sfee2", "Errors.ExternalIDP.LinkingNotAllowed"), }, }, + { + name: "auto linking not allowed (manual linking allowed), ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent( + context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "userName", + "firstName", + "lastName", + "nickName", + "displayName", + language.German, + domain.GenderFemale, + "email@Address.ch", + false, + ), + ), + ), + expectFilter( + eventFromEventPusher( + org.NewIDPConfigAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "config1", + "name", + domain.IDPConfigTypeOIDC, + domain.IDPConfigStylingTypeUnspecified, + true, + ), + ), + eventFromEventPusher( + org.NewIDPOIDCConfigAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "clientID", + "config1", + "issuer", + "authEndpoint", + "tokenEndpoint", + nil, + domain.OIDCMappingFieldUnspecified, + domain.OIDCMappingFieldUnspecified, + ), + ), + ), + expectFilter( + eventFromEventPusher( + org.NewIDPConfigAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "config1", + "name", + domain.IDPConfigTypeOIDC, + domain.IDPConfigStylingTypeUnspecified, + true, + ), + ), + eventFromEventPusher( + org.NewIDPOIDCConfigAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "clientID", + "config1", + "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{ + IsLinkingAllowed: gu.Ptr(true), + AutoLinkingOption: gu.Ptr(domain.AutoLinkingOptionUnspecified), + }), + }, + ) + return e + }(), + ), + ), + expectPush( + user.NewUserIDPLinkAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "config1", + "name", + "externaluser1", + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + resourceOwner: "org1", + links: []*domain.UserIDPLink{ + { + ObjectRoot: models.ObjectRoot{ + AggregateID: "user1", + }, + IDPConfigID: "config1", + DisplayName: "name", + ExternalUserID: "externaluser1", + }, + }, + }, + res: res{}, + }, + { + name: "manual linking not allowed (auto linking allowed), ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent( + context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "userName", + "firstName", + "lastName", + "nickName", + "displayName", + language.German, + domain.GenderFemale, + "email@Address.ch", + false, + ), + ), + ), + expectFilter( + eventFromEventPusher( + org.NewIDPConfigAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "config1", + "name", + domain.IDPConfigTypeOIDC, + domain.IDPConfigStylingTypeUnspecified, + true, + ), + ), + eventFromEventPusher( + org.NewIDPOIDCConfigAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "clientID", + "config1", + "issuer", + "authEndpoint", + "tokenEndpoint", + nil, + domain.OIDCMappingFieldUnspecified, + domain.OIDCMappingFieldUnspecified, + ), + ), + ), + expectFilter( + eventFromEventPusher( + org.NewIDPConfigAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "config1", + "name", + domain.IDPConfigTypeOIDC, + domain.IDPConfigStylingTypeUnspecified, + true, + ), + ), + eventFromEventPusher( + org.NewIDPOIDCConfigAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "clientID", + "config1", + "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{ + IsLinkingAllowed: gu.Ptr(false), + AutoLinkingOption: gu.Ptr(domain.AutoLinkingOptionEmail), + }), + }, + ) + return e + }(), + ), + ), + expectPush( + user.NewUserIDPLinkAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "config1", + "name", + "externaluser1", + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + resourceOwner: "org1", + links: []*domain.UserIDPLink{ + { + ObjectRoot: models.ObjectRoot{ + AggregateID: "user1", + }, + IDPConfigID: "config1", + DisplayName: "name", + ExternalUserID: "externaluser1", + }, + }, + }, + res: res{}, + }, { name: "add external idp org config, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanAddedEvent( @@ -409,8 +626,7 @@ func TestCommandSide_BulkAddUserIDPLinks(t *testing.T) { { name: "add external idp iam config, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanAddedEvent( @@ -509,7 +725,7 @@ func TestCommandSide_BulkAddUserIDPLinks(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), } err := r.BulkAddedUserIDPLinks(tt.args.ctx, tt.args.userID, tt.args.resourceOwner, tt.args.links) assert.ErrorIs(t, err, tt.res.err) @@ -519,7 +735,7 @@ func TestCommandSide_BulkAddUserIDPLinks(t *testing.T) { func TestCommandSide_RemoveUserIDPLink(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(*testing.T) *eventstore.Eventstore checkPermission domain.PermissionCheck } type args struct { @@ -539,9 +755,7 @@ func TestCommandSide_RemoveUserIDPLink(t *testing.T) { { name: "invalid idp, invalid argument error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), checkPermission: newMockPermissionCheckAllowed(), }, args: args{ @@ -561,9 +775,7 @@ func TestCommandSide_RemoveUserIDPLink(t *testing.T) { { name: "aggregate id missing, invalid argument error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), checkPermission: newMockPermissionCheckAllowed(), }, args: args{ @@ -580,8 +792,7 @@ func TestCommandSide_RemoveUserIDPLink(t *testing.T) { { name: "user removed, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewUserIDPLinkAddedEvent(context.Background(), @@ -620,8 +831,7 @@ func TestCommandSide_RemoveUserIDPLink(t *testing.T) { { name: "external idp not existing, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), checkPermission: newMockPermissionCheckAllowed(), @@ -643,8 +853,7 @@ func TestCommandSide_RemoveUserIDPLink(t *testing.T) { { name: "remove external idp, permission error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewUserIDPLinkAddedEvent(context.Background(), @@ -675,8 +884,7 @@ func TestCommandSide_RemoveUserIDPLink(t *testing.T) { { name: "remove external idp, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewUserIDPLinkAddedEvent(context.Background(), @@ -717,7 +925,7 @@ func TestCommandSide_RemoveUserIDPLink(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.RemoveUserIDPLink(tt.args.ctx, tt.args.link) @@ -736,7 +944,7 @@ func TestCommandSide_RemoveUserIDPLink(t *testing.T) { func TestCommandSide_ExternalLoginCheck(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(*testing.T) *eventstore.Eventstore } type args struct { ctx context.Context @@ -756,9 +964,7 @@ func TestCommandSide_ExternalLoginCheck(t *testing.T) { { name: "userid missing, invalid argument error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), }, args: args{ ctx: context.Background(), @@ -772,8 +978,7 @@ func TestCommandSide_ExternalLoginCheck(t *testing.T) { { name: "user removed, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewUserIDPLinkAddedEvent(context.Background(), @@ -806,8 +1011,7 @@ func TestCommandSide_ExternalLoginCheck(t *testing.T) { { name: "external login check, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), @@ -852,7 +1056,7 @@ func TestCommandSide_ExternalLoginCheck(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), } err := r.UserIDPLoginChecked(tt.args.ctx, tt.args.orgID, tt.args.userID, tt.args.authRequest) if tt.res.err == nil { diff --git a/proto/zitadel/idp.proto b/proto/zitadel/idp.proto index 2f434f50cc..cbb2be9db0 100644 --- a/proto/zitadel/idp.proto +++ b/proto/zitadel/idp.proto @@ -500,12 +500,12 @@ message AzureADConfig { message Options { bool is_linking_allowed = 1 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - description: "Enable if users should be able to link an existing ZITADEL user with an external account."; + description: "Enable if users should be able to manually link an existing ZITADEL user with an external account. Disable if users should only be allowed to link the proposed account in case of active auto_linking."; } ]; bool is_creation_allowed = 2 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - description: "Enable if users should be able to create a new account in ZITADEL when using an external account."; + description: "Enable if users should be able to manually create a new account in ZITADEL when using an external account. Disable if users should not be able to edit account information when auto_creation is enabled."; } ]; bool is_auto_creation = 3 [ From 64a3bb3149ee740e164122b17fc4985980a5c2a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Wed, 14 Aug 2024 17:18:14 +0300 Subject: [PATCH 32/39] feat(v3alpha): web key resource (#8262) # Which Problems Are Solved Implement a new API service that allows management of OIDC signing web keys. This allows users to manage rotation of the instance level keys. which are currently managed based on expiry. The API accepts the generation of the following key types and parameters: - RSA keys with 2048, 3072 or 4096 bit in size and: - Signing with SHA-256 (RS256) - Signing with SHA-384 (RS384) - Signing with SHA-512 (RS512) - ECDSA keys with - P256 curve - P384 curve - P512 curve - ED25519 keys # How the Problems Are Solved Keys are serialized for storage using the JSON web key format from the `jose` library. This is the format that will be used by OIDC for signing, verification and publication. Each instance can have a number of key pairs. All existing public keys are meant to be used for token verification and publication the keys endpoint. Keys can be activated and the active private key is meant to sign new tokens. There is always exactly 1 active signing key: 1. When the first key for an instance is generated, it is automatically activated. 2. Activation of the next key automatically deactivates the previously active key. 3. Keys cannot be manually deactivated from the API 4. Active keys cannot be deleted # Additional Changes - Query methods that later will be used by the OIDC package are already implemented. Preparation for #8031 - Fix indentation in french translation for instance event - Move user_schema translations to consistent positions in all translation files # Additional Context - Closes #8030 - Part of #7809 --------- Co-authored-by: Elio Bischof --- cmd/defaults.yaml | 17 + cmd/initialise/config.go | 6 +- cmd/mirror/config.go | 1 + cmd/ready/config.go | 1 + cmd/setup/config.go | 2 + cmd/start/config.go | 1 + cmd/start/start.go | 4 + docs/docusaurus.config.js | 8 + docs/sidebars.js | 14 + internal/api/grpc/feature/v2/converter.go | 2 + .../api/grpc/feature/v2/converter_test.go | 10 + internal/api/grpc/feature/v2beta/converter.go | 2 + .../api/grpc/feature/v2beta/converter_test.go | 10 + .../v3alpha/execution_integration_test.go | 20 +- .../execution_target_integration_test.go | 2 +- .../action/v3alpha/query_integration_test.go | 6 +- .../action/v3alpha/target_integration_test.go | 8 +- .../api/grpc/resources/webkey/v3/server.go | 47 ++ .../api/grpc/resources/webkey/v3/webkey.go | 87 ++ .../resources/webkey/v3/webkey_converter.go | 173 ++++ .../webkey/v3/webkey_converter_test.go | 529 ++++++++++++ .../webkey/v3/webkey_integration_test.go | 245 ++++++ internal/api/oidc/key.go | 3 +- internal/api/oidc/key_test.go | 12 +- internal/api/saml/certificate.go | 19 +- internal/api/saml/storage.go | 6 +- internal/command/command.go | 3 + internal/command/instance.go | 46 +- internal/command/instance_features.go | 25 +- internal/command/instance_features_model.go | 5 + internal/command/key_pair.go | 9 +- internal/command/key_pair_model.go | 3 +- internal/command/web_key.go | 188 +++++ internal/command/web_key_model.go | 131 +++ internal/command/web_key_test.go | 754 ++++++++++++++++++ internal/crypto/crypto.go | 19 + internal/crypto/ellipticcurve_enumer.go | 116 +++ internal/crypto/rsabits_enumer.go | 136 ++++ internal/crypto/rsahasher_enumer.go | 116 +++ internal/crypto/web_key.go | 238 ++++++ internal/crypto/web_key_test.go | 269 +++++++ internal/crypto/webkeyconfigtype_enumer.go | 116 +++ internal/domain/key_pair.go | 25 +- internal/domain/web_key.go | 11 + internal/eventstore/aggregate.go | 16 +- .../repository/mock/repository.mock.impl.go | 2 +- internal/feature/feature.go | 2 + internal/feature/key_enumer.go | 12 +- internal/integration/client.go | 13 +- internal/query/certificate.go | 3 +- internal/query/certificate_test.go | 3 +- internal/query/instance_features.go | 1 + internal/query/instance_features_model.go | 3 + internal/query/key.go | 11 +- internal/query/key_test.go | 13 +- .../query/projection/instance_features.go | 4 + internal/query/projection/key_test.go | 13 +- internal/query/projection/projection.go | 3 + internal/query/projection/web_key.go | 165 ++++ internal/query/web_key.go | 154 ++++ internal/query/web_key_by_state.sql | 5 + internal/query/web_key_list.sql | 4 + internal/query/web_key_model.go | 74 ++ internal/query/web_key_public_keys.sql | 3 + internal/query/web_key_test.go | 382 +++++++++ .../feature/feature_v2/eventstore.go | 1 + .../repository/feature/feature_v2/feature.go | 1 + internal/repository/keypair/key_pair.go | 5 +- internal/repository/webkey/aggregate.go | 25 + internal/repository/webkey/eventstore.go | 12 + internal/repository/webkey/webkey.go | 160 ++++ internal/static/i18n/bg.yaml | 14 + internal/static/i18n/cs.yaml | 13 + internal/static/i18n/de.yaml | 13 + internal/static/i18n/en.yaml | 13 + internal/static/i18n/es.yaml | 13 + internal/static/i18n/fr.yaml | 268 ++++--- internal/static/i18n/it.yaml | 25 +- internal/static/i18n/ja.yaml | 13 + internal/static/i18n/mk.yaml | 13 + internal/static/i18n/nl.yaml | 13 + internal/static/i18n/pl.yaml | 13 + internal/static/i18n/pt.yaml | 13 + internal/static/i18n/ru.yaml | 14 + internal/static/i18n/sv.yaml | 13 + internal/static/i18n/zh.yaml | 25 +- proto/zitadel/feature/v2/instance.proto | 14 + proto/zitadel/feature/v2beta/instance.proto | 14 + .../resources/webkey/v3alpha/config.proto | 41 + .../resources/webkey/v3alpha/key.proto | 31 + .../webkey/v3alpha/webkey_service.proto | 278 +++++++ 91 files changed, 5133 insertions(+), 256 deletions(-) create mode 100644 internal/api/grpc/resources/webkey/v3/server.go create mode 100644 internal/api/grpc/resources/webkey/v3/webkey.go create mode 100644 internal/api/grpc/resources/webkey/v3/webkey_converter.go create mode 100644 internal/api/grpc/resources/webkey/v3/webkey_converter_test.go create mode 100644 internal/api/grpc/resources/webkey/v3/webkey_integration_test.go create mode 100644 internal/command/web_key.go create mode 100644 internal/command/web_key_model.go create mode 100644 internal/command/web_key_test.go create mode 100644 internal/crypto/ellipticcurve_enumer.go create mode 100644 internal/crypto/rsabits_enumer.go create mode 100644 internal/crypto/rsahasher_enumer.go create mode 100644 internal/crypto/web_key.go create mode 100644 internal/crypto/web_key_test.go create mode 100644 internal/crypto/webkeyconfigtype_enumer.go create mode 100644 internal/domain/web_key.go create mode 100644 internal/query/projection/web_key.go create mode 100644 internal/query/web_key.go create mode 100644 internal/query/web_key_by_state.sql create mode 100644 internal/query/web_key_list.sql create mode 100644 internal/query/web_key_model.go create mode 100644 internal/query/web_key_public_keys.sql create mode 100644 internal/query/web_key_test.go create mode 100644 internal/repository/webkey/aggregate.go create mode 100644 internal/repository/webkey/eventstore.go create mode 100644 internal/repository/webkey/webkey.go create mode 100644 proto/zitadel/resources/webkey/v3alpha/config.proto create mode 100644 proto/zitadel/resources/webkey/v3alpha/key.proto create mode 100644 proto/zitadel/resources/webkey/v3alpha/webkey_service.proto diff --git a/cmd/defaults.yaml b/cmd/defaults.yaml index 6afbaddbd7..45d598ee86 100644 --- a/cmd/defaults.yaml +++ b/cmd/defaults.yaml @@ -757,6 +757,19 @@ DefaultInstance: MaxOTPAttempts: 0 # ZITADEL_DEFAULTINSTANCE_LOCKOUTPOLICY_MAXOTPATTEMPTS ShouldShowLockoutFailure: true # ZITADEL_DEFAULTINSTANCE_LOCKOUTPOLICY_SHOULDSHOWLOCKOUTFAILURE 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: "ecdsa" + # Config: + # Curve: "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 # Overwrites the system defaults @@ -1002,6 +1015,9 @@ InternalAuthZ: - "iam.feature.delete" - "iam.restrictions.read" - "iam.restrictions.write" + - "iam.web_key.write" + - "iam.web_key.delete" + - "iam.web_key.read" - "org.read" - "org.global.read" - "org.create" @@ -1078,6 +1094,7 @@ InternalAuthZ: - "iam.flow.read" - "iam.restrictions.read" - "iam.feature.read" + - "iam.web_key.read" - "org.read" - "org.member.read" - "org.idp.read" diff --git a/cmd/initialise/config.go b/cmd/initialise/config.go index b3499ea7ad..3fe7173860 100644 --- a/cmd/initialise/config.go +++ b/cmd/initialise/config.go @@ -1,6 +1,7 @@ package initialise import ( + "github.com/mitchellh/mapstructure" "github.com/spf13/viper" "github.com/zitadel/logging" @@ -17,7 +18,10 @@ type Config struct { func MustNewConfig(v *viper.Viper) *Config { config := new(Config) err := v.Unmarshal(config, - viper.DecodeHook(database.DecodeHook), + viper.DecodeHook(mapstructure.ComposeDecodeHookFunc( + database.DecodeHook, + mapstructure.TextUnmarshallerHookFunc(), + )), ) logging.OnError(err).Fatal("unable to read config") diff --git a/cmd/mirror/config.go b/cmd/mirror/config.go index 5d2ec8fac7..cc98000869 100644 --- a/cmd/mirror/config.go +++ b/cmd/mirror/config.go @@ -74,6 +74,7 @@ func mustNewConfig(v *viper.Viper, config any) { database.DecodeHook, actions.HTTPConfigDecodeHook, hook.EnumHookFunc(internal_authz.MemberTypeString), + mapstructure.TextUnmarshallerHookFunc(), )), ) logging.OnError(err).Fatal("unable to read default config") diff --git a/cmd/ready/config.go b/cmd/ready/config.go index aaa7e2d7ee..f5067c562e 100644 --- a/cmd/ready/config.go +++ b/cmd/ready/config.go @@ -27,6 +27,7 @@ func MustNewConfig(v *viper.Viper) *Config { mapstructure.StringToTimeHookFunc(time.RFC3339), mapstructure.StringToSliceHookFunc(","), hook.EnumHookFunc(internal_authz.MemberTypeString), + mapstructure.TextUnmarshallerHookFunc(), )), ) logging.OnError(err).Fatal("unable to read default config") diff --git a/cmd/setup/config.go b/cmd/setup/config.go index 2bee4642aa..6ac1767ca6 100644 --- a/cmd/setup/config.go +++ b/cmd/setup/config.go @@ -74,6 +74,7 @@ func MustNewConfig(v *viper.Viper) *Config { mapstructure.StringToTimeDurationHookFunc(), mapstructure.StringToTimeHookFunc(time.RFC3339), mapstructure.StringToSliceHookFunc(","), + mapstructure.TextUnmarshallerHookFunc(), )), ) logging.OnError(err).Fatal("unable to read default config") @@ -139,6 +140,7 @@ func MustNewSteps(v *viper.Viper) *Steps { mapstructure.StringToTimeDurationHookFunc(), mapstructure.StringToTimeHookFunc(time.RFC3339), mapstructure.StringToSliceHookFunc(","), + mapstructure.TextUnmarshallerHookFunc(), )), ) logging.OnError(err).Fatal("unable to read steps") diff --git a/cmd/start/config.go b/cmd/start/config.go index 4ac5da13ab..71175024e6 100644 --- a/cmd/start/config.go +++ b/cmd/start/config.go @@ -100,6 +100,7 @@ func MustNewConfig(v *viper.Viper) *Config { mapstructure.StringToTimeDurationHookFunc(), mapstructure.StringToTimeHookFunc(time.RFC3339), mapstructure.StringToSliceHookFunc(","), + mapstructure.TextUnmarshallerHookFunc(), )), ) logging.OnError(err).Fatal("unable to read config") diff --git a/cmd/start/start.go b/cmd/start/start.go index d542e8be62..f944ef0327 100644 --- a/cmd/start/start.go +++ b/cmd/start/start.go @@ -44,6 +44,7 @@ import ( org_v2 "github.com/zitadel/zitadel/internal/api/grpc/org/v2" org_v2beta "github.com/zitadel/zitadel/internal/api/grpc/org/v2beta" action_v3_alpha "github.com/zitadel/zitadel/internal/api/grpc/resources/action/v3alpha" + "github.com/zitadel/zitadel/internal/api/grpc/resources/webkey/v3" session_v2 "github.com/zitadel/zitadel/internal/api/grpc/session/v2" session_v2beta "github.com/zitadel/zitadel/internal/api/grpc/session/v2beta" settings_v2 "github.com/zitadel/zitadel/internal/api/grpc/settings/v2" @@ -442,6 +443,9 @@ func startAPIs( if err := apis.RegisterService(ctx, user_schema_v3_alpha.CreateServer(commands, queries)); err != nil { return nil, err } + if err := apis.RegisterService(ctx, webkey.CreateServer(commands, queries)); err != nil { + return nil, err + } instanceInterceptor := middleware.InstanceInterceptor(queries, config.ExternalDomain, login.IgnoreInstanceEndpoints...) assetsCache := middleware.AssetsCacheInterceptor(config.AssetStorage.Cache.MaxAge, config.AssetStorage.Cache.SharedMaxAge) apis.RegisterHandlerOnPrefix(assets.HandlerPrefix, assets.NewHandler(commands, verifier, config.InternalAuthZ, id.SonyFlakeGenerator(), store, queries, middleware.CallDurationHandler, instanceInterceptor.Handler, assetsCache.Handler, limitingAccessInterceptor.Handle)) diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js index c067ef25c4..b65a53d20b 100644 --- a/docs/docusaurus.config.js +++ b/docs/docusaurus.config.js @@ -340,6 +340,14 @@ module.exports = { categoryLinkSource: "auto", }, }, + webkey_v3: { + specPath: ".artifacts/openapi/zitadel/resources/webkey/v3alpha/webkey_service.swagger.json", + outputDir: "docs/apis/resources/webkey_service_v3", + sidebarOptions: { + groupPathsBy: "tag", + categoryLinkSource: "auto", + }, + }, feature_v2: { specPath: ".artifacts/openapi/zitadel/feature/v2/feature_service.swagger.json", outputDir: "docs/apis/resources/feature_service_v2", diff --git a/docs/sidebars.js b/docs/sidebars.js index 2c377ea48f..49107a380c 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -732,6 +732,20 @@ module.exports = { }, items: require("./docs/apis/resources/action_service_v3/sidebar.ts"), }, + { + type: "category", + label: "Web key Lifecycle (Preview)", + link: { + type: "generated-index", + title: "Action Service API (Preview)", + slug: "/apis/resources/action_service_v3", + description: + "This API is intended to manage web keys for a ZITADEL instance, used to sign and validate OIDC tokens.\n" + + "\n" + + "This project is in preview state. It can AND will continue breaking until a stable version is released.", + }, + items: require("./docs/apis/resources/webkey_service_v3/sidebar.ts"), + }, ], }, { diff --git a/internal/api/grpc/feature/v2/converter.go b/internal/api/grpc/feature/v2/converter.go index 4d0698feaf..3d35694bdd 100644 --- a/internal/api/grpc/feature/v2/converter.go +++ b/internal/api/grpc/feature/v2/converter.go @@ -42,6 +42,7 @@ func instanceFeaturesToCommand(req *feature_pb.SetInstanceFeaturesRequest) *comm TokenExchange: req.OidcTokenExchange, Actions: req.Actions, ImprovedPerformance: improvedPerformanceListToDomain(req.ImprovedPerformance), + WebKey: req.WebKey, } } @@ -55,6 +56,7 @@ func instanceFeaturesToPb(f *query.InstanceFeatures) *feature_pb.GetInstanceFeat OidcTokenExchange: featureSourceToFlagPb(&f.TokenExchange), Actions: featureSourceToFlagPb(&f.Actions), ImprovedPerformance: featureSourceToImprovedPerformanceFlagPb(&f.ImprovedPerformance), + WebKey: featureSourceToFlagPb(&f.WebKey), } } diff --git a/internal/api/grpc/feature/v2/converter_test.go b/internal/api/grpc/feature/v2/converter_test.go index 7c2cf5fc39..e6335145b0 100644 --- a/internal/api/grpc/feature/v2/converter_test.go +++ b/internal/api/grpc/feature/v2/converter_test.go @@ -123,6 +123,7 @@ func Test_instanceFeaturesToCommand(t *testing.T) { OidcTokenExchange: gu.Ptr(true), Actions: gu.Ptr(true), ImprovedPerformance: nil, + WebKey: gu.Ptr(true), } want := &command.InstanceFeatures{ LoginDefaultOrg: gu.Ptr(true), @@ -132,6 +133,7 @@ func Test_instanceFeaturesToCommand(t *testing.T) { TokenExchange: gu.Ptr(true), Actions: gu.Ptr(true), ImprovedPerformance: nil, + WebKey: gu.Ptr(true), } got := instanceFeaturesToCommand(arg) assert.Equal(t, want, got) @@ -172,6 +174,10 @@ func Test_instanceFeaturesToPb(t *testing.T) { Level: feature.LevelSystem, Value: []feature.ImprovedPerformanceType{feature.ImprovedPerformanceTypeOrgByID}, }, + WebKey: query.FeatureSource[bool]{ + Level: feature.LevelInstance, + Value: true, + }, } want := &feature_pb.GetInstanceFeaturesResponse{ Details: &object.Details{ @@ -207,6 +213,10 @@ func Test_instanceFeaturesToPb(t *testing.T) { ExecutionPaths: []feature_pb.ImprovedPerformance{feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_ORG_BY_ID}, Source: feature_pb.Source_SOURCE_SYSTEM, }, + WebKey: &feature_pb.FeatureFlag{ + Enabled: true, + Source: feature_pb.Source_SOURCE_INSTANCE, + }, } got := instanceFeaturesToPb(arg) assert.Equal(t, want, got) diff --git a/internal/api/grpc/feature/v2beta/converter.go b/internal/api/grpc/feature/v2beta/converter.go index c866cc017d..16654d1e6b 100644 --- a/internal/api/grpc/feature/v2beta/converter.go +++ b/internal/api/grpc/feature/v2beta/converter.go @@ -42,6 +42,7 @@ func instanceFeaturesToCommand(req *feature_pb.SetInstanceFeaturesRequest) *comm TokenExchange: req.OidcTokenExchange, Actions: req.Actions, ImprovedPerformance: improvedPerformanceListToDomain(req.ImprovedPerformance), + WebKey: req.WebKey, } } @@ -55,6 +56,7 @@ func instanceFeaturesToPb(f *query.InstanceFeatures) *feature_pb.GetInstanceFeat OidcTokenExchange: featureSourceToFlagPb(&f.TokenExchange), Actions: featureSourceToFlagPb(&f.Actions), ImprovedPerformance: featureSourceToImprovedPerformanceFlagPb(&f.ImprovedPerformance), + WebKey: featureSourceToFlagPb(&f.WebKey), } } diff --git a/internal/api/grpc/feature/v2beta/converter_test.go b/internal/api/grpc/feature/v2beta/converter_test.go index 35dbf98014..b8a69f86a8 100644 --- a/internal/api/grpc/feature/v2beta/converter_test.go +++ b/internal/api/grpc/feature/v2beta/converter_test.go @@ -123,6 +123,7 @@ func Test_instanceFeaturesToCommand(t *testing.T) { OidcTokenExchange: gu.Ptr(true), Actions: gu.Ptr(true), ImprovedPerformance: nil, + WebKey: gu.Ptr(true), } want := &command.InstanceFeatures{ LoginDefaultOrg: gu.Ptr(true), @@ -132,6 +133,7 @@ func Test_instanceFeaturesToCommand(t *testing.T) { TokenExchange: gu.Ptr(true), Actions: gu.Ptr(true), ImprovedPerformance: nil, + WebKey: gu.Ptr(true), } got := instanceFeaturesToCommand(arg) assert.Equal(t, want, got) @@ -172,6 +174,10 @@ func Test_instanceFeaturesToPb(t *testing.T) { Level: feature.LevelSystem, Value: []feature.ImprovedPerformanceType{feature.ImprovedPerformanceTypeOrgByID}, }, + WebKey: query.FeatureSource[bool]{ + Level: feature.LevelInstance, + Value: true, + }, } want := &feature_pb.GetInstanceFeaturesResponse{ Details: &object.Details{ @@ -207,6 +213,10 @@ func Test_instanceFeaturesToPb(t *testing.T) { ExecutionPaths: []feature_pb.ImprovedPerformance{feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_ORG_BY_ID}, Source: feature_pb.Source_SOURCE_SYSTEM, }, + WebKey: &feature_pb.FeatureFlag{ + Enabled: true, + Source: feature_pb.Source_SOURCE_INSTANCE, + }, } got := instanceFeaturesToPb(arg) assert.Equal(t, want, got) diff --git a/internal/api/grpc/resources/action/v3alpha/execution_integration_test.go b/internal/api/grpc/resources/action/v3alpha/execution_integration_test.go index 3056d450c6..326e5e62be 100644 --- a/internal/api/grpc/resources/action/v3alpha/execution_integration_test.go +++ b/internal/api/grpc/resources/action/v3alpha/execution_integration_test.go @@ -188,8 +188,8 @@ func TestServer_SetExecution_Request(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // We want to have the same response no matter how often we call the function - Tester.Client.ActionV3.SetExecution(tt.ctx, tt.req) - got, err := Tester.Client.ActionV3.SetExecution(tt.ctx, tt.req) + Tester.Client.ActionV3Alpha.SetExecution(tt.ctx, tt.req) + got, err := Tester.Client.ActionV3Alpha.SetExecution(tt.ctx, tt.req) if tt.wantErr { require.Error(t, err) return @@ -326,8 +326,8 @@ func TestServer_SetExecution_Request_Include(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // We want to have the same response no matter how often we call the function - Tester.Client.ActionV3.SetExecution(tt.ctx, tt.req) - got, err := Tester.Client.ActionV3.SetExecution(tt.ctx, tt.req) + Tester.Client.ActionV3Alpha.SetExecution(tt.ctx, tt.req) + got, err := Tester.Client.ActionV3Alpha.SetExecution(tt.ctx, tt.req) if tt.wantErr { require.Error(t, err) return @@ -506,8 +506,8 @@ func TestServer_SetExecution_Response(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // We want to have the same response no matter how often we call the function - Tester.Client.ActionV3.SetExecution(tt.ctx, tt.req) - got, err := Tester.Client.ActionV3.SetExecution(tt.ctx, tt.req) + Tester.Client.ActionV3Alpha.SetExecution(tt.ctx, tt.req) + got, err := Tester.Client.ActionV3Alpha.SetExecution(tt.ctx, tt.req) if tt.wantErr { require.Error(t, err) return @@ -692,8 +692,8 @@ func TestServer_SetExecution_Event(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // We want to have the same response no matter how often we call the function - Tester.Client.ActionV3.SetExecution(tt.ctx, tt.req) - got, err := Tester.Client.ActionV3.SetExecution(tt.ctx, tt.req) + Tester.Client.ActionV3Alpha.SetExecution(tt.ctx, tt.req) + got, err := Tester.Client.ActionV3Alpha.SetExecution(tt.ctx, tt.req) if tt.wantErr { require.Error(t, err) return @@ -791,8 +791,8 @@ func TestServer_SetExecution_Function(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // We want to have the same response no matter how often we call the function - Tester.Client.ActionV3.SetExecution(tt.ctx, tt.req) - got, err := Tester.Client.ActionV3.SetExecution(tt.ctx, tt.req) + Tester.Client.ActionV3Alpha.SetExecution(tt.ctx, tt.req) + got, err := Tester.Client.ActionV3Alpha.SetExecution(tt.ctx, tt.req) if tt.wantErr { require.Error(t, err) return diff --git a/internal/api/grpc/resources/action/v3alpha/execution_target_integration_test.go b/internal/api/grpc/resources/action/v3alpha/execution_target_integration_test.go index 0fe042eb08..80cf83138a 100644 --- a/internal/api/grpc/resources/action/v3alpha/execution_target_integration_test.go +++ b/internal/api/grpc/resources/action/v3alpha/execution_target_integration_test.go @@ -248,7 +248,7 @@ func TestServer_ExecutionTarget(t *testing.T) { defer close() } - got, err := Tester.Client.ActionV3.GetTarget(tt.ctx, tt.req) + got, err := Tester.Client.ActionV3Alpha.GetTarget(tt.ctx, tt.req) if tt.wantErr { require.Error(t, err) return diff --git a/internal/api/grpc/resources/action/v3alpha/query_integration_test.go b/internal/api/grpc/resources/action/v3alpha/query_integration_test.go index b4f7578286..2542e8672b 100644 --- a/internal/api/grpc/resources/action/v3alpha/query_integration_test.go +++ b/internal/api/grpc/resources/action/v3alpha/query_integration_test.go @@ -214,7 +214,7 @@ func TestServer_GetTarget(t *testing.T) { err := tt.args.dep(tt.args.ctx, tt.args.req, tt.want) require.NoError(t, err) } - got, getErr := Tester.Client.ActionV3.GetTarget(tt.args.ctx, tt.args.req) + got, getErr := Tester.Client.ActionV3Alpha.GetTarget(tt.args.ctx, tt.args.req) if tt.wantErr { assert.Error(t, getErr, "Error: "+getErr.Error()) } else { @@ -476,7 +476,7 @@ func TestServer_ListTargets(t *testing.T) { } require.EventuallyWithT(t, func(ttt *assert.CollectT) { - got, listErr := Tester.Client.ActionV3.SearchTargets(tt.args.ctx, tt.args.req) + got, listErr := Tester.Client.ActionV3Alpha.SearchTargets(tt.args.ctx, tt.args.req) if tt.wantErr { assert.Error(ttt, listErr, "Error: "+listErr.Error()) } else { @@ -864,7 +864,7 @@ func TestServer_SearchExecutions(t *testing.T) { } require.EventuallyWithT(t, func(ttt *assert.CollectT) { - got, listErr := Tester.Client.ActionV3.SearchExecutions(tt.args.ctx, tt.args.req) + got, listErr := Tester.Client.ActionV3Alpha.SearchExecutions(tt.args.ctx, tt.args.req) if tt.wantErr { assert.Error(t, listErr, "Error: "+listErr.Error()) } else { diff --git a/internal/api/grpc/resources/action/v3alpha/target_integration_test.go b/internal/api/grpc/resources/action/v3alpha/target_integration_test.go index c94c080674..849ed7649b 100644 --- a/internal/api/grpc/resources/action/v3alpha/target_integration_test.go +++ b/internal/api/grpc/resources/action/v3alpha/target_integration_test.go @@ -197,7 +197,7 @@ func TestServer_CreateTarget(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := Tester.Client.ActionV3.CreateTarget(tt.ctx, &action.CreateTargetRequest{Target: tt.req}) + got, err := Tester.Client.ActionV3Alpha.CreateTarget(tt.ctx, &action.CreateTargetRequest{Target: tt.req}) if tt.wantErr { require.Error(t, err) return @@ -382,8 +382,8 @@ func TestServer_PatchTarget(t *testing.T) { err := tt.prepare(tt.args.req) require.NoError(t, err) // We want to have the same response no matter how often we call the function - Tester.Client.ActionV3.PatchTarget(tt.args.ctx, tt.args.req) - got, err := Tester.Client.ActionV3.PatchTarget(tt.args.ctx, tt.args.req) + Tester.Client.ActionV3Alpha.PatchTarget(tt.args.ctx, tt.args.req) + got, err := Tester.Client.ActionV3Alpha.PatchTarget(tt.args.ctx, tt.args.req) if tt.wantErr { require.Error(t, err) return @@ -438,7 +438,7 @@ func TestServer_DeleteTarget(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := Tester.Client.ActionV3.DeleteTarget(tt.ctx, tt.req) + got, err := Tester.Client.ActionV3Alpha.DeleteTarget(tt.ctx, tt.req) if tt.wantErr { require.Error(t, err) return diff --git a/internal/api/grpc/resources/webkey/v3/server.go b/internal/api/grpc/resources/webkey/v3/server.go new file mode 100644 index 0000000000..4e97965932 --- /dev/null +++ b/internal/api/grpc/resources/webkey/v3/server.go @@ -0,0 +1,47 @@ +package webkey + +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/query" + webkey "github.com/zitadel/zitadel/pkg/grpc/resources/webkey/v3alpha" +) + +type Server struct { + webkey.UnimplementedZITADELWebKeysServer + command *command.Commands + query *query.Queries +} + +func CreateServer( + command *command.Commands, + query *query.Queries, +) *Server { + return &Server{ + command: command, + query: query, + } +} + +func (s *Server) RegisterServer(grpcServer *grpc.Server) { + webkey.RegisterZITADELWebKeysServer(grpcServer, s) +} + +func (s *Server) AppName() string { + return webkey.ZITADELWebKeys_ServiceDesc.ServiceName +} + +func (s *Server) MethodPrefix() string { + return webkey.ZITADELWebKeys_ServiceDesc.ServiceName +} + +func (s *Server) AuthMethods() authz.MethodMapping { + return webkey.ZITADELWebKeys_AuthMethods +} + +func (s *Server) RegisterGateway() server.RegisterGatewayFunc { + return webkey.RegisterZITADELWebKeysHandler +} diff --git a/internal/api/grpc/resources/webkey/v3/webkey.go b/internal/api/grpc/resources/webkey/v3/webkey.go new file mode 100644 index 0000000000..8a6e72f950 --- /dev/null +++ b/internal/api/grpc/resources/webkey/v3/webkey.go @@ -0,0 +1,87 @@ +package webkey + +import ( + "context" + + "github.com/zitadel/zitadel/internal/api/authz" + resource_object "github.com/zitadel/zitadel/internal/api/grpc/resources/object/v3alpha" + "github.com/zitadel/zitadel/internal/telemetry/tracing" + "github.com/zitadel/zitadel/internal/zerrors" + object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha" + webkey "github.com/zitadel/zitadel/pkg/grpc/resources/webkey/v3alpha" +) + +func (s *Server) CreateWebKey(ctx context.Context, req *webkey.CreateWebKeyRequest) (_ *webkey.CreateWebKeyResponse, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + if err = checkWebKeyFeature(ctx); err != nil { + return nil, err + } + webKey, err := s.command.CreateWebKey(ctx, createWebKeyRequestToConfig(req)) + if err != nil { + return nil, err + } + + return &webkey.CreateWebKeyResponse{ + Details: resource_object.DomainToDetailsPb(webKey.ObjectDetails, object.OwnerType_OWNER_TYPE_INSTANCE, authz.GetInstance(ctx).InstanceID()), + }, nil +} + +func (s *Server) ActivateWebKey(ctx context.Context, req *webkey.ActivateWebKeyRequest) (_ *webkey.ActivateWebKeyResponse, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + if err = checkWebKeyFeature(ctx); err != nil { + return nil, err + } + details, err := s.command.ActivateWebKey(ctx, req.GetId()) + if err != nil { + return nil, err + } + + return &webkey.ActivateWebKeyResponse{ + Details: resource_object.DomainToDetailsPb(details, object.OwnerType_OWNER_TYPE_INSTANCE, authz.GetInstance(ctx).InstanceID()), + }, nil +} + +func (s *Server) DeleteWebKey(ctx context.Context, req *webkey.DeleteWebKeyRequest) (_ *webkey.DeleteWebKeyResponse, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + if err = checkWebKeyFeature(ctx); err != nil { + return nil, err + } + details, err := s.command.DeleteWebKey(ctx, req.GetId()) + if err != nil { + return nil, err + } + + return &webkey.DeleteWebKeyResponse{ + Details: resource_object.DomainToDetailsPb(details, object.OwnerType_OWNER_TYPE_INSTANCE, authz.GetInstance(ctx).InstanceID()), + }, nil +} + +func (s *Server) ListWebKeys(ctx context.Context, _ *webkey.ListWebKeysRequest) (_ *webkey.ListWebKeysResponse, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + if err = checkWebKeyFeature(ctx); err != nil { + return nil, err + } + list, err := s.query.ListWebKeys(ctx) + if err != nil { + return nil, err + } + + return &webkey.ListWebKeysResponse{ + WebKeys: webKeyDetailsListToPb(list, authz.GetInstance(ctx).InstanceID()), + }, nil +} + +func checkWebKeyFeature(ctx context.Context) error { + if !authz.GetFeatures(ctx).WebKey { + return zerrors.ThrowPreconditionFailed(nil, "WEBKEY-Ohx6E", "Errors.WebKey.FeatureDisabled") + } + return nil +} diff --git a/internal/api/grpc/resources/webkey/v3/webkey_converter.go b/internal/api/grpc/resources/webkey/v3/webkey_converter.go new file mode 100644 index 0000000000..b460775dd5 --- /dev/null +++ b/internal/api/grpc/resources/webkey/v3/webkey_converter.go @@ -0,0 +1,173 @@ +package webkey + +import ( + resource_object "github.com/zitadel/zitadel/internal/api/grpc/resources/object/v3alpha" + "github.com/zitadel/zitadel/internal/crypto" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/query" + object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha" + webkey "github.com/zitadel/zitadel/pkg/grpc/resources/webkey/v3alpha" +) + +func createWebKeyRequestToConfig(req *webkey.CreateWebKeyRequest) crypto.WebKeyConfig { + switch config := req.GetKey().GetConfig().(type) { + case *webkey.WebKey_Rsa: + return webKeyRSAConfigToCrypto(config.Rsa) + case *webkey.WebKey_Ecdsa: + return webKeyECDSAConfigToCrypto(config.Ecdsa) + case *webkey.WebKey_Ed25519: + return new(crypto.WebKeyED25519Config) + default: + return webKeyRSAConfigToCrypto(nil) + } +} + +func webKeyRSAConfigToCrypto(config *webkey.WebKeyRSAConfig) *crypto.WebKeyRSAConfig { + out := new(crypto.WebKeyRSAConfig) + + switch config.GetBits() { + case webkey.WebKeyRSAConfig_RSA_BITS_UNSPECIFIED: + out.Bits = crypto.RSABits2048 + case webkey.WebKeyRSAConfig_RSA_BITS_2048: + out.Bits = crypto.RSABits2048 + case webkey.WebKeyRSAConfig_RSA_BITS_3072: + out.Bits = crypto.RSABits3072 + case webkey.WebKeyRSAConfig_RSA_BITS_4096: + out.Bits = crypto.RSABits4096 + default: + out.Bits = crypto.RSABits2048 + } + + switch config.GetHasher() { + case webkey.WebKeyRSAConfig_RSA_HASHER_UNSPECIFIED: + out.Hasher = crypto.RSAHasherSHA256 + case webkey.WebKeyRSAConfig_RSA_HASHER_SHA256: + out.Hasher = crypto.RSAHasherSHA256 + case webkey.WebKeyRSAConfig_RSA_HASHER_SHA384: + out.Hasher = crypto.RSAHasherSHA384 + case webkey.WebKeyRSAConfig_RSA_HASHER_SHA512: + out.Hasher = crypto.RSAHasherSHA512 + default: + out.Hasher = crypto.RSAHasherSHA256 + } + + return out +} + +func webKeyECDSAConfigToCrypto(config *webkey.WebKeyECDSAConfig) *crypto.WebKeyECDSAConfig { + out := new(crypto.WebKeyECDSAConfig) + + switch config.GetCurve() { + case webkey.WebKeyECDSAConfig_ECDSA_CURVE_UNSPECIFIED: + out.Curve = crypto.EllipticCurveP256 + case webkey.WebKeyECDSAConfig_ECDSA_CURVE_P256: + out.Curve = crypto.EllipticCurveP256 + case webkey.WebKeyECDSAConfig_ECDSA_CURVE_P384: + out.Curve = crypto.EllipticCurveP384 + case webkey.WebKeyECDSAConfig_ECDSA_CURVE_P512: + out.Curve = crypto.EllipticCurveP512 + default: + out.Curve = crypto.EllipticCurveP256 + } + + return out +} + +func webKeyDetailsListToPb(list []query.WebKeyDetails, instanceID string) []*webkey.GetWebKey { + out := make([]*webkey.GetWebKey, len(list)) + for i := range list { + out[i] = webKeyDetailsToPb(&list[i], instanceID) + } + return out +} + +func webKeyDetailsToPb(details *query.WebKeyDetails, instanceID string) *webkey.GetWebKey { + out := &webkey.GetWebKey{ + Details: resource_object.DomainToDetailsPb(&domain.ObjectDetails{ + ID: details.KeyID, + CreationDate: details.CreationDate, + EventDate: details.ChangeDate, + }, object.OwnerType_OWNER_TYPE_INSTANCE, instanceID), + State: webKeyStateToPb(details.State), + Config: &webkey.WebKey{}, + } + + switch config := details.Config.(type) { + case *crypto.WebKeyRSAConfig: + out.Config.Config = &webkey.WebKey_Rsa{ + Rsa: webKeyRSAConfigToPb(config), + } + case *crypto.WebKeyECDSAConfig: + out.Config.Config = &webkey.WebKey_Ecdsa{ + Ecdsa: webKeyECDSAConfigToPb(config), + } + case *crypto.WebKeyED25519Config: + out.Config.Config = &webkey.WebKey_Ed25519{ + Ed25519: new(webkey.WebKeyED25519Config), + } + } + + return out +} + +func webKeyStateToPb(state domain.WebKeyState) webkey.WebKeyState { + switch state { + case domain.WebKeyStateUnspecified: + return webkey.WebKeyState_STATE_UNSPECIFIED + case domain.WebKeyStateInitial: + return webkey.WebKeyState_STATE_INITIAL + case domain.WebKeyStateActive: + return webkey.WebKeyState_STATE_ACTIVE + case domain.WebKeyStateInactive: + return webkey.WebKeyState_STATE_INACTIVE + case domain.WebKeyStateRemoved: + return webkey.WebKeyState_STATE_REMOVED + default: + return webkey.WebKeyState_STATE_UNSPECIFIED + } +} + +func webKeyRSAConfigToPb(config *crypto.WebKeyRSAConfig) *webkey.WebKeyRSAConfig { + out := new(webkey.WebKeyRSAConfig) + + switch config.Bits { + case crypto.RSABitsUnspecified: + out.Bits = webkey.WebKeyRSAConfig_RSA_BITS_UNSPECIFIED + case crypto.RSABits2048: + out.Bits = webkey.WebKeyRSAConfig_RSA_BITS_2048 + case crypto.RSABits3072: + out.Bits = webkey.WebKeyRSAConfig_RSA_BITS_3072 + case crypto.RSABits4096: + out.Bits = webkey.WebKeyRSAConfig_RSA_BITS_4096 + } + + switch config.Hasher { + case crypto.RSAHasherUnspecified: + out.Hasher = webkey.WebKeyRSAConfig_RSA_HASHER_UNSPECIFIED + case crypto.RSAHasherSHA256: + out.Hasher = webkey.WebKeyRSAConfig_RSA_HASHER_SHA256 + case crypto.RSAHasherSHA384: + out.Hasher = webkey.WebKeyRSAConfig_RSA_HASHER_SHA384 + case crypto.RSAHasherSHA512: + out.Hasher = webkey.WebKeyRSAConfig_RSA_HASHER_SHA512 + } + + return out +} + +func webKeyECDSAConfigToPb(config *crypto.WebKeyECDSAConfig) *webkey.WebKeyECDSAConfig { + out := new(webkey.WebKeyECDSAConfig) + + switch config.Curve { + case crypto.EllipticCurveUnspecified: + out.Curve = webkey.WebKeyECDSAConfig_ECDSA_CURVE_UNSPECIFIED + case crypto.EllipticCurveP256: + out.Curve = webkey.WebKeyECDSAConfig_ECDSA_CURVE_P256 + case crypto.EllipticCurveP384: + out.Curve = webkey.WebKeyECDSAConfig_ECDSA_CURVE_P384 + case crypto.EllipticCurveP512: + out.Curve = webkey.WebKeyECDSAConfig_ECDSA_CURVE_P512 + } + + return out +} diff --git a/internal/api/grpc/resources/webkey/v3/webkey_converter_test.go b/internal/api/grpc/resources/webkey/v3/webkey_converter_test.go new file mode 100644 index 0000000000..e755d2be08 --- /dev/null +++ b/internal/api/grpc/resources/webkey/v3/webkey_converter_test.go @@ -0,0 +1,529 @@ +package webkey + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/crypto" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/query" + object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha" + resource_object "github.com/zitadel/zitadel/pkg/grpc/resources/object/v3alpha" + webkey "github.com/zitadel/zitadel/pkg/grpc/resources/webkey/v3alpha" +) + +func Test_createWebKeyRequestToConfig(t *testing.T) { + type args struct { + req *webkey.CreateWebKeyRequest + } + tests := []struct { + name string + args args + want crypto.WebKeyConfig + }{ + { + name: "RSA", + args: args{&webkey.CreateWebKeyRequest{ + Key: &webkey.WebKey{ + Config: &webkey.WebKey_Rsa{ + Rsa: &webkey.WebKeyRSAConfig{ + Bits: webkey.WebKeyRSAConfig_RSA_BITS_3072, + Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA384, + }, + }, + }, + }}, + want: &crypto.WebKeyRSAConfig{ + Bits: crypto.RSABits3072, + Hasher: crypto.RSAHasherSHA384, + }, + }, + { + name: "ECDSA", + args: args{&webkey.CreateWebKeyRequest{ + Key: &webkey.WebKey{ + Config: &webkey.WebKey_Ecdsa{ + Ecdsa: &webkey.WebKeyECDSAConfig{ + Curve: webkey.WebKeyECDSAConfig_ECDSA_CURVE_P384, + }, + }, + }, + }}, + want: &crypto.WebKeyECDSAConfig{ + Curve: crypto.EllipticCurveP384, + }, + }, + { + name: "ED25519", + args: args{&webkey.CreateWebKeyRequest{ + Key: &webkey.WebKey{ + Config: &webkey.WebKey_Ed25519{ + Ed25519: &webkey.WebKeyED25519Config{}, + }, + }, + }}, + want: &crypto.WebKeyED25519Config{}, + }, + { + name: "default", + args: args{&webkey.CreateWebKeyRequest{}}, + want: &crypto.WebKeyRSAConfig{ + Bits: crypto.RSABits2048, + Hasher: crypto.RSAHasherSHA256, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := createWebKeyRequestToConfig(tt.args.req) + assert.Equal(t, tt.want, got) + }) + } +} + +func Test_webKeyRSAConfigToCrypto(t *testing.T) { + type args struct { + config *webkey.WebKeyRSAConfig + } + tests := []struct { + name string + args args + want *crypto.WebKeyRSAConfig + }{ + { + name: "unspecified", + args: args{&webkey.WebKeyRSAConfig{ + Bits: webkey.WebKeyRSAConfig_RSA_BITS_UNSPECIFIED, + Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_UNSPECIFIED, + }}, + want: &crypto.WebKeyRSAConfig{ + Bits: crypto.RSABits2048, + Hasher: crypto.RSAHasherSHA256, + }, + }, + { + name: "2048, RSA256", + args: args{&webkey.WebKeyRSAConfig{ + Bits: webkey.WebKeyRSAConfig_RSA_BITS_2048, + Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA256, + }}, + want: &crypto.WebKeyRSAConfig{ + Bits: crypto.RSABits2048, + Hasher: crypto.RSAHasherSHA256, + }, + }, + { + name: "3072, RSA384", + args: args{&webkey.WebKeyRSAConfig{ + Bits: webkey.WebKeyRSAConfig_RSA_BITS_3072, + Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA384, + }}, + want: &crypto.WebKeyRSAConfig{ + Bits: crypto.RSABits3072, + Hasher: crypto.RSAHasherSHA384, + }, + }, + { + name: "4096, RSA512", + args: args{&webkey.WebKeyRSAConfig{ + Bits: webkey.WebKeyRSAConfig_RSA_BITS_4096, + Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA512, + }}, + want: &crypto.WebKeyRSAConfig{ + Bits: crypto.RSABits4096, + Hasher: crypto.RSAHasherSHA512, + }, + }, + { + name: "invalid", + args: args{&webkey.WebKeyRSAConfig{ + Bits: 99, + Hasher: 99, + }}, + want: &crypto.WebKeyRSAConfig{ + Bits: crypto.RSABits2048, + Hasher: crypto.RSAHasherSHA256, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := webKeyRSAConfigToCrypto(tt.args.config) + assert.Equal(t, tt.want, got) + }) + } +} + +func Test_webKeyECDSAConfigToCrypto(t *testing.T) { + type args struct { + config *webkey.WebKeyECDSAConfig + } + tests := []struct { + name string + args args + want *crypto.WebKeyECDSAConfig + }{ + { + name: "unspecified", + args: args{&webkey.WebKeyECDSAConfig{ + Curve: webkey.WebKeyECDSAConfig_ECDSA_CURVE_UNSPECIFIED, + }}, + want: &crypto.WebKeyECDSAConfig{ + Curve: crypto.EllipticCurveP256, + }, + }, + { + name: "P256", + args: args{&webkey.WebKeyECDSAConfig{ + Curve: webkey.WebKeyECDSAConfig_ECDSA_CURVE_P256, + }}, + want: &crypto.WebKeyECDSAConfig{ + Curve: crypto.EllipticCurveP256, + }, + }, + { + name: "P384", + args: args{&webkey.WebKeyECDSAConfig{ + Curve: webkey.WebKeyECDSAConfig_ECDSA_CURVE_P384, + }}, + want: &crypto.WebKeyECDSAConfig{ + Curve: crypto.EllipticCurveP384, + }, + }, + { + name: "P512", + args: args{&webkey.WebKeyECDSAConfig{ + Curve: webkey.WebKeyECDSAConfig_ECDSA_CURVE_P512, + }}, + want: &crypto.WebKeyECDSAConfig{ + Curve: crypto.EllipticCurveP512, + }, + }, + { + name: "invalid", + args: args{&webkey.WebKeyECDSAConfig{ + Curve: 99, + }}, + want: &crypto.WebKeyECDSAConfig{ + Curve: crypto.EllipticCurveP256, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := webKeyECDSAConfigToCrypto(tt.args.config) + assert.Equal(t, tt.want, got) + }) + } +} + +func Test_webKeyDetailsListToPb(t *testing.T) { + instanceID := "ownerid" + list := []query.WebKeyDetails{ + { + KeyID: "key1", + CreationDate: time.Unix(123, 456), + ChangeDate: time.Unix(789, 0), + Sequence: 123, + State: domain.WebKeyStateActive, + Config: &crypto.WebKeyRSAConfig{ + Bits: crypto.RSABits3072, + Hasher: crypto.RSAHasherSHA384, + }, + }, + { + KeyID: "key2", + CreationDate: time.Unix(123, 456), + ChangeDate: time.Unix(789, 0), + Sequence: 123, + State: domain.WebKeyStateActive, + Config: &crypto.WebKeyED25519Config{}, + }, + } + want := []*webkey.GetWebKey{ + { + Details: &resource_object.Details{ + Id: "key1", + Created: ×tamppb.Timestamp{Seconds: 123, Nanos: 456}, + Changed: ×tamppb.Timestamp{Seconds: 789, Nanos: 0}, + Owner: &object.Owner{Type: object.OwnerType_OWNER_TYPE_INSTANCE, Id: instanceID}, + }, + State: webkey.WebKeyState_STATE_ACTIVE, + Config: &webkey.WebKey{ + Config: &webkey.WebKey_Rsa{ + Rsa: &webkey.WebKeyRSAConfig{ + Bits: webkey.WebKeyRSAConfig_RSA_BITS_3072, + Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA384, + }, + }, + }, + }, + { + Details: &resource_object.Details{ + Id: "key2", + Created: ×tamppb.Timestamp{Seconds: 123, Nanos: 456}, + Changed: ×tamppb.Timestamp{Seconds: 789, Nanos: 0}, + Owner: &object.Owner{Type: object.OwnerType_OWNER_TYPE_INSTANCE, Id: instanceID}, + }, + State: webkey.WebKeyState_STATE_ACTIVE, + Config: &webkey.WebKey{ + Config: &webkey.WebKey_Ed25519{ + Ed25519: &webkey.WebKeyED25519Config{}, + }, + }, + }, + } + got := webKeyDetailsListToPb(list, instanceID) + assert.Equal(t, want, got) +} + +func Test_webKeyDetailsToPb(t *testing.T) { + instanceID := "ownerid" + type args struct { + details *query.WebKeyDetails + } + tests := []struct { + name string + args args + want *webkey.GetWebKey + }{ + { + name: "RSA", + args: args{&query.WebKeyDetails{ + KeyID: "keyID", + CreationDate: time.Unix(123, 456), + ChangeDate: time.Unix(789, 0), + Sequence: 123, + State: domain.WebKeyStateActive, + Config: &crypto.WebKeyRSAConfig{ + Bits: crypto.RSABits3072, + Hasher: crypto.RSAHasherSHA384, + }, + }}, + want: &webkey.GetWebKey{ + Details: &resource_object.Details{ + Id: "keyID", + Created: ×tamppb.Timestamp{Seconds: 123, Nanos: 456}, + Changed: ×tamppb.Timestamp{Seconds: 789, Nanos: 0}, + Owner: &object.Owner{Type: object.OwnerType_OWNER_TYPE_INSTANCE, Id: instanceID}, + }, + State: webkey.WebKeyState_STATE_ACTIVE, + Config: &webkey.WebKey{ + Config: &webkey.WebKey_Rsa{ + Rsa: &webkey.WebKeyRSAConfig{ + Bits: webkey.WebKeyRSAConfig_RSA_BITS_3072, + Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA384, + }, + }, + }, + }, + }, + { + name: "ECDSA", + args: args{&query.WebKeyDetails{ + KeyID: "keyID", + CreationDate: time.Unix(123, 456), + ChangeDate: time.Unix(789, 0), + Sequence: 123, + State: domain.WebKeyStateActive, + Config: &crypto.WebKeyECDSAConfig{ + Curve: crypto.EllipticCurveP384, + }, + }}, + want: &webkey.GetWebKey{ + Details: &resource_object.Details{ + Id: "keyID", + Created: ×tamppb.Timestamp{Seconds: 123, Nanos: 456}, + Changed: ×tamppb.Timestamp{Seconds: 789, Nanos: 0}, + Owner: &object.Owner{Type: object.OwnerType_OWNER_TYPE_INSTANCE, Id: instanceID}, + }, + State: webkey.WebKeyState_STATE_ACTIVE, + Config: &webkey.WebKey{ + Config: &webkey.WebKey_Ecdsa{ + Ecdsa: &webkey.WebKeyECDSAConfig{ + Curve: webkey.WebKeyECDSAConfig_ECDSA_CURVE_P384, + }, + }, + }, + }, + }, + { + name: "ED25519", + args: args{&query.WebKeyDetails{ + KeyID: "keyID", + CreationDate: time.Unix(123, 456), + ChangeDate: time.Unix(789, 0), + Sequence: 123, + State: domain.WebKeyStateActive, + Config: &crypto.WebKeyED25519Config{}, + }}, + want: &webkey.GetWebKey{ + Details: &resource_object.Details{ + Id: "keyID", + Created: ×tamppb.Timestamp{Seconds: 123, Nanos: 456}, + Changed: ×tamppb.Timestamp{Seconds: 789, Nanos: 0}, + Owner: &object.Owner{Type: object.OwnerType_OWNER_TYPE_INSTANCE, Id: instanceID}, + }, + State: webkey.WebKeyState_STATE_ACTIVE, + Config: &webkey.WebKey{ + Config: &webkey.WebKey_Ed25519{ + Ed25519: &webkey.WebKeyED25519Config{}, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := webKeyDetailsToPb(tt.args.details, instanceID) + assert.Equal(t, tt.want, got) + }) + } +} + +func Test_webKeyStateToPb(t *testing.T) { + type args struct { + state domain.WebKeyState + } + tests := []struct { + name string + args args + want webkey.WebKeyState + }{ + { + name: "unspecified", + args: args{domain.WebKeyStateUnspecified}, + want: webkey.WebKeyState_STATE_UNSPECIFIED, + }, + { + name: "initial", + args: args{domain.WebKeyStateInitial}, + want: webkey.WebKeyState_STATE_INITIAL, + }, + { + name: "active", + args: args{domain.WebKeyStateActive}, + want: webkey.WebKeyState_STATE_ACTIVE, + }, + { + name: "inactive", + args: args{domain.WebKeyStateInactive}, + want: webkey.WebKeyState_STATE_INACTIVE, + }, + { + name: "removed", + args: args{domain.WebKeyStateRemoved}, + want: webkey.WebKeyState_STATE_REMOVED, + }, + { + name: "invalid", + args: args{99}, + want: webkey.WebKeyState_STATE_UNSPECIFIED, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := webKeyStateToPb(tt.args.state) + assert.Equal(t, tt.want, got) + }) + } +} + +func Test_webKeyRSAConfigToPb(t *testing.T) { + type args struct { + config *crypto.WebKeyRSAConfig + } + tests := []struct { + name string + args args + want *webkey.WebKeyRSAConfig + }{ + { + name: "2048, RSA256", + args: args{&crypto.WebKeyRSAConfig{ + Bits: crypto.RSABits2048, + Hasher: crypto.RSAHasherSHA256, + }}, + want: &webkey.WebKeyRSAConfig{ + Bits: webkey.WebKeyRSAConfig_RSA_BITS_2048, + Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA256, + }, + }, + { + name: "3072, RSA384", + args: args{&crypto.WebKeyRSAConfig{ + Bits: crypto.RSABits3072, + Hasher: crypto.RSAHasherSHA384, + }}, + want: &webkey.WebKeyRSAConfig{ + Bits: webkey.WebKeyRSAConfig_RSA_BITS_3072, + Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA384, + }, + }, + { + name: "4096, RSA512", + args: args{&crypto.WebKeyRSAConfig{ + Bits: crypto.RSABits4096, + Hasher: crypto.RSAHasherSHA512, + }}, + want: &webkey.WebKeyRSAConfig{ + Bits: webkey.WebKeyRSAConfig_RSA_BITS_4096, + Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA512, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := webKeyRSAConfigToPb(tt.args.config) + assert.Equal(t, tt.want, got) + }) + } +} + +func Test_webKeyECDSAConfigToPb(t *testing.T) { + type args struct { + config *crypto.WebKeyECDSAConfig + } + tests := []struct { + name string + args args + want *webkey.WebKeyECDSAConfig + }{ + { + name: "P256", + args: args{&crypto.WebKeyECDSAConfig{ + Curve: crypto.EllipticCurveP256, + }}, + want: &webkey.WebKeyECDSAConfig{ + Curve: webkey.WebKeyECDSAConfig_ECDSA_CURVE_P256, + }, + }, + { + name: "P384", + args: args{&crypto.WebKeyECDSAConfig{ + Curve: crypto.EllipticCurveP384, + }}, + want: &webkey.WebKeyECDSAConfig{ + Curve: webkey.WebKeyECDSAConfig_ECDSA_CURVE_P384, + }, + }, + { + name: "P512", + args: args{&crypto.WebKeyECDSAConfig{ + Curve: crypto.EllipticCurveP512, + }}, + want: &webkey.WebKeyECDSAConfig{ + Curve: webkey.WebKeyECDSAConfig_ECDSA_CURVE_P512, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := webKeyECDSAConfigToPb(tt.args.config) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/internal/api/grpc/resources/webkey/v3/webkey_integration_test.go b/internal/api/grpc/resources/webkey/v3/webkey_integration_test.go new file mode 100644 index 0000000000..2fae24fb0f --- /dev/null +++ b/internal/api/grpc/resources/webkey/v3/webkey_integration_test.go @@ -0,0 +1,245 @@ +//go:build integration + +package webkey_test + +import ( + "context" + "net" + "os" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/integration" + "github.com/zitadel/zitadel/pkg/grpc/feature/v2" + object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha" + resource_object "github.com/zitadel/zitadel/pkg/grpc/resources/object/v3alpha" + webkey "github.com/zitadel/zitadel/pkg/grpc/resources/webkey/v3alpha" +) + +var ( + CTX context.Context + SystemCTX context.Context + Tester *integration.Tester +) + +func TestMain(m *testing.M) { + os.Exit(func() int { + ctx, _, cancel := integration.Contexts(time.Hour) + defer cancel() + + Tester = integration.NewTester(ctx) + defer Tester.Done() + + SystemCTX = Tester.WithAuthorization(ctx, integration.SystemUser) + CTX = Tester.WithAuthorization(ctx, integration.IAMOwner) + return m.Run() + }()) +} + +func TestServer_Feature_Disabled(t *testing.T) { + client, iamCTX := createInstanceAndClients(t, false) + + t.Run("CreateWebKey", func(t *testing.T) { + _, err := client.CreateWebKey(iamCTX, &webkey.CreateWebKeyRequest{}) + assertFeatureDisabledError(t, err) + }) + t.Run("ActivateWebKey", func(t *testing.T) { + _, err := client.ActivateWebKey(iamCTX, &webkey.ActivateWebKeyRequest{ + Id: "1", + }) + assertFeatureDisabledError(t, err) + }) + t.Run("DeleteWebKey", func(t *testing.T) { + _, err := client.DeleteWebKey(iamCTX, &webkey.DeleteWebKeyRequest{ + Id: "1", + }) + assertFeatureDisabledError(t, err) + }) + t.Run("ListWebKeys", func(t *testing.T) { + _, err := client.ListWebKeys(iamCTX, &webkey.ListWebKeysRequest{}) + assertFeatureDisabledError(t, err) + }) +} + +func TestServer_ListWebKeys(t *testing.T) { + client, iamCtx := createInstanceAndClients(t, true) + // After the feature is first enabled, we can expect 2 generated keys with the default config. + checkWebKeyListState(iamCtx, t, client, 2, "", &webkey.WebKey_Rsa{ + Rsa: &webkey.WebKeyRSAConfig{ + Bits: webkey.WebKeyRSAConfig_RSA_BITS_2048, + Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA256, + }, + }) +} + +func TestServer_CreateWebKey(t *testing.T) { + client, iamCtx := createInstanceAndClients(t, true) + _, err := client.CreateWebKey(iamCtx, &webkey.CreateWebKeyRequest{ + Key: &webkey.WebKey{ + Config: &webkey.WebKey_Rsa{ + Rsa: &webkey.WebKeyRSAConfig{ + Bits: webkey.WebKeyRSAConfig_RSA_BITS_2048, + Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA256, + }, + }, + }, + }) + require.NoError(t, err) + + checkWebKeyListState(iamCtx, t, client, 3, "", &webkey.WebKey_Rsa{ + Rsa: &webkey.WebKeyRSAConfig{ + Bits: webkey.WebKeyRSAConfig_RSA_BITS_2048, + Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA256, + }, + }) +} + +func TestServer_ActivateWebKey(t *testing.T) { + client, iamCtx := createInstanceAndClients(t, true) + resp, err := client.CreateWebKey(iamCtx, &webkey.CreateWebKeyRequest{ + Key: &webkey.WebKey{ + Config: &webkey.WebKey_Rsa{ + Rsa: &webkey.WebKeyRSAConfig{ + Bits: webkey.WebKeyRSAConfig_RSA_BITS_2048, + Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA256, + }, + }, + }, + }) + require.NoError(t, err) + + _, err = client.ActivateWebKey(iamCtx, &webkey.ActivateWebKeyRequest{ + Id: resp.GetDetails().GetId(), + }) + require.NoError(t, err) + + checkWebKeyListState(iamCtx, t, client, 3, resp.GetDetails().GetId(), &webkey.WebKey_Rsa{ + Rsa: &webkey.WebKeyRSAConfig{ + Bits: webkey.WebKeyRSAConfig_RSA_BITS_2048, + Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA256, + }, + }) +} + +func TestServer_DeleteWebKey(t *testing.T) { + client, iamCtx := createInstanceAndClients(t, true) + keyIDs := make([]string, 2) + for i := 0; i < 2; i++ { + resp, err := client.CreateWebKey(iamCtx, &webkey.CreateWebKeyRequest{ + Key: &webkey.WebKey{ + Config: &webkey.WebKey_Rsa{ + Rsa: &webkey.WebKeyRSAConfig{ + Bits: webkey.WebKeyRSAConfig_RSA_BITS_2048, + Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA256, + }, + }, + }, + }) + require.NoError(t, err) + keyIDs[i] = resp.GetDetails().GetId() + } + _, err := client.ActivateWebKey(iamCtx, &webkey.ActivateWebKeyRequest{ + Id: keyIDs[0], + }) + require.NoError(t, err) + + ok := t.Run("cannot delete active key", func(t *testing.T) { + _, err := client.DeleteWebKey(iamCtx, &webkey.DeleteWebKeyRequest{ + Id: keyIDs[0], + }) + require.Error(t, err) + s := status.Convert(err) + assert.Equal(t, codes.FailedPrecondition, s.Code()) + assert.Contains(t, s.Message(), "COMMAND-Chai1") + }) + if !ok { + return + } + + ok = t.Run("delete inactive key", func(t *testing.T) { + _, err := client.DeleteWebKey(iamCtx, &webkey.DeleteWebKeyRequest{ + Id: keyIDs[1], + }) + require.NoError(t, err) + }) + if !ok { + return + } + + // There are 2 keys from feature setup, +2 created, -1 deleted = 3 + checkWebKeyListState(iamCtx, t, client, 3, keyIDs[0], &webkey.WebKey_Rsa{ + Rsa: &webkey.WebKeyRSAConfig{ + Bits: webkey.WebKeyRSAConfig_RSA_BITS_2048, + Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA256, + }, + }) +} + +func createInstanceAndClients(t *testing.T, enableFeature bool) (webkey.ZITADELWebKeysClient, context.Context) { + domain, _, _, iamCTX := Tester.UseIsolatedInstance(t, CTX, SystemCTX) + cc, err := grpc.NewClient( + net.JoinHostPort(domain, "8080"), + grpc.WithTransportCredentials(insecure.NewCredentials()), + ) + require.NoError(t, err) + + if enableFeature { + features := feature.NewFeatureServiceClient(cc) + _, err = features.SetInstanceFeatures(iamCTX, &feature.SetInstanceFeaturesRequest{ + WebKey: proto.Bool(true), + }) + require.NoError(t, err) + time.Sleep(time.Second) + } + + return webkey.NewZITADELWebKeysClient(cc), iamCTX +} + +func assertFeatureDisabledError(t *testing.T, err error) { + t.Helper() + require.Error(t, err) + s := status.Convert(err) + assert.Equal(t, codes.FailedPrecondition, s.Code()) + assert.Contains(t, s.Message(), "WEBKEY-Ohx6E") +} + +func checkWebKeyListState(ctx context.Context, t *testing.T, client webkey.ZITADELWebKeysClient, nKeys int, expectActiveKeyID string, config any) { + resp, err := client.ListWebKeys(ctx, &webkey.ListWebKeysRequest{}) + require.NoError(t, err) + list := resp.GetWebKeys() + require.Len(t, list, nKeys) + + now := time.Now() + var gotActiveKeyID string + for _, key := range list { + integration.AssertResourceDetails(t, &resource_object.Details{ + Created: timestamppb.Now(), + Changed: timestamppb.Now(), + Owner: &object.Owner{ + Type: object.OwnerType_OWNER_TYPE_INSTANCE, + Id: Tester.Instance.InstanceID(), + }, + }, key.GetDetails()) + assert.WithinRange(t, key.GetDetails().GetChanged().AsTime(), now.Add(-time.Minute), now.Add(time.Minute)) + assert.NotEqual(t, webkey.WebKeyState_STATE_UNSPECIFIED, key.GetState()) + assert.NotEqual(t, webkey.WebKeyState_STATE_REMOVED, key.GetState()) + assert.Equal(t, config, key.GetConfig().GetConfig()) + + if key.GetState() == webkey.WebKeyState_STATE_ACTIVE { + gotActiveKeyID = key.GetDetails().GetId() + } + } + assert.NotEmpty(t, gotActiveKeyID) + if expectActiveKeyID != "" { + assert.Equal(t, expectActiveKeyID, gotActiveKeyID) + } +} diff --git a/internal/api/oidc/key.go b/internal/api/oidc/key.go index c4102c1fd2..2db5baf832 100644 --- a/internal/api/oidc/key.go +++ b/internal/api/oidc/key.go @@ -14,7 +14,6 @@ import ( "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" "github.com/zitadel/zitadel/internal/query" "github.com/zitadel/zitadel/internal/repository/instance" @@ -208,7 +207,7 @@ func (k keySetMap) getKey(keyID string) (*jose.JSONWebKey, error) { return &jose.JSONWebKey{ Key: pubKey, KeyID: keyID, - Use: domain.KeyUsageSigning.String(), + Use: crypto.KeyUsageSigning.String(), }, nil } diff --git a/internal/api/oidc/key_test.go b/internal/api/oidc/key_test.go index e7cf39c090..3f84722a9b 100644 --- a/internal/api/oidc/key_test.go +++ b/internal/api/oidc/key_test.go @@ -12,14 +12,14 @@ import ( "github.com/stretchr/testify/require" "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/query" ) type publicKey struct { id string alg string - use domain.KeyUsage + use crypto.KeyUsage seq uint64 expiry time.Time key any @@ -33,7 +33,7 @@ func (k *publicKey) Algorithm() string { return k.alg } -func (k *publicKey) Use() domain.KeyUsage { +func (k *publicKey) Use() crypto.KeyUsage { return k.use } @@ -55,21 +55,21 @@ var ( "key1": { id: "key1", alg: "alg", - use: domain.KeyUsageSigning, + use: crypto.KeyUsageSigning, seq: 1, expiry: clock.Now().Add(time.Minute), }, "key2": { id: "key2", alg: "alg", - use: domain.KeyUsageSigning, + use: crypto.KeyUsageSigning, seq: 3, expiry: clock.Now().Add(10 * time.Hour), }, "exp1": { id: "key2", alg: "alg", - use: domain.KeyUsageSigning, + use: crypto.KeyUsageSigning, seq: 4, expiry: clock.Now().Add(-time.Hour), }, diff --git a/internal/api/saml/certificate.go b/internal/api/saml/certificate.go index 144a7e10a0..2eac0e4d36 100644 --- a/internal/api/saml/certificate.go +++ b/internal/api/saml/certificate.go @@ -11,7 +11,6 @@ import ( "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" "github.com/zitadel/zitadel/internal/query" "github.com/zitadel/zitadel/internal/repository/instance" @@ -53,7 +52,7 @@ func (c *CertificateAndKey) ID() string { return c.id } -func (p *Storage) GetCertificateAndKey(ctx context.Context, usage domain.KeyUsage) (certAndKey *key.CertificateAndKey, err error) { +func (p *Storage) GetCertificateAndKey(ctx context.Context, usage crypto.KeyUsage) (certAndKey *key.CertificateAndKey, err error) { err = retry(func() error { certAndKey, err = p.getCertificateAndKey(ctx, usage) if err != nil { @@ -67,7 +66,7 @@ func (p *Storage) GetCertificateAndKey(ctx context.Context, usage domain.KeyUsag return certAndKey, err } -func (p *Storage) getCertificateAndKey(ctx context.Context, usage domain.KeyUsage) (*key.CertificateAndKey, error) { +func (p *Storage) getCertificateAndKey(ctx context.Context, usage crypto.KeyUsage) (*key.CertificateAndKey, error) { certs, err := p.query.ActiveCertificates(ctx, time.Now().Add(gracefulPeriod), usage) if err != nil { return nil, err @@ -87,7 +86,7 @@ func (p *Storage) getCertificateAndKey(ctx context.Context, usage domain.KeyUsag func (p *Storage) refreshCertificate( ctx context.Context, - usage domain.KeyUsage, + usage crypto.KeyUsage, position float64, ) error { ok, err := p.ensureIsLatestCertificate(ctx, position) @@ -112,7 +111,7 @@ func (p *Storage) ensureIsLatestCertificate(ctx context.Context, position float6 return position >= maxSequence, nil } -func (p *Storage) lockAndGenerateCertificateAndKey(ctx context.Context, usage domain.KeyUsage) error { +func (p *Storage) lockAndGenerateCertificateAndKey(ctx context.Context, usage crypto.KeyUsage) error { ctx, cancel := context.WithCancel(ctx) defer cancel() ctx = setSAMLCtx(ctx) @@ -128,8 +127,8 @@ func (p *Storage) lockAndGenerateCertificateAndKey(ctx context.Context, usage do } switch usage { - case domain.KeyUsageSAMLMetadataSigning, domain.KeyUsageSAMLResponseSinging: - certAndKey, err := p.GetCertificateAndKey(ctx, domain.KeyUsageSAMLCA) + case crypto.KeyUsageSAMLMetadataSigning, crypto.KeyUsageSAMLResponseSinging: + certAndKey, err := p.GetCertificateAndKey(ctx, crypto.KeyUsageSAMLCA) if err != nil { return fmt.Errorf("error while reading ca certificate: %w", err) } @@ -138,14 +137,14 @@ func (p *Storage) lockAndGenerateCertificateAndKey(ctx context.Context, usage do } switch usage { - case domain.KeyUsageSAMLMetadataSigning: + case crypto.KeyUsageSAMLMetadataSigning: return p.command.GenerateSAMLMetadataCertificate(setSAMLCtx(ctx), p.certificateAlgorithm, certAndKey.Key, certAndKey.Certificate) - case domain.KeyUsageSAMLResponseSinging: + case crypto.KeyUsageSAMLResponseSinging: return p.command.GenerateSAMLResponseCertificate(setSAMLCtx(ctx), p.certificateAlgorithm, certAndKey.Key, certAndKey.Certificate) default: return fmt.Errorf("unknown usage") } - case domain.KeyUsageSAMLCA: + case crypto.KeyUsageSAMLCA: return p.command.GenerateSAMLCACertificate(setSAMLCtx(ctx), p.certificateAlgorithm) default: return fmt.Errorf("unknown certificate usage") diff --git a/internal/api/saml/storage.go b/internal/api/saml/storage.go index bd8afffe54..ca523398f7 100644 --- a/internal/api/saml/storage.go +++ b/internal/api/saml/storage.go @@ -87,15 +87,15 @@ func (p *Storage) Health(context.Context) error { } func (p *Storage) GetCA(ctx context.Context) (*key.CertificateAndKey, error) { - return p.GetCertificateAndKey(ctx, domain.KeyUsageSAMLCA) + return p.GetCertificateAndKey(ctx, crypto.KeyUsageSAMLCA) } func (p *Storage) GetMetadataSigningKey(ctx context.Context) (*key.CertificateAndKey, error) { - return p.GetCertificateAndKey(ctx, domain.KeyUsageSAMLMetadataSigning) + return p.GetCertificateAndKey(ctx, crypto.KeyUsageSAMLMetadataSigning) } func (p *Storage) GetResponseSigningKey(ctx context.Context) (*key.CertificateAndKey, error) { - return p.GetCertificateAndKey(ctx, domain.KeyUsageSAMLResponseSinging) + return p.GetCertificateAndKey(ctx, crypto.KeyUsageSAMLResponseSinging) } func (p *Storage) CreateAuthRequest(ctx context.Context, req *samlp.AuthnRequestType, acsUrl, protocolBinding, relayState, applicationID string) (_ models.AuthRequestInt, err error) { diff --git a/internal/command/command.go b/internal/command/command.go index 22a0ba819b..89f23e6ff7 100644 --- a/internal/command/command.go +++ b/internal/command/command.go @@ -13,6 +13,7 @@ import ( "sync" "time" + "github.com/go-jose/go-jose/v4" "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/api/authz" @@ -76,6 +77,7 @@ type Commands struct { defaultSecretGenerators *SecretGenerators 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) GrpcMethodExisting func(method string) bool GrpcServiceExisting func(method string) bool @@ -157,6 +159,7 @@ func StartCommands( defaultRefreshTokenIdleLifetime: defaultRefreshTokenIdleLifetime, defaultSecretGenerators: defaultSecretGenerators, samlCertificateAndKeyGenerator: samlCertificateAndKeyGenerator(defaults.KeyConfig.CertificateSize, defaults.KeyConfig.CertificateLifetime), + webKeyGenerator: crypto.GenerateEncryptedWebKey, // always true for now until we can check with an eventlist EventExisting: func(event string) bool { return true }, // always true for now until we can check with an eventlist diff --git a/internal/command/instance.go b/internal/command/instance.go index 4d3e1d2528..a0cc773019 100644 --- a/internal/command/instance.go +++ b/internal/command/instance.go @@ -34,12 +34,20 @@ const ( ) type InstanceSetup struct { - zitadel ZitadelConfig - InstanceName string - CustomDomain string - DefaultLanguage language.Tag - Org InstanceOrgSetup - SecretGenerators *SecretGenerators + zitadel ZitadelConfig + InstanceName string + CustomDomain string + DefaultLanguage language.Tag + Org InstanceOrgSetup + SecretGenerators *SecretGenerators + WebKeys struct { + Type crypto.WebKeyConfigType + Config struct { + RSABits crypto.RSABits + RSAHasher crypto.RSAHasher + EllipticCurve crypto.EllipticCurve + } + } PasswordComplexityPolicy struct { MinLength uint64 HasLowercase bool @@ -267,6 +275,9 @@ func setUpInstance(ctx context.Context, c *Commands, setup *InstanceSetup) (vali return nil, nil, nil, err } setupSMTPSettings(c, &validations, setup.SMTPConfiguration, instanceAgg) + if err := setupWebKeys(c, &validations, setup.zitadel.instanceID, setup); err != nil { + return nil, nil, nil, err + } setupOIDCSettings(c, &validations, setup.OIDCSettings, instanceAgg) setupFeatures(&validations, setup.Features, setup.zitadel.instanceID) setupLimits(c, &validations, limits.NewAggregate(setup.zitadel.limitsID, setup.zitadel.instanceID), setup.Limits) @@ -390,6 +401,29 @@ func setupFeatures(validations *[]preparation.Validation, features *InstanceFeat } } +func setupWebKeys(c *Commands, validations *[]preparation.Validation, instanceID string, setup *InstanceSetup) error { + var conf crypto.WebKeyConfig + switch setup.WebKeys.Type { + case crypto.WebKeyConfigTypeUnspecified: + return nil // config disabled, skip + case crypto.WebKeyConfigTypeRSA: + conf = &crypto.WebKeyRSAConfig{ + Bits: setup.WebKeys.Config.RSABits, + Hasher: setup.WebKeys.Config.RSAHasher, + } + case crypto.WebKeyConfigTypeECDSA: + conf = &crypto.WebKeyECDSAConfig{ + Curve: setup.WebKeys.Config.EllipticCurve, + } + case crypto.WebKeyConfigTypeED25519: + conf = &crypto.WebKeyED25519Config{} + default: + return zerrors.ThrowInternalf(nil, "COMMAND-sieX0", "Errors.Internal unknown web key type %q", setup.WebKeys.Type) + } + *validations = append(*validations, c.prepareGenerateInitialWebKeys(instanceID, conf)) + return nil +} + func setupOIDCSettings(commands *Commands, validations *[]preparation.Validation, oidcSettings *OIDCSettings, instanceAgg *instance.Aggregate) { if oidcSettings == nil { return diff --git a/internal/command/instance_features.go b/internal/command/instance_features.go index 3acc789d1b..e6e448da9e 100644 --- a/internal/command/instance_features.go +++ b/internal/command/instance_features.go @@ -3,8 +3,11 @@ package command import ( "context" + "github.com/muhlemmer/gu" + "github.com/zitadel/zitadel/internal/api/authz" "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" "github.com/zitadel/zitadel/internal/feature" @@ -20,6 +23,7 @@ type InstanceFeatures struct { TokenExchange *bool Actions *bool ImprovedPerformance []feature.ImprovedPerformanceType + WebKey *bool } func (m *InstanceFeatures) isEmpty() bool { @@ -30,7 +34,8 @@ func (m *InstanceFeatures) isEmpty() bool { m.TokenExchange == nil && m.Actions == nil && // nil check to allow unset improvements - m.ImprovedPerformance == nil + m.ImprovedPerformance == nil && + m.WebKey == nil } func (c *Commands) SetInstanceFeatures(ctx context.Context, f *InstanceFeatures) (*domain.ObjectDetails, error) { @@ -41,6 +46,9 @@ func (c *Commands) SetInstanceFeatures(ctx context.Context, f *InstanceFeatures) if err := c.eventstore.FilterToQueryReducer(ctx, wm); err != nil { return nil, err } + if err := c.setupWebKeyFeature(ctx, wm, f); err != nil { + return nil, err + } commands := wm.setCommands(ctx, f) if len(commands) == 0 { return writeModelToObjectDetails(wm.WriteModel), nil @@ -61,6 +69,21 @@ func prepareSetFeatures(instanceID string, f *InstanceFeatures) preparation.Vali } } +// setupWebKeyFeature generates the initial web keys for the instance, +// if the feature is enabled in the request and the feature wasn't enabled already in the writeModel. +// [Commands.GenerateInitialWebKeys] checks if keys already exist and does nothing if that's the case. +// The default config of a RSA key with 2048 and the SHA256 hasher is assumed. +// Users can customize this after using the webkey/v3 API. +func (c *Commands) setupWebKeyFeature(ctx context.Context, wm *InstanceFeaturesWriteModel, f *InstanceFeatures) error { + if !gu.Value(f.WebKey) || gu.Value(wm.WebKey) { + return nil + } + return c.GenerateInitialWebKeys(ctx, &crypto.WebKeyRSAConfig{ + Bits: crypto.RSABits2048, + Hasher: crypto.RSAHasherSHA256, + }) +} + func (c *Commands) ResetInstanceFeatures(ctx context.Context) (*domain.ObjectDetails, error) { instanceID := authz.GetInstance(ctx).InstanceID() wm := NewInstanceFeaturesWriteModel(instanceID) diff --git a/internal/command/instance_features_model.go b/internal/command/instance_features_model.go index bfd606e672..bdb46d2e04 100644 --- a/internal/command/instance_features_model.go +++ b/internal/command/instance_features_model.go @@ -67,6 +67,7 @@ func (m *InstanceFeaturesWriteModel) Query() *eventstore.SearchQueryBuilder { feature_v2.InstanceTokenExchangeEventType, feature_v2.InstanceActionsEventType, feature_v2.InstanceImprovedPerformanceEventType, + feature_v2.InstanceWebKeyEventType, ). Builder().ResourceOwner(m.ResourceOwner) } @@ -100,6 +101,9 @@ func reduceInstanceFeature(features *InstanceFeatures, key feature.Key, value an case feature.KeyImprovedPerformance: v := value.([]feature.ImprovedPerformanceType) features.ImprovedPerformance = v + case feature.KeyWebKey: + v := value.(bool) + features.WebKey = &v } } @@ -113,5 +117,6 @@ func (wm *InstanceFeaturesWriteModel) setCommands(ctx context.Context, f *Instan cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.UserSchema, f.UserSchema, feature_v2.InstanceUserSchemaEventType) cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.Actions, f.Actions, feature_v2.InstanceActionsEventType) cmds = appendFeatureSliceUpdate(ctx, cmds, aggregate, wm.ImprovedPerformance, f.ImprovedPerformance, feature_v2.InstanceImprovedPerformanceEventType) + cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.WebKey, f.WebKey, feature_v2.InstanceWebKeyEventType) return cmds } diff --git a/internal/command/key_pair.go b/internal/command/key_pair.go index ac379aa964..90eaf7e3da 100644 --- a/internal/command/key_pair.go +++ b/internal/command/key_pair.go @@ -10,7 +10,6 @@ import ( "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/crypto" - "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/repository/keypair" ) @@ -32,7 +31,7 @@ func (c *Commands) GenerateSigningKeyPair(ctx context.Context, algorithm string) _, err = c.eventstore.Push(ctx, keypair.NewAddedEvent( ctx, keyAgg, - domain.KeyUsageSigning, + crypto.KeyUsageSigning, algorithm, privateCrypto, publicCrypto, privateKeyExp, publicKeyExp)) @@ -69,7 +68,7 @@ func (c *Commands) GenerateSAMLCACertificate(ctx context.Context, algorithm stri keypair.NewAddedEvent( ctx, keyAgg, - domain.KeyUsageSAMLCA, + crypto.KeyUsageSAMLCA, algorithm, privateCrypto, publicCrypto, after, after, @@ -115,7 +114,7 @@ func (c *Commands) GenerateSAMLResponseCertificate(ctx context.Context, algorith keypair.NewAddedEvent( ctx, keyAgg, - domain.KeyUsageSAMLResponseSinging, + crypto.KeyUsageSAMLResponseSinging, algorithm, privateCrypto, publicCrypto, after, after, @@ -160,7 +159,7 @@ func (c *Commands) GenerateSAMLMetadataCertificate(ctx context.Context, algorith keypair.NewAddedEvent( ctx, keyAgg, - domain.KeyUsageSAMLMetadataSigning, + crypto.KeyUsageSAMLMetadataSigning, algorithm, privateCrypto, publicCrypto, after, after), diff --git a/internal/command/key_pair_model.go b/internal/command/key_pair_model.go index e3796f6c64..fe052166b3 100644 --- a/internal/command/key_pair_model.go +++ b/internal/command/key_pair_model.go @@ -1,6 +1,7 @@ package command import ( + "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/repository/keypair" @@ -9,7 +10,7 @@ import ( type KeyPairWriteModel struct { eventstore.WriteModel - Usage domain.KeyUsage + Usage crypto.KeyUsage Algorithm string PrivateKey *domain.Key PublicKey *domain.Key diff --git a/internal/command/web_key.go b/internal/command/web_key.go new file mode 100644 index 0000000000..e8481541c3 --- /dev/null +++ b/internal/command/web_key.go @@ -0,0 +1,188 @@ +package command + +import ( + "context" + + "github.com/zitadel/zitadel/internal/api/authz" + "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" + "github.com/zitadel/zitadel/internal/repository/webkey" + "github.com/zitadel/zitadel/internal/telemetry/tracing" + "github.com/zitadel/zitadel/internal/zerrors" +) + +type WebKeyDetails struct { + KeyID string + ObjectDetails *domain.ObjectDetails +} + +// CreateWebKey creates one web key pair for the instance. +// If the instance does not have an active key, the new key is activated. +func (c *Commands) CreateWebKey(ctx context.Context, conf crypto.WebKeyConfig) (_ *WebKeyDetails, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + _, activeID, err := c.getAllWebKeys(ctx) + if err != nil { + return nil, err + } + addedCmd, aggregate, err := c.generateWebKeyCommand(ctx, authz.GetInstance(ctx).InstanceID(), conf) + if err != nil { + return nil, err + } + commands := []eventstore.Command{addedCmd} + if activeID == "" { + commands = append(commands, webkey.NewActivatedEvent(ctx, aggregate)) + } + model := NewWebKeyWriteModel(aggregate.ID, authz.GetInstance(ctx).InstanceID()) + err = c.pushAppendAndReduce(ctx, model, commands...) + if err != nil { + return nil, err + } + return &WebKeyDetails{ + KeyID: aggregate.ID, + ObjectDetails: writeModelToObjectDetails(&model.WriteModel), + }, nil +} + +// GenerateInitialWebKeys creates 2 web key pairs for the instance. +// The first key is activated for signing use. +// If the instance already has keys, this is noop. +func (c *Commands) GenerateInitialWebKeys(ctx context.Context, conf crypto.WebKeyConfig) (err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + keys, _, err := c.getAllWebKeys(ctx) + if err != nil { + return err + } + if len(keys) != 0 { + return nil + } + commands, err := c.generateInitialWebKeysCommands(ctx, authz.GetInstance(ctx).InstanceID(), conf) + if err != nil { + return err + } + _, err = c.eventstore.Push(ctx, commands...) + return err +} + +func (c *Commands) generateInitialWebKeysCommands(ctx context.Context, instanceID string, conf crypto.WebKeyConfig) (_ []eventstore.Command, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + commands := make([]eventstore.Command, 0, 3) + for i := 0; i < 2; i++ { + addedCmd, aggregate, err := c.generateWebKeyCommand(ctx, instanceID, conf) + if err != nil { + return nil, err + } + commands = append(commands, addedCmd) + if i == 0 { + commands = append(commands, webkey.NewActivatedEvent(ctx, aggregate)) + } + } + return commands, nil +} + +func (c *Commands) generateWebKeyCommand(ctx context.Context, instanceID string, conf crypto.WebKeyConfig) (_ eventstore.Command, _ *eventstore.Aggregate, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + keyID, err := c.idGenerator.Next() + if err != nil { + return nil, nil, err + } + encryptedPrivate, public, err := c.webKeyGenerator(keyID, c.keyAlgorithm, conf) + if err != nil { + return nil, nil, err + } + aggregate := webkey.NewAggregate(keyID, instanceID) + addedCmd, err := webkey.NewAddedEvent(ctx, aggregate, encryptedPrivate, public, conf) + if err != nil { + return nil, nil, err + } + return addedCmd, aggregate, nil +} + +// ActivateWebKey activates the key identified by keyID. +// Any previously activated key on the current instance is deactivated. +func (c *Commands) ActivateWebKey(ctx context.Context, keyID string) (_ *domain.ObjectDetails, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + keys, activeID, err := c.getAllWebKeys(ctx) + if err != nil { + return nil, err + } + if activeID == keyID { + return writeModelToObjectDetails( + &keys[activeID].WriteModel, + ), nil + } + nextActive, ok := keys[keyID] + if !ok { + return nil, zerrors.ThrowNotFound(nil, "COMMAND-teiG3", "Errors.WebKey.NotFound") + } + + commands := make([]eventstore.Command, 0, 2) + commands = append(commands, webkey.NewActivatedEvent(ctx, + webkey.AggregateFromWriteModel(ctx, &nextActive.WriteModel), + )) + if activeID != "" { + commands = append(commands, webkey.NewDeactivatedEvent(ctx, + webkey.AggregateFromWriteModel(ctx, &keys[activeID].WriteModel), + )) + } + err = c.pushAppendAndReduce(ctx, nextActive, commands...) + if err != nil { + return nil, err + } + return writeModelToObjectDetails(&nextActive.WriteModel), nil +} + +// getAllWebKeys searches for all web keys on the instance and returns a map of key IDs. +// activeID is the id of the currently active key. +func (c *Commands) getAllWebKeys(ctx context.Context) (_ map[string]*WebKeyWriteModel, activeID string, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + models := newWebKeyWriteModels(authz.GetInstance(ctx).InstanceID()) + if err = c.eventstore.FilterToQueryReducer(ctx, models); err != nil { + return nil, "", err + } + return models.keys, models.activeID, nil +} + +func (c *Commands) DeleteWebKey(ctx context.Context, keyID string) (_ *domain.ObjectDetails, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + model := NewWebKeyWriteModel(keyID, authz.GetInstance(ctx).InstanceID()) + if err = c.eventstore.FilterToQueryReducer(ctx, model); err != nil { + return nil, err + } + if model.State == domain.WebKeyStateUnspecified { + return nil, zerrors.ThrowNotFound(nil, "COMMAND-ooCa7", "Errors.WebKey.NotFound") + } + if model.State == domain.WebKeyStateActive { + return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-Chai1", "Errors.WebKey.ActiveDelete") + } + err = c.pushAppendAndReduce(ctx, model, webkey.NewRemovedEvent(ctx, + webkey.AggregateFromWriteModel(ctx, &model.WriteModel), + )) + if err != nil { + return nil, err + } + return writeModelToObjectDetails(&model.WriteModel), nil +} + +func (c *Commands) prepareGenerateInitialWebKeys(instanceID string, conf crypto.WebKeyConfig) preparation.Validation { + return func() (preparation.CreateCommands, error) { + return func(ctx context.Context, _ preparation.FilterToQueryReducer) ([]eventstore.Command, error) { + return c.generateInitialWebKeysCommands(ctx, instanceID, conf) + }, nil + } +} diff --git a/internal/command/web_key_model.go b/internal/command/web_key_model.go new file mode 100644 index 0000000000..aca375bb5f --- /dev/null +++ b/internal/command/web_key_model.go @@ -0,0 +1,131 @@ +package command + +import ( + "github.com/go-jose/go-jose/v4" + + "github.com/zitadel/zitadel/internal/crypto" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/repository/webkey" +) + +type WebKeyWriteModel struct { + eventstore.WriteModel + State domain.WebKeyState + PrivateKey *crypto.CryptoValue + PublicKey *jose.JSONWebKey +} + +func NewWebKeyWriteModel(keyID, resourceOwner string) *WebKeyWriteModel { + return &WebKeyWriteModel{ + WriteModel: eventstore.WriteModel{ + AggregateID: keyID, + ResourceOwner: resourceOwner, + }, + } +} + +func (wm *WebKeyWriteModel) AppendEvents(events ...eventstore.Event) { + wm.WriteModel.AppendEvents(events...) +} + +func (wm *WebKeyWriteModel) Reduce() error { + for _, event := range wm.Events { + if event.Aggregate().ID != wm.AggregateID { + continue + } + switch e := event.(type) { + case *webkey.AddedEvent: + wm.State = domain.WebKeyStateInitial + wm.PrivateKey = e.PrivateKey + wm.PublicKey = e.PublicKey + case *webkey.ActivatedEvent: + wm.State = domain.WebKeyStateActive + case *webkey.DeactivatedEvent: + wm.State = domain.WebKeyStateInactive + case *webkey.RemovedEvent: + wm.State = domain.WebKeyStateRemoved + wm.PrivateKey = nil + wm.PublicKey = nil + } + } + return wm.WriteModel.Reduce() +} + +func (wm *WebKeyWriteModel) Query() *eventstore.SearchQueryBuilder { + return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). + ResourceOwner(wm.ResourceOwner). + AddQuery(). + AggregateTypes(webkey.AggregateType). + AggregateIDs(wm.AggregateID). + EventTypes( + webkey.AddedEventType, + webkey.ActivatedEventType, + webkey.DeactivatedEventType, + webkey.RemovedEventType, + ). + Builder() +} + +type webKeyWriteModels struct { + resourceOwner string + events []eventstore.Event + keys map[string]*WebKeyWriteModel + activeID string +} + +func newWebKeyWriteModels(resourceOwner string) *webKeyWriteModels { + return &webKeyWriteModels{ + resourceOwner: resourceOwner, + keys: make(map[string]*WebKeyWriteModel), + } +} + +func (models *webKeyWriteModels) AppendEvents(events ...eventstore.Event) { + models.events = append(models.events, events...) +} + +func (models *webKeyWriteModels) Reduce() error { + for _, event := range models.events { + aggregate := event.Aggregate() + if models.keys[aggregate.ID] == nil { + models.keys[aggregate.ID] = NewWebKeyWriteModel(aggregate.ID, aggregate.ResourceOwner) + } + + switch event.(type) { + case *webkey.AddedEvent: + break + case *webkey.ActivatedEvent: + models.activeID = aggregate.ID + case *webkey.DeactivatedEvent: + if models.activeID == aggregate.ID { + models.activeID = "" + } + case *webkey.RemovedEvent: + delete(models.keys, aggregate.ID) + continue + } + + model := models.keys[aggregate.ID] + model.AppendEvents(event) + if err := model.Reduce(); err != nil { + return err + } + } + models.events = models.events[0:0] + return nil +} + +func (models *webKeyWriteModels) Query() *eventstore.SearchQueryBuilder { + return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). + ResourceOwner(models.resourceOwner). + AddQuery(). + AggregateTypes(webkey.AggregateType). + EventTypes( + webkey.AddedEventType, + webkey.ActivatedEventType, + webkey.DeactivatedEventType, + webkey.RemovedEventType, + ). + Builder() +} diff --git a/internal/command/web_key_test.go b/internal/command/web_key_test.go new file mode 100644 index 0000000000..63463de1df --- /dev/null +++ b/internal/command/web_key_test.go @@ -0,0 +1,754 @@ +package command + +import ( + "context" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "io" + "testing" + + "github.com/go-jose/go-jose/v4" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "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" + "github.com/zitadel/zitadel/internal/id" + id_mock "github.com/zitadel/zitadel/internal/id/mock" + "github.com/zitadel/zitadel/internal/repository/webkey" + "github.com/zitadel/zitadel/internal/zerrors" +) + +func TestCommands_CreateWebKey(t *testing.T) { + ctx := authz.NewMockContextWithPermissions("instance1", "org1", "user1", nil) + key, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader) + require.NoError(t, err) + + type fields struct { + eventstore func(*testing.T) *eventstore.Eventstore + idGenerator id.Generator + webKeyGenerator func(keyID string, alg crypto.EncryptionAlgorithm, genConfig crypto.WebKeyConfig) (encryptedPrivate *crypto.CryptoValue, public *jose.JSONWebKey, err error) + } + type args struct { + conf crypto.WebKeyConfig + } + tests := []struct { + name string + fields fields + args args + want *WebKeyDetails + wantErr error + }{ + { + name: "filter error", + fields: fields{ + eventstore: expectEventstore( + expectFilterError(io.ErrClosedPipe), + ), + }, + args: args{ + &crypto.WebKeyECDSAConfig{ + Curve: crypto.EllipticCurveP384, + }, + }, + wantErr: io.ErrClosedPipe, + }, + { + name: "generate error", + fields: fields{ + eventstore: expectEventstore( + expectFilter(), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "key1"), + webKeyGenerator: func(string, crypto.EncryptionAlgorithm, crypto.WebKeyConfig) (*crypto.CryptoValue, *jose.JSONWebKey, error) { + return nil, nil, io.ErrClosedPipe + }, + }, + args: args{ + &crypto.WebKeyECDSAConfig{ + Curve: crypto.EllipticCurveP384, + }, + }, + wantErr: io.ErrClosedPipe, + }, + { + name: "generate key, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher(mustNewWebkeyAddedEvent(ctx, + webkey.NewAggregate("key1", "instance1"), + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "alg", + KeyID: "encKey", + Crypted: []byte("crypted"), + }, + &jose.JSONWebKey{ + Key: &key.PublicKey, + KeyID: "key1", + Algorithm: string(jose.ES384), + Use: crypto.KeyUsageSigning.String(), + }, + &crypto.WebKeyECDSAConfig{ + Curve: crypto.EllipticCurveP384, + }, + )), + eventFromEventPusher(webkey.NewActivatedEvent(ctx, + webkey.NewAggregate("key1", "instance1"), + )), + ), + expectPush( + mustNewWebkeyAddedEvent(ctx, + webkey.NewAggregate("key2", "instance1"), + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "alg", + KeyID: "encKey", + Crypted: []byte("crypted"), + }, + &jose.JSONWebKey{ + Key: &key.PublicKey, + KeyID: "key2", + Algorithm: string(jose.ES384), + Use: crypto.KeyUsageSigning.String(), + }, + &crypto.WebKeyECDSAConfig{ + Curve: crypto.EllipticCurveP384, + }, + ), + ), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "key2"), + webKeyGenerator: func(keyID string, _ crypto.EncryptionAlgorithm, _ crypto.WebKeyConfig) (*crypto.CryptoValue, *jose.JSONWebKey, error) { + return &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "alg", + KeyID: "encKey", + Crypted: []byte("crypted"), + }, &jose.JSONWebKey{ + Key: &key.PublicKey, + KeyID: keyID, + Algorithm: string(jose.ES384), + Use: crypto.KeyUsageSigning.String(), + }, nil + }, + }, + args: args{ + &crypto.WebKeyECDSAConfig{ + Curve: crypto.EllipticCurveP384, + }, + }, + want: &WebKeyDetails{ + KeyID: "key2", + ObjectDetails: &domain.ObjectDetails{ + ResourceOwner: "instance1", + ID: "key2", + }, + }, + }, + { + name: "generate and activate key, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter(), + expectPush( + mustNewWebkeyAddedEvent(ctx, + webkey.NewAggregate("key1", "instance1"), + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "alg", + KeyID: "encKey", + Crypted: []byte("crypted"), + }, + &jose.JSONWebKey{ + Key: &key.PublicKey, + KeyID: "key1", + Algorithm: string(jose.ES384), + Use: crypto.KeyUsageSigning.String(), + }, + &crypto.WebKeyECDSAConfig{ + Curve: crypto.EllipticCurveP384, + }, + ), + webkey.NewActivatedEvent(ctx, + webkey.NewAggregate("key1", "instance1"), + ), + ), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "key1"), + webKeyGenerator: func(keyID string, _ crypto.EncryptionAlgorithm, _ crypto.WebKeyConfig) (*crypto.CryptoValue, *jose.JSONWebKey, error) { + return &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "alg", + KeyID: "encKey", + Crypted: []byte("crypted"), + }, &jose.JSONWebKey{ + Key: &key.PublicKey, + KeyID: keyID, + Algorithm: string(jose.ES384), + Use: crypto.KeyUsageSigning.String(), + }, nil + }, + }, + args: args{ + &crypto.WebKeyECDSAConfig{ + Curve: crypto.EllipticCurveP384, + }, + }, + want: &WebKeyDetails{ + KeyID: "key1", + ObjectDetails: &domain.ObjectDetails{ + ResourceOwner: "instance1", + ID: "key1", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &Commands{ + eventstore: tt.fields.eventstore(t), + idGenerator: tt.fields.idGenerator, + webKeyGenerator: tt.fields.webKeyGenerator, + } + got, err := c.CreateWebKey(ctx, tt.args.conf) + require.ErrorIs(t, err, tt.wantErr) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestCommands_GenerateInitialWebKeys(t *testing.T) { + ctx := authz.NewMockContextWithPermissions("instance1", "org1", "user1", nil) + key, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader) + require.NoError(t, err) + + type fields struct { + eventstore func(*testing.T) *eventstore.Eventstore + idGenerator id.Generator + webKeyGenerator func(keyID string, alg crypto.EncryptionAlgorithm, genConfig crypto.WebKeyConfig) (encryptedPrivate *crypto.CryptoValue, public *jose.JSONWebKey, err error) + } + type args struct { + conf crypto.WebKeyConfig + } + tests := []struct { + name string + fields fields + args args + wantErr error + }{ + { + name: "filter error", + fields: fields{ + eventstore: expectEventstore( + expectFilterError(io.ErrClosedPipe), + ), + }, + args: args{ + &crypto.WebKeyECDSAConfig{ + Curve: crypto.EllipticCurveP384, + }, + }, + wantErr: io.ErrClosedPipe, + }, + { + name: "key found, noop", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher(mustNewWebkeyAddedEvent(ctx, + webkey.NewAggregate("key1", "instance1"), + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "alg", + KeyID: "encKey", + Crypted: []byte("crypted"), + }, + &jose.JSONWebKey{ + Key: &key.PublicKey, + KeyID: "key1", + Algorithm: string(jose.ES384), + Use: crypto.KeyUsageSigning.String(), + }, + &crypto.WebKeyECDSAConfig{ + Curve: crypto.EllipticCurveP384, + }, + )), + eventFromEventPusher(webkey.NewActivatedEvent(ctx, + webkey.NewAggregate("key1", "instance1"), + )), + ), + ), + }, + args: args{ + &crypto.WebKeyECDSAConfig{ + Curve: crypto.EllipticCurveP384, + }, + }, + wantErr: nil, + }, + { + name: "id generator error", + fields: fields{ + eventstore: expectEventstore(expectFilter()), + idGenerator: id_mock.NewIDGeneratorExpectError(t, io.ErrUnexpectedEOF), + }, + args: args{ + &crypto.WebKeyECDSAConfig{ + Curve: crypto.EllipticCurveP384, + }, + }, + wantErr: io.ErrUnexpectedEOF, + }, + { + name: "keys generated and activated", + fields: fields{ + eventstore: expectEventstore( + expectFilter(), + expectPush( + mustNewWebkeyAddedEvent(ctx, + webkey.NewAggregate("key1", "instance1"), + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "alg", + KeyID: "encKey", + Crypted: []byte("crypted"), + }, + &jose.JSONWebKey{ + Key: &key.PublicKey, + KeyID: "key1", + Algorithm: string(jose.ES384), + Use: crypto.KeyUsageSigning.String(), + }, + &crypto.WebKeyECDSAConfig{ + Curve: crypto.EllipticCurveP384, + }, + ), + webkey.NewActivatedEvent(ctx, + webkey.NewAggregate("key1", "instance1"), + ), + mustNewWebkeyAddedEvent(ctx, + webkey.NewAggregate("key2", "instance1"), + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "alg", + KeyID: "encKey", + Crypted: []byte("crypted"), + }, + &jose.JSONWebKey{ + Key: &key.PublicKey, + KeyID: "key2", + Algorithm: string(jose.ES384), + Use: crypto.KeyUsageSigning.String(), + }, + &crypto.WebKeyECDSAConfig{ + Curve: crypto.EllipticCurveP384, + }, + ), + ), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "key1", "key2"), + webKeyGenerator: func(keyID string, _ crypto.EncryptionAlgorithm, _ crypto.WebKeyConfig) (*crypto.CryptoValue, *jose.JSONWebKey, error) { + return &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "alg", + KeyID: "encKey", + Crypted: []byte("crypted"), + }, &jose.JSONWebKey{ + Key: &key.PublicKey, + KeyID: keyID, + Algorithm: string(jose.ES384), + Use: crypto.KeyUsageSigning.String(), + }, nil + }, + }, + args: args{ + &crypto.WebKeyECDSAConfig{ + Curve: crypto.EllipticCurveP384, + }, + }, + wantErr: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &Commands{ + eventstore: tt.fields.eventstore(t), + idGenerator: tt.fields.idGenerator, + webKeyGenerator: tt.fields.webKeyGenerator, + } + err := c.GenerateInitialWebKeys(ctx, tt.args.conf) + require.ErrorIs(t, err, tt.wantErr) + }) + } +} + +func TestCommands_ActivateWebKey(t *testing.T) { + ctx := authz.NewMockContextWithPermissions("instance1", "org1", "user1", nil) + key, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader) + require.NoError(t, err) + + type fields struct { + eventstore func(*testing.T) *eventstore.Eventstore + webKeyGenerator func(keyID string, alg crypto.EncryptionAlgorithm, genConfig crypto.WebKeyConfig) (encryptedPrivate *crypto.CryptoValue, public *jose.JSONWebKey, err error) + } + type args struct { + keyID string + } + tests := []struct { + name string + fields fields + args args + want *domain.ObjectDetails + wantErr error + }{ + { + name: "filter error", + fields: fields{ + eventstore: expectEventstore( + expectFilterError(io.ErrClosedPipe), + ), + }, + args: args{"key2"}, + wantErr: io.ErrClosedPipe, + }, + { + name: "no changes", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher(mustNewWebkeyAddedEvent(ctx, + webkey.NewAggregate("key1", "instance1"), + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "alg", + KeyID: "encKey", + Crypted: []byte("crypted"), + }, + &jose.JSONWebKey{ + Key: &key.PublicKey, + KeyID: "key1", + Algorithm: string(jose.ES384), + Use: crypto.KeyUsageSigning.String(), + }, + &crypto.WebKeyECDSAConfig{ + Curve: crypto.EllipticCurveP384, + }, + )), + eventFromEventPusher(webkey.NewActivatedEvent(ctx, + webkey.NewAggregate("key1", "instance1"), + )), + ), + ), + }, + args: args{"key1"}, + want: &domain.ObjectDetails{ + ResourceOwner: "instance1", + ID: "key1", + }, + }, + { + name: "not found error", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher(mustNewWebkeyAddedEvent(ctx, + webkey.NewAggregate("key1", "instance1"), + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "alg", + KeyID: "encKey", + Crypted: []byte("crypted"), + }, + &jose.JSONWebKey{ + Key: &key.PublicKey, + KeyID: "key1", + Algorithm: string(jose.ES384), + Use: crypto.KeyUsageSigning.String(), + }, + &crypto.WebKeyECDSAConfig{ + Curve: crypto.EllipticCurveP384, + }, + )), + eventFromEventPusher(webkey.NewActivatedEvent(ctx, + webkey.NewAggregate("key1", "instance1"), + )), + ), + ), + }, + args: args{"key2"}, + wantErr: zerrors.ThrowNotFound(nil, "COMMAND-teiG3", "Errors.WebKey.NotFound"), + }, + { + name: "activate next, de-activate old, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher(mustNewWebkeyAddedEvent(ctx, + webkey.NewAggregate("key1", "instance1"), + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "alg", + KeyID: "encKey", + Crypted: []byte("crypted"), + }, + &jose.JSONWebKey{ + Key: &key.PublicKey, + KeyID: "key1", + Algorithm: string(jose.ES384), + Use: crypto.KeyUsageSigning.String(), + }, + &crypto.WebKeyECDSAConfig{ + Curve: crypto.EllipticCurveP384, + }, + )), + eventFromEventPusher(webkey.NewActivatedEvent(ctx, + webkey.NewAggregate("key1", "instance1"), + )), + eventFromEventPusher(mustNewWebkeyAddedEvent(ctx, + webkey.NewAggregate("key2", "instance1"), + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "alg", + KeyID: "encKey", + Crypted: []byte("crypted"), + }, + &jose.JSONWebKey{ + Key: &key.PublicKey, + KeyID: "key2", + Algorithm: string(jose.ES384), + Use: crypto.KeyUsageSigning.String(), + }, + &crypto.WebKeyECDSAConfig{ + Curve: crypto.EllipticCurveP384, + }, + )), + ), + expectPush( + webkey.NewActivatedEvent(ctx, + webkey.NewAggregate("key2", "instance1"), + ), + webkey.NewDeactivatedEvent(ctx, + webkey.NewAggregate("key1", "instance1"), + ), + ), + ), + }, + args: args{"key2"}, + want: &domain.ObjectDetails{ + ResourceOwner: "instance1", + ID: "key2", + }, + }, + { + name: "activate next, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher(mustNewWebkeyAddedEvent(ctx, + webkey.NewAggregate("key1", "instance1"), + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "alg", + KeyID: "encKey", + Crypted: []byte("crypted"), + }, + &jose.JSONWebKey{ + Key: &key.PublicKey, + KeyID: "key1", + Algorithm: string(jose.ES384), + Use: crypto.KeyUsageSigning.String(), + }, + &crypto.WebKeyECDSAConfig{ + Curve: crypto.EllipticCurveP384, + }, + )), + ), + expectPush( + webkey.NewActivatedEvent(ctx, + webkey.NewAggregate("key1", "instance1"), + ), + ), + ), + }, + args: args{"key1"}, + want: &domain.ObjectDetails{ + ResourceOwner: "instance1", + ID: "key1", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &Commands{ + eventstore: tt.fields.eventstore(t), + webKeyGenerator: tt.fields.webKeyGenerator, + } + got, err := c.ActivateWebKey(ctx, tt.args.keyID) + require.ErrorIs(t, err, tt.wantErr) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestCommands_DeleteWebKey(t *testing.T) { + ctx := authz.NewMockContextWithPermissions("instance1", "org1", "user1", nil) + key, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader) + require.NoError(t, err) + + type fields struct { + eventstore func(*testing.T) *eventstore.Eventstore + } + type args struct { + keyID string + } + tests := []struct { + name string + fields fields + args args + want *domain.ObjectDetails + wantErr error + }{ + { + name: "filter error", + fields: fields{ + eventstore: expectEventstore( + expectFilterError(io.ErrClosedPipe), + ), + }, + args: args{"key1"}, + wantErr: io.ErrClosedPipe, + }, + { + name: "not found error", + fields: fields{ + eventstore: expectEventstore( + expectFilter(), + ), + }, + args: args{"key1"}, + wantErr: zerrors.ThrowNotFound(nil, "COMMAND-ooCa7", "Errors.WebKey.NotFound"), + }, + { + name: "key active error", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher(mustNewWebkeyAddedEvent(ctx, + webkey.NewAggregate("key1", "instance1"), + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "alg", + KeyID: "encKey", + Crypted: []byte("crypted"), + }, + &jose.JSONWebKey{ + Key: &key.PublicKey, + KeyID: "key1", + Algorithm: string(jose.ES384), + Use: crypto.KeyUsageSigning.String(), + }, + &crypto.WebKeyECDSAConfig{ + Curve: crypto.EllipticCurveP384, + }, + )), + eventFromEventPusher(webkey.NewActivatedEvent(ctx, + webkey.NewAggregate("key1", "instance1"), + )), + ), + ), + }, + args: args{"key1"}, + wantErr: zerrors.ThrowPreconditionFailed(nil, "COMMAND-Chai1", "Errors.WebKey.ActiveDelete"), + }, + { + name: "delete deactivated key", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher(mustNewWebkeyAddedEvent(ctx, + webkey.NewAggregate("key1", "instance1"), + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "alg", + KeyID: "encKey", + Crypted: []byte("crypted"), + }, + &jose.JSONWebKey{ + Key: &key.PublicKey, + KeyID: "key1", + Algorithm: string(jose.ES384), + Use: crypto.KeyUsageSigning.String(), + }, + &crypto.WebKeyECDSAConfig{ + Curve: crypto.EllipticCurveP384, + }, + )), + eventFromEventPusher(webkey.NewActivatedEvent(ctx, + webkey.NewAggregate("key1", "instance1"), + )), + eventFromEventPusher(mustNewWebkeyAddedEvent(ctx, + webkey.NewAggregate("key2", "instance1"), + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "alg", + KeyID: "encKey", + Crypted: []byte("crypted"), + }, + &jose.JSONWebKey{ + Key: &key.PublicKey, + KeyID: "key2", + Algorithm: string(jose.ES384), + Use: crypto.KeyUsageSigning.String(), + }, + &crypto.WebKeyECDSAConfig{ + Curve: crypto.EllipticCurveP384, + }, + )), + eventFromEventPusher(webkey.NewActivatedEvent(ctx, + webkey.NewAggregate("key2", "instance1"), + )), + eventFromEventPusher(webkey.NewDeactivatedEvent(ctx, + webkey.NewAggregate("key1", "instance1"), + )), + ), + expectPush( + webkey.NewRemovedEvent(ctx, webkey.NewAggregate("key1", "instance1")), + ), + ), + }, + args: args{"key1"}, + want: &domain.ObjectDetails{ + ResourceOwner: "instance1", + ID: "key1", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &Commands{ + eventstore: tt.fields.eventstore(t), + } + got, err := c.DeleteWebKey(ctx, tt.args.keyID) + require.ErrorIs(t, err, tt.wantErr) + assert.Equal(t, tt.want, got) + }) + } +} + +func mustNewWebkeyAddedEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, + privateKey *crypto.CryptoValue, + publicKey *jose.JSONWebKey, + config crypto.WebKeyConfig) *webkey.AddedEvent { + event, err := webkey.NewAddedEvent(ctx, aggregate, privateKey, publicKey, config) + if err != nil { + panic(err) + } + return event +} diff --git a/internal/crypto/crypto.go b/internal/crypto/crypto.go index a74f97a054..ff3b6e2418 100644 --- a/internal/crypto/crypto.go +++ b/internal/crypto/crypto.go @@ -68,6 +68,14 @@ func Encrypt(value []byte, alg EncryptionAlgorithm) (*CryptoValue, error) { }, nil } +func EncryptJSON(obj any, alg EncryptionAlgorithm) (*CryptoValue, error) { + data, err := json.Marshal(obj) + if err != nil { + return nil, zerrors.ThrowInternal(err, "CRYPT-Ei6doF", "error encrypting value") + } + return Encrypt(data, alg) +} + func Decrypt(value *CryptoValue, alg EncryptionAlgorithm) ([]byte, error) { if err := checkEncryptionAlgorithm(value, alg); err != nil { return nil, err @@ -75,6 +83,17 @@ func Decrypt(value *CryptoValue, alg EncryptionAlgorithm) ([]byte, error) { return alg.Decrypt(value.Crypted, value.KeyID) } +func DecryptJSON(value *CryptoValue, dst any, alg EncryptionAlgorithm) error { + data, err := Decrypt(value, alg) + if err != nil { + return err + } + if err = json.Unmarshal(data, dst); err != nil { + return zerrors.ThrowInternal(err, "CRYPT-Jaik2R", "error decrypting value") + } + return nil +} + // DecryptString decrypts the value using the key identified by keyID. // When the decrypted value contains non-UTF8 characters an error is returned. func DecryptString(value *CryptoValue, alg EncryptionAlgorithm) (string, error) { diff --git a/internal/crypto/ellipticcurve_enumer.go b/internal/crypto/ellipticcurve_enumer.go new file mode 100644 index 0000000000..770f4e46c9 --- /dev/null +++ b/internal/crypto/ellipticcurve_enumer.go @@ -0,0 +1,116 @@ +// Code generated by "enumer -type EllipticCurve -trimprefix EllipticCurve -text -json -linecomment"; DO NOT EDIT. + +package crypto + +import ( + "encoding/json" + "fmt" + "strings" +) + +const _EllipticCurveName = "P256P384P512" + +var _EllipticCurveIndex = [...]uint8{0, 0, 4, 8, 12} + +const _EllipticCurveLowerName = "p256p384p512" + +func (i EllipticCurve) String() string { + if i < 0 || i >= EllipticCurve(len(_EllipticCurveIndex)-1) { + return fmt.Sprintf("EllipticCurve(%d)", i) + } + return _EllipticCurveName[_EllipticCurveIndex[i]:_EllipticCurveIndex[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 _EllipticCurveNoOp() { + var x [1]struct{} + _ = x[EllipticCurveUnspecified-(0)] + _ = x[EllipticCurveP256-(1)] + _ = x[EllipticCurveP384-(2)] + _ = x[EllipticCurveP512-(3)] +} + +var _EllipticCurveValues = []EllipticCurve{EllipticCurveUnspecified, EllipticCurveP256, EllipticCurveP384, EllipticCurveP512} + +var _EllipticCurveNameToValueMap = map[string]EllipticCurve{ + _EllipticCurveName[0:0]: EllipticCurveUnspecified, + _EllipticCurveLowerName[0:0]: EllipticCurveUnspecified, + _EllipticCurveName[0:4]: EllipticCurveP256, + _EllipticCurveLowerName[0:4]: EllipticCurveP256, + _EllipticCurveName[4:8]: EllipticCurveP384, + _EllipticCurveLowerName[4:8]: EllipticCurveP384, + _EllipticCurveName[8:12]: EllipticCurveP512, + _EllipticCurveLowerName[8:12]: EllipticCurveP512, +} + +var _EllipticCurveNames = []string{ + _EllipticCurveName[0:0], + _EllipticCurveName[0:4], + _EllipticCurveName[4:8], + _EllipticCurveName[8:12], +} + +// EllipticCurveString retrieves an enum value from the enum constants string name. +// Throws an error if the param is not part of the enum. +func EllipticCurveString(s string) (EllipticCurve, error) { + if val, ok := _EllipticCurveNameToValueMap[s]; ok { + return val, nil + } + + if val, ok := _EllipticCurveNameToValueMap[strings.ToLower(s)]; ok { + return val, nil + } + return 0, fmt.Errorf("%s does not belong to EllipticCurve values", s) +} + +// EllipticCurveValues returns all values of the enum +func EllipticCurveValues() []EllipticCurve { + return _EllipticCurveValues +} + +// EllipticCurveStrings returns a slice of all String values of the enum +func EllipticCurveStrings() []string { + strs := make([]string, len(_EllipticCurveNames)) + copy(strs, _EllipticCurveNames) + return strs +} + +// IsAEllipticCurve returns "true" if the value is listed in the enum definition. "false" otherwise +func (i EllipticCurve) IsAEllipticCurve() bool { + for _, v := range _EllipticCurveValues { + if i == v { + return true + } + } + return false +} + +// MarshalJSON implements the json.Marshaler interface for EllipticCurve +func (i EllipticCurve) MarshalJSON() ([]byte, error) { + return json.Marshal(i.String()) +} + +// UnmarshalJSON implements the json.Unmarshaler interface for EllipticCurve +func (i *EllipticCurve) UnmarshalJSON(data []byte) error { + var s string + if err := json.Unmarshal(data, &s); err != nil { + return fmt.Errorf("EllipticCurve should be a string, got %s", data) + } + + var err error + *i, err = EllipticCurveString(s) + return err +} + +// MarshalText implements the encoding.TextMarshaler interface for EllipticCurve +func (i EllipticCurve) MarshalText() ([]byte, error) { + return []byte(i.String()), nil +} + +// UnmarshalText implements the encoding.TextUnmarshaler interface for EllipticCurve +func (i *EllipticCurve) UnmarshalText(text []byte) error { + var err error + *i, err = EllipticCurveString(string(text)) + return err +} diff --git a/internal/crypto/rsabits_enumer.go b/internal/crypto/rsabits_enumer.go new file mode 100644 index 0000000000..f3856c87c0 --- /dev/null +++ b/internal/crypto/rsabits_enumer.go @@ -0,0 +1,136 @@ +// Code generated by "enumer -type RSABits -trimprefix RSABits -text -json -linecomment"; DO NOT EDIT. + +package crypto + +import ( + "encoding/json" + "fmt" + "strings" +) + +const ( + _RSABitsName_0 = "" + _RSABitsLowerName_0 = "" + _RSABitsName_1 = "2048" + _RSABitsLowerName_1 = "2048" + _RSABitsName_2 = "3072" + _RSABitsLowerName_2 = "3072" + _RSABitsName_3 = "4096" + _RSABitsLowerName_3 = "4096" +) + +var ( + _RSABitsIndex_0 = [...]uint8{0, 0} + _RSABitsIndex_1 = [...]uint8{0, 4} + _RSABitsIndex_2 = [...]uint8{0, 4} + _RSABitsIndex_3 = [...]uint8{0, 4} +) + +func (i RSABits) String() string { + switch { + case i == 0: + return _RSABitsName_0 + case i == 2048: + return _RSABitsName_1 + case i == 3072: + return _RSABitsName_2 + case i == 4096: + return _RSABitsName_3 + default: + return fmt.Sprintf("RSABits(%d)", i) + } +} + +// An "invalid array index" compiler error signifies that the constant values have changed. +// Re-run the stringer command to generate them again. +func _RSABitsNoOp() { + var x [1]struct{} + _ = x[RSABitsUnspecified-(0)] + _ = x[RSABits2048-(2048)] + _ = x[RSABits3072-(3072)] + _ = x[RSABits4096-(4096)] +} + +var _RSABitsValues = []RSABits{RSABitsUnspecified, RSABits2048, RSABits3072, RSABits4096} + +var _RSABitsNameToValueMap = map[string]RSABits{ + _RSABitsName_0[0:0]: RSABitsUnspecified, + _RSABitsLowerName_0[0:0]: RSABitsUnspecified, + _RSABitsName_1[0:4]: RSABits2048, + _RSABitsLowerName_1[0:4]: RSABits2048, + _RSABitsName_2[0:4]: RSABits3072, + _RSABitsLowerName_2[0:4]: RSABits3072, + _RSABitsName_3[0:4]: RSABits4096, + _RSABitsLowerName_3[0:4]: RSABits4096, +} + +var _RSABitsNames = []string{ + _RSABitsName_0[0:0], + _RSABitsName_1[0:4], + _RSABitsName_2[0:4], + _RSABitsName_3[0:4], +} + +// RSABitsString retrieves an enum value from the enum constants string name. +// Throws an error if the param is not part of the enum. +func RSABitsString(s string) (RSABits, error) { + if val, ok := _RSABitsNameToValueMap[s]; ok { + return val, nil + } + + if val, ok := _RSABitsNameToValueMap[strings.ToLower(s)]; ok { + return val, nil + } + return 0, fmt.Errorf("%s does not belong to RSABits values", s) +} + +// RSABitsValues returns all values of the enum +func RSABitsValues() []RSABits { + return _RSABitsValues +} + +// RSABitsStrings returns a slice of all String values of the enum +func RSABitsStrings() []string { + strs := make([]string, len(_RSABitsNames)) + copy(strs, _RSABitsNames) + return strs +} + +// IsARSABits returns "true" if the value is listed in the enum definition. "false" otherwise +func (i RSABits) IsARSABits() bool { + for _, v := range _RSABitsValues { + if i == v { + return true + } + } + return false +} + +// MarshalJSON implements the json.Marshaler interface for RSABits +func (i RSABits) MarshalJSON() ([]byte, error) { + return json.Marshal(i.String()) +} + +// UnmarshalJSON implements the json.Unmarshaler interface for RSABits +func (i *RSABits) UnmarshalJSON(data []byte) error { + var s string + if err := json.Unmarshal(data, &s); err != nil { + return fmt.Errorf("RSABits should be a string, got %s", data) + } + + var err error + *i, err = RSABitsString(s) + return err +} + +// MarshalText implements the encoding.TextMarshaler interface for RSABits +func (i RSABits) MarshalText() ([]byte, error) { + return []byte(i.String()), nil +} + +// UnmarshalText implements the encoding.TextUnmarshaler interface for RSABits +func (i *RSABits) UnmarshalText(text []byte) error { + var err error + *i, err = RSABitsString(string(text)) + return err +} diff --git a/internal/crypto/rsahasher_enumer.go b/internal/crypto/rsahasher_enumer.go new file mode 100644 index 0000000000..31023d8731 --- /dev/null +++ b/internal/crypto/rsahasher_enumer.go @@ -0,0 +1,116 @@ +// Code generated by "enumer -type RSAHasher -trimprefix RSAHasher -text -json -linecomment"; DO NOT EDIT. + +package crypto + +import ( + "encoding/json" + "fmt" + "strings" +) + +const _RSAHasherName = "SHA256SHA384SHA512" + +var _RSAHasherIndex = [...]uint8{0, 0, 6, 12, 18} + +const _RSAHasherLowerName = "sha256sha384sha512" + +func (i RSAHasher) String() string { + if i < 0 || i >= RSAHasher(len(_RSAHasherIndex)-1) { + return fmt.Sprintf("RSAHasher(%d)", i) + } + return _RSAHasherName[_RSAHasherIndex[i]:_RSAHasherIndex[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 _RSAHasherNoOp() { + var x [1]struct{} + _ = x[RSAHasherUnspecified-(0)] + _ = x[RSAHasherSHA256-(1)] + _ = x[RSAHasherSHA384-(2)] + _ = x[RSAHasherSHA512-(3)] +} + +var _RSAHasherValues = []RSAHasher{RSAHasherUnspecified, RSAHasherSHA256, RSAHasherSHA384, RSAHasherSHA512} + +var _RSAHasherNameToValueMap = map[string]RSAHasher{ + _RSAHasherName[0:0]: RSAHasherUnspecified, + _RSAHasherLowerName[0:0]: RSAHasherUnspecified, + _RSAHasherName[0:6]: RSAHasherSHA256, + _RSAHasherLowerName[0:6]: RSAHasherSHA256, + _RSAHasherName[6:12]: RSAHasherSHA384, + _RSAHasherLowerName[6:12]: RSAHasherSHA384, + _RSAHasherName[12:18]: RSAHasherSHA512, + _RSAHasherLowerName[12:18]: RSAHasherSHA512, +} + +var _RSAHasherNames = []string{ + _RSAHasherName[0:0], + _RSAHasherName[0:6], + _RSAHasherName[6:12], + _RSAHasherName[12:18], +} + +// RSAHasherString retrieves an enum value from the enum constants string name. +// Throws an error if the param is not part of the enum. +func RSAHasherString(s string) (RSAHasher, error) { + if val, ok := _RSAHasherNameToValueMap[s]; ok { + return val, nil + } + + if val, ok := _RSAHasherNameToValueMap[strings.ToLower(s)]; ok { + return val, nil + } + return 0, fmt.Errorf("%s does not belong to RSAHasher values", s) +} + +// RSAHasherValues returns all values of the enum +func RSAHasherValues() []RSAHasher { + return _RSAHasherValues +} + +// RSAHasherStrings returns a slice of all String values of the enum +func RSAHasherStrings() []string { + strs := make([]string, len(_RSAHasherNames)) + copy(strs, _RSAHasherNames) + return strs +} + +// IsARSAHasher returns "true" if the value is listed in the enum definition. "false" otherwise +func (i RSAHasher) IsARSAHasher() bool { + for _, v := range _RSAHasherValues { + if i == v { + return true + } + } + return false +} + +// MarshalJSON implements the json.Marshaler interface for RSAHasher +func (i RSAHasher) MarshalJSON() ([]byte, error) { + return json.Marshal(i.String()) +} + +// UnmarshalJSON implements the json.Unmarshaler interface for RSAHasher +func (i *RSAHasher) UnmarshalJSON(data []byte) error { + var s string + if err := json.Unmarshal(data, &s); err != nil { + return fmt.Errorf("RSAHasher should be a string, got %s", data) + } + + var err error + *i, err = RSAHasherString(s) + return err +} + +// MarshalText implements the encoding.TextMarshaler interface for RSAHasher +func (i RSAHasher) MarshalText() ([]byte, error) { + return []byte(i.String()), nil +} + +// UnmarshalText implements the encoding.TextUnmarshaler interface for RSAHasher +func (i *RSAHasher) UnmarshalText(text []byte) error { + var err error + *i, err = RSAHasherString(string(text)) + return err +} diff --git a/internal/crypto/web_key.go b/internal/crypto/web_key.go new file mode 100644 index 0000000000..c769cb1213 --- /dev/null +++ b/internal/crypto/web_key.go @@ -0,0 +1,238 @@ +package crypto + +import ( + "crypto/ecdsa" + "crypto/ed25519" + "crypto/elliptic" + "crypto/rand" + "crypto/rsa" + "encoding/json" + + "github.com/go-jose/go-jose/v4" + "github.com/muhlemmer/gu" + + "github.com/zitadel/zitadel/internal/zerrors" +) + +type KeyUsage int32 + +const ( + KeyUsageSigning KeyUsage = iota + KeyUsageSAMLMetadataSigning + KeyUsageSAMLResponseSinging + KeyUsageSAMLCA +) + +func (u KeyUsage) String() string { + switch u { + case KeyUsageSigning: + return "sig" + case KeyUsageSAMLCA: + return "saml_ca" + case KeyUsageSAMLResponseSinging: + return "saml_response_sig" + case KeyUsageSAMLMetadataSigning: + return "saml_metadata_sig" + } + return "" +} + +//go:generate enumer -type WebKeyConfigType -trimprefix WebKeyConfigType -text -json -linecomment +type WebKeyConfigType int + +const ( + WebKeyConfigTypeUnspecified WebKeyConfigType = iota // + WebKeyConfigTypeRSA + WebKeyConfigTypeECDSA + WebKeyConfigTypeED25519 +) + +//go:generate enumer -type RSABits -trimprefix RSABits -text -json -linecomment +type RSABits int + +const ( + RSABitsUnspecified RSABits = 0 // + RSABits2048 RSABits = 2048 + RSABits3072 RSABits = 3072 + RSABits4096 RSABits = 4096 +) + +type RSAHasher int + +//go:generate enumer -type RSAHasher -trimprefix RSAHasher -text -json -linecomment +const ( + RSAHasherUnspecified RSAHasher = iota // + RSAHasherSHA256 + RSAHasherSHA384 + RSAHasherSHA512 +) + +type EllipticCurve int + +//go:generate enumer -type EllipticCurve -trimprefix EllipticCurve -text -json -linecomment +const ( + EllipticCurveUnspecified EllipticCurve = iota // + EllipticCurveP256 + EllipticCurveP384 + EllipticCurveP512 +) + +type WebKeyConfig interface { + Alg() jose.SignatureAlgorithm + Type() WebKeyConfigType // Type is needed to make Unmarshal work + IsValid() error +} + +func UnmarshalWebKeyConfig(data []byte, configType WebKeyConfigType) (config WebKeyConfig, err error) { + switch configType { + case WebKeyConfigTypeUnspecified: + return nil, zerrors.ThrowInternal(nil, "CRYPT-Ii3AiH", "Errors.Internal") + case WebKeyConfigTypeRSA: + config = new(WebKeyRSAConfig) + case WebKeyConfigTypeECDSA: + config = new(WebKeyECDSAConfig) + case WebKeyConfigTypeED25519: + config = new(WebKeyED25519Config) + default: + return nil, zerrors.ThrowInternal(nil, "CRYPT-Eig8ho", "Errors.Internal") + } + if err = json.Unmarshal(data, config); err != nil { + return nil, zerrors.ThrowInternal(err, "CRYPT-waeR0N", "Errors.Internal") + } + return config, nil +} + +type WebKeyRSAConfig struct { + Bits RSABits + Hasher RSAHasher +} + +func (c WebKeyRSAConfig) Alg() jose.SignatureAlgorithm { + switch c.Hasher { + case RSAHasherUnspecified: + return "" + case RSAHasherSHA256: + return jose.RS256 + case RSAHasherSHA384: + return jose.RS384 + case RSAHasherSHA512: + return jose.RS512 + default: + return "" + } +} + +func (WebKeyRSAConfig) Type() WebKeyConfigType { + return WebKeyConfigTypeRSA +} + +func (c WebKeyRSAConfig) IsValid() error { + if !c.Bits.IsARSABits() || c.Bits == RSABitsUnspecified { + return zerrors.ThrowInvalidArgument(nil, "CRYPTO-eaz3T", "Errors.WebKey.Config") + } + if !c.Hasher.IsARSAHasher() || c.Hasher == RSAHasherUnspecified { + return zerrors.ThrowInvalidArgument(nil, "CRYPTO-ODie7", "Errors.WebKey.Config") + } + return nil +} + +type WebKeyECDSAConfig struct { + Curve EllipticCurve +} + +func (c WebKeyECDSAConfig) Alg() jose.SignatureAlgorithm { + switch c.Curve { + case EllipticCurveUnspecified: + return "" + case EllipticCurveP256: + return jose.ES256 + case EllipticCurveP384: + return jose.ES384 + case EllipticCurveP512: + return jose.ES512 + default: + return "" + } +} + +func (WebKeyECDSAConfig) Type() WebKeyConfigType { + return WebKeyConfigTypeECDSA +} + +func (c WebKeyECDSAConfig) IsValid() error { + if !c.Curve.IsAEllipticCurve() || c.Curve == EllipticCurveUnspecified { + return zerrors.ThrowInvalidArgument(nil, "CRYPTO-Ii2ai", "Errors.WebKey.Config") + } + return nil +} + +func (c WebKeyECDSAConfig) GetCurve() elliptic.Curve { + switch c.Curve { + case EllipticCurveUnspecified: + return nil + case EllipticCurveP256: + return elliptic.P256() + case EllipticCurveP384: + return elliptic.P384() + case EllipticCurveP512: + return elliptic.P521() + default: + return nil + } +} + +type WebKeyED25519Config struct{} + +func (WebKeyED25519Config) Alg() jose.SignatureAlgorithm { + return jose.EdDSA +} + +func (WebKeyED25519Config) Type() WebKeyConfigType { + return WebKeyConfigTypeED25519 +} + +func (WebKeyED25519Config) IsValid() error { + return nil +} + +func GenerateEncryptedWebKey(keyID string, alg EncryptionAlgorithm, genConfig WebKeyConfig) (encryptedPrivate *CryptoValue, public *jose.JSONWebKey, err error) { + private, public, err := generateWebKey(keyID, genConfig) + if err != nil { + return nil, nil, err + } + encryptedPrivate, err = EncryptJSON(private, alg) + if err != nil { + return nil, nil, err + } + return encryptedPrivate, public, nil +} + +func generateWebKey(keyID string, genConfig WebKeyConfig) (private, public *jose.JSONWebKey, err error) { + if err = genConfig.IsValid(); err != nil { + return nil, nil, err + } + var key any + switch conf := genConfig.(type) { + case *WebKeyRSAConfig: + key, err = rsa.GenerateKey(rand.Reader, int(conf.Bits)) + case *WebKeyECDSAConfig: + key, err = ecdsa.GenerateKey(conf.GetCurve(), rand.Reader) + case *WebKeyED25519Config: + _, key, err = ed25519.GenerateKey(rand.Reader) + } + if err != nil { + return nil, nil, err + } + + private = newJSONWebkey(key, keyID, genConfig.Alg()) + return private, gu.Ptr(private.Public()), err +} + +func newJSONWebkey(key any, keyID string, algorithm jose.SignatureAlgorithm) *jose.JSONWebKey { + return &jose.JSONWebKey{ + Key: key, + KeyID: keyID, + Algorithm: string(algorithm), + Use: KeyUsageSigning.String(), + } +} diff --git a/internal/crypto/web_key_test.go b/internal/crypto/web_key_test.go new file mode 100644 index 0000000000..e6b1a5a56b --- /dev/null +++ b/internal/crypto/web_key_test.go @@ -0,0 +1,269 @@ +package crypto + +import ( + "crypto/elliptic" + "testing" + + "github.com/go-jose/go-jose/v4" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/zitadel/zitadel/internal/zerrors" +) + +func TestUnmarshalWebKeyConfig(t *testing.T) { + type args struct { + data []byte + configType WebKeyConfigType + } + tests := []struct { + name string + args args + wantConfig WebKeyConfig + wantErr error + }{ + { + name: "unspecified", + args: args{ + []byte(`{}`), + WebKeyConfigTypeUnspecified, + }, + wantErr: zerrors.ThrowInternal(nil, "CRYPT-Ii3AiH", "Errors.Internal"), + }, + { + name: "rsa", + args: args{ + []byte(`{"bits":"2048", "hasher":"sha256"}`), + WebKeyConfigTypeRSA, + }, + wantConfig: &WebKeyRSAConfig{ + Bits: RSABits2048, + Hasher: RSAHasherSHA256, + }, + }, + { + name: "ecdsa", + args: args{ + []byte(`{"curve":"p256"}`), + WebKeyConfigTypeECDSA, + }, + wantConfig: &WebKeyECDSAConfig{ + Curve: EllipticCurveP256, + }, + }, + { + name: "ed25519", + args: args{ + []byte(`{}`), + WebKeyConfigTypeED25519, + }, + wantConfig: &WebKeyED25519Config{}, + }, + { + name: "unknown type error", + args: args{ + []byte(`{"curve":0}`), + 99, + }, + wantErr: zerrors.ThrowInternal(nil, "CRYPT-Eig8ho", "Errors.Internal"), + }, + { + name: "unmarshal error", + args: args{ + []byte(`~~`), + WebKeyConfigTypeED25519, + }, + wantErr: zerrors.ThrowInternal(nil, "CRYPT-waeR0N", "Errors.Internal"), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotConfig, err := UnmarshalWebKeyConfig(tt.args.data, tt.args.configType) + require.ErrorIs(t, err, tt.wantErr) + assert.Equal(t, gotConfig, tt.wantConfig) + }) + } +} + +func TestWebKeyECDSAConfig_Alg(t *testing.T) { + type fields struct { + Curve EllipticCurve + } + tests := []struct { + name string + fields fields + want jose.SignatureAlgorithm + }{ + { + name: "unspecified", + fields: fields{ + Curve: EllipticCurveUnspecified, + }, + want: "", + }, + { + name: "P256", + fields: fields{ + Curve: EllipticCurveP256, + }, + want: jose.ES256, + }, + { + name: "P384", + fields: fields{ + Curve: EllipticCurveP384, + }, + want: jose.ES384, + }, + { + name: "P512", + fields: fields{ + Curve: EllipticCurveP512, + }, + want: jose.ES512, + }, + { + name: "default", + fields: fields{ + Curve: 99, + }, + want: "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := WebKeyECDSAConfig{ + Curve: tt.fields.Curve, + } + got := c.Alg() + assert.Equal(t, tt.want, got) + }) + } +} + +func TestWebKeyECDSAConfig_GetCurve(t *testing.T) { + type fields struct { + Curve EllipticCurve + } + tests := []struct { + name string + fields fields + want elliptic.Curve + }{ + { + name: "unspecified", + fields: fields{EllipticCurveUnspecified}, + want: nil, + }, + { + name: "P256", + fields: fields{EllipticCurveP256}, + want: elliptic.P256(), + }, + { + name: "P384", + fields: fields{EllipticCurveP384}, + want: elliptic.P384(), + }, + { + name: "P512", + fields: fields{EllipticCurveP512}, + want: elliptic.P521(), + }, + { + name: "default", + fields: fields{99}, + want: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := WebKeyECDSAConfig{ + Curve: tt.fields.Curve, + } + got := c.GetCurve() + assert.Equal(t, tt.want, got) + }) + } +} + +func Test_generateEncryptedWebKey(t *testing.T) { + type args struct { + keyID string + genConfig WebKeyConfig + } + tests := []struct { + name string + args args + assertPrivate func(t *testing.T, got *jose.JSONWebKey) + assertPublic func(t *testing.T, got *jose.JSONWebKey) + wantErr error + }{ + { + name: "invalid", + args: args{ + keyID: "keyID", + genConfig: &WebKeyRSAConfig{ + Bits: RSABitsUnspecified, + Hasher: RSAHasherSHA256, + }, + }, + wantErr: zerrors.ThrowInvalidArgument(nil, "CRYPTO-eaz3T", "Errors.WebKey.Config"), + }, + { + name: "RSA", + args: args{ + keyID: "keyID", + genConfig: &WebKeyRSAConfig{ + Bits: RSABits2048, + Hasher: RSAHasherSHA256, + }, + }, + assertPrivate: assertJSONWebKey("keyID", "RS256", "sig", false), + assertPublic: assertJSONWebKey("keyID", "RS256", "sig", true), + }, + { + name: "ECDSA", + args: args{ + keyID: "keyID", + genConfig: &WebKeyECDSAConfig{ + Curve: EllipticCurveP256, + }, + }, + assertPrivate: assertJSONWebKey("keyID", "ES256", "sig", false), + assertPublic: assertJSONWebKey("keyID", "ES256", "sig", true), + }, + { + name: "ED25519", + args: args{ + keyID: "keyID", + genConfig: &WebKeyED25519Config{}, + }, + assertPrivate: assertJSONWebKey("keyID", "EdDSA", "sig", false), + assertPublic: assertJSONWebKey("keyID", "EdDSA", "sig", true), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotPrivate, gotPublic, err := generateWebKey(tt.args.keyID, tt.args.genConfig) + require.ErrorIs(t, err, tt.wantErr) + if tt.assertPrivate != nil { + tt.assertPrivate(t, gotPrivate) + } + if tt.assertPublic != nil { + tt.assertPublic(t, gotPublic) + } + }) + } +} + +func assertJSONWebKey(keyID, algorithm, use string, isPublic bool) func(t *testing.T, got *jose.JSONWebKey) { + return func(t *testing.T, got *jose.JSONWebKey) { + assert.NotNil(t, got) + assert.NotNil(t, got.Key, "key") + assert.Equal(t, keyID, got.KeyID, "keyID") + assert.Equal(t, algorithm, got.Algorithm, "algorithm") + assert.Equal(t, use, got.Use, "user") + assert.Equal(t, isPublic, got.IsPublic(), "isPublic") + } +} diff --git a/internal/crypto/webkeyconfigtype_enumer.go b/internal/crypto/webkeyconfigtype_enumer.go new file mode 100644 index 0000000000..2725402013 --- /dev/null +++ b/internal/crypto/webkeyconfigtype_enumer.go @@ -0,0 +1,116 @@ +// Code generated by "enumer -type WebKeyConfigType -trimprefix WebKeyConfigType -text -json -linecomment"; DO NOT EDIT. + +package crypto + +import ( + "encoding/json" + "fmt" + "strings" +) + +const _WebKeyConfigTypeName = "RSAECDSAED25519" + +var _WebKeyConfigTypeIndex = [...]uint8{0, 0, 3, 8, 15} + +const _WebKeyConfigTypeLowerName = "rsaecdsaed25519" + +func (i WebKeyConfigType) String() string { + if i < 0 || i >= WebKeyConfigType(len(_WebKeyConfigTypeIndex)-1) { + return fmt.Sprintf("WebKeyConfigType(%d)", i) + } + return _WebKeyConfigTypeName[_WebKeyConfigTypeIndex[i]:_WebKeyConfigTypeIndex[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 _WebKeyConfigTypeNoOp() { + var x [1]struct{} + _ = x[WebKeyConfigTypeUnspecified-(0)] + _ = x[WebKeyConfigTypeRSA-(1)] + _ = x[WebKeyConfigTypeECDSA-(2)] + _ = x[WebKeyConfigTypeED25519-(3)] +} + +var _WebKeyConfigTypeValues = []WebKeyConfigType{WebKeyConfigTypeUnspecified, WebKeyConfigTypeRSA, WebKeyConfigTypeECDSA, WebKeyConfigTypeED25519} + +var _WebKeyConfigTypeNameToValueMap = map[string]WebKeyConfigType{ + _WebKeyConfigTypeName[0:0]: WebKeyConfigTypeUnspecified, + _WebKeyConfigTypeLowerName[0:0]: WebKeyConfigTypeUnspecified, + _WebKeyConfigTypeName[0:3]: WebKeyConfigTypeRSA, + _WebKeyConfigTypeLowerName[0:3]: WebKeyConfigTypeRSA, + _WebKeyConfigTypeName[3:8]: WebKeyConfigTypeECDSA, + _WebKeyConfigTypeLowerName[3:8]: WebKeyConfigTypeECDSA, + _WebKeyConfigTypeName[8:15]: WebKeyConfigTypeED25519, + _WebKeyConfigTypeLowerName[8:15]: WebKeyConfigTypeED25519, +} + +var _WebKeyConfigTypeNames = []string{ + _WebKeyConfigTypeName[0:0], + _WebKeyConfigTypeName[0:3], + _WebKeyConfigTypeName[3:8], + _WebKeyConfigTypeName[8:15], +} + +// WebKeyConfigTypeString retrieves an enum value from the enum constants string name. +// Throws an error if the param is not part of the enum. +func WebKeyConfigTypeString(s string) (WebKeyConfigType, error) { + if val, ok := _WebKeyConfigTypeNameToValueMap[s]; ok { + return val, nil + } + + if val, ok := _WebKeyConfigTypeNameToValueMap[strings.ToLower(s)]; ok { + return val, nil + } + return 0, fmt.Errorf("%s does not belong to WebKeyConfigType values", s) +} + +// WebKeyConfigTypeValues returns all values of the enum +func WebKeyConfigTypeValues() []WebKeyConfigType { + return _WebKeyConfigTypeValues +} + +// WebKeyConfigTypeStrings returns a slice of all String values of the enum +func WebKeyConfigTypeStrings() []string { + strs := make([]string, len(_WebKeyConfigTypeNames)) + copy(strs, _WebKeyConfigTypeNames) + return strs +} + +// IsAWebKeyConfigType returns "true" if the value is listed in the enum definition. "false" otherwise +func (i WebKeyConfigType) IsAWebKeyConfigType() bool { + for _, v := range _WebKeyConfigTypeValues { + if i == v { + return true + } + } + return false +} + +// MarshalJSON implements the json.Marshaler interface for WebKeyConfigType +func (i WebKeyConfigType) MarshalJSON() ([]byte, error) { + return json.Marshal(i.String()) +} + +// UnmarshalJSON implements the json.Unmarshaler interface for WebKeyConfigType +func (i *WebKeyConfigType) UnmarshalJSON(data []byte) error { + var s string + if err := json.Unmarshal(data, &s); err != nil { + return fmt.Errorf("WebKeyConfigType should be a string, got %s", data) + } + + var err error + *i, err = WebKeyConfigTypeString(s) + return err +} + +// MarshalText implements the encoding.TextMarshaler interface for WebKeyConfigType +func (i WebKeyConfigType) MarshalText() ([]byte, error) { + return []byte(i.String()), nil +} + +// UnmarshalText implements the encoding.TextUnmarshaler interface for WebKeyConfigType +func (i *WebKeyConfigType) UnmarshalText(text []byte) error { + var err error + *i, err = WebKeyConfigTypeString(string(text)) + return err +} diff --git a/internal/domain/key_pair.go b/internal/domain/key_pair.go index ff0ec773f7..ffc0e38e53 100644 --- a/internal/domain/key_pair.go +++ b/internal/domain/key_pair.go @@ -10,36 +10,13 @@ import ( type KeyPair struct { es_models.ObjectRoot - Usage KeyUsage + Usage crypto.KeyUsage Algorithm string PrivateKey *Key PublicKey *Key Certificate *Key } -type KeyUsage int32 - -const ( - KeyUsageSigning KeyUsage = iota - KeyUsageSAMLMetadataSigning - KeyUsageSAMLResponseSinging - KeyUsageSAMLCA -) - -func (u KeyUsage) String() string { - switch u { - case KeyUsageSigning: - return "sig" - case KeyUsageSAMLCA: - return "saml_ca" - case KeyUsageSAMLResponseSinging: - return "saml_response_sig" - case KeyUsageSAMLMetadataSigning: - return "saml_metadata_sig" - } - return "" -} - type Key struct { Key *crypto.CryptoValue Expiry time.Time diff --git a/internal/domain/web_key.go b/internal/domain/web_key.go new file mode 100644 index 0000000000..4246ee7708 --- /dev/null +++ b/internal/domain/web_key.go @@ -0,0 +1,11 @@ +package domain + +type WebKeyState int + +const ( + WebKeyStateUnspecified WebKeyState = iota + WebKeyStateInitial + WebKeyStateActive + WebKeyStateInactive + WebKeyStateRemoved +) diff --git a/internal/eventstore/aggregate.go b/internal/eventstore/aggregate.go index 9939d8335c..87282e9007 100644 --- a/internal/eventstore/aggregate.go +++ b/internal/eventstore/aggregate.go @@ -48,15 +48,25 @@ func WithInstanceID(id string) aggregateOpt { } } -// AggregateFromWriteModel maps the given WriteModel to an Aggregate +// AggregateFromWriteModel maps the given WriteModel to an Aggregate. +// Deprecated: Creates linter errors on missing context. Use [AggregateFromWriteModelCtx] instead. func AggregateFromWriteModel( wm *WriteModel, typ AggregateType, version Version, +) *Aggregate { + return AggregateFromWriteModelCtx(context.Background(), wm, typ, version) +} + +// AggregateFromWriteModelCtx maps the given WriteModel to an Aggregate. +func AggregateFromWriteModelCtx( + ctx context.Context, + wm *WriteModel, + typ AggregateType, + version Version, ) *Aggregate { return NewAggregate( - // TODO: the linter complains if this function is called without passing a context - context.Background(), + ctx, wm.AggregateID, typ, version, diff --git a/internal/eventstore/repository/mock/repository.mock.impl.go b/internal/eventstore/repository/mock/repository.mock.impl.go index 6ae64ddf0f..d41521ad8f 100644 --- a/internal/eventstore/repository/mock/repository.mock.impl.go +++ b/internal/eventstore/repository/mock/repository.mock.impl.go @@ -166,7 +166,7 @@ func (e *mockEvent) DataAsBytes() []byte { } payload, err := json.Marshal(e.Payload()) if err != nil { - panic("unable to unmarshal") + panic(err) } return payload } diff --git a/internal/feature/feature.go b/internal/feature/feature.go index 5f0453f078..34dd5d908a 100644 --- a/internal/feature/feature.go +++ b/internal/feature/feature.go @@ -14,6 +14,7 @@ const ( KeyTokenExchange KeyActions KeyImprovedPerformance + KeyWebKey ) //go:generate enumer -type Level -transform snake -trimprefix Level @@ -37,6 +38,7 @@ type Features struct { TokenExchange bool `json:"token_exchange,omitempty"` Actions bool `json:"actions,omitempty"` ImprovedPerformance []ImprovedPerformanceType `json:"improved_performance,omitempty"` + WebKey bool `json:"web_key,omitempty"` } type ImprovedPerformanceType int32 diff --git a/internal/feature/key_enumer.go b/internal/feature/key_enumer.go index ca3e156b61..6452a258c3 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_performance" +const _KeyName = "unspecifiedlogin_default_orgtrigger_introspection_projectionslegacy_introspectionuser_schematoken_exchangeactionsimproved_performanceweb_key" -var _KeyIndex = [...]uint8{0, 11, 28, 61, 81, 92, 106, 113, 133} +var _KeyIndex = [...]uint8{0, 11, 28, 61, 81, 92, 106, 113, 133, 140} -const _KeyLowerName = "unspecifiedlogin_default_orgtrigger_introspection_projectionslegacy_introspectionuser_schematoken_exchangeactionsimproved_performance" +const _KeyLowerName = "unspecifiedlogin_default_orgtrigger_introspection_projectionslegacy_introspectionuser_schematoken_exchangeactionsimproved_performanceweb_key" func (i Key) String() string { if i < 0 || i >= Key(len(_KeyIndex)-1) { @@ -32,9 +32,10 @@ func _KeyNoOp() { _ = x[KeyTokenExchange-(5)] _ = x[KeyActions-(6)] _ = x[KeyImprovedPerformance-(7)] + _ = x[KeyWebKey-(8)] } -var _KeyValues = []Key{KeyUnspecified, KeyLoginDefaultOrg, KeyTriggerIntrospectionProjections, KeyLegacyIntrospection, KeyUserSchema, KeyTokenExchange, KeyActions, KeyImprovedPerformance} +var _KeyValues = []Key{KeyUnspecified, KeyLoginDefaultOrg, KeyTriggerIntrospectionProjections, KeyLegacyIntrospection, KeyUserSchema, KeyTokenExchange, KeyActions, KeyImprovedPerformance, KeyWebKey} var _KeyNameToValueMap = map[string]Key{ _KeyName[0:11]: KeyUnspecified, @@ -53,6 +54,8 @@ var _KeyNameToValueMap = map[string]Key{ _KeyLowerName[106:113]: KeyActions, _KeyName[113:133]: KeyImprovedPerformance, _KeyLowerName[113:133]: KeyImprovedPerformance, + _KeyName[133:140]: KeyWebKey, + _KeyLowerName[133:140]: KeyWebKey, } var _KeyNames = []string{ @@ -64,6 +67,7 @@ var _KeyNames = []string{ _KeyName[92:106], _KeyName[106:113], _KeyName[113:133], + _KeyName[133:140], } // KeyString retrieves an enum value from the enum constants string name. diff --git a/internal/integration/client.go b/internal/integration/client.go index 9e5dc63dde..947c11508b 100644 --- a/internal/integration/client.go +++ b/internal/integration/client.go @@ -36,6 +36,7 @@ import ( org "github.com/zitadel/zitadel/pkg/grpc/org/v2" org_v2beta "github.com/zitadel/zitadel/pkg/grpc/org/v2beta" action "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha" + webkey_v3alpha "github.com/zitadel/zitadel/pkg/grpc/resources/webkey/v3alpha" session "github.com/zitadel/zitadel/pkg/grpc/session/v2" session_v2beta "github.com/zitadel/zitadel/pkg/grpc/session/v2beta" settings "github.com/zitadel/zitadel/pkg/grpc/settings/v2" @@ -63,10 +64,11 @@ type Client struct { OrgV2beta org_v2beta.OrganizationServiceClient OrgV2 org.OrganizationServiceClient System system.SystemServiceClient - ActionV3 action.ZITADELActionsClient + ActionV3Alpha action.ZITADELActionsClient FeatureV2beta feature_v2beta.FeatureServiceClient FeatureV2 feature.FeatureServiceClient UserSchemaV3 schema.UserSchemaServiceClient + WebKeyV3Alpha webkey_v3alpha.ZITADELWebKeysClient } func newClient(cc *grpc.ClientConn) Client { @@ -86,10 +88,11 @@ func newClient(cc *grpc.ClientConn) Client { OrgV2beta: org_v2beta.NewOrganizationServiceClient(cc), OrgV2: org.NewOrganizationServiceClient(cc), System: system.NewSystemServiceClient(cc), - ActionV3: action.NewZITADELActionsClient(cc), + ActionV3Alpha: action.NewZITADELActionsClient(cc), FeatureV2beta: feature_v2beta.NewFeatureServiceClient(cc), FeatureV2: feature.NewFeatureServiceClient(cc), UserSchemaV3: schema.NewUserSchemaServiceClient(cc), + WebKeyV3Alpha: webkey_v3alpha.NewZITADELWebKeysClient(cc), } } @@ -649,20 +652,20 @@ func (s *Tester) CreateTarget(ctx context.Context, t *testing.T, name, endpoint RestAsync: &action.SetRESTAsync{}, } } - target, err := s.Client.ActionV3.CreateTarget(ctx, &action.CreateTargetRequest{Target: reqTarget}) + target, err := s.Client.ActionV3Alpha.CreateTarget(ctx, &action.CreateTargetRequest{Target: reqTarget}) require.NoError(t, err) return target } func (s *Tester) DeleteExecution(ctx context.Context, t *testing.T, cond *action.Condition) { - _, err := s.Client.ActionV3.SetExecution(ctx, &action.SetExecutionRequest{ + _, err := s.Client.ActionV3Alpha.SetExecution(ctx, &action.SetExecutionRequest{ Condition: cond, }) require.NoError(t, err) } func (s *Tester) SetExecution(ctx context.Context, t *testing.T, cond *action.Condition, targets []*action.ExecutionTargetType) *action.SetExecutionResponse { - target, err := s.Client.ActionV3.SetExecution(ctx, &action.SetExecutionRequest{ + target, err := s.Client.ActionV3Alpha.SetExecution(ctx, &action.SetExecutionRequest{ Condition: cond, Execution: &action.Execution{ Targets: targets, diff --git a/internal/query/certificate.go b/internal/query/certificate.go index f4254e0231..e4d53213cf 100644 --- a/internal/query/certificate.go +++ b/internal/query/certificate.go @@ -10,7 +10,6 @@ import ( "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/crypto" - "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/query/projection" "github.com/zitadel/zitadel/internal/telemetry/tracing" "github.com/zitadel/zitadel/internal/zerrors" @@ -66,7 +65,7 @@ var ( } ) -func (q *Queries) ActiveCertificates(ctx context.Context, t time.Time, usage domain.KeyUsage) (certs *Certificates, err error) { +func (q *Queries) ActiveCertificates(ctx context.Context, t time.Time, usage crypto.KeyUsage) (certs *Certificates, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() diff --git a/internal/query/certificate_test.go b/internal/query/certificate_test.go index a6c862c8ad..01e563de11 100644 --- a/internal/query/certificate_test.go +++ b/internal/query/certificate_test.go @@ -9,7 +9,6 @@ import ( "testing" "github.com/zitadel/zitadel/internal/crypto" - "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/zerrors" ) @@ -109,7 +108,7 @@ func Test_CertificatePrepares(t *testing.T) { sequence: 20211109, resourceOwner: "ro", algorithm: "", - use: domain.KeyUsageSAMLMetadataSigning, + use: crypto.KeyUsageSAMLMetadataSigning, }, expiry: testNow, certificate: []byte("privateKey"), diff --git a/internal/query/instance_features.go b/internal/query/instance_features.go index 12d8e0d80d..f10039fa66 100644 --- a/internal/query/instance_features.go +++ b/internal/query/instance_features.go @@ -16,6 +16,7 @@ type InstanceFeatures struct { TokenExchange FeatureSource[bool] Actions FeatureSource[bool] ImprovedPerformance FeatureSource[[]feature.ImprovedPerformanceType] + WebKey FeatureSource[bool] } func (q *Queries) GetInstanceFeatures(ctx context.Context, cascade bool) (_ *InstanceFeatures, err error) { diff --git a/internal/query/instance_features_model.go b/internal/query/instance_features_model.go index 215442c911..a2ab09d263 100644 --- a/internal/query/instance_features_model.go +++ b/internal/query/instance_features_model.go @@ -67,6 +67,7 @@ func (m *InstanceFeaturesReadModel) Query() *eventstore.SearchQueryBuilder { feature_v2.InstanceTokenExchangeEventType, feature_v2.InstanceActionsEventType, feature_v2.InstanceImprovedPerformanceEventType, + feature_v2.InstanceWebKeyEventType, ). Builder().ResourceOwner(m.ResourceOwner) } @@ -115,6 +116,8 @@ func reduceInstanceFeatureSet[T any](features *InstanceFeatures, event *feature_ features.Actions.set(level, event.Value) case feature.KeyImprovedPerformance: features.ImprovedPerformance.set(level, event.Value) + case feature.KeyWebKey: + features.WebKey.set(level, event.Value) } return nil } diff --git a/internal/query/key.go b/internal/query/key.go index ae733e8dd3..d7475e424b 100644 --- a/internal/query/key.go +++ b/internal/query/key.go @@ -11,7 +11,6 @@ import ( "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/crypto" - "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/query/projection" "github.com/zitadel/zitadel/internal/repository/keypair" @@ -22,7 +21,7 @@ import ( type Key interface { ID() string Algorithm() string - Use() domain.KeyUsage + Use() crypto.KeyUsage Sequence() uint64 } @@ -55,7 +54,7 @@ type key struct { sequence uint64 resourceOwner string algorithm string - use domain.KeyUsage + use crypto.KeyUsage } func (k *key) ID() string { @@ -66,7 +65,7 @@ func (k *key) Algorithm() string { return k.algorithm } -func (k *key) Use() domain.KeyUsage { +func (k *key) Use() crypto.KeyUsage { return k.use } @@ -222,7 +221,7 @@ func (q *Queries) ActivePrivateSigningKey(ctx context.Context, t time.Time) (key query, args, err := stmt.Where( sq.And{ sq.Eq{ - KeyColUse.identifier(): domain.KeyUsageSigning, + KeyColUse.identifier(): crypto.KeyUsageSigning, KeyColInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), }, sq.Gt{KeyPrivateColExpiry.identifier(): t}, @@ -358,7 +357,7 @@ type PublicKeyReadModel struct { Algorithm string Key *crypto.CryptoValue Expiry time.Time - Usage domain.KeyUsage + Usage crypto.KeyUsage } func NewPublicKeyReadModel(keyID, resourceOwner string) *PublicKeyReadModel { diff --git a/internal/query/key_test.go b/internal/query/key_test.go index 70e5860eb0..a977bfb58e 100644 --- a/internal/query/key_test.go +++ b/internal/query/key_test.go @@ -19,7 +19,6 @@ import ( "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" key_repo "github.com/zitadel/zitadel/internal/repository/keypair" "github.com/zitadel/zitadel/internal/zerrors" @@ -131,7 +130,7 @@ func Test_KeyPrepares(t *testing.T) { sequence: 20211109, resourceOwner: "ro", algorithm: "RS256", - use: domain.KeyUsageSigning, + use: crypto.KeyUsageSigning, }, expiry: testNow, publicKey: &rsa.PublicKey{ @@ -212,7 +211,7 @@ func Test_KeyPrepares(t *testing.T) { sequence: 20211109, resourceOwner: "ro", algorithm: "RS256", - use: domain.KeyUsageSigning, + use: crypto.KeyUsageSigning, }, expiry: testNow, privateKey: &crypto.CryptoValue{ @@ -306,7 +305,7 @@ func TestQueries_GetPublicKeyByID(t *testing.T) { InstanceID: "instanceID", Version: key_repo.AggregateVersion, }, - domain.KeyUsageSigning, "alg", + crypto.KeyUsageSigning, "alg", &crypto.CryptoValue{ CryptoType: crypto.TypeEncryption, Algorithm: "alg", @@ -345,7 +344,7 @@ func TestQueries_GetPublicKeyByID(t *testing.T) { InstanceID: "instanceID", Version: key_repo.AggregateVersion, }, - domain.KeyUsageSigning, "alg", + crypto.KeyUsageSigning, "alg", &crypto.CryptoValue{ CryptoType: crypto.TypeEncryption, Algorithm: "alg", @@ -385,7 +384,7 @@ func TestQueries_GetPublicKeyByID(t *testing.T) { InstanceID: "instanceID", Version: key_repo.AggregateVersion, }, - domain.KeyUsageSigning, "alg", + crypto.KeyUsageSigning, "alg", &crypto.CryptoValue{ CryptoType: crypto.TypeEncryption, Algorithm: "alg", @@ -416,7 +415,7 @@ func TestQueries_GetPublicKeyByID(t *testing.T) { id: "keyID", resourceOwner: "instanceID", algorithm: "alg", - use: domain.KeyUsageSigning, + use: crypto.KeyUsageSigning, }, expiry: future, publicKey: func() *rsa.PublicKey { diff --git a/internal/query/projection/instance_features.go b/internal/query/projection/instance_features.go index 06090a2f5d..d24fe6d203 100644 --- a/internal/query/projection/instance_features.go +++ b/internal/query/projection/instance_features.go @@ -88,6 +88,10 @@ func (*instanceFeatureProjection) Reducers() []handler.AggregateReducer { Event: feature_v2.InstanceImprovedPerformanceEventType, Reduce: reduceInstanceSetFeature[[]feature.ImprovedPerformanceType], }, + { + Event: feature_v2.InstanceWebKeyEventType, + Reduce: reduceInstanceSetFeature[bool], + }, { Event: instance.InstanceRemovedEventType, Reduce: reduceInstanceRemovedHelper(InstanceDomainInstanceIDCol), diff --git a/internal/query/projection/key_test.go b/internal/query/projection/key_test.go index 7022c1b9ec..75358cce12 100644 --- a/internal/query/projection/key_test.go +++ b/internal/query/projection/key_test.go @@ -8,7 +8,6 @@ import ( "go.uber.org/mock/gomock" "github.com/zitadel/zitadel/internal/crypto" - "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/eventstore/handler/v2" "github.com/zitadel/zitadel/internal/repository/instance" @@ -33,7 +32,7 @@ func TestKeyProjection_reduces(t *testing.T) { testEvent( keypair.AddedEventType, keypair.AggregateType, - keypairAddedEventData(domain.KeyUsageSigning, time.Now().Add(time.Hour)), + keypairAddedEventData(crypto.KeyUsageSigning, time.Now().Add(time.Hour)), ), keypair.AddedEventMapper), }, reduce: (&keyProjection{encryptionAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t))}).reduceKeyPairAdded, @@ -52,7 +51,7 @@ func TestKeyProjection_reduces(t *testing.T) { "instance-id", uint64(15), "algorithm", - domain.KeyUsageSigning, + crypto.KeyUsageSigning, }, }, { @@ -89,7 +88,7 @@ func TestKeyProjection_reduces(t *testing.T) { testEvent( keypair.AddedEventType, keypair.AggregateType, - keypairAddedEventData(domain.KeyUsageSigning, time.Now().Add(-time.Hour)), + keypairAddedEventData(crypto.KeyUsageSigning, time.Now().Add(-time.Hour)), ), keypair.AddedEventMapper), }, reduce: (&keyProjection{}).reduceKeyPairAdded, @@ -132,7 +131,7 @@ func TestKeyProjection_reduces(t *testing.T) { testEvent( keypair.AddedCertificateEventType, keypair.AggregateType, - certificateAddedEventData(domain.KeyUsageSAMLMetadataSigning, time.Now().Add(time.Hour)), + certificateAddedEventData(crypto.KeyUsageSAMLMetadataSigning, time.Now().Add(time.Hour)), ), keypair.AddedCertificateEventMapper), }, reduce: (&keyProjection{certEncryptionAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t))}).reduceCertificateAdded, @@ -170,10 +169,10 @@ func TestKeyProjection_reduces(t *testing.T) { } } -func keypairAddedEventData(usage domain.KeyUsage, t time.Time) []byte { +func keypairAddedEventData(usage crypto.KeyUsage, t time.Time) []byte { return []byte(`{"algorithm": "algorithm", "usage": ` + fmt.Sprintf("%d", usage) + `, "privateKey": {"key": {"cryptoType": 0, "algorithm": "enc", "keyID": "id", "crypted": "cHJpdmF0ZUtleQ=="}, "expiry": "` + t.Format(time.RFC3339) + `"}, "publicKey": {"key": {"cryptoType": 0, "algorithm": "enc", "keyID": "id", "crypted": "cHVibGljS2V5"}, "expiry": "` + t.Format(time.RFC3339) + `"}}`) } -func certificateAddedEventData(usage domain.KeyUsage, t time.Time) []byte { +func certificateAddedEventData(usage crypto.KeyUsage, t time.Time) []byte { return []byte(`{"algorithm": "algorithm", "usage": ` + fmt.Sprintf("%d", usage) + `, "certificate": {"key": {"cryptoType": 0, "algorithm": "enc", "keyID": "id", "crypted": "cHJpdmF0ZUtleQ=="}, "expiry": "` + t.Format(time.RFC3339) + `"}}`) } diff --git a/internal/query/projection/projection.go b/internal/query/projection/projection.go index a7776d24af..0151a9953b 100644 --- a/internal/query/projection/projection.go +++ b/internal/query/projection/projection.go @@ -78,6 +78,7 @@ var ( TargetProjection *handler.Handler ExecutionProjection *handler.Handler UserSchemaProjection *handler.Handler + WebKeyProjection *handler.Handler ProjectGrantFields *handler.FieldHandler OrgDomainVerifiedFields *handler.FieldHandler @@ -163,6 +164,7 @@ func Create(ctx context.Context, sqlClient *database.DB, es handler.EventStore, TargetProjection = newTargetProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["targets"])) ExecutionProjection = newExecutionProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["executions"])) UserSchemaProjection = newUserSchemaProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["user_schemas"])) + WebKeyProjection = newWebKeyProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["web_keys"])) ProjectGrantFields = newFillProjectGrantFields(applyCustomConfig(projectionConfig, config.Customizations[fieldsProjectGrant])) OrgDomainVerifiedFields = newFillOrgDomainVerifiedFields(applyCustomConfig(projectionConfig, config.Customizations[fieldsOrgDomainVerified])) @@ -292,5 +294,6 @@ func newProjectionsList() { TargetProjection, ExecutionProjection, UserSchemaProjection, + WebKeyProjection, } } diff --git a/internal/query/projection/web_key.go b/internal/query/projection/web_key.go new file mode 100644 index 0000000000..231f5ddfb1 --- /dev/null +++ b/internal/query/projection/web_key.go @@ -0,0 +1,165 @@ +package projection + +import ( + "context" + + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/eventstore" + old_handler "github.com/zitadel/zitadel/internal/eventstore/handler" + "github.com/zitadel/zitadel/internal/eventstore/handler/v2" + "github.com/zitadel/zitadel/internal/repository/instance" + "github.com/zitadel/zitadel/internal/repository/webkey" + "github.com/zitadel/zitadel/internal/zerrors" +) + +const ( + WebKeyTable = "projections.web_keys" + + WebKeyInstanceIDCol = "instance_id" + WebKeyKeyIDCol = "key_id" + WebKeyCreationDateCol = "creation_date" + WebKeyChangeDateCol = "change_date" + WebKeySequenceCol = "sequence" + WebKeyStateCol = "state" + WebKeyPrivateKeyCol = "private_key" + WebKeyPublicKeyCol = "public_key" + WebKeyConfigCol = "config" + WebKeyConfigTypeCol = "config_type" +) + +type webKeyProjection struct{} + +func newWebKeyProjection(ctx context.Context, config handler.Config) *handler.Handler { + return handler.NewHandler(ctx, &config, new(webKeyProjection)) +} + +func (*webKeyProjection) Name() string { + return WebKeyTable +} + +func (*webKeyProjection) Init() *old_handler.Check { + return handler.NewTableCheck( + handler.NewTable( + []*handler.InitColumn{ + handler.NewColumn(WebKeyInstanceIDCol, handler.ColumnTypeText), + handler.NewColumn(WebKeyKeyIDCol, handler.ColumnTypeText), + handler.NewColumn(WebKeyCreationDateCol, handler.ColumnTypeTimestamp), + handler.NewColumn(WebKeyChangeDateCol, handler.ColumnTypeTimestamp), + handler.NewColumn(WebKeySequenceCol, handler.ColumnTypeInt64), + handler.NewColumn(WebKeyStateCol, handler.ColumnTypeInt64), + handler.NewColumn(WebKeyPrivateKeyCol, handler.ColumnTypeJSONB), + handler.NewColumn(WebKeyPublicKeyCol, handler.ColumnTypeJSONB), + handler.NewColumn(WebKeyConfigCol, handler.ColumnTypeJSONB), + handler.NewColumn(WebKeyConfigTypeCol, handler.ColumnTypeInt64), + }, + handler.NewPrimaryKey(WebKeyInstanceIDCol, WebKeyKeyIDCol), + + // index to find the current active private key for an instance. + handler.WithIndex(handler.NewIndex( + "web_key_state", + []string{WebKeyInstanceIDCol, WebKeyStateCol}, + handler.WithInclude( + WebKeyPrivateKeyCol, + ), + )), + ), + ) +} + +func (p *webKeyProjection) Reducers() []handler.AggregateReducer { + return []handler.AggregateReducer{{ + Aggregate: webkey.AggregateType, + EventReducers: []handler.EventReducer{ + { + Event: webkey.AddedEventType, + Reduce: p.reduceWebKeyAdded, + }, + { + Event: webkey.ActivatedEventType, + Reduce: p.reduceWebKeyActivated, + }, + { + Event: webkey.DeactivatedEventType, + Reduce: p.reduceWebKeyDeactivated, + }, + { + Event: webkey.RemovedEventType, + Reduce: p.reduceWebKeyRemoved, + }, + { + Event: instance.InstanceRemovedEventType, + Reduce: reduceInstanceRemovedHelper(WebKeyInstanceIDCol), + }, + }, + }} +} + +func (p *webKeyProjection) reduceWebKeyAdded(event eventstore.Event) (*handler.Statement, error) { + e, ok := event.(*webkey.AddedEvent) + if !ok { + return nil, zerrors.ThrowInvalidArgumentf(nil, "PROJE-jei2K", "reduce.wrong.event.type %s", webkey.AddedEventType) + } + return handler.NewCreateStatement(e, + []handler.Column{ + handler.NewCol(WebKeyInstanceIDCol, e.Agg.InstanceID), + handler.NewCol(WebKeyKeyIDCol, e.Agg.ID), + handler.NewCol(WebKeyCreationDateCol, e.CreationDate()), + handler.NewCol(WebKeyChangeDateCol, e.CreationDate()), + handler.NewCol(WebKeySequenceCol, e.Sequence()), + handler.NewCol(WebKeyStateCol, domain.WebKeyStateInitial), + handler.NewCol(WebKeyPrivateKeyCol, e.PrivateKey), + handler.NewCol(WebKeyPublicKeyCol, e.PublicKey), + handler.NewCol(WebKeyConfigCol, e.Config), + handler.NewCol(WebKeyConfigTypeCol, e.ConfigType), + }, + ), nil +} + +func (p *webKeyProjection) reduceWebKeyActivated(event eventstore.Event) (*handler.Statement, error) { + e, ok := event.(*webkey.ActivatedEvent) + if !ok { + return nil, zerrors.ThrowInvalidArgumentf(nil, "PROJE-iiQu2", "reduce.wrong.event.type %s", webkey.ActivatedEventType) + } + return handler.NewUpdateStatement(e, + []handler.Column{ + handler.NewCol(WebKeyChangeDateCol, e.CreationDate()), + handler.NewCol(WebKeySequenceCol, e.Sequence()), + handler.NewCol(WebKeyStateCol, domain.WebKeyStateActive), + }, + []handler.Condition{ + handler.NewCond(WebKeyInstanceIDCol, e.Agg.InstanceID), + handler.NewCond(WebKeyKeyIDCol, e.Agg.ID), + }, + ), nil +} + +func (p *webKeyProjection) reduceWebKeyDeactivated(event eventstore.Event) (*handler.Statement, error) { + e, ok := event.(*webkey.DeactivatedEvent) + if !ok { + return nil, zerrors.ThrowInvalidArgumentf(nil, "PROJE-zei3E", "reduce.wrong.event.type %s", webkey.DeactivatedEventType) + } + return handler.NewUpdateStatement(e, + []handler.Column{ + handler.NewCol(WebKeyChangeDateCol, e.CreationDate()), + handler.NewCol(WebKeySequenceCol, e.Sequence()), + handler.NewCol(WebKeyStateCol, domain.WebKeyStateInactive), + }, + []handler.Condition{ + handler.NewCond(WebKeyInstanceIDCol, e.Agg.InstanceID), + handler.NewCond(WebKeyKeyIDCol, e.Agg.ID), + }, + ), nil +} + +func (p *webKeyProjection) reduceWebKeyRemoved(event eventstore.Event) (*handler.Statement, error) { + e, ok := event.(*webkey.RemovedEvent) + if !ok { + return nil, zerrors.ThrowInvalidArgumentf(nil, "PROJE-Zei6f", "reduce.wrong.event.type %s", webkey.RemovedEventType) + } + return handler.NewDeleteStatement(e, + []handler.Condition{ + handler.NewCond(WebKeyInstanceIDCol, e.Agg.InstanceID), + handler.NewCond(WebKeyKeyIDCol, e.Agg.ID), + }, + ), nil +} diff --git a/internal/query/web_key.go b/internal/query/web_key.go new file mode 100644 index 0000000000..f8930a6280 --- /dev/null +++ b/internal/query/web_key.go @@ -0,0 +1,154 @@ +package query + +import ( + "context" + "database/sql" + _ "embed" + "encoding/json" + "errors" + "time" + + "github.com/go-jose/go-jose/v4" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/crypto" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/telemetry/tracing" + "github.com/zitadel/zitadel/internal/zerrors" +) + +var ( + //go:embed web_key_by_state.sql + webKeyByStateQuery string + //go:embed web_key_list.sql + webKeyListQuery string + //go:embed web_key_public_keys.sql + webKeyPublicKeysQuery string +) + +// GetPublicWebKeyByID gets a public key by it's keyID directly from the eventstore. +func (q *Queries) GetPublicWebKeyByID(ctx context.Context, keyID string) (webKey *jose.JSONWebKey, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + model := NewWebKeyReadModel(keyID, authz.GetInstance(ctx).InstanceID()) + if err = q.eventstore.FilterToQueryReducer(ctx, model); err != nil { + return nil, err + } + if model.State == domain.WebKeyStateUnspecified || model.State == domain.WebKeyStateRemoved { + return nil, zerrors.ThrowNotFound(nil, "QUERY-AiCh0", "Errors.WebKey.NotFound") + } + return model.PublicKey, nil +} + +// GetActiveSigningWebKey gets the current active signing key from the web_keys projection. +// The active signing key is eventual consistent. +func (q *Queries) GetActiveSigningWebKey(ctx context.Context) (webKey *jose.JSONWebKey, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + var keyValue *crypto.CryptoValue + err = q.client.QueryRowContext(ctx, func(row *sql.Row) error { + return row.Scan(&keyValue) + }, + webKeyByStateQuery, + authz.GetInstance(ctx).InstanceID(), + domain.WebKeyStateActive, + ) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, zerrors.ThrowInternal(err, "QUERY-Opoh7", "Errors.WebKey.NoActive") + } + return nil, zerrors.ThrowInternal(err, "QUERY-Shoo0", "Errors.Internal") + } + if err = crypto.DecryptJSON(keyValue, &webKey, q.keyEncryptionAlgorithm); err != nil { + return nil, zerrors.ThrowInternal(err, "QUERY-Iuk0s", "Errors.Internal") + } + return webKey, nil +} + +type WebKeyDetails struct { + KeyID string + CreationDate time.Time + ChangeDate time.Time + Sequence int64 + State domain.WebKeyState + Config crypto.WebKeyConfig +} + +type WebKeyList struct { + Keys []WebKeyDetails +} + +// ListWebKeys gets a list of [WebKeyDetails] for the complete instance from the web_keys projection. +// The list is eventual consistent. +func (q *Queries) ListWebKeys(ctx context.Context) (list []WebKeyDetails, 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 ( + configData []byte + configType crypto.WebKeyConfigType + ) + var details WebKeyDetails + if err = rows.Scan( + &details.KeyID, + &details.CreationDate, + &details.ChangeDate, + &details.Sequence, + &details.State, + &configData, + &configType, + ); err != nil { + return err + } + details.Config, err = crypto.UnmarshalWebKeyConfig(configData, configType) + if err != nil { + return err + } + list = append(list, details) + } + return rows.Err() + }, + webKeyListQuery, + authz.GetInstance(ctx).InstanceID(), + ) + if err != nil { + return nil, zerrors.ThrowInternal(err, "QUERY-Ohl3A", "Errors.Internal") + } + return list, nil +} + +// GetWebKeySet gets a JSON Web Key set from the web_keys projection. +// The set contains all existing public keys for the instance. +// The set is eventual consistent. +func (q *Queries) GetWebKeySet(ctx context.Context) (_ *jose.JSONWebKeySet, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + var keys []jose.JSONWebKey + + err = q.client.QueryContext(ctx, func(rows *sql.Rows) error { + for rows.Next() { + var webKeyData []byte + if err = rows.Scan(&webKeyData); err != nil { + return err + } + var webKey jose.JSONWebKey + if err = json.Unmarshal(webKeyData, &webKey); err != nil { + return err + } + keys = append(keys, webKey) + } + return rows.Err() + }, + webKeyPublicKeysQuery, + authz.GetInstance(ctx).InstanceID(), + ) + if err != nil { + return nil, zerrors.ThrowInternal(err, "QUERY-Eeng7", "Errors.Internal") + } + return &jose.JSONWebKeySet{Keys: keys}, nil +} diff --git a/internal/query/web_key_by_state.sql b/internal/query/web_key_by_state.sql new file mode 100644 index 0000000000..3d7875477f --- /dev/null +++ b/internal/query/web_key_by_state.sql @@ -0,0 +1,5 @@ +select private_key +from projections.web_keys +where instance_id = $1 +and state = $2 +limit 1; diff --git a/internal/query/web_key_list.sql b/internal/query/web_key_list.sql new file mode 100644 index 0000000000..1671e55eef --- /dev/null +++ b/internal/query/web_key_list.sql @@ -0,0 +1,4 @@ +select key_id, creation_date, change_date, sequence, state, config, config_type +from projections.web_keys +where instance_id = $1 +order by creation_date asc; diff --git a/internal/query/web_key_model.go b/internal/query/web_key_model.go new file mode 100644 index 0000000000..117f2ba202 --- /dev/null +++ b/internal/query/web_key_model.go @@ -0,0 +1,74 @@ +package query + +import ( + "github.com/go-jose/go-jose/v4" + + "github.com/zitadel/zitadel/internal/crypto" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/repository/webkey" +) + +type WebKeyReadModel struct { + eventstore.ReadModel + State domain.WebKeyState + PrivateKey *crypto.CryptoValue + PublicKey *jose.JSONWebKey + Config crypto.WebKeyConfig +} + +func NewWebKeyReadModel(keyID, resourceOwner string) *WebKeyReadModel { + return &WebKeyReadModel{ + ReadModel: eventstore.ReadModel{ + AggregateID: keyID, + ResourceOwner: resourceOwner, + }, + } +} + +func (wm *WebKeyReadModel) AppendEvents(events ...eventstore.Event) { + wm.ReadModel.AppendEvents(events...) +} + +func (wm *WebKeyReadModel) Reduce() error { + for _, event := range wm.Events { + switch e := event.(type) { + case *webkey.AddedEvent: + if err := wm.reduceAdded(e); err != nil { + return err + } + case *webkey.ActivatedEvent: + wm.State = domain.WebKeyStateActive + case *webkey.DeactivatedEvent: + wm.State = domain.WebKeyStateInactive + case *webkey.RemovedEvent: + wm.State = domain.WebKeyStateRemoved + wm.PrivateKey = nil + wm.PublicKey = nil + } + } + return wm.ReadModel.Reduce() +} + +func (wm *WebKeyReadModel) reduceAdded(e *webkey.AddedEvent) (err error) { + wm.State = domain.WebKeyStateInitial + wm.PrivateKey = e.PrivateKey + wm.PublicKey = e.PublicKey + wm.Config, err = crypto.UnmarshalWebKeyConfig(e.Config, e.ConfigType) + return err +} + +func (wm *WebKeyReadModel) Query() *eventstore.SearchQueryBuilder { + return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). + ResourceOwner(wm.ResourceOwner). + AddQuery(). + AggregateTypes(webkey.AggregateType). + AggregateIDs(wm.AggregateID). + EventTypes( + webkey.AddedEventType, + webkey.ActivatedEventType, + webkey.DeactivatedEventType, + webkey.RemovedEventType, + ). + Builder() +} diff --git a/internal/query/web_key_public_keys.sql b/internal/query/web_key_public_keys.sql new file mode 100644 index 0000000000..e59ca6174a --- /dev/null +++ b/internal/query/web_key_public_keys.sql @@ -0,0 +1,3 @@ +select public_key +from projections.web_keys +where instance_id = $1; diff --git a/internal/query/web_key_test.go b/internal/query/web_key_test.go new file mode 100644 index 0000000000..6008ec6528 --- /dev/null +++ b/internal/query/web_key_test.go @@ -0,0 +1,382 @@ +package query + +import ( + "context" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "database/sql" + "database/sql/driver" + "encoding/json" + "io" + "regexp" + "strconv" + "testing" + "time" + + "github.com/go-jose/go-jose/v4" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/crypto" + "github.com/zitadel/zitadel/internal/database" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/repository/webkey" + "github.com/zitadel/zitadel/internal/zerrors" +) + +func TestQueries_GetPublicWebKeyByID(t *testing.T) { + ctx := authz.NewMockContextWithPermissions("instance1", "org1", "user1", nil) + key, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader) + require.NoError(t, err) + + type fields struct { + eventstore func(*testing.T) *eventstore.Eventstore + } + type args struct { + keyID string + } + tests := []struct { + name string + fields fields + args args + want *jose.JSONWebKey + wantErr error + }{ + { + name: "filter error", + fields: fields{ + eventstore: expectEventstore( + expectFilterError(io.ErrClosedPipe), + ), + }, + args: args{"key1"}, + wantErr: io.ErrClosedPipe, + }, + { + name: "not found error", + fields: fields{ + eventstore: expectEventstore( + expectFilter(), + ), + }, + args: args{"key1"}, + wantErr: zerrors.ThrowNotFound(nil, "QUERY-AiCh0", "Errors.WebKey.NotFound"), + }, + { + name: "removed, not found error", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher(mustNewWebkeyAddedEvent(ctx, + webkey.NewAggregate("key1", "instance1"), + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "alg", + KeyID: "encKey", + Crypted: []byte("crypted"), + }, + &jose.JSONWebKey{ + Key: &key.PublicKey, + KeyID: "key1", + Algorithm: string(jose.ES384), + Use: crypto.KeyUsageSigning.String(), + }, + &crypto.WebKeyECDSAConfig{ + Curve: crypto.EllipticCurveP384, + }, + )), + eventFromEventPusher(webkey.NewRemovedEvent(ctx, + webkey.NewAggregate("key1", "instance1"), + )), + ), + ), + }, + args: args{"key1"}, + wantErr: zerrors.ThrowNotFound(nil, "QUERY-AiCh0", "Errors.WebKey.NotFound"), + }, + { + name: "ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher(mustNewWebkeyAddedEvent(ctx, + webkey.NewAggregate("key1", "instance1"), + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "alg", + KeyID: "encKey", + Crypted: []byte("crypted"), + }, + &jose.JSONWebKey{ + Key: &key.PublicKey, + KeyID: "key1", + Algorithm: string(jose.ES384), + Use: crypto.KeyUsageSigning.String(), + }, + &crypto.WebKeyECDSAConfig{ + Curve: crypto.EllipticCurveP384, + }, + )), + ), + ), + }, + args: args{"key1"}, + want: &jose.JSONWebKey{ + Key: &key.PublicKey, + KeyID: "key1", + Algorithm: string(jose.ES384), + Use: crypto.KeyUsageSigning.String(), + Certificates: []*x509.Certificate{}, + CertificateThumbprintSHA1: []byte{}, + CertificateThumbprintSHA256: []byte{}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + q := &Queries{ + eventstore: tt.fields.eventstore(t), + } + got, err := q.GetPublicWebKeyByID(ctx, tt.args.keyID) + require.ErrorIs(t, err, tt.wantErr) + assert.Equal(t, tt.want, got) + }) + } +} + +func mustNewWebkeyAddedEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, + privateKey *crypto.CryptoValue, + publicKey *jose.JSONWebKey, + config crypto.WebKeyConfig) *webkey.AddedEvent { + event, err := webkey.NewAddedEvent(ctx, aggregate, privateKey, publicKey, config) + if err != nil { + panic(err) + } + return event +} + +func TestQueries_GetActiveSigningWebKey(t *testing.T) { + ctx := authz.NewMockContextWithPermissions("instance1", "org1", "user1", nil) + expQuery := regexp.QuoteMeta(webKeyByStateQuery) + queryArgs := []driver.Value{"instance1", domain.WebKeyStateActive} + cols := []string{"private_key"} + + alg := crypto.CreateMockEncryptionAlg(gomock.NewController(t)) + encryptedPrivate, _, err := crypto.GenerateEncryptedWebKey("key1", alg, &crypto.WebKeyED25519Config{}) + require.NoError(t, err) + + var expectedWebKey *jose.JSONWebKey + err = crypto.DecryptJSON(encryptedPrivate, &expectedWebKey, alg) + require.NoError(t, err) + + tests := []struct { + name string + mock sqlExpectation + want *jose.JSONWebKey + wantErr error + }{ + { + name: "no active error", + mock: mockQueryErr(expQuery, sql.ErrNoRows, queryArgs...), + wantErr: zerrors.ThrowInternal(sql.ErrNoRows, "QUERY-Opoh7", "Errors.WebKey.NoActive"), + }, + { + name: "internal error", + mock: mockQueryErr(expQuery, sql.ErrConnDone, queryArgs...), + wantErr: zerrors.ThrowInternal(sql.ErrConnDone, "QUERY-Shoo0", "Errors.Internal"), + }, + { + name: "invalid crypto value error", + mock: mockQuery(expQuery, cols, []driver.Value{&crypto.CryptoValue{}}, queryArgs...), + wantErr: zerrors.ThrowInvalidArgument(nil, "CRYPT-Nx7XlT", "value was encrypted with a different key"), + }, + { + name: "found, ok", + mock: mockQuery(expQuery, cols, []driver.Value{encryptedPrivate}, queryArgs...), + want: expectedWebKey, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + execMock(t, tt.mock, func(db *sql.DB) { + q := &Queries{ + client: &database.DB{ + DB: db, + Database: &prepareDB{}, + }, + keyEncryptionAlgorithm: alg, + } + got, err := q.GetActiveSigningWebKey(ctx) + require.ErrorIs(t, err, tt.wantErr) + assert.Equal(t, tt.want, got) + }) + }) + } +} + +func TestQueries_ListWebKeys(t *testing.T) { + ctx := authz.NewMockContextWithPermissions("instance1", "org1", "user1", nil) + expQuery := regexp.QuoteMeta(webKeyListQuery) + queryArgs := []driver.Value{"instance1"} + cols := []string{"key_id", "creation_date", "change_date", "sequence", "state", "config", "config_type"} + + webKeyConfig := &crypto.WebKeyRSAConfig{ + Bits: crypto.RSABits4096, + Hasher: crypto.RSAHasherSHA512, + } + webKeyConfigJSON, err := json.Marshal(webKeyConfig) + require.NoError(t, err) + + tests := []struct { + name string + mock sqlExpectation + want []WebKeyDetails + wantErr error + }{ + { + name: "internal error", + mock: mockQueryErr(expQuery, sql.ErrConnDone, queryArgs...), + wantErr: zerrors.ThrowInternal(sql.ErrConnDone, "QUERY-Ohl3A", "Errors.Internal"), + }, + { + name: "invalid json error", + mock: mockQueriesScanErr(expQuery, cols, [][]driver.Value{ + { + "key1", + time.Unix(1, 2), + time.Unix(3, 4), + 1, + domain.WebKeyStateActive, + "~~~~~", + crypto.WebKeyConfigTypeRSA, + }, + }, queryArgs...), + wantErr: zerrors.ThrowInternal(err, "QUERY-Ohl3A", "Errors.Internal"), + }, + { + name: "ok", + mock: mockQueries(expQuery, cols, [][]driver.Value{ + { + "key1", + time.Unix(1, 2), + time.Unix(3, 4), + 1, + domain.WebKeyStateActive, + webKeyConfigJSON, + crypto.WebKeyConfigTypeRSA, + }, + { + "key2", + time.Unix(5, 6), + time.Unix(7, 8), + 2, + domain.WebKeyStateInitial, + webKeyConfigJSON, + crypto.WebKeyConfigTypeRSA, + }, + }, queryArgs...), + want: []WebKeyDetails{ + { + KeyID: "key1", + CreationDate: time.Unix(1, 2), + ChangeDate: time.Unix(3, 4), + Sequence: 1, + State: domain.WebKeyStateActive, + Config: webKeyConfig, + }, + { + KeyID: "key2", + CreationDate: time.Unix(5, 6), + ChangeDate: time.Unix(7, 8), + Sequence: 2, + State: domain.WebKeyStateInitial, + Config: webKeyConfig, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + execMock(t, tt.mock, func(db *sql.DB) { + q := &Queries{ + client: &database.DB{ + DB: db, + Database: &prepareDB{}, + }, + } + got, err := q.ListWebKeys(ctx) + require.ErrorIs(t, err, tt.wantErr) + assert.Equal(t, tt.want, got) + }) + }) + } +} + +func TestQueries_GetWebKeySet(t *testing.T) { + ctx := authz.NewMockContextWithPermissions("instance1", "org1", "user1", nil) + expQuery := regexp.QuoteMeta(webKeyPublicKeysQuery) + queryArgs := []driver.Value{"instance1"} + cols := []string{"public_key"} + + alg := crypto.CreateMockEncryptionAlg(gomock.NewController(t)) + conf := &crypto.WebKeyED25519Config{} + expectedKeySet := &jose.JSONWebKeySet{ + Keys: make([]jose.JSONWebKey, 3), + } + expectedRows := make([][]driver.Value, 3) + + for i := 0; i < 3; i++ { + _, pubKey, err := crypto.GenerateEncryptedWebKey(strconv.Itoa(i), alg, conf) + require.NoError(t, err) + pubKeyJSON, err := json.Marshal(pubKey) + require.NoError(t, err) + err = json.Unmarshal(pubKeyJSON, &expectedKeySet.Keys[i]) + require.NoError(t, err) + expectedRows[i] = []driver.Value{pubKeyJSON} + } + + tests := []struct { + name string + mock sqlExpectation + want *jose.JSONWebKeySet + wantErr error + }{ + { + name: "internal error", + mock: mockQueryErr(expQuery, sql.ErrConnDone, queryArgs...), + wantErr: zerrors.ThrowInternal(sql.ErrConnDone, "QUERY-Eeng7", "Errors.Internal"), + }, + { + name: "invalid json error", + mock: mockQueriesScanErr(expQuery, cols, [][]driver.Value{{"~~~"}}, queryArgs...), + wantErr: zerrors.ThrowInternal(nil, "QUERY-Eeng7", "Errors.Internal"), + }, + { + name: "ok", + mock: mockQueries(expQuery, cols, expectedRows, queryArgs...), + want: expectedKeySet, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + execMock(t, tt.mock, func(db *sql.DB) { + q := &Queries{ + client: &database.DB{ + DB: db, + Database: &prepareDB{}, + }, + } + got, err := q.GetWebKeySet(ctx) + require.ErrorIs(t, err, tt.wantErr) + assert.Equal(t, tt.want, got) + }) + }) + } +} diff --git a/internal/repository/feature/feature_v2/eventstore.go b/internal/repository/feature/feature_v2/eventstore.go index bd8b22eec8..97b4e4ed3a 100644 --- a/internal/repository/feature/feature_v2/eventstore.go +++ b/internal/repository/feature/feature_v2/eventstore.go @@ -23,4 +23,5 @@ func init() { eventstore.RegisterFilterEventMapper(AggregateType, InstanceTokenExchangeEventType, eventstore.GenericEventMapper[SetEvent[bool]]) eventstore.RegisterFilterEventMapper(AggregateType, InstanceActionsEventType, eventstore.GenericEventMapper[SetEvent[bool]]) eventstore.RegisterFilterEventMapper(AggregateType, InstanceImprovedPerformanceEventType, eventstore.GenericEventMapper[SetEvent[[]feature.ImprovedPerformanceType]]) + eventstore.RegisterFilterEventMapper(AggregateType, InstanceWebKeyEventType, eventstore.GenericEventMapper[SetEvent[bool]]) } diff --git a/internal/repository/feature/feature_v2/feature.go b/internal/repository/feature/feature_v2/feature.go index d5beea8bf4..27e1ed40fc 100644 --- a/internal/repository/feature/feature_v2/feature.go +++ b/internal/repository/feature/feature_v2/feature.go @@ -28,6 +28,7 @@ var ( InstanceTokenExchangeEventType = setEventTypeFromFeature(feature.LevelInstance, feature.KeyTokenExchange) InstanceActionsEventType = setEventTypeFromFeature(feature.LevelInstance, feature.KeyActions) InstanceImprovedPerformanceEventType = setEventTypeFromFeature(feature.LevelInstance, feature.KeyImprovedPerformance) + InstanceWebKeyEventType = setEventTypeFromFeature(feature.LevelInstance, feature.KeyWebKey) ) const ( diff --git a/internal/repository/keypair/key_pair.go b/internal/repository/keypair/key_pair.go index 8bf2e77080..aeb02bb06e 100644 --- a/internal/repository/keypair/key_pair.go +++ b/internal/repository/keypair/key_pair.go @@ -5,7 +5,6 @@ import ( "time" "github.com/zitadel/zitadel/internal/crypto" - "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/zerrors" ) @@ -18,7 +17,7 @@ const ( type AddedEvent struct { eventstore.BaseEvent `json:"-"` - Usage domain.KeyUsage `json:"usage"` + Usage crypto.KeyUsage `json:"usage"` Algorithm string `json:"algorithm"` PrivateKey *Key `json:"privateKey"` PublicKey *Key `json:"publicKey"` @@ -40,7 +39,7 @@ func (e *AddedEvent) UniqueConstraints() []*eventstore.UniqueConstraint { func NewAddedEvent( ctx context.Context, aggregate *eventstore.Aggregate, - usage domain.KeyUsage, + usage crypto.KeyUsage, algorithm string, privateCrypto, publicCrypto *crypto.CryptoValue, diff --git a/internal/repository/webkey/aggregate.go b/internal/repository/webkey/aggregate.go new file mode 100644 index 0000000000..afe63aaee5 --- /dev/null +++ b/internal/repository/webkey/aggregate.go @@ -0,0 +1,25 @@ +package webkey + +import ( + "context" + + "github.com/zitadel/zitadel/internal/eventstore" +) + +const ( + AggregateType = "web_key" + AggregateVersion = "v1" +) + +func NewAggregate(id, resourceOwner string) *eventstore.Aggregate { + return &eventstore.Aggregate{ + Type: AggregateType, + Version: AggregateVersion, + ID: id, + ResourceOwner: resourceOwner, + } +} + +func AggregateFromWriteModel(ctx context.Context, wm *eventstore.WriteModel) *eventstore.Aggregate { + return eventstore.AggregateFromWriteModelCtx(ctx, wm, AggregateType, AggregateVersion) +} diff --git a/internal/repository/webkey/eventstore.go b/internal/repository/webkey/eventstore.go new file mode 100644 index 0000000000..d02d9b1c43 --- /dev/null +++ b/internal/repository/webkey/eventstore.go @@ -0,0 +1,12 @@ +package webkey + +import ( + "github.com/zitadel/zitadel/internal/eventstore" +) + +func init() { + eventstore.RegisterFilterEventMapper(AggregateType, AddedEventType, eventstore.GenericEventMapper[AddedEvent]) + eventstore.RegisterFilterEventMapper(AggregateType, ActivatedEventType, eventstore.GenericEventMapper[ActivatedEvent]) + eventstore.RegisterFilterEventMapper(AggregateType, DeactivatedEventType, eventstore.GenericEventMapper[DeactivatedEvent]) + eventstore.RegisterFilterEventMapper(AggregateType, RemovedEventType, eventstore.GenericEventMapper[RemovedEvent]) +} diff --git a/internal/repository/webkey/webkey.go b/internal/repository/webkey/webkey.go new file mode 100644 index 0000000000..e5e3c5f020 --- /dev/null +++ b/internal/repository/webkey/webkey.go @@ -0,0 +1,160 @@ +package webkey + +import ( + "context" + "encoding/json" + + "github.com/go-jose/go-jose/v4" + + "github.com/zitadel/zitadel/internal/crypto" + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/zerrors" +) + +const ( + UniqueWebKeyType = "web_key" +) + +const ( + eventTypePrefix = eventstore.EventType("web_key.") + AddedEventType = eventTypePrefix + "added" + ActivatedEventType = eventTypePrefix + "activated" + DeactivatedEventType = eventTypePrefix + "deactivated" + RemovedEventType = eventTypePrefix + "removed" +) + +type AddedEvent struct { + *eventstore.BaseEvent `json:"-"` + + PrivateKey *crypto.CryptoValue `json:"privateKey"` + PublicKey *jose.JSONWebKey `json:"publicKey"` + Config json.RawMessage `json:"config"` + ConfigType crypto.WebKeyConfigType `json:"configType"` +} + +func (e *AddedEvent) Payload() interface{} { + return e +} + +func (e *AddedEvent) UniqueConstraints() []*eventstore.UniqueConstraint { + return []*eventstore.UniqueConstraint{ + eventstore.NewAddEventUniqueConstraint(UniqueWebKeyType, e.Agg.ID, "Errors.WebKey.Duplicate"), + } +} + +func (e *AddedEvent) SetBaseEvent(base *eventstore.BaseEvent) { + e.BaseEvent = base +} + +func NewAddedEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, + privateKey *crypto.CryptoValue, + publicKey *jose.JSONWebKey, + config crypto.WebKeyConfig, +) (*AddedEvent, error) { + configJson, err := json.Marshal(config) + if err != nil { + return nil, zerrors.ThrowInternal(err, "WEBKEY-IY9fa", "Errors.Internal") + } + return &AddedEvent{ + BaseEvent: eventstore.NewBaseEventForPush( + ctx, + aggregate, + AddedEventType, + ), + PrivateKey: privateKey, + PublicKey: publicKey, + Config: configJson, + ConfigType: config.Type(), + }, nil +} + +type ActivatedEvent struct { + *eventstore.BaseEvent `json:"-"` +} + +func (e *ActivatedEvent) Payload() interface{} { + return e +} + +func (e *ActivatedEvent) UniqueConstraints() []*eventstore.UniqueConstraint { + return nil +} + +func (e *ActivatedEvent) SetBaseEvent(base *eventstore.BaseEvent) { + e.BaseEvent = base +} + +func NewActivatedEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, +) *ActivatedEvent { + return &ActivatedEvent{ + BaseEvent: eventstore.NewBaseEventForPush( + ctx, + aggregate, + ActivatedEventType, + ), + } +} + +type DeactivatedEvent struct { + *eventstore.BaseEvent `json:"-"` +} + +func (e *DeactivatedEvent) Payload() interface{} { + return e +} + +func (e *DeactivatedEvent) UniqueConstraints() []*eventstore.UniqueConstraint { + return nil +} + +func (e *DeactivatedEvent) SetBaseEvent(base *eventstore.BaseEvent) { + e.BaseEvent = base +} + +func NewDeactivatedEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, +) *DeactivatedEvent { + return &DeactivatedEvent{ + BaseEvent: eventstore.NewBaseEventForPush( + ctx, + aggregate, + DeactivatedEventType, + ), + } +} + +type RemovedEvent struct { + *eventstore.BaseEvent `json:"-"` +} + +func (e *RemovedEvent) Payload() interface{} { + return e +} + +func (e *RemovedEvent) UniqueConstraints() []*eventstore.UniqueConstraint { + return []*eventstore.UniqueConstraint{ + eventstore.NewRemoveUniqueConstraint(UniqueWebKeyType, e.Agg.ID), + } +} + +func (e *RemovedEvent) SetBaseEvent(base *eventstore.BaseEvent) { + e.BaseEvent = base +} + +func NewRemovedEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, +) *RemovedEvent { + return &RemovedEvent{ + BaseEvent: eventstore.NewBaseEventForPush( + ctx, + aggregate, + RemovedEventType, + ), + } +} diff --git a/internal/static/i18n/bg.yaml b/internal/static/i18n/bg.yaml index fd7bfee660..7f868b3c35 100644 --- a/internal/static/i18n/bg.yaml +++ b/internal/static/i18n/bg.yaml @@ -603,6 +603,13 @@ Errors: NotForAPI: Имитирани токени не са разрешени за API Impersonation: PolicyDisabled: Имитирането е деактивирано в политиката за сигурност на екземпляра + WebKey: + ActiveDelete: Не може да се изтрие активен уеб ключ + Config: Невалидна конфигурация на уеб ключ + Duplicate: ID на уеб ключ не е уникален + FeatureDisabled: Ключовата уеб функция е деактивирана + NoActive: Не е намерен активен уеб ключ + NotFound: Уеб ключът не е намерен AggregateTypes: action: Действие @@ -626,6 +633,7 @@ AggregateTypes: restrictions: Ограничения system: Система session: Сесия + web_key: Уеб ключ EventTypes: execution: @@ -1342,6 +1350,12 @@ EventTypes: deactivated: Потребителската схема е деактивирана reactivated: Потребителската схема е активирана отново deleted: Потребителската схема е изтрита + web_key: + added: Добавен уеб ключ + activated: Уеб ключът е активиран + deactivated: Уеб ключът е деактивиран + removed: Уеб ключът е премахнат + Application: OIDC: UnsupportedVersion: Вашата OIDC версия не се поддържа diff --git a/internal/static/i18n/cs.yaml b/internal/static/i18n/cs.yaml index 986bc6327f..2baf69411c 100644 --- a/internal/static/i18n/cs.yaml +++ b/internal/static/i18n/cs.yaml @@ -584,6 +584,13 @@ Errors: NotForAPI: Zosobněné tokeny nejsou pro API povoleny Impersonation: PolicyDisabled: Zosobnění je zakázáno v zásadách zabezpečení instance + WebKey: + ActiveDelete: Aktivní webový klíč nelze smazat + Config: Neplatná konfigurace webového klíče + Duplicate: ID webového klíče není jedinečné + FeatureDisabled: Funkce webového klíče je zakázána + NoActive: Nebyl nalezen žádný aktivní webový klíč + NotFound: Webový klíč nebyl nalezen AggregateTypes: action: Akce @@ -607,6 +614,7 @@ AggregateTypes: restrictions: Omezení system: Systém session: Sezení + web_key: Webový klíč EventTypes: execution: @@ -1308,6 +1316,11 @@ EventTypes: deactivated: Uživatelské schéma deaktivováno reactivated: Uživatelské schéma bylo znovu aktivováno deleted: Uživatelské schéma bylo smazáno + web_key: + added: Přidán webový klíč + activated: Web Key aktivován + deactivated: Web Key deaktivován + removed: Odstraňte webový klíč Application: OIDC: diff --git a/internal/static/i18n/de.yaml b/internal/static/i18n/de.yaml index ec7d5976b5..fb9ab8e21f 100644 --- a/internal/static/i18n/de.yaml +++ b/internal/static/i18n/de.yaml @@ -586,6 +586,13 @@ Errors: NotForAPI: Imitierte Token sind für die API nicht zulässig Impersonation: PolicyDisabled: Der Identitätswechsel ist in der Sicherheitsrichtlinie der Instanz deaktiviert + WebKey: + ActiveDelete: Aktiver Webschlüssel kann nicht gelöscht werden + Config: Ungültige Webschlüsselkonfiguration + Duplicate: Webschlüssel-ID nicht eindeutig + FeatureDisabled: Webschlüsselfunktion deaktiviert + NoActive: Kein aktiver Webschlüssel gefunden + NotFound: Webschlüssel nicht gefunden AggregateTypes: action: Action @@ -609,6 +616,7 @@ AggregateTypes: restrictions: Restriktionen system: System session: Session + web_key: Webschlüssel EventTypes: execution: @@ -1310,6 +1318,11 @@ EventTypes: deactivated: Benutzerschema deaktiviert reactivated: Benutzerschema reaktiviert deleted: Benutzerschema gelöscht + web_key: + added: Web Key hinzugefügt + activated: Web Key aktiviert + deactivated: Web Key deaktiviert + removed: Web Key entfernen Application: OIDC: diff --git a/internal/static/i18n/en.yaml b/internal/static/i18n/en.yaml index b1bf5907cf..0b1908fc8c 100644 --- a/internal/static/i18n/en.yaml +++ b/internal/static/i18n/en.yaml @@ -586,6 +586,13 @@ Errors: NotForAPI: Impersonated tokens not allowed for API Impersonation: PolicyDisabled: Impersonation is disabled in the instance security policy + WebKey: + ActiveDelete: Cannot delete active web key + Config: Invalid web key config + Duplicate: Web key ID not unique + FeatureDisabled: Web key feature disabled + NoActive: No active web key found + NotFound: Web key not found AggregateTypes: @@ -610,6 +617,7 @@ AggregateTypes: restrictions: Restrictions system: System session: Session + web_key: Web Key EventTypes: execution: @@ -1311,6 +1319,11 @@ EventTypes: deactivated: User Schema deactivated reactivated: User Schema reactivated deleted: User Schema deleted + web_key: + added: Web Key added + activated: Web Key activated + deactivated: Web Key deactivated + removed: Web Key removed Application: OIDC: diff --git a/internal/static/i18n/es.yaml b/internal/static/i18n/es.yaml index cec5cfedd1..5ab5ee454b 100644 --- a/internal/static/i18n/es.yaml +++ b/internal/static/i18n/es.yaml @@ -586,6 +586,13 @@ Errors: NotForAPI: Tokens suplantados no permitidos para API Impersonation: PolicyDisabled: La suplantación está deshabilitada en la política de seguridad de la instancia. + WebKey: + ActiveDelete: No se puede eliminar la clave web activa + Config: Configuración de clave web no válida + Duplicate: ID de clave web no único + FeatureDisabled: Función de clave web deshabilitada + NoActive: No se encontró ninguna clave web activa + NotFound: Clave web no encontrada AggregateTypes: action: Acción @@ -609,6 +616,7 @@ AggregateTypes: restrictions: Restricciones system: Sistema session: Sesión + web_key: Clave web EventTypes: execution: @@ -1310,6 +1318,11 @@ EventTypes: deactivated: Esquema de usuario desactivado reactivated: Esquema de usuario reactivado deleted: Esquema de usuario eliminado + web_key: + added: Clave web añadida + activated: Clave web activada + deactivated: Clave web desactivada + removed: Clave web eliminada Application: OIDC: diff --git a/internal/static/i18n/fr.yaml b/internal/static/i18n/fr.yaml index 9871725704..79f1333956 100644 --- a/internal/static/i18n/fr.yaml +++ b/internal/static/i18n/fr.yaml @@ -586,6 +586,13 @@ Errors: NotForAPI: Les jetons usurpés d'identité ne sont pas autorisés pour l'API Impersonation: PolicyDisabled: L'usurpation d'identité est désactivée dans la politique de sécurité de l'instance + WebKey: + ActiveDelete: Impossible de supprimer la clé Web active + Config: Configuration de clé Web non valide + Duplicate: L'ID de clé Web n'est pas unique + FeatureDisabled: Fonctionnalité de clé Web désactivée + NoActive: Aucune clé Web active trouvée + NotFound: Clé Web introuvable AggregateTypes: action: Action @@ -609,6 +616,7 @@ AggregateTypes: restrictions: Restrictions system: Système session: Session + web_key: Clé Web EventTypes: execution: @@ -1171,140 +1179,146 @@ EventTypes: deactivated: Action désactivée reactivated: Action réactivée removed: Action supprimée + instance: + added: Instance ajoutée + changed: Instance modifiée + customtext: + removed: Texte personnalisé supprimé + set: Ensemble de texte personnalisé + template: + removed: Modèle de texte personnalisé supprimé + default: + language: + set: Langue par défaut définie + org: + set: Ensemble d'organisation par défaut + domain: + added: Domaine ajouté + primary: + set: Ensemble de domaines principal + removed: Domaine supprimé + iam: + console: + set: Ensemble d'applications Console ZITADEL + project: + set: ZITADEL project set + mail: + template: + added: Modèle de courrier électronique ajouté + changed: Modèle d'e-mail modifié + text: + added: Texte de l'e-mail ajouté + changed: Le texte de l'e-mail a été modifié + member: + added: Membre de l'instance ajouté + changed: Membre de l'instance modifié + removed: Membre de l'instance supprimé + cascade: + removed: Cascade de membres de l'instance supprimée + notification: + provider: + debug: + fileadded: Fournisseur de notification de débogage de fichiers ajouté + filechanged: Le fournisseur de notification de débogage de fichier a été modifié + fileremoved: Fournisseur de notification de débogage de fichier supprimé + logadded: Fournisseur de notification de débogage de journal ajouté + logchanged: Le fournisseur de notification de débogage du journal a été modifié + logremoved: Fournisseur de notification de débogage du journal supprimé + oidc: + settings: + added: Paramètres OIDC ajoutés + changed: Paramètres OIDC modifiés + policy: + domain: + added: Politique de domaine ajoutée + changed: Politique de domaine modifiée + label: + activated: Politique d'étiquetage activée + added: Politique d'étiquetage ajoutée + assets: + removed: L'élément de la stratégie d'étiquette a été supprimé + changed: Politique d'étiquetage modifiée + font: + added: Police ajoutée à la stratégie d'étiquette + removed: Police supprimée de la stratégie relative aux étiquettes + icon: + added: Icône ajoutée à la politique d'étiquetage + removed: Icône supprimée des règles relatives aux étiquettes + dark: + added: Icône ajoutée à la politique d'étiquette sombre + removed: Icône supprimée de la politique relative aux étiquettes sombres + logo: + added: Logo ajouté à la politique d'étiquetage + removed: Logo supprimé de la politique relative aux étiquettes + dark: + added: Logo ajouté à la politique relative aux étiquettes sombres + removed: Logo supprimé de la politique relative aux étiquettes sombres + lockout: + added: Politique de verrouillage ajoutée + changed: La politique de verrouillage a été modifiée + login: + added: Politique de connexion ajoutée + changed: Politique de connexion modifiée + idpprovider: + added: Fournisseur d'identité ajouté à la politique de connexion + cascade: + removed: Cascade de fournisseurs d'identité supprimée de la stratégie de connexion + removed: Fournisseur d'identité supprimé de la stratégie de connexion + multifactor: + added: Multifactor ajouté à la politique de connexion + removed: Multifactor supprimé de la politique de connexion + secondfactor: + added: Deuxième facteur ajouté à la politique de connexion + removed: Deuxième facteur supprimé de la politique de connexion + password: + age: + added: Politique d'âge du mot de passe ajoutée + changed: La politique relative à l'âge du mot de passe a été modifiée + complexity: + added: Politique de complexité des mots de passe ajoutée + changed: Politique de complexité des mots de passe supprimée + privacy: + added: Politique de confidentialité ajoutée + changed: Politique de confidentialité modifiée + security: + set: Ensemble de règles de sécurité + + removed: Instance removed + secret: + generator: + added: Générateur de secrets ajouté + changed: Le générateur de secrets a changé + removed: Générateur de secrets supprimé + sms: + configtwilio: + activated: Configuration SMS Twilio activée + added: Configuration SMS Twilio ajoutée + changed: La configuration des SMS Twilio a été modifiée + deactivated: Configuration SMS Twilio désactivée + removed: Configuration SMS Twilio supprimée + token: + changed: Jeton de configuration SMS Twilio modifié + smtp: + config: + added: Configuration SMTP ajoutée + changed: Configuration SMTP modifiée + activated: Configuration SMTP activée + deactivated: Configuration SMTP désactivée + password: + changed: Mot de passe de configuration SMTP modifié + removed: Configuration SMTP supprimée user_schema: created: Schéma utilisateur créé updated: Schéma utilisateur mis à jour deactivated: Schéma utilisateur désactivé reactivated: Schéma utilisateur réactivé deleted: Schéma utilisateur supprimé -instance: - added: Instance ajoutée - changed: Instance modifiée - customtext: - removed: Texte personnalisé supprimé - set: Ensemble de texte personnalisé - template: - removed: Modèle de texte personnalisé supprimé - default: - language: - set: Langue par défaut définie - org: - set: Ensemble d'organisation par défaut - domain: - added: Domaine ajouté - primary: - set: Ensemble de domaines principal - removed: Domaine supprimé - iam: - console: - set: Ensemble d'applications Console ZITADEL - project: - set: ZITADEL project set - mail: - template: - added: Modèle de courrier électronique ajouté - changed: Modèle d'e-mail modifié - text: - added: Texte de l'e-mail ajouté - changed: Le texte de l'e-mail a été modifié - member: - added: Membre de l'instance ajouté - changed: Membre de l'instance modifié - removed: Membre de l'instance supprimé - cascade: - removed: Cascade de membres de l'instance supprimée - notification: - provider: - debug: - fileadded: Fournisseur de notification de débogage de fichiers ajouté - filechanged: Le fournisseur de notification de débogage de fichier a été modifié - fileremoved: Fournisseur de notification de débogage de fichier supprimé - logadded: Fournisseur de notification de débogage de journal ajouté - logchanged: Le fournisseur de notification de débogage du journal a été modifié - logremoved: Fournisseur de notification de débogage du journal supprimé - oidc: - settings: - added: Paramètres OIDC ajoutés - changed: Paramètres OIDC modifiés - policy: - domain: - added: Politique de domaine ajoutée - changed: Politique de domaine modifiée - label: - activated: Politique d'étiquetage activée - added: Politique d'étiquetage ajoutée - assets: - removed: L'élément de la stratégie d'étiquette a été supprimé - changed: Politique d'étiquetage modifiée - font: - added: Police ajoutée à la stratégie d'étiquette - removed: Police supprimée de la stratégie relative aux étiquettes - icon: - added: Icône ajoutée à la politique d'étiquetage - removed: Icône supprimée des règles relatives aux étiquettes - dark: - added: Icône ajoutée à la politique d'étiquette sombre - removed: Icône supprimée de la politique relative aux étiquettes sombres - logo: - added: Logo ajouté à la politique d'étiquetage - removed: Logo supprimé de la politique relative aux étiquettes - dark: - added: Logo ajouté à la politique relative aux étiquettes sombres - removed: Logo supprimé de la politique relative aux étiquettes sombres - lockout: - added: Politique de verrouillage ajoutée - changed: La politique de verrouillage a été modifiée - login: - added: Politique de connexion ajoutée - changed: Politique de connexion modifiée - idpprovider: - added: Fournisseur d'identité ajouté à la politique de connexion - cascade: - removed: Cascade de fournisseurs d'identité supprimée de la stratégie de connexion - removed: Fournisseur d'identité supprimé de la stratégie de connexion - multifactor: - added: Multifactor ajouté à la politique de connexion - removed: Multifactor supprimé de la politique de connexion - secondfactor: - added: Deuxième facteur ajouté à la politique de connexion - removed: Deuxième facteur supprimé de la politique de connexion - password: - age: - added: Politique d'âge du mot de passe ajoutée - changed: La politique relative à l'âge du mot de passe a été modifiée - complexity: - added: Politique de complexité des mots de passe ajoutée - changed: Politique de complexité des mots de passe supprimée - privacy: - added: Politique de confidentialité ajoutée - changed: Politique de confidentialité modifiée - security: - set: Ensemble de règles de sécurité + web_key: + added: Clé Web ajoutée + activated: Clé Web activée + deactivated: Clé Web désactivée + removed: Clé Web supprimée - removed: Instance removed - secret: - generator: - added: Générateur de secrets ajouté - changed: Le générateur de secrets a changé - removed: Générateur de secrets supprimé - sms: - configtwilio: - activated: Configuration SMS Twilio activée - added: Configuration SMS Twilio ajoutée - changed: La configuration des SMS Twilio a été modifiée - deactivated: Configuration SMS Twilio désactivée - removed: Configuration SMS Twilio supprimée - token: - changed: Jeton de configuration SMS Twilio modifié - smtp: - config: - added: Configuration SMTP ajoutée - changed: Configuration SMTP modifiée - activated: Configuration SMTP activée - deactivated: Configuration SMTP désactivée - password: - changed: Mot de passe de configuration SMTP modifié - removed: Configuration SMTP supprimée Application: OIDC: UnsupportedVersion: Votre version de l'OIDC n'est pas prise en charge diff --git a/internal/static/i18n/it.yaml b/internal/static/i18n/it.yaml index 73180b982b..2df6792a70 100644 --- a/internal/static/i18n/it.yaml +++ b/internal/static/i18n/it.yaml @@ -586,6 +586,13 @@ Errors: NotForAPI: Token rappresentati non consentiti per l'API Impersonation: PolicyDisabled: La rappresentazione è disabilitata nella policy di sicurezza dell'istanza + WebKey: + ActiveDelete: Impossibile eliminare la chiave Web attiva + Config: Configurazione chiave Web non valida + Duplicate: ID chiave Web non univoco + FeatureDisabled: Funzione chiave Web disabilitata + NoActive: Nessuna chiave Web attiva trovata + NotFound: Chiave Web non trovata AggregateTypes: action: Azione @@ -609,6 +616,7 @@ AggregateTypes: restrictions: Restrizioni system: Sistema session: Sessione + web_key: Chiave Web EventTypes: execution: @@ -1172,12 +1180,6 @@ EventTypes: deactivated: Azione disattivata reactivated: Azione riattivata removed: Azione rimossa - user_schema: - created: Schema utente creato - updated: Schema utente aggiornato - deactivated: Schema utente disattivato - reactivated: Schema utente riattivato - deleted: Schema utente eliminato instance: added: Istanza aggiunta changed: L'istanza è cambiata @@ -1306,6 +1308,17 @@ EventTypes: password: changed: La password della configurazione SMTP è cambiata removed: Configurazione SMTP rimossa + user_schema: + created: Schema utente creato + updated: Schema utente aggiornato + deactivated: Schema utente disattivato + reactivated: Schema utente riattivato + deleted: Schema utente eliminato + web_key: + added: Web Key aggiunto + activated: Web Key attivato + deactivated: Web Key disattivato + removed: Web Key rimosso Application: OIDC: diff --git a/internal/static/i18n/ja.yaml b/internal/static/i18n/ja.yaml index 3b8b4cbb92..32e1c645f5 100644 --- a/internal/static/i18n/ja.yaml +++ b/internal/static/i18n/ja.yaml @@ -575,6 +575,13 @@ Errors: NotForAPI: 偽装されたトークンは API では許可されません Impersonation: PolicyDisabled: インスタンスのセキュリティ ポリシーで偽装が無効になっています + WebKey: + ActiveDelete: アクティブな Web キーを削除できません + Config: 無効な Web キー設定 + Duplicate: Web キー ID が一意ではありません + FeatureDisabled: Web キー機能が無効です + NoActive: アクティブな Web キーが見つかりません + NotFound: Web キーが見つかりません AggregateTypes: action: アクション @@ -598,6 +605,7 @@ AggregateTypes: restrictions: 制限 system: システム session: セッション + web_key: Web キー EventTypes: execution: @@ -1296,6 +1304,11 @@ EventTypes: deactivated: ユーザースキーマが非アクティブ化されました reactivated: ユーザースキーマが再アクティブ化されました deleted: ユーザースキーマが削除されました + web_key: + added: Web キーが追加されました + activated: Web キーが有効化されました + deactivated: Web キーが無効化されました + removed: Web キーが削除されました Application: OIDC: diff --git a/internal/static/i18n/mk.yaml b/internal/static/i18n/mk.yaml index cec3cbb506..627cdabd7b 100644 --- a/internal/static/i18n/mk.yaml +++ b/internal/static/i18n/mk.yaml @@ -585,6 +585,13 @@ Errors: NotForAPI: Имитирани токени не се дозволени за API Impersonation: PolicyDisabled: Имитирањето е оневозможено во политиката за безбедност на примерот + WebKey: + ActiveDelete: Не може да се избрише активниот веб-клуч + Config: Неважечка конфигурација на веб-клуч + Duplicate: ID на веб-клучот не е единствен + FeatureDisabled: Функцијата за веб-клуч е оневозможена + NoActive: Не е пронајден активен веб-клуч + NotFound: Веб-клучот не е пронајден AggregateTypes: action: Акција @@ -608,6 +615,7 @@ AggregateTypes: restrictions: Ограничувања system: Систем session: Сесија + web_key: Веб клуч EventTypes: execution: @@ -1308,6 +1316,11 @@ EventTypes: deactivated: Корисничката шема е деактивирана reactivated: Корисничката шема е реактивирана deleted: Корисничката шема е избришана + web_key: + added: Додаден е веб-клуч + activated: Веб-клучот е активиран + deactivated: Веб-клучот е деактивиран + removed: Веб-клучот е отстранет Application: OIDC: diff --git a/internal/static/i18n/nl.yaml b/internal/static/i18n/nl.yaml index 062ee7f5c9..b9f29169e7 100644 --- a/internal/static/i18n/nl.yaml +++ b/internal/static/i18n/nl.yaml @@ -586,6 +586,13 @@ Errors: NotForAPI: Nagebootste tokens zijn niet toegestaan voor API Impersonation: PolicyDisabled: Nabootsing van identiteit is uitgeschakeld in het beveiligingsbeleid van de instantie. + WebKey: + ActiveDelete: Kan actieve websleutel niet verwijderen + Config: Ongeldige websleutelconfiguratie + Duplicate: Websleutel-ID niet uniek + FeatureDisabled: Websleutelfunctie uitgeschakeld + NoActive: Geen actieve websleutel gevonden + NotFound: Websleutel niet gevonden AggregateTypes: action: Actie @@ -609,6 +616,7 @@ AggregateTypes: restrictions: Beperkingen system: Systeem session: Sessie + web_key: Websleutel EventTypes: execution: @@ -1305,6 +1313,11 @@ EventTypes: deactivated: Gebruikersschema gedeactiveerd reactivated: Gebruikersschema opnieuw geactiveerd deleted: Gebruikersschema verwijderd + web_key: + added: Web Key toegevoegd + activated: Web Key geactiveerd + deactivated: Web Key gedeactiveerd + removed: Web Key verwijderd Application: OIDC: diff --git a/internal/static/i18n/pl.yaml b/internal/static/i18n/pl.yaml index 4705320d84..36da9b1775 100644 --- a/internal/static/i18n/pl.yaml +++ b/internal/static/i18n/pl.yaml @@ -586,6 +586,13 @@ Errors: NotForAPI: Podrabiane tokeny nie są dozwolone w interfejsie API Impersonation: PolicyDisabled: Podszywanie się jest wyłączone w polityce bezpieczeństwa instancji + WebKey: + ActiveDelete: Nie można usunąć aktywnego klucza internetowego + Config: Nieprawidłowa konfiguracja klucza internetowego + Duplicate: Identyfikator klucza internetowego nie jest unikalny + FeatureDisabled: Funkcja klucza internetowego jest wyłączona + NoActive: Nie znaleziono aktywnego klucza internetowego + NotFound: Nie znaleziono klucza internetowego AggregateTypes: action: Działanie @@ -609,6 +616,7 @@ AggregateTypes: restrictions: Ograniczenia system: System session: Sesja + web_key: Klucz internetowy EventTypes: execution: @@ -1310,6 +1318,11 @@ EventTypes: deactivated: Schemat użytkownika dezaktywowany reactivated: Schemat użytkownika został ponownie aktywowany deleted: Schemat użytkownika został usunięty + web_key: + added: Dodano klucz internetowy + activated: Klucz internetowy aktywowano + deactivated: Klucz internetowy dezaktywowano + removed: Klucz internetowy usunięto Application: OIDC: diff --git a/internal/static/i18n/pt.yaml b/internal/static/i18n/pt.yaml index acb69e2c0b..cb6e013829 100644 --- a/internal/static/i18n/pt.yaml +++ b/internal/static/i18n/pt.yaml @@ -581,6 +581,13 @@ Errors: NotForAPI: Tokens personificados não permitidos para API Impersonation: PolicyDisabled: A representação está desativada na política de segurança da instância + WebKey: + ActiveDelete: Não é possível eliminar a chave web ativa + Config: Configuração de chave web inválida + Duplicate: ID da chave Web não exclusivo + FeatureDisabled: Recurso chave da Web desativado + NoActive: Nenhuma chave web ativa encontrada + NotFound: Chave Web não encontrada AggregateTypes: action: Ação @@ -604,6 +611,7 @@ AggregateTypes: restrictions: Restrições system: Sistema session: Sessão + web_key: Chave da Web EventTypes: execution: @@ -1302,6 +1310,11 @@ EventTypes: deactivated: Esquema de usuário desativado reactivated: Esquema do usuário reativado deleted: Esquema do usuário excluído + web_key: + added: Chave Web adicionada + activated: Chave Web ativada + deactivated: Chave Web desativada + removed: Chave Web removida Application: OIDC: diff --git a/internal/static/i18n/ru.yaml b/internal/static/i18n/ru.yaml index 36918b9c1f..df7015bc02 100644 --- a/internal/static/i18n/ru.yaml +++ b/internal/static/i18n/ru.yaml @@ -575,6 +575,13 @@ Errors: NotForAPI: Олицетворенные токены не разрешены для API. Impersonation: PolicyDisabled: Олицетворение отключено в политике безопасности экземпляра. + WebKey: + ActiveDelete: Невозможно удалить активный веб-ключ + Config: Неверная конфигурация веб-ключа + Duplicate: Идентификатор веб-ключа не уникален + FeatureDisabled: Функция веб-ключа отключена + NoActive: Активный веб-ключ не найден + NotFound: Веб-ключ не найден AggregateTypes: action: Действие @@ -598,6 +605,7 @@ AggregateTypes: restrictions: Ограничения system: Система session: Сеанс + web_key: Веб-ключ EventTypes: execution: @@ -1296,6 +1304,12 @@ EventTypes: deactivated: Пользовательская схема деактивирована reactivated: Пользовательская схема повторно активирована deleted: Пользовательская схема удалена + web_key: + added: Добавлен веб-ключ + activated: Веб-ключ активирован + deactivated: Веб-ключ деактивирован + removed: Веб-ключ удален + Application: OIDC: UnsupportedVersion: Ваша версия OIDC не поддерживается diff --git a/internal/static/i18n/sv.yaml b/internal/static/i18n/sv.yaml index ee0a6a3b04..45326841ea 100644 --- a/internal/static/i18n/sv.yaml +++ b/internal/static/i18n/sv.yaml @@ -586,6 +586,13 @@ Errors: NotForAPI: Imitationstoken tillåts inte för API Impersonation: PolicyDisabled: Imitation är inaktiverad i instansens säkerhetspolicy + WebKey: + ActiveDelete: Det går inte att ta bort aktiv webbnyckel + Config: Ogiltig webbnyckelkonfiguration + Duplicate: Webnyckel-ID är inte unikt + FeatureDisabled: Webnyckelfunktion inaktiverad + NoActive: Ingen aktiv webbnyckel hittades + NotFound: Webnyckel hittades inte AggregateTypes: action: Åtgärd @@ -609,6 +616,7 @@ AggregateTypes: restrictions: Restriktioner system: System session: Session + web_key: Webbnyckel EventTypes: execution: @@ -1310,6 +1318,11 @@ EventTypes: deactivated: Användarschema avaktiverat reactivated: Användarschema återaktiverat deleted: Användarschema borttaget + web_key: + added: Webbnyckel har lagts till + activated: Webbnyckel aktiverad + deactivated: Webnyckel avaktiverad + removed: Webbnyckeln har tagits bort Application: OIDC: diff --git a/internal/static/i18n/zh.yaml b/internal/static/i18n/zh.yaml index 9d22c30891..05fa703240 100644 --- a/internal/static/i18n/zh.yaml +++ b/internal/static/i18n/zh.yaml @@ -586,6 +586,13 @@ Errors: NotForAPI: API 不允许使用模拟令牌 Impersonation: PolicyDisabled: 实例安全策略中禁用模拟 + WebKey: + ActiveDelete: 无法删除活动 Web 密钥 + Config: 无效的 Web 密钥配置 + Duplicate: Web 密钥 ID 不唯一 + FeatureDisabled: Web 密钥功能已禁用 + NoActive: 未找到活动 Web 密钥 + NotFound: 未找到 Web 密钥 AggregateTypes: action: 动作 @@ -609,6 +616,7 @@ AggregateTypes: restrictions: 限制 system: 系统 session: 会话 + web_key: Web 密钥 EventTypes: execution: @@ -1175,12 +1183,6 @@ EventTypes: deactivated: 停用动作 reactivated: 启用动作 removed: 删除动作 - user_schema: - created: 已创建用户架构 - updated: 用户架构已更新 - deactivated: 用户架构已停用 - reactivated: 用户架构已重新激活 - deleted: 用户架构已删除 instance: added: 实例已添加 changed: 实例已更改 @@ -1309,6 +1311,17 @@ EventTypes: password: changed: SMTP 配置密码已更改 removed: SMTP 配置已删除 + user_schema: + created: 已创建用户架构 + updated: 用户架构已更新 + deactivated: 用户架构已停用 + reactivated: 用户架构已重新激活 + deleted: 用户架构已删除 + web_key: + added: 已添加 Web Key + activated: 已激活 Web Key + deactivated: 已停用 Web Key + removed: 已删除 Web Key Application: OIDC: diff --git a/proto/zitadel/feature/v2/instance.proto b/proto/zitadel/feature/v2/instance.proto index 52b28f2101..24c6df5db6 100644 --- a/proto/zitadel/feature/v2/instance.proto +++ b/proto/zitadel/feature/v2/instance.proto @@ -58,6 +58,13 @@ message SetInstanceFeaturesRequest{ description: "Improves performance of specified execution paths."; } ]; + + optional bool web_key = 8 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "true"; + description: "Enable the webkey/v3alpha API. The first time this feature is enabled, web keys are generated and activated."; + } + ]; } message SetInstanceFeaturesResponse { @@ -129,4 +136,11 @@ message GetInstanceFeaturesResponse { description: "Improves performance of specified execution paths."; } ]; + + FeatureFlag web_key = 9 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "true"; + description: "Enable the webkey/v3alpha API. The first time this feature is enabled, web keys are generated and activated."; + } + ]; } diff --git a/proto/zitadel/feature/v2beta/instance.proto b/proto/zitadel/feature/v2beta/instance.proto index 292fcc5101..33d00af3eb 100644 --- a/proto/zitadel/feature/v2beta/instance.proto +++ b/proto/zitadel/feature/v2beta/instance.proto @@ -58,6 +58,13 @@ message SetInstanceFeaturesRequest{ description: "Improves performance of specified execution paths."; } ]; + + optional bool web_key = 8 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "true"; + description: "Enable the webkey/v3alpha API. The first time this feature is enabled, web keys are generated and activated."; + } + ]; } message SetInstanceFeaturesResponse { @@ -129,4 +136,11 @@ message GetInstanceFeaturesResponse { description: "Improves performance of specified execution paths."; } ]; + + FeatureFlag web_key = 9 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "true"; + description: "Enable the webkey/v3alpha API. The first time this feature is enabled, web keys are generated and activated."; + } + ]; } diff --git a/proto/zitadel/resources/webkey/v3alpha/config.proto b/proto/zitadel/resources/webkey/v3alpha/config.proto new file mode 100644 index 0000000000..170334afa5 --- /dev/null +++ b/proto/zitadel/resources/webkey/v3alpha/config.proto @@ -0,0 +1,41 @@ +syntax = "proto3"; + +package zitadel.resources.webkey.v3alpha; + +import "validate/validate.proto"; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/resources/webkey/v3alpha;webkey"; + +message WebKeyRSAConfig { + enum RSABits { + RSA_BITS_UNSPECIFIED = 0; + RSA_BITS_2048 = 1; + RSA_BITS_3072 = 2; + RSA_BITS_4096 = 3; + } + + enum RSAHasher { + RSA_HASHER_UNSPECIFIED = 0; + RSA_HASHER_SHA256 = 1; + RSA_HASHER_SHA384 = 2; + RSA_HASHER_SHA512 = 3; + } + + // bit size of the RSA key + RSABits bits = 1 [(validate.rules).enum = {defined_only: true, not_in: [0]}]; + // signing algrithm used + RSAHasher hasher = 2 [(validate.rules).enum = {defined_only: true, not_in: [0]}]; +} + +message WebKeyECDSAConfig { + enum ECDSACurve { + ECDSA_CURVE_UNSPECIFIED = 0; + ECDSA_CURVE_P256 = 1; + ECDSA_CURVE_P384 = 2; + ECDSA_CURVE_P512 = 3; + } + + ECDSACurve curve = 1 [(validate.rules).enum = {defined_only: true, not_in: [0]}]; +} + +message WebKeyED25519Config {} diff --git a/proto/zitadel/resources/webkey/v3alpha/key.proto b/proto/zitadel/resources/webkey/v3alpha/key.proto new file mode 100644 index 0000000000..47486f7aee --- /dev/null +++ b/proto/zitadel/resources/webkey/v3alpha/key.proto @@ -0,0 +1,31 @@ +syntax = "proto3"; + +package zitadel.resources.webkey.v3alpha; + +import "google/protobuf/timestamp.proto"; +import "zitadel/resources/webkey/v3alpha/config.proto"; +import "zitadel/resources/object/v3alpha/object.proto"; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/resources/webkey/v3alpha;webkey"; + +enum WebKeyState { + STATE_UNSPECIFIED = 0; + STATE_INITIAL = 1; + STATE_ACTIVE = 2; + STATE_INACTIVE = 3; + STATE_REMOVED = 4; +} + +message GetWebKey { + zitadel.resources.object.v3alpha.Details details = 1; + WebKey config = 2; + WebKeyState state = 3; +} + +message WebKey { + oneof config { + WebKeyRSAConfig rsa = 6; + WebKeyECDSAConfig ecdsa = 7; + WebKeyED25519Config ed25519 = 8; + } +} diff --git a/proto/zitadel/resources/webkey/v3alpha/webkey_service.proto b/proto/zitadel/resources/webkey/v3alpha/webkey_service.proto new file mode 100644 index 0000000000..c79424095b --- /dev/null +++ b/proto/zitadel/resources/webkey/v3alpha/webkey_service.proto @@ -0,0 +1,278 @@ +syntax = "proto3"; + +package zitadel.resources.webkey.v3alpha; + +import "google/api/annotations.proto"; +import "google/api/field_behavior.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; +import "validate/validate.proto"; + +import "zitadel/protoc_gen_zitadel/v2/options.proto"; + +import "zitadel/resources/webkey/v3alpha/key.proto"; +import "zitadel/resources/object/v3alpha/object.proto"; +import "zitadel/object/v3alpha/object.proto"; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/resources/webkey/v3alpha;webkey"; + +option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { + info: { + title: "Web key Service"; + version: "3.0-preview"; + description: "This API is intended to manage web keys for a ZITADEL instance, used to sign and validate OIDC tokens. This project is in preview 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/LICENSE"; + }; + }; + schemes: HTTPS; + schemes: HTTP; + + consumes: "application/json"; + produces: "application/json"; + + consumes: "application/grpc"; + produces: "application/grpc"; + + consumes: "application/grpc-web+proto"; + produces: "application/grpc-web+proto"; + + host: "${ZITADEL_DOMAIN}"; + base_path: "/resources/v3alpha/web_keys"; + + 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 ZITADELWebKeys { + rpc CreateWebKey(CreateWebKeyRequest) returns (CreateWebKeyResponse) { + option (google.api.http) = { + post: "/" + body: "key" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "iam.web_key.write" + } + http_response: { + success_code: 201 + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + summary: "Generate a web key pair for the instance"; + description: "Generate a private and public key pair. The private key can be used to sign OIDC tokens after activation. The public key can be used to valite OIDC tokens." + responses: { + key: "200" + value: { + description: "OK"; + } + }; + }; + } + + rpc ActivateWebKey(ActivateWebKeyRequest) returns (ActivateWebKeyResponse) { + option (google.api.http) = { + post: "/{id}/_activate" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "iam.web_key.write" + } + http_response: { + success_code: 200 + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + summary: "Activate a signing key for the instance"; + description: "Switch the active signing web key. The previously active key will be deactivated." + responses: { + key: "200" + value: { + description: "OK"; + } + }; + }; + } + + rpc DeleteWebKey(DeleteWebKeyRequest) returns (DeleteWebKeyResponse) { + option (google.api.http) = { + delete: "/{id}" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "iam.web_key.delete" + } + http_response: { + success_code: 200 + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + summary: "Generate a web key pair for the instance"; + description: "Delete a web key. Only inactive keys can be deleted. Once a key is deleted, any tokens signed by this key will be invalid." + responses: { + key: "200" + value: { + description: "OK"; + } + }; + }; + } + + rpc ListWebKeys(ListWebKeysRequest) returns (ListWebKeysResponse) { + option (google.api.http) = { + get: "/" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "iam.web_key.read" + } + http_response: { + success_code: 200 + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + summary: "Generate a web key pair for the instance"; + description: "List web key details for the instance" + responses: { + key: "200" + value: { + description: "OK"; + } + }; + }; + } +} + +message CreateWebKeyRequest { + optional zitadel.object.v3alpha.Instance instance = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"domain from HOST or :authority header\"" + } + ]; + WebKey key = 2; +} + +message CreateWebKeyResponse { + zitadel.resources.object.v3alpha.Details details = 1; +} + +message ActivateWebKeyRequest { + optional zitadel.object.v3alpha.Instance instance = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"domain from HOST or :authority header\"" + } + ]; + 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: "\"69629026806489455\""; + } + ]; +} + +message ActivateWebKeyResponse { + zitadel.resources.object.v3alpha.Details details = 1; +} + +message DeleteWebKeyRequest { + optional zitadel.object.v3alpha.Instance instance = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"domain from HOST or :authority header\"" + } + ]; + 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: "\"69629026806489455\""; + } + ]; +} + +message DeleteWebKeyResponse { + zitadel.resources.object.v3alpha.Details details = 1; +} + +message ListWebKeysRequest { + optional zitadel.object.v3alpha.Instance instance = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"domain from HOST or :authority header\"" + } + ]; +} + +message ListWebKeysResponse { + repeated GetWebKey web_keys = 1; +} \ No newline at end of file From 3e3d46ac0d1a8063d863357031dacc3ef262d34e Mon Sep 17 00:00:00 2001 From: Stefan Benz <46600784+stebenz@users.noreply.github.com> Date: Wed, 14 Aug 2024 20:18:29 +0200 Subject: [PATCH 33/39] feat: idp v2 api GetIDPByID (#8425) # Which Problems Are Solved GetIDPByID as endpoint in the API v2 so that it can be available for the new login. # How the Problems Are Solved Create GetIDPByID endpoint with IDP v2 API, throught the GetProviderByID implementation from admin and management API. # Additional Changes - Remove the OwnerType attribute from the response, as the information is available through the resourceOwner. - correct refs to messages in proto which are used for doc generation - renaming of elements for API v3 # Additional Context Closes #8337 --------- Co-authored-by: Livio Spring --- cmd/start/start.go | 4 + docs/docusaurus.config.js | 8 + docs/sidebars.js | 14 +- internal/api/grpc/admin/idp.go | 2 +- internal/api/grpc/idp/v2/query.go | 369 +++++++++++++++++ .../api/grpc/idp/v2/query_integration_test.go | 235 +++++++++++ internal/api/grpc/idp/v2/server.go | 56 +++ .../grpc/idp/v2/server_integration_test.go | 40 ++ internal/api/grpc/management/idp.go | 2 +- internal/api/ui/login/policy_handler.go | 2 +- internal/domain/permission.go | 2 + internal/integration/client.go | 68 ++- internal/query/idp_template.go | 25 +- pkg/grpc/idp/v2/idp.go | 4 + proto/zitadel/idp/v2/idp.proto | 391 ++++++++++++++++++ proto/zitadel/idp/v2/idp_service.proto | 136 ++++++ .../action/v3alpha/action_service.proto | 10 +- .../webkey/v3alpha/webkey_service.proto | 2 +- 18 files changed, 1348 insertions(+), 22 deletions(-) create mode 100644 internal/api/grpc/idp/v2/query.go create mode 100644 internal/api/grpc/idp/v2/query_integration_test.go create mode 100644 internal/api/grpc/idp/v2/server.go create mode 100644 internal/api/grpc/idp/v2/server_integration_test.go create mode 100644 pkg/grpc/idp/v2/idp.go create mode 100644 proto/zitadel/idp/v2/idp.proto create mode 100644 proto/zitadel/idp/v2/idp_service.proto diff --git a/cmd/start/start.go b/cmd/start/start.go index f944ef0327..0ecff76a9b 100644 --- a/cmd/start/start.go +++ b/cmd/start/start.go @@ -38,6 +38,7 @@ import ( "github.com/zitadel/zitadel/internal/api/grpc/auth" 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" "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" @@ -437,6 +438,9 @@ func startAPIs( if err := apis.RegisterService(ctx, feature_v2.CreateServer(commands, queries)); err != nil { return nil, err } + if err := apis.RegisterService(ctx, idp_v2.CreateServer(commands, queries, permissionCheck)); err != nil { + return nil, err + } if err := apis.RegisterService(ctx, action_v3_alpha.CreateServer(config.SystemDefaults, commands, queries, domain.AllFunctions, apis.ListGrpcMethods, apis.ListGrpcServices)); err != nil { return nil, err } diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js index b65a53d20b..3aad64d9ef 100644 --- a/docs/docusaurus.config.js +++ b/docs/docusaurus.config.js @@ -356,6 +356,14 @@ module.exports = { categoryLinkSource: "auto", }, }, + idp_v2: { + specPath: ".artifacts/openapi/zitadel/idp/v2/idp_service.swagger.json", + outputDir: "docs/apis/resources/idp_service_v2", + sidebarOptions: { + groupPathsBy: "tag", + categoryLinkSource: "auto", + }, + }, }, }, ], diff --git a/docs/sidebars.js b/docs/sidebars.js index 49107a380c..98667395f1 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -673,12 +673,24 @@ module.exports = { link: { type: "generated-index", title: "Feature Service API", - slug: "/apis/resources/feature_service/v2", + slug: "/apis/resources/feature_service_v2", description: 'This API is intended to manage features for ZITADEL. Feature settings that are available on multiple "levels", such as instance and organization. The higher level instance acts as a default for the lower level. When a feature is set on multiple levels, the lower level takes precedence. Features can be experimental where ZITADEL will assume a sane default, such as disabled. When over time confidence in such a feature grows, ZITADEL can default to enabling the feature. As a final step we might choose to always enable a feature and remove the setting from this API, reserving the proto field number. Such removal is not considered a breaking change. Setting a removed field will effectively result in a no-op.\n' }, items: require("./docs/apis/resources/feature_service_v2/sidebar.ts"), }, + { + type: "category", + label: "Identity Provider Lifecycle", + link: { + type: "generated-index", + title: "Identity Provider Service API", + slug: "/apis/resources/idp_service_v2", + description: + 'This API is intended to manage identity providers (IdPs) for ZITADEL.\n' + }, + items: require("./docs/apis/resources/idp_service_v2/sidebar.ts"), + }, ], }, { diff --git a/internal/api/grpc/admin/idp.go b/internal/api/grpc/admin/idp.go index 8182ad63b9..5528a94dbb 100644 --- a/internal/api/grpc/admin/idp.go +++ b/internal/api/grpc/admin/idp.go @@ -157,7 +157,7 @@ func (s *Server) GetProviderByID(ctx context.Context, req *admin_pb.GetProviderB if err != nil { return nil, err } - idp, err := s.query.IDPTemplateByID(ctx, true, req.Id, false, instanceIDQuery) + idp, err := s.query.IDPTemplateByID(ctx, true, req.Id, false, nil, instanceIDQuery) if err != nil { return nil, err } diff --git a/internal/api/grpc/idp/v2/query.go b/internal/api/grpc/idp/v2/query.go new file mode 100644 index 0000000000..476ac3dcdd --- /dev/null +++ b/internal/api/grpc/idp/v2/query.go @@ -0,0 +1,369 @@ +package idp + +import ( + "context" + + "github.com/crewjam/saml" + "github.com/muhlemmer/gu" + "google.golang.org/protobuf/types/known/durationpb" + + "github.com/zitadel/zitadel/internal/api/grpc/object/v2" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/idp/providers/azuread" + "github.com/zitadel/zitadel/internal/query" + idp_rp "github.com/zitadel/zitadel/internal/repository/idp" + idp_pb "github.com/zitadel/zitadel/pkg/grpc/idp/v2" +) + +func (s *Server) GetIDPByID(ctx context.Context, req *idp_pb.GetIDPByIDRequest) (*idp_pb.GetIDPByIDResponse, error) { + idp, err := s.query.IDPTemplateByID(ctx, true, req.Id, false, s.checkPermission) + if err != nil { + return nil, err + } + return &idp_pb.GetIDPByIDResponse{Idp: idpToPb(idp)}, nil +} + +func idpToPb(idp *query.IDPTemplate) *idp_pb.IDP { + return &idp_pb.IDP{ + Id: idp.ID, + Details: object.DomainToDetailsPb( + &domain.ObjectDetails{ + Sequence: idp.Sequence, + EventDate: idp.ChangeDate, + ResourceOwner: idp.ResourceOwner, + }), + State: idpStateToPb(idp.State), + Name: idp.Name, + Type: idpTypeToPb(idp.Type), + Config: configToPb(idp), + } +} + +func idpStateToPb(state domain.IDPState) idp_pb.IDPState { + switch state { + case domain.IDPStateActive: + return idp_pb.IDPState_IDP_STATE_ACTIVE + case domain.IDPStateInactive: + return idp_pb.IDPState_IDP_STATE_INACTIVE + case domain.IDPStateUnspecified: + return idp_pb.IDPState_IDP_STATE_UNSPECIFIED + case domain.IDPStateMigrated: + return idp_pb.IDPState_IDP_STATE_MIGRATED + case domain.IDPStateRemoved: + return idp_pb.IDPState_IDP_STATE_REMOVED + default: + return idp_pb.IDPState_IDP_STATE_UNSPECIFIED + } +} + +func idpTypeToPb(idpType domain.IDPType) idp_pb.IDPType { + switch idpType { + case domain.IDPTypeOIDC: + return idp_pb.IDPType_IDP_TYPE_OIDC + case domain.IDPTypeJWT: + return idp_pb.IDPType_IDP_TYPE_JWT + case domain.IDPTypeOAuth: + return idp_pb.IDPType_IDP_TYPE_OAUTH + case domain.IDPTypeLDAP: + return idp_pb.IDPType_IDP_TYPE_LDAP + case domain.IDPTypeAzureAD: + return idp_pb.IDPType_IDP_TYPE_AZURE_AD + case domain.IDPTypeGitHub: + return idp_pb.IDPType_IDP_TYPE_GITHUB + case domain.IDPTypeGitHubEnterprise: + return idp_pb.IDPType_IDP_TYPE_GITHUB_ES + case domain.IDPTypeGitLab: + return idp_pb.IDPType_IDP_TYPE_GITLAB + case domain.IDPTypeGitLabSelfHosted: + return idp_pb.IDPType_IDP_TYPE_GITLAB_SELF_HOSTED + case domain.IDPTypeGoogle: + return idp_pb.IDPType_IDP_TYPE_GOOGLE + case domain.IDPTypeApple: + return idp_pb.IDPType_IDP_TYPE_APPLE + case domain.IDPTypeSAML: + return idp_pb.IDPType_IDP_TYPE_SAML + case domain.IDPTypeUnspecified: + return idp_pb.IDPType_IDP_TYPE_UNSPECIFIED + default: + return idp_pb.IDPType_IDP_TYPE_UNSPECIFIED + } +} + +func configToPb(config *query.IDPTemplate) *idp_pb.IDPConfig { + idpConfig := &idp_pb.IDPConfig{ + Options: &idp_pb.Options{ + IsLinkingAllowed: config.IsLinkingAllowed, + IsCreationAllowed: config.IsCreationAllowed, + IsAutoCreation: config.IsAutoCreation, + IsAutoUpdate: config.IsAutoUpdate, + AutoLinking: autoLinkingOptionToPb(config.AutoLinking), + }, + } + if config.OAuthIDPTemplate != nil { + oauthConfigToPb(idpConfig, config.OAuthIDPTemplate) + return idpConfig + } + if config.OIDCIDPTemplate != nil { + oidcConfigToPb(idpConfig, config.OIDCIDPTemplate) + return idpConfig + } + if config.JWTIDPTemplate != nil { + jwtConfigToPb(idpConfig, config.JWTIDPTemplate) + return idpConfig + } + if config.AzureADIDPTemplate != nil { + azureConfigToPb(idpConfig, config.AzureADIDPTemplate) + return idpConfig + } + if config.GitHubIDPTemplate != nil { + githubConfigToPb(idpConfig, config.GitHubIDPTemplate) + return idpConfig + } + if config.GitHubEnterpriseIDPTemplate != nil { + githubEnterpriseConfigToPb(idpConfig, config.GitHubEnterpriseIDPTemplate) + return idpConfig + } + if config.GitLabIDPTemplate != nil { + gitlabConfigToPb(idpConfig, config.GitLabIDPTemplate) + return idpConfig + } + if config.GitLabSelfHostedIDPTemplate != nil { + gitlabSelfHostedConfigToPb(idpConfig, config.GitLabSelfHostedIDPTemplate) + return idpConfig + } + if config.GoogleIDPTemplate != nil { + googleConfigToPb(idpConfig, config.GoogleIDPTemplate) + return idpConfig + } + if config.LDAPIDPTemplate != nil { + ldapConfigToPb(idpConfig, config.LDAPIDPTemplate) + return idpConfig + } + if config.AppleIDPTemplate != nil { + appleConfigToPb(idpConfig, config.AppleIDPTemplate) + return idpConfig + } + if config.SAMLIDPTemplate != nil { + samlConfigToPb(idpConfig, config.SAMLIDPTemplate) + return idpConfig + } + return idpConfig +} + +func autoLinkingOptionToPb(linking domain.AutoLinkingOption) idp_pb.AutoLinkingOption { + switch linking { + case domain.AutoLinkingOptionUnspecified: + return idp_pb.AutoLinkingOption_AUTO_LINKING_OPTION_UNSPECIFIED + case domain.AutoLinkingOptionUsername: + return idp_pb.AutoLinkingOption_AUTO_LINKING_OPTION_USERNAME + case domain.AutoLinkingOptionEmail: + return idp_pb.AutoLinkingOption_AUTO_LINKING_OPTION_EMAIL + default: + return idp_pb.AutoLinkingOption_AUTO_LINKING_OPTION_UNSPECIFIED + } +} + +func oauthConfigToPb(idpConfig *idp_pb.IDPConfig, template *query.OAuthIDPTemplate) { + idpConfig.Config = &idp_pb.IDPConfig_Oauth{ + Oauth: &idp_pb.OAuthConfig{ + ClientId: template.ClientID, + AuthorizationEndpoint: template.AuthorizationEndpoint, + TokenEndpoint: template.TokenEndpoint, + UserEndpoint: template.UserEndpoint, + Scopes: template.Scopes, + IdAttribute: template.IDAttribute, + }, + } +} + +func oidcConfigToPb(idpConfig *idp_pb.IDPConfig, template *query.OIDCIDPTemplate) { + idpConfig.Config = &idp_pb.IDPConfig_Oidc{ + Oidc: &idp_pb.GenericOIDCConfig{ + ClientId: template.ClientID, + Issuer: template.Issuer, + Scopes: template.Scopes, + IsIdTokenMapping: template.IsIDTokenMapping, + }, + } +} + +func jwtConfigToPb(idpConfig *idp_pb.IDPConfig, template *query.JWTIDPTemplate) { + idpConfig.Config = &idp_pb.IDPConfig_Jwt{ + Jwt: &idp_pb.JWTConfig{ + JwtEndpoint: template.Endpoint, + Issuer: template.Issuer, + KeysEndpoint: template.KeysEndpoint, + HeaderName: template.HeaderName, + }, + } +} + +func azureConfigToPb(idpConfig *idp_pb.IDPConfig, template *query.AzureADIDPTemplate) { + idpConfig.Config = &idp_pb.IDPConfig_AzureAd{ + AzureAd: &idp_pb.AzureADConfig{ + ClientId: template.ClientID, + Tenant: azureTenantToPb(template.Tenant), + EmailVerified: template.IsEmailVerified, + Scopes: template.Scopes, + }, + } +} + +func azureTenantToPb(tenant string) *idp_pb.AzureADTenant { + var tenantType idp_pb.IsAzureADTenantType + switch azuread.TenantType(tenant) { + case azuread.CommonTenant: + tenantType = &idp_pb.AzureADTenant_TenantType{TenantType: idp_pb.AzureADTenantType_AZURE_AD_TENANT_TYPE_COMMON} + case azuread.OrganizationsTenant: + tenantType = &idp_pb.AzureADTenant_TenantType{TenantType: idp_pb.AzureADTenantType_AZURE_AD_TENANT_TYPE_ORGANISATIONS} + case azuread.ConsumersTenant: + tenantType = &idp_pb.AzureADTenant_TenantType{TenantType: idp_pb.AzureADTenantType_AZURE_AD_TENANT_TYPE_CONSUMERS} + default: + tenantType = &idp_pb.AzureADTenant_TenantId{TenantId: tenant} + } + return &idp_pb.AzureADTenant{Type: tenantType} +} + +func githubConfigToPb(idpConfig *idp_pb.IDPConfig, template *query.GitHubIDPTemplate) { + idpConfig.Config = &idp_pb.IDPConfig_Github{ + Github: &idp_pb.GitHubConfig{ + ClientId: template.ClientID, + Scopes: template.Scopes, + }, + } +} + +func githubEnterpriseConfigToPb(idpConfig *idp_pb.IDPConfig, template *query.GitHubEnterpriseIDPTemplate) { + idpConfig.Config = &idp_pb.IDPConfig_GithubEs{ + GithubEs: &idp_pb.GitHubEnterpriseServerConfig{ + ClientId: template.ClientID, + AuthorizationEndpoint: template.AuthorizationEndpoint, + TokenEndpoint: template.TokenEndpoint, + UserEndpoint: template.UserEndpoint, + Scopes: template.Scopes, + }, + } +} + +func gitlabConfigToPb(idpConfig *idp_pb.IDPConfig, template *query.GitLabIDPTemplate) { + idpConfig.Config = &idp_pb.IDPConfig_Gitlab{ + Gitlab: &idp_pb.GitLabConfig{ + ClientId: template.ClientID, + Scopes: template.Scopes, + }, + } +} + +func gitlabSelfHostedConfigToPb(idpConfig *idp_pb.IDPConfig, template *query.GitLabSelfHostedIDPTemplate) { + idpConfig.Config = &idp_pb.IDPConfig_GitlabSelfHosted{ + GitlabSelfHosted: &idp_pb.GitLabSelfHostedConfig{ + ClientId: template.ClientID, + Issuer: template.Issuer, + Scopes: template.Scopes, + }, + } +} + +func googleConfigToPb(idpConfig *idp_pb.IDPConfig, template *query.GoogleIDPTemplate) { + idpConfig.Config = &idp_pb.IDPConfig_Google{ + Google: &idp_pb.GoogleConfig{ + ClientId: template.ClientID, + Scopes: template.Scopes, + }, + } +} + +func ldapConfigToPb(idpConfig *idp_pb.IDPConfig, template *query.LDAPIDPTemplate) { + var timeout *durationpb.Duration + if template.Timeout != 0 { + timeout = durationpb.New(template.Timeout) + } + idpConfig.Config = &idp_pb.IDPConfig_Ldap{ + Ldap: &idp_pb.LDAPConfig{ + Servers: template.Servers, + StartTls: template.StartTLS, + BaseDn: template.BaseDN, + BindDn: template.BindDN, + UserBase: template.UserBase, + UserObjectClasses: template.UserObjectClasses, + UserFilters: template.UserFilters, + Timeout: timeout, + Attributes: ldapAttributesToPb(template.LDAPAttributes), + }, + } +} + +func ldapAttributesToPb(attributes idp_rp.LDAPAttributes) *idp_pb.LDAPAttributes { + return &idp_pb.LDAPAttributes{ + IdAttribute: attributes.IDAttribute, + FirstNameAttribute: attributes.FirstNameAttribute, + LastNameAttribute: attributes.LastNameAttribute, + DisplayNameAttribute: attributes.DisplayNameAttribute, + NickNameAttribute: attributes.NickNameAttribute, + PreferredUsernameAttribute: attributes.PreferredUsernameAttribute, + EmailAttribute: attributes.EmailAttribute, + EmailVerifiedAttribute: attributes.EmailVerifiedAttribute, + PhoneAttribute: attributes.PhoneAttribute, + PhoneVerifiedAttribute: attributes.PhoneVerifiedAttribute, + PreferredLanguageAttribute: attributes.PreferredLanguageAttribute, + AvatarUrlAttribute: attributes.AvatarURLAttribute, + ProfileAttribute: attributes.ProfileAttribute, + } +} + +func appleConfigToPb(idpConfig *idp_pb.IDPConfig, template *query.AppleIDPTemplate) { + idpConfig.Config = &idp_pb.IDPConfig_Apple{ + Apple: &idp_pb.AppleConfig{ + ClientId: template.ClientID, + TeamId: template.TeamID, + KeyId: template.KeyID, + Scopes: template.Scopes, + }, + } +} + +func samlConfigToPb(idpConfig *idp_pb.IDPConfig, template *query.SAMLIDPTemplate) { + nameIDFormat := idp_pb.SAMLNameIDFormat_SAML_NAME_ID_FORMAT_PERSISTENT + if template.NameIDFormat.Valid { + nameIDFormat = nameIDToPb(template.NameIDFormat.V) + } + idpConfig.Config = &idp_pb.IDPConfig_Saml{ + Saml: &idp_pb.SAMLConfig{ + MetadataXml: template.Metadata, + Binding: bindingToPb(template.Binding), + WithSignedRequest: template.WithSignedRequest, + NameIdFormat: nameIDFormat, + TransientMappingAttributeName: gu.Ptr(template.TransientMappingAttributeName), + }, + } +} + +func bindingToPb(binding string) idp_pb.SAMLBinding { + switch binding { + case "": + return idp_pb.SAMLBinding_SAML_BINDING_UNSPECIFIED + case saml.HTTPPostBinding: + return idp_pb.SAMLBinding_SAML_BINDING_POST + case saml.HTTPRedirectBinding: + return idp_pb.SAMLBinding_SAML_BINDING_REDIRECT + case saml.HTTPArtifactBinding: + return idp_pb.SAMLBinding_SAML_BINDING_ARTIFACT + default: + return idp_pb.SAMLBinding_SAML_BINDING_UNSPECIFIED + } +} + +func nameIDToPb(format domain.SAMLNameIDFormat) idp_pb.SAMLNameIDFormat { + switch format { + case domain.SAMLNameIDFormatUnspecified: + return idp_pb.SAMLNameIDFormat_SAML_NAME_ID_FORMAT_UNSPECIFIED + case domain.SAMLNameIDFormatEmailAddress: + return idp_pb.SAMLNameIDFormat_SAML_NAME_ID_FORMAT_EMAIL_ADDRESS + case domain.SAMLNameIDFormatPersistent: + return idp_pb.SAMLNameIDFormat_SAML_NAME_ID_FORMAT_PERSISTENT + case domain.SAMLNameIDFormatTransient: + return idp_pb.SAMLNameIDFormat_SAML_NAME_ID_FORMAT_TRANSIENT + default: + return idp_pb.SAMLNameIDFormat_SAML_NAME_ID_FORMAT_UNSPECIFIED + } +} diff --git a/internal/api/grpc/idp/v2/query_integration_test.go b/internal/api/grpc/idp/v2/query_integration_test.go new file mode 100644 index 0000000000..1135e33547 --- /dev/null +++ b/internal/api/grpc/idp/v2/query_integration_test.go @@ -0,0 +1,235 @@ +//go:build integration + +package idp_test + +import ( + "context" + "fmt" + "testing" + "time" + + "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/idp/v2" + "github.com/zitadel/zitadel/pkg/grpc/object/v2" +) + +type idpAttr struct { + ID string + Name string + Details *object.Details +} + +func TestServer_GetIDPByID(t *testing.T) { + type args struct { + ctx context.Context + req *idp.GetIDPByIDRequest + dep func(ctx context.Context, request *idp.GetIDPByIDRequest) *idpAttr + } + tests := []struct { + name string + args args + want *idp.GetIDPByIDResponse + wantErr bool + }{ + { + name: "idp by ID, no id provided", + args: args{ + IamCTX, + &idp.GetIDPByIDRequest{ + Id: "", + }, + func(ctx context.Context, request *idp.GetIDPByIDRequest) *idpAttr { + return nil + }, + }, + wantErr: true, + }, + { + name: "idp by ID, not found", + args: args{ + IamCTX, + &idp.GetIDPByIDRequest{ + Id: "unknown", + }, + func(ctx context.Context, request *idp.GetIDPByIDRequest) *idpAttr { + return nil + }, + }, + wantErr: true, + }, + { + name: "idp by ID, instance, ok", + args: args{ + IamCTX, + &idp.GetIDPByIDRequest{}, + func(ctx context.Context, request *idp.GetIDPByIDRequest) *idpAttr { + name := fmt.Sprintf("GetIDPByID%d", time.Now().UnixNano()) + resp := Tester.AddGenericOAuthIDP(ctx, name) + request.Id = resp.Id + return &idpAttr{ + resp.GetId(), + name, + &object.Details{ + Sequence: resp.Details.Sequence, + ChangeDate: resp.Details.ChangeDate, + ResourceOwner: resp.Details.ResourceOwner, + }} + }, + }, + want: &idp.GetIDPByIDResponse{ + Idp: &idp.IDP{ + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + }, + State: idp.IDPState_IDP_STATE_ACTIVE, + Type: idp.IDPType_IDP_TYPE_OAUTH, + Config: &idp.IDPConfig{ + Config: &idp.IDPConfig_Oauth{ + Oauth: &idp.OAuthConfig{ + ClientId: "clientID", + AuthorizationEndpoint: "https://example.com/oauth/v2/authorize", + TokenEndpoint: "https://example.com/oauth/v2/token", + UserEndpoint: "https://api.example.com/user", + Scopes: []string{"openid", "profile", "email"}, + IdAttribute: "id", + }, + }, + Options: &idp.Options{ + IsLinkingAllowed: true, + IsCreationAllowed: true, + IsAutoCreation: true, + IsAutoUpdate: true, + AutoLinking: idp.AutoLinkingOption_AUTO_LINKING_OPTION_USERNAME, + }, + }, + }, + }, + }, + { + name: "idp by ID, instance, no permission", + args: args{ + UserCTX, + &idp.GetIDPByIDRequest{}, + func(ctx context.Context, request *idp.GetIDPByIDRequest) *idpAttr { + name := fmt.Sprintf("GetIDPByID%d", time.Now().UnixNano()) + resp := Tester.AddGenericOAuthIDP(IamCTX, name) + request.Id = resp.Id + return &idpAttr{ + resp.GetId(), + name, + &object.Details{ + Sequence: resp.Details.Sequence, + ChangeDate: resp.Details.ChangeDate, + ResourceOwner: resp.Details.ResourceOwner, + }} + }, + }, + wantErr: true, + }, + { + name: "idp by ID, org, ok", + args: args{ + CTX, + &idp.GetIDPByIDRequest{}, + func(ctx context.Context, request *idp.GetIDPByIDRequest) *idpAttr { + name := fmt.Sprintf("GetIDPByID%d", time.Now().UnixNano()) + resp := Tester.AddOrgGenericOAuthIDP(ctx, name) + request.Id = resp.Id + return &idpAttr{ + resp.GetId(), + name, + &object.Details{ + Sequence: resp.Details.Sequence, + ChangeDate: resp.Details.ChangeDate, + ResourceOwner: resp.Details.ResourceOwner, + }} + }, + }, + want: &idp.GetIDPByIDResponse{ + Idp: &idp.IDP{ + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + }, + State: idp.IDPState_IDP_STATE_ACTIVE, + Type: idp.IDPType_IDP_TYPE_OAUTH, + Config: &idp.IDPConfig{ + Config: &idp.IDPConfig_Oauth{ + Oauth: &idp.OAuthConfig{ + ClientId: "clientID", + AuthorizationEndpoint: "https://example.com/oauth/v2/authorize", + TokenEndpoint: "https://example.com/oauth/v2/token", + UserEndpoint: "https://api.example.com/user", + Scopes: []string{"openid", "profile", "email"}, + IdAttribute: "id", + }, + }, + Options: &idp.Options{ + IsLinkingAllowed: true, + IsCreationAllowed: true, + IsAutoCreation: true, + IsAutoUpdate: true, + AutoLinking: idp.AutoLinkingOption_AUTO_LINKING_OPTION_USERNAME, + }, + }, + }, + }, + }, + { + name: "idp by ID, org, no permission", + args: args{ + UserCTX, + &idp.GetIDPByIDRequest{}, + func(ctx context.Context, request *idp.GetIDPByIDRequest) *idpAttr { + name := fmt.Sprintf("GetIDPByID%d", time.Now().UnixNano()) + resp := Tester.AddOrgGenericOAuthIDP(CTX, name) + request.Id = resp.Id + return &idpAttr{ + resp.GetId(), + name, + &object.Details{ + Sequence: resp.Details.Sequence, + ChangeDate: resp.Details.ChangeDate, + ResourceOwner: resp.Details.ResourceOwner, + }} + }, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + idpAttr := tt.args.dep(tt.args.ctx, tt.args.req) + retryDuration := time.Minute + if ctxDeadline, ok := CTX.Deadline(); ok { + retryDuration = time.Until(ctxDeadline) + } + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + got, getErr := Client.GetIDPByID(tt.args.ctx, tt.args.req) + assertErr := assert.NoError + if tt.wantErr { + assertErr = assert.Error + } + assertErr(ttt, getErr) + if getErr != nil { + return + } + + // set provided info from creation + tt.want.Idp.Details = idpAttr.Details + tt.want.Idp.Name = idpAttr.Name + tt.want.Idp.Id = idpAttr.ID + + // first check for details, mgmt and admin api don't fill the details correctly + integration.AssertDetails(t, tt.want.Idp, got.Idp) + // then set details + tt.want.Idp.Details = got.Idp.Details + // to check the rest of the content + assert.Equal(ttt, tt.want.Idp, got.Idp) + }, retryDuration, time.Second) + }) + } +} diff --git a/internal/api/grpc/idp/v2/server.go b/internal/api/grpc/idp/v2/server.go new file mode 100644 index 0000000000..246e980434 --- /dev/null +++ b/internal/api/grpc/idp/v2/server.go @@ -0,0 +1,56 @@ +package idp + +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/domain" + "github.com/zitadel/zitadel/internal/query" + "github.com/zitadel/zitadel/pkg/grpc/idp/v2" +) + +var _ idp.IdentityProviderServiceServer = (*Server)(nil) + +type Server struct { + idp.UnimplementedIdentityProviderServiceServer + command *command.Commands + query *query.Queries + + checkPermission domain.PermissionCheck +} + +type Config struct{} + +func CreateServer( + command *command.Commands, + query *query.Queries, + checkPermission domain.PermissionCheck, +) *Server { + return &Server{ + command: command, + query: query, + checkPermission: checkPermission, + } +} + +func (s *Server) RegisterServer(grpcServer *grpc.Server) { + idp.RegisterIdentityProviderServiceServer(grpcServer, s) +} + +func (s *Server) AppName() string { + return idp.IdentityProviderService_ServiceDesc.ServiceName +} + +func (s *Server) MethodPrefix() string { + return idp.IdentityProviderService_ServiceDesc.ServiceName +} + +func (s *Server) AuthMethods() authz.MethodMapping { + return idp.IdentityProviderService_AuthMethods +} + +func (s *Server) RegisterGateway() server.RegisterGatewayFunc { + return idp.RegisterIdentityProviderServiceHandler +} diff --git a/internal/api/grpc/idp/v2/server_integration_test.go b/internal/api/grpc/idp/v2/server_integration_test.go new file mode 100644 index 0000000000..9e8f44a311 --- /dev/null +++ b/internal/api/grpc/idp/v2/server_integration_test.go @@ -0,0 +1,40 @@ +//go:build integration + +package idp_test + +import ( + "context" + "os" + "testing" + "time" + + "github.com/zitadel/zitadel/internal/integration" + idp_pb "github.com/zitadel/zitadel/pkg/grpc/idp/v2" +) + +var ( + CTX context.Context + IamCTX context.Context + UserCTX context.Context + SystemCTX context.Context + ErrCTX context.Context + Tester *integration.Tester + Client idp_pb.IdentityProviderServiceClient +) + +func TestMain(m *testing.M) { + os.Exit(func() int { + ctx, errCtx, cancel := integration.Contexts(time.Hour) + defer cancel() + + Tester = integration.NewTester(ctx) + defer Tester.Done() + + UserCTX = Tester.WithAuthorization(ctx, integration.Login) + IamCTX = Tester.WithAuthorization(ctx, integration.IAMOwner) + SystemCTX = Tester.WithAuthorization(ctx, integration.SystemUser) + CTX, ErrCTX = Tester.WithAuthorization(ctx, integration.OrgOwner), errCtx + Client = Tester.Client.IDPv2 + return m.Run() + }()) +} diff --git a/internal/api/grpc/management/idp.go b/internal/api/grpc/management/idp.go index f013015258..66b659d1ea 100644 --- a/internal/api/grpc/management/idp.go +++ b/internal/api/grpc/management/idp.go @@ -149,7 +149,7 @@ func (s *Server) GetProviderByID(ctx context.Context, req *mgmt_pb.GetProviderBy if err != nil { return nil, err } - idp, err := s.query.IDPTemplateByID(ctx, true, req.Id, false, orgIDQuery) + idp, err := s.query.IDPTemplateByID(ctx, true, req.Id, false, nil, orgIDQuery) if err != nil { return nil, err } diff --git a/internal/api/ui/login/policy_handler.go b/internal/api/ui/login/policy_handler.go index e5e6336068..2ff4ac9b78 100644 --- a/internal/api/ui/login/policy_handler.go +++ b/internal/api/ui/login/policy_handler.go @@ -18,7 +18,7 @@ func (l *Login) getOrgDomainPolicy(r *http.Request, orgID string) (*query.Domain } func (l *Login) getIDPByID(r *http.Request, id string) (*query.IDPTemplate, error) { - return l.query.IDPTemplateByID(r.Context(), false, id, false) + return l.query.IDPTemplateByID(r.Context(), false, id, false, nil) } func (l *Login) getLoginPolicy(r *http.Request, orgID string) (*query.LoginPolicy, error) { diff --git a/internal/domain/permission.go b/internal/domain/permission.go index cf2d02d426..75d7f792ff 100644 --- a/internal/domain/permission.go +++ b/internal/domain/permission.go @@ -33,4 +33,6 @@ const ( PermissionUserCredentialWrite = "user.credential.write" PermissionSessionWrite = "session.write" PermissionSessionDelete = "session.delete" + PermissionIDPRead = "iam.idp.read" + PermissionOrgIDPRead = "org.idp.read" ) diff --git a/internal/integration/client.go b/internal/integration/client.go index 947c11508b..a69ce6a1ee 100644 --- a/internal/integration/client.go +++ b/internal/integration/client.go @@ -24,22 +24,24 @@ import ( "github.com/zitadel/zitadel/internal/idp/providers/ldap" openid "github.com/zitadel/zitadel/internal/idp/providers/oidc" "github.com/zitadel/zitadel/internal/idp/providers/saml" - "github.com/zitadel/zitadel/internal/repository/idp" + idp_rp "github.com/zitadel/zitadel/internal/repository/idp" "github.com/zitadel/zitadel/pkg/grpc/admin" "github.com/zitadel/zitadel/pkg/grpc/auth" "github.com/zitadel/zitadel/pkg/grpc/feature/v2" 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" mgmt "github.com/zitadel/zitadel/pkg/grpc/management" - object "github.com/zitadel/zitadel/pkg/grpc/object/v2" + "github.com/zitadel/zitadel/pkg/grpc/object/v2" oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2" oidc_pb_v2beta "github.com/zitadel/zitadel/pkg/grpc/oidc/v2beta" - org "github.com/zitadel/zitadel/pkg/grpc/org/v2" + "github.com/zitadel/zitadel/pkg/grpc/org/v2" org_v2beta "github.com/zitadel/zitadel/pkg/grpc/org/v2beta" action "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha" webkey_v3alpha "github.com/zitadel/zitadel/pkg/grpc/resources/webkey/v3alpha" - session "github.com/zitadel/zitadel/pkg/grpc/session/v2" + "github.com/zitadel/zitadel/pkg/grpc/session/v2" session_v2beta "github.com/zitadel/zitadel/pkg/grpc/session/v2beta" - settings "github.com/zitadel/zitadel/pkg/grpc/settings/v2" + "github.com/zitadel/zitadel/pkg/grpc/settings/v2" settings_v2beta "github.com/zitadel/zitadel/pkg/grpc/settings/v2beta" "github.com/zitadel/zitadel/pkg/grpc/system" user_pb "github.com/zitadel/zitadel/pkg/grpc/user" @@ -69,6 +71,7 @@ type Client struct { FeatureV2 feature.FeatureServiceClient UserSchemaV3 schema.UserSchemaServiceClient WebKeyV3Alpha webkey_v3alpha.ZITADELWebKeysClient + IDPv2 idp_pb.IdentityProviderServiceClient } func newClient(cc *grpc.ClientConn) Client { @@ -93,6 +96,7 @@ func newClient(cc *grpc.ClientConn) Client { FeatureV2: feature.NewFeatureServiceClient(cc), UserSchemaV3: schema.NewUserSchemaServiceClient(cc), WebKeyV3Alpha: webkey_v3alpha.NewZITADELWebKeysClient(cc), + IDPv2: idp_pb.NewIdentityProviderServiceClient(cc), } } @@ -367,6 +371,28 @@ func (s *Tester) SetUserPassword(ctx context.Context, userID, password string, c return resp.GetDetails() } +func (s *Tester) AddGenericOAuthIDP(ctx context.Context, name string) *admin.AddGenericOAuthProviderResponse { + resp, err := s.Client.Admin.AddGenericOAuthProvider(ctx, &admin.AddGenericOAuthProviderRequest{ + Name: name, + ClientId: "clientID", + ClientSecret: "clientSecret", + AuthorizationEndpoint: "https://example.com/oauth/v2/authorize", + TokenEndpoint: "https://example.com/oauth/v2/token", + UserEndpoint: "https://api.example.com/user", + Scopes: []string{"openid", "profile", "email"}, + IdAttribute: "id", + ProviderOptions: &idp.Options{ + IsLinkingAllowed: true, + IsCreationAllowed: true, + IsAutoCreation: true, + IsAutoUpdate: true, + AutoLinking: idp.AutoLinkingOption_AUTO_LINKING_OPTION_USERNAME, + }, + }) + logging.OnError(err).Fatal("create generic OAuth idp") + return resp +} + func (s *Tester) AddGenericOAuthProvider(t *testing.T, ctx context.Context) string { ctx = authz.WithInstance(ctx, s.Instance) id, _, err := s.Commands.AddInstanceGenericOAuthProvider(ctx, command.GenericOAuthProvider{ @@ -378,7 +404,7 @@ func (s *Tester) AddGenericOAuthProvider(t *testing.T, ctx context.Context) stri UserEndpoint: "https://api.example.com/user", Scopes: []string{"openid", "profile", "email"}, IDAttribute: "id", - IDPOptions: idp.Options{ + IDPOptions: idp_rp.Options{ IsLinkingAllowed: true, IsCreationAllowed: true, IsAutoCreation: true, @@ -389,6 +415,28 @@ func (s *Tester) AddGenericOAuthProvider(t *testing.T, ctx context.Context) stri return id } +func (s *Tester) AddOrgGenericOAuthIDP(ctx context.Context, name string) *mgmt.AddGenericOAuthProviderResponse { + resp, err := s.Client.Mgmt.AddGenericOAuthProvider(ctx, &mgmt.AddGenericOAuthProviderRequest{ + Name: name, + ClientId: "clientID", + ClientSecret: "clientSecret", + AuthorizationEndpoint: "https://example.com/oauth/v2/authorize", + TokenEndpoint: "https://example.com/oauth/v2/token", + UserEndpoint: "https://api.example.com/user", + Scopes: []string{"openid", "profile", "email"}, + IdAttribute: "id", + ProviderOptions: &idp.Options{ + IsLinkingAllowed: true, + IsCreationAllowed: true, + IsAutoCreation: true, + IsAutoUpdate: true, + AutoLinking: idp.AutoLinkingOption_AUTO_LINKING_OPTION_USERNAME, + }, + }) + logging.OnError(err).Fatal("create generic OAuth idp") + return resp +} + func (s *Tester) AddOrgGenericOAuthProvider(t *testing.T, ctx context.Context, orgID string) string { ctx = authz.WithInstance(ctx, s.Instance) id, _, err := s.Commands.AddOrgGenericOAuthProvider(ctx, orgID, @@ -401,7 +449,7 @@ func (s *Tester) AddOrgGenericOAuthProvider(t *testing.T, ctx context.Context, o UserEndpoint: "https://api.example.com/user", Scopes: []string{"openid", "profile", "email"}, IDAttribute: "id", - IDPOptions: idp.Options{ + IDPOptions: idp_rp.Options{ IsLinkingAllowed: true, IsCreationAllowed: true, IsAutoCreation: true, @@ -417,7 +465,7 @@ func (s *Tester) AddSAMLProvider(t *testing.T, ctx context.Context) string { id, _, err := s.Server.Commands.AddInstanceSAMLProvider(ctx, command.SAMLProvider{ Name: "saml-idp", Metadata: []byte("\n \n \n \n \n MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=\n \n \n \n \n \n \n MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=\n \n \n \n \n \n \n \n urn:oasis:names:tc:SAML:2.0:nameid-format:transient\n \n \n \n"), - IDPOptions: idp.Options{ + IDPOptions: idp_rp.Options{ IsLinkingAllowed: true, IsCreationAllowed: true, IsAutoCreation: true, @@ -435,7 +483,7 @@ func (s *Tester) AddSAMLRedirectProvider(t *testing.T, ctx context.Context, tran Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect", Metadata: []byte("\n \n \n \n \n MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=\n \n \n \n \n \n \n MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=\n \n \n \n \n \n \n \n urn:oasis:names:tc:SAML:2.0:nameid-format:transient\n \n \n"), TransientMappingAttributeName: transientMappingAttributeName, - IDPOptions: idp.Options{ + IDPOptions: idp_rp.Options{ IsLinkingAllowed: true, IsCreationAllowed: true, IsAutoCreation: true, @@ -452,7 +500,7 @@ func (s *Tester) AddSAMLPostProvider(t *testing.T, ctx context.Context) string { Name: "saml-idp-post", Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST", Metadata: []byte("\n \n \n \n \n MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=\n \n \n \n \n \n \n MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=\n \n \n \n \n \n \n \n urn:oasis:names:tc:SAML:2.0:nameid-format:transient\n \n \n"), - IDPOptions: idp.Options{ + IDPOptions: idp_rp.Options{ IsLinkingAllowed: true, IsCreationAllowed: true, IsAutoCreation: true, diff --git a/internal/query/idp_template.go b/internal/query/idp_template.go index 2b835f0e69..e3250f1ae7 100644 --- a/internal/query/idp_template.go +++ b/internal/query/idp_template.go @@ -712,8 +712,29 @@ var ( } ) -// IDPTemplateByID searches for the requested id -func (q *Queries) IDPTemplateByID(ctx context.Context, shouldTriggerBulk bool, id string, withOwnerRemoved bool, queries ...SearchQuery) (template *IDPTemplate, err error) { +// IDPTemplateByID searches for the requested id with permission check if necessary +func (q *Queries) IDPTemplateByID(ctx context.Context, shouldTriggerBulk bool, id string, withOwnerRemoved bool, permissionCheck domain.PermissionCheck, queries ...SearchQuery) (template *IDPTemplate, err error) { + idp, err := q.idpTemplateByID(ctx, shouldTriggerBulk, id, withOwnerRemoved, queries...) + if err != nil { + return nil, err + } + if permissionCheck != nil { + switch idp.OwnerType { + case domain.IdentityProviderTypeSystem: + if err := permissionCheck(ctx, domain.PermissionIDPRead, idp.ResourceOwner, idp.ID); err != nil { + return nil, err + } + case domain.IdentityProviderTypeOrg: + if err := permissionCheck(ctx, domain.PermissionOrgIDPRead, idp.ResourceOwner, idp.ID); err != nil { + return nil, err + } + } + } + return idp, nil +} + +// idpTemplateByID searches for the requested id +func (q *Queries) idpTemplateByID(ctx context.Context, shouldTriggerBulk bool, id string, withOwnerRemoved bool, queries ...SearchQuery) (template *IDPTemplate, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() diff --git a/pkg/grpc/idp/v2/idp.go b/pkg/grpc/idp/v2/idp.go new file mode 100644 index 0000000000..514f6c5e33 --- /dev/null +++ b/pkg/grpc/idp/v2/idp.go @@ -0,0 +1,4 @@ +package idp + +type IsIDPConfig = isIDPConfig_Config +type IsAzureADTenantType = isAzureADTenant_Type diff --git a/proto/zitadel/idp/v2/idp.proto b/proto/zitadel/idp/v2/idp.proto new file mode 100644 index 0000000000..784e717d3a --- /dev/null +++ b/proto/zitadel/idp/v2/idp.proto @@ -0,0 +1,391 @@ +syntax = "proto3"; + +package zitadel.idp.v2; + +import "zitadel/protoc_gen_zitadel/v2/options.proto"; +import "zitadel/object/v2/object.proto"; +import "google/api/annotations.proto"; +import "google/api/field_behavior.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; +import "validate/validate.proto"; +import "google/protobuf/duration.proto"; + + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/idp/v2;idp"; + +message IDP { + // Unique identifier for the identity provider. + string id = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629023906488334\""; + } + ]; + zitadel.object.v2.Details details = 2; + // Current state of the identity provider. + IDPState state = 3; + string name = 4 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"Google\""; + } + ]; + // Type of the identity provider, for example OIDC, JWT, LDAP and SAML. + IDPType type = 5; + // Configuration for the type of the identity provider. + IDPConfig config = 6; +} + +enum IDPState { + IDP_STATE_UNSPECIFIED = 0; + IDP_STATE_ACTIVE = 1; + IDP_STATE_INACTIVE = 2; + IDP_STATE_REMOVED = 3; + IDP_STATE_MIGRATED = 4; +} + +enum IDPType { + IDP_TYPE_UNSPECIFIED = 0; + IDP_TYPE_OIDC = 1; + IDP_TYPE_JWT = 2; + IDP_TYPE_LDAP = 3; + IDP_TYPE_OAUTH = 4; + IDP_TYPE_AZURE_AD = 5; + IDP_TYPE_GITHUB = 6; + IDP_TYPE_GITHUB_ES = 7; + IDP_TYPE_GITLAB = 8; + IDP_TYPE_GITLAB_SELF_HOSTED = 9; + IDP_TYPE_GOOGLE = 10; + IDP_TYPE_APPLE = 11; + IDP_TYPE_SAML = 12; +} + +enum SAMLBinding { + SAML_BINDING_UNSPECIFIED = 0; + SAML_BINDING_POST = 1; + SAML_BINDING_REDIRECT = 2; + SAML_BINDING_ARTIFACT = 3; +} + +enum SAMLNameIDFormat { + SAML_NAME_ID_FORMAT_UNSPECIFIED = 0; + SAML_NAME_ID_FORMAT_EMAIL_ADDRESS = 1; + SAML_NAME_ID_FORMAT_PERSISTENT = 2; + SAML_NAME_ID_FORMAT_TRANSIENT = 3; +} + +message IDPConfig { + Options options = 1; + oneof config { + LDAPConfig ldap = 2; + GoogleConfig google = 3; + OAuthConfig oauth = 4; + GenericOIDCConfig oidc = 5; + JWTConfig jwt = 6; + GitHubConfig github = 7; + GitHubEnterpriseServerConfig github_es = 8; + GitLabConfig gitlab = 9; + GitLabSelfHostedConfig gitlab_self_hosted = 10; + AzureADConfig azure_ad = 11; + AppleConfig apple = 12; + SAMLConfig saml = 13; + } +} + +message JWTConfig { + // The endpoint where the JWT can be extracted. + string jwt_endpoint = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"https://accounts.google.com\""; + } + ]; + // The issuer of the JWT (for validation). + string issuer = 2 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"https://accounts.google.com\""; + } + ]; + // The endpoint to the key (JWK) which is used to sign the JWT with. + string keys_endpoint = 3 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"https://accounts.google.com/keys\""; + } + ]; + // The name of the header where the JWT is sent in, default is authorization. + string header_name = 4 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"x-auth-token\""; + } + ]; +} + +message OAuthConfig { + // Client id generated by the identity provider. + string client_id = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"client-id\""; + } + ]; + // The endpoint where ZITADEL send the user to authenticate. + string authorization_endpoint = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"https://accounts.google.com/o/oauth2/v2/auth\""; + } + ]; + // The endpoint where ZITADEL can get the token. + string token_endpoint = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"https://oauth2.googleapis.com/token\""; + } + ]; + // The endpoint where ZITADEL can get the user information. + string user_endpoint = 4 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"https://openidconnect.googleapis.com/v1/userinfo\""; + } + ]; + // The scopes requested by ZITADEL during the request on the identity provider. + repeated string scopes = 5 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "[\"openid\", \"profile\", \"email\"]"; + } + ]; + // Defines how the attribute is called where ZITADEL can get the id of the user. + string id_attribute = 6 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"user_id\""; + } + ]; +} + +message GenericOIDCConfig { + // The OIDC issuer of the identity provider. + string issuer = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"https://accounts.google.com/\""; + } + ]; + // Client id generated by the identity provider. + string client_id = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"client-id\""; + } + ]; + // The scopes requested by ZITADEL during the request on the identity provider. + repeated string scopes = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "[\"openid\", \"profile\", \"email\"]"; + } + ]; + // If true, provider information get mapped from the id token, not from the userinfo endpoint. + bool is_id_token_mapping = 4 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "true"; + } + ]; +} + +message GitHubConfig { + // The client ID of the GitHub App. + string client_id = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"client-id\""; + } + ]; + // The scopes requested by ZITADEL during the request to GitHub. + repeated string scopes = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "[\"openid\", \"profile\", \"email\"]"; + } + ]; +} + +message GitHubEnterpriseServerConfig { + // The client ID of the GitHub App. + string client_id = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"client-id\""; + } + ]; + string authorization_endpoint = 2; + string token_endpoint = 3; + string user_endpoint = 4; + // The scopes requested by ZITADEL during the request to GitHub. + repeated string scopes = 5 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "[\"openid\", \"profile\", \"email\"]"; + } + ]; +} + +message GoogleConfig { + // Client id of the Google application. + string client_id = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"client-id\""; + } + ]; + // The scopes requested by ZITADEL during the request to Google. + repeated string scopes = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "[\"openid\", \"profile\", \"email\"]"; + } + ]; +} + +message GitLabConfig { + // Client id of the GitLab application. + string client_id = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"client-id\""; + } + ]; + // The scopes requested by ZITADEL during the request to GitLab. + repeated string scopes = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "[\"openid\", \"profile\", \"email\"]"; + } + ]; +} + +message GitLabSelfHostedConfig { + string issuer = 1; + // Client id of the GitLab application. + string client_id = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"client-id\""; + } + ]; + // The scopes requested by ZITADEL during the request to GitLab. + repeated string scopes = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "[\"openid\", \"profile\", \"email\"]"; + } + ]; +} + +message LDAPConfig { + repeated string servers = 1; + bool start_tls = 2; + string base_dn = 3; + string bind_dn = 4; + string user_base = 5; + repeated string user_object_classes = 6; + repeated string user_filters = 7; + google.protobuf.Duration timeout = 8; + LDAPAttributes attributes = 9; +} + +message SAMLConfig { + // Metadata of the SAML identity provider. + bytes metadata_xml = 1; + // Binding which defines the type of communication with the identity provider. + SAMLBinding binding = 2; + // Boolean which defines if the authentication requests are signed. + bool with_signed_request = 3; + // `nameid-format` for the SAML Request. + SAMLNameIDFormat name_id_format = 4; + // 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; +} + +message AzureADConfig { + // Client id of the Azure AD application + string client_id = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"client-id\""; + } + ]; + // Defines what user accounts should be able to login (Personal, Organizational, All). + AzureADTenant tenant = 2; + // Azure AD doesn't send if the email has been verified. Enable this if the user email should always be added verified in ZITADEL (no verification emails will be sent). + bool email_verified = 3; + // The scopes requested by ZITADEL during the request to Azure AD. + repeated string scopes = 4 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "[\"openid\", \"profile\", \"email\", \"User.Read\"]"; + } + ]; +} + +message Options { + // Enable if users should be able to link an existing ZITADEL user with an external account. + bool is_linking_allowed = 1; + // Enable if users should be able to create a new account in ZITADEL when using an external account. + bool is_creation_allowed = 2; + // Enable if a new account in ZITADEL should be created automatically when login with an external account. + bool is_auto_creation = 3; + // Enable if a the ZITADEL account fields should be updated automatically on each login. + bool is_auto_update = 4; + // Enable if users should get prompted to link an existing ZITADEL user to an external account if the selected attribute matches. + AutoLinkingOption auto_linking = 5 ; +} + +enum AutoLinkingOption { + // AUTO_LINKING_OPTION_UNSPECIFIED disables the auto linking prompt. + AUTO_LINKING_OPTION_UNSPECIFIED = 0; + // AUTO_LINKING_OPTION_USERNAME will use the username of the external user to check for a corresponding ZITADEL user. + AUTO_LINKING_OPTION_USERNAME = 1; + // AUTO_LINKING_OPTION_EMAIL will use the email of the external user to check for a corresponding ZITADEL user with the same verified email + // Note that in case multiple users match, no prompt will be shown. + AUTO_LINKING_OPTION_EMAIL = 2; +} + +message LDAPAttributes { + string id_attribute = 1 [(validate.rules).string = {max_len: 200}]; + string first_name_attribute = 2 [(validate.rules).string = {max_len: 200}]; + string last_name_attribute = 3 [(validate.rules).string = {max_len: 200}]; + string display_name_attribute = 4 [(validate.rules).string = {max_len: 200}]; + string nick_name_attribute = 5 [(validate.rules).string = {max_len: 200}]; + string preferred_username_attribute = 6 [(validate.rules).string = {max_len: 200}]; + string email_attribute = 7 [(validate.rules).string = {max_len: 200}]; + string email_verified_attribute = 8 [(validate.rules).string = {max_len: 200}]; + string phone_attribute = 9 [(validate.rules).string = {max_len: 200}]; + string phone_verified_attribute = 10 [(validate.rules).string = {max_len: 200}]; + string preferred_language_attribute = 11 [(validate.rules).string = {max_len: 200}]; + string avatar_url_attribute = 12 [(validate.rules).string = {max_len: 200}]; + string profile_attribute = 13 [(validate.rules).string = {max_len: 200}]; +} + +enum AzureADTenantType { + AZURE_AD_TENANT_TYPE_COMMON = 0; + AZURE_AD_TENANT_TYPE_ORGANISATIONS = 1; + AZURE_AD_TENANT_TYPE_CONSUMERS = 2; +} + +message AzureADTenant { + oneof type { + AzureADTenantType tenant_type = 1; + string tenant_id = 2; + } +} + +message AppleConfig { + // Client id (App ID or Service ID) provided by Apple. + string client_id = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"com.client.id\""; + } + ]; + // Team ID provided by Apple. + string team_id = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"ALT03JV3OS\""; + } + ]; + // ID of the private key generated by Apple. + string key_id = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"OGKDK25KD\""; + } + ]; + // The scopes requested by ZITADEL during the request to Apple. + repeated string scopes = 4 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "[\"name\", \"email\"]"; + } + ]; +} \ No newline at end of file diff --git a/proto/zitadel/idp/v2/idp_service.proto b/proto/zitadel/idp/v2/idp_service.proto new file mode 100644 index 0000000000..2d5306cea6 --- /dev/null +++ b/proto/zitadel/idp/v2/idp_service.proto @@ -0,0 +1,136 @@ +syntax = "proto3"; + +package zitadel.idp.v2; + +import "zitadel/protoc_gen_zitadel/v2/options.proto"; +import "zitadel/object/v2/object.proto"; +import "google/api/annotations.proto"; +import "google/api/field_behavior.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; +import "validate/validate.proto"; +import "zitadel/idp/v2/idp.proto"; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/idp/v2;idp"; + +option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { + info: { + title: "Identity Provider Service"; + version: "2.0"; + description: "This API is intended to manage identity providers (IdPs) in a ZITADEL instance."; + contact:{ + name: "ZITADEL" + url: "https://zitadel.com" + email: "hi@zitadel.com" + } + license: { + name: "Apache 2.0", + url: "https://github.com/zitadel/zitadel/blob/main/LICENSE"; + }; + }; + 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 IdentityProviderService { + + // Get identity provider (IdP) by ID + // + // Returns an identity provider (social/enterprise login) by its ID, which can be of the type Google, AzureAD, etc. + rpc GetIDPByID (GetIDPByIDRequest) returns (GetIDPByIDResponse) { + option (google.api.http) = { + get: "/v2/idps/{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: "OK"; + } + }; + }; + } +} + +message GetIDPByIDRequest { + string id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; +} + +message GetIDPByIDResponse { + zitadel.idp.v2.IDP idp = 1; +} diff --git a/proto/zitadel/resources/action/v3alpha/action_service.proto b/proto/zitadel/resources/action/v3alpha/action_service.proto index 08d57e93e5..228899310a 100644 --- a/proto/zitadel/resources/action/v3alpha/action_service.proto +++ b/proto/zitadel/resources/action/v3alpha/action_service.proto @@ -47,7 +47,7 @@ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { consumes: "application/grpc-web+proto"; produces: "application/grpc-web+proto"; - host: "${ZITADEL_DOMAIN}"; + host: "$CUSTOM-DOMAIN"; base_path: "/resources/v3alpha/actions"; external_docs: { @@ -60,8 +60,8 @@ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { value: { type: TYPE_OAUTH2; flow: FLOW_ACCESS_CODE; - authorization_url: "${ZITADEL_DOMAIN}/oauth/v2/authorize"; - token_url: "${ZITADEL_DOMAIN}/oauth/v2/token"; + authorization_url: "$CUSTOM-DOMAIN/oauth/v2/authorize"; + token_url: "$CUSTOM-DOMAIN/oauth/v2/token"; scopes: { scope: { key: "openid"; @@ -135,7 +135,7 @@ service ZITADELActions { description: "Target successfully created"; schema: { json_schema: { - ref: "#/definitions/CreateTargetResponse"; + ref: "#/definitions/v3alphaCreateTargetResponse"; } } }; @@ -278,7 +278,7 @@ service ZITADELActions { description: "Execution successfully updated or left unchanged"; schema: { json_schema: { - ref: "#/definitions/SetExecutionResponse"; + ref: "#/definitions/v3alphaSetExecutionResponse"; } } }; diff --git a/proto/zitadel/resources/webkey/v3alpha/webkey_service.proto b/proto/zitadel/resources/webkey/v3alpha/webkey_service.proto index c79424095b..72263c7b73 100644 --- a/proto/zitadel/resources/webkey/v3alpha/webkey_service.proto +++ b/proto/zitadel/resources/webkey/v3alpha/webkey_service.proto @@ -42,7 +42,7 @@ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { consumes: "application/grpc-web+proto"; produces: "application/grpc-web+proto"; - host: "${ZITADEL_DOMAIN}"; + host: "$CUSTOM-DOMAIN"; base_path: "/resources/v3alpha/web_keys"; external_docs: { From 5fab533e3708ff221b3f6d6b392f1ca1c49500a6 Mon Sep 17 00:00:00 2001 From: Stefan Benz <46600784+stebenz@users.noreply.github.com> Date: Thu, 15 Aug 2024 06:37:06 +0200 Subject: [PATCH 34/39] feat: org v2 ListOrganizations (#8411) # Which Problems Are Solved Org v2 service does not have a ListOrganizations endpoint. # How the Problems Are Solved Implement ListOrganizations endpoint. # Additional Changes - moved descriptions in the protos to comments - corrected the RemoveNoPermissions for the ListUsers, to get the correct TotalResults # Additional Context For new typescript login --- docs/docusaurus.config.js | 8 + docs/sidebars.js | 12 + internal/api/grpc/admin/export.go | 4 +- internal/api/grpc/admin/org.go | 4 +- internal/api/grpc/auth/user.go | 2 +- internal/api/grpc/management/org.go | 2 +- internal/api/grpc/management/user.go | 2 +- .../api/grpc/org/v2/org_integration_test.go | 16 +- internal/api/grpc/org/v2/query.go | 132 ++++++ .../api/grpc/org/v2/query_integration_test.go | 443 ++++++++++++++++++ internal/api/grpc/user/v2/query.go | 3 +- .../grpc/user/v2/query_integration_test.go | 8 +- internal/api/grpc/user/v2beta/query.go | 3 +- .../user/v2beta/query_integration_test.go | 4 + internal/api/ui/login/login.go | 2 +- internal/domain/permission.go | 1 + internal/integration/client.go | 34 ++ internal/query/org.go | 25 +- internal/query/org_test.go | 124 +++++ internal/query/user.go | 45 +- internal/query/user_test.go | 4 +- proto/zitadel/org/v2/org.proto | 43 ++ proto/zitadel/org/v2/org_service.proto | 62 ++- proto/zitadel/org/v2/query.proto | 83 ++++ proto/zitadel/user/v2/user_service.proto | 3 - 25 files changed, 1017 insertions(+), 52 deletions(-) create mode 100644 internal/api/grpc/org/v2/query.go create mode 100644 internal/api/grpc/org/v2/query_integration_test.go create mode 100644 proto/zitadel/org/v2/org.proto create mode 100644 proto/zitadel/org/v2/query.proto diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js index 3aad64d9ef..a07f6e8c78 100644 --- a/docs/docusaurus.config.js +++ b/docs/docusaurus.config.js @@ -356,6 +356,14 @@ module.exports = { categoryLinkSource: "auto", }, }, + org_v2: { + specPath: ".artifacts/openapi/zitadel/org/v2/org_service.swagger.json", + outputDir: "docs/apis/resources/org_service_v2", + sidebarOptions: { + groupPathsBy: "tag", + categoryLinkSource: "auto", + }, + }, idp_v2: { specPath: ".artifacts/openapi/zitadel/idp/v2/idp_service.swagger.json", outputDir: "docs/apis/resources/idp_service_v2", diff --git a/docs/sidebars.js b/docs/sidebars.js index 98667395f1..ffa910b54f 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -679,6 +679,18 @@ module.exports = { }, items: require("./docs/apis/resources/feature_service_v2/sidebar.ts"), }, + { + type: "category", + label: "Organization Lifecycle", + link: { + type: "generated-index", + title: "Organization Service API", + slug: "/apis/resources/org_service/v2", + description: + 'This API is intended to manage organizations for ZITADEL. \n' + }, + items: require("./docs/apis/resources/org_service_v2/sidebar.ts"), + }, { type: "category", label: "Identity Provider Lifecycle", diff --git a/internal/api/grpc/admin/export.go b/internal/api/grpc/admin/export.go index 9a8f075c7c..8394ae78d9 100644 --- a/internal/api/grpc/admin/export.go +++ b/internal/api/grpc/admin/export.go @@ -36,7 +36,7 @@ func (s *Server) ExportData(ctx context.Context, req *admin_pb.ExportDataRequest } orgSearchQuery.Queries = []query.SearchQuery{orgIDsSearchQuery} } - queriedOrgs, err := s.query.SearchOrgs(ctx, orgSearchQuery) + queriedOrgs, err := s.query.SearchOrgs(ctx, orgSearchQuery, nil) if err != nil { return nil, err } @@ -554,7 +554,7 @@ func (s *Server) getUsers(ctx context.Context, org string, withPasswords bool, w if err != nil { return nil, nil, nil, nil, err } - users, err := s.query.SearchUsers(ctx, &query.UserSearchQueries{Queries: []query.SearchQuery{orgSearch}}) + users, err := s.query.SearchUsers(ctx, &query.UserSearchQueries{Queries: []query.SearchQuery{orgSearch}}, nil) if err != nil { return nil, nil, nil, nil, err } diff --git a/internal/api/grpc/admin/org.go b/internal/api/grpc/admin/org.go index 81064cebff..934de1b570 100644 --- a/internal/api/grpc/admin/org.go +++ b/internal/api/grpc/admin/org.go @@ -59,7 +59,7 @@ func (s *Server) ListOrgs(ctx context.Context, req *admin_pb.ListOrgsRequest) (* if err != nil { return nil, err } - orgs, err := s.query.SearchOrgs(ctx, queries) + orgs, err := s.query.SearchOrgs(ctx, queries, nil) if err != nil { return nil, err } @@ -108,7 +108,7 @@ func (s *Server) getClaimedUserIDsOfOrgDomain(ctx context.Context, orgDomain str if err != nil { return nil, err } - users, err := s.query.SearchUsers(ctx, &query.UserSearchQueries{Queries: []query.SearchQuery{loginName}}) + users, err := s.query.SearchUsers(ctx, &query.UserSearchQueries{Queries: []query.SearchQuery{loginName}}, nil) if err != nil { return nil, err } diff --git a/internal/api/grpc/auth/user.go b/internal/api/grpc/auth/user.go index 7c874bf765..90e0ddc1d6 100644 --- a/internal/api/grpc/auth/user.go +++ b/internal/api/grpc/auth/user.go @@ -220,7 +220,7 @@ func (s *Server) ListMyProjectOrgs(ctx context.Context, req *auth_pb.ListMyProje } } - orgs, err := s.query.SearchOrgs(ctx, queries) + orgs, err := s.query.SearchOrgs(ctx, queries, nil) if err != nil { return nil, err } diff --git a/internal/api/grpc/management/org.go b/internal/api/grpc/management/org.go index 91ef8e3b84..d25d46d852 100644 --- a/internal/api/grpc/management/org.go +++ b/internal/api/grpc/management/org.go @@ -330,7 +330,7 @@ func (s *Server) getClaimedUserIDsOfOrgDomain(ctx context.Context, orgDomain, or } queries = append(queries, owner) } - users, err := s.query.SearchUsers(ctx, &query.UserSearchQueries{Queries: queries}) + users, err := s.query.SearchUsers(ctx, &query.UserSearchQueries{Queries: queries}, nil) if err != nil { return nil, err } diff --git a/internal/api/grpc/management/user.go b/internal/api/grpc/management/user.go index 64d99ea786..981e7823ab 100644 --- a/internal/api/grpc/management/user.go +++ b/internal/api/grpc/management/user.go @@ -68,7 +68,7 @@ func (s *Server) ListUsers(ctx context.Context, req *mgmt_pb.ListUsersRequest) ( if err != nil { return nil, err } - res, err := s.query.SearchUsers(ctx, queries) + res, err := s.query.SearchUsers(ctx, queries, nil) if err != nil { return nil, err } diff --git a/internal/api/grpc/org/v2/org_integration_test.go b/internal/api/grpc/org/v2/org_integration_test.go index 9f3f9fa64b..2005ea84b2 100644 --- a/internal/api/grpc/org/v2/org_integration_test.go +++ b/internal/api/grpc/org/v2/org_integration_test.go @@ -19,22 +19,26 @@ import ( ) var ( - CTX context.Context - Tester *integration.Tester - Client org.OrganizationServiceClient - User *user.AddHumanUserResponse + CTX context.Context + OwnerCTX context.Context + UserCTX context.Context + Tester *integration.Tester + Client org.OrganizationServiceClient + User *user.AddHumanUserResponse ) func TestMain(m *testing.M) { os.Exit(func() int { - ctx, errCtx, cancel := integration.Contexts(5 * time.Minute) + ctx, _, cancel := integration.Contexts(5 * time.Minute) defer cancel() Tester = integration.NewTester(ctx) defer Tester.Done() Client = Tester.Client.OrgV2 - CTX, _ = Tester.WithAuthorization(ctx, integration.IAMOwner), errCtx + CTX = Tester.WithAuthorization(ctx, integration.IAMOwner) + OwnerCTX = Tester.WithAuthorization(ctx, integration.OrgOwner) + UserCTX = Tester.WithAuthorization(ctx, integration.Login) User = Tester.CreateHumanUser(CTX) return m.Run() }()) diff --git a/internal/api/grpc/org/v2/query.go b/internal/api/grpc/org/v2/query.go new file mode 100644 index 0000000000..772726fd40 --- /dev/null +++ b/internal/api/grpc/org/v2/query.go @@ -0,0 +1,132 @@ +package org + +import ( + "context" + + "github.com/zitadel/zitadel/internal/api/grpc/object/v2" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/query" + "github.com/zitadel/zitadel/internal/zerrors" + "github.com/zitadel/zitadel/pkg/grpc/org/v2" +) + +func (s *Server) ListOrganizations(ctx context.Context, req *org.ListOrganizationsRequest) (*org.ListOrganizationsResponse, error) { + queries, err := listOrgRequestToModel(req) + if err != nil { + return nil, err + } + orgs, err := s.query.SearchOrgs(ctx, queries, s.checkPermission) + if err != nil { + return nil, err + } + return &org.ListOrganizationsResponse{ + Result: organizationsToPb(orgs.Orgs), + Details: object.ToListDetails(orgs.SearchResponse), + }, nil +} + +func listOrgRequestToModel(req *org.ListOrganizationsRequest) (*query.OrgSearchQueries, error) { + offset, limit, asc := object.ListQueryToQuery(req.Query) + queries, err := orgQueriesToQuery(req.Queries) + if err != nil { + return nil, err + } + return &query.OrgSearchQueries{ + SearchRequest: query.SearchRequest{ + Offset: offset, + Limit: limit, + SortingColumn: fieldNameToOrganizationColumn(req.SortingColumn), + Asc: asc, + }, + Queries: queries, + }, nil +} + +func orgQueriesToQuery(queries []*org.SearchQuery) (_ []query.SearchQuery, err error) { + q := make([]query.SearchQuery, len(queries)) + for i, query := range queries { + q[i], err = orgQueryToQuery(query) + if err != nil { + return nil, err + } + } + return q, nil +} + +func orgQueryToQuery(orgQuery *org.SearchQuery) (query.SearchQuery, error) { + switch q := orgQuery.Query.(type) { + case *org.SearchQuery_DomainQuery: + return query.NewOrgDomainSearchQuery(object.TextMethodToQuery(q.DomainQuery.Method), q.DomainQuery.Domain) + case *org.SearchQuery_NameQuery: + return query.NewOrgNameSearchQuery(object.TextMethodToQuery(q.NameQuery.Method), q.NameQuery.Name) + case *org.SearchQuery_StateQuery: + return query.NewOrgStateSearchQuery(orgStateToDomain(q.StateQuery.State)) + case *org.SearchQuery_IdQuery: + return query.NewOrgIDSearchQuery(q.IdQuery.Id) + default: + return nil, zerrors.ThrowInvalidArgument(nil, "ORG-vR9nC", "List.Query.Invalid") + } +} + +func orgStateToPb(state domain.OrgState) org.OrganizationState { + switch state { + case domain.OrgStateActive: + return org.OrganizationState_ORGANIZATION_STATE_ACTIVE + case domain.OrgStateInactive: + return org.OrganizationState_ORGANIZATION_STATE_INACTIVE + case domain.OrgStateRemoved: + return org.OrganizationState_ORGANIZATION_STATE_REMOVED + case domain.OrgStateUnspecified: + fallthrough + default: + return org.OrganizationState_ORGANIZATION_STATE_UNSPECIFIED + } +} + +func orgStateToDomain(state org.OrganizationState) domain.OrgState { + switch state { + case org.OrganizationState_ORGANIZATION_STATE_ACTIVE: + return domain.OrgStateActive + case org.OrganizationState_ORGANIZATION_STATE_INACTIVE: + return domain.OrgStateInactive + case org.OrganizationState_ORGANIZATION_STATE_REMOVED: + return domain.OrgStateRemoved + case org.OrganizationState_ORGANIZATION_STATE_UNSPECIFIED: + fallthrough + default: + return domain.OrgStateUnspecified + } +} + +func fieldNameToOrganizationColumn(fieldName org.OrganizationFieldName) query.Column { + switch fieldName { + case org.OrganizationFieldName_ORGANIZATION_FIELD_NAME_NAME: + return query.OrgColumnName + case org.OrganizationFieldName_ORGANIZATION_FIELD_NAME_UNSPECIFIED: + return query.Column{} + default: + return query.Column{} + } +} + +func organizationsToPb(orgs []*query.Org) []*org.Organization { + o := make([]*org.Organization, len(orgs)) + for i, org := range orgs { + o[i] = organizationToPb(org) + } + return o +} + +func organizationToPb(organization *query.Org) *org.Organization { + return &org.Organization{ + Id: organization.ID, + Name: organization.Name, + PrimaryDomain: organization.Domain, + Details: object.DomainToDetailsPb(&domain.ObjectDetails{ + Sequence: organization.Sequence, + EventDate: organization.ChangeDate, + ResourceOwner: organization.ResourceOwner, + }), + State: orgStateToPb(organization.State), + } +} diff --git a/internal/api/grpc/org/v2/query_integration_test.go b/internal/api/grpc/org/v2/query_integration_test.go new file mode 100644 index 0000000000..5110b8652d --- /dev/null +++ b/internal/api/grpc/org/v2/query_integration_test.go @@ -0,0 +1,443 @@ +//go:build integration + +package org_test + +import ( + "context" + "fmt" + "strconv" + "testing" + "time" + + "github.com/brianvoe/gofakeit/v6" + "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/org/v2" +) + +type orgAttr struct { + ID string + Name string + Details *object.Details +} + +func TestServer_ListOrganizations(t *testing.T) { + type args struct { + ctx context.Context + req *org.ListOrganizationsRequest + dep func(ctx context.Context, request *org.ListOrganizationsRequest) ([]orgAttr, error) + } + tests := []struct { + name string + args args + want *org.ListOrganizationsResponse + wantErr bool + }{ + { + name: "list org by id, ok, multiple", + args: args{ + CTX, + &org.ListOrganizationsRequest{ + Queries: []*org.SearchQuery{ + OrganizationIdQuery(Tester.Organisation.ID), + }, + }, + func(ctx context.Context, request *org.ListOrganizationsRequest) ([]orgAttr, error) { + count := 3 + orgs := make([]orgAttr, count) + prefix := fmt.Sprintf("ListOrgs%d", time.Now().UnixNano()) + for i := 0; i < count; i++ { + name := prefix + strconv.Itoa(i) + orgResp := Tester.CreateOrganization(ctx, name, fmt.Sprintf("%d@mouse.com", time.Now().UnixNano())) + orgs[i] = orgAttr{ + ID: orgResp.GetOrganizationId(), + Name: name, + Details: orgResp.GetDetails(), + } + } + request.Queries = []*org.SearchQuery{ + OrganizationNamePrefixQuery(prefix), + } + return orgs, nil + }, + }, + want: &org.ListOrganizationsResponse{ + Details: &object.ListDetails{ + TotalResult: 3, + Timestamp: timestamppb.Now(), + }, + SortingColumn: 0, + Result: []*org.Organization{ + { + State: org.OrganizationState_ORGANIZATION_STATE_ACTIVE, + }, + { + State: org.OrganizationState_ORGANIZATION_STATE_ACTIVE, + }, + { + State: org.OrganizationState_ORGANIZATION_STATE_ACTIVE, + }, + }, + }, + }, + { + name: "list org by id, ok", + args: args{ + CTX, + &org.ListOrganizationsRequest{ + Queries: []*org.SearchQuery{ + OrganizationIdQuery(Tester.Organisation.ID), + }, + }, + nil, + }, + want: &org.ListOrganizationsResponse{ + Details: &object.ListDetails{ + TotalResult: 1, + Timestamp: timestamppb.Now(), + }, + SortingColumn: 0, + Result: []*org.Organization{ + { + State: org.OrganizationState_ORGANIZATION_STATE_ACTIVE, + Name: Tester.Organisation.Name, + Details: &object.Details{ + Sequence: Tester.Organisation.Sequence, + ChangeDate: timestamppb.New(Tester.Organisation.ChangeDate), + ResourceOwner: Tester.Organisation.ResourceOwner, + }, + Id: Tester.Organisation.ID, + PrimaryDomain: Tester.Organisation.Domain, + }, + }, + }, + }, + { + name: "list org by name, ok", + args: args{ + CTX, + &org.ListOrganizationsRequest{ + Queries: []*org.SearchQuery{ + OrganizationNameQuery(Tester.Organisation.Name), + }, + }, + nil, + }, + want: &org.ListOrganizationsResponse{ + Details: &object.ListDetails{ + TotalResult: 1, + Timestamp: timestamppb.Now(), + }, + SortingColumn: 0, + Result: []*org.Organization{ + { + State: org.OrganizationState_ORGANIZATION_STATE_ACTIVE, + Name: Tester.Organisation.Name, + Details: &object.Details{ + Sequence: Tester.Organisation.Sequence, + ChangeDate: timestamppb.New(Tester.Organisation.ChangeDate), + ResourceOwner: Tester.Organisation.ResourceOwner, + }, + Id: Tester.Organisation.ID, + PrimaryDomain: Tester.Organisation.Domain, + }, + }, + }, + }, + { + name: "list org by domain, ok", + args: args{ + CTX, + &org.ListOrganizationsRequest{ + Queries: []*org.SearchQuery{ + OrganizationDomainQuery(Tester.Organisation.Domain), + }, + }, + nil, + }, + want: &org.ListOrganizationsResponse{ + Details: &object.ListDetails{ + TotalResult: 1, + Timestamp: timestamppb.Now(), + }, + SortingColumn: 0, + Result: []*org.Organization{ + { + State: org.OrganizationState_ORGANIZATION_STATE_ACTIVE, + Name: Tester.Organisation.Name, + Details: &object.Details{ + Sequence: Tester.Organisation.Sequence, + ChangeDate: timestamppb.New(Tester.Organisation.ChangeDate), + ResourceOwner: Tester.Organisation.ResourceOwner, + }, + Id: Tester.Organisation.ID, + PrimaryDomain: Tester.Organisation.Domain, + }, + }, + }, + }, + { + name: "list org by inactive state, ok", + args: args{ + CTX, + &org.ListOrganizationsRequest{ + Queries: []*org.SearchQuery{}, + }, + func(ctx context.Context, request *org.ListOrganizationsRequest) ([]orgAttr, error) { + name := gofakeit.Name() + orgResp := Tester.CreateOrganization(ctx, name, gofakeit.Email()) + deactivateOrgResp := Tester.DeactivateOrganization(ctx, orgResp.GetOrganizationId()) + request.Queries = []*org.SearchQuery{ + OrganizationIdQuery(orgResp.GetOrganizationId()), + OrganizationStateQuery(org.OrganizationState_ORGANIZATION_STATE_INACTIVE), + } + return []orgAttr{{ + ID: orgResp.GetOrganizationId(), + Name: name, + Details: &object.Details{ + ResourceOwner: deactivateOrgResp.GetDetails().GetResourceOwner(), + Sequence: deactivateOrgResp.GetDetails().GetSequence(), + ChangeDate: deactivateOrgResp.GetDetails().GetChangeDate(), + }, + }}, nil + }, + }, + want: &org.ListOrganizationsResponse{ + Details: &object.ListDetails{ + TotalResult: 1, + Timestamp: timestamppb.Now(), + }, + SortingColumn: 0, + Result: []*org.Organization{ + { + State: org.OrganizationState_ORGANIZATION_STATE_INACTIVE, + Details: &object.Details{}, + }, + }, + }, + }, + { + name: "list org by domain, ok, sorted", + args: args{ + CTX, + &org.ListOrganizationsRequest{ + Queries: []*org.SearchQuery{ + OrganizationDomainQuery(Tester.Organisation.Domain), + }, + SortingColumn: org.OrganizationFieldName_ORGANIZATION_FIELD_NAME_NAME, + }, + nil, + }, + want: &org.ListOrganizationsResponse{ + Details: &object.ListDetails{ + TotalResult: 1, + Timestamp: timestamppb.Now(), + }, + SortingColumn: 1, + Result: []*org.Organization{ + { + State: org.OrganizationState_ORGANIZATION_STATE_ACTIVE, + Name: Tester.Organisation.Name, + Details: &object.Details{ + Sequence: Tester.Organisation.Sequence, + ChangeDate: timestamppb.New(Tester.Organisation.ChangeDate), + ResourceOwner: Tester.Organisation.ResourceOwner, + }, + Id: Tester.Organisation.ID, + PrimaryDomain: Tester.Organisation.Domain, + }, + }, + }, + }, + { + name: "list org, no result", + args: args{ + CTX, + &org.ListOrganizationsRequest{ + Queries: []*org.SearchQuery{ + OrganizationDomainQuery("notexisting"), + }, + }, + nil, + }, + want: &org.ListOrganizationsResponse{ + Details: &object.ListDetails{ + TotalResult: 0, + Timestamp: timestamppb.Now(), + }, + SortingColumn: 0, + Result: []*org.Organization{}, + }, + }, + { + name: "list org, no login", + args: args{ + context.Background(), + &org.ListOrganizationsRequest{ + Queries: []*org.SearchQuery{ + OrganizationDomainQuery("nopermission"), + }, + }, + nil, + }, + wantErr: true, + }, + { + name: "list org, no permission", + args: args{ + UserCTX, + &org.ListOrganizationsRequest{}, + nil, + }, + want: &org.ListOrganizationsResponse{ + Details: &object.ListDetails{ + TotalResult: 0, + Timestamp: timestamppb.Now(), + }, + SortingColumn: 1, + Result: []*org.Organization{}, + }, + }, + { + name: "list org, no permission org owner", + args: args{ + OwnerCTX, + &org.ListOrganizationsRequest{ + Queries: []*org.SearchQuery{ + OrganizationDomainQuery("nopermission"), + }, + }, + nil, + }, + want: &org.ListOrganizationsResponse{ + Details: &object.ListDetails{ + TotalResult: 0, + Timestamp: timestamppb.Now(), + }, + SortingColumn: 1, + Result: []*org.Organization{}, + }, + }, + { + name: "list org, org owner", + args: args{ + OwnerCTX, + &org.ListOrganizationsRequest{}, + nil, + }, + want: &org.ListOrganizationsResponse{ + Details: &object.ListDetails{ + TotalResult: 1, + Timestamp: timestamppb.Now(), + }, + SortingColumn: 1, + Result: []*org.Organization{ + { + State: org.OrganizationState_ORGANIZATION_STATE_ACTIVE, + Name: Tester.Organisation.Name, + Details: &object.Details{ + Sequence: Tester.Organisation.Sequence, + ChangeDate: timestamppb.New(Tester.Organisation.ChangeDate), + ResourceOwner: Tester.Organisation.ResourceOwner, + }, + Id: Tester.Organisation.ID, + PrimaryDomain: Tester.Organisation.Domain, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.args.dep != nil { + orgs, err := tt.args.dep(tt.args.ctx, tt.args.req) + require.NoError(t, err) + if len(orgs) > 0 { + for i, org := range orgs { + tt.want.Result[i].Name = org.Name + tt.want.Result[i].Id = org.ID + tt.want.Result[i].Details = org.Details + } + } + } + + retryDuration := time.Minute + if ctxDeadline, ok := CTX.Deadline(); ok { + retryDuration = time.Until(ctxDeadline) + } + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + got, listErr := Client.ListOrganizations(tt.args.ctx, tt.args.req) + assertErr := assert.NoError + if tt.wantErr { + assertErr = assert.Error + } + assertErr(ttt, listErr) + if listErr != nil { + return + } + + // totalResult is unrelated to the tests here so gets carried over, can vary from the count of results due to permissions + tt.want.Details.TotalResult = got.Details.TotalResult + // always first check length, otherwise its failed anyway + assert.Len(ttt, got.Result, len(tt.want.Result)) + + for i := range tt.want.Result { + // domain from result, as it is generated though the create + tt.want.Result[i].PrimaryDomain = got.Result[i].PrimaryDomain + // sequence from result, as it can be with different sequence from create + tt.want.Result[i].Details.Sequence = got.Result[i].Details.Sequence + } + + for i := range tt.want.Result { + assert.Contains(ttt, got.Result, tt.want.Result[i]) + } + integration.AssertListDetails(t, tt.want, got) + }, retryDuration, time.Millisecond*100, "timeout waiting for expected user result") + }) + } +} + +func OrganizationIdQuery(resourceowner string) *org.SearchQuery { + return &org.SearchQuery{Query: &org.SearchQuery_IdQuery{ + IdQuery: &org.OrganizationIDQuery{ + Id: resourceowner, + }, + }} +} + +func OrganizationNameQuery(name string) *org.SearchQuery { + return &org.SearchQuery{Query: &org.SearchQuery_NameQuery{ + NameQuery: &org.OrganizationNameQuery{ + Name: name, + }, + }} +} + +func OrganizationNamePrefixQuery(name string) *org.SearchQuery { + return &org.SearchQuery{Query: &org.SearchQuery_NameQuery{ + NameQuery: &org.OrganizationNameQuery{ + Name: name, + Method: object.TextQueryMethod_TEXT_QUERY_METHOD_STARTS_WITH, + }, + }} +} + +func OrganizationDomainQuery(domain string) *org.SearchQuery { + return &org.SearchQuery{Query: &org.SearchQuery_DomainQuery{ + DomainQuery: &org.OrganizationDomainQuery{ + Domain: domain, + }, + }} +} + +func OrganizationStateQuery(state org.OrganizationState) *org.SearchQuery { + return &org.SearchQuery{Query: &org.SearchQuery_StateQuery{ + StateQuery: &org.OrganizationStateQuery{ + State: state, + }, + }} +} diff --git a/internal/api/grpc/user/v2/query.go b/internal/api/grpc/user/v2/query.go index 95262a66ae..d40e4d47d9 100644 --- a/internal/api/grpc/user/v2/query.go +++ b/internal/api/grpc/user/v2/query.go @@ -39,11 +39,10 @@ func (s *Server) ListUsers(ctx context.Context, req *user.ListUsersRequest) (*us if err != nil { return nil, err } - res, err := s.query.SearchUsers(ctx, queries) + res, err := s.query.SearchUsers(ctx, queries, s.checkPermission) if err != nil { return nil, err } - res.RemoveNoPermission(ctx, s.checkPermission) return &user.ListUsersResponse{ Result: UsersToPb(res.Users, s.assetAPIPrefix(ctx)), Details: object.ToListDetails(res.SearchResponse), diff --git a/internal/api/grpc/user/v2/query_integration_test.go b/internal/api/grpc/user/v2/query_integration_test.go index f7fcf8fbe5..7509a9d430 100644 --- a/internal/api/grpc/user/v2/query_integration_test.go +++ b/internal/api/grpc/user/v2/query_integration_test.go @@ -13,8 +13,8 @@ import ( "github.com/stretchr/testify/require" "google.golang.org/protobuf/types/known/timestamppb" - object "github.com/zitadel/zitadel/pkg/grpc/object/v2" - user "github.com/zitadel/zitadel/pkg/grpc/user/v2" + "github.com/zitadel/zitadel/pkg/grpc/object/v2" + "github.com/zitadel/zitadel/pkg/grpc/user/v2" "github.com/zitadel/zitadel/internal/integration" ) @@ -914,6 +914,10 @@ func TestServer_ListUsers(t *testing.T) { assert.Len(ttt, tt.want.Result, len(infos)) // always first check length, otherwise its failed anyway assert.Len(ttt, got.Result, len(tt.want.Result)) + + // totalResult is unrelated to the tests here so gets carried over, can vary from the count of results due to permissions + tt.want.Details.TotalResult = got.Details.TotalResult + // fill in userid and username as it is generated for i := range infos { tt.want.Result[i].UserId = infos[i].UserID diff --git a/internal/api/grpc/user/v2beta/query.go b/internal/api/grpc/user/v2beta/query.go index 0eaeba5ca1..28e0a0c2e7 100644 --- a/internal/api/grpc/user/v2beta/query.go +++ b/internal/api/grpc/user/v2beta/query.go @@ -39,11 +39,10 @@ func (s *Server) ListUsers(ctx context.Context, req *user.ListUsersRequest) (*us if err != nil { return nil, err } - res, err := s.query.SearchUsers(ctx, queries) + res, err := s.query.SearchUsers(ctx, queries, s.checkPermission) if err != nil { return nil, err } - res.RemoveNoPermission(ctx, s.checkPermission) return &user.ListUsersResponse{ Result: UsersToPb(res.Users, s.assetAPIPrefix(ctx)), Details: object.ToListDetails(res.SearchResponse), diff --git a/internal/api/grpc/user/v2beta/query_integration_test.go b/internal/api/grpc/user/v2beta/query_integration_test.go index 124e47bb27..1b375e4091 100644 --- a/internal/api/grpc/user/v2beta/query_integration_test.go +++ b/internal/api/grpc/user/v2beta/query_integration_test.go @@ -923,6 +923,10 @@ func TestServer_ListUsers(t *testing.T) { // always first check length, otherwise its failed anyway assert.Len(ttt, got.Result, len(tt.want.Result)) // fill in userid and username as it is generated + + // totalResult is unrelated to the tests here so gets carried over, can vary from the count of results due to permissions + tt.want.Details.TotalResult = got.Details.TotalResult + for i := range infos { tt.want.Result[i].UserId = infos[i].UserID tt.want.Result[i].Username = infos[i].Username diff --git a/internal/api/ui/login/login.go b/internal/api/ui/login/login.go index bda1ecac59..57f6a5f9a3 100644 --- a/internal/api/ui/login/login.go +++ b/internal/api/ui/login/login.go @@ -171,7 +171,7 @@ func (l *Login) getClaimedUserIDsOfOrgDomain(ctx context.Context, orgName string if err != nil { return nil, err } - users, err := l.query.SearchUsers(ctx, &query.UserSearchQueries{Queries: []query.SearchQuery{loginName}}) + users, err := l.query.SearchUsers(ctx, &query.UserSearchQueries{Queries: []query.SearchQuery{loginName}}, nil) if err != nil { return nil, err } diff --git a/internal/domain/permission.go b/internal/domain/permission.go index 75d7f792ff..7e0cfcfc89 100644 --- a/internal/domain/permission.go +++ b/internal/domain/permission.go @@ -33,6 +33,7 @@ const ( PermissionUserCredentialWrite = "user.credential.write" PermissionSessionWrite = "session.write" PermissionSessionDelete = "session.delete" + PermissionOrgRead = "org.read" PermissionIDPRead = "iam.idp.read" PermissionOrgIDPRead = "org.idp.read" ) diff --git a/internal/integration/client.go b/internal/integration/client.go index a69ce6a1ee..55f80938bf 100644 --- a/internal/integration/client.go +++ b/internal/integration/client.go @@ -15,6 +15,7 @@ import ( "golang.org/x/oauth2" "golang.org/x/text/language" "google.golang.org/grpc" + "google.golang.org/grpc/metadata" "google.golang.org/protobuf/types/known/durationpb" "google.golang.org/protobuf/types/known/structpb" @@ -258,6 +259,39 @@ func (s *Tester) CreateOrganization(ctx context.Context, name, adminEmail string return resp } +func (s *Tester) DeactivateOrganization(ctx context.Context, orgID string) *mgmt.DeactivateOrgResponse { + resp, err := s.Client.Mgmt.DeactivateOrg( + SetOrgID(ctx, orgID), + &mgmt.DeactivateOrgRequest{}, + ) + logging.OnError(err).Fatal("deactivate org") + return resp +} + +func SetOrgID(ctx context.Context, orgID string) context.Context { + md, ok := metadata.FromOutgoingContext(ctx) + if !ok { + return metadata.AppendToOutgoingContext(ctx, "x-zitadel-orgid", orgID) + } + md.Set("x-zitadel-orgid", orgID) + return metadata.NewOutgoingContext(ctx, md) +} + +func (s *Tester) CreateOrganizationWithUserID(ctx context.Context, name, userID string) *org.AddOrganizationResponse { + resp, err := s.Client.OrgV2.AddOrganization(ctx, &org.AddOrganizationRequest{ + Name: name, + Admins: []*org.AddOrganizationRequest_Admin{ + { + UserType: &org.AddOrganizationRequest_Admin_UserId{ + UserId: userID, + }, + }, + }, + }) + logging.OnError(err).Fatal("create org") + return resp +} + func (s *Tester) CreateHumanUserVerified(ctx context.Context, org, email string) *user.AddHumanUserResponse { resp, err := s.Client.UserV2.AddHumanUser(ctx, &user.AddHumanUserRequest{ Organization: &object.Organization{ diff --git a/internal/query/org.go b/internal/query/org.go index 9bff752cb1..50e2d4dbde 100644 --- a/internal/query/org.go +++ b/internal/query/org.go @@ -4,6 +4,7 @@ import ( "context" "database/sql" "errors" + "slices" "time" sq "github.com/Masterminds/squirrel" @@ -82,6 +83,17 @@ type Org struct { Domain string } +func orgsCheckPermission(ctx context.Context, orgs *Orgs, permissionCheck domain_pkg.PermissionCheck) { + orgs.Orgs = slices.DeleteFunc(orgs.Orgs, + func(org *Org) bool { + if err := permissionCheck(ctx, domain_pkg.PermissionOrgRead, org.ID, org.ID); err != nil { + return true + } + return false + }, + ) +} + type OrgSearchQueries struct { SearchRequest Queries []SearchQuery @@ -254,7 +266,18 @@ func (q *Queries) ExistsOrg(ctx context.Context, id, domain string) (verifiedID return org.ID, nil } -func (q *Queries) SearchOrgs(ctx context.Context, queries *OrgSearchQueries) (orgs *Orgs, err error) { +func (q *Queries) SearchOrgs(ctx context.Context, queries *OrgSearchQueries, permissionCheck domain_pkg.PermissionCheck) (*Orgs, error) { + orgs, err := q.searchOrgs(ctx, queries) + if err != nil { + return nil, err + } + if permissionCheck != nil { + orgsCheckPermission(ctx, orgs, permissionCheck) + } + return orgs, nil +} + +func (q *Queries) searchOrgs(ctx context.Context, queries *OrgSearchQueries) (orgs *Orgs, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() diff --git a/internal/query/org_test.go b/internal/query/org_test.go index 6923156390..b8c6073dbe 100644 --- a/internal/query/org_test.go +++ b/internal/query/org_test.go @@ -10,6 +10,7 @@ import ( "testing" "github.com/DATA-DOG/go-sqlmock" + "github.com/stretchr/testify/require" "github.com/zitadel/zitadel/internal/database" db_mock "github.com/zitadel/zitadel/internal/database/mock" @@ -441,3 +442,126 @@ func TestQueries_IsOrgUnique(t *testing.T) { } } + +func TestOrg_RemoveNoPermission(t *testing.T) { + type want struct { + orgs []*Org + } + tests := []struct { + name string + want want + orgs *Orgs + permissions []string + }{ + { + "permissions for all", + want{ + orgs: []*Org{ + {ID: "first"}, {ID: "second"}, {ID: "third"}, + }, + }, + &Orgs{ + Orgs: []*Org{ + {ID: "first"}, {ID: "second"}, {ID: "third"}, + }, + }, + []string{"first", "second", "third"}, + }, + { + "permissions for one, first", + want{ + orgs: []*Org{ + {ID: "first"}, + }, + }, + &Orgs{ + Orgs: []*Org{ + {ID: "first"}, {ID: "second"}, {ID: "third"}, + }, + }, + []string{"first"}, + }, + { + "permissions for one, second", + want{ + orgs: []*Org{ + {ID: "second"}, + }, + }, + &Orgs{ + Orgs: []*Org{ + {ID: "first"}, {ID: "second"}, {ID: "third"}, + }, + }, + []string{"second"}, + }, + { + "permissions for one, third", + want{ + orgs: []*Org{ + {ID: "third"}, + }, + }, + &Orgs{ + Orgs: []*Org{ + {ID: "first"}, {ID: "second"}, {ID: "third"}, + }, + }, + []string{"third"}, + }, + { + "permissions for two, first third", + want{ + orgs: []*Org{ + {ID: "first"}, {ID: "third"}, + }, + }, + &Orgs{ + Orgs: []*Org{ + {ID: "first"}, {ID: "second"}, {ID: "third"}, + }, + }, + []string{"first", "third"}, + }, + { + "permissions for two, second third", + want{ + orgs: []*Org{ + {ID: "second"}, {ID: "third"}, + }, + }, + &Orgs{ + Orgs: []*Org{ + {ID: "first"}, {ID: "second"}, {ID: "third"}, + }, + }, + []string{"second", "third"}, + }, + { + "no permissions", + want{ + orgs: []*Org{}, + }, + &Orgs{ + Orgs: []*Org{ + {ID: "first"}, {ID: "second"}, {ID: "third"}, + }, + }, + []string{}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + checkPermission := func(ctx context.Context, permission, orgID, resourceID string) (err error) { + for _, perm := range tt.permissions { + if resourceID == perm { + return nil + } + } + return errors.New("failed") + } + orgsCheckPermission(context.Background(), tt.orgs, checkPermission) + require.Equal(t, tt.want.orgs, tt.orgs.Orgs) + }) + } +} diff --git a/internal/query/user.go b/internal/query/user.go index bb3758b17b..497cd89d6d 100644 --- a/internal/query/user.go +++ b/internal/query/user.go @@ -5,6 +5,7 @@ import ( "database/sql" _ "embed" "errors" + "slices" "strings" "time" @@ -123,27 +124,18 @@ type NotifyUser struct { PasswordSet bool } -func (u *Users) RemoveNoPermission(ctx context.Context, permissionCheck domain.PermissionCheck) { - removableIndexes := make([]int, 0) - for i := range u.Users { - ctxData := authz.GetCtxData(ctx) - if ctxData.UserID != u.Users[i].ID { - if err := permissionCheck(ctx, domain.PermissionUserRead, u.Users[i].ResourceOwner, u.Users[i].ID); err != nil { - removableIndexes = append(removableIndexes, i) +func usersCheckPermission(ctx context.Context, users *Users, permissionCheck domain.PermissionCheck) { + ctxData := authz.GetCtxData(ctx) + users.Users = slices.DeleteFunc(users.Users, + func(user *User) bool { + if ctxData.UserID != user.ID { + if err := permissionCheck(ctx, domain.PermissionUserRead, user.ResourceOwner, user.ID); err != nil { + return true + } } - } - } - removed := 0 - for _, removeIndex := range removableIndexes { - u.Users = removeUser(u.Users, removeIndex-removed) - removed++ - } - // reset count as some users could be removed - u.SearchResponse.Count = uint64(len(u.Users)) -} - -func removeUser(slice []*User, s int) []*User { - return append(slice[:s], slice[s+1:]...) + return false + }, + ) } type UserSearchQueries struct { @@ -597,7 +589,18 @@ func (q *Queries) GetNotifyUser(ctx context.Context, shouldTriggered bool, queri return user, err } -func (q *Queries) SearchUsers(ctx context.Context, queries *UserSearchQueries) (users *Users, err error) { +func (q *Queries) SearchUsers(ctx context.Context, queries *UserSearchQueries, permissionCheck domain.PermissionCheck) (*Users, error) { + users, err := q.searchUsers(ctx, queries) + if err != nil { + return nil, err + } + if permissionCheck != nil { + usersCheckPermission(ctx, users, permissionCheck) + } + return users, nil +} + +func (q *Queries) searchUsers(ctx context.Context, queries *UserSearchQueries) (users *Users, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() diff --git a/internal/query/user_test.go b/internal/query/user_test.go index 04271fb649..0a6b0c36e7 100644 --- a/internal/query/user_test.go +++ b/internal/query/user_test.go @@ -17,7 +17,7 @@ import ( "github.com/zitadel/zitadel/internal/zerrors" ) -func Test_RemoveNoPermission(t *testing.T) { +func TestUser_RemoveNoPermission(t *testing.T) { type want struct { users []*User } @@ -134,7 +134,7 @@ func Test_RemoveNoPermission(t *testing.T) { } return errors.New("failed") } - tt.users.RemoveNoPermission(context.Background(), checkPermission) + usersCheckPermission(context.Background(), tt.users, checkPermission) require.Equal(t, tt.want.users, tt.users.Users) }) } diff --git a/proto/zitadel/org/v2/org.proto b/proto/zitadel/org/v2/org.proto new file mode 100644 index 0000000000..1f23b85eb5 --- /dev/null +++ b/proto/zitadel/org/v2/org.proto @@ -0,0 +1,43 @@ +syntax = "proto3"; + +package zitadel.org.v2; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/org/v2;org"; + +import "google/api/field_behavior.proto"; +import "google/protobuf/timestamp.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; +import "validate/validate.proto"; +import "zitadel/object/v2/object.proto"; + + +message Organization { + // Unique identifier of the organization. + string id = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629023906488334\"" + } + ]; + zitadel.object.v2.Details details = 2; + // Current state of the organization, for example active, inactive and deleted. + OrganizationState state = 3; + // Name of the organization. + string name = 4 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"ZITADEL\""; + } + ]; + // Primary domain used in the organization. + string primary_domain = 5 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"zitadel.cloud\""; + } + ]; +} + +enum OrganizationState { + ORGANIZATION_STATE_UNSPECIFIED = 0; + ORGANIZATION_STATE_ACTIVE = 1; + ORGANIZATION_STATE_INACTIVE = 2; + ORGANIZATION_STATE_REMOVED = 3; +} \ No newline at end of file diff --git a/proto/zitadel/org/v2/org_service.proto b/proto/zitadel/org/v2/org_service.proto index 9b0006c46f..3917fc85a6 100644 --- a/proto/zitadel/org/v2/org_service.proto +++ b/proto/zitadel/org/v2/org_service.proto @@ -1,6 +1,5 @@ syntax = "proto3"; - package zitadel.org.v2; import "zitadel/object/v2/object.proto"; @@ -18,12 +17,14 @@ import "google/protobuf/duration.proto"; import "google/protobuf/struct.proto"; import "protoc-gen-openapiv2/options/annotations.proto"; import "validate/validate.proto"; +import "zitadel/org/v2/org.proto"; +import "zitadel/org/v2/query.proto"; option go_package = "github.com/zitadel/zitadel/pkg/grpc/org/v2;org"; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { info: { - title: "User Service"; + title: "Organization Service"; version: "2.0"; description: "This API is intended to manage organizations in a ZITADEL instance."; contact:{ @@ -111,7 +112,9 @@ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { service OrganizationService { - // Create a new organization and grant the user(s) permission to manage it + // Create an 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. rpc AddOrganization(AddOrganizationRequest) returns (AddOrganizationResponse) { option (google.api.http) = { post: "/v2/organizations" @@ -128,8 +131,6 @@ service OrganizationService { }; 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." responses: { key: "200" value: { @@ -138,6 +139,42 @@ service OrganizationService { }; }; } + + // Search Organizations + // + // Search for Organizations. By default, we will return all organization of the instance. Make sure to include a limit and sorting for pagination.. + rpc ListOrganizations(ListOrganizationsRequest) returns (ListOrganizationsResponse) { + option (google.api.http) = { + post: "/v2/organizations/_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 organizations matching the query"; + } + }; + responses: { + key: "400"; + value: { + description: "invalid list query"; + schema: { + json_schema: { + ref: "#/definitions/rpcStatus"; + }; + }; + }; + }; + }; + } } message AddOrganizationRequest{ @@ -172,3 +209,18 @@ message AddOrganizationResponse{ string organization_id = 2; repeated CreatedAdmin created_admins = 3; } + +message ListOrganizationsRequest { + //list limitations and ordering + zitadel.object.v2.ListQuery query = 1; + // the field the result is sorted + zitadel.org.v2.OrganizationFieldName sorting_column = 2; + //criteria the client is looking for + repeated zitadel.org.v2.SearchQuery queries = 3; +} + +message ListOrganizationsResponse { + zitadel.object.v2.ListDetails details = 1; + zitadel.org.v2.OrganizationFieldName sorting_column = 2; + repeated zitadel.org.v2.Organization result = 3; +} diff --git a/proto/zitadel/org/v2/query.proto b/proto/zitadel/org/v2/query.proto new file mode 100644 index 0000000000..bf0d8fb92a --- /dev/null +++ b/proto/zitadel/org/v2/query.proto @@ -0,0 +1,83 @@ +syntax = "proto3"; + +package zitadel.org.v2; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/org/v2;org"; + +import "google/api/annotations.proto"; +import "google/api/field_behavior.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; +import "validate/validate.proto"; +import "zitadel/org/v2/org.proto"; +import "zitadel/object/v2/object.proto"; + + +message SearchQuery { + oneof query { + option (validate.required) = true; + + OrganizationNameQuery name_query = 1; + OrganizationDomainQuery domain_query = 2; + OrganizationStateQuery state_query = 3; + OrganizationIDQuery id_query = 4; + } +} + +message OrganizationNameQuery { + // Name of the organization. + string name = 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: "\"gigi-giraffe\""; + } + ]; + // Defines which text equality method is used. + zitadel.object.v2.TextQueryMethod method = 2 [ + (validate.rules).enum.defined_only = true + ]; +} + +message OrganizationDomainQuery { + // Domain used in organization, not necessary primary domain. + string domain = 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: "\"citadel.cloud\""; + } + ]; + // Defines which text equality method is used. + zitadel.object.v2.TextQueryMethod method = 2 [ + (validate.rules).enum.defined_only = true + ]; +} + +message OrganizationStateQuery { + // Current state of the organization. + OrganizationState state = 1 [ + (validate.rules).enum.defined_only = true + ]; +} + +message OrganizationIDQuery { + // Unique identifier of the organization. + 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: "\"69629023906488334\"" + } + ]; +} + +enum OrganizationFieldName { + ORGANIZATION_FIELD_NAME_UNSPECIFIED = 0; + ORGANIZATION_FIELD_NAME_NAME = 1; +} \ No newline at end of file diff --git a/proto/zitadel/user/v2/user_service.proto b/proto/zitadel/user/v2/user_service.proto index 11230594dc..214c600bda 100644 --- a/proto/zitadel/user/v2/user_service.proto +++ b/proto/zitadel/user/v2/user_service.proto @@ -179,9 +179,6 @@ service UserService { auth_option: { permission: "authenticated" } - http_response: { - success_code: 200 - } }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { From 0af37d45e9ad1c6b8fe22f4b4f4629d57d22b58b Mon Sep 17 00:00:00 2001 From: Livio Spring Date: Thu, 15 Aug 2024 07:39:54 +0200 Subject: [PATCH 35/39] fix: handle user remove correctly in v1 sessions for login (#8432) # Which Problems Are Solved In case a user was deleted and recreated with the same id, they would never be able to authenticate through the login UI, since it would return an error "User not active". This was due to the check in the auth request / session handling for the login UI, where the user removed event would terminate an further event check and ignore the newly added user. # How the Problems Are Solved - The user removed event no longer returns an error, but is handled as a session termination event. (A user removed event will already delete the user and the preceding `activeUserById` function will deny the authentication.) # Additional Changes Updated tests to be able to handle multiple events in the mocks. # Additional Context closes https://github.com/zitadel/zitadel/issues/8201 Co-authored-by: Silvan --- .../eventsourcing/eventstore/auth_request.go | 2 - .../eventstore/auth_request_test.go | 141 ++++++++++++------ .../repository/view/model/user_session.go | 3 +- 3 files changed, 99 insertions(+), 47 deletions(-) diff --git a/internal/auth/repository/eventsourcing/eventstore/auth_request.go b/internal/auth/repository/eventsourcing/eventstore/auth_request.go index dda1a5fdbe..bef602ff03 100644 --- a/internal/auth/repository/eventsourcing/eventstore/auth_request.go +++ b/internal/auth/repository/eventsourcing/eventstore/auth_request.go @@ -1615,8 +1615,6 @@ func userSessionByIDs(ctx context.Context, provider userSessionViewProvider, eve if userAgentID != agentID { continue } - case user_repo.UserRemovedType: - return nil, zerrors.ThrowPreconditionFailed(nil, "EVENT-dG2fe", "Errors.User.NotActive") } err := sessionCopy.AppendEvent(event) logging.WithFields("traceID", tracing.TraceIDFromCtx(ctx)).OnError(err).Warn("error appending event") diff --git a/internal/auth/repository/eventsourcing/eventstore/auth_request_test.go b/internal/auth/repository/eventsourcing/eventstore/auth_request_test.go index c99ddc6f10..7308d6ce13 100644 --- a/internal/auth/repository/eventsourcing/eventstore/auth_request_test.go +++ b/internal/auth/repository/eventsourcing/eventstore/auth_request_test.go @@ -110,15 +110,12 @@ func (m *mockViewNoUser) UserByID(context.Context, string, string) (*user_view_m } type mockEventUser struct { - Event eventstore.Event + Events []eventstore.Event CodeExists bool } func (m *mockEventUser) UserEventsByID(ctx context.Context, id string, changeDate time.Time, types []eventstore.EventType) ([]eventstore.Event, error) { - if m.Event != nil { - return []eventstore.Event{m.Event}, nil - } - return nil, nil + return m.Events, nil } func (m *mockEventUser) PasswordCodeExists(ctx context.Context, userID string) (bool, error) { @@ -725,9 +722,11 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { fields{ userViewProvider: &mockViewUser{}, userEventProvider: &mockEventUser{ - Event: &es_models.Event{ - AggregateType: user_repo.AggregateType, - Typ: user_repo.UserDeactivatedType, + Events: []eventstore.Event{ + &es_models.Event{ + AggregateType: user_repo.AggregateType, + Typ: user_repo.UserDeactivatedType, + }, }, }, orgViewProvider: &mockViewOrg{State: domain.OrgStateActive}, @@ -747,9 +746,11 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { fields{ userViewProvider: &mockViewUser{}, userEventProvider: &mockEventUser{ - Event: &es_models.Event{ - AggregateType: user_repo.AggregateType, - Typ: user_repo.UserLockedType, + Events: []eventstore.Event{ + &es_models.Event{ + AggregateType: user_repo.AggregateType, + Typ: user_repo.UserLockedType, + }, }, }, orgViewProvider: &mockViewOrg{State: domain.OrgStateActive}, @@ -2290,10 +2291,12 @@ func Test_userSessionByIDs(t *testing.T) { agentID: "agentID", user: &user_model.UserView{ID: "id", HumanView: &user_model.HumanView{FirstName: "FirstName"}}, eventProvider: &mockEventUser{ - Event: &es_models.Event{ - AggregateType: user_repo.AggregateType, - Typ: user_repo.UserV1MFAOTPCheckSucceededType, - CreationDate: testNow, + Events: []eventstore.Event{ + &es_models.Event{ + AggregateType: user_repo.AggregateType, + Typ: user_repo.UserV1MFAOTPCheckSucceededType, + CreationDate: testNow, + }, }, }, }, @@ -2313,14 +2316,16 @@ func Test_userSessionByIDs(t *testing.T) { agentID: "agentID", user: &user_model.UserView{ID: "id"}, eventProvider: &mockEventUser{ - Event: &es_models.Event{ - AggregateType: user_repo.AggregateType, - Typ: user_repo.UserV1MFAOTPCheckSucceededType, - CreationDate: testNow, - Data: func() []byte { - data, _ := json.Marshal(&user_es_model.AuthRequest{UserAgentID: "otherID"}) - return data - }(), + Events: []eventstore.Event{ + &es_models.Event{ + AggregateType: user_repo.AggregateType, + Typ: user_repo.UserV1MFAOTPCheckSucceededType, + CreationDate: testNow, + Data: func() []byte { + data, _ := json.Marshal(&user_es_model.AuthRequest{UserAgentID: "otherID"}) + return data + }(), + }, }, }, }, @@ -2340,14 +2345,16 @@ func Test_userSessionByIDs(t *testing.T) { agentID: "agentID", user: &user_model.UserView{ID: "id", HumanView: &user_model.HumanView{FirstName: "FirstName"}}, eventProvider: &mockEventUser{ - Event: &es_models.Event{ - AggregateType: user_repo.AggregateType, - Typ: user_repo.UserV1MFAOTPCheckSucceededType, - CreationDate: testNow, - Data: func() []byte { - data, _ := json.Marshal(&user_es_model.AuthRequest{UserAgentID: "agentID"}) - return data - }(), + Events: []eventstore.Event{ + &es_models.Event{ + AggregateType: user_repo.AggregateType, + Typ: user_repo.UserV1MFAOTPCheckSucceededType, + CreationDate: testNow, + Data: func() []byte { + data, _ := json.Marshal(&user_es_model.AuthRequest{UserAgentID: "agentID"}) + return data + }(), + }, }, }, }, @@ -2359,7 +2366,7 @@ func Test_userSessionByIDs(t *testing.T) { nil, }, { - "new user events (user deleted), precondition failed error", + "new user events (user deleted), session terminated", args{ userProvider: &mockViewUserSession{ PasswordVerification: testNow, @@ -2367,14 +2374,57 @@ func Test_userSessionByIDs(t *testing.T) { agentID: "agentID", user: &user_model.UserView{ID: "id"}, eventProvider: &mockEventUser{ - Event: &es_models.Event{ - AggregateType: user_repo.AggregateType, - Typ: user_repo.UserRemovedType, + Events: []eventstore.Event{ + &es_models.Event{ + AggregateType: user_repo.AggregateType, + Typ: user_repo.UserRemovedType, + CreationDate: testNow, + }, }, }, }, + &user_model.UserSessionView{ + ChangeDate: testNow, + State: domain.UserSessionStateTerminated, + }, + nil, + }, + { + "new user events (user deleted, readded and password checked)", + args{ + userProvider: &mockViewUserSession{ + PasswordVerification: testNow, + }, + agentID: "agentID", + user: &user_model.UserView{ID: "id"}, + eventProvider: &mockEventUser{ + Events: []eventstore.Event{ + &es_models.Event{ + AggregateType: user_repo.AggregateType, + Typ: user_repo.UserRemovedType, + }, + &es_models.Event{ + AggregateType: user_repo.AggregateType, + Typ: user_repo.HumanAddedType, + }, + &es_models.Event{ + AggregateType: user_repo.AggregateType, + Typ: user_repo.HumanPasswordCheckSucceededType, + CreationDate: testNow, + Data: func() []byte { + data, _ := json.Marshal(&user_es_model.AuthRequest{UserAgentID: "agentID"}) + return data + }(), + }, + }, + }, + }, + &user_model.UserSessionView{ + ChangeDate: testNow, + PasswordVerification: testNow, + State: domain.UserSessionStateActive, + }, nil, - zerrors.IsPreconditionFailed, }, } for _, tt := range tests { @@ -2456,14 +2506,16 @@ func Test_userByID(t *testing.T) { PasswordChangeRequired: true, }, eventProvider: &mockEventUser{ - Event: &es_models.Event{ - AggregateType: user_repo.AggregateType, - Typ: user_repo.UserV1PasswordChangedType, - CreationDate: testNow, - Data: func() []byte { - data, _ := json.Marshal(user_es_model.Password{ChangeRequired: false, Secret: &crypto.CryptoValue{}}) - return data - }(), + Events: []eventstore.Event{ + &es_models.Event{ + AggregateType: user_repo.AggregateType, + Typ: user_repo.UserV1PasswordChangedType, + CreationDate: testNow, + Data: func() []byte { + data, _ := json.Marshal(user_es_model.Password{ChangeRequired: false, Secret: &crypto.CryptoValue{}}) + return data + }(), + }, }, }, }, @@ -2552,6 +2604,7 @@ func TestAuthRequestRepo_VerifyPassword_IgnoreUnknownUsernames(t *testing.T) { a.SetPolicyOrgID("instance1") return a } + type fields struct { AuthRequests func(*testing.T, string) cache.AuthRequestCache UserViewProvider userViewProvider diff --git a/internal/user/repository/view/model/user_session.go b/internal/user/repository/view/model/user_session.go index d761592551..48eafd50a6 100644 --- a/internal/user/repository/view/model/user_session.go +++ b/internal/user/repository/view/model/user_session.go @@ -199,7 +199,8 @@ func (v *UserSessionView) AppendEvent(event eventstore.Event) error { case user.UserV1SignedOutType, user.HumanSignedOutType, user.UserLockedType, - user.UserDeactivatedType: + user.UserDeactivatedType, + user.UserRemovedType: v.PasswordlessVerification = sql.NullTime{Time: time.Time{}, Valid: true} v.PasswordVerification = sql.NullTime{Time: time.Time{}, Valid: true} v.SecondFactorVerification = sql.NullTime{Time: time.Time{}, Valid: true} From 11d01b9b356f9f335ccb08f76bb98ec34287b384 Mon Sep 17 00:00:00 2001 From: Livio Spring Date: Fri, 16 Aug 2024 00:08:52 +0200 Subject: [PATCH 36/39] fix(console): allow user filtering with read permission (#8152) # Which Problems Are Solved The filter option was not displayed on the user list page for users who only have `user.read` permission, e.g. an IAM_OWNER_VIEWER or ORG_OWNER_VIEWER # How the Problems Are Solved - Filter is correctly displayed. # Additional Changes None. # Additional Context - noticed by a customer - needs backports --- .../user-list/user-table/user-table.component.html | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/console/src/app/pages/users/user-list/user-table/user-table.component.html b/console/src/app/pages/users/user-list/user-table/user-table.component.html index d7bfecb8ad..cc81a4d4c0 100644 --- a/console/src/app/pages/users/user-list/user-table/user-table.component.html +++ b/console/src/app/pages/users/user-list/user-table/user-table.component.html @@ -57,11 +57,14 @@
- + + +