From 016676e1dc21f031eb3819179f7c2827ad30ca3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Thu, 26 Jun 2025 19:17:45 +0300 Subject: [PATCH 1/3] chore(oidc): graduate webkey to stable (#10122) # Which Problems Are Solved Stabilize the usage of webkeys. # How the Problems Are Solved - Remove all legacy signing key code from the OIDC API - Remove the webkey feature flag from proto - Remove the webkey feature flag from console - Cleanup documentation # Additional Changes - Resolved some canonical header linter errors in OIDC - Use the constant for `projections.lock` in the saml package. # Additional Context - Closes #10029 - After #10105 - After #10061 --- cmd/start/start.go | 1 - .../components/features/features.component.ts | 1 - 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/hu.json | 2 - console/src/assets/i18n/id.json | 2 - console/src/assets/i18n/it.json | 2 - console/src/assets/i18n/ja.json | 2 - console/src/assets/i18n/ko.json | 2 - console/src/assets/i18n/mk.json | 2 - console/src/assets/i18n/nl.json | 2 - console/src/assets/i18n/pl.json | 2 - console/src/assets/i18n/pt.json | 2 - console/src/assets/i18n/ro.json | 2 - console/src/assets/i18n/ru.json | 2 - console/src/assets/i18n/sv.json | 2 - console/src/assets/i18n/zh.json | 2 - .../guides/integrate/login/oidc/webkeys.md | 7 - 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 - .../webkey_integration_test.go | 62 +-- internal/api/grpc/webkey/v2beta/webkey.go | 21 - internal/api/oidc/access_token.go | 2 +- internal/api/oidc/auth_request_converter.go | 9 +- .../api/oidc/integration_test/keys_test.go | 41 +- .../oidc/integration_test/userinfo_test.go | 10 +- internal/api/oidc/key.go | 210 +------- internal/api/oidc/op.go | 12 +- internal/api/oidc/server.go | 2 +- internal/api/oidc/server_test.go | 89 ---- internal/api/oidc/token.go | 21 +- internal/api/saml/certificate.go | 3 +- .../eventstore/token_verifier.go | 29 +- internal/command/instance_features.go | 23 - internal/command/instance_features_model.go | 5 - internal/command/key_pair.go | 25 - internal/crypto/rsa.go | 8 - internal/feature/feature.go | 4 +- internal/feature/key_enumer.go | 65 +-- .../handlers/back_channel_logout.go | 18 +- .../handlers/mock/commands.mock.go | 113 ++--- .../handlers/mock/queries.mock.go | 137 +++--- .../notification/handlers/mock/queue.mock.go | 11 +- internal/notification/handlers/queries.go | 2 - internal/query/instance_features.go | 1 - internal/query/instance_features_model.go | 3 - internal/query/key.go | 317 ------------ internal/query/key_test.go | 453 ------------------ .../query/projection/instance_features.go | 4 - .../feature/feature_v2/eventstore.go | 1 - .../repository/feature/feature_v2/feature.go | 1 - proto/zitadel/feature/v2/instance.proto | 22 +- proto/zitadel/feature/v2beta/instance.proto | 22 +- 59 files changed, 203 insertions(+), 1614 deletions(-) delete mode 100644 internal/query/key_test.go diff --git a/cmd/start/start.go b/cmd/start/start.go index 8820480f0c..3c3b5cb3e0 100644 --- a/cmd/start/start.go +++ b/cmd/start/start.go @@ -550,7 +550,6 @@ func startAPIs( keys.OIDC, keys.OIDCKey, eventstore, - dbClient, userAgentInterceptor, instanceInterceptor.Handler, limitingAccessInterceptor, diff --git a/console/src/app/components/features/features.component.ts b/console/src/app/components/features/features.component.ts index 70e038bae8..ace2788fcf 100644 --- a/console/src/app/components/features/features.component.ts +++ b/console/src/app/components/features/features.component.ts @@ -38,7 +38,6 @@ const FEATURE_KEYS = [ 'oidcTriggerIntrospectionProjections', 'permissionCheckV2', 'userSchema', - 'webKey', ] as const; export type ToggleState = { source: Source; enabled: boolean }; diff --git a/console/src/assets/i18n/bg.json b/console/src/assets/i18n/bg.json index 7d594e8318..50c0d66027 100644 --- a/console/src/assets/i18n/bg.json +++ b/console/src/assets/i18n/bg.json @@ -1641,8 +1641,6 @@ "ENABLEBACKCHANNELLOGOUT_DESCRIPTION": "Back-Channel Logout имплементира OpenID Connect Back-Channel Logout 1.0 и може да се използва за уведомяване на клиентите за прекратяване на сесията при OpenID доставчика.", "PERMISSIONCHECKV2": "Проверка на разрешения V2", "PERMISSIONCHECKV2_DESCRIPTION": "Ако флагът е активиран, ще можете да използвате новия API и неговите функции.", - "WEBKEY": "Уеб ключ", - "WEBKEY_DESCRIPTION": "Ако флагът е активиран, ще можете да използвате новия API и неговите функции.", "STATES": { "INHERITED": "Наследено", "ENABLED": "Активирано", diff --git a/console/src/assets/i18n/cs.json b/console/src/assets/i18n/cs.json index 2ee5d9d0c5..5b4547ccb4 100644 --- a/console/src/assets/i18n/cs.json +++ b/console/src/assets/i18n/cs.json @@ -1642,8 +1642,6 @@ "ENABLEBACKCHANNELLOGOUT_DESCRIPTION": "Back-Channel Logout implementuje OpenID Connect Back-Channel Logout 1.0 a může být použit k informování klientů o ukončení relace u poskytovatele OpenID.", "PERMISSIONCHECKV2": "Kontrola oprávnění V2", "PERMISSIONCHECKV2_DESCRIPTION": "Pokud je příznak povolen, budete moci používat nový API a jeho funkce.", - "WEBKEY": "Webový klíč", - "WEBKEY_DESCRIPTION": "Pokud je příznak povolen, budete moci používat nový API a jeho funkce.", "STATES": { "INHERITED": "Děděno", "ENABLED": "Povoleno", diff --git a/console/src/assets/i18n/de.json b/console/src/assets/i18n/de.json index b8f8363d13..8fec6498ec 100644 --- a/console/src/assets/i18n/de.json +++ b/console/src/assets/i18n/de.json @@ -1642,8 +1642,6 @@ "ENABLEBACKCHANNELLOGOUT_DESCRIPTION": "Der Back-Channel-Logout implementiert OpenID Connect Back-Channel Logout 1.0 und kann verwendet werden, um Clients über die Beendigung der Sitzung beim OpenID-Provider zu benachrichtigen.", "PERMISSIONCHECKV2": "Berechtigungsprüfung V2", "PERMISSIONCHECKV2_DESCRIPTION": "Wenn die Flagge aktiviert ist, können Sie die neue API und ihre Funktionen verwenden.", - "WEBKEY": "Web-Schlüssel", - "WEBKEY_DESCRIPTION": "Wenn die Flagge aktiviert ist, können Sie die neue API und ihre Funktionen verwenden.", "STATES": { "INHERITED": "Erben", "ENABLED": "Aktiviert", diff --git a/console/src/assets/i18n/en.json b/console/src/assets/i18n/en.json index fe152acb81..95fd55bfef 100644 --- a/console/src/assets/i18n/en.json +++ b/console/src/assets/i18n/en.json @@ -1645,8 +1645,6 @@ "ENABLEBACKCHANNELLOGOUT_DESCRIPTION": "The Back-Channel Logout implements OpenID Connect Back-Channel Logout 1.0 and can be used to notify clients about session termination at the OpenID Provider.", "PERMISSIONCHECKV2": "Permission Check V2", "PERMISSIONCHECKV2_DESCRIPTION": "If the flag is enabled, you'll be able to use the new API and its features.", - "WEBKEY": "Web Key", - "WEBKEY_DESCRIPTION": "If the flag is enabled, you'll be able to use the new API and its features.", "STATES": { "INHERITED": "Inherit", "ENABLED": "Enabled", diff --git a/console/src/assets/i18n/es.json b/console/src/assets/i18n/es.json index fff111fd1d..359aa4a0b5 100644 --- a/console/src/assets/i18n/es.json +++ b/console/src/assets/i18n/es.json @@ -1643,8 +1643,6 @@ "ENABLEBACKCHANNELLOGOUT_DESCRIPTION": "El Back-Channel Logout implementa OpenID Connect Back-Channel Logout 1.0 y se puede usar para notificar a los clientes sobre la terminación de la sesión en el proveedor de OpenID.", "PERMISSIONCHECKV2": "Verificación de permisos V2", "PERMISSIONCHECKV2_DESCRIPTION": "Si la bandera está habilitada, podrá usar la nueva API y sus funciones.", - "WEBKEY": "Clave web", - "WEBKEY_DESCRIPTION": "Si la bandera está habilitada, podrá usar la nueva API y sus funciones.", "STATES": { "INHERITED": "Heredado", "ENABLED": "Habilitado", diff --git a/console/src/assets/i18n/fr.json b/console/src/assets/i18n/fr.json index fc5cf69602..0864a2f8c0 100644 --- a/console/src/assets/i18n/fr.json +++ b/console/src/assets/i18n/fr.json @@ -1642,8 +1642,6 @@ "ENABLEBACKCHANNELLOGOUT_DESCRIPTION": "Le Back-Channel Logout implémente OpenID Connect Back-Channel Logout 1.0 et peut être utilisé pour notifier les clients de la fin de session chez le fournisseur OpenID.", "PERMISSIONCHECKV2": "Vérification des permissions V2", "PERMISSIONCHECKV2_DESCRIPTION": "Si le drapeau est activé, vous pourrez utiliser la nouvelle API et ses fonctionnalités.", - "WEBKEY": "Clé web", - "WEBKEY_DESCRIPTION": "Si le drapeau est activé, vous pourrez utiliser la nouvelle API et ses fonctionnalités.", "STATES": { "INHERITED": "Hérité", "ENABLED": "Activé", diff --git a/console/src/assets/i18n/hu.json b/console/src/assets/i18n/hu.json index d7dd32b15a..a87122dc52 100644 --- a/console/src/assets/i18n/hu.json +++ b/console/src/assets/i18n/hu.json @@ -1640,8 +1640,6 @@ "ENABLEBACKCHANNELLOGOUT_DESCRIPTION": "A Back-Channel Logout megvalósítja az OpenID Connect Back-Channel Logout 1.0-t, és használható az ügyfelek értesítésére a munkamenet befejezéséről az OpenID szolgáltatónál.", "PERMISSIONCHECKV2": "Engedély ellenőrzés V2", "PERMISSIONCHECKV2_DESCRIPTION": "Ha a zászló engedélyezve van, használhatja az új API-t és annak funkcióit.", - "WEBKEY": "Webkulcs", - "WEBKEY_DESCRIPTION": "Ha a zászló engedélyezve van, használhatja az új API-t és annak funkcióit.", "STATES": { "INHERITED": "Örököl", "ENABLED": "Engedélyezve", diff --git a/console/src/assets/i18n/id.json b/console/src/assets/i18n/id.json index 3dcd7b36b7..3f245d03c5 100644 --- a/console/src/assets/i18n/id.json +++ b/console/src/assets/i18n/id.json @@ -1513,8 +1513,6 @@ "ENABLEBACKCHANNELLOGOUT_DESCRIPTION": "The Back-Channel Logout implements OpenID Connect Back-Channel Logout 1.0 and can be used to notify clients about session termination at the OpenID Provider.", "PERMISSIONCHECKV2": "Permission Check V2", "PERMISSIONCHECKV2_DESCRIPTION": "If the flag is enabled, you'll be able to use the new API and its features.", - "WEBKEY": "Web Key", - "WEBKEY_DESCRIPTION": "If the flag is enabled, you'll be able to use the new API and its features.", "STATES": { "INHERITED": "Mewarisi", "ENABLED": "Diaktifkan", "DISABLED": "Dengan disabilitas" }, "INHERITED_DESCRIPTION": "Ini menetapkan nilai ke nilai default sistem.", "INHERITEDINDICATOR_DESCRIPTION": { diff --git a/console/src/assets/i18n/it.json b/console/src/assets/i18n/it.json index da1df2ba38..e127281433 100644 --- a/console/src/assets/i18n/it.json +++ b/console/src/assets/i18n/it.json @@ -1642,8 +1642,6 @@ "ENABLEBACKCHANNELLOGOUT_DESCRIPTION": "Il Back-Channel Logout implementa OpenID Connect Back-Channel Logout 1.0 e può essere utilizzato per notificare ai client la terminazione della sessione presso il provider OpenID.", "PERMISSIONCHECKV2": "Controllo permessi V2", "PERMISSIONCHECKV2_DESCRIPTION": "Se il flag è abilitato, potrai utilizzare la nuova API e le sue funzionalità.", - "WEBKEY": "Chiave Web", - "WEBKEY_DESCRIPTION": "Se il flag è abilitato, potrai utilizzare la nuova API e le sue funzionalità.", "STATES": { "INHERITED": "Predefinito", "ENABLED": "Abilitato", diff --git a/console/src/assets/i18n/ja.json b/console/src/assets/i18n/ja.json index 1828a8ada8..250561e938 100644 --- a/console/src/assets/i18n/ja.json +++ b/console/src/assets/i18n/ja.json @@ -1642,8 +1642,6 @@ "ENABLEBACKCHANNELLOGOUT_DESCRIPTION": "バックチャネルログアウトは OpenID Connect バックチャネルログアウト 1.0 を実装し、OpenID プロバイダーでのセッション終了についてクライアントに通知するために使用できます。", "PERMISSIONCHECKV2": "権限チェック V2", "PERMISSIONCHECKV2_DESCRIPTION": "フラグが有効になっている場合、新しい API とその機能を使用できます。", - "WEBKEY": "ウェブキー", - "WEBKEY_DESCRIPTION": "フラグが有効になっている場合、新しい API とその機能を使用できます。", "STATES": { "INHERITED": "継承", "ENABLED": "有効", diff --git a/console/src/assets/i18n/ko.json b/console/src/assets/i18n/ko.json index af5fa65972..716375941d 100644 --- a/console/src/assets/i18n/ko.json +++ b/console/src/assets/i18n/ko.json @@ -1642,8 +1642,6 @@ "ENABLEBACKCHANNELLOGOUT_DESCRIPTION": "백채널 로그아웃은 OpenID Connect 백채널 로그아웃 1.0을 구현하며, OpenID 제공자에서 세션 종료에 대해 클라이언트에게 알리는 데 사용할 수 있습니다.", "PERMISSIONCHECKV2": "권한 확인 V2", "PERMISSIONCHECKV2_DESCRIPTION": "플래그가 활성화되면 새로운 API와 그 기능을 사용할 수 있습니다.", - "WEBKEY": "웹 키", - "WEBKEY_DESCRIPTION": "플래그가 활성화되면 새로운 API와 그 기능을 사용할 수 있습니다.", "STATES": { "INHERITED": "상속", "ENABLED": "활성화됨", diff --git a/console/src/assets/i18n/mk.json b/console/src/assets/i18n/mk.json index 1e85e06928..39836f5dfc 100644 --- a/console/src/assets/i18n/mk.json +++ b/console/src/assets/i18n/mk.json @@ -1643,8 +1643,6 @@ "ENABLEBACKCHANNELLOGOUT_DESCRIPTION": "Back-Channel Logout имплементира OpenID Connect Back-Channel Logout 1.0 и може да се користи за известување на клиентите за завршување на сесијата кај OpenID провајдерот.", "PERMISSIONCHECKV2": "Проверка на дозволи V2", "PERMISSIONCHECKV2_DESCRIPTION": "Ако знамето е овозможено, ќе можете да ја користите новата API и нејзините функции.", - "WEBKEY": "Веб клуч", - "WEBKEY_DESCRIPTION": "Ако знамето е овозможено, ќе можете да ја користите новата API и нејзините функции.", "STATES": { "INHERITED": "Наследи", "ENABLED": "Овозможено", diff --git a/console/src/assets/i18n/nl.json b/console/src/assets/i18n/nl.json index c3de881784..c49867aa3e 100644 --- a/console/src/assets/i18n/nl.json +++ b/console/src/assets/i18n/nl.json @@ -1642,8 +1642,6 @@ "ENABLEBACKCHANNELLOGOUT_DESCRIPTION": "De Back-Channel Logout implementeert OpenID Connect Back-Channel Logout 1.0 en kan worden gebruikt om clients te informeren over het beëindigen van de sessie bij de OpenID-provider.", "PERMISSIONCHECKV2": "Permissiecontrole V2", "PERMISSIONCHECKV2_DESCRIPTION": "Als de vlag is ingeschakeld, kunt u de nieuwe API en de bijbehorende functies gebruiken.", - "WEBKEY": "Websleutel", - "WEBKEY_DESCRIPTION": "Als de vlag is ingeschakeld, kunt u de nieuwe API en de bijbehorende functies gebruiken.", "STATES": { "INHERITED": "Overgenomen", "ENABLED": "Ingeschakeld", diff --git a/console/src/assets/i18n/pl.json b/console/src/assets/i18n/pl.json index ca5476463d..abf2e1ba8a 100644 --- a/console/src/assets/i18n/pl.json +++ b/console/src/assets/i18n/pl.json @@ -1641,8 +1641,6 @@ "ENABLEBACKCHANNELLOGOUT_DESCRIPTION": "Back-Channel Logout implementuje OpenID Connect Back-Channel Logout 1.0 i może być używany do powiadamiania klientów o zakończeniu sesji u dostawcy OpenID.", "PERMISSIONCHECKV2": "Sprawdzanie uprawnień V2", "PERMISSIONCHECKV2_DESCRIPTION": "Jeśli flaga jest włączona, będziesz mógł korzystać z nowego API i jego funkcji.", - "WEBKEY": "Klucz Web", - "WEBKEY_DESCRIPTION": "Jeśli flaga jest włączona, będziesz mógł korzystać z nowego API i jego funkcji.", "STATES": { "INHERITED": "Dziedziczony", "ENABLED": "Włączony", diff --git a/console/src/assets/i18n/pt.json b/console/src/assets/i18n/pt.json index 7c68a4cada..8b858bd44e 100644 --- a/console/src/assets/i18n/pt.json +++ b/console/src/assets/i18n/pt.json @@ -1643,8 +1643,6 @@ "ENABLEBACKCHANNELLOGOUT_DESCRIPTION": "O Logout de Back-Channel implementa o OpenID Connect Back-Channel Logout 1.0 e pode ser usado para notificar os clientes sobre a terminação da sessão no Provedor de OpenID.", "PERMISSIONCHECKV2": "Verificação de Permissão V2", "PERMISSIONCHECKV2_DESCRIPTION": "Se a bandeira estiver ativada, você poderá usar a nova API e seus recursos.", - "WEBKEY": "Chave Web", - "WEBKEY_DESCRIPTION": "Se a bandeira estiver ativada, você poderá usar a nova API e seus recursos.", "STATES": { "INHERITED": "Herdade", "ENABLED": "Habilitado", diff --git a/console/src/assets/i18n/ro.json b/console/src/assets/i18n/ro.json index 0e0802a17c..d2f51a81e0 100644 --- a/console/src/assets/i18n/ro.json +++ b/console/src/assets/i18n/ro.json @@ -1640,8 +1640,6 @@ "ENABLEBACKCHANNELLOGOUT_DESCRIPTION": "Logout-ul Back-Channel implementează OpenID Connect Back-Channel Logout 1.0 și poate fi folosit pentru a notifica clienții despre terminarea sesiunii la Producătorul OpenID.", "PERMISSIONCHECKV2": "Verificare Permisiuni V2", "PERMISSIONCHECKV2_DESCRIPTION": "Dacă steagul este activat, veți putea folosi noua API și funcțiile sale.", - "WEBKEY": "Cheie Web", - "WEBKEY_DESCRIPTION": "Dacă steagul este activat, veți putea folosi noua API și funcțiile sale.", "STATES": { "INHERITED": "Moșteniți", "ENABLED": "Activat", diff --git a/console/src/assets/i18n/ru.json b/console/src/assets/i18n/ru.json index 8e06568a82..3070b311e7 100644 --- a/console/src/assets/i18n/ru.json +++ b/console/src/assets/i18n/ru.json @@ -1695,8 +1695,6 @@ "ENABLEBACKCHANNELLOGOUT_DESCRIPTION": "Back-Channel Logout реализует OpenID Connect Back-Channel Logout 1.0 и может использоваться для уведомления клиентов о завершении сеанса у поставщика OpenID.", "PERMISSIONCHECKV2": "Проверка Разрешений V2", "PERMISSIONCHECKV2_DESCRIPTION": "Если флаг включен, вы сможете использовать новый API и его функции.", - "WEBKEY": "Веб-ключ", - "WEBKEY_DESCRIPTION": "Если флаг включен, вы сможете использовать новый API и его функции.", "STATES": { "INHERITED": "Наследовать", "ENABLED": "Включено", diff --git a/console/src/assets/i18n/sv.json b/console/src/assets/i18n/sv.json index 1b80021a67..8f03501054 100644 --- a/console/src/assets/i18n/sv.json +++ b/console/src/assets/i18n/sv.json @@ -1646,8 +1646,6 @@ "ENABLEBACKCHANNELLOGOUT_DESCRIPTION": "Back-Channel Logout implementerar OpenID Connect Back-Channel Logout 1.0 och kan användas för att meddela klienter om sessionens avslutning hos OpenID-leverantören.", "PERMISSIONCHECKV2": "Behörighetskontroll V2", "PERMISSIONCHECKV2_DESCRIPTION": "Om flaggan är aktiverad kan du använda den nya API:n och dess funktioner.", - "WEBKEY": "Webbnyckel", - "WEBKEY_DESCRIPTION": "Om flaggan är aktiverad kan du använda den nya API:n och dess funktioner.", "STATES": { "INHERITED": "Ärv", "ENABLED": "Aktiverad", diff --git a/console/src/assets/i18n/zh.json b/console/src/assets/i18n/zh.json index 9565b61eca..0431405979 100644 --- a/console/src/assets/i18n/zh.json +++ b/console/src/assets/i18n/zh.json @@ -1642,8 +1642,6 @@ "ENABLEBACKCHANNELLOGOUT_DESCRIPTION": "Back-Channel 注销实现了 OpenID Connect Back-Channel Logout 1.0,可用于通知客户端在 OpenID 提供商处终止会话。", "PERMISSIONCHECKV2": "权限检查 V2", "PERMISSIONCHECKV2_DESCRIPTION": "如果启用该标志,您将能够使用新的 API 及其功能。", - "WEBKEY": "Web 密钥", - "WEBKEY_DESCRIPTION": "如果启用该标志,您将能够使用新的 API 及其功能。", "STATES": { "INHERITED": "继承", "ENABLED": "已启用", diff --git a/docs/docs/guides/integrate/login/oidc/webkeys.md b/docs/docs/guides/integrate/login/oidc/webkeys.md index 62f62a90e0..288284fefc 100644 --- a/docs/docs/guides/integrate/login/oidc/webkeys.md +++ b/docs/docs/guides/integrate/login/oidc/webkeys.md @@ -20,13 +20,6 @@ JWT access tokens, instead of [introspection](/docs/apis/openidoauth/endpoints#i ZITADEL uses public key verification when API calls are made or when the userInfo or introspection endpoints are called with a JWT access token. -:::info -Web keys are an [experimental](/docs/support/software-release-cycles-support#beta) feature. Be sure to enable the `web_key` [feature](/docs/apis/resources/feature_service_v2/feature-service-set-instance-features) before using it. - -The documentation describes the state of the feature in ZITADEL V3. -Test the feature and add improvement or bug reports directly to the [github repository](https://github.com/zitadel/zitadel) or let us know your general feedback in the [discord thread](https://discord.com/channels/927474939156643850/1329100936127320175/threads/1332344892629717075)! -::: - ### JSON Web Key ZITADEL implements the [RFC7517 - JSON Web Key (JWK)](https://www.rfc-editor.org/rfc/rfc7517) format for storage and distribution of public keys. diff --git a/internal/api/grpc/feature/v2/converter.go b/internal/api/grpc/feature/v2/converter.go index 56d3009457..1f0a3b21e7 100644 --- a/internal/api/grpc/feature/v2/converter.go +++ b/internal/api/grpc/feature/v2/converter.go @@ -58,7 +58,6 @@ func instanceFeaturesToCommand(req *feature_pb.SetInstanceFeaturesRequest) (*com UserSchema: req.UserSchema, TokenExchange: req.OidcTokenExchange, ImprovedPerformance: improvedPerformanceListToDomain(req.ImprovedPerformance), - WebKey: req.WebKey, DebugOIDCParentError: req.DebugOidcParentError, OIDCSingleV1SessionTermination: req.OidcSingleV1SessionTermination, DisableUserTokenEvent: req.DisableUserTokenEvent, @@ -77,7 +76,6 @@ func instanceFeaturesToPb(f *query.InstanceFeatures) *feature_pb.GetInstanceFeat UserSchema: featureSourceToFlagPb(&f.UserSchema), OidcTokenExchange: featureSourceToFlagPb(&f.TokenExchange), ImprovedPerformance: featureSourceToImprovedPerformanceFlagPb(&f.ImprovedPerformance), - WebKey: featureSourceToFlagPb(&f.WebKey), DebugOidcParentError: featureSourceToFlagPb(&f.DebugOIDCParentError), OidcSingleV1SessionTermination: featureSourceToFlagPb(&f.OIDCSingleV1SessionTermination), DisableUserTokenEvent: featureSourceToFlagPb(&f.DisableUserTokenEvent), diff --git a/internal/api/grpc/feature/v2/converter_test.go b/internal/api/grpc/feature/v2/converter_test.go index b77ed438f5..d09f1839ba 100644 --- a/internal/api/grpc/feature/v2/converter_test.go +++ b/internal/api/grpc/feature/v2/converter_test.go @@ -153,7 +153,6 @@ func Test_instanceFeaturesToCommand(t *testing.T) { UserSchema: gu.Ptr(true), OidcTokenExchange: gu.Ptr(true), ImprovedPerformance: nil, - WebKey: gu.Ptr(true), DebugOidcParentError: gu.Ptr(true), OidcSingleV1SessionTermination: gu.Ptr(true), EnableBackChannelLogout: gu.Ptr(true), @@ -169,7 +168,6 @@ func Test_instanceFeaturesToCommand(t *testing.T) { UserSchema: gu.Ptr(true), TokenExchange: gu.Ptr(true), ImprovedPerformance: nil, - WebKey: gu.Ptr(true), DebugOIDCParentError: gu.Ptr(true), OIDCSingleV1SessionTermination: gu.Ptr(true), EnableBackChannelLogout: gu.Ptr(true), @@ -211,10 +209,6 @@ func Test_instanceFeaturesToPb(t *testing.T) { Level: feature.LevelSystem, Value: []feature.ImprovedPerformanceType{feature.ImprovedPerformanceTypeOrgByID}, }, - WebKey: query.FeatureSource[bool]{ - Level: feature.LevelInstance, - Value: true, - }, OIDCSingleV1SessionTermination: query.FeatureSource[bool]{ Level: feature.LevelInstance, Value: true, @@ -265,10 +259,6 @@ 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, - }, DebugOidcParentError: &feature_pb.FeatureFlag{ Enabled: false, Source: feature_pb.Source_SOURCE_UNSPECIFIED, diff --git a/internal/api/grpc/feature/v2beta/converter.go b/internal/api/grpc/feature/v2beta/converter.go index 406146fdbe..8927b16e29 100644 --- a/internal/api/grpc/feature/v2beta/converter.go +++ b/internal/api/grpc/feature/v2beta/converter.go @@ -38,7 +38,6 @@ func instanceFeaturesToCommand(req *feature_pb.SetInstanceFeaturesRequest) *comm UserSchema: req.UserSchema, TokenExchange: req.OidcTokenExchange, ImprovedPerformance: improvedPerformanceListToDomain(req.ImprovedPerformance), - WebKey: req.WebKey, DebugOIDCParentError: req.DebugOidcParentError, OIDCSingleV1SessionTermination: req.OidcSingleV1SessionTermination, } @@ -52,7 +51,6 @@ func instanceFeaturesToPb(f *query.InstanceFeatures) *feature_pb.GetInstanceFeat UserSchema: featureSourceToFlagPb(&f.UserSchema), OidcTokenExchange: featureSourceToFlagPb(&f.TokenExchange), ImprovedPerformance: featureSourceToImprovedPerformanceFlagPb(&f.ImprovedPerformance), - WebKey: featureSourceToFlagPb(&f.WebKey), DebugOidcParentError: featureSourceToFlagPb(&f.DebugOIDCParentError), OidcSingleV1SessionTermination: featureSourceToFlagPb(&f.OIDCSingleV1SessionTermination), } diff --git a/internal/api/grpc/feature/v2beta/converter_test.go b/internal/api/grpc/feature/v2beta/converter_test.go index 2395574733..5fdb5e993e 100644 --- a/internal/api/grpc/feature/v2beta/converter_test.go +++ b/internal/api/grpc/feature/v2beta/converter_test.go @@ -111,7 +111,6 @@ func Test_instanceFeaturesToCommand(t *testing.T) { UserSchema: gu.Ptr(true), OidcTokenExchange: gu.Ptr(true), ImprovedPerformance: nil, - WebKey: gu.Ptr(true), OidcSingleV1SessionTermination: gu.Ptr(true), } want := &command.InstanceFeatures{ @@ -120,7 +119,6 @@ func Test_instanceFeaturesToCommand(t *testing.T) { UserSchema: gu.Ptr(true), TokenExchange: gu.Ptr(true), ImprovedPerformance: nil, - WebKey: gu.Ptr(true), OIDCSingleV1SessionTermination: gu.Ptr(true), } got := instanceFeaturesToCommand(arg) @@ -154,10 +152,6 @@ func Test_instanceFeaturesToPb(t *testing.T) { Level: feature.LevelSystem, Value: []feature.ImprovedPerformanceType{feature.ImprovedPerformanceTypeOrgByID}, }, - WebKey: query.FeatureSource[bool]{ - Level: feature.LevelInstance, - Value: true, - }, OIDCSingleV1SessionTermination: query.FeatureSource[bool]{ Level: feature.LevelInstance, Value: true, @@ -189,10 +183,6 @@ 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, - }, DebugOidcParentError: &feature_pb.FeatureFlag{ Enabled: false, Source: feature_pb.Source_SOURCE_UNSPECIFIED, diff --git a/internal/api/grpc/webkey/v2beta/integration_test/webkey_integration_test.go b/internal/api/grpc/webkey/v2beta/integration_test/webkey_integration_test.go index 002669c233..0cbf629b43 100644 --- a/internal/api/grpc/webkey/v2beta/integration_test/webkey_integration_test.go +++ b/internal/api/grpc/webkey/v2beta/integration_test/webkey_integration_test.go @@ -12,11 +12,9 @@ import ( "github.com/stretchr/testify/require" "google.golang.org/grpc/codes" "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" webkey "github.com/zitadel/zitadel/pkg/grpc/webkey/v2beta" ) @@ -33,34 +31,8 @@ func TestMain(m *testing.M) { }()) } -func TestServer_Feature_Disabled(t *testing.T) { - instance, iamCtx, _ := createInstance(t, false) - client := instance.Client.WebKeyV2Beta - - 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) { - instance, iamCtx, creationDate := createInstance(t, true) + instance, iamCtx, creationDate := createInstance(t) // After the feature is first enabled, we can expect 2 generated keys with the default config. checkWebKeyListState(iamCtx, t, instance, 2, "", &webkey.WebKey_Rsa{ Rsa: &webkey.RSA{ @@ -71,7 +43,7 @@ func TestServer_ListWebKeys(t *testing.T) { } func TestServer_CreateWebKey(t *testing.T) { - instance, iamCtx, creationDate := createInstance(t, true) + instance, iamCtx, creationDate := createInstance(t) client := instance.Client.WebKeyV2Beta _, err := client.CreateWebKey(iamCtx, &webkey.CreateWebKeyRequest{ @@ -93,7 +65,7 @@ func TestServer_CreateWebKey(t *testing.T) { } func TestServer_ActivateWebKey(t *testing.T) { - instance, iamCtx, creationDate := createInstance(t, true) + instance, iamCtx, creationDate := createInstance(t) client := instance.Client.WebKeyV2Beta resp, err := client.CreateWebKey(iamCtx, &webkey.CreateWebKeyRequest{ @@ -120,7 +92,7 @@ func TestServer_ActivateWebKey(t *testing.T) { } func TestServer_DeleteWebKey(t *testing.T) { - instance, iamCtx, creationDate := createInstance(t, true) + instance, iamCtx, creationDate := createInstance(t) client := instance.Client.WebKeyV2Beta keyIDs := make([]string, 2) @@ -197,40 +169,22 @@ func TestServer_DeleteWebKey(t *testing.T) { }, creationDate) } -func createInstance(t *testing.T, enableFeature bool) (*integration.Instance, context.Context, *timestamppb.Timestamp) { +func createInstance(t *testing.T) (*integration.Instance, context.Context, *timestamppb.Timestamp) { instance := integration.NewInstance(CTX) creationDate := timestamppb.Now() iamCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) - if enableFeature { - _, err := instance.Client.FeatureV2.SetInstanceFeatures(iamCTX, &feature.SetInstanceFeaturesRequest{ - WebKey: proto.Bool(true), - }) - require.NoError(t, err) - } - retryDuration, tick := integration.WaitForAndTickWithMaxDuration(iamCTX, time.Minute) assert.EventuallyWithT(t, func(collect *assert.CollectT) { resp, err := instance.Client.WebKeyV2Beta.ListWebKeys(iamCTX, &webkey.ListWebKeysRequest{}) - if enableFeature { - assert.NoError(collect, err) - assert.Len(collect, resp.GetWebKeys(), 2) - } else { - assert.Error(collect, err) - } + assert.NoError(collect, err) + assert.Len(collect, resp.GetWebKeys(), 2) + }, retryDuration, tick) return instance, iamCTX, creationDate } -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, instance *integration.Instance, nKeys int, expectActiveKeyID string, config any, creationDate *timestamppb.Timestamp) { t.Helper() diff --git a/internal/api/grpc/webkey/v2beta/webkey.go b/internal/api/grpc/webkey/v2beta/webkey.go index d45288dff2..469d6fc9a6 100644 --- a/internal/api/grpc/webkey/v2beta/webkey.go +++ b/internal/api/grpc/webkey/v2beta/webkey.go @@ -5,9 +5,7 @@ import ( "google.golang.org/protobuf/types/known/timestamppb" - "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/telemetry/tracing" - "github.com/zitadel/zitadel/internal/zerrors" webkey "github.com/zitadel/zitadel/pkg/grpc/webkey/v2beta" ) @@ -15,9 +13,6 @@ func (s *Server) CreateWebKey(ctx context.Context, req *webkey.CreateWebKeyReque 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 @@ -33,9 +28,6 @@ func (s *Server) ActivateWebKey(ctx context.Context, req *webkey.ActivateWebKeyR 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 @@ -50,9 +42,6 @@ func (s *Server) DeleteWebKey(ctx context.Context, req *webkey.DeleteWebKeyReque ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - if err = checkWebKeyFeature(ctx); err != nil { - return nil, err - } deletedAt, err := s.command.DeleteWebKey(ctx, req.GetId()) if err != nil { return nil, err @@ -71,9 +60,6 @@ func (s *Server) ListWebKeys(ctx context.Context, _ *webkey.ListWebKeysRequest) 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 @@ -83,10 +69,3 @@ func (s *Server) ListWebKeys(ctx context.Context, _ *webkey.ListWebKeysRequest) WebKeys: webKeyDetailsListToPb(list), }, 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/oidc/access_token.go b/internal/api/oidc/access_token.go index 2f2880efc2..5c0b9c9f66 100644 --- a/internal/api/oidc/access_token.go +++ b/internal/api/oidc/access_token.go @@ -53,7 +53,7 @@ func (s *Server) verifyAccessToken(ctx context.Context, tkn string) (_ *accessTo tokenID, subject = split[0], split[1] } else { verifier := op.NewAccessTokenVerifier(op.IssuerFromContext(ctx), s.accessTokenKeySet, - op.WithSupportedAccessTokenSigningAlgorithms(supportedSigningAlgs(ctx)...), + op.WithSupportedAccessTokenSigningAlgorithms(supportedSigningAlgs()...), ) claims, err := op.VerifyAccessToken[*oidc.AccessTokenClaims](ctx, tkn, verifier) if err != nil { diff --git a/internal/api/oidc/auth_request_converter.go b/internal/api/oidc/auth_request_converter.go index 2144ca8ba1..064af20de0 100644 --- a/internal/api/oidc/auth_request_converter.go +++ b/internal/api/oidc/auth_request_converter.go @@ -140,13 +140,8 @@ func HttpHeadersFromContext(ctx context.Context) (userAgent, acceptLang string) if !ok { return } - if agents, ok := ctxHeaders[http_utils.UserAgentHeader]; ok { - userAgent = agents[0] - } - if langs, ok := ctxHeaders[http_utils.AcceptLanguage]; ok { - acceptLang = langs[0] - } - return userAgent, acceptLang + return ctxHeaders.Get(http_utils.UserAgentHeader), + ctxHeaders.Get(http_utils.AcceptLanguage) } func IpFromContext(ctx context.Context) net.IP { diff --git a/internal/api/oidc/integration_test/keys_test.go b/internal/api/oidc/integration_test/keys_test.go index 8b66e980d0..a6223cf1ee 100644 --- a/internal/api/oidc/integration_test/keys_test.go +++ b/internal/api/oidc/integration_test/keys_test.go @@ -14,12 +14,10 @@ import ( "github.com/stretchr/testify/require" "github.com/zitadel/oidc/v3/pkg/client" "github.com/zitadel/oidc/v3/pkg/oidc" - "google.golang.org/protobuf/proto" http_util "github.com/zitadel/zitadel/internal/api/http" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/integration" - "github.com/zitadel/zitadel/pkg/grpc/feature/v2" oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2" ) @@ -53,25 +51,16 @@ func TestServer_Keys(t *testing.T) { require.NoError(t, err) tests := []struct { - name string - webKeyFeature bool - wantLen int + name string + wantLen int }{ { - name: "legacy only", - webKeyFeature: false, - wantLen: 1, - }, - { - name: "webkeys with legacy", - webKeyFeature: true, - wantLen: 3, // 1 legacy + 2 created by enabling feature flag + name: "webkeys", + wantLen: 2, // 2 from instance creation. }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - ensureWebKeyFeature(t, instance, tt.webKeyFeature) - assert.EventuallyWithT(t, func(ttt *assert.CollectT) { resp, err := http.Get(discovery.JwksURI) require.NoError(ttt, err) @@ -92,30 +81,10 @@ func TestServer_Keys(t *testing.T) { } cacheControl := resp.Header.Get("cache-control") - if tt.webKeyFeature { - require.Equal(ttt, "max-age=300, must-revalidate", cacheControl) - return - } - require.Equal(ttt, "no-store", cacheControl) + require.Equal(ttt, "max-age=300, must-revalidate", cacheControl) }, time.Minute, time.Second/10) }) } } - -func ensureWebKeyFeature(t *testing.T, instance *integration.Instance, set bool) { - ctxIam := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) - - _, err := instance.Client.FeatureV2.SetInstanceFeatures(ctxIam, &feature.SetInstanceFeaturesRequest{ - WebKey: proto.Bool(set), - }) - require.NoError(t, err) - - t.Cleanup(func() { - _, err := instance.Client.FeatureV2.SetInstanceFeatures(ctxIam, &feature.SetInstanceFeaturesRequest{ - WebKey: proto.Bool(false), - }) - require.NoError(t, err) - }) -} diff --git a/internal/api/oidc/integration_test/userinfo_test.go b/internal/api/oidc/integration_test/userinfo_test.go index bf201b242e..b3bc836343 100644 --- a/internal/api/oidc/integration_test/userinfo_test.go +++ b/internal/api/oidc/integration_test/userinfo_test.go @@ -35,21 +35,14 @@ func TestServer_UserInfo(t *testing.T) { tests := []struct { name string trigger bool - webKey bool }{ { name: "trigger enabled", trigger: true, }, - - // This is the only functional test we need to cover web keys. - // - By creating tokens the signer is tested - // - When obtaining the tokens, the RP verifies the ID Token using the key set from the jwks endpoint. - // - By calling userinfo with the access token as JWT, the Token Verifier with the public key cache is tested. { - name: "web keys", + name: "trigger disabled", trigger: false, - webKey: true, }, } @@ -57,7 +50,6 @@ func TestServer_UserInfo(t *testing.T) { t.Run(tt.name, func(t *testing.T) { _, err := Instance.Client.FeatureV2.SetInstanceFeatures(iamOwnerCTX, &feature.SetInstanceFeaturesRequest{ OidcTriggerIntrospectionProjections: &tt.trigger, - WebKey: &tt.webKey, }) require.NoError(t, err) testServer_UserInfo(t) diff --git a/internal/api/oidc/key.go b/internal/api/oidc/key.go index 852bbc7db8..61f874664f 100644 --- a/internal/api/oidc/key.go +++ b/internal/api/oidc/key.go @@ -10,18 +10,12 @@ import ( "github.com/go-jose/go-jose/v4" "github.com/jonboulle/clockwork" - "github.com/muhlemmer/gu" - "github.com/shopspring/decimal" - "github.com/zitadel/logging" "github.com/zitadel/oidc/v3/pkg/op" "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/eventstore" "github.com/zitadel/zitadel/internal/query" - "github.com/zitadel/zitadel/internal/repository/instance" - "github.com/zitadel/zitadel/internal/repository/keypair" "github.com/zitadel/zitadel/internal/telemetry/tracing" "github.com/zitadel/zitadel/internal/zerrors" ) @@ -36,11 +30,8 @@ var supportedWebKeyAlgs = []string{ string(jose.ES512), } -func supportedSigningAlgs(ctx context.Context) []string { - if authz.GetFeatures(ctx).WebKey { - return supportedWebKeyAlgs - } - return []string{string(jose.RS256)} +func supportedSigningAlgs() []string { + return supportedWebKeyAlgs } type cachedPublicKey struct { @@ -211,15 +202,6 @@ func withKeyExpiryCheck(check bool) keySetOption { } } -func jsonWebkey(key query.PublicKey) *jose.JSONWebKey { - return &jose.JSONWebKey{ - KeyID: key.ID(), - Algorithm: key.Algorithm(), - Use: key.Use().String(), - Key: key.Key(), - } -} - // keySetMap is a mapping of key IDs to public key data. type keySetMap map[string][]byte @@ -250,7 +232,6 @@ func (k keySetMap) VerifySignature(ctx context.Context, jws *jose.JSONWebSignatu } const ( - locksTable = "projections.locks" signingKey = "signing_key" oidcUser = "OIDC" @@ -279,203 +260,36 @@ func (s *SigningKey) ID() string { return s.id } -// PublicKey wraps the query.PublicKey to implement the op.Key interface -type PublicKey struct { - key query.PublicKey -} - -func (s *PublicKey) Algorithm() jose.SignatureAlgorithm { - return jose.SignatureAlgorithm(s.key.Algorithm()) -} - -func (s *PublicKey) Use() string { - return s.key.Use().String() -} - -func (s *PublicKey) Key() interface{} { - return s.key.Key() -} - -func (s *PublicKey) ID() string { - return s.key.ID() -} - // KeySet implements the op.Storage interface func (o *OPStorage) KeySet(ctx context.Context) (keys []op.Key, err error) { - ctx, span := tracing.NewSpan(ctx) - defer func() { span.EndWithError(err) }() - err = retry(func() error { - publicKeys, err := o.query.ActivePublicKeys(ctx, time.Now()) - if err != nil { - return err - } - keys = make([]op.Key, len(publicKeys.Keys)) - for i, key := range publicKeys.Keys { - keys[i] = &PublicKey{key} - } - return nil - }) - return keys, err + panic(o.panicErr("KeySet")) } // SignatureAlgorithms implements the op.Storage interface func (o *OPStorage) SignatureAlgorithms(ctx context.Context) ([]jose.SignatureAlgorithm, error) { - key, err := o.SigningKey(ctx) - if err != nil { - logging.WithError(err).Warn("unable to fetch signing key") - return nil, err - } - return []jose.SignatureAlgorithm{key.SignatureAlgorithm()}, nil + panic(o.panicErr("SignatureAlgorithms")) } // SigningKey implements the op.Storage interface func (o *OPStorage) SigningKey(ctx context.Context) (key op.SigningKey, err error) { - err = retry(func() error { - key, err = o.getSigningKey(ctx) - if err != nil { - return err - } - if key == nil { - return zerrors.ThrowNotFound(nil, "OIDC-ve4Qu", "Errors.Internal") - } - return nil - }) - return key, err -} - -func (o *OPStorage) getSigningKey(ctx context.Context) (op.SigningKey, error) { - keys, err := o.query.ActivePrivateSigningKey(ctx, time.Now().Add(gracefulPeriod)) - if err != nil { - return nil, err - } - if len(keys.Keys) > 0 { - return PrivateKeyToSigningKey(SelectSigningKey(keys.Keys), o.encAlg) - } - var position decimal.Decimal - if keys.State != nil { - position = keys.State.Position - } - return nil, o.refreshSigningKey(ctx, position) -} - -func (o *OPStorage) refreshSigningKey(ctx context.Context, position decimal.Decimal) error { - ok, err := o.ensureIsLatestKey(ctx, position) - if err != nil || !ok { - return zerrors.ThrowInternal(err, "OIDC-ASfh3", "cannot ensure that projection is up to date") - } - err = o.lockAndGenerateSigningKeyPair(ctx) - if err != nil { - return zerrors.ThrowInternal(err, "OIDC-ADh31", "could not create signing key") - } - return zerrors.ThrowInternal(nil, "OIDC-Df1bh", "") -} - -func (o *OPStorage) ensureIsLatestKey(ctx context.Context, position decimal.Decimal) (bool, error) { - maxSequence, err := o.getMaxKeyPosition(ctx) - if err != nil { - return false, fmt.Errorf("error retrieving new events: %w", err) - } - return position.GreaterThanOrEqual(maxSequence), nil -} - -func PrivateKeyToSigningKey(key query.PrivateKey, algorithm crypto.EncryptionAlgorithm) (_ op.SigningKey, err error) { - keyData, err := crypto.Decrypt(key.Key(), algorithm) - if err != nil { - return nil, err - } - privateKey, err := crypto.BytesToPrivateKey(keyData) - if err != nil { - return nil, err - } - return &SigningKey{ - algorithm: jose.SignatureAlgorithm(key.Algorithm()), - key: privateKey, - id: key.ID(), - }, nil -} - -func (o *OPStorage) lockAndGenerateSigningKeyPair(ctx context.Context) error { - logging.Info("lock and generate signing key pair") - - ctx, cancel := context.WithCancel(ctx) - defer cancel() - - errs := o.locker.Lock(ctx, lockDuration, authz.GetInstance(ctx).InstanceID()) - err, ok := <-errs - if err != nil || !ok { - if zerrors.IsErrorAlreadyExists(err) { - return nil - } - logging.OnError(err).Debug("initial lock failed") - return err - } - - return o.command.GenerateSigningKeyPair(setOIDCCtx(ctx), "RS256") -} - -func (o *OPStorage) getMaxKeyPosition(ctx context.Context) (decimal.Decimal, error) { - return o.eventstore.LatestPosition(ctx, - eventstore.NewSearchQueryBuilder(eventstore.ColumnsMaxPosition). - ResourceOwner(authz.GetInstance(ctx).InstanceID()). - AwaitOpenTransactions(). - AddQuery(). - AggregateTypes( - keypair.AggregateType, - instance.AggregateType, - ). - EventTypes( - keypair.AddedEventType, - instance.InstanceRemovedEventType, - ). - Builder(), - ) -} - -func SelectSigningKey(keys []query.PrivateKey) query.PrivateKey { - return keys[len(keys)-1] -} - -func setOIDCCtx(ctx context.Context) context.Context { - return authz.SetCtxData(ctx, authz.CtxData{UserID: oidcUser, OrgID: authz.GetInstance(ctx).InstanceID()}) -} - -func retry(retryable func() error) (err error) { - for i := 0; i < retryCount; i++ { - err = retryable() - if err == nil { - return nil - } - time.Sleep(retryBackoff) - } - return err + panic(o.panicErr("SigningKey")) } func (s *Server) Keys(ctx context.Context, r *op.Request[struct{}]) (_ *op.Response, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - if !authz.GetFeatures(ctx).WebKey { - return s.LegacyServer.Keys(ctx, r) - } - keyset, err := s.query.GetWebKeySet(ctx) if err != nil { return nil, err } - // Return legacy keys, so we do not invalidate all tokens - // once the feature flag is enabled. - legacyKeys, err := s.query.ActivePublicKeys(ctx, time.Now()) - logging.OnError(err).Error("oidc server: active public keys (legacy)") - appendPublicKeysToWebKeySet(keyset, legacyKeys) - resp := op.NewResponse(keyset) if s.jwksCacheControlMaxAge != 0 { resp.Header.Set(http_util.CacheControl, fmt.Sprintf("max-age=%d, must-revalidate", int(s.jwksCacheControlMaxAge/time.Second)), ) } - return resp, nil } @@ -497,20 +311,10 @@ func appendPublicKeysToWebKeySet(keyset *jose.JSONWebKeySet, pubkeys *query.Publ func queryKeyFunc(q *query.Queries) func(ctx context.Context, keyID string) (*jose.JSONWebKey, *time.Time, error) { return func(ctx context.Context, keyID string) (*jose.JSONWebKey, *time.Time, error) { - if authz.GetFeatures(ctx).WebKey { - webKey, err := q.GetPublicWebKeyByID(ctx, keyID) - if err == nil { - return webKey, nil, nil - } - if !zerrors.IsNotFound(err) { - return nil, nil, err - } - } - - pubKey, err := q.GetPublicKeyByID(ctx, keyID) + webKey, err := q.GetPublicWebKeyByID(ctx, keyID) if err != nil { return nil, nil, err } - return jsonWebkey(pubKey), gu.Ptr(pubKey.Expiry()), nil + return webKey, nil, nil } } diff --git a/internal/api/oidc/op.go b/internal/api/oidc/op.go index d7171b957b..6f59ce3525 100644 --- a/internal/api/oidc/op.go +++ b/internal/api/oidc/op.go @@ -18,10 +18,8 @@ import ( "github.com/zitadel/zitadel/internal/cache" "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/crypto" - "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/domain/federatedlogout" "github.com/zitadel/zitadel/internal/eventstore" - "github.com/zitadel/zitadel/internal/eventstore/handler/crdb" "github.com/zitadel/zitadel/internal/query" "github.com/zitadel/zitadel/internal/telemetry/metrics" "github.com/zitadel/zitadel/internal/zerrors" @@ -75,7 +73,6 @@ type OPStorage struct { defaultRefreshTokenIdleExpiration time.Duration defaultRefreshTokenExpiration time.Duration encAlg crypto.EncryptionAlgorithm - locker crdb.Locker assetAPIPrefix func(ctx context.Context) string contextToIssuer func(context.Context) string federateLogoutCache cache.Cache[federatedlogout.Index, string, *federatedlogout.FederatedLogout] @@ -91,14 +88,14 @@ type Provider struct { // IDTokenHintVerifier configures a Verifier and supported signing algorithms based on the Web Key feature in the context. func (o *Provider) IDTokenHintVerifier(ctx context.Context) *op.IDTokenHintVerifier { return op.NewIDTokenHintVerifier(op.IssuerFromContext(ctx), o.idTokenHintKeySet, op.WithSupportedIDTokenHintSigningAlgorithms( - supportedSigningAlgs(ctx)..., + supportedSigningAlgs()..., )) } // AccessTokenVerifier configures a Verifier and supported signing algorithms based on the Web Key feature in the context. func (o *Provider) AccessTokenVerifier(ctx context.Context) *op.AccessTokenVerifier { return op.NewAccessTokenVerifier(op.IssuerFromContext(ctx), o.accessTokenKeySet, op.WithSupportedAccessTokenSigningAlgorithms( - supportedSigningAlgs(ctx)..., + supportedSigningAlgs()..., )) } @@ -113,7 +110,6 @@ func NewServer( encryptionAlg crypto.EncryptionAlgorithm, cryptoKey []byte, es *eventstore.Eventstore, - projections *database.DB, userAgentCookie, instanceHandler func(http.Handler) http.Handler, accessHandler *middleware.AccessInterceptor, fallbackLogger *slog.Logger, @@ -124,7 +120,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, ContextToIssuer, federatedLogoutCache) + storage := newStorage(config, command, query, repo, encryptionAlg, es, ContextToIssuer, federatedLogoutCache) keyCache := newPublicKeyCache(ctx, config.PublicKeyCacheMaxAge, queryKeyFunc(query)) accessTokenKeySet := newOidcKeySet(keyCache, withKeyExpiryCheck(true)) idTokenHintKeySet := newOidcKeySet(keyCache) @@ -236,7 +232,6 @@ func newStorage( repo repository.Repository, encAlg crypto.EncryptionAlgorithm, es *eventstore.Eventstore, - db *database.DB, contextToIssuer func(context.Context) string, federateLogoutCache cache.Cache[federatedlogout.Index, string, *federatedlogout.FederatedLogout], ) *OPStorage { @@ -253,7 +248,6 @@ func newStorage( defaultRefreshTokenIdleExpiration: config.DefaultRefreshTokenIdleExpiration, defaultRefreshTokenExpiration: config.DefaultRefreshTokenExpiration, encAlg: encAlg, - locker: crdb.NewLocker(db.DB, locksTable, signingKey), assetAPIPrefix: assets.AssetAPI(), contextToIssuer: contextToIssuer, federateLogoutCache: federateLogoutCache, diff --git a/internal/api/oidc/server.go b/internal/api/oidc/server.go index 1a0854e2a6..df7127443f 100644 --- a/internal/api/oidc/server.go +++ b/internal/api/oidc/server.go @@ -188,7 +188,7 @@ func (s *Server) createDiscoveryConfig(ctx context.Context, supportedUILocales o }, GrantTypesSupported: op.GrantTypes(s.Provider()), SubjectTypesSupported: op.SubjectTypes(s.Provider()), - IDTokenSigningAlgValuesSupported: supportedSigningAlgs(ctx), + IDTokenSigningAlgValuesSupported: supportedSigningAlgs(), RequestObjectSigningAlgValuesSupported: op.RequestObjectSigAlgorithms(s.Provider()), TokenEndpointAuthMethodsSupported: op.AuthMethodsTokenEndpoint(s.Provider()), TokenEndpointAuthSigningAlgValuesSupported: op.TokenSigAlgorithms(s.Provider()), diff --git a/internal/api/oidc/server_test.go b/internal/api/oidc/server_test.go index 76d073151a..9bf22fd210 100644 --- a/internal/api/oidc/server_test.go +++ b/internal/api/oidc/server_test.go @@ -8,9 +8,6 @@ import ( "github.com/zitadel/oidc/v3/pkg/oidc" "github.com/zitadel/oidc/v3/pkg/op" "golang.org/x/text/language" - - "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/feature" ) func TestServer_createDiscoveryConfig(t *testing.T) { @@ -63,92 +60,6 @@ func TestServer_createDiscoveryConfig(t *testing.T) { ctx: op.ContextWithIssuer(context.Background(), "https://issuer.com"), supportedUILocales: []language.Tag{language.English, language.German}, }, - &oidc.DiscoveryConfiguration{ - Issuer: "https://issuer.com", - AuthorizationEndpoint: "https://issuer.com/auth", - TokenEndpoint: "https://issuer.com/token", - IntrospectionEndpoint: "https://issuer.com/introspect", - UserinfoEndpoint: "https://issuer.com/userinfo", - RevocationEndpoint: "https://issuer.com/revoke", - EndSessionEndpoint: "https://issuer.com/logout", - DeviceAuthorizationEndpoint: "https://issuer.com/device", - CheckSessionIframe: "", - JwksURI: "https://issuer.com/keys", - RegistrationEndpoint: "", - ScopesSupported: []string{oidc.ScopeOpenID, oidc.ScopeProfile, oidc.ScopeEmail, oidc.ScopePhone, oidc.ScopeAddress, oidc.ScopeOfflineAccess}, - ResponseTypesSupported: []string{string(oidc.ResponseTypeCode), string(oidc.ResponseTypeIDTokenOnly), string(oidc.ResponseTypeIDToken)}, - ResponseModesSupported: []string{string(oidc.ResponseModeQuery), string(oidc.ResponseModeFragment), string(oidc.ResponseModeFormPost)}, - GrantTypesSupported: []oidc.GrantType{oidc.GrantTypeCode, oidc.GrantTypeImplicit, oidc.GrantTypeRefreshToken, oidc.GrantTypeBearer}, - ACRValuesSupported: nil, - SubjectTypesSupported: []string{"public"}, - IDTokenSigningAlgValuesSupported: []string{"RS256"}, - IDTokenEncryptionAlgValuesSupported: nil, - IDTokenEncryptionEncValuesSupported: nil, - UserinfoSigningAlgValuesSupported: nil, - UserinfoEncryptionAlgValuesSupported: nil, - UserinfoEncryptionEncValuesSupported: nil, - RequestObjectSigningAlgValuesSupported: []string{"RS256"}, - RequestObjectEncryptionAlgValuesSupported: nil, - RequestObjectEncryptionEncValuesSupported: nil, - TokenEndpointAuthMethodsSupported: []oidc.AuthMethod{oidc.AuthMethodNone, oidc.AuthMethodBasic, oidc.AuthMethodPost, oidc.AuthMethodPrivateKeyJWT}, - TokenEndpointAuthSigningAlgValuesSupported: []string{"RS256"}, - RevocationEndpointAuthMethodsSupported: []oidc.AuthMethod{oidc.AuthMethodNone, oidc.AuthMethodBasic, oidc.AuthMethodPost, oidc.AuthMethodPrivateKeyJWT}, - RevocationEndpointAuthSigningAlgValuesSupported: []string{"RS256"}, - IntrospectionEndpointAuthMethodsSupported: []oidc.AuthMethod{oidc.AuthMethodBasic, oidc.AuthMethodPrivateKeyJWT}, - IntrospectionEndpointAuthSigningAlgValuesSupported: []string{"RS256"}, - DisplayValuesSupported: nil, - ClaimTypesSupported: nil, - ClaimsSupported: []string{"sub", "aud", "exp", "iat", "iss", "auth_time", "nonce", "acr", "amr", "c_hash", "at_hash", "act", "scopes", "client_id", "azp", "preferred_username", "name", "family_name", "given_name", "locale", "email", "email_verified", "phone_number", "phone_number_verified"}, - ClaimsParameterSupported: false, - CodeChallengeMethodsSupported: []oidc.CodeChallengeMethod{"S256"}, - ServiceDocumentation: "", - ClaimsLocalesSupported: nil, - UILocalesSupported: []language.Tag{language.English, language.German}, - RequestParameterSupported: true, - RequestURIParameterSupported: false, - RequireRequestURIRegistration: false, - OPPolicyURI: "", - OPTermsOfServiceURI: "", - }, - }, - { - "web keys feature enabled", - fields{ - LegacyServer: op.NewLegacyServer( - func() *op.Provider { - //nolint:staticcheck - provider, _ := op.NewForwardedOpenIDProvider("path", - &op.Config{ - CodeMethodS256: true, - AuthMethodPost: true, - AuthMethodPrivateKeyJWT: true, - GrantTypeRefreshToken: true, - RequestObjectSupported: true, - }, - nil, - ) - return provider - }(), - op.Endpoints{ - Authorization: op.NewEndpoint("auth"), - Token: op.NewEndpoint("token"), - Introspection: op.NewEndpoint("introspect"), - Userinfo: op.NewEndpoint("userinfo"), - Revocation: op.NewEndpoint("revoke"), - EndSession: op.NewEndpoint("logout"), - JwksURI: op.NewEndpoint("keys"), - DeviceAuthorization: op.NewEndpoint("device"), - }, - ), - signingKeyAlgorithm: "RS256", - }, - args{ - ctx: authz.WithFeatures( - op.ContextWithIssuer(context.Background(), "https://issuer.com"), - feature.Features{WebKey: true}, - ), - supportedUILocales: []language.Tag{language.English, language.German}, - }, &oidc.DiscoveryConfiguration{ Issuer: "https://issuer.com", AuthorizationEndpoint: "https://issuer.com/auth", diff --git a/internal/api/oidc/token.go b/internal/api/oidc/token.go index 485f455784..2efc0fb583 100644 --- a/internal/api/oidc/token.go +++ b/internal/api/oidc/token.go @@ -12,7 +12,6 @@ import ( "github.com/zitadel/oidc/v3/pkg/oidc" "github.com/zitadel/oidc/v3/pkg/op" - "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/telemetry/tracing" @@ -64,14 +63,13 @@ func (s *Server) accessTokenResponseFromSession(ctx context.Context, client op.C type SignerFunc func(ctx context.Context) (jose.Signer, jose.SignatureAlgorithm, error) func (s *Server) getSignerOnce() SignerFunc { - return GetSignerOnce(s.query.GetActiveSigningWebKey, s.Provider().Storage().SigningKey) + return GetSignerOnce(s.query.GetActiveSigningWebKey) } // GetSignerOnce returns a function which retrieves the instance's signer from the database once. // Repeated calls of the returned function return the same results. func GetSignerOnce( getActiveSigningWebKey func(ctx context.Context) (*jose.JSONWebKey, error), - getSigningKey func(ctx context.Context) (op.SigningKey, error), ) SignerFunc { var ( once sync.Once @@ -84,23 +82,12 @@ func GetSignerOnce( ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - if authz.GetFeatures(ctx).WebKey { - var webKey *jose.JSONWebKey - webKey, err = getActiveSigningWebKey(ctx) - if err != nil { - return - } - signer, signAlg, err = signerFromWebKey(webKey) - return - } - - var signingKey op.SigningKey - signingKey, err = getSigningKey(ctx) + var webKey *jose.JSONWebKey + webKey, err = getActiveSigningWebKey(ctx) if err != nil { return } - signAlg = signingKey.SignatureAlgorithm() - signer, err = op.SignerFromKey(signingKey) + signer, signAlg, err = signerFromWebKey(webKey) }) return signer, signAlg, err } diff --git a/internal/api/saml/certificate.go b/internal/api/saml/certificate.go index 14752cd5cd..e0eb31255e 100644 --- a/internal/api/saml/certificate.go +++ b/internal/api/saml/certificate.go @@ -14,13 +14,14 @@ import ( "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/query" + "github.com/zitadel/zitadel/internal/query/projection" "github.com/zitadel/zitadel/internal/repository/instance" "github.com/zitadel/zitadel/internal/repository/keypair" "github.com/zitadel/zitadel/internal/zerrors" ) const ( - locksTable = "projections.locks" + locksTable = projection.LocksTable signingKey = "signing_key" samlUser = "SAML" diff --git a/internal/authz/repository/eventsourcing/eventstore/token_verifier.go b/internal/authz/repository/eventsourcing/eventstore/token_verifier.go index b707631c22..d6c14afea3 100644 --- a/internal/authz/repository/eventsourcing/eventstore/token_verifier.go +++ b/internal/authz/repository/eventsourcing/eventstore/token_verifier.go @@ -4,7 +4,6 @@ import ( "context" "encoding/base64" "fmt" - "slices" "strings" "time" @@ -329,18 +328,10 @@ type openIDKeySet struct { // VerifySignature implements the oidc.KeySet interface // providing an implementation for the keys retrieved directly from Queries func (o *openIDKeySet) VerifySignature(ctx context.Context, jws *jose.JSONWebSignature) (payload []byte, err error) { - keySet := new(jose.JSONWebKeySet) - if authz.GetFeatures(ctx).WebKey { - keySet, err = o.Queries.GetWebKeySet(ctx) - if err != nil { - return nil, err - } - } - legacyKeySet, err := o.Queries.ActivePublicKeys(ctx, time.Now()) + keySet, err := o.Queries.GetWebKeySet(ctx) if err != nil { - return nil, fmt.Errorf("error fetching keys: %w", err) + return nil, err } - appendPublicKeysToWebKeySet(keySet, legacyKeySet) keyID, alg := oidc.GetKeyIDAndAlg(jws) key, err := oidc.FindMatchingKey(keyID, oidc.KeyUseSignature, alg, keySet.Keys...) if err != nil { @@ -348,19 +339,3 @@ func (o *openIDKeySet) VerifySignature(ctx context.Context, jws *jose.JSONWebSig } return jws.Verify(&key) } - -func appendPublicKeysToWebKeySet(keyset *jose.JSONWebKeySet, pubkeys *query.PublicKeys) { - if pubkeys == nil || len(pubkeys.Keys) == 0 { - return - } - keyset.Keys = slices.Grow(keyset.Keys, len(pubkeys.Keys)) - - for _, key := range pubkeys.Keys { - keyset.Keys = append(keyset.Keys, jose.JSONWebKey{ - Key: key.Key(), - KeyID: key.ID(), - Algorithm: key.Algorithm(), - Use: key.Use().String(), - }) - } -} diff --git a/internal/command/instance_features.go b/internal/command/instance_features.go index 4d35d5a318..21de5653a9 100644 --- a/internal/command/instance_features.go +++ b/internal/command/instance_features.go @@ -3,11 +3,8 @@ 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" @@ -21,7 +18,6 @@ type InstanceFeatures struct { UserSchema *bool TokenExchange *bool ImprovedPerformance []feature.ImprovedPerformanceType - WebKey *bool DebugOIDCParentError *bool OIDCSingleV1SessionTermination *bool DisableUserTokenEvent *bool @@ -38,7 +34,6 @@ func (m *InstanceFeatures) isEmpty() bool { m.TokenExchange == nil && // nil check to allow unset improvements m.ImprovedPerformance == nil && - m.WebKey == nil && m.DebugOIDCParentError == nil && m.OIDCSingleV1SessionTermination == nil && m.DisableUserTokenEvent == nil && @@ -55,9 +50,6 @@ 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 @@ -78,21 +70,6 @@ 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 399013aded..8ca2865eae 100644 --- a/internal/command/instance_features_model.go +++ b/internal/command/instance_features_model.go @@ -71,7 +71,6 @@ func (m *InstanceFeaturesWriteModel) Query() *eventstore.SearchQueryBuilder { feature_v2.InstanceUserSchemaEventType, feature_v2.InstanceTokenExchangeEventType, feature_v2.InstanceImprovedPerformanceEventType, - feature_v2.InstanceWebKeyEventType, feature_v2.InstanceDebugOIDCParentErrorEventType, feature_v2.InstanceOIDCSingleV1SessionTerminationEventType, feature_v2.InstanceDisableUserTokenEvent, @@ -106,9 +105,6 @@ 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 case feature.KeyDebugOIDCParentError: v := value.(bool) features.DebugOIDCParentError = &v @@ -140,7 +136,6 @@ func (wm *InstanceFeaturesWriteModel) setCommands(ctx context.Context, f *Instan cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.TokenExchange, f.TokenExchange, feature_v2.InstanceTokenExchangeEventType) cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.UserSchema, f.UserSchema, feature_v2.InstanceUserSchemaEventType) cmds = appendFeatureSliceUpdate(ctx, cmds, aggregate, wm.ImprovedPerformance, f.ImprovedPerformance, feature_v2.InstanceImprovedPerformanceEventType) - cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.WebKey, f.WebKey, feature_v2.InstanceWebKeyEventType) cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.DebugOIDCParentError, f.DebugOIDCParentError, feature_v2.InstanceDebugOIDCParentErrorEventType) cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.OIDCSingleV1SessionTermination, f.OIDCSingleV1SessionTermination, feature_v2.InstanceOIDCSingleV1SessionTerminationEventType) cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.DisableUserTokenEvent, f.DisableUserTokenEvent, feature_v2.InstanceDisableUserTokenEvent) diff --git a/internal/command/key_pair.go b/internal/command/key_pair.go index 90eaf7e3da..76193431d6 100644 --- a/internal/command/key_pair.go +++ b/internal/command/key_pair.go @@ -13,31 +13,6 @@ import ( "github.com/zitadel/zitadel/internal/repository/keypair" ) -func (c *Commands) GenerateSigningKeyPair(ctx context.Context, algorithm string) error { - privateCrypto, publicCrypto, err := crypto.GenerateEncryptedKeyPair(c.keySize, c.keyAlgorithm) - if err != nil { - return err - } - keyID, err := c.idGenerator.Next() - if err != nil { - return err - } - - privateKeyExp := time.Now().UTC().Add(c.privateKeyLifetime) - publicKeyExp := time.Now().UTC().Add(c.publicKeyLifetime) - - keyPairWriteModel := NewKeyPairWriteModel(keyID, authz.GetInstance(ctx).InstanceID()) - keyAgg := KeyPairAggregateFromWriteModel(&keyPairWriteModel.WriteModel) - _, err = c.eventstore.Push(ctx, keypair.NewAddedEvent( - ctx, - keyAgg, - crypto.KeyUsageSigning, - algorithm, - privateCrypto, publicCrypto, - privateKeyExp, publicKeyExp)) - return err -} - func (c *Commands) GenerateSAMLCACertificate(ctx context.Context, algorithm string) error { now := time.Now().UTC() after := now.Add(c.certificateLifetime) diff --git a/internal/crypto/rsa.go b/internal/crypto/rsa.go index 198610d8aa..3fd9a77569 100644 --- a/internal/crypto/rsa.go +++ b/internal/crypto/rsa.go @@ -21,14 +21,6 @@ func GenerateKeyPair(bits int) (*rsa.PrivateKey, *rsa.PublicKey, error) { return privkey, &privkey.PublicKey, nil } -func GenerateEncryptedKeyPair(bits int, alg EncryptionAlgorithm) (*CryptoValue, *CryptoValue, error) { - privateKey, publicKey, err := GenerateKeyPair(bits) - if err != nil { - return nil, nil, err - } - return EncryptKeys(privateKey, publicKey, alg) -} - type CertificateInformations struct { SerialNumber *big.Int Organisation []string diff --git a/internal/feature/feature.go b/internal/feature/feature.go index 2e32b6b122..107b06edf1 100644 --- a/internal/feature/feature.go +++ b/internal/feature/feature.go @@ -9,7 +9,7 @@ import ( type Key int const ( - // Reserved: 3, 6 + // Reserved: 3, 6, 8 KeyUnspecified Key = 0 KeyLoginDefaultOrg Key = 1 @@ -17,7 +17,6 @@ const ( KeyUserSchema Key = 4 KeyTokenExchange Key = 5 KeyImprovedPerformance Key = 7 - KeyWebKey Key = 8 KeyDebugOIDCParentError Key = 9 KeyOIDCSingleV1SessionTermination Key = 10 KeyDisableUserTokenEvent Key = 11 @@ -46,7 +45,6 @@ type Features struct { UserSchema bool `json:"user_schema,omitempty"` TokenExchange bool `json:"token_exchange,omitempty"` ImprovedPerformance []ImprovedPerformanceType `json:"improved_performance,omitempty"` - WebKey bool `json:"web_key,omitempty"` DebugOIDCParentError bool `json:"debug_oidc_parent_error,omitempty"` OIDCSingleV1SessionTermination bool `json:"oidc_single_v1_session_termination,omitempty"` DisableUserTokenEvent bool `json:"disable_user_token_event,omitempty"` diff --git a/internal/feature/key_enumer.go b/internal/feature/key_enumer.go index e06199120a..1b4fb9a3ad 100644 --- a/internal/feature/key_enumer.go +++ b/internal/feature/key_enumer.go @@ -12,14 +12,17 @@ const ( _KeyLowerName_0 = "unspecifiedlogin_default_orgtrigger_introspection_projections" _KeyName_1 = "user_schematoken_exchange" _KeyLowerName_1 = "user_schematoken_exchange" - _KeyName_2 = "improved_performanceweb_keydebug_oidc_parent_erroroidc_single_v1_session_terminationdisable_user_token_eventenable_back_channel_logoutlogin_v2permission_check_v2console_use_v2_user_api" - _KeyLowerName_2 = "improved_performanceweb_keydebug_oidc_parent_erroroidc_single_v1_session_terminationdisable_user_token_eventenable_back_channel_logoutlogin_v2permission_check_v2console_use_v2_user_api" + _KeyName_2 = "improved_performance" + _KeyLowerName_2 = "improved_performance" + _KeyName_3 = "debug_oidc_parent_erroroidc_single_v1_session_terminationdisable_user_token_eventenable_back_channel_logoutlogin_v2permission_check_v2console_use_v2_user_api" + _KeyLowerName_3 = "debug_oidc_parent_erroroidc_single_v1_session_terminationdisable_user_token_eventenable_back_channel_logoutlogin_v2permission_check_v2console_use_v2_user_api" ) var ( _KeyIndex_0 = [...]uint8{0, 11, 28, 61} _KeyIndex_1 = [...]uint8{0, 11, 25} - _KeyIndex_2 = [...]uint8{0, 20, 27, 50, 84, 108, 134, 142, 161, 184} + _KeyIndex_2 = [...]uint8{0, 20} + _KeyIndex_3 = [...]uint8{0, 23, 57, 81, 107, 115, 134, 157} ) func (i Key) String() string { @@ -29,9 +32,11 @@ func (i Key) String() string { case 4 <= i && i <= 5: i -= 4 return _KeyName_1[_KeyIndex_1[i]:_KeyIndex_1[i+1]] - case 7 <= i && i <= 15: - i -= 7 - return _KeyName_2[_KeyIndex_2[i]:_KeyIndex_2[i+1]] + case i == 7: + return _KeyName_2 + case 9 <= i && i <= 15: + i -= 9 + return _KeyName_3[_KeyIndex_3[i]:_KeyIndex_3[i+1]] default: return fmt.Sprintf("Key(%d)", i) } @@ -47,7 +52,6 @@ func _KeyNoOp() { _ = x[KeyUserSchema-(4)] _ = x[KeyTokenExchange-(5)] _ = x[KeyImprovedPerformance-(7)] - _ = x[KeyWebKey-(8)] _ = x[KeyDebugOIDCParentError-(9)] _ = x[KeyOIDCSingleV1SessionTermination-(10)] _ = x[KeyDisableUserTokenEvent-(11)] @@ -57,7 +61,7 @@ func _KeyNoOp() { _ = x[KeyConsoleUseV2UserApi-(15)] } -var _KeyValues = []Key{KeyUnspecified, KeyLoginDefaultOrg, KeyTriggerIntrospectionProjections, KeyUserSchema, KeyTokenExchange, KeyImprovedPerformance, KeyWebKey, KeyDebugOIDCParentError, KeyOIDCSingleV1SessionTermination, KeyDisableUserTokenEvent, KeyEnableBackChannelLogout, KeyLoginV2, KeyPermissionCheckV2, KeyConsoleUseV2UserApi} +var _KeyValues = []Key{KeyUnspecified, KeyLoginDefaultOrg, KeyTriggerIntrospectionProjections, KeyUserSchema, KeyTokenExchange, KeyImprovedPerformance, KeyDebugOIDCParentError, KeyOIDCSingleV1SessionTermination, KeyDisableUserTokenEvent, KeyEnableBackChannelLogout, KeyLoginV2, KeyPermissionCheckV2, KeyConsoleUseV2UserApi} var _KeyNameToValueMap = map[string]Key{ _KeyName_0[0:11]: KeyUnspecified, @@ -72,22 +76,20 @@ var _KeyNameToValueMap = map[string]Key{ _KeyLowerName_1[11:25]: KeyTokenExchange, _KeyName_2[0:20]: KeyImprovedPerformance, _KeyLowerName_2[0:20]: KeyImprovedPerformance, - _KeyName_2[20:27]: KeyWebKey, - _KeyLowerName_2[20:27]: KeyWebKey, - _KeyName_2[27:50]: KeyDebugOIDCParentError, - _KeyLowerName_2[27:50]: KeyDebugOIDCParentError, - _KeyName_2[50:84]: KeyOIDCSingleV1SessionTermination, - _KeyLowerName_2[50:84]: KeyOIDCSingleV1SessionTermination, - _KeyName_2[84:108]: KeyDisableUserTokenEvent, - _KeyLowerName_2[84:108]: KeyDisableUserTokenEvent, - _KeyName_2[108:134]: KeyEnableBackChannelLogout, - _KeyLowerName_2[108:134]: KeyEnableBackChannelLogout, - _KeyName_2[134:142]: KeyLoginV2, - _KeyLowerName_2[134:142]: KeyLoginV2, - _KeyName_2[142:161]: KeyPermissionCheckV2, - _KeyLowerName_2[142:161]: KeyPermissionCheckV2, - _KeyName_2[161:184]: KeyConsoleUseV2UserApi, - _KeyLowerName_2[161:184]: KeyConsoleUseV2UserApi, + _KeyName_3[0:23]: KeyDebugOIDCParentError, + _KeyLowerName_3[0:23]: KeyDebugOIDCParentError, + _KeyName_3[23:57]: KeyOIDCSingleV1SessionTermination, + _KeyLowerName_3[23:57]: KeyOIDCSingleV1SessionTermination, + _KeyName_3[57:81]: KeyDisableUserTokenEvent, + _KeyLowerName_3[57:81]: KeyDisableUserTokenEvent, + _KeyName_3[81:107]: KeyEnableBackChannelLogout, + _KeyLowerName_3[81:107]: KeyEnableBackChannelLogout, + _KeyName_3[107:115]: KeyLoginV2, + _KeyLowerName_3[107:115]: KeyLoginV2, + _KeyName_3[115:134]: KeyPermissionCheckV2, + _KeyLowerName_3[115:134]: KeyPermissionCheckV2, + _KeyName_3[134:157]: KeyConsoleUseV2UserApi, + _KeyLowerName_3[134:157]: KeyConsoleUseV2UserApi, } var _KeyNames = []string{ @@ -97,14 +99,13 @@ var _KeyNames = []string{ _KeyName_1[0:11], _KeyName_1[11:25], _KeyName_2[0:20], - _KeyName_2[20:27], - _KeyName_2[27:50], - _KeyName_2[50:84], - _KeyName_2[84:108], - _KeyName_2[108:134], - _KeyName_2[134:142], - _KeyName_2[142:161], - _KeyName_2[161:184], + _KeyName_3[0:23], + _KeyName_3[23:57], + _KeyName_3[57:81], + _KeyName_3[81:107], + _KeyName_3[107:115], + _KeyName_3[115:134], + _KeyName_3[134:157], } // KeyString retrieves an enum value from the enum constants string name. diff --git a/internal/notification/handlers/back_channel_logout.go b/internal/notification/handlers/back_channel_logout.go index f1a99146ca..983915ac28 100644 --- a/internal/notification/handlers/back_channel_logout.go +++ b/internal/notification/handlers/back_channel_logout.go @@ -7,10 +7,8 @@ import ( "sync" "time" - "github.com/zitadel/logging" "github.com/zitadel/oidc/v3/pkg/crypto" "github.com/zitadel/oidc/v3/pkg/oidc" - "github.com/zitadel/oidc/v3/pkg/op" "github.com/zitadel/zitadel/internal/api/authz" http_utils "github.com/zitadel/zitadel/internal/api/http" @@ -149,7 +147,7 @@ func (u *backChannelLogoutNotifier) terminateSession(ctx context.Context, id str return err } - getSigner := zoidc.GetSignerOnce(u.queries.GetActiveSigningWebKey, u.signingKey) + getSigner := zoidc.GetSignerOnce(u.queries.GetActiveSigningWebKey) var wg sync.WaitGroup wg.Add(len(sessions.sessions)) @@ -172,20 +170,6 @@ func (u *backChannelLogoutNotifier) terminateSession(ctx context.Context, id str return errors.Join(errs...) } -func (u *backChannelLogoutNotifier) signingKey(ctx context.Context) (op.SigningKey, error) { - keys, err := u.queries.ActivePrivateSigningKey(ctx, time.Now()) - if err != nil { - return nil, err - } - if len(keys.Keys) == 0 { - logging.WithFields("instanceID", authz.GetInstance(ctx).InstanceID()). - Info("There's no active signing key and automatic rotation is not supported for back channel logout." + - "Please enable the webkey management feature on your instance") - return nil, zerrors.ThrowPreconditionFailed(nil, "HANDL-DF3nf", "no active signing key") - } - return zoidc.PrivateKeyToSigningKey(zoidc.SelectSigningKey(keys.Keys), u.keyEncryptionAlg) -} - func (u *backChannelLogoutNotifier) sendLogoutToken(ctx context.Context, oidcSession *backChannelLogoutOIDCSessions, e eventstore.Event, getSigner zoidc.SignerFunc) error { token, err := u.logoutToken(ctx, oidcSession, getSigner) if err != nil { diff --git a/internal/notification/handlers/mock/commands.mock.go b/internal/notification/handlers/mock/commands.mock.go index 7d41c30f30..ec327de8e8 100644 --- a/internal/notification/handlers/mock/commands.mock.go +++ b/internal/notification/handlers/mock/commands.mock.go @@ -23,6 +23,7 @@ import ( type MockCommands struct { ctrl *gomock.Controller recorder *MockCommandsMockRecorder + isgomock struct{} } // MockCommandsMockRecorder is the mock recorder for MockCommands. @@ -43,197 +44,197 @@ func (m *MockCommands) EXPECT() *MockCommandsMockRecorder { } // HumanEmailVerificationCodeSent mocks base method. -func (m *MockCommands) HumanEmailVerificationCodeSent(arg0 context.Context, arg1, arg2 string) error { +func (m *MockCommands) HumanEmailVerificationCodeSent(ctx context.Context, orgID, userID string) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "HumanEmailVerificationCodeSent", arg0, arg1, arg2) + ret := m.ctrl.Call(m, "HumanEmailVerificationCodeSent", ctx, orgID, userID) ret0, _ := ret[0].(error) return ret0 } // HumanEmailVerificationCodeSent indicates an expected call of HumanEmailVerificationCodeSent. -func (mr *MockCommandsMockRecorder) HumanEmailVerificationCodeSent(arg0, arg1, arg2 any) *gomock.Call { +func (mr *MockCommandsMockRecorder) HumanEmailVerificationCodeSent(ctx, orgID, userID any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HumanEmailVerificationCodeSent", reflect.TypeOf((*MockCommands)(nil).HumanEmailVerificationCodeSent), arg0, arg1, arg2) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HumanEmailVerificationCodeSent", reflect.TypeOf((*MockCommands)(nil).HumanEmailVerificationCodeSent), ctx, orgID, userID) } // HumanInitCodeSent mocks base method. -func (m *MockCommands) HumanInitCodeSent(arg0 context.Context, arg1, arg2 string) error { +func (m *MockCommands) HumanInitCodeSent(ctx context.Context, orgID, userID string) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "HumanInitCodeSent", arg0, arg1, arg2) + ret := m.ctrl.Call(m, "HumanInitCodeSent", ctx, orgID, userID) ret0, _ := ret[0].(error) return ret0 } // HumanInitCodeSent indicates an expected call of HumanInitCodeSent. -func (mr *MockCommandsMockRecorder) HumanInitCodeSent(arg0, arg1, arg2 any) *gomock.Call { +func (mr *MockCommandsMockRecorder) HumanInitCodeSent(ctx, orgID, userID any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HumanInitCodeSent", reflect.TypeOf((*MockCommands)(nil).HumanInitCodeSent), arg0, arg1, arg2) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HumanInitCodeSent", reflect.TypeOf((*MockCommands)(nil).HumanInitCodeSent), ctx, orgID, userID) } // HumanOTPEmailCodeSent mocks base method. -func (m *MockCommands) HumanOTPEmailCodeSent(arg0 context.Context, arg1, arg2 string) error { +func (m *MockCommands) HumanOTPEmailCodeSent(ctx context.Context, userID, resourceOwner string) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "HumanOTPEmailCodeSent", arg0, arg1, arg2) + ret := m.ctrl.Call(m, "HumanOTPEmailCodeSent", ctx, userID, resourceOwner) ret0, _ := ret[0].(error) return ret0 } // HumanOTPEmailCodeSent indicates an expected call of HumanOTPEmailCodeSent. -func (mr *MockCommandsMockRecorder) HumanOTPEmailCodeSent(arg0, arg1, arg2 any) *gomock.Call { +func (mr *MockCommandsMockRecorder) HumanOTPEmailCodeSent(ctx, userID, resourceOwner any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HumanOTPEmailCodeSent", reflect.TypeOf((*MockCommands)(nil).HumanOTPEmailCodeSent), arg0, arg1, arg2) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HumanOTPEmailCodeSent", reflect.TypeOf((*MockCommands)(nil).HumanOTPEmailCodeSent), ctx, userID, resourceOwner) } // HumanOTPSMSCodeSent mocks base method. -func (m *MockCommands) HumanOTPSMSCodeSent(arg0 context.Context, arg1, arg2 string, arg3 *senders.CodeGeneratorInfo) error { +func (m *MockCommands) HumanOTPSMSCodeSent(ctx context.Context, userID, resourceOwner string, generatorInfo *senders.CodeGeneratorInfo) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "HumanOTPSMSCodeSent", arg0, arg1, arg2, arg3) + ret := m.ctrl.Call(m, "HumanOTPSMSCodeSent", ctx, userID, resourceOwner, generatorInfo) ret0, _ := ret[0].(error) return ret0 } // HumanOTPSMSCodeSent indicates an expected call of HumanOTPSMSCodeSent. -func (mr *MockCommandsMockRecorder) HumanOTPSMSCodeSent(arg0, arg1, arg2, arg3 any) *gomock.Call { +func (mr *MockCommandsMockRecorder) HumanOTPSMSCodeSent(ctx, userID, resourceOwner, generatorInfo any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HumanOTPSMSCodeSent", reflect.TypeOf((*MockCommands)(nil).HumanOTPSMSCodeSent), arg0, arg1, arg2, arg3) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HumanOTPSMSCodeSent", reflect.TypeOf((*MockCommands)(nil).HumanOTPSMSCodeSent), ctx, userID, resourceOwner, generatorInfo) } // HumanPasswordlessInitCodeSent mocks base method. -func (m *MockCommands) HumanPasswordlessInitCodeSent(arg0 context.Context, arg1, arg2, arg3 string) error { +func (m *MockCommands) HumanPasswordlessInitCodeSent(ctx context.Context, userID, resourceOwner, codeID string) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "HumanPasswordlessInitCodeSent", arg0, arg1, arg2, arg3) + ret := m.ctrl.Call(m, "HumanPasswordlessInitCodeSent", ctx, userID, resourceOwner, codeID) ret0, _ := ret[0].(error) return ret0 } // HumanPasswordlessInitCodeSent indicates an expected call of HumanPasswordlessInitCodeSent. -func (mr *MockCommandsMockRecorder) HumanPasswordlessInitCodeSent(arg0, arg1, arg2, arg3 any) *gomock.Call { +func (mr *MockCommandsMockRecorder) HumanPasswordlessInitCodeSent(ctx, userID, resourceOwner, codeID any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HumanPasswordlessInitCodeSent", reflect.TypeOf((*MockCommands)(nil).HumanPasswordlessInitCodeSent), arg0, arg1, arg2, arg3) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HumanPasswordlessInitCodeSent", reflect.TypeOf((*MockCommands)(nil).HumanPasswordlessInitCodeSent), ctx, userID, resourceOwner, codeID) } // HumanPhoneVerificationCodeSent mocks base method. -func (m *MockCommands) HumanPhoneVerificationCodeSent(arg0 context.Context, arg1, arg2 string, arg3 *senders.CodeGeneratorInfo) error { +func (m *MockCommands) HumanPhoneVerificationCodeSent(ctx context.Context, orgID, userID string, generatorInfo *senders.CodeGeneratorInfo) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "HumanPhoneVerificationCodeSent", arg0, arg1, arg2, arg3) + ret := m.ctrl.Call(m, "HumanPhoneVerificationCodeSent", ctx, orgID, userID, generatorInfo) ret0, _ := ret[0].(error) return ret0 } // HumanPhoneVerificationCodeSent indicates an expected call of HumanPhoneVerificationCodeSent. -func (mr *MockCommandsMockRecorder) HumanPhoneVerificationCodeSent(arg0, arg1, arg2, arg3 any) *gomock.Call { +func (mr *MockCommandsMockRecorder) HumanPhoneVerificationCodeSent(ctx, orgID, userID, generatorInfo any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HumanPhoneVerificationCodeSent", reflect.TypeOf((*MockCommands)(nil).HumanPhoneVerificationCodeSent), arg0, arg1, arg2, arg3) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HumanPhoneVerificationCodeSent", reflect.TypeOf((*MockCommands)(nil).HumanPhoneVerificationCodeSent), ctx, orgID, userID, generatorInfo) } // InviteCodeSent mocks base method. -func (m *MockCommands) InviteCodeSent(arg0 context.Context, arg1, arg2 string) error { +func (m *MockCommands) InviteCodeSent(ctx context.Context, orgID, userID string) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "InviteCodeSent", arg0, arg1, arg2) + ret := m.ctrl.Call(m, "InviteCodeSent", ctx, orgID, userID) ret0, _ := ret[0].(error) return ret0 } // InviteCodeSent indicates an expected call of InviteCodeSent. -func (mr *MockCommandsMockRecorder) InviteCodeSent(arg0, arg1, arg2 any) *gomock.Call { +func (mr *MockCommandsMockRecorder) InviteCodeSent(ctx, orgID, userID any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InviteCodeSent", reflect.TypeOf((*MockCommands)(nil).InviteCodeSent), arg0, arg1, arg2) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InviteCodeSent", reflect.TypeOf((*MockCommands)(nil).InviteCodeSent), ctx, orgID, userID) } // MilestonePushed mocks base method. -func (m *MockCommands) MilestonePushed(arg0 context.Context, arg1 string, arg2 milestone.Type, arg3 []string) error { +func (m *MockCommands) MilestonePushed(ctx context.Context, instanceID string, msType milestone.Type, endpoints []string) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "MilestonePushed", arg0, arg1, arg2, arg3) + ret := m.ctrl.Call(m, "MilestonePushed", ctx, instanceID, msType, endpoints) ret0, _ := ret[0].(error) return ret0 } // MilestonePushed indicates an expected call of MilestonePushed. -func (mr *MockCommandsMockRecorder) MilestonePushed(arg0, arg1, arg2, arg3 any) *gomock.Call { +func (mr *MockCommandsMockRecorder) MilestonePushed(ctx, instanceID, msType, endpoints any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MilestonePushed", reflect.TypeOf((*MockCommands)(nil).MilestonePushed), arg0, arg1, arg2, arg3) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MilestonePushed", reflect.TypeOf((*MockCommands)(nil).MilestonePushed), ctx, instanceID, msType, endpoints) } // OTPEmailSent mocks base method. -func (m *MockCommands) OTPEmailSent(arg0 context.Context, arg1, arg2 string) error { +func (m *MockCommands) OTPEmailSent(ctx context.Context, sessionID, resourceOwner string) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "OTPEmailSent", arg0, arg1, arg2) + ret := m.ctrl.Call(m, "OTPEmailSent", ctx, sessionID, resourceOwner) ret0, _ := ret[0].(error) return ret0 } // OTPEmailSent indicates an expected call of OTPEmailSent. -func (mr *MockCommandsMockRecorder) OTPEmailSent(arg0, arg1, arg2 any) *gomock.Call { +func (mr *MockCommandsMockRecorder) OTPEmailSent(ctx, sessionID, resourceOwner any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OTPEmailSent", reflect.TypeOf((*MockCommands)(nil).OTPEmailSent), arg0, arg1, arg2) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OTPEmailSent", reflect.TypeOf((*MockCommands)(nil).OTPEmailSent), ctx, sessionID, resourceOwner) } // OTPSMSSent mocks base method. -func (m *MockCommands) OTPSMSSent(arg0 context.Context, arg1, arg2 string, arg3 *senders.CodeGeneratorInfo) error { +func (m *MockCommands) OTPSMSSent(ctx context.Context, sessionID, resourceOwner string, generatorInfo *senders.CodeGeneratorInfo) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "OTPSMSSent", arg0, arg1, arg2, arg3) + ret := m.ctrl.Call(m, "OTPSMSSent", ctx, sessionID, resourceOwner, generatorInfo) ret0, _ := ret[0].(error) return ret0 } // OTPSMSSent indicates an expected call of OTPSMSSent. -func (mr *MockCommandsMockRecorder) OTPSMSSent(arg0, arg1, arg2, arg3 any) *gomock.Call { +func (mr *MockCommandsMockRecorder) OTPSMSSent(ctx, sessionID, resourceOwner, generatorInfo any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OTPSMSSent", reflect.TypeOf((*MockCommands)(nil).OTPSMSSent), arg0, arg1, arg2, arg3) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OTPSMSSent", reflect.TypeOf((*MockCommands)(nil).OTPSMSSent), ctx, sessionID, resourceOwner, generatorInfo) } // PasswordChangeSent mocks base method. -func (m *MockCommands) PasswordChangeSent(arg0 context.Context, arg1, arg2 string) error { +func (m *MockCommands) PasswordChangeSent(ctx context.Context, orgID, userID string) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "PasswordChangeSent", arg0, arg1, arg2) + ret := m.ctrl.Call(m, "PasswordChangeSent", ctx, orgID, userID) ret0, _ := ret[0].(error) return ret0 } // PasswordChangeSent indicates an expected call of PasswordChangeSent. -func (mr *MockCommandsMockRecorder) PasswordChangeSent(arg0, arg1, arg2 any) *gomock.Call { +func (mr *MockCommandsMockRecorder) PasswordChangeSent(ctx, orgID, userID any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PasswordChangeSent", reflect.TypeOf((*MockCommands)(nil).PasswordChangeSent), arg0, arg1, arg2) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PasswordChangeSent", reflect.TypeOf((*MockCommands)(nil).PasswordChangeSent), ctx, orgID, userID) } // PasswordCodeSent mocks base method. -func (m *MockCommands) PasswordCodeSent(arg0 context.Context, arg1, arg2 string, arg3 *senders.CodeGeneratorInfo) error { +func (m *MockCommands) PasswordCodeSent(ctx context.Context, orgID, userID string, generatorInfo *senders.CodeGeneratorInfo) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "PasswordCodeSent", arg0, arg1, arg2, arg3) + ret := m.ctrl.Call(m, "PasswordCodeSent", ctx, orgID, userID, generatorInfo) ret0, _ := ret[0].(error) return ret0 } // PasswordCodeSent indicates an expected call of PasswordCodeSent. -func (mr *MockCommandsMockRecorder) PasswordCodeSent(arg0, arg1, arg2, arg3 any) *gomock.Call { +func (mr *MockCommandsMockRecorder) PasswordCodeSent(ctx, orgID, userID, generatorInfo any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PasswordCodeSent", reflect.TypeOf((*MockCommands)(nil).PasswordCodeSent), arg0, arg1, arg2, arg3) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PasswordCodeSent", reflect.TypeOf((*MockCommands)(nil).PasswordCodeSent), ctx, orgID, userID, generatorInfo) } // UsageNotificationSent mocks base method. -func (m *MockCommands) UsageNotificationSent(arg0 context.Context, arg1 *quota.NotificationDueEvent) error { +func (m *MockCommands) UsageNotificationSent(ctx context.Context, dueEvent *quota.NotificationDueEvent) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UsageNotificationSent", arg0, arg1) + ret := m.ctrl.Call(m, "UsageNotificationSent", ctx, dueEvent) ret0, _ := ret[0].(error) return ret0 } // UsageNotificationSent indicates an expected call of UsageNotificationSent. -func (mr *MockCommandsMockRecorder) UsageNotificationSent(arg0, arg1 any) *gomock.Call { +func (mr *MockCommandsMockRecorder) UsageNotificationSent(ctx, dueEvent any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UsageNotificationSent", reflect.TypeOf((*MockCommands)(nil).UsageNotificationSent), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UsageNotificationSent", reflect.TypeOf((*MockCommands)(nil).UsageNotificationSent), ctx, dueEvent) } // UserDomainClaimedSent mocks base method. -func (m *MockCommands) UserDomainClaimedSent(arg0 context.Context, arg1, arg2 string) error { +func (m *MockCommands) UserDomainClaimedSent(ctx context.Context, orgID, userID string) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UserDomainClaimedSent", arg0, arg1, arg2) + ret := m.ctrl.Call(m, "UserDomainClaimedSent", ctx, orgID, userID) ret0, _ := ret[0].(error) return ret0 } // UserDomainClaimedSent indicates an expected call of UserDomainClaimedSent. -func (mr *MockCommandsMockRecorder) UserDomainClaimedSent(arg0, arg1, arg2 any) *gomock.Call { +func (mr *MockCommandsMockRecorder) UserDomainClaimedSent(ctx, orgID, userID any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UserDomainClaimedSent", reflect.TypeOf((*MockCommands)(nil).UserDomainClaimedSent), arg0, arg1, arg2) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UserDomainClaimedSent", reflect.TypeOf((*MockCommands)(nil).UserDomainClaimedSent), ctx, orgID, userID) } diff --git a/internal/notification/handlers/mock/queries.mock.go b/internal/notification/handlers/mock/queries.mock.go index 670d3f3896..2cf53d1b2a 100644 --- a/internal/notification/handlers/mock/queries.mock.go +++ b/internal/notification/handlers/mock/queries.mock.go @@ -12,7 +12,6 @@ package mock import ( context "context" reflect "reflect" - time "time" jose "github.com/go-jose/go-jose/v4" authz "github.com/zitadel/zitadel/internal/api/authz" @@ -26,6 +25,7 @@ import ( type MockQueries struct { ctrl *gomock.Controller recorder *MockQueriesMockRecorder + isgomock struct{} } // MockQueriesMockRecorder is the mock recorder for MockQueries. @@ -60,240 +60,225 @@ func (mr *MockQueriesMockRecorder) ActiveInstances() *gomock.Call { } // ActiveLabelPolicyByOrg mocks base method. -func (m *MockQueries) ActiveLabelPolicyByOrg(arg0 context.Context, arg1 string, arg2 bool) (*query.LabelPolicy, error) { +func (m *MockQueries) ActiveLabelPolicyByOrg(ctx context.Context, orgID string, withOwnerRemoved bool) (*query.LabelPolicy, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ActiveLabelPolicyByOrg", arg0, arg1, arg2) + ret := m.ctrl.Call(m, "ActiveLabelPolicyByOrg", ctx, orgID, withOwnerRemoved) ret0, _ := ret[0].(*query.LabelPolicy) ret1, _ := ret[1].(error) return ret0, ret1 } // ActiveLabelPolicyByOrg indicates an expected call of ActiveLabelPolicyByOrg. -func (mr *MockQueriesMockRecorder) ActiveLabelPolicyByOrg(arg0, arg1, arg2 any) *gomock.Call { +func (mr *MockQueriesMockRecorder) ActiveLabelPolicyByOrg(ctx, orgID, withOwnerRemoved any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ActiveLabelPolicyByOrg", reflect.TypeOf((*MockQueries)(nil).ActiveLabelPolicyByOrg), arg0, arg1, arg2) -} - -// ActivePrivateSigningKey mocks base method. -func (m *MockQueries) ActivePrivateSigningKey(arg0 context.Context, arg1 time.Time) (*query.PrivateKeys, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ActivePrivateSigningKey", arg0, arg1) - ret0, _ := ret[0].(*query.PrivateKeys) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// ActivePrivateSigningKey indicates an expected call of ActivePrivateSigningKey. -func (mr *MockQueriesMockRecorder) ActivePrivateSigningKey(arg0, arg1 any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ActivePrivateSigningKey", reflect.TypeOf((*MockQueries)(nil).ActivePrivateSigningKey), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ActiveLabelPolicyByOrg", reflect.TypeOf((*MockQueries)(nil).ActiveLabelPolicyByOrg), ctx, orgID, withOwnerRemoved) } // CustomTextListByTemplate mocks base method. -func (m *MockQueries) CustomTextListByTemplate(arg0 context.Context, arg1, arg2 string, arg3 bool) (*query.CustomTexts, error) { +func (m *MockQueries) CustomTextListByTemplate(ctx context.Context, aggregateID, template string, withOwnerRemoved bool) (*query.CustomTexts, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CustomTextListByTemplate", arg0, arg1, arg2, arg3) + ret := m.ctrl.Call(m, "CustomTextListByTemplate", ctx, aggregateID, template, withOwnerRemoved) ret0, _ := ret[0].(*query.CustomTexts) ret1, _ := ret[1].(error) return ret0, ret1 } // CustomTextListByTemplate indicates an expected call of CustomTextListByTemplate. -func (mr *MockQueriesMockRecorder) CustomTextListByTemplate(arg0, arg1, arg2, arg3 any) *gomock.Call { +func (mr *MockQueriesMockRecorder) CustomTextListByTemplate(ctx, aggregateID, template, withOwnerRemoved any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CustomTextListByTemplate", reflect.TypeOf((*MockQueries)(nil).CustomTextListByTemplate), arg0, arg1, arg2, arg3) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CustomTextListByTemplate", reflect.TypeOf((*MockQueries)(nil).CustomTextListByTemplate), ctx, aggregateID, template, withOwnerRemoved) } // GetActiveSigningWebKey mocks base method. -func (m *MockQueries) GetActiveSigningWebKey(arg0 context.Context) (*jose.JSONWebKey, error) { +func (m *MockQueries) GetActiveSigningWebKey(ctx context.Context) (*jose.JSONWebKey, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetActiveSigningWebKey", arg0) + ret := m.ctrl.Call(m, "GetActiveSigningWebKey", ctx) ret0, _ := ret[0].(*jose.JSONWebKey) ret1, _ := ret[1].(error) return ret0, ret1 } // GetActiveSigningWebKey indicates an expected call of GetActiveSigningWebKey. -func (mr *MockQueriesMockRecorder) GetActiveSigningWebKey(arg0 any) *gomock.Call { +func (mr *MockQueriesMockRecorder) GetActiveSigningWebKey(ctx any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetActiveSigningWebKey", reflect.TypeOf((*MockQueries)(nil).GetActiveSigningWebKey), arg0) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetActiveSigningWebKey", reflect.TypeOf((*MockQueries)(nil).GetActiveSigningWebKey), ctx) } // GetDefaultLanguage mocks base method. -func (m *MockQueries) GetDefaultLanguage(arg0 context.Context) language.Tag { +func (m *MockQueries) GetDefaultLanguage(ctx context.Context) language.Tag { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetDefaultLanguage", arg0) + ret := m.ctrl.Call(m, "GetDefaultLanguage", ctx) ret0, _ := ret[0].(language.Tag) return ret0 } // GetDefaultLanguage indicates an expected call of GetDefaultLanguage. -func (mr *MockQueriesMockRecorder) GetDefaultLanguage(arg0 any) *gomock.Call { +func (mr *MockQueriesMockRecorder) GetDefaultLanguage(ctx any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDefaultLanguage", reflect.TypeOf((*MockQueries)(nil).GetDefaultLanguage), arg0) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDefaultLanguage", reflect.TypeOf((*MockQueries)(nil).GetDefaultLanguage), ctx) } // GetInstanceRestrictions mocks base method. -func (m *MockQueries) GetInstanceRestrictions(arg0 context.Context) (query.Restrictions, error) { +func (m *MockQueries) GetInstanceRestrictions(ctx context.Context) (query.Restrictions, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetInstanceRestrictions", arg0) + ret := m.ctrl.Call(m, "GetInstanceRestrictions", ctx) ret0, _ := ret[0].(query.Restrictions) ret1, _ := ret[1].(error) return ret0, ret1 } // GetInstanceRestrictions indicates an expected call of GetInstanceRestrictions. -func (mr *MockQueriesMockRecorder) GetInstanceRestrictions(arg0 any) *gomock.Call { +func (mr *MockQueriesMockRecorder) GetInstanceRestrictions(ctx any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetInstanceRestrictions", reflect.TypeOf((*MockQueries)(nil).GetInstanceRestrictions), arg0) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetInstanceRestrictions", reflect.TypeOf((*MockQueries)(nil).GetInstanceRestrictions), ctx) } // GetNotifyUserByID mocks base method. -func (m *MockQueries) GetNotifyUserByID(arg0 context.Context, arg1 bool, arg2 string) (*query.NotifyUser, error) { +func (m *MockQueries) GetNotifyUserByID(ctx context.Context, shouldTriggered bool, userID string) (*query.NotifyUser, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetNotifyUserByID", arg0, arg1, arg2) + ret := m.ctrl.Call(m, "GetNotifyUserByID", ctx, shouldTriggered, userID) ret0, _ := ret[0].(*query.NotifyUser) ret1, _ := ret[1].(error) return ret0, ret1 } // GetNotifyUserByID indicates an expected call of GetNotifyUserByID. -func (mr *MockQueriesMockRecorder) GetNotifyUserByID(arg0, arg1, arg2 any) *gomock.Call { +func (mr *MockQueriesMockRecorder) GetNotifyUserByID(ctx, shouldTriggered, userID any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNotifyUserByID", reflect.TypeOf((*MockQueries)(nil).GetNotifyUserByID), arg0, arg1, arg2) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNotifyUserByID", reflect.TypeOf((*MockQueries)(nil).GetNotifyUserByID), ctx, shouldTriggered, userID) } // InstanceByID mocks base method. -func (m *MockQueries) InstanceByID(arg0 context.Context, arg1 string) (authz.Instance, error) { +func (m *MockQueries) InstanceByID(ctx context.Context, id string) (authz.Instance, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "InstanceByID", arg0, arg1) + ret := m.ctrl.Call(m, "InstanceByID", ctx, id) ret0, _ := ret[0].(authz.Instance) ret1, _ := ret[1].(error) return ret0, ret1 } // InstanceByID indicates an expected call of InstanceByID. -func (mr *MockQueriesMockRecorder) InstanceByID(arg0, arg1 any) *gomock.Call { +func (mr *MockQueriesMockRecorder) InstanceByID(ctx, id any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InstanceByID", reflect.TypeOf((*MockQueries)(nil).InstanceByID), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InstanceByID", reflect.TypeOf((*MockQueries)(nil).InstanceByID), ctx, id) } // MailTemplateByOrg mocks base method. -func (m *MockQueries) MailTemplateByOrg(arg0 context.Context, arg1 string, arg2 bool) (*query.MailTemplate, error) { +func (m *MockQueries) MailTemplateByOrg(ctx context.Context, orgID string, withOwnerRemoved bool) (*query.MailTemplate, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "MailTemplateByOrg", arg0, arg1, arg2) + ret := m.ctrl.Call(m, "MailTemplateByOrg", ctx, orgID, withOwnerRemoved) ret0, _ := ret[0].(*query.MailTemplate) ret1, _ := ret[1].(error) return ret0, ret1 } // MailTemplateByOrg indicates an expected call of MailTemplateByOrg. -func (mr *MockQueriesMockRecorder) MailTemplateByOrg(arg0, arg1, arg2 any) *gomock.Call { +func (mr *MockQueriesMockRecorder) MailTemplateByOrg(ctx, orgID, withOwnerRemoved any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MailTemplateByOrg", reflect.TypeOf((*MockQueries)(nil).MailTemplateByOrg), arg0, arg1, arg2) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MailTemplateByOrg", reflect.TypeOf((*MockQueries)(nil).MailTemplateByOrg), ctx, orgID, withOwnerRemoved) } // NotificationPolicyByOrg mocks base method. -func (m *MockQueries) NotificationPolicyByOrg(arg0 context.Context, arg1 bool, arg2 string, arg3 bool) (*query.NotificationPolicy, error) { +func (m *MockQueries) NotificationPolicyByOrg(ctx context.Context, shouldTriggerBulk bool, orgID string, withOwnerRemoved bool) (*query.NotificationPolicy, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "NotificationPolicyByOrg", arg0, arg1, arg2, arg3) + ret := m.ctrl.Call(m, "NotificationPolicyByOrg", ctx, shouldTriggerBulk, orgID, withOwnerRemoved) ret0, _ := ret[0].(*query.NotificationPolicy) ret1, _ := ret[1].(error) return ret0, ret1 } // NotificationPolicyByOrg indicates an expected call of NotificationPolicyByOrg. -func (mr *MockQueriesMockRecorder) NotificationPolicyByOrg(arg0, arg1, arg2, arg3 any) *gomock.Call { +func (mr *MockQueriesMockRecorder) NotificationPolicyByOrg(ctx, shouldTriggerBulk, orgID, withOwnerRemoved any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NotificationPolicyByOrg", reflect.TypeOf((*MockQueries)(nil).NotificationPolicyByOrg), arg0, arg1, arg2, arg3) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NotificationPolicyByOrg", reflect.TypeOf((*MockQueries)(nil).NotificationPolicyByOrg), ctx, shouldTriggerBulk, orgID, withOwnerRemoved) } // NotificationProviderByIDAndType mocks base method. -func (m *MockQueries) NotificationProviderByIDAndType(arg0 context.Context, arg1 string, arg2 domain.NotificationProviderType) (*query.DebugNotificationProvider, error) { +func (m *MockQueries) NotificationProviderByIDAndType(ctx context.Context, aggID string, providerType domain.NotificationProviderType) (*query.DebugNotificationProvider, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "NotificationProviderByIDAndType", arg0, arg1, arg2) + ret := m.ctrl.Call(m, "NotificationProviderByIDAndType", ctx, aggID, providerType) ret0, _ := ret[0].(*query.DebugNotificationProvider) ret1, _ := ret[1].(error) return ret0, ret1 } // NotificationProviderByIDAndType indicates an expected call of NotificationProviderByIDAndType. -func (mr *MockQueriesMockRecorder) NotificationProviderByIDAndType(arg0, arg1, arg2 any) *gomock.Call { +func (mr *MockQueriesMockRecorder) NotificationProviderByIDAndType(ctx, aggID, providerType any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NotificationProviderByIDAndType", reflect.TypeOf((*MockQueries)(nil).NotificationProviderByIDAndType), arg0, arg1, arg2) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NotificationProviderByIDAndType", reflect.TypeOf((*MockQueries)(nil).NotificationProviderByIDAndType), ctx, aggID, providerType) } // SMSProviderConfigActive mocks base method. -func (m *MockQueries) SMSProviderConfigActive(arg0 context.Context, arg1 string) (*query.SMSConfig, error) { +func (m *MockQueries) SMSProviderConfigActive(ctx context.Context, resourceOwner string) (*query.SMSConfig, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SMSProviderConfigActive", arg0, arg1) + ret := m.ctrl.Call(m, "SMSProviderConfigActive", ctx, resourceOwner) ret0, _ := ret[0].(*query.SMSConfig) ret1, _ := ret[1].(error) return ret0, ret1 } // SMSProviderConfigActive indicates an expected call of SMSProviderConfigActive. -func (mr *MockQueriesMockRecorder) SMSProviderConfigActive(arg0, arg1 any) *gomock.Call { +func (mr *MockQueriesMockRecorder) SMSProviderConfigActive(ctx, resourceOwner any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SMSProviderConfigActive", reflect.TypeOf((*MockQueries)(nil).SMSProviderConfigActive), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SMSProviderConfigActive", reflect.TypeOf((*MockQueries)(nil).SMSProviderConfigActive), ctx, resourceOwner) } // SMTPConfigActive mocks base method. -func (m *MockQueries) SMTPConfigActive(arg0 context.Context, arg1 string) (*query.SMTPConfig, error) { +func (m *MockQueries) SMTPConfigActive(ctx context.Context, resourceOwner string) (*query.SMTPConfig, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SMTPConfigActive", arg0, arg1) + ret := m.ctrl.Call(m, "SMTPConfigActive", ctx, resourceOwner) ret0, _ := ret[0].(*query.SMTPConfig) ret1, _ := ret[1].(error) return ret0, ret1 } // SMTPConfigActive indicates an expected call of SMTPConfigActive. -func (mr *MockQueriesMockRecorder) SMTPConfigActive(arg0, arg1 any) *gomock.Call { +func (mr *MockQueriesMockRecorder) SMTPConfigActive(ctx, resourceOwner any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SMTPConfigActive", reflect.TypeOf((*MockQueries)(nil).SMTPConfigActive), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SMTPConfigActive", reflect.TypeOf((*MockQueries)(nil).SMTPConfigActive), ctx, resourceOwner) } // SearchInstanceDomains mocks base method. -func (m *MockQueries) SearchInstanceDomains(arg0 context.Context, arg1 *query.InstanceDomainSearchQueries) (*query.InstanceDomains, error) { +func (m *MockQueries) SearchInstanceDomains(ctx context.Context, queries *query.InstanceDomainSearchQueries) (*query.InstanceDomains, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SearchInstanceDomains", arg0, arg1) + ret := m.ctrl.Call(m, "SearchInstanceDomains", ctx, queries) ret0, _ := ret[0].(*query.InstanceDomains) ret1, _ := ret[1].(error) return ret0, ret1 } // SearchInstanceDomains indicates an expected call of SearchInstanceDomains. -func (mr *MockQueriesMockRecorder) SearchInstanceDomains(arg0, arg1 any) *gomock.Call { +func (mr *MockQueriesMockRecorder) SearchInstanceDomains(ctx, queries any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SearchInstanceDomains", reflect.TypeOf((*MockQueries)(nil).SearchInstanceDomains), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SearchInstanceDomains", reflect.TypeOf((*MockQueries)(nil).SearchInstanceDomains), ctx, queries) } // SearchMilestones mocks base method. -func (m *MockQueries) SearchMilestones(arg0 context.Context, arg1 []string, arg2 *query.MilestonesSearchQueries) (*query.Milestones, error) { +func (m *MockQueries) SearchMilestones(ctx context.Context, instanceIDs []string, queries *query.MilestonesSearchQueries) (*query.Milestones, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SearchMilestones", arg0, arg1, arg2) + ret := m.ctrl.Call(m, "SearchMilestones", ctx, instanceIDs, queries) ret0, _ := ret[0].(*query.Milestones) ret1, _ := ret[1].(error) return ret0, ret1 } // SearchMilestones indicates an expected call of SearchMilestones. -func (mr *MockQueriesMockRecorder) SearchMilestones(arg0, arg1, arg2 any) *gomock.Call { +func (mr *MockQueriesMockRecorder) SearchMilestones(ctx, instanceIDs, queries any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SearchMilestones", reflect.TypeOf((*MockQueries)(nil).SearchMilestones), arg0, arg1, arg2) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SearchMilestones", reflect.TypeOf((*MockQueries)(nil).SearchMilestones), ctx, instanceIDs, queries) } // SessionByID mocks base method. -func (m *MockQueries) SessionByID(arg0 context.Context, arg1 bool, arg2, arg3 string, arg4 domain.PermissionCheck) (*query.Session, error) { +func (m *MockQueries) SessionByID(ctx context.Context, shouldTriggerBulk bool, id, sessionToken string, check domain.PermissionCheck) (*query.Session, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SessionByID", arg0, arg1, arg2, arg3, arg4) + ret := m.ctrl.Call(m, "SessionByID", ctx, shouldTriggerBulk, id, sessionToken, check) ret0, _ := ret[0].(*query.Session) ret1, _ := ret[1].(error) return ret0, ret1 } // SessionByID indicates an expected call of SessionByID. -func (mr *MockQueriesMockRecorder) SessionByID(arg0, arg1, arg2, arg3, arg4 any) *gomock.Call { +func (mr *MockQueriesMockRecorder) SessionByID(ctx, shouldTriggerBulk, id, sessionToken, check any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SessionByID", reflect.TypeOf((*MockQueries)(nil).SessionByID), arg0, arg1, arg2, arg3, arg4) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SessionByID", reflect.TypeOf((*MockQueries)(nil).SessionByID), ctx, shouldTriggerBulk, id, sessionToken, check) } diff --git a/internal/notification/handlers/mock/queue.mock.go b/internal/notification/handlers/mock/queue.mock.go index e1387595db..e9cf3efed1 100644 --- a/internal/notification/handlers/mock/queue.mock.go +++ b/internal/notification/handlers/mock/queue.mock.go @@ -22,6 +22,7 @@ import ( type MockQueue struct { ctrl *gomock.Controller recorder *MockQueueMockRecorder + isgomock struct{} } // MockQueueMockRecorder is the mock recorder for MockQueue. @@ -42,10 +43,10 @@ func (m *MockQueue) EXPECT() *MockQueueMockRecorder { } // Insert mocks base method. -func (m *MockQueue) Insert(arg0 context.Context, arg1 river.JobArgs, arg2 ...queue.InsertOpt) error { +func (m *MockQueue) Insert(ctx context.Context, args river.JobArgs, opts ...queue.InsertOpt) error { m.ctrl.T.Helper() - varargs := []any{arg0, arg1} - for _, a := range arg2 { + varargs := []any{ctx, args} + for _, a := range opts { varargs = append(varargs, a) } ret := m.ctrl.Call(m, "Insert", varargs...) @@ -54,8 +55,8 @@ func (m *MockQueue) Insert(arg0 context.Context, arg1 river.JobArgs, arg2 ...que } // Insert indicates an expected call of Insert. -func (mr *MockQueueMockRecorder) Insert(arg0, arg1 any, arg2 ...any) *gomock.Call { +func (mr *MockQueueMockRecorder) Insert(ctx, args any, opts ...any) *gomock.Call { mr.mock.ctrl.T.Helper() - varargs := append([]any{arg0, arg1}, arg2...) + varargs := append([]any{ctx, args}, opts...) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Insert", reflect.TypeOf((*MockQueue)(nil).Insert), varargs...) } diff --git a/internal/notification/handlers/queries.go b/internal/notification/handlers/queries.go index a3d68e4797..d9ff1b4201 100644 --- a/internal/notification/handlers/queries.go +++ b/internal/notification/handlers/queries.go @@ -2,7 +2,6 @@ package handlers import ( "context" - "time" "github.com/go-jose/go-jose/v4" "golang.org/x/text/language" @@ -30,7 +29,6 @@ type Queries interface { GetInstanceRestrictions(ctx context.Context) (restrictions query.Restrictions, err error) InstanceByID(ctx context.Context, id string) (instance authz.Instance, err error) GetActiveSigningWebKey(ctx context.Context) (*jose.JSONWebKey, error) - ActivePrivateSigningKey(ctx context.Context, t time.Time) (keys *query.PrivateKeys, err error) ActiveInstances() []string } diff --git a/internal/query/instance_features.go b/internal/query/instance_features.go index 501cfc4e9c..9e0081a542 100644 --- a/internal/query/instance_features.go +++ b/internal/query/instance_features.go @@ -14,7 +14,6 @@ type InstanceFeatures struct { UserSchema FeatureSource[bool] TokenExchange FeatureSource[bool] ImprovedPerformance FeatureSource[[]feature.ImprovedPerformanceType] - WebKey FeatureSource[bool] DebugOIDCParentError FeatureSource[bool] OIDCSingleV1SessionTermination FeatureSource[bool] DisableUserTokenEvent FeatureSource[bool] diff --git a/internal/query/instance_features_model.go b/internal/query/instance_features_model.go index 7130044fbf..a30009e9ee 100644 --- a/internal/query/instance_features_model.go +++ b/internal/query/instance_features_model.go @@ -67,7 +67,6 @@ func (m *InstanceFeaturesReadModel) Query() *eventstore.SearchQueryBuilder { feature_v2.InstanceUserSchemaEventType, feature_v2.InstanceTokenExchangeEventType, feature_v2.InstanceImprovedPerformanceEventType, - feature_v2.InstanceWebKeyEventType, feature_v2.InstanceDebugOIDCParentErrorEventType, feature_v2.InstanceOIDCSingleV1SessionTerminationEventType, feature_v2.InstanceDisableUserTokenEvent, @@ -121,8 +120,6 @@ func reduceInstanceFeatureSet[T any](features *InstanceFeatures, event *feature_ features.TokenExchange.set(level, event.Value) case feature.KeyImprovedPerformance: features.ImprovedPerformance.set(level, event.Value) - case feature.KeyWebKey: - features.WebKey.set(level, event.Value) case feature.KeyDebugOIDCParentError: features.DebugOIDCParentError.set(level, event.Value) case feature.KeyOIDCSingleV1SessionTermination: diff --git a/internal/query/key.go b/internal/query/key.go index 4831d88654..e7b81bb951 100644 --- a/internal/query/key.go +++ b/internal/query/key.go @@ -1,20 +1,10 @@ package query import ( - "context" - "crypto/rsa" - "database/sql" "time" - sq "github.com/Masterminds/squirrel" - - "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/crypto" - "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/query/projection" - "github.com/zitadel/zitadel/internal/repository/keypair" - "github.com/zitadel/zitadel/internal/telemetry/tracing" - "github.com/zitadel/zitadel/internal/zerrors" ) type Key interface { @@ -36,11 +26,6 @@ type PublicKey interface { Key() interface{} } -type PrivateKeys struct { - SearchResponse - Keys []PrivateKey -} - type PublicKeys struct { SearchResponse Keys []PublicKey @@ -72,34 +57,6 @@ func (k *key) Sequence() uint64 { return k.sequence } -type privateKey struct { - key - expiry time.Time - privateKey *crypto.CryptoValue -} - -func (k *privateKey) Expiry() time.Time { - return k.expiry -} - -func (k *privateKey) Key() *crypto.CryptoValue { - return k.privateKey -} - -type rsaPublicKey struct { - key - expiry time.Time - publicKey *rsa.PublicKey -} - -func (r *rsaPublicKey) Expiry() time.Time { - return r.expiry -} - -func (r *rsaPublicKey) Key() interface{} { - return r.publicKey -} - var ( keyTable = table{ name: projection.KeyProjectionTable, @@ -157,277 +114,3 @@ var ( table: keyPrivateTable, } ) - -var ( - keyPublicTable = table{ - name: projection.KeyPublicTable, - instanceIDCol: projection.KeyPrivateColumnInstanceID, - } - KeyPublicColID = Column{ - name: projection.KeyPublicColumnID, - table: keyPublicTable, - } - KeyPublicColExpiry = Column{ - name: projection.KeyPublicColumnExpiry, - table: keyPublicTable, - } - KeyPublicColKey = Column{ - name: projection.KeyPublicColumnKey, - table: keyPublicTable, - } -) - -func (q *Queries) ActivePublicKeys(ctx context.Context, t time.Time) (keys *PublicKeys, err error) { - ctx, span := tracing.NewSpan(ctx) - defer func() { span.EndWithError(err) }() - - query, scan := preparePublicKeysQuery() - if t.IsZero() { - t = time.Now() - } - stmt, args, err := query.Where( - sq.And{ - sq.Eq{KeyColInstanceID.identifier(): authz.GetInstance(ctx).InstanceID()}, - sq.Gt{KeyPublicColExpiry.identifier(): t}, - }).ToSql() - if err != nil { - return nil, zerrors.ThrowInternal(err, "QUERY-SDFfg", "Errors.Query.SQLStatement") - } - - err = q.client.QueryContext(ctx, func(rows *sql.Rows) error { - keys, err = scan(rows) - return err - }, stmt, args...) - if err != nil { - return nil, zerrors.ThrowInternal(err, "QUERY-Sghn4", "Errors.Internal") - } - - keys.State, err = q.latestState(ctx, keyTable) - if !zerrors.IsNotFound(err) { - return keys, err - } - return keys, nil -} - -func (q *Queries) ActivePrivateSigningKey(ctx context.Context, t time.Time) (keys *PrivateKeys, err error) { - ctx, span := tracing.NewSpan(ctx) - defer func() { span.EndWithError(err) }() - - stmt, scan := preparePrivateKeysQuery() - if t.IsZero() { - t = time.Now() - } - query, args, err := stmt.Where( - sq.And{ - sq.Eq{ - KeyColUse.identifier(): crypto.KeyUsageSigning, - KeyColInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), - }, - sq.Gt{KeyPrivateColExpiry.identifier(): t}, - }).OrderBy(KeyPrivateColExpiry.identifier()).ToSql() - if err != nil { - return nil, zerrors.ThrowInternal(err, "QUERY-SDff2", "Errors.Query.SQLStatement") - } - - err = q.client.QueryContext(ctx, func(rows *sql.Rows) error { - keys, err = scan(rows) - return err - }, query, args...) - if err != nil { - return nil, zerrors.ThrowInternal(err, "QUERY-WRFG4", "Errors.Internal") - } - keys.State, err = q.latestState(ctx, keyTable) - if !zerrors.IsNotFound(err) { - return keys, err - } - return keys, nil -} - -func preparePublicKeysQuery() (sq.SelectBuilder, func(*sql.Rows) (*PublicKeys, error)) { - return sq.Select( - KeyColID.identifier(), - KeyColCreationDate.identifier(), - KeyColChangeDate.identifier(), - KeyColSequence.identifier(), - KeyColResourceOwner.identifier(), - KeyColAlgorithm.identifier(), - KeyColUse.identifier(), - KeyPublicColExpiry.identifier(), - KeyPublicColKey.identifier(), - countColumn.identifier(), - ).From(keyTable.identifier()). - LeftJoin(join(KeyPublicColID, KeyColID)). - PlaceholderFormat(sq.Dollar), - func(rows *sql.Rows) (*PublicKeys, error) { - keys := make([]PublicKey, 0) - var count uint64 - for rows.Next() { - k := new(rsaPublicKey) - var keyValue []byte - err := rows.Scan( - &k.id, - &k.creationDate, - &k.changeDate, - &k.sequence, - &k.resourceOwner, - &k.algorithm, - &k.use, - &k.expiry, - &keyValue, - &count, - ) - if err != nil { - return nil, err - } - k.publicKey, err = crypto.BytesToPublicKey(keyValue) - if err != nil { - return nil, err - } - keys = append(keys, k) - } - - if err := rows.Close(); err != nil { - return nil, zerrors.ThrowInternal(err, "QUERY-rKd6k", "Errors.Query.CloseRows") - } - - return &PublicKeys{ - Keys: keys, - SearchResponse: SearchResponse{ - Count: count, - }, - }, nil - } -} - -func preparePrivateKeysQuery() (sq.SelectBuilder, func(*sql.Rows) (*PrivateKeys, error)) { - return sq.Select( - KeyColID.identifier(), - KeyColCreationDate.identifier(), - KeyColChangeDate.identifier(), - KeyColSequence.identifier(), - KeyColResourceOwner.identifier(), - KeyColAlgorithm.identifier(), - KeyColUse.identifier(), - KeyPrivateColExpiry.identifier(), - KeyPrivateColKey.identifier(), - countColumn.identifier(), - ).From(keyTable.identifier()). - LeftJoin(join(KeyPrivateColID, KeyColID)). - PlaceholderFormat(sq.Dollar), - func(rows *sql.Rows) (*PrivateKeys, error) { - keys := make([]PrivateKey, 0) - var count uint64 - for rows.Next() { - k := new(privateKey) - err := rows.Scan( - &k.id, - &k.creationDate, - &k.changeDate, - &k.sequence, - &k.resourceOwner, - &k.algorithm, - &k.use, - &k.expiry, - &k.privateKey, - &count, - ) - if err != nil { - return nil, err - } - keys = append(keys, k) - } - - if err := rows.Close(); err != nil { - return nil, zerrors.ThrowInternal(err, "QUERY-rKd6k", "Errors.Query.CloseRows") - } - - return &PrivateKeys{ - Keys: keys, - SearchResponse: SearchResponse{ - Count: count, - }, - }, nil - } -} - -type PublicKeyReadModel struct { - eventstore.ReadModel - - Algorithm string - Key *crypto.CryptoValue - Expiry time.Time - Usage crypto.KeyUsage -} - -func NewPublicKeyReadModel(keyID, resourceOwner string) *PublicKeyReadModel { - return &PublicKeyReadModel{ - ReadModel: eventstore.ReadModel{ - AggregateID: keyID, - ResourceOwner: resourceOwner, - }, - } -} - -func (wm *PublicKeyReadModel) AppendEvents(events ...eventstore.Event) { - wm.ReadModel.AppendEvents(events...) -} - -func (wm *PublicKeyReadModel) Reduce() error { - for _, event := range wm.Events { - switch e := event.(type) { - case *keypair.AddedEvent: - wm.Algorithm = e.Algorithm - wm.Key = e.PublicKey.Key - wm.Expiry = e.PublicKey.Expiry - wm.Usage = e.Usage - default: - } - } - return wm.ReadModel.Reduce() -} - -func (wm *PublicKeyReadModel) Query() *eventstore.SearchQueryBuilder { - return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). - AwaitOpenTransactions(). - ResourceOwner(wm.ResourceOwner). - AddQuery(). - AggregateTypes(keypair.AggregateType). - AggregateIDs(wm.AggregateID). - EventTypes(keypair.AddedEventType). - Builder() -} - -func (q *Queries) GetPublicKeyByID(ctx context.Context, keyID string) (_ PublicKey, err error) { - ctx, span := tracing.NewSpan(ctx) - defer func() { span.EndWithError(err) }() - - model := NewPublicKeyReadModel(keyID, authz.GetInstance(ctx).InstanceID()) - if err := q.eventstore.FilterToQueryReducer(ctx, model); err != nil { - return nil, err - } - if model.Algorithm == "" || model.Key == nil { - return nil, zerrors.ThrowNotFound(err, "QUERY-Ahf7x", "Errors.Key.NotFound") - } - keyValue, err := crypto.Decrypt(model.Key, q.keyEncryptionAlgorithm) - if err != nil { - return nil, zerrors.ThrowInternal(err, "QUERY-Ie4oh", "Errors.Internal") - } - publicKey, err := crypto.BytesToPublicKey(keyValue) - if err != nil { - return nil, zerrors.ThrowInternal(err, "QUERY-Kai2Z", "Errors.Internal") - } - - return &rsaPublicKey{ - key: key{ - id: model.AggregateID, - creationDate: model.CreationDate, - changeDate: model.ChangeDate, - sequence: model.ProcessedSequence, - resourceOwner: model.ResourceOwner, - algorithm: model.Algorithm, - use: model.Usage, - }, - expiry: model.Expiry, - publicKey: publicKey, - }, nil -} diff --git a/internal/query/key_test.go b/internal/query/key_test.go deleted file mode 100644 index 7bc029fd7f..0000000000 --- a/internal/query/key_test.go +++ /dev/null @@ -1,453 +0,0 @@ -package query - -import ( - "context" - "crypto/rsa" - "database/sql" - "database/sql/driver" - "errors" - "fmt" - "io" - "math/big" - "regexp" - "testing" - "time" - - "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/eventstore" - key_repo "github.com/zitadel/zitadel/internal/repository/keypair" - "github.com/zitadel/zitadel/internal/zerrors" -) - -var ( - preparePublicKeysStmt = `SELECT projections.keys4.id,` + - ` projections.keys4.creation_date,` + - ` projections.keys4.change_date,` + - ` projections.keys4.sequence,` + - ` projections.keys4.resource_owner,` + - ` projections.keys4.algorithm,` + - ` projections.keys4.use,` + - ` projections.keys4_public.expiry,` + - ` projections.keys4_public.key,` + - ` COUNT(*) OVER ()` + - ` FROM projections.keys4` + - ` LEFT JOIN projections.keys4_public ON projections.keys4.id = projections.keys4_public.id AND projections.keys4.instance_id = projections.keys4_public.instance_id` - preparePublicKeysCols = []string{ - "id", - "creation_date", - "change_date", - "sequence", - "resource_owner", - "algorithm", - "use", - "expiry", - "key", - "count", - } - - preparePrivateKeysStmt = `SELECT projections.keys4.id,` + - ` projections.keys4.creation_date,` + - ` projections.keys4.change_date,` + - ` projections.keys4.sequence,` + - ` projections.keys4.resource_owner,` + - ` projections.keys4.algorithm,` + - ` projections.keys4.use,` + - ` projections.keys4_private.expiry,` + - ` projections.keys4_private.key,` + - ` COUNT(*) OVER ()` + - ` FROM projections.keys4` + - ` LEFT JOIN projections.keys4_private ON projections.keys4.id = projections.keys4_private.id AND projections.keys4.instance_id = projections.keys4_private.instance_id` -) - -func Test_KeyPrepares(t *testing.T) { - type want struct { - sqlExpectations sqlExpectation - err checkErr - } - tests := []struct { - name string - prepare interface{} - want want - object interface{} - }{ - { - name: "preparePublicKeysQuery no result", - prepare: preparePublicKeysQuery, - want: want{ - sqlExpectations: mockQueries( - regexp.QuoteMeta(preparePublicKeysStmt), - nil, - nil, - ), - err: func(err error) (error, bool) { - if !zerrors.IsNotFound(err) { - return fmt.Errorf("err should be zitadel.NotFoundError got: %w", err), false - } - return nil, true - }, - }, - object: &PublicKeys{Keys: []PublicKey{}}, - }, - { - name: "preparePublicKeysQuery found", - prepare: preparePublicKeysQuery, - want: want{ - sqlExpectations: mockQueries( - regexp.QuoteMeta(preparePublicKeysStmt), - preparePublicKeysCols, - [][]driver.Value{ - { - "key-id", - testNow, - testNow, - uint64(20211109), - "ro", - "RS256", - 0, - testNow, - []byte("-----BEGIN RSA PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsvX9P58JFxEs5C+L+H7W\nduFSWL5EPzber7C2m94klrSV6q0bAcrYQnGwFOlveThsY200hRbadKaKjHD7qIKH\nDEe0IY2PSRht33Jye52AwhkRw+M3xuQH/7R8LydnsNFk2KHpr5X2SBv42e37LjkE\nslKSaMRgJW+v0KZ30piY8QsdFRKKaVg5/Ajt1YToM1YVsdHXJ3vmXFMtypLdxwUD\ndIaLEX6pFUkU75KSuEQ/E2luT61Q3ta9kOWm9+0zvi7OMcbdekJT7mzcVnh93R1c\n13ZhQCLbh9A7si8jKFtaMWevjayrvqQABEcTN9N4Hoxcyg6l4neZtRDk75OMYcqm\nDQIDAQAB\n-----END RSA PUBLIC KEY-----\n"), - }, - }, - ), - }, - object: &PublicKeys{ - SearchResponse: SearchResponse{ - Count: 1, - }, - Keys: []PublicKey{ - &rsaPublicKey{ - key: key{ - id: "key-id", - creationDate: testNow, - changeDate: testNow, - sequence: 20211109, - resourceOwner: "ro", - algorithm: "RS256", - use: crypto.KeyUsageSigning, - }, - expiry: testNow, - publicKey: &rsa.PublicKey{ - E: 65537, - N: fromBase16("b2f5fd3f9f0917112ce42f8bf87ed676e15258be443f36deafb0b69bde2496b495eaad1b01cad84271b014e96f79386c636d348516da74a68a8c70fba882870c47b4218d8f49186ddf72727b9d80c21911c3e337c6e407ffb47c2f2767b0d164d8a1e9af95f6481bf8d9edfb2e3904b2529268c460256fafd0a677d29898f10b1d15128a695839fc08edd584e8335615b1d1d7277be65c532dca92ddc7050374868b117ea9154914ef9292b8443f13696e4fad50ded6bd90e5a6f7ed33be2ece31c6dd7a4253ee6cdc56787ddd1d5cd776614022db87d03bb22f23285b5a3167af8dacabbea40004471337d3781e8c5cca0ea5e27799b510e4ef938c61caa60d"), - }, - }, - }, - }, - }, - { - name: "preparePublicKeysQuery sql err", - prepare: preparePublicKeysQuery, - want: want{ - sqlExpectations: mockQueryErr( - regexp.QuoteMeta(preparePublicKeysStmt), - 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: (*PublicKeys)(nil), - }, - { - name: "preparePrivateKeysQuery no result", - prepare: preparePrivateKeysQuery, - want: want{ - sqlExpectations: mockQueries( - regexp.QuoteMeta(preparePrivateKeysStmt), - nil, - nil, - ), - err: func(err error) (error, bool) { - if !zerrors.IsNotFound(err) { - return fmt.Errorf("err should be zitadel.NotFoundError got: %w", err), false - } - return nil, true - }, - }, - object: &PrivateKeys{Keys: []PrivateKey{}}, - }, - { - name: "preparePrivateKeysQuery found", - prepare: preparePrivateKeysQuery, - want: want{ - sqlExpectations: mockQueries( - regexp.QuoteMeta(preparePrivateKeysStmt), - preparePublicKeysCols, - [][]driver.Value{ - { - "key-id", - testNow, - testNow, - uint64(20211109), - "ro", - "RS256", - 0, - testNow, - []byte(`{"Algorithm": "enc", "Crypted": "cHJpdmF0ZUtleQ==", "CryptoType": 0, "KeyID": "id"}`), - }, - }, - ), - }, - object: &PrivateKeys{ - SearchResponse: SearchResponse{ - Count: 1, - }, - Keys: []PrivateKey{ - &privateKey{ - key: key{ - id: "key-id", - creationDate: testNow, - changeDate: testNow, - sequence: 20211109, - resourceOwner: "ro", - algorithm: "RS256", - use: crypto.KeyUsageSigning, - }, - expiry: testNow, - privateKey: &crypto.CryptoValue{ - CryptoType: crypto.TypeEncryption, - Algorithm: "enc", - KeyID: "id", - Crypted: []byte("privateKey"), - }, - }, - }, - }, - }, - { - name: "preparePrivateKeysQuery sql err", - prepare: preparePrivateKeysQuery, - want: want{ - sqlExpectations: mockQueryErr( - regexp.QuoteMeta(preparePrivateKeysStmt), - 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: (*PrivateKeys)(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) - }) - } -} - -func fromBase16(base16 string) *big.Int { - i, ok := new(big.Int).SetString(base16, 16) - if !ok { - panic("bad number: " + base16) - } - return i -} - -const pubKey = `-----BEGIN PUBLIC KEY----- -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAs38btwb3c7r0tMaQpGvB -mY+mPwMU/LpfuPoC0k2t4RsKp0fv40SMl50CRrHgk395wch8PMPYbl3+8TtYAJuy -rFALIj3Ff1UcKIk0hOH5DDsfh7/q2wFuncTmS6bifYo8CfSq2vDGnM7nZnEvxY/M -fSydZdcmIqlkUpfQmtzExw9+tSe5Dxq6gn5JtlGgLgZGt69r5iMMrTEGhhVAXzNu -MZbmlCoBru+rC8ITlTX/0V1ZcsSbL8tYWhthyu9x6yjo1bH85wiVI4gs0MhU8f2a -+kjL/KGZbR14Ua2eo6tonBZLC5DHWM2TkYXgRCDPufjcgmzN0Lm91E4P8KvBcvly -6QIDAQAB ------END PUBLIC KEY----- -` - -func TestQueries_GetPublicKeyByID(t *testing.T) { - now := time.Now() - future := now.Add(time.Hour) - - tests := []struct { - name string - eventstore func(*testing.T) *eventstore.Eventstore - encryption func(*testing.T) *crypto.MockEncryptionAlgorithm - want *rsaPublicKey - wantErr error - }{ - { - name: "filter error", - eventstore: expectEventstore( - expectFilterError(io.ErrClosedPipe), - ), - wantErr: io.ErrClosedPipe, - }, - { - name: "not found error", - eventstore: expectEventstore( - expectFilter(), - ), - wantErr: zerrors.ThrowNotFound(nil, "QUERY-Ahf7x", "Errors.Key.NotFound"), - }, - { - name: "decrypt error", - eventstore: expectEventstore( - expectFilter( - eventFromEventPusher(key_repo.NewAddedEvent(context.Background(), - &eventstore.Aggregate{ - ID: "keyID", - Type: key_repo.AggregateType, - ResourceOwner: "instanceID", - InstanceID: "instanceID", - Version: key_repo.AggregateVersion, - }, - crypto.KeyUsageSigning, "alg", - &crypto.CryptoValue{ - CryptoType: crypto.TypeEncryption, - Algorithm: "alg", - KeyID: "keyID", - Crypted: []byte("private"), - }, - &crypto.CryptoValue{ - CryptoType: crypto.TypeEncryption, - Algorithm: "alg", - KeyID: "keyID", - Crypted: []byte("public"), - }, - future, - future, - )), - ), - ), - encryption: func(t *testing.T) *crypto.MockEncryptionAlgorithm { - encryption := crypto.NewMockEncryptionAlgorithm(gomock.NewController(t)) - expect := encryption.EXPECT() - expect.Algorithm().Return("alg") - expect.DecryptionKeyIDs().Return([]string{}) - return encryption - }, - wantErr: zerrors.ThrowInternal(nil, "QUERY-Ie4oh", "Errors.Internal"), - }, - { - name: "parse error", - eventstore: expectEventstore( - expectFilter( - eventFromEventPusher(key_repo.NewAddedEvent(context.Background(), - &eventstore.Aggregate{ - ID: "keyID", - Type: key_repo.AggregateType, - ResourceOwner: "instanceID", - InstanceID: "instanceID", - Version: key_repo.AggregateVersion, - }, - crypto.KeyUsageSigning, "alg", - &crypto.CryptoValue{ - CryptoType: crypto.TypeEncryption, - Algorithm: "alg", - KeyID: "keyID", - Crypted: []byte("private"), - }, - &crypto.CryptoValue{ - CryptoType: crypto.TypeEncryption, - Algorithm: "alg", - KeyID: "keyID", - Crypted: []byte("public"), - }, - future, - future, - )), - ), - ), - encryption: func(t *testing.T) *crypto.MockEncryptionAlgorithm { - encryption := crypto.NewMockEncryptionAlgorithm(gomock.NewController(t)) - expect := encryption.EXPECT() - expect.Algorithm().Return("alg") - expect.DecryptionKeyIDs().Return([]string{"keyID"}) - expect.Decrypt([]byte("public"), "keyID").Return([]byte("foo"), nil) - return encryption - }, - wantErr: zerrors.ThrowInternal(nil, "QUERY-Kai2Z", "Errors.Internal"), - }, - { - name: "success", - eventstore: expectEventstore( - expectFilter( - eventFromEventPusher(key_repo.NewAddedEvent(context.Background(), - &eventstore.Aggregate{ - ID: "keyID", - Type: key_repo.AggregateType, - ResourceOwner: "instanceID", - InstanceID: "instanceID", - Version: key_repo.AggregateVersion, - }, - crypto.KeyUsageSigning, "alg", - &crypto.CryptoValue{ - CryptoType: crypto.TypeEncryption, - Algorithm: "alg", - KeyID: "keyID", - Crypted: []byte("private"), - }, - &crypto.CryptoValue{ - CryptoType: crypto.TypeEncryption, - Algorithm: "alg", - KeyID: "keyID", - Crypted: []byte("public"), - }, - future, - future, - )), - ), - ), - encryption: func(t *testing.T) *crypto.MockEncryptionAlgorithm { - encryption := crypto.NewMockEncryptionAlgorithm(gomock.NewController(t)) - expect := encryption.EXPECT() - expect.Algorithm().Return("alg") - expect.DecryptionKeyIDs().Return([]string{"keyID"}) - expect.Decrypt([]byte("public"), "keyID").Return([]byte(pubKey), nil) - return encryption - }, - want: &rsaPublicKey{ - key: key{ - id: "keyID", - resourceOwner: "instanceID", - algorithm: "alg", - use: crypto.KeyUsageSigning, - }, - expiry: future, - publicKey: func() *rsa.PublicKey { - publicKey, err := crypto.BytesToPublicKey([]byte(pubKey)) - if err != nil { - panic(err) - } - return publicKey - }(), - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - q := &Queries{ - eventstore: tt.eventstore(t), - } - if tt.encryption != nil { - q.keyEncryptionAlgorithm = tt.encryption(t) - } - ctx := authz.NewMockContext("instanceID", "orgID", "loginClient") - key, err := q.GetPublicKeyByID(ctx, "keyID") - if tt.wantErr != nil { - require.ErrorIs(t, err, tt.wantErr) - return - } - require.NoError(t, err) - require.NotNil(t, key) - - got := key.(*rsaPublicKey) - assert.WithinDuration(t, tt.want.expiry, got.expiry, time.Second) - tt.want.expiry = time.Time{} - got.expiry = time.Time{} - assert.Equal(t, tt.want, got) - }) - } -} diff --git a/internal/query/projection/instance_features.go b/internal/query/projection/instance_features.go index 443353c2e5..3c33ff6fdf 100644 --- a/internal/query/projection/instance_features.go +++ b/internal/query/projection/instance_features.go @@ -80,10 +80,6 @@ func (*instanceFeatureProjection) Reducers() []handler.AggregateReducer { Event: feature_v2.InstanceImprovedPerformanceEventType, Reduce: reduceInstanceSetFeature[[]feature.ImprovedPerformanceType], }, - { - Event: feature_v2.InstanceWebKeyEventType, - Reduce: reduceInstanceSetFeature[bool], - }, { Event: feature_v2.InstanceDebugOIDCParentErrorEventType, Reduce: reduceInstanceSetFeature[bool], diff --git a/internal/repository/feature/feature_v2/eventstore.go b/internal/repository/feature/feature_v2/eventstore.go index 62fa568fca..25d0f270f6 100644 --- a/internal/repository/feature/feature_v2/eventstore.go +++ b/internal/repository/feature/feature_v2/eventstore.go @@ -24,7 +24,6 @@ func init() { eventstore.RegisterFilterEventMapper(AggregateType, InstanceUserSchemaEventType, eventstore.GenericEventMapper[SetEvent[bool]]) eventstore.RegisterFilterEventMapper(AggregateType, InstanceTokenExchangeEventType, eventstore.GenericEventMapper[SetEvent[bool]]) eventstore.RegisterFilterEventMapper(AggregateType, InstanceImprovedPerformanceEventType, eventstore.GenericEventMapper[SetEvent[[]feature.ImprovedPerformanceType]]) - eventstore.RegisterFilterEventMapper(AggregateType, InstanceWebKeyEventType, eventstore.GenericEventMapper[SetEvent[bool]]) eventstore.RegisterFilterEventMapper(AggregateType, InstanceDebugOIDCParentErrorEventType, eventstore.GenericEventMapper[SetEvent[bool]]) eventstore.RegisterFilterEventMapper(AggregateType, InstanceOIDCSingleV1SessionTerminationEventType, eventstore.GenericEventMapper[SetEvent[bool]]) eventstore.RegisterFilterEventMapper(AggregateType, InstanceDisableUserTokenEvent, eventstore.GenericEventMapper[SetEvent[bool]]) diff --git a/internal/repository/feature/feature_v2/feature.go b/internal/repository/feature/feature_v2/feature.go index f75fae618b..a87042d72a 100644 --- a/internal/repository/feature/feature_v2/feature.go +++ b/internal/repository/feature/feature_v2/feature.go @@ -29,7 +29,6 @@ var ( InstanceUserSchemaEventType = setEventTypeFromFeature(feature.LevelInstance, feature.KeyUserSchema) InstanceTokenExchangeEventType = setEventTypeFromFeature(feature.LevelInstance, feature.KeyTokenExchange) InstanceImprovedPerformanceEventType = setEventTypeFromFeature(feature.LevelInstance, feature.KeyImprovedPerformance) - InstanceWebKeyEventType = setEventTypeFromFeature(feature.LevelInstance, feature.KeyWebKey) InstanceDebugOIDCParentErrorEventType = setEventTypeFromFeature(feature.LevelInstance, feature.KeyDebugOIDCParentError) InstanceOIDCSingleV1SessionTerminationEventType = setEventTypeFromFeature(feature.LevelInstance, feature.KeyOIDCSingleV1SessionTermination) InstanceDisableUserTokenEvent = setEventTypeFromFeature(feature.LevelInstance, feature.KeyDisableUserTokenEvent) diff --git a/proto/zitadel/feature/v2/instance.proto b/proto/zitadel/feature/v2/instance.proto index 0455befb46..efbe5e3cdf 100644 --- a/proto/zitadel/feature/v2/instance.proto +++ b/proto/zitadel/feature/v2/instance.proto @@ -11,8 +11,8 @@ import "zitadel/feature/v2/feature.proto"; option go_package = "github.com/zitadel/zitadel/pkg/grpc/feature/v2;feature"; message SetInstanceFeaturesRequest{ - reserved 3, 6; - reserved "oidc_legacy_introspection", "actions"; + reserved 3, 6, 8; + reserved "oidc_legacy_introspection", "actions", "web_key"; optional bool login_default_org = 1 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { example: "true"; @@ -49,13 +49,6 @@ message SetInstanceFeaturesRequest{ } ]; - 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."; - } - ]; - optional bool debug_oidc_parent_error = 9 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { example: "true"; @@ -125,8 +118,8 @@ message GetInstanceFeaturesRequest { } message GetInstanceFeaturesResponse { - reserved 4, 7; - reserved "oidc_legacy_introspection", "actions"; + reserved 4, 7, 9; + reserved "oidc_legacy_introspection", "actions", "web_key"; zitadel.object.v2.Details details = 1; FeatureFlag login_default_org = 2 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -163,13 +156,6 @@ message GetInstanceFeaturesResponse { } ]; - 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."; - } - ]; - FeatureFlag debug_oidc_parent_error = 10 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { example: "true"; diff --git a/proto/zitadel/feature/v2beta/instance.proto b/proto/zitadel/feature/v2beta/instance.proto index 8028305fe4..7968668e50 100644 --- a/proto/zitadel/feature/v2beta/instance.proto +++ b/proto/zitadel/feature/v2beta/instance.proto @@ -11,8 +11,8 @@ import "zitadel/feature/v2beta/feature.proto"; option go_package = "github.com/zitadel/zitadel/pkg/grpc/feature/v2beta;feature"; message SetInstanceFeaturesRequest{ - reserved 3, 6; - reserved "oidc_legacy_introspection", "actions"; + reserved 3, 6, 8; + reserved "oidc_legacy_introspection", "actions", "web_key"; optional bool login_default_org = 1 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { example: "true"; @@ -49,13 +49,6 @@ message SetInstanceFeaturesRequest{ } ]; - 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."; - } - ]; - optional bool debug_oidc_parent_error = 9 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { example: "true"; @@ -91,8 +84,8 @@ message GetInstanceFeaturesRequest { } message GetInstanceFeaturesResponse { - reserved 4, 7; - reserved "oidc_legacy_introspection", "actions"; + reserved 4, 7, 9; + reserved "oidc_legacy_introspection", "actions", "web_key"; zitadel.object.v2beta.Details details = 1; FeatureFlag login_default_org = 2 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -129,13 +122,6 @@ message GetInstanceFeaturesResponse { } ]; - 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."; - } - ]; - FeatureFlag debug_oidc_parent_error = 10 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { example: "true"; From 2691dae2b6355d3e962be791db28b88d4b763f98 Mon Sep 17 00:00:00 2001 From: "Marco A." Date: Fri, 27 Jun 2025 17:25:44 +0200 Subject: [PATCH 2/3] feat: App API v2 (#10077) # Which Problems Are Solved This PR *partially* addresses #9450 . Specifically, it implements the resource based API for the apps. APIs for app keys ARE not part of this PR. # How the Problems Are Solved - `CreateApplication`, `PatchApplication` (update) and `RegenerateClientSecret` endpoints are now unique for all app types: API, SAML and OIDC apps. - All new endpoints have integration tests - All new endpoints are using permission checks V2 # Additional Changes - The `ListApplications` endpoint allows to do sorting (see protobuf for details) and filtering by app type (see protobuf). - SAML and OIDC update endpoint can now receive requests for partial updates # Additional Context Partially addresses #9450 --- cmd/start/start.go | 5 + internal/api/grpc/admin/export.go | 2 +- internal/api/grpc/app/v2beta/app.go | 208 +++ .../api/grpc/app/v2beta/convert/api_app.go | 60 + .../grpc/app/v2beta/convert/api_app_test.go | 149 ++ .../api/grpc/app/v2beta/convert/convert.go | 165 ++ .../grpc/app/v2beta/convert/convert_test.go | 520 ++++++ .../api/grpc/app/v2beta/convert/oidc_app.go | 291 ++++ .../grpc/app/v2beta/convert/oidc_app_test.go | 755 +++++++++ .../api/grpc/app/v2beta/convert/saml_app.go | 77 + .../grpc/app/v2beta/convert/saml_app_test.go | 256 +++ .../app/v2beta/integration_test/app_test.go | 1446 +++++++++++++++++ .../app/v2beta/integration_test/query_test.go | 575 +++++++ .../v2beta/integration_test/server_test.go | 205 +++ internal/api/grpc/app/v2beta/query.go | 37 + internal/api/grpc/app/v2beta/server.go | 57 + internal/api/grpc/filter/v2/converter.go | 23 + .../grpc/management/project_application.go | 10 +- .../project_application_converter.go | 64 +- .../eventsourcing/view/application.go | 2 +- internal/command/permission_checks.go | 8 + internal/command/project_application.go | 28 +- internal/command/project_application_api.go | 37 +- .../command/project_application_api_test.go | 21 +- internal/command/project_application_oidc.go | 71 +- .../command/project_application_oidc_model.go | 82 +- .../command/project_application_oidc_test.go | 279 ++-- internal/command/project_application_saml.go | 40 +- .../command/project_application_saml_model.go | 22 +- .../command/project_application_saml_test.go | 161 +- internal/command/project_application_test.go | 133 +- internal/command/project_converter.go | 43 +- internal/command/project_model.go | 12 +- internal/domain/application_oidc.go | 93 +- internal/domain/application_oidc_test.go | 57 +- internal/domain/application_saml.go | 13 +- internal/domain/permission.go | 3 + internal/integration/client.go | 3 + internal/project/model/oidc_config.go | 4 +- internal/query/app.go | 92 +- pkg/grpc/app/v2beta/application.go | 5 + proto/zitadel/app/v2beta/api.proto | 26 + proto/zitadel/app/v2beta/app.proto | 94 ++ proto/zitadel/app/v2beta/app_service.proto | 788 +++++++++ proto/zitadel/app/v2beta/login.proto | 18 + proto/zitadel/app/v2beta/oidc.proto | 166 ++ proto/zitadel/app/v2beta/saml.proto | 20 + proto/zitadel/management.proto | 222 +-- 48 files changed, 6845 insertions(+), 603 deletions(-) create mode 100644 internal/api/grpc/app/v2beta/app.go create mode 100644 internal/api/grpc/app/v2beta/convert/api_app.go create mode 100644 internal/api/grpc/app/v2beta/convert/api_app_test.go create mode 100644 internal/api/grpc/app/v2beta/convert/convert.go create mode 100644 internal/api/grpc/app/v2beta/convert/convert_test.go create mode 100644 internal/api/grpc/app/v2beta/convert/oidc_app.go create mode 100644 internal/api/grpc/app/v2beta/convert/oidc_app_test.go create mode 100644 internal/api/grpc/app/v2beta/convert/saml_app.go create mode 100644 internal/api/grpc/app/v2beta/convert/saml_app_test.go create mode 100644 internal/api/grpc/app/v2beta/integration_test/app_test.go create mode 100644 internal/api/grpc/app/v2beta/integration_test/query_test.go create mode 100644 internal/api/grpc/app/v2beta/integration_test/server_test.go create mode 100644 internal/api/grpc/app/v2beta/query.go create mode 100644 internal/api/grpc/app/v2beta/server.go create mode 100644 pkg/grpc/app/v2beta/application.go create mode 100644 proto/zitadel/app/v2beta/api.proto create mode 100644 proto/zitadel/app/v2beta/app.proto create mode 100644 proto/zitadel/app/v2beta/app_service.proto create mode 100644 proto/zitadel/app/v2beta/login.proto create mode 100644 proto/zitadel/app/v2beta/oidc.proto create mode 100644 proto/zitadel/app/v2beta/saml.proto diff --git a/cmd/start/start.go b/cmd/start/start.go index 3c3b5cb3e0..dbd6289041 100644 --- a/cmd/start/start.go +++ b/cmd/start/start.go @@ -36,6 +36,7 @@ import ( internal_authz "github.com/zitadel/zitadel/internal/api/authz" action_v2_beta "github.com/zitadel/zitadel/internal/api/grpc/action/v2beta" "github.com/zitadel/zitadel/internal/api/grpc/admin" + app "github.com/zitadel/zitadel/internal/api/grpc/app/v2beta" "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" @@ -509,6 +510,10 @@ func startAPIs( if err := apis.RegisterService(ctx, debug_events.CreateServer(commands, queries)); err != nil { return nil, err } + if err := apis.RegisterService(ctx, app.CreateServer(commands, queries, permissionCheck)); 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.SystemAuthZ, config.InternalAuthZ, id.SonyFlakeGenerator(), store, queries, middleware.CallDurationHandler, instanceInterceptor.Handler, assetsCache.Handler, limitingAccessInterceptor.Handle)) diff --git a/internal/api/grpc/admin/export.go b/internal/api/grpc/admin/export.go index b5d36272d4..8024cd9d6e 100644 --- a/internal/api/grpc/admin/export.go +++ b/internal/api/grpc/admin/export.go @@ -783,7 +783,7 @@ func (s *Server) getProjectsAndApps(ctx context.Context, org string) ([]*v1_pb.D if err != nil { return nil, nil, nil, nil, nil, err } - apps, err := s.query.SearchApps(ctx, &query.AppSearchQueries{Queries: []query.SearchQuery{appSearch}}, false) + apps, err := s.query.SearchApps(ctx, &query.AppSearchQueries{Queries: []query.SearchQuery{appSearch}}, nil) if err != nil { return nil, nil, nil, nil, nil, err } diff --git a/internal/api/grpc/app/v2beta/app.go b/internal/api/grpc/app/v2beta/app.go new file mode 100644 index 0000000000..48c602f454 --- /dev/null +++ b/internal/api/grpc/app/v2beta/app.go @@ -0,0 +1,208 @@ +package app + +import ( + "context" + "strings" + "time" + + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/api/grpc/app/v2beta/convert" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/zerrors" + app "github.com/zitadel/zitadel/pkg/grpc/app/v2beta" +) + +func (s *Server) CreateApplication(ctx context.Context, req *app.CreateApplicationRequest) (*app.CreateApplicationResponse, error) { + switch t := req.GetCreationRequestType().(type) { + case *app.CreateApplicationRequest_ApiRequest: + apiApp, err := s.command.AddAPIApplication(ctx, convert.CreateAPIApplicationRequestToDomain(req.GetName(), req.GetProjectId(), req.GetId(), t.ApiRequest), "") + if err != nil { + return nil, err + } + + return &app.CreateApplicationResponse{ + AppId: apiApp.AppID, + CreationDate: timestamppb.New(apiApp.ChangeDate), + CreationResponseType: &app.CreateApplicationResponse_ApiResponse{ + ApiResponse: &app.CreateAPIApplicationResponse{ + ClientId: apiApp.ClientID, + ClientSecret: apiApp.ClientSecretString, + }, + }, + }, nil + + case *app.CreateApplicationRequest_OidcRequest: + oidcAppRequest, err := convert.CreateOIDCAppRequestToDomain(req.GetName(), req.GetProjectId(), req.GetOidcRequest()) + if err != nil { + return nil, err + } + + oidcApp, err := s.command.AddOIDCApplication(ctx, oidcAppRequest, "") + if err != nil { + return nil, err + } + + return &app.CreateApplicationResponse{ + AppId: oidcApp.AppID, + CreationDate: timestamppb.New(oidcApp.ChangeDate), + CreationResponseType: &app.CreateApplicationResponse_OidcResponse{ + OidcResponse: &app.CreateOIDCApplicationResponse{ + ClientId: oidcApp.ClientID, + ClientSecret: oidcApp.ClientSecretString, + NoneCompliant: oidcApp.Compliance.NoneCompliant, + ComplianceProblems: convert.ComplianceProblemsToLocalizedMessages(oidcApp.Compliance.Problems), + }, + }, + }, nil + + case *app.CreateApplicationRequest_SamlRequest: + samlAppRequest, err := convert.CreateSAMLAppRequestToDomain(req.GetName(), req.GetProjectId(), req.GetSamlRequest()) + if err != nil { + return nil, err + } + + samlApp, err := s.command.AddSAMLApplication(ctx, samlAppRequest, "") + if err != nil { + return nil, err + } + + return &app.CreateApplicationResponse{ + AppId: samlApp.AppID, + CreationDate: timestamppb.New(samlApp.ChangeDate), + CreationResponseType: &app.CreateApplicationResponse_SamlResponse{ + SamlResponse: &app.CreateSAMLApplicationResponse{}, + }, + }, nil + default: + return nil, zerrors.ThrowInvalidArgument(nil, "APP-0iiN46", "unknown app type") + } +} + +func (s *Server) UpdateApplication(ctx context.Context, req *app.UpdateApplicationRequest) (*app.UpdateApplicationResponse, error) { + var changedTime time.Time + + if name := strings.TrimSpace(req.GetName()); name != "" { + updatedDetails, err := s.command.UpdateApplicationName( + ctx, + req.GetProjectId(), + &domain.ChangeApp{ + AppID: req.GetId(), + AppName: name, + }, + "", + ) + if err != nil { + return nil, err + } + + changedTime = updatedDetails.EventDate + } + + switch t := req.GetUpdateRequestType().(type) { + case *app.UpdateApplicationRequest_ApiConfigurationRequest: + updatedAPIApp, err := s.command.UpdateAPIApplication(ctx, convert.UpdateAPIApplicationConfigurationRequestToDomain(req.GetId(), req.GetProjectId(), t.ApiConfigurationRequest), "") + if err != nil { + return nil, err + } + + changedTime = updatedAPIApp.ChangeDate + + case *app.UpdateApplicationRequest_OidcConfigurationRequest: + oidcApp, err := convert.UpdateOIDCAppConfigRequestToDomain(req.GetId(), req.GetProjectId(), t.OidcConfigurationRequest) + if err != nil { + return nil, err + } + + updatedOIDCApp, err := s.command.UpdateOIDCApplication(ctx, oidcApp, "") + if err != nil { + return nil, err + } + + changedTime = updatedOIDCApp.ChangeDate + + case *app.UpdateApplicationRequest_SamlConfigurationRequest: + samlApp, err := convert.UpdateSAMLAppConfigRequestToDomain(req.GetId(), req.GetProjectId(), t.SamlConfigurationRequest) + if err != nil { + return nil, err + } + + updatedSAMLApp, err := s.command.UpdateSAMLApplication(ctx, samlApp, "") + if err != nil { + return nil, err + } + + changedTime = updatedSAMLApp.ChangeDate + } + + return &app.UpdateApplicationResponse{ + ChangeDate: timestamppb.New(changedTime), + }, nil +} + +func (s *Server) DeleteApplication(ctx context.Context, req *app.DeleteApplicationRequest) (*app.DeleteApplicationResponse, error) { + details, err := s.command.RemoveApplication(ctx, req.GetProjectId(), req.GetId(), "") + if err != nil { + return nil, err + } + + return &app.DeleteApplicationResponse{ + DeletionDate: timestamppb.New(details.EventDate), + }, nil +} + +func (s *Server) DeactivateApplication(ctx context.Context, req *app.DeactivateApplicationRequest) (*app.DeactivateApplicationResponse, error) { + details, err := s.command.DeactivateApplication(ctx, req.GetProjectId(), req.GetId(), "") + if err != nil { + return nil, err + } + + return &app.DeactivateApplicationResponse{ + DeactivationDate: timestamppb.New(details.EventDate), + }, nil + +} + +func (s *Server) ReactivateApplication(ctx context.Context, req *app.ReactivateApplicationRequest) (*app.ReactivateApplicationResponse, error) { + details, err := s.command.ReactivateApplication(ctx, req.GetProjectId(), req.GetId(), "") + if err != nil { + return nil, err + } + + return &app.ReactivateApplicationResponse{ + ReactivationDate: timestamppb.New(details.EventDate), + }, nil + +} + +func (s *Server) RegenerateClientSecret(ctx context.Context, req *app.RegenerateClientSecretRequest) (*app.RegenerateClientSecretResponse, error) { + var secret string + var changeDate time.Time + + switch req.GetAppType().(type) { + case *app.RegenerateClientSecretRequest_IsApi: + config, err := s.command.ChangeAPIApplicationSecret(ctx, req.GetProjectId(), req.GetApplicationId(), "") + if err != nil { + return nil, err + } + secret = config.ClientSecretString + changeDate = config.ChangeDate + + case *app.RegenerateClientSecretRequest_IsOidc: + config, err := s.command.ChangeOIDCApplicationSecret(ctx, req.GetProjectId(), req.GetApplicationId(), "") + if err != nil { + return nil, err + } + + secret = config.ClientSecretString + changeDate = config.ChangeDate + + default: + return nil, zerrors.ThrowInvalidArgument(nil, "APP-aLWIzw", "unknown app type") + } + + return &app.RegenerateClientSecretResponse{ + ClientSecret: secret, + CreationDate: timestamppb.New(changeDate), + }, nil +} diff --git a/internal/api/grpc/app/v2beta/convert/api_app.go b/internal/api/grpc/app/v2beta/convert/api_app.go new file mode 100644 index 0000000000..bad76ab0d5 --- /dev/null +++ b/internal/api/grpc/app/v2beta/convert/api_app.go @@ -0,0 +1,60 @@ +package convert + +import ( + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/eventstore/v1/models" + "github.com/zitadel/zitadel/internal/query" + app "github.com/zitadel/zitadel/pkg/grpc/app/v2beta" +) + +func CreateAPIApplicationRequestToDomain(name, projectID, appID string, app *app.CreateAPIApplicationRequest) *domain.APIApp { + return &domain.APIApp{ + ObjectRoot: models.ObjectRoot{ + AggregateID: projectID, + }, + AppName: name, + AppID: appID, + AuthMethodType: apiAuthMethodTypeToDomain(app.GetAuthMethodType()), + } +} + +func UpdateAPIApplicationConfigurationRequestToDomain(appID, projectID string, app *app.UpdateAPIApplicationConfigurationRequest) *domain.APIApp { + return &domain.APIApp{ + ObjectRoot: models.ObjectRoot{ + AggregateID: projectID, + }, + AppID: appID, + AuthMethodType: apiAuthMethodTypeToDomain(app.GetAuthMethodType()), + } +} + +func appAPIConfigToPb(apiApp *query.APIApp) app.ApplicationConfig { + return &app.Application_ApiConfig{ + ApiConfig: &app.APIConfig{ + ClientId: apiApp.ClientID, + AuthMethodType: apiAuthMethodTypeToPb(apiApp.AuthMethodType), + }, + } +} + +func apiAuthMethodTypeToDomain(authType app.APIAuthMethodType) domain.APIAuthMethodType { + switch authType { + case app.APIAuthMethodType_API_AUTH_METHOD_TYPE_BASIC: + return domain.APIAuthMethodTypeBasic + case app.APIAuthMethodType_API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT: + return domain.APIAuthMethodTypePrivateKeyJWT + default: + return domain.APIAuthMethodTypeBasic + } +} + +func apiAuthMethodTypeToPb(methodType domain.APIAuthMethodType) app.APIAuthMethodType { + switch methodType { + case domain.APIAuthMethodTypeBasic: + return app.APIAuthMethodType_API_AUTH_METHOD_TYPE_BASIC + case domain.APIAuthMethodTypePrivateKeyJWT: + return app.APIAuthMethodType_API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT + default: + return app.APIAuthMethodType_API_AUTH_METHOD_TYPE_BASIC + } +} diff --git a/internal/api/grpc/app/v2beta/convert/api_app_test.go b/internal/api/grpc/app/v2beta/convert/api_app_test.go new file mode 100644 index 0000000000..9f15c3df76 --- /dev/null +++ b/internal/api/grpc/app/v2beta/convert/api_app_test.go @@ -0,0 +1,149 @@ +package convert + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/eventstore/v1/models" + app "github.com/zitadel/zitadel/pkg/grpc/app/v2beta" +) + +func TestCreateAPIApplicationRequestToDomain(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + appName string + projectID string + appID string + req *app.CreateAPIApplicationRequest + want *domain.APIApp + }{ + { + name: "basic auth method", + appName: "my-app", + projectID: "proj-1", + appID: "someID", + req: &app.CreateAPIApplicationRequest{ + AuthMethodType: app.APIAuthMethodType_API_AUTH_METHOD_TYPE_BASIC, + }, + want: &domain.APIApp{ + ObjectRoot: models.ObjectRoot{AggregateID: "proj-1"}, + AppName: "my-app", + AuthMethodType: domain.APIAuthMethodTypeBasic, + AppID: "someID", + }, + }, + { + name: "private key jwt", + appName: "jwt-app", + projectID: "proj-2", + req: &app.CreateAPIApplicationRequest{ + AuthMethodType: app.APIAuthMethodType_API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT, + }, + want: &domain.APIApp{ + ObjectRoot: models.ObjectRoot{AggregateID: "proj-2"}, + AppName: "jwt-app", + AuthMethodType: domain.APIAuthMethodTypePrivateKeyJWT, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + // When + got := CreateAPIApplicationRequestToDomain(tt.appName, tt.projectID, tt.appID, tt.req) + + // Then + assert.Equal(t, tt.want, got) + }) + } +} + +func TestUpdateAPIApplicationConfigurationRequestToDomain(t *testing.T) { + t.Parallel() + tests := []struct { + name string + appID string + projectID string + req *app.UpdateAPIApplicationConfigurationRequest + want *domain.APIApp + }{ + { + name: "basic auth method", + appID: "app-1", + projectID: "proj-1", + req: &app.UpdateAPIApplicationConfigurationRequest{ + AuthMethodType: app.APIAuthMethodType_API_AUTH_METHOD_TYPE_BASIC, + }, + want: &domain.APIApp{ + ObjectRoot: models.ObjectRoot{AggregateID: "proj-1"}, + AppID: "app-1", + AuthMethodType: domain.APIAuthMethodTypeBasic, + }, + }, + { + name: "private key jwt", + appID: "app-2", + projectID: "proj-2", + req: &app.UpdateAPIApplicationConfigurationRequest{ + AuthMethodType: app.APIAuthMethodType_API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT, + }, + want: &domain.APIApp{ + ObjectRoot: models.ObjectRoot{AggregateID: "proj-2"}, + AppID: "app-2", + AuthMethodType: domain.APIAuthMethodTypePrivateKeyJWT, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + // When + got := UpdateAPIApplicationConfigurationRequestToDomain(tt.appID, tt.projectID, tt.req) + + // Then + assert.Equal(t, tt.want, got) + }) + } +} + +func Test_apiAuthMethodTypeToPb(t *testing.T) { + t.Parallel() + + tt := []struct { + name string + methodType domain.APIAuthMethodType + expectedResult app.APIAuthMethodType + }{ + { + name: "basic auth method", + methodType: domain.APIAuthMethodTypeBasic, + expectedResult: app.APIAuthMethodType_API_AUTH_METHOD_TYPE_BASIC, + }, + { + name: "private key jwt", + methodType: domain.APIAuthMethodTypePrivateKeyJWT, + expectedResult: app.APIAuthMethodType_API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT, + }, + { + name: "unknown auth method defaults to basic", + expectedResult: app.APIAuthMethodType_API_AUTH_METHOD_TYPE_BASIC, + }, + } + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + // When + res := apiAuthMethodTypeToPb(tc.methodType) + + // Then + assert.Equal(t, tc.expectedResult, res) + }) + } +} diff --git a/internal/api/grpc/app/v2beta/convert/convert.go b/internal/api/grpc/app/v2beta/convert/convert.go new file mode 100644 index 0000000000..c732b3a0c5 --- /dev/null +++ b/internal/api/grpc/app/v2beta/convert/convert.go @@ -0,0 +1,165 @@ +package convert + +import ( + "net/url" + + "github.com/muhlemmer/gu" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/api/grpc/filter/v2" + "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" + app "github.com/zitadel/zitadel/pkg/grpc/app/v2beta" +) + +func AppToPb(query_app *query.App) *app.Application { + if query_app == nil { + return &app.Application{} + } + + return &app.Application{ + Id: query_app.ID, + CreationDate: timestamppb.New(query_app.CreationDate), + ChangeDate: timestamppb.New(query_app.ChangeDate), + State: appStateToPb(query_app.State), + Name: query_app.Name, + Config: appConfigToPb(query_app), + } +} + +func AppsToPb(queryApps []*query.App) []*app.Application { + pbApps := make([]*app.Application, len(queryApps)) + + for i, queryApp := range queryApps { + pbApps[i] = AppToPb(queryApp) + } + + return pbApps +} + +func ListApplicationsRequestToModel(sysDefaults systemdefaults.SystemDefaults, req *app.ListApplicationsRequest) (*query.AppSearchQueries, error) { + offset, limit, asc, err := filter.PaginationPbToQuery(sysDefaults, req.GetPagination()) + if err != nil { + return nil, err + } + + queries, err := appQueriesToModel(req.GetFilters()) + if err != nil { + return nil, err + } + projectQuery, err := query.NewAppProjectIDSearchQuery(req.GetProjectId()) + if err != nil { + return nil, err + } + + queries = append(queries, projectQuery) + return &query.AppSearchQueries{ + SearchRequest: query.SearchRequest{ + Offset: offset, + Limit: limit, + Asc: asc, + SortingColumn: appSortingToColumn(req.GetSortingColumn()), + }, + + Queries: queries, + }, nil +} + +func appSortingToColumn(sortingCriteria app.AppSorting) query.Column { + switch sortingCriteria { + case app.AppSorting_APP_SORT_BY_CHANGE_DATE: + return query.AppColumnChangeDate + case app.AppSorting_APP_SORT_BY_CREATION_DATE: + return query.AppColumnCreationDate + case app.AppSorting_APP_SORT_BY_NAME: + return query.AppColumnName + case app.AppSorting_APP_SORT_BY_STATE: + return query.AppColumnState + case app.AppSorting_APP_SORT_BY_ID: + fallthrough + default: + return query.AppColumnID + } +} + +func appStateToPb(state domain.AppState) app.AppState { + switch state { + case domain.AppStateActive: + return app.AppState_APP_STATE_ACTIVE + case domain.AppStateInactive: + return app.AppState_APP_STATE_INACTIVE + case domain.AppStateRemoved: + return app.AppState_APP_STATE_REMOVED + case domain.AppStateUnspecified: + fallthrough + default: + return app.AppState_APP_STATE_UNSPECIFIED + } +} + +func appConfigToPb(app *query.App) app.ApplicationConfig { + if app.OIDCConfig != nil { + return appOIDCConfigToPb(app.OIDCConfig) + } + if app.SAMLConfig != nil { + return appSAMLConfigToPb(app.SAMLConfig) + } + return appAPIConfigToPb(app.APIConfig) +} + +func loginVersionToDomain(version *app.LoginVersion) (*domain.LoginVersion, *string, error) { + switch v := version.GetVersion().(type) { + case nil: + return gu.Ptr(domain.LoginVersionUnspecified), gu.Ptr(""), nil + case *app.LoginVersion_LoginV1: + return gu.Ptr(domain.LoginVersion1), gu.Ptr(""), nil + case *app.LoginVersion_LoginV2: + _, err := url.Parse(v.LoginV2.GetBaseUri()) + return gu.Ptr(domain.LoginVersion2), gu.Ptr(v.LoginV2.GetBaseUri()), err + default: + return gu.Ptr(domain.LoginVersionUnspecified), gu.Ptr(""), nil + } +} + +func loginVersionToPb(version domain.LoginVersion, baseURI *string) *app.LoginVersion { + switch version { + case domain.LoginVersionUnspecified: + return nil + case domain.LoginVersion1: + return &app.LoginVersion{Version: &app.LoginVersion_LoginV1{LoginV1: &app.LoginV1{}}} + case domain.LoginVersion2: + return &app.LoginVersion{Version: &app.LoginVersion_LoginV2{LoginV2: &app.LoginV2{BaseUri: baseURI}}} + default: + return nil + } +} + +func appQueriesToModel(queries []*app.ApplicationSearchFilter) (toReturn []query.SearchQuery, err error) { + toReturn = make([]query.SearchQuery, len(queries)) + for i, query := range queries { + toReturn[i], err = appQueryToModel(query) + if err != nil { + return nil, err + } + } + return toReturn, nil +} + +func appQueryToModel(appQuery *app.ApplicationSearchFilter) (query.SearchQuery, error) { + switch q := appQuery.GetFilter().(type) { + case *app.ApplicationSearchFilter_NameFilter: + return query.NewAppNameSearchQuery(filter.TextMethodPbToQuery(q.NameFilter.GetMethod()), q.NameFilter.Name) + case *app.ApplicationSearchFilter_StateFilter: + return query.NewAppStateSearchQuery(domain.AppState(q.StateFilter)) + case *app.ApplicationSearchFilter_ApiAppOnly: + return query.NewNotNullQuery(query.AppAPIConfigColumnAppID) + case *app.ApplicationSearchFilter_OidcAppOnly: + return query.NewNotNullQuery(query.AppOIDCConfigColumnAppID) + case *app.ApplicationSearchFilter_SamlAppOnly: + return query.NewNotNullQuery(query.AppSAMLConfigColumnAppID) + default: + return nil, zerrors.ThrowInvalidArgument(nil, "CONV-z2mAGy", "List.Query.Invalid") + } +} diff --git a/internal/api/grpc/app/v2beta/convert/convert_test.go b/internal/api/grpc/app/v2beta/convert/convert_test.go new file mode 100644 index 0000000000..5835691d43 --- /dev/null +++ b/internal/api/grpc/app/v2beta/convert/convert_test.go @@ -0,0 +1,520 @@ +package convert + +import ( + "errors" + "fmt" + "net/url" + "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" + + filter "github.com/zitadel/zitadel/internal/api/grpc/filter/v2beta" + "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" + app "github.com/zitadel/zitadel/pkg/grpc/app/v2beta" + filter_pb_v2 "github.com/zitadel/zitadel/pkg/grpc/filter/v2" + filter_pb_v2_beta "github.com/zitadel/zitadel/pkg/grpc/filter/v2beta" +) + +func TestAppToPb(t *testing.T) { + t.Parallel() + + now := time.Now() + + tt := []struct { + testName string + inputQueryApp *query.App + expectedPbApp *app.Application + }{ + { + testName: "full app conversion", + inputQueryApp: &query.App{ + ID: "id", + CreationDate: now, + ChangeDate: now, + State: domain.AppStateActive, + Name: "test-app", + APIConfig: &query.APIApp{}, + }, + expectedPbApp: &app.Application{ + Id: "id", + CreationDate: timestamppb.New(now), + ChangeDate: timestamppb.New(now), + State: app.AppState_APP_STATE_ACTIVE, + Name: "test-app", + Config: &app.Application_ApiConfig{ + ApiConfig: &app.APIConfig{}, + }, + }, + }, + { + testName: "nil app", + inputQueryApp: nil, + expectedPbApp: &app.Application{}, + }, + } + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + t.Parallel() + + // When + res := AppToPb(tc.inputQueryApp) + + // Then + assert.Equal(t, tc.expectedPbApp, res) + }) + } +} + +func TestListApplicationsRequestToModel(t *testing.T) { + t.Parallel() + + validSearchByNameQuery, err := query.NewAppNameSearchQuery(filter.TextMethodPbToQuery(filter_pb_v2_beta.TextFilterMethod_TEXT_FILTER_METHOD_EQUALS), "test") + require.NoError(t, err) + + validSearchByProjectQuery, err := query.NewAppProjectIDSearchQuery("project1") + require.NoError(t, err) + + sysDefaults := systemdefaults.SystemDefaults{DefaultQueryLimit: 100, MaxQueryLimit: 150} + + tt := []struct { + testName string + req *app.ListApplicationsRequest + + expectedResponse *query.AppSearchQueries + expectedError error + }{ + { + testName: "invalid pagination limit", + req: &app.ListApplicationsRequest{ + Pagination: &filter_pb_v2.PaginationRequest{Asc: true, Limit: uint32(sysDefaults.MaxQueryLimit + 1)}, + }, + expectedResponse: nil, + expectedError: zerrors.ThrowInvalidArgumentf(fmt.Errorf("given: %d, allowed: %d", sysDefaults.MaxQueryLimit+1, sysDefaults.MaxQueryLimit), "QUERY-4M0fs", "Errors.Query.LimitExceeded"), + }, + { + testName: "empty request", + req: &app.ListApplicationsRequest{ + ProjectId: "project1", + Pagination: &filter_pb_v2.PaginationRequest{Asc: true}, + }, + expectedResponse: &query.AppSearchQueries{ + SearchRequest: query.SearchRequest{ + Offset: 0, + Limit: 100, + Asc: true, + SortingColumn: query.AppColumnID, + }, + Queries: []query.SearchQuery{ + validSearchByProjectQuery, + }, + }, + }, + { + testName: "valid request", + req: &app.ListApplicationsRequest{ + ProjectId: "project1", + Filters: []*app.ApplicationSearchFilter{ + { + Filter: &app.ApplicationSearchFilter_NameFilter{NameFilter: &app.ApplicationNameQuery{Name: "test"}}, + }, + }, + SortingColumn: app.AppSorting_APP_SORT_BY_NAME, + Pagination: &filter_pb_v2.PaginationRequest{Asc: true}, + }, + + expectedResponse: &query.AppSearchQueries{ + SearchRequest: query.SearchRequest{ + Offset: 0, + Limit: 100, + Asc: true, + SortingColumn: query.AppColumnName, + }, + Queries: []query.SearchQuery{ + validSearchByNameQuery, + validSearchByProjectQuery, + }, + }, + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + t.Parallel() + + // When + got, err := ListApplicationsRequestToModel(sysDefaults, tc.req) + + // Then + assert.Equal(t, tc.expectedError, err) + assert.Equal(t, tc.expectedResponse, got) + }) + } +} + +func TestAppSortingToColumn(t *testing.T) { + t.Parallel() + + tt := []struct { + name string + sorting app.AppSorting + expected query.Column + }{ + { + name: "sort by change date", + sorting: app.AppSorting_APP_SORT_BY_CHANGE_DATE, + expected: query.AppColumnChangeDate, + }, + { + name: "sort by creation date", + sorting: app.AppSorting_APP_SORT_BY_CREATION_DATE, + expected: query.AppColumnCreationDate, + }, + { + name: "sort by name", + sorting: app.AppSorting_APP_SORT_BY_NAME, + expected: query.AppColumnName, + }, + { + name: "sort by state", + sorting: app.AppSorting_APP_SORT_BY_STATE, + expected: query.AppColumnState, + }, + { + name: "sort by ID", + sorting: app.AppSorting_APP_SORT_BY_ID, + expected: query.AppColumnID, + }, + { + name: "unknown sorting defaults to ID", + sorting: app.AppSorting(99), + expected: query.AppColumnID, + }, + } + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + // When + result := appSortingToColumn(tc.sorting) + + // Then + assert.Equal(t, tc.expected, result) + }) + } +} + +func TestAppStateToPb(t *testing.T) { + t.Parallel() + + tt := []struct { + name string + state domain.AppState + expected app.AppState + }{ + { + name: "active state", + state: domain.AppStateActive, + expected: app.AppState_APP_STATE_ACTIVE, + }, + { + name: "inactive state", + state: domain.AppStateInactive, + expected: app.AppState_APP_STATE_INACTIVE, + }, + { + name: "removed state", + state: domain.AppStateRemoved, + expected: app.AppState_APP_STATE_REMOVED, + }, + { + name: "unspecified state", + state: domain.AppStateUnspecified, + expected: app.AppState_APP_STATE_UNSPECIFIED, + }, + { + name: "unknown state defaults to unspecified", + state: domain.AppState(99), + expected: app.AppState_APP_STATE_UNSPECIFIED, + }, + } + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + // When + result := appStateToPb(tc.state) + + // Then + assert.Equal(t, tc.expected, result) + }) + } +} + +func TestAppConfigToPb(t *testing.T) { + t.Parallel() + + tt := []struct { + name string + app *query.App + expected app.ApplicationConfig + }{ + { + name: "OIDC config", + app: &query.App{ + OIDCConfig: &query.OIDCApp{}, + }, + expected: &app.Application_OidcConfig{ + OidcConfig: &app.OIDCConfig{ + ResponseTypes: []app.OIDCResponseType{}, + GrantTypes: []app.OIDCGrantType{}, + ComplianceProblems: []*app.OIDCLocalizedMessage{}, + ClockSkew: &durationpb.Duration{}, + }, + }, + }, + { + name: "SAML config", + app: &query.App{ + SAMLConfig: &query.SAMLApp{}, + }, + expected: &app.Application_SamlConfig{ + SamlConfig: &app.SAMLConfig{ + Metadata: &app.SAMLConfig_MetadataXml{}, + }, + }, + }, + { + name: "API config", + app: &query.App{ + APIConfig: &query.APIApp{}, + }, + expected: &app.Application_ApiConfig{ + ApiConfig: &app.APIConfig{}, + }, + }, + } + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + // When + result := appConfigToPb(tc.app) + + // Then + assert.Equal(t, tc.expected, result) + }) + } +} + +func TestLoginVersionToDomain(t *testing.T) { + t.Parallel() + + tt := []struct { + name string + version *app.LoginVersion + expectedVer *domain.LoginVersion + expectedURI *string + expectedError error + }{ + { + name: "nil version", + version: nil, + expectedVer: gu.Ptr(domain.LoginVersionUnspecified), + expectedURI: gu.Ptr(""), + }, + { + name: "login v1", + version: &app.LoginVersion{Version: &app.LoginVersion_LoginV1{LoginV1: &app.LoginV1{}}}, + expectedVer: gu.Ptr(domain.LoginVersion1), + expectedURI: gu.Ptr(""), + }, + { + name: "login v2 valid URI", + version: &app.LoginVersion{Version: &app.LoginVersion_LoginV2{LoginV2: &app.LoginV2{BaseUri: gu.Ptr("https://valid.url")}}}, + expectedVer: gu.Ptr(domain.LoginVersion2), + expectedURI: gu.Ptr("https://valid.url"), + }, + { + name: "login v2 invalid URI", + version: &app.LoginVersion{Version: &app.LoginVersion_LoginV2{LoginV2: &app.LoginV2{BaseUri: gu.Ptr("://invalid")}}}, + expectedVer: gu.Ptr(domain.LoginVersion2), + expectedURI: gu.Ptr("://invalid"), + expectedError: &url.Error{Op: "parse", URL: "://invalid", Err: errors.New("missing protocol scheme")}, + }, + { + name: "unknown version type", + version: &app.LoginVersion{}, + expectedVer: gu.Ptr(domain.LoginVersionUnspecified), + expectedURI: gu.Ptr(""), + }, + } + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + // When + version, uri, err := loginVersionToDomain(tc.version) + + // Then + assert.Equal(t, tc.expectedVer, version) + assert.Equal(t, tc.expectedURI, uri) + assert.Equal(t, tc.expectedError, err) + }) + } +} + +func TestLoginVersionToPb(t *testing.T) { + t.Parallel() + + tt := []struct { + name string + version domain.LoginVersion + baseURI *string + expected *app.LoginVersion + }{ + { + name: "unspecified version", + version: domain.LoginVersionUnspecified, + baseURI: nil, + expected: nil, + }, + { + name: "login v1", + version: domain.LoginVersion1, + baseURI: nil, + expected: &app.LoginVersion{ + Version: &app.LoginVersion_LoginV1{ + LoginV1: &app.LoginV1{}, + }, + }, + }, + { + name: "login v2", + version: domain.LoginVersion2, + baseURI: gu.Ptr("https://example.com"), + expected: &app.LoginVersion{ + Version: &app.LoginVersion_LoginV2{ + LoginV2: &app.LoginV2{ + BaseUri: gu.Ptr("https://example.com"), + }, + }, + }, + }, + { + name: "unknown version", + version: domain.LoginVersion(99), + baseURI: nil, + expected: nil, + }, + } + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + // When + result := loginVersionToPb(tc.version, tc.baseURI) + + // Then + assert.Equal(t, tc.expected, result) + }) + } +} + +func TestAppQueryToModel(t *testing.T) { + t.Parallel() + + validAppNameSearchQuery, err := query.NewAppNameSearchQuery(query.TextEquals, "test") + require.NoError(t, err) + + validAppStateSearchQuery, err := query.NewAppStateSearchQuery(domain.AppStateActive) + require.NoError(t, err) + + tt := []struct { + name string + query *app.ApplicationSearchFilter + + expectedQuery query.SearchQuery + expectedError error + }{ + { + name: "name query", + query: &app.ApplicationSearchFilter{ + Filter: &app.ApplicationSearchFilter_NameFilter{ + NameFilter: &app.ApplicationNameQuery{ + Name: "test", + Method: filter_pb_v2.TextFilterMethod_TEXT_FILTER_METHOD_EQUALS, + }, + }, + }, + expectedQuery: validAppNameSearchQuery, + }, + { + name: "state query", + query: &app.ApplicationSearchFilter{ + Filter: &app.ApplicationSearchFilter_StateFilter{ + StateFilter: app.AppState_APP_STATE_ACTIVE, + }, + }, + expectedQuery: validAppStateSearchQuery, + }, + { + name: "api app only query", + query: &app.ApplicationSearchFilter{ + Filter: &app.ApplicationSearchFilter_ApiAppOnly{}, + }, + expectedQuery: &query.NotNullQuery{ + Column: query.AppAPIConfigColumnAppID, + }, + }, + { + name: "oidc app only query", + query: &app.ApplicationSearchFilter{ + Filter: &app.ApplicationSearchFilter_OidcAppOnly{}, + }, + expectedQuery: &query.NotNullQuery{ + Column: query.AppOIDCConfigColumnAppID, + }, + }, + { + name: "saml app only query", + query: &app.ApplicationSearchFilter{ + Filter: &app.ApplicationSearchFilter_SamlAppOnly{}, + }, + expectedQuery: &query.NotNullQuery{ + Column: query.AppSAMLConfigColumnAppID, + }, + }, + { + name: "invalid query type", + query: &app.ApplicationSearchFilter{}, + expectedQuery: nil, + expectedError: zerrors.ThrowInvalidArgument(nil, "CONV-z2mAGy", "List.Query.Invalid"), + }, + } + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + // When + result, err := appQueryToModel(tc.query) + + // Then + assert.Equal(t, tc.expectedError, err) + assert.Equal(t, tc.expectedQuery, result) + }) + } +} diff --git a/internal/api/grpc/app/v2beta/convert/oidc_app.go b/internal/api/grpc/app/v2beta/convert/oidc_app.go new file mode 100644 index 0000000000..223e43d166 --- /dev/null +++ b/internal/api/grpc/app/v2beta/convert/oidc_app.go @@ -0,0 +1,291 @@ +package convert + +import ( + "github.com/muhlemmer/gu" + "google.golang.org/protobuf/types/known/durationpb" + + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/eventstore/v1/models" + "github.com/zitadel/zitadel/internal/query" + app "github.com/zitadel/zitadel/pkg/grpc/app/v2beta" +) + +func CreateOIDCAppRequestToDomain(name, projectID string, req *app.CreateOIDCApplicationRequest) (*domain.OIDCApp, error) { + loginVersion, loginBaseURI, err := loginVersionToDomain(req.GetLoginVersion()) + if err != nil { + return nil, err + } + return &domain.OIDCApp{ + ObjectRoot: models.ObjectRoot{ + AggregateID: projectID, + }, + AppName: name, + OIDCVersion: gu.Ptr(domain.OIDCVersionV1), + RedirectUris: req.GetRedirectUris(), + ResponseTypes: oidcResponseTypesToDomain(req.GetResponseTypes()), + GrantTypes: oidcGrantTypesToDomain(req.GetGrantTypes()), + ApplicationType: gu.Ptr(oidcApplicationTypeToDomain(req.GetAppType())), + AuthMethodType: gu.Ptr(oidcAuthMethodTypeToDomain(req.GetAuthMethodType())), + PostLogoutRedirectUris: req.GetPostLogoutRedirectUris(), + DevMode: &req.DevMode, + AccessTokenType: gu.Ptr(oidcTokenTypeToDomain(req.GetAccessTokenType())), + AccessTokenRoleAssertion: gu.Ptr(req.GetAccessTokenRoleAssertion()), + IDTokenRoleAssertion: gu.Ptr(req.GetIdTokenRoleAssertion()), + IDTokenUserinfoAssertion: gu.Ptr(req.GetIdTokenUserinfoAssertion()), + ClockSkew: gu.Ptr(req.GetClockSkew().AsDuration()), + AdditionalOrigins: req.GetAdditionalOrigins(), + SkipNativeAppSuccessPage: gu.Ptr(req.GetSkipNativeAppSuccessPage()), + BackChannelLogoutURI: gu.Ptr(req.GetBackChannelLogoutUri()), + LoginVersion: loginVersion, + LoginBaseURI: loginBaseURI, + }, nil +} + +func UpdateOIDCAppConfigRequestToDomain(appID, projectID string, app *app.UpdateOIDCApplicationConfigurationRequest) (*domain.OIDCApp, error) { + loginVersion, loginBaseURI, err := loginVersionToDomain(app.GetLoginVersion()) + if err != nil { + return nil, err + } + return &domain.OIDCApp{ + ObjectRoot: models.ObjectRoot{ + AggregateID: projectID, + }, + AppID: appID, + RedirectUris: app.RedirectUris, + ResponseTypes: oidcResponseTypesToDomain(app.ResponseTypes), + GrantTypes: oidcGrantTypesToDomain(app.GrantTypes), + ApplicationType: oidcApplicationTypeToDomainPtr(app.AppType), + AuthMethodType: oidcAuthMethodTypeToDomainPtr(app.AuthMethodType), + PostLogoutRedirectUris: app.PostLogoutRedirectUris, + DevMode: app.DevMode, + AccessTokenType: oidcTokenTypeToDomainPtr(app.AccessTokenType), + AccessTokenRoleAssertion: app.AccessTokenRoleAssertion, + IDTokenRoleAssertion: app.IdTokenRoleAssertion, + IDTokenUserinfoAssertion: app.IdTokenUserinfoAssertion, + ClockSkew: gu.Ptr(app.GetClockSkew().AsDuration()), + AdditionalOrigins: app.AdditionalOrigins, + SkipNativeAppSuccessPage: app.SkipNativeAppSuccessPage, + BackChannelLogoutURI: app.BackChannelLogoutUri, + LoginVersion: loginVersion, + LoginBaseURI: loginBaseURI, + }, nil +} + +func oidcResponseTypesToDomain(responseTypes []app.OIDCResponseType) []domain.OIDCResponseType { + if len(responseTypes) == 0 { + return []domain.OIDCResponseType{domain.OIDCResponseTypeCode} + } + oidcResponseTypes := make([]domain.OIDCResponseType, len(responseTypes)) + for i, responseType := range responseTypes { + switch responseType { + case app.OIDCResponseType_OIDC_RESPONSE_TYPE_UNSPECIFIED: + oidcResponseTypes[i] = domain.OIDCResponseTypeUnspecified + case app.OIDCResponseType_OIDC_RESPONSE_TYPE_CODE: + oidcResponseTypes[i] = domain.OIDCResponseTypeCode + case app.OIDCResponseType_OIDC_RESPONSE_TYPE_ID_TOKEN: + oidcResponseTypes[i] = domain.OIDCResponseTypeIDToken + case app.OIDCResponseType_OIDC_RESPONSE_TYPE_ID_TOKEN_TOKEN: + oidcResponseTypes[i] = domain.OIDCResponseTypeIDTokenToken + } + } + return oidcResponseTypes +} + +func oidcGrantTypesToDomain(grantTypes []app.OIDCGrantType) []domain.OIDCGrantType { + if len(grantTypes) == 0 { + return []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode} + } + oidcGrantTypes := make([]domain.OIDCGrantType, len(grantTypes)) + for i, grantType := range grantTypes { + switch grantType { + case app.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE: + oidcGrantTypes[i] = domain.OIDCGrantTypeAuthorizationCode + case app.OIDCGrantType_OIDC_GRANT_TYPE_IMPLICIT: + oidcGrantTypes[i] = domain.OIDCGrantTypeImplicit + case app.OIDCGrantType_OIDC_GRANT_TYPE_REFRESH_TOKEN: + oidcGrantTypes[i] = domain.OIDCGrantTypeRefreshToken + case app.OIDCGrantType_OIDC_GRANT_TYPE_DEVICE_CODE: + oidcGrantTypes[i] = domain.OIDCGrantTypeDeviceCode + case app.OIDCGrantType_OIDC_GRANT_TYPE_TOKEN_EXCHANGE: + oidcGrantTypes[i] = domain.OIDCGrantTypeTokenExchange + } + } + return oidcGrantTypes +} + +func oidcApplicationTypeToDomainPtr(appType *app.OIDCAppType) *domain.OIDCApplicationType { + if appType == nil { + return nil + } + + res := oidcApplicationTypeToDomain(*appType) + return &res +} + +func oidcApplicationTypeToDomain(appType app.OIDCAppType) domain.OIDCApplicationType { + switch appType { + case app.OIDCAppType_OIDC_APP_TYPE_WEB: + return domain.OIDCApplicationTypeWeb + case app.OIDCAppType_OIDC_APP_TYPE_USER_AGENT: + return domain.OIDCApplicationTypeUserAgent + case app.OIDCAppType_OIDC_APP_TYPE_NATIVE: + return domain.OIDCApplicationTypeNative + } + return domain.OIDCApplicationTypeWeb +} + +func oidcAuthMethodTypeToDomainPtr(authType *app.OIDCAuthMethodType) *domain.OIDCAuthMethodType { + if authType == nil { + return nil + } + + res := oidcAuthMethodTypeToDomain(*authType) + return &res +} + +func oidcAuthMethodTypeToDomain(authType app.OIDCAuthMethodType) domain.OIDCAuthMethodType { + switch authType { + case app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_BASIC: + return domain.OIDCAuthMethodTypeBasic + case app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_POST: + return domain.OIDCAuthMethodTypePost + case app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_NONE: + return domain.OIDCAuthMethodTypeNone + case app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT: + return domain.OIDCAuthMethodTypePrivateKeyJWT + default: + return domain.OIDCAuthMethodTypeBasic + } +} + +func oidcTokenTypeToDomainPtr(tokenType *app.OIDCTokenType) *domain.OIDCTokenType { + if tokenType == nil { + return nil + } + + res := oidcTokenTypeToDomain(*tokenType) + return &res +} + +func oidcTokenTypeToDomain(tokenType app.OIDCTokenType) domain.OIDCTokenType { + switch tokenType { + case app.OIDCTokenType_OIDC_TOKEN_TYPE_BEARER: + return domain.OIDCTokenTypeBearer + case app.OIDCTokenType_OIDC_TOKEN_TYPE_JWT: + return domain.OIDCTokenTypeJWT + default: + return domain.OIDCTokenTypeBearer + } +} + +func ComplianceProblemsToLocalizedMessages(complianceProblems []string) []*app.OIDCLocalizedMessage { + converted := make([]*app.OIDCLocalizedMessage, len(complianceProblems)) + for i, p := range complianceProblems { + converted[i] = &app.OIDCLocalizedMessage{Key: p} + } + + return converted +} + +func appOIDCConfigToPb(oidcApp *query.OIDCApp) *app.Application_OidcConfig { + return &app.Application_OidcConfig{ + OidcConfig: &app.OIDCConfig{ + RedirectUris: oidcApp.RedirectURIs, + ResponseTypes: oidcResponseTypesFromModel(oidcApp.ResponseTypes), + GrantTypes: oidcGrantTypesFromModel(oidcApp.GrantTypes), + AppType: oidcApplicationTypeToPb(oidcApp.AppType), + ClientId: oidcApp.ClientID, + AuthMethodType: oidcAuthMethodTypeToPb(oidcApp.AuthMethodType), + PostLogoutRedirectUris: oidcApp.PostLogoutRedirectURIs, + Version: app.OIDCVersion_OIDC_VERSION_1_0, + NoneCompliant: len(oidcApp.ComplianceProblems) != 0, + ComplianceProblems: ComplianceProblemsToLocalizedMessages(oidcApp.ComplianceProblems), + DevMode: oidcApp.IsDevMode, + AccessTokenType: oidcTokenTypeToPb(oidcApp.AccessTokenType), + AccessTokenRoleAssertion: oidcApp.AssertAccessTokenRole, + IdTokenRoleAssertion: oidcApp.AssertIDTokenRole, + IdTokenUserinfoAssertion: oidcApp.AssertIDTokenUserinfo, + ClockSkew: durationpb.New(oidcApp.ClockSkew), + AdditionalOrigins: oidcApp.AdditionalOrigins, + AllowedOrigins: oidcApp.AllowedOrigins, + SkipNativeAppSuccessPage: oidcApp.SkipNativeAppSuccessPage, + BackChannelLogoutUri: oidcApp.BackChannelLogoutURI, + LoginVersion: loginVersionToPb(oidcApp.LoginVersion, oidcApp.LoginBaseURI), + }, + } +} + +func oidcResponseTypesFromModel(responseTypes []domain.OIDCResponseType) []app.OIDCResponseType { + oidcResponseTypes := make([]app.OIDCResponseType, len(responseTypes)) + for i, responseType := range responseTypes { + switch responseType { + case domain.OIDCResponseTypeUnspecified: + oidcResponseTypes[i] = app.OIDCResponseType_OIDC_RESPONSE_TYPE_UNSPECIFIED + case domain.OIDCResponseTypeCode: + oidcResponseTypes[i] = app.OIDCResponseType_OIDC_RESPONSE_TYPE_CODE + case domain.OIDCResponseTypeIDToken: + oidcResponseTypes[i] = app.OIDCResponseType_OIDC_RESPONSE_TYPE_ID_TOKEN + case domain.OIDCResponseTypeIDTokenToken: + oidcResponseTypes[i] = app.OIDCResponseType_OIDC_RESPONSE_TYPE_ID_TOKEN_TOKEN + } + } + return oidcResponseTypes +} + +func oidcGrantTypesFromModel(grantTypes []domain.OIDCGrantType) []app.OIDCGrantType { + oidcGrantTypes := make([]app.OIDCGrantType, len(grantTypes)) + for i, grantType := range grantTypes { + switch grantType { + case domain.OIDCGrantTypeAuthorizationCode: + oidcGrantTypes[i] = app.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE + case domain.OIDCGrantTypeImplicit: + oidcGrantTypes[i] = app.OIDCGrantType_OIDC_GRANT_TYPE_IMPLICIT + case domain.OIDCGrantTypeRefreshToken: + oidcGrantTypes[i] = app.OIDCGrantType_OIDC_GRANT_TYPE_REFRESH_TOKEN + case domain.OIDCGrantTypeDeviceCode: + oidcGrantTypes[i] = app.OIDCGrantType_OIDC_GRANT_TYPE_DEVICE_CODE + case domain.OIDCGrantTypeTokenExchange: + oidcGrantTypes[i] = app.OIDCGrantType_OIDC_GRANT_TYPE_TOKEN_EXCHANGE + } + } + return oidcGrantTypes +} + +func oidcApplicationTypeToPb(appType domain.OIDCApplicationType) app.OIDCAppType { + switch appType { + case domain.OIDCApplicationTypeWeb: + return app.OIDCAppType_OIDC_APP_TYPE_WEB + case domain.OIDCApplicationTypeUserAgent: + return app.OIDCAppType_OIDC_APP_TYPE_USER_AGENT + case domain.OIDCApplicationTypeNative: + return app.OIDCAppType_OIDC_APP_TYPE_NATIVE + default: + return app.OIDCAppType_OIDC_APP_TYPE_WEB + } +} + +func oidcAuthMethodTypeToPb(authType domain.OIDCAuthMethodType) app.OIDCAuthMethodType { + switch authType { + case domain.OIDCAuthMethodTypeBasic: + return app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_BASIC + case domain.OIDCAuthMethodTypePost: + return app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_POST + case domain.OIDCAuthMethodTypeNone: + return app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_NONE + case domain.OIDCAuthMethodTypePrivateKeyJWT: + return app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT + default: + return app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_BASIC + } +} + +func oidcTokenTypeToPb(tokenType domain.OIDCTokenType) app.OIDCTokenType { + switch tokenType { + case domain.OIDCTokenTypeBearer: + return app.OIDCTokenType_OIDC_TOKEN_TYPE_BEARER + case domain.OIDCTokenTypeJWT: + return app.OIDCTokenType_OIDC_TOKEN_TYPE_JWT + default: + return app.OIDCTokenType_OIDC_TOKEN_TYPE_BEARER + } +} diff --git a/internal/api/grpc/app/v2beta/convert/oidc_app_test.go b/internal/api/grpc/app/v2beta/convert/oidc_app_test.go new file mode 100644 index 0000000000..a6b3f0b709 --- /dev/null +++ b/internal/api/grpc/app/v2beta/convert/oidc_app_test.go @@ -0,0 +1,755 @@ +package convert + +import ( + "net/url" + "testing" + "time" + + "github.com/muhlemmer/gu" + "github.com/stretchr/testify/assert" + "google.golang.org/protobuf/types/known/durationpb" + + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/eventstore/v1/models" + "github.com/zitadel/zitadel/internal/query" + app "github.com/zitadel/zitadel/pkg/grpc/app/v2beta" +) + +func TestCreateOIDCAppRequestToDomain(t *testing.T) { + t.Parallel() + + tt := []struct { + testName string + projectID string + req *app.CreateOIDCApplicationRequest + + expectedModel *domain.OIDCApp + expectedError error + }{ + { + testName: "unparsable login version 2 URL", + projectID: "pid", + req: &app.CreateOIDCApplicationRequest{ + LoginVersion: &app.LoginVersion{Version: &app.LoginVersion_LoginV2{ + LoginV2: &app.LoginV2{BaseUri: gu.Ptr("%+o")}}, + }, + }, + expectedModel: nil, + expectedError: &url.Error{ + URL: "%+o", + Op: "parse", + Err: url.EscapeError("%+o"), + }, + }, + { + testName: "all fields set", + projectID: "project1", + req: &app.CreateOIDCApplicationRequest{ + RedirectUris: []string{"https://redirect"}, + ResponseTypes: []app.OIDCResponseType{app.OIDCResponseType_OIDC_RESPONSE_TYPE_CODE}, + GrantTypes: []app.OIDCGrantType{app.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE}, + AppType: app.OIDCAppType_OIDC_APP_TYPE_WEB, + AuthMethodType: app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_BASIC, + PostLogoutRedirectUris: []string{"https://logout"}, + DevMode: true, + AccessTokenType: app.OIDCTokenType_OIDC_TOKEN_TYPE_BEARER, + AccessTokenRoleAssertion: true, + IdTokenRoleAssertion: true, + IdTokenUserinfoAssertion: true, + ClockSkew: durationpb.New(5 * time.Second), + AdditionalOrigins: []string{"https://origin"}, + SkipNativeAppSuccessPage: true, + BackChannelLogoutUri: "https://backchannel", + LoginVersion: &app.LoginVersion{Version: &app.LoginVersion_LoginV2{LoginV2: &app.LoginV2{ + BaseUri: gu.Ptr("https://login"), + }}}, + }, + expectedModel: &domain.OIDCApp{ + ObjectRoot: models.ObjectRoot{AggregateID: "project1"}, + AppName: "all fields set", + OIDCVersion: gu.Ptr(domain.OIDCVersionV1), + RedirectUris: []string{"https://redirect"}, + ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeCode}, + GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode}, + ApplicationType: gu.Ptr(domain.OIDCApplicationTypeWeb), + AuthMethodType: gu.Ptr(domain.OIDCAuthMethodTypeBasic), + PostLogoutRedirectUris: []string{"https://logout"}, + DevMode: gu.Ptr(true), + AccessTokenType: gu.Ptr(domain.OIDCTokenTypeBearer), + AccessTokenRoleAssertion: gu.Ptr(true), + IDTokenRoleAssertion: gu.Ptr(true), + IDTokenUserinfoAssertion: gu.Ptr(true), + ClockSkew: gu.Ptr(5 * time.Second), + AdditionalOrigins: []string{"https://origin"}, + SkipNativeAppSuccessPage: gu.Ptr(true), + BackChannelLogoutURI: gu.Ptr("https://backchannel"), + LoginVersion: gu.Ptr(domain.LoginVersion2), + LoginBaseURI: gu.Ptr("https://login"), + }, + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + t.Parallel() + + // When + res, err := CreateOIDCAppRequestToDomain(tc.testName, tc.projectID, tc.req) + + // Then + assert.Equal(t, tc.expectedError, err) + assert.Equal(t, tc.expectedModel, res) + }) + } +} + +func TestUpdateOIDCAppConfigRequestToDomain(t *testing.T) { + t.Parallel() + + tt := []struct { + testName string + + appID string + projectID string + req *app.UpdateOIDCApplicationConfigurationRequest + + expectedModel *domain.OIDCApp + expectedError error + }{ + { + testName: "unparsable login version 2 URL", + appID: "app1", + projectID: "pid", + req: &app.UpdateOIDCApplicationConfigurationRequest{ + LoginVersion: &app.LoginVersion{Version: &app.LoginVersion_LoginV2{ + LoginV2: &app.LoginV2{BaseUri: gu.Ptr("%+o")}, + }}, + }, + expectedModel: nil, + expectedError: &url.Error{ + URL: "%+o", + Op: "parse", + Err: url.EscapeError("%+o"), + }, + }, + { + testName: "successful Update", + appID: "app1", + projectID: "proj1", + req: &app.UpdateOIDCApplicationConfigurationRequest{ + RedirectUris: []string{"https://redirect"}, + ResponseTypes: []app.OIDCResponseType{app.OIDCResponseType_OIDC_RESPONSE_TYPE_CODE}, + GrantTypes: []app.OIDCGrantType{app.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE}, + AppType: gu.Ptr(app.OIDCAppType_OIDC_APP_TYPE_WEB), + AuthMethodType: gu.Ptr(app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_BASIC), + PostLogoutRedirectUris: []string{"https://logout"}, + DevMode: gu.Ptr(true), + AccessTokenType: gu.Ptr(app.OIDCTokenType_OIDC_TOKEN_TYPE_BEARER), + AccessTokenRoleAssertion: gu.Ptr(true), + IdTokenRoleAssertion: gu.Ptr(true), + IdTokenUserinfoAssertion: gu.Ptr(true), + ClockSkew: durationpb.New(5 * time.Second), + AdditionalOrigins: []string{"https://origin"}, + SkipNativeAppSuccessPage: gu.Ptr(true), + BackChannelLogoutUri: gu.Ptr("https://backchannel"), + LoginVersion: &app.LoginVersion{Version: &app.LoginVersion_LoginV2{ + LoginV2: &app.LoginV2{BaseUri: gu.Ptr("https://login")}, + }}, + }, + expectedModel: &domain.OIDCApp{ + ObjectRoot: models.ObjectRoot{AggregateID: "proj1"}, + AppID: "app1", + RedirectUris: []string{"https://redirect"}, + ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeCode}, + GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode}, + ApplicationType: gu.Ptr(domain.OIDCApplicationTypeWeb), + AuthMethodType: gu.Ptr(domain.OIDCAuthMethodTypeBasic), + PostLogoutRedirectUris: []string{"https://logout"}, + DevMode: gu.Ptr(true), + AccessTokenType: gu.Ptr(domain.OIDCTokenTypeBearer), + AccessTokenRoleAssertion: gu.Ptr(true), + IDTokenRoleAssertion: gu.Ptr(true), + IDTokenUserinfoAssertion: gu.Ptr(true), + ClockSkew: gu.Ptr(5 * time.Second), + AdditionalOrigins: []string{"https://origin"}, + SkipNativeAppSuccessPage: gu.Ptr(true), + BackChannelLogoutURI: gu.Ptr("https://backchannel"), + LoginVersion: gu.Ptr(domain.LoginVersion2), + LoginBaseURI: gu.Ptr("https://login"), + }, + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + t.Parallel() + + // When + got, err := UpdateOIDCAppConfigRequestToDomain(tc.appID, tc.projectID, tc.req) + + // Then + assert.Equal(t, tc.expectedError, err) + assert.Equal(t, tc.expectedModel, got) + }) + } +} + +func TestOIDCResponseTypesToDomain(t *testing.T) { + t.Parallel() + + tt := []struct { + testName string + inputResponseType []app.OIDCResponseType + expectedResponse []domain.OIDCResponseType + }{ + { + testName: "empty response types", + inputResponseType: []app.OIDCResponseType{}, + expectedResponse: []domain.OIDCResponseType{domain.OIDCResponseTypeCode}, + }, + { + testName: "all response types", + inputResponseType: []app.OIDCResponseType{ + app.OIDCResponseType_OIDC_RESPONSE_TYPE_UNSPECIFIED, + app.OIDCResponseType_OIDC_RESPONSE_TYPE_CODE, + app.OIDCResponseType_OIDC_RESPONSE_TYPE_ID_TOKEN, + app.OIDCResponseType_OIDC_RESPONSE_TYPE_ID_TOKEN_TOKEN, + }, + expectedResponse: []domain.OIDCResponseType{ + domain.OIDCResponseTypeUnspecified, + domain.OIDCResponseTypeCode, + domain.OIDCResponseTypeIDToken, + domain.OIDCResponseTypeIDTokenToken, + }, + }, + { + testName: "single response type", + inputResponseType: []app.OIDCResponseType{ + app.OIDCResponseType_OIDC_RESPONSE_TYPE_CODE, + }, + expectedResponse: []domain.OIDCResponseType{ + domain.OIDCResponseTypeCode, + }, + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + t.Parallel() + + // When + res := oidcResponseTypesToDomain(tc.inputResponseType) + + // Then + assert.Equal(t, tc.expectedResponse, res) + }) + } +} + +func TestOIDCGrantTypesToDomain(t *testing.T) { + t.Parallel() + + tt := []struct { + testName string + inputGrantType []app.OIDCGrantType + expectedGrants []domain.OIDCGrantType + }{ + { + testName: "empty grant types", + inputGrantType: []app.OIDCGrantType{}, + expectedGrants: []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode}, + }, + { + testName: "all grant types", + inputGrantType: []app.OIDCGrantType{ + app.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE, + app.OIDCGrantType_OIDC_GRANT_TYPE_IMPLICIT, + app.OIDCGrantType_OIDC_GRANT_TYPE_REFRESH_TOKEN, + app.OIDCGrantType_OIDC_GRANT_TYPE_DEVICE_CODE, + app.OIDCGrantType_OIDC_GRANT_TYPE_TOKEN_EXCHANGE, + }, + expectedGrants: []domain.OIDCGrantType{ + domain.OIDCGrantTypeAuthorizationCode, + domain.OIDCGrantTypeImplicit, + domain.OIDCGrantTypeRefreshToken, + domain.OIDCGrantTypeDeviceCode, + domain.OIDCGrantTypeTokenExchange, + }, + }, + { + testName: "single grant type", + inputGrantType: []app.OIDCGrantType{ + app.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE, + }, + expectedGrants: []domain.OIDCGrantType{ + domain.OIDCGrantTypeAuthorizationCode, + }, + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + t.Parallel() + + // When + res := oidcGrantTypesToDomain(tc.inputGrantType) + + // Then + assert.Equal(t, tc.expectedGrants, res) + }) + } +} + +func TestOIDCApplicationTypeToDomain(t *testing.T) { + t.Parallel() + + tt := []struct { + name string + appType app.OIDCAppType + expected domain.OIDCApplicationType + }{ + { + name: "web type", + appType: app.OIDCAppType_OIDC_APP_TYPE_WEB, + expected: domain.OIDCApplicationTypeWeb, + }, + { + name: "user agent type", + appType: app.OIDCAppType_OIDC_APP_TYPE_USER_AGENT, + expected: domain.OIDCApplicationTypeUserAgent, + }, + { + name: "native type", + appType: app.OIDCAppType_OIDC_APP_TYPE_NATIVE, + expected: domain.OIDCApplicationTypeNative, + }, + { + name: "unspecified type defaults to web", + expected: domain.OIDCApplicationTypeWeb, + }, + } + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + // When + result := oidcApplicationTypeToDomain(tc.appType) + + // Then + assert.Equal(t, tc.expected, result) + }) + } +} + +func TestOIDCAuthMethodTypeToDomain(t *testing.T) { + t.Parallel() + + tt := []struct { + name string + authType app.OIDCAuthMethodType + expectedResponse domain.OIDCAuthMethodType + }{ + { + name: "basic auth type", + authType: app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_BASIC, + expectedResponse: domain.OIDCAuthMethodTypeBasic, + }, + { + name: "post auth type", + authType: app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_POST, + expectedResponse: domain.OIDCAuthMethodTypePost, + }, + { + name: "none auth type", + authType: app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_NONE, + expectedResponse: domain.OIDCAuthMethodTypeNone, + }, + { + name: "private key jwt auth type", + authType: app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT, + expectedResponse: domain.OIDCAuthMethodTypePrivateKeyJWT, + }, + { + name: "unspecified auth type defaults to basic", + expectedResponse: domain.OIDCAuthMethodTypeBasic, + }, + } + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + // When + res := oidcAuthMethodTypeToDomain(tc.authType) + + // Then + assert.Equal(t, tc.expectedResponse, res) + }) + } +} + +func TestOIDCTokenTypeToDomain(t *testing.T) { + t.Parallel() + + tt := []struct { + name string + tokenType app.OIDCTokenType + expectedType domain.OIDCTokenType + }{ + { + name: "bearer token type", + tokenType: app.OIDCTokenType_OIDC_TOKEN_TYPE_BEARER, + expectedType: domain.OIDCTokenTypeBearer, + }, + { + name: "jwt token type", + tokenType: app.OIDCTokenType_OIDC_TOKEN_TYPE_JWT, + expectedType: domain.OIDCTokenTypeJWT, + }, + { + name: "unspecified defaults to bearer", + expectedType: domain.OIDCTokenTypeBearer, + }, + } + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + // When + result := oidcTokenTypeToDomain(tc.tokenType) + + // Then + assert.Equal(t, tc.expectedType, result) + }) + } +} +func TestAppOIDCConfigToPb(t *testing.T) { + t.Parallel() + + tt := []struct { + name string + input *query.OIDCApp + expected *app.Application_OidcConfig + }{ + { + name: "empty config", + input: &query.OIDCApp{}, + expected: &app.Application_OidcConfig{ + OidcConfig: &app.OIDCConfig{ + Version: app.OIDCVersion_OIDC_VERSION_1_0, + ComplianceProblems: []*app.OIDCLocalizedMessage{}, + ClockSkew: durationpb.New(0), + ResponseTypes: []app.OIDCResponseType{}, + GrantTypes: []app.OIDCGrantType{}, + }, + }, + }, + { + name: "full config", + input: &query.OIDCApp{ + RedirectURIs: []string{"https://example.com/callback"}, + ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeCode}, + GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode}, + AppType: domain.OIDCApplicationTypeWeb, + ClientID: "client123", + AuthMethodType: domain.OIDCAuthMethodTypeBasic, + PostLogoutRedirectURIs: []string{"https://example.com/logout"}, + ComplianceProblems: []string{"problem1", "problem2"}, + IsDevMode: true, + AccessTokenType: domain.OIDCTokenTypeBearer, + AssertAccessTokenRole: true, + AssertIDTokenRole: true, + AssertIDTokenUserinfo: true, + ClockSkew: 5 * time.Second, + AdditionalOrigins: []string{"https://app.example.com"}, + AllowedOrigins: []string{"https://allowed.example.com"}, + SkipNativeAppSuccessPage: true, + BackChannelLogoutURI: "https://example.com/backchannel", + LoginVersion: domain.LoginVersion2, + LoginBaseURI: gu.Ptr("https://login.example.com"), + }, + expected: &app.Application_OidcConfig{ + OidcConfig: &app.OIDCConfig{ + RedirectUris: []string{"https://example.com/callback"}, + ResponseTypes: []app.OIDCResponseType{app.OIDCResponseType_OIDC_RESPONSE_TYPE_CODE}, + GrantTypes: []app.OIDCGrantType{app.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE}, + AppType: app.OIDCAppType_OIDC_APP_TYPE_WEB, + ClientId: "client123", + AuthMethodType: app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_BASIC, + PostLogoutRedirectUris: []string{"https://example.com/logout"}, + Version: app.OIDCVersion_OIDC_VERSION_1_0, + NoneCompliant: true, + ComplianceProblems: []*app.OIDCLocalizedMessage{ + {Key: "problem1"}, + {Key: "problem2"}, + }, + DevMode: true, + AccessTokenType: app.OIDCTokenType_OIDC_TOKEN_TYPE_BEARER, + AccessTokenRoleAssertion: true, + IdTokenRoleAssertion: true, + IdTokenUserinfoAssertion: true, + ClockSkew: durationpb.New(5 * time.Second), + AdditionalOrigins: []string{"https://app.example.com"}, + AllowedOrigins: []string{"https://allowed.example.com"}, + SkipNativeAppSuccessPage: true, + BackChannelLogoutUri: "https://example.com/backchannel", + LoginVersion: &app.LoginVersion{ + Version: &app.LoginVersion_LoginV2{ + LoginV2: &app.LoginV2{ + BaseUri: gu.Ptr("https://login.example.com"), + }, + }, + }, + }, + }, + }, + } + + for _, tt := range tt { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + // When + result := appOIDCConfigToPb(tt.input) + + // Then + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestOIDCResponseTypesFromModel(t *testing.T) { + t.Parallel() + + tt := []struct { + name string + responseTypes []domain.OIDCResponseType + expected []app.OIDCResponseType + }{ + { + name: "empty response types", + responseTypes: []domain.OIDCResponseType{}, + expected: []app.OIDCResponseType{}, + }, + { + name: "all response types", + responseTypes: []domain.OIDCResponseType{ + domain.OIDCResponseTypeUnspecified, + domain.OIDCResponseTypeCode, + domain.OIDCResponseTypeIDToken, + domain.OIDCResponseTypeIDTokenToken, + }, + expected: []app.OIDCResponseType{ + app.OIDCResponseType_OIDC_RESPONSE_TYPE_UNSPECIFIED, + app.OIDCResponseType_OIDC_RESPONSE_TYPE_CODE, + app.OIDCResponseType_OIDC_RESPONSE_TYPE_ID_TOKEN, + app.OIDCResponseType_OIDC_RESPONSE_TYPE_ID_TOKEN_TOKEN, + }, + }, + { + name: "single response type", + responseTypes: []domain.OIDCResponseType{ + domain.OIDCResponseTypeCode, + }, + expected: []app.OIDCResponseType{ + app.OIDCResponseType_OIDC_RESPONSE_TYPE_CODE, + }, + }, + } + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + // When + result := oidcResponseTypesFromModel(tc.responseTypes) + + // Then + assert.Equal(t, tc.expected, result) + }) + } +} +func TestOIDCGrantTypesFromModel(t *testing.T) { + t.Parallel() + + tt := []struct { + name string + grantTypes []domain.OIDCGrantType + expected []app.OIDCGrantType + }{ + { + name: "empty grant types", + grantTypes: []domain.OIDCGrantType{}, + expected: []app.OIDCGrantType{}, + }, + { + name: "all grant types", + grantTypes: []domain.OIDCGrantType{ + domain.OIDCGrantTypeAuthorizationCode, + domain.OIDCGrantTypeImplicit, + domain.OIDCGrantTypeRefreshToken, + domain.OIDCGrantTypeDeviceCode, + domain.OIDCGrantTypeTokenExchange, + }, + expected: []app.OIDCGrantType{ + app.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE, + app.OIDCGrantType_OIDC_GRANT_TYPE_IMPLICIT, + app.OIDCGrantType_OIDC_GRANT_TYPE_REFRESH_TOKEN, + app.OIDCGrantType_OIDC_GRANT_TYPE_DEVICE_CODE, + app.OIDCGrantType_OIDC_GRANT_TYPE_TOKEN_EXCHANGE, + }, + }, + { + name: "single grant type", + grantTypes: []domain.OIDCGrantType{ + domain.OIDCGrantTypeAuthorizationCode, + }, + expected: []app.OIDCGrantType{ + app.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE, + }, + }, + } + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + // When + result := oidcGrantTypesFromModel(tc.grantTypes) + + // Then + assert.Equal(t, tc.expected, result) + }) + } +} + +func TestOIDCApplicationTypeToPb(t *testing.T) { + t.Parallel() + + tt := []struct { + name string + appType domain.OIDCApplicationType + expected app.OIDCAppType + }{ + { + name: "web type", + appType: domain.OIDCApplicationTypeWeb, + expected: app.OIDCAppType_OIDC_APP_TYPE_WEB, + }, + { + name: "user agent type", + appType: domain.OIDCApplicationTypeUserAgent, + expected: app.OIDCAppType_OIDC_APP_TYPE_USER_AGENT, + }, + { + name: "native type", + appType: domain.OIDCApplicationTypeNative, + expected: app.OIDCAppType_OIDC_APP_TYPE_NATIVE, + }, + { + name: "unspecified type defaults to web", + appType: domain.OIDCApplicationType(999), // Invalid value to trigger default case + expected: app.OIDCAppType_OIDC_APP_TYPE_WEB, + }, + } + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + // When + result := oidcApplicationTypeToPb(tc.appType) + + // Then + assert.Equal(t, tc.expected, result) + }) + } +} + +func TestOIDCAuthMethodTypeToPb(t *testing.T) { + t.Parallel() + + tt := []struct { + name string + authType domain.OIDCAuthMethodType + expected app.OIDCAuthMethodType + }{ + { + name: "basic auth type", + authType: domain.OIDCAuthMethodTypeBasic, + expected: app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_BASIC, + }, + { + name: "post auth type", + authType: domain.OIDCAuthMethodTypePost, + expected: app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_POST, + }, + { + name: "none auth type", + authType: domain.OIDCAuthMethodTypeNone, + expected: app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_NONE, + }, + { + name: "private key jwt auth type", + authType: domain.OIDCAuthMethodTypePrivateKeyJWT, + expected: app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT, + }, + { + name: "unknown auth type defaults to basic", + authType: domain.OIDCAuthMethodType(999), + expected: app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_BASIC, + }, + } + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + // When + result := oidcAuthMethodTypeToPb(tc.authType) + + // Then + assert.Equal(t, tc.expected, result) + }) + } +} + +func TestOIDCTokenTypeToPb(t *testing.T) { + t.Parallel() + + tt := []struct { + name string + tokenType domain.OIDCTokenType + expected app.OIDCTokenType + }{ + { + name: "bearer token type", + tokenType: domain.OIDCTokenTypeBearer, + expected: app.OIDCTokenType_OIDC_TOKEN_TYPE_BEARER, + }, + { + name: "jwt token type", + tokenType: domain.OIDCTokenTypeJWT, + expected: app.OIDCTokenType_OIDC_TOKEN_TYPE_JWT, + }, + { + name: "unknown token type defaults to bearer", + tokenType: domain.OIDCTokenType(999), // Invalid value to trigger default case + expected: app.OIDCTokenType_OIDC_TOKEN_TYPE_BEARER, + }, + } + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + // When + result := oidcTokenTypeToPb(tc.tokenType) + + // Then + assert.Equal(t, tc.expected, result) + }) + } +} diff --git a/internal/api/grpc/app/v2beta/convert/saml_app.go b/internal/api/grpc/app/v2beta/convert/saml_app.go new file mode 100644 index 0000000000..7f1bef082b --- /dev/null +++ b/internal/api/grpc/app/v2beta/convert/saml_app.go @@ -0,0 +1,77 @@ +package convert + +import ( + "github.com/muhlemmer/gu" + + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/eventstore/v1/models" + "github.com/zitadel/zitadel/internal/query" + app "github.com/zitadel/zitadel/pkg/grpc/app/v2beta" +) + +func CreateSAMLAppRequestToDomain(name, projectID string, req *app.CreateSAMLApplicationRequest) (*domain.SAMLApp, error) { + loginVersion, loginBaseURI, err := loginVersionToDomain(req.GetLoginVersion()) + if err != nil { + return nil, err + } + return &domain.SAMLApp{ + ObjectRoot: models.ObjectRoot{ + AggregateID: projectID, + }, + AppName: name, + Metadata: req.GetMetadataXml(), + MetadataURL: gu.Ptr(req.GetMetadataUrl()), + LoginVersion: loginVersion, + LoginBaseURI: loginBaseURI, + }, nil +} + +func UpdateSAMLAppConfigRequestToDomain(appID, projectID string, app *app.UpdateSAMLApplicationConfigurationRequest) (*domain.SAMLApp, error) { + loginVersion, loginBaseURI, err := loginVersionToDomain(app.GetLoginVersion()) + if err != nil { + return nil, err + } + + metasXML, metasURL := metasToDomain(app.GetMetadata()) + return &domain.SAMLApp{ + ObjectRoot: models.ObjectRoot{ + AggregateID: projectID, + }, + AppID: appID, + Metadata: metasXML, + MetadataURL: metasURL, + LoginVersion: loginVersion, + LoginBaseURI: loginBaseURI, + }, nil +} + +func metasToDomain(metas app.MetaType) ([]byte, *string) { + switch t := metas.(type) { + case *app.UpdateSAMLApplicationConfigurationRequest_MetadataXml: + return t.MetadataXml, nil + case *app.UpdateSAMLApplicationConfigurationRequest_MetadataUrl: + return nil, &t.MetadataUrl + case nil: + return nil, nil + default: + return nil, nil + } +} + +func appSAMLConfigToPb(samlApp *query.SAMLApp) app.ApplicationConfig { + if samlApp == nil { + return &app.Application_SamlConfig{ + SamlConfig: &app.SAMLConfig{ + Metadata: &app.SAMLConfig_MetadataXml{}, + LoginVersion: &app.LoginVersion{}, + }, + } + } + + return &app.Application_SamlConfig{ + SamlConfig: &app.SAMLConfig{ + Metadata: &app.SAMLConfig_MetadataXml{MetadataXml: samlApp.Metadata}, + LoginVersion: loginVersionToPb(samlApp.LoginVersion, samlApp.LoginBaseURI), + }, + } +} diff --git a/internal/api/grpc/app/v2beta/convert/saml_app_test.go b/internal/api/grpc/app/v2beta/convert/saml_app_test.go new file mode 100644 index 0000000000..b41ec432b6 --- /dev/null +++ b/internal/api/grpc/app/v2beta/convert/saml_app_test.go @@ -0,0 +1,256 @@ +package convert + +import ( + "fmt" + "net/url" + "testing" + + "github.com/brianvoe/gofakeit/v6" + "github.com/muhlemmer/gu" + "github.com/stretchr/testify/assert" + + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/eventstore/v1/models" + "github.com/zitadel/zitadel/internal/query" + app "github.com/zitadel/zitadel/pkg/grpc/app/v2beta" +) + +func samlMetadataGen(entityID string) []byte { + str := fmt.Sprintf(` + + + urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified + + + + +`, + entityID) + + return []byte(str) +} + +func TestCreateSAMLAppRequestToDomain(t *testing.T) { + t.Parallel() + + genMetaForValidRequest := samlMetadataGen(gofakeit.URL()) + + tt := []struct { + testName string + appName string + projectID string + req *app.CreateSAMLApplicationRequest + + expectedResponse *domain.SAMLApp + expectedError error + }{ + { + testName: "login version error", + appName: "test-app", + projectID: "proj-1", + req: &app.CreateSAMLApplicationRequest{ + Metadata: &app.CreateSAMLApplicationRequest_MetadataXml{ + MetadataXml: samlMetadataGen(gofakeit.URL()), + }, + LoginVersion: &app.LoginVersion{ + Version: &app.LoginVersion_LoginV2{ + LoginV2: &app.LoginV2{BaseUri: gu.Ptr("%+o")}, + }, + }, + }, + expectedError: &url.Error{ + URL: "%+o", + Op: "parse", + Err: url.EscapeError("%+o"), + }, + }, + { + testName: "valid request", + appName: "test-app", + projectID: "proj-1", + req: &app.CreateSAMLApplicationRequest{ + Metadata: &app.CreateSAMLApplicationRequest_MetadataXml{ + MetadataXml: genMetaForValidRequest, + }, + LoginVersion: nil, + }, + + expectedResponse: &domain.SAMLApp{ + ObjectRoot: models.ObjectRoot{AggregateID: "proj-1"}, + AppName: "test-app", + Metadata: genMetaForValidRequest, + MetadataURL: gu.Ptr(""), + LoginVersion: gu.Ptr(domain.LoginVersionUnspecified), + LoginBaseURI: gu.Ptr(""), + State: 0, + }, + }, + { + testName: "nil request", + appName: "test-app", + projectID: "proj-1", + req: nil, + + expectedResponse: &domain.SAMLApp{ + AppName: "test-app", + ObjectRoot: models.ObjectRoot{AggregateID: "proj-1"}, + MetadataURL: gu.Ptr(""), + LoginVersion: gu.Ptr(domain.LoginVersionUnspecified), + LoginBaseURI: gu.Ptr(""), + }, + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + t.Parallel() + + // When + res, err := CreateSAMLAppRequestToDomain(tc.appName, tc.projectID, tc.req) + + // Then + assert.Equal(t, tc.expectedError, err) + assert.Equal(t, tc.expectedResponse, res) + }) + } +} +func TestUpdateSAMLAppConfigRequestToDomain(t *testing.T) { + t.Parallel() + + genMetaForValidRequest := samlMetadataGen(gofakeit.URL()) + + tt := []struct { + testName string + appID string + projectID string + req *app.UpdateSAMLApplicationConfigurationRequest + + expectedResponse *domain.SAMLApp + expectedError error + }{ + { + testName: "login version error", + appID: "app-1", + projectID: "proj-1", + req: &app.UpdateSAMLApplicationConfigurationRequest{ + Metadata: &app.UpdateSAMLApplicationConfigurationRequest_MetadataXml{ + MetadataXml: samlMetadataGen(gofakeit.URL()), + }, + LoginVersion: &app.LoginVersion{ + Version: &app.LoginVersion_LoginV2{ + LoginV2: &app.LoginV2{BaseUri: gu.Ptr("%+o")}, + }, + }, + }, + expectedError: &url.Error{ + URL: "%+o", + Op: "parse", + Err: url.EscapeError("%+o"), + }, + }, + { + testName: "valid request", + appID: "app-1", + projectID: "proj-1", + req: &app.UpdateSAMLApplicationConfigurationRequest{ + Metadata: &app.UpdateSAMLApplicationConfigurationRequest_MetadataXml{ + MetadataXml: genMetaForValidRequest, + }, + LoginVersion: nil, + }, + expectedResponse: &domain.SAMLApp{ + ObjectRoot: models.ObjectRoot{AggregateID: "proj-1"}, + AppID: "app-1", + Metadata: genMetaForValidRequest, + LoginVersion: gu.Ptr(domain.LoginVersionUnspecified), + LoginBaseURI: gu.Ptr(""), + }, + }, + { + testName: "nil request", + appID: "app-1", + projectID: "proj-1", + req: nil, + expectedResponse: &domain.SAMLApp{ + ObjectRoot: models.ObjectRoot{AggregateID: "proj-1"}, + AppID: "app-1", + LoginVersion: gu.Ptr(domain.LoginVersionUnspecified), + LoginBaseURI: gu.Ptr(""), + }, + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + t.Parallel() + + // When + res, err := UpdateSAMLAppConfigRequestToDomain(tc.appID, tc.projectID, tc.req) + + // Then + assert.Equal(t, tc.expectedError, err) + assert.Equal(t, tc.expectedResponse, res) + }) + } +} + +func TestAppSAMLConfigToPb(t *testing.T) { + t.Parallel() + + metadata := samlMetadataGen(gofakeit.URL()) + + tt := []struct { + name string + inputSAMLApp *query.SAMLApp + + expectedPbApp app.ApplicationConfig + }{ + { + name: "valid conversion", + inputSAMLApp: &query.SAMLApp{ + Metadata: metadata, + LoginVersion: domain.LoginVersion2, + LoginBaseURI: gu.Ptr("https://example.com"), + }, + expectedPbApp: &app.Application_SamlConfig{ + SamlConfig: &app.SAMLConfig{ + Metadata: &app.SAMLConfig_MetadataXml{ + MetadataXml: metadata, + }, + LoginVersion: &app.LoginVersion{ + Version: &app.LoginVersion_LoginV2{ + LoginV2: &app.LoginV2{BaseUri: gu.Ptr("https://example.com")}, + }, + }, + }, + }, + }, + { + name: "nil saml app", + inputSAMLApp: nil, + expectedPbApp: &app.Application_SamlConfig{ + SamlConfig: &app.SAMLConfig{ + Metadata: &app.SAMLConfig_MetadataXml{}, + LoginVersion: &app.LoginVersion{}, + }, + }, + }, + } + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + // When + got := appSAMLConfigToPb(tc.inputSAMLApp) + + // Then + assert.Equal(t, tc.expectedPbApp, got) + }) + } +} diff --git a/internal/api/grpc/app/v2beta/integration_test/app_test.go b/internal/api/grpc/app/v2beta/integration_test/app_test.go new file mode 100644 index 0000000000..1ba46987cf --- /dev/null +++ b/internal/api/grpc/app/v2beta/integration_test/app_test.go @@ -0,0 +1,1446 @@ +//go:build integration + +package instance_test + +import ( + "context" + "fmt" + "testing" + + "github.com/brianvoe/gofakeit/v6" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + app "github.com/zitadel/zitadel/pkg/grpc/app/v2beta" + org "github.com/zitadel/zitadel/pkg/grpc/org/v2beta" +) + +func TestCreateApplication(t *testing.T) { + p := instance.CreateProject(IAMOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.Name(), false, false) + + t.Parallel() + + notExistingProjectID := gofakeit.UUID() + + tt := []struct { + testName string + creationRequest *app.CreateApplicationRequest + inputCtx context.Context + + expectedResponseType string + expectedErrorType codes.Code + }{ + { + testName: "when project for API app creation is not found should return failed precondition error", + inputCtx: IAMOwnerCtx, + creationRequest: &app.CreateApplicationRequest{ + ProjectId: notExistingProjectID, + Name: "App Name", + CreationRequestType: &app.CreateApplicationRequest_ApiRequest{ + ApiRequest: &app.CreateAPIApplicationRequest{ + AuthMethodType: app.APIAuthMethodType_API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT, + }, + }, + }, + expectedErrorType: codes.FailedPrecondition, + }, + { + testName: "when CreateAPIApp request is valid should create app and return no error", + inputCtx: IAMOwnerCtx, + creationRequest: &app.CreateApplicationRequest{ + ProjectId: p.GetId(), + Name: "App Name", + CreationRequestType: &app.CreateApplicationRequest_ApiRequest{ + ApiRequest: &app.CreateAPIApplicationRequest{ + AuthMethodType: app.APIAuthMethodType_API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT, + }, + }, + }, + expectedResponseType: fmt.Sprintf("%T", &app.CreateApplicationResponse_ApiResponse{}), + }, + { + testName: "when project for OIDC app creation is not found should return failed precondition error", + inputCtx: IAMOwnerCtx, + creationRequest: &app.CreateApplicationRequest{ + ProjectId: notExistingProjectID, + Name: "App Name", + CreationRequestType: &app.CreateApplicationRequest_OidcRequest{ + OidcRequest: &app.CreateOIDCApplicationRequest{ + RedirectUris: []string{"http://example.com"}, + ResponseTypes: []app.OIDCResponseType{app.OIDCResponseType_OIDC_RESPONSE_TYPE_CODE}, + GrantTypes: []app.OIDCGrantType{app.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE}, + AppType: app.OIDCAppType_OIDC_APP_TYPE_WEB, + AuthMethodType: app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_BASIC, + PostLogoutRedirectUris: []string{"http://example.com/home"}, + Version: app.OIDCVersion_OIDC_VERSION_1_0, + AccessTokenType: app.OIDCTokenType_OIDC_TOKEN_TYPE_JWT, + BackChannelLogoutUri: "http://example.com/logout", + LoginVersion: &app.LoginVersion{ + Version: &app.LoginVersion_LoginV2{ + LoginV2: &app.LoginV2{ + BaseUri: &baseURI, + }, + }, + }, + }, + }, + }, + expectedErrorType: codes.FailedPrecondition, + }, + { + testName: "when CreateOIDCApp request is valid should create app and return no error", + inputCtx: IAMOwnerCtx, + creationRequest: &app.CreateApplicationRequest{ + ProjectId: p.GetId(), + Name: gofakeit.AppName(), + CreationRequestType: &app.CreateApplicationRequest_OidcRequest{ + OidcRequest: &app.CreateOIDCApplicationRequest{ + RedirectUris: []string{"http://example.com"}, + ResponseTypes: []app.OIDCResponseType{app.OIDCResponseType_OIDC_RESPONSE_TYPE_CODE}, + GrantTypes: []app.OIDCGrantType{app.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE}, + AppType: app.OIDCAppType_OIDC_APP_TYPE_WEB, + AuthMethodType: app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_BASIC, + PostLogoutRedirectUris: []string{"http://example.com/home"}, + Version: app.OIDCVersion_OIDC_VERSION_1_0, + AccessTokenType: app.OIDCTokenType_OIDC_TOKEN_TYPE_JWT, + BackChannelLogoutUri: "http://example.com/logout", + LoginVersion: &app.LoginVersion{ + Version: &app.LoginVersion_LoginV2{ + LoginV2: &app.LoginV2{ + BaseUri: &baseURI, + }, + }, + }, + }, + }, + }, + + expectedResponseType: fmt.Sprintf("%T", &app.CreateApplicationResponse_OidcResponse{}), + }, + { + testName: "when project for SAML app creation is not found should return failed precondition error", + inputCtx: IAMOwnerCtx, + creationRequest: &app.CreateApplicationRequest{ + ProjectId: notExistingProjectID, + Name: gofakeit.AppName(), + CreationRequestType: &app.CreateApplicationRequest_SamlRequest{ + SamlRequest: &app.CreateSAMLApplicationRequest{ + Metadata: &app.CreateSAMLApplicationRequest_MetadataUrl{ + MetadataUrl: "http://example.com/metas", + }, + LoginVersion: &app.LoginVersion{ + Version: &app.LoginVersion_LoginV2{ + LoginV2: &app.LoginV2{ + BaseUri: &baseURI, + }, + }, + }, + }, + }, + }, + expectedErrorType: codes.FailedPrecondition, + }, + { + testName: "when CreateSAMLApp request is valid should create app and return no error", + inputCtx: IAMOwnerCtx, + creationRequest: &app.CreateApplicationRequest{ + ProjectId: p.GetId(), + Name: gofakeit.AppName(), + CreationRequestType: &app.CreateApplicationRequest_SamlRequest{ + SamlRequest: &app.CreateSAMLApplicationRequest{ + Metadata: &app.CreateSAMLApplicationRequest_MetadataXml{ + MetadataXml: samlMetadataGen(gofakeit.URL()), + }, + LoginVersion: &app.LoginVersion{ + Version: &app.LoginVersion_LoginV2{ + LoginV2: &app.LoginV2{ + BaseUri: &baseURI, + }, + }, + }, + }, + }, + }, + expectedResponseType: fmt.Sprintf("%T", &app.CreateApplicationResponse_SamlResponse{}), + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + t.Parallel() + res, err := instance.Client.AppV2Beta.CreateApplication(tc.inputCtx, tc.creationRequest) + + require.Equal(t, tc.expectedErrorType, status.Code(err)) + if tc.expectedErrorType == codes.OK { + resType := fmt.Sprintf("%T", res.GetCreationResponseType()) + assert.Equal(t, tc.expectedResponseType, resType) + assert.NotZero(t, res.GetAppId()) + assert.NotZero(t, res.GetCreationDate()) + } + }) + } +} + +func TestCreateApplication_WithDifferentPermissions(t *testing.T) { + p, projectOwnerCtx := getProjectAndProjectContext(t, instance, IAMOwnerCtx) + + t.Parallel() + + tt := []struct { + testName string + creationRequest *app.CreateApplicationRequest + inputCtx context.Context + + expectedResponseType string + expectedErrorType codes.Code + }{ + // Login User with no project.app.write + { + testName: "when user has no project.app.write permission for API request should return permission error", + inputCtx: LoginUserCtx, + creationRequest: &app.CreateApplicationRequest{ + ProjectId: p.GetId(), + Name: gofakeit.Name(), + CreationRequestType: &app.CreateApplicationRequest_ApiRequest{ + ApiRequest: &app.CreateAPIApplicationRequest{ + AuthMethodType: app.APIAuthMethodType_API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT, + }, + }, + }, + expectedErrorType: codes.PermissionDenied, + }, + { + testName: "when user has no project.app.write permission for OIDC request should return permission error", + inputCtx: LoginUserCtx, + creationRequest: &app.CreateApplicationRequest{ + ProjectId: p.GetId(), + Name: gofakeit.AppName(), + CreationRequestType: &app.CreateApplicationRequest_OidcRequest{ + OidcRequest: &app.CreateOIDCApplicationRequest{ + RedirectUris: []string{"http://example.com"}, + ResponseTypes: []app.OIDCResponseType{app.OIDCResponseType_OIDC_RESPONSE_TYPE_CODE}, + GrantTypes: []app.OIDCGrantType{app.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE}, + AppType: app.OIDCAppType_OIDC_APP_TYPE_WEB, + AuthMethodType: app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_BASIC, + PostLogoutRedirectUris: []string{"http://example.com/home"}, + Version: app.OIDCVersion_OIDC_VERSION_1_0, + AccessTokenType: app.OIDCTokenType_OIDC_TOKEN_TYPE_JWT, + BackChannelLogoutUri: "http://example.com/logout", + LoginVersion: &app.LoginVersion{ + Version: &app.LoginVersion_LoginV2{ + LoginV2: &app.LoginV2{ + BaseUri: &baseURI, + }, + }, + }, + }, + }, + }, + + expectedErrorType: codes.PermissionDenied, + }, + { + testName: "when user has no project.app.write permission for SAML request should return permission error", + inputCtx: LoginUserCtx, + creationRequest: &app.CreateApplicationRequest{ + ProjectId: p.GetId(), + Name: gofakeit.AppName(), + CreationRequestType: &app.CreateApplicationRequest_SamlRequest{ + SamlRequest: &app.CreateSAMLApplicationRequest{ + Metadata: &app.CreateSAMLApplicationRequest_MetadataXml{ + MetadataXml: samlMetadataGen(gofakeit.URL()), + }, + LoginVersion: &app.LoginVersion{ + Version: &app.LoginVersion_LoginV2{ + LoginV2: &app.LoginV2{ + BaseUri: &baseURI, + }, + }, + }, + }, + }, + }, + expectedErrorType: codes.PermissionDenied, + }, + + // OrgOwner with project.app.write permission + { + testName: "when user is OrgOwner API request should succeed", + inputCtx: OrgOwnerCtx, + creationRequest: &app.CreateApplicationRequest{ + ProjectId: p.GetId(), + Name: gofakeit.Name(), + CreationRequestType: &app.CreateApplicationRequest_ApiRequest{ + ApiRequest: &app.CreateAPIApplicationRequest{ + AuthMethodType: app.APIAuthMethodType_API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT, + }, + }, + }, + expectedResponseType: fmt.Sprintf("%T", &app.CreateApplicationResponse_ApiResponse{}), + }, + { + testName: "when user is OrgOwner OIDC request should succeed", + inputCtx: OrgOwnerCtx, + creationRequest: &app.CreateApplicationRequest{ + ProjectId: p.GetId(), + Name: gofakeit.AppName(), + CreationRequestType: &app.CreateApplicationRequest_OidcRequest{ + OidcRequest: &app.CreateOIDCApplicationRequest{ + RedirectUris: []string{"http://example.com"}, + ResponseTypes: []app.OIDCResponseType{app.OIDCResponseType_OIDC_RESPONSE_TYPE_CODE}, + GrantTypes: []app.OIDCGrantType{app.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE}, + AppType: app.OIDCAppType_OIDC_APP_TYPE_WEB, + AuthMethodType: app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_BASIC, + PostLogoutRedirectUris: []string{"http://example.com/home"}, + Version: app.OIDCVersion_OIDC_VERSION_1_0, + AccessTokenType: app.OIDCTokenType_OIDC_TOKEN_TYPE_JWT, + BackChannelLogoutUri: "http://example.com/logout", + LoginVersion: &app.LoginVersion{ + Version: &app.LoginVersion_LoginV2{ + LoginV2: &app.LoginV2{ + BaseUri: &baseURI, + }, + }, + }, + }, + }, + }, + + expectedResponseType: fmt.Sprintf("%T", &app.CreateApplicationResponse_OidcResponse{}), + }, + { + testName: "when user is OrgOwner SAML request should succeed", + inputCtx: OrgOwnerCtx, + creationRequest: &app.CreateApplicationRequest{ + ProjectId: p.GetId(), + Name: gofakeit.AppName(), + CreationRequestType: &app.CreateApplicationRequest_SamlRequest{ + SamlRequest: &app.CreateSAMLApplicationRequest{ + Metadata: &app.CreateSAMLApplicationRequest_MetadataXml{ + MetadataXml: samlMetadataGen(gofakeit.URL()), + }, + LoginVersion: &app.LoginVersion{ + Version: &app.LoginVersion_LoginV2{ + LoginV2: &app.LoginV2{ + BaseUri: &baseURI, + }, + }, + }, + }, + }, + }, + expectedResponseType: fmt.Sprintf("%T", &app.CreateApplicationResponse_SamlResponse{}), + }, + + // Project owner with project.app.write permission + { + testName: "when user is ProjectOwner API request should succeed", + inputCtx: projectOwnerCtx, + creationRequest: &app.CreateApplicationRequest{ + ProjectId: p.GetId(), + Name: gofakeit.Name(), + CreationRequestType: &app.CreateApplicationRequest_ApiRequest{ + ApiRequest: &app.CreateAPIApplicationRequest{ + AuthMethodType: app.APIAuthMethodType_API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT, + }, + }, + }, + expectedResponseType: fmt.Sprintf("%T", &app.CreateApplicationResponse_ApiResponse{}), + }, + { + testName: "when user is ProjectOwner OIDC request should succeed", + inputCtx: projectOwnerCtx, + creationRequest: &app.CreateApplicationRequest{ + ProjectId: p.GetId(), + Name: gofakeit.AppName(), + CreationRequestType: &app.CreateApplicationRequest_OidcRequest{ + OidcRequest: &app.CreateOIDCApplicationRequest{ + RedirectUris: []string{"http://example.com"}, + ResponseTypes: []app.OIDCResponseType{app.OIDCResponseType_OIDC_RESPONSE_TYPE_CODE}, + GrantTypes: []app.OIDCGrantType{app.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE}, + AppType: app.OIDCAppType_OIDC_APP_TYPE_WEB, + AuthMethodType: app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_BASIC, + PostLogoutRedirectUris: []string{"http://example.com/home"}, + Version: app.OIDCVersion_OIDC_VERSION_1_0, + AccessTokenType: app.OIDCTokenType_OIDC_TOKEN_TYPE_JWT, + BackChannelLogoutUri: "http://example.com/logout", + LoginVersion: &app.LoginVersion{ + Version: &app.LoginVersion_LoginV2{ + LoginV2: &app.LoginV2{ + BaseUri: &baseURI, + }, + }, + }, + }, + }, + }, + + expectedResponseType: fmt.Sprintf("%T", &app.CreateApplicationResponse_OidcResponse{}), + }, + { + testName: "when user is ProjectOwner SAML request should succeed", + inputCtx: projectOwnerCtx, + creationRequest: &app.CreateApplicationRequest{ + ProjectId: p.GetId(), + Name: gofakeit.AppName(), + CreationRequestType: &app.CreateApplicationRequest_SamlRequest{ + SamlRequest: &app.CreateSAMLApplicationRequest{ + Metadata: &app.CreateSAMLApplicationRequest_MetadataXml{ + MetadataXml: samlMetadataGen(gofakeit.URL()), + }, + LoginVersion: &app.LoginVersion{ + Version: &app.LoginVersion_LoginV2{ + LoginV2: &app.LoginV2{ + BaseUri: &baseURI, + }, + }, + }, + }, + }, + }, + expectedResponseType: fmt.Sprintf("%T", &app.CreateApplicationResponse_SamlResponse{}), + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + t.Parallel() + res, err := instance.Client.AppV2Beta.CreateApplication(tc.inputCtx, tc.creationRequest) + + require.Equal(t, tc.expectedErrorType, status.Code(err)) + if tc.expectedErrorType == codes.OK { + resType := fmt.Sprintf("%T", res.GetCreationResponseType()) + assert.Equal(t, tc.expectedResponseType, resType) + assert.NotZero(t, res.GetAppId()) + assert.NotZero(t, res.GetCreationDate()) + } + }) + } +} + +func TestUpdateApplication(t *testing.T) { + orgNotInCtx := instance.CreateOrganization(IAMOwnerCtx, gofakeit.Name(), gofakeit.Email()) + pNotInCtx := instance.CreateProject(IAMOwnerCtx, t, orgNotInCtx.GetOrganizationId(), gofakeit.AppName(), false, false) + + p := instance.CreateProject(IAMOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.Name(), false, false) + + baseURI := "http://example.com" + + t.Cleanup(func() { + instance.Client.OrgV2beta.DeleteOrganization(IAMOwnerCtx, &org.DeleteOrganizationRequest{ + Id: orgNotInCtx.GetOrganizationId(), + }) + }) + + reqForAppNameCreation := &app.CreateApplicationRequest_ApiRequest{ + ApiRequest: &app.CreateAPIApplicationRequest{AuthMethodType: app.APIAuthMethodType_API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT}, + } + reqForAPIAppCreation := reqForAppNameCreation + + reqForOIDCAppCreation := &app.CreateApplicationRequest_OidcRequest{ + OidcRequest: &app.CreateOIDCApplicationRequest{ + RedirectUris: []string{"http://example.com"}, + ResponseTypes: []app.OIDCResponseType{app.OIDCResponseType_OIDC_RESPONSE_TYPE_CODE}, + GrantTypes: []app.OIDCGrantType{app.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE}, + AppType: app.OIDCAppType_OIDC_APP_TYPE_WEB, + AuthMethodType: app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_BASIC, + PostLogoutRedirectUris: []string{"http://example.com/home"}, + Version: app.OIDCVersion_OIDC_VERSION_1_0, + AccessTokenType: app.OIDCTokenType_OIDC_TOKEN_TYPE_JWT, + BackChannelLogoutUri: "http://example.com/logout", + LoginVersion: &app.LoginVersion{ + Version: &app.LoginVersion_LoginV2{ + LoginV2: &app.LoginV2{ + BaseUri: &baseURI, + }, + }, + }, + }, + } + + samlMetas := samlMetadataGen(gofakeit.URL()) + reqForSAMLAppCreation := &app.CreateApplicationRequest_SamlRequest{ + SamlRequest: &app.CreateSAMLApplicationRequest{ + Metadata: &app.CreateSAMLApplicationRequest_MetadataXml{ + MetadataXml: samlMetas, + }, + LoginVersion: &app.LoginVersion{ + Version: &app.LoginVersion_LoginV2{ + LoginV2: &app.LoginV2{ + BaseUri: &baseURI, + }, + }, + }, + }, + } + + appForNameChange, appNameChangeErr := instance.Client.AppV2Beta.CreateApplication(IAMOwnerCtx, &app.CreateApplicationRequest{ + ProjectId: p.GetId(), + Name: gofakeit.AppName(), + CreationRequestType: reqForAppNameCreation, + }) + require.Nil(t, appNameChangeErr) + + appForAPIConfigChange, appAPIConfigChangeErr := instance.Client.AppV2Beta.CreateApplication(IAMOwnerCtx, &app.CreateApplicationRequest{ + ProjectId: p.GetId(), + Name: gofakeit.AppName(), + CreationRequestType: reqForAPIAppCreation, + }) + require.Nil(t, appAPIConfigChangeErr) + + appForOIDCConfigChange, appOIDCConfigChangeErr := instance.Client.AppV2Beta.CreateApplication(IAMOwnerCtx, &app.CreateApplicationRequest{ + ProjectId: p.GetId(), + Name: gofakeit.AppName(), + CreationRequestType: reqForOIDCAppCreation, + }) + require.Nil(t, appOIDCConfigChangeErr) + + appForSAMLConfigChange, appSAMLConfigChangeErr := instance.Client.AppV2Beta.CreateApplication(IAMOwnerCtx, &app.CreateApplicationRequest{ + ProjectId: p.GetId(), + Name: gofakeit.AppName(), + CreationRequestType: reqForSAMLAppCreation, + }) + require.Nil(t, appSAMLConfigChangeErr) + + t.Parallel() + + tt := []struct { + testName string + inputCtx context.Context + updateRequest *app.UpdateApplicationRequest + + expectedErrorType codes.Code + }{ + { + testName: "when app for app name change request is not found should return not found error", + inputCtx: IAMOwnerCtx, + updateRequest: &app.UpdateApplicationRequest{ + ProjectId: pNotInCtx.GetId(), + Id: appForNameChange.GetAppId(), + Name: "New name", + }, + expectedErrorType: codes.NotFound, + }, + { + testName: "when request for app name change is valid should return updated timestamp", + inputCtx: IAMOwnerCtx, + updateRequest: &app.UpdateApplicationRequest{ + ProjectId: p.GetId(), + Id: appForNameChange.GetAppId(), + + Name: "New name", + }, + }, + + { + testName: "when app for API config change request is not found should return not found error", + inputCtx: IAMOwnerCtx, + updateRequest: &app.UpdateApplicationRequest{ + ProjectId: pNotInCtx.GetId(), + Id: appForAPIConfigChange.GetAppId(), + UpdateRequestType: &app.UpdateApplicationRequest_ApiConfigurationRequest{ + ApiConfigurationRequest: &app.UpdateAPIApplicationConfigurationRequest{ + AuthMethodType: app.APIAuthMethodType_API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT, + }, + }, + }, + expectedErrorType: codes.NotFound, + }, + { + testName: "when request for API config change is valid should return updated timestamp", + inputCtx: IAMOwnerCtx, + updateRequest: &app.UpdateApplicationRequest{ + ProjectId: p.GetId(), + Id: appForAPIConfigChange.GetAppId(), + UpdateRequestType: &app.UpdateApplicationRequest_ApiConfigurationRequest{ + ApiConfigurationRequest: &app.UpdateAPIApplicationConfigurationRequest{ + AuthMethodType: app.APIAuthMethodType_API_AUTH_METHOD_TYPE_BASIC, + }, + }, + }, + }, + { + testName: "when app for OIDC config change request is not found should return not found error", + inputCtx: IAMOwnerCtx, + updateRequest: &app.UpdateApplicationRequest{ + ProjectId: pNotInCtx.GetId(), + Id: appForOIDCConfigChange.GetAppId(), + UpdateRequestType: &app.UpdateApplicationRequest_OidcConfigurationRequest{ + OidcConfigurationRequest: &app.UpdateOIDCApplicationConfigurationRequest{ + PostLogoutRedirectUris: []string{"http://example.com/home2"}, + }, + }, + }, + expectedErrorType: codes.NotFound, + }, + { + testName: "when request for OIDC config change is valid should return updated timestamp", + inputCtx: IAMOwnerCtx, + updateRequest: &app.UpdateApplicationRequest{ + ProjectId: p.GetId(), + Id: appForOIDCConfigChange.GetAppId(), + UpdateRequestType: &app.UpdateApplicationRequest_OidcConfigurationRequest{ + OidcConfigurationRequest: &app.UpdateOIDCApplicationConfigurationRequest{ + PostLogoutRedirectUris: []string{"http://example.com/home2"}, + }, + }, + }, + }, + + { + testName: "when app for SAML config change request is not found should return not found error", + inputCtx: IAMOwnerCtx, + updateRequest: &app.UpdateApplicationRequest{ + ProjectId: pNotInCtx.GetId(), + Id: appForSAMLConfigChange.GetAppId(), + UpdateRequestType: &app.UpdateApplicationRequest_SamlConfigurationRequest{ + SamlConfigurationRequest: &app.UpdateSAMLApplicationConfigurationRequest{ + Metadata: &app.UpdateSAMLApplicationConfigurationRequest_MetadataXml{ + MetadataXml: samlMetas, + }, + LoginVersion: &app.LoginVersion{Version: &app.LoginVersion_LoginV1{LoginV1: &app.LoginV1{}}}, + }, + }, + }, + expectedErrorType: codes.NotFound, + }, + { + testName: "when request for SAML config change is valid should return updated timestamp", + inputCtx: IAMOwnerCtx, + updateRequest: &app.UpdateApplicationRequest{ + ProjectId: p.GetId(), + Id: appForSAMLConfigChange.GetAppId(), + UpdateRequestType: &app.UpdateApplicationRequest_SamlConfigurationRequest{ + SamlConfigurationRequest: &app.UpdateSAMLApplicationConfigurationRequest{ + Metadata: &app.UpdateSAMLApplicationConfigurationRequest_MetadataXml{ + MetadataXml: samlMetas, + }, + LoginVersion: &app.LoginVersion{Version: &app.LoginVersion_LoginV1{LoginV1: &app.LoginV1{}}}, + }, + }, + }, + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + t.Parallel() + res, err := instance.Client.AppV2Beta.UpdateApplication(tc.inputCtx, tc.updateRequest) + + require.Equal(t, tc.expectedErrorType, status.Code(err)) + if tc.expectedErrorType == codes.OK { + assert.NotZero(t, res.GetChangeDate()) + } + }) + } +} + +func TestUpdateApplication_WithDifferentPermissions(t *testing.T) { + baseURI := "http://example.com" + + p, projectOwnerCtx := getProjectAndProjectContext(t, instance, IAMOwnerCtx) + + reqForAppNameCreation := &app.CreateApplicationRequest_ApiRequest{ + ApiRequest: &app.CreateAPIApplicationRequest{AuthMethodType: app.APIAuthMethodType_API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT}, + } + + appForNameChange, appNameChangeErr := instance.Client.AppV2Beta.CreateApplication(IAMOwnerCtx, &app.CreateApplicationRequest{ + ProjectId: p.GetId(), + Name: gofakeit.AppName(), + CreationRequestType: reqForAppNameCreation, + }) + require.Nil(t, appNameChangeErr) + + appForAPIConfigChangeForProjectOwner := createAPIApp(t, p.GetId()) + appForAPIConfigChangeForOrgOwner := createAPIApp(t, p.GetId()) + appForAPIConfigChangeForLoginUser := createAPIApp(t, p.GetId()) + + appForOIDCConfigChangeForProjectOwner := createOIDCApp(t, baseURI, p.GetId()) + appForOIDCConfigChangeForOrgOwner := createOIDCApp(t, baseURI, p.GetId()) + appForOIDCConfigChangeForLoginUser := createOIDCApp(t, baseURI, p.GetId()) + + samlMetasForProjectOwner, appForSAMLConfigChangeForProjectOwner := createSAMLApp(t, baseURI, p.GetId()) + samlMetasForOrgOwner, appForSAMLConfigChangeForOrgOwner := createSAMLApp(t, baseURI, p.GetId()) + samlMetasForLoginUser, appForSAMLConfigChangeForLoginUser := createSAMLApp(t, baseURI, p.GetId()) + + t.Parallel() + + tt := []struct { + testName string + inputCtx context.Context + updateRequest *app.UpdateApplicationRequest + + expectedErrorType codes.Code + }{ + // ProjectOwner + { + testName: "when user is ProjectOwner app name request should succeed", + inputCtx: projectOwnerCtx, + updateRequest: &app.UpdateApplicationRequest{ + ProjectId: p.GetId(), + Id: appForNameChange.GetAppId(), + + Name: gofakeit.AppName(), + }, + }, + { + testName: "when user is ProjectOwner API app request should succeed", + inputCtx: projectOwnerCtx, + updateRequest: &app.UpdateApplicationRequest{ + ProjectId: p.GetId(), + Id: appForAPIConfigChangeForProjectOwner.GetAppId(), + UpdateRequestType: &app.UpdateApplicationRequest_ApiConfigurationRequest{ + ApiConfigurationRequest: &app.UpdateAPIApplicationConfigurationRequest{ + AuthMethodType: app.APIAuthMethodType_API_AUTH_METHOD_TYPE_BASIC, + }, + }, + }, + }, + { + testName: "when user is ProjectOwner OIDC app request should succeed", + inputCtx: projectOwnerCtx, + updateRequest: &app.UpdateApplicationRequest{ + ProjectId: p.GetId(), + Id: appForOIDCConfigChangeForProjectOwner.GetAppId(), + UpdateRequestType: &app.UpdateApplicationRequest_OidcConfigurationRequest{ + OidcConfigurationRequest: &app.UpdateOIDCApplicationConfigurationRequest{ + PostLogoutRedirectUris: []string{"http://example.com/home2"}, + }, + }, + }, + }, + { + testName: "when user is ProjectOwner SAML request should succeed", + inputCtx: projectOwnerCtx, + updateRequest: &app.UpdateApplicationRequest{ + ProjectId: p.GetId(), + Id: appForSAMLConfigChangeForProjectOwner.GetAppId(), + UpdateRequestType: &app.UpdateApplicationRequest_SamlConfigurationRequest{ + SamlConfigurationRequest: &app.UpdateSAMLApplicationConfigurationRequest{ + Metadata: &app.UpdateSAMLApplicationConfigurationRequest_MetadataXml{ + MetadataXml: samlMetasForProjectOwner, + }, + LoginVersion: &app.LoginVersion{Version: &app.LoginVersion_LoginV1{LoginV1: &app.LoginV1{}}}, + }, + }, + }, + }, + + // OrgOwner context + { + testName: "when user is OrgOwner app name request should succeed", + inputCtx: OrgOwnerCtx, + updateRequest: &app.UpdateApplicationRequest{ + ProjectId: p.GetId(), + Id: appForNameChange.GetAppId(), + + Name: gofakeit.AppName(), + }, + }, + { + testName: "when user is OrgOwner API app request should succeed", + inputCtx: OrgOwnerCtx, + updateRequest: &app.UpdateApplicationRequest{ + ProjectId: p.GetId(), + Id: appForAPIConfigChangeForOrgOwner.GetAppId(), + UpdateRequestType: &app.UpdateApplicationRequest_ApiConfigurationRequest{ + ApiConfigurationRequest: &app.UpdateAPIApplicationConfigurationRequest{ + AuthMethodType: app.APIAuthMethodType_API_AUTH_METHOD_TYPE_BASIC, + }, + }, + }, + }, + { + testName: "when user is OrgOwner OIDC app request should succeed", + inputCtx: OrgOwnerCtx, + updateRequest: &app.UpdateApplicationRequest{ + ProjectId: p.GetId(), + Id: appForOIDCConfigChangeForOrgOwner.GetAppId(), + UpdateRequestType: &app.UpdateApplicationRequest_OidcConfigurationRequest{ + OidcConfigurationRequest: &app.UpdateOIDCApplicationConfigurationRequest{ + PostLogoutRedirectUris: []string{"http://example.com/home2"}, + }, + }, + }, + }, + { + testName: "when user is OrgOwner SAML request should succeed", + inputCtx: OrgOwnerCtx, + updateRequest: &app.UpdateApplicationRequest{ + ProjectId: p.GetId(), + Id: appForSAMLConfigChangeForOrgOwner.GetAppId(), + UpdateRequestType: &app.UpdateApplicationRequest_SamlConfigurationRequest{ + SamlConfigurationRequest: &app.UpdateSAMLApplicationConfigurationRequest{ + Metadata: &app.UpdateSAMLApplicationConfigurationRequest_MetadataXml{ + MetadataXml: samlMetasForOrgOwner, + }, + LoginVersion: &app.LoginVersion{Version: &app.LoginVersion_LoginV1{LoginV1: &app.LoginV1{}}}, + }, + }, + }, + }, + + // LoginUser + { + testName: "when user has no project.app.write permission for app name change request should return permission error", + inputCtx: LoginUserCtx, + updateRequest: &app.UpdateApplicationRequest{ + ProjectId: p.GetId(), + Id: appForNameChange.GetAppId(), + + Name: gofakeit.AppName(), + }, + expectedErrorType: codes.PermissionDenied, + }, + { + testName: "when user has no project.app.write permission for API request should return permission error", + inputCtx: LoginUserCtx, + updateRequest: &app.UpdateApplicationRequest{ + ProjectId: p.GetId(), + Id: appForAPIConfigChangeForLoginUser.GetAppId(), + UpdateRequestType: &app.UpdateApplicationRequest_ApiConfigurationRequest{ + ApiConfigurationRequest: &app.UpdateAPIApplicationConfigurationRequest{ + AuthMethodType: app.APIAuthMethodType_API_AUTH_METHOD_TYPE_BASIC, + }, + }, + }, + expectedErrorType: codes.PermissionDenied, + }, + { + testName: "when user has no project.app.write permission for OIDC request should return permission error", + inputCtx: LoginUserCtx, + updateRequest: &app.UpdateApplicationRequest{ + ProjectId: p.GetId(), + Id: appForOIDCConfigChangeForLoginUser.GetAppId(), + UpdateRequestType: &app.UpdateApplicationRequest_OidcConfigurationRequest{ + OidcConfigurationRequest: &app.UpdateOIDCApplicationConfigurationRequest{ + PostLogoutRedirectUris: []string{"http://example.com/home2"}, + }, + }, + }, + expectedErrorType: codes.PermissionDenied, + }, + { + testName: "when user has no project.app.write permission for SAML request should return permission error", + inputCtx: LoginUserCtx, + updateRequest: &app.UpdateApplicationRequest{ + ProjectId: p.GetId(), + Id: appForSAMLConfigChangeForLoginUser.GetAppId(), + UpdateRequestType: &app.UpdateApplicationRequest_SamlConfigurationRequest{ + SamlConfigurationRequest: &app.UpdateSAMLApplicationConfigurationRequest{ + Metadata: &app.UpdateSAMLApplicationConfigurationRequest_MetadataXml{ + MetadataXml: samlMetasForLoginUser, + }, + LoginVersion: &app.LoginVersion{Version: &app.LoginVersion_LoginV1{LoginV1: &app.LoginV1{}}}, + }, + }, + }, + expectedErrorType: codes.PermissionDenied, + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + t.Parallel() + res, err := instance.Client.AppV2Beta.UpdateApplication(tc.inputCtx, tc.updateRequest) + + require.Equal(t, tc.expectedErrorType, status.Code(err)) + if tc.expectedErrorType == codes.OK { + assert.NotZero(t, res.GetChangeDate()) + } + }) + } +} + +func TestDeleteApplication(t *testing.T) { + p := instance.CreateProject(IAMOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.Name(), false, false) + + reqForAppNameCreation := &app.CreateApplicationRequest_ApiRequest{ + ApiRequest: &app.CreateAPIApplicationRequest{AuthMethodType: app.APIAuthMethodType_API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT}, + } + + appToDelete, appNameChangeErr := instance.Client.AppV2Beta.CreateApplication(IAMOwnerCtx, &app.CreateApplicationRequest{ + ProjectId: p.GetId(), + Name: gofakeit.AppName(), + CreationRequestType: reqForAppNameCreation, + }) + require.Nil(t, appNameChangeErr) + + t.Parallel() + tt := []struct { + testName string + deleteRequest *app.DeleteApplicationRequest + inputCtx context.Context + + expectedErrorType codes.Code + }{ + { + testName: "when app to delete is not found should return not found error", + inputCtx: IAMOwnerCtx, + deleteRequest: &app.DeleteApplicationRequest{ + ProjectId: p.GetId(), + Id: gofakeit.Sentence(2), + }, + expectedErrorType: codes.NotFound, + }, + { + testName: "when app to delete is found should return deletion time", + inputCtx: IAMOwnerCtx, + deleteRequest: &app.DeleteApplicationRequest{ + ProjectId: p.GetId(), + Id: appToDelete.GetAppId(), + }, + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + t.Parallel() + + // When + res, err := instance.Client.AppV2Beta.DeleteApplication(tc.inputCtx, tc.deleteRequest) + + // Then + require.Equal(t, tc.expectedErrorType, status.Code(err)) + if tc.expectedErrorType == codes.OK { + assert.NotZero(t, res.GetDeletionDate()) + } + }) + } +} + +func TestDeleteApplication_WithDifferentPermissions(t *testing.T) { + p, projectOwnerCtx := getProjectAndProjectContext(t, instance, IAMOwnerCtx) + + appToDeleteForLoginUser := createAPIApp(t, p.GetId()) + appToDeleteForProjectOwner := createAPIApp(t, p.GetId()) + appToDeleteForOrgOwner := createAPIApp(t, p.GetId()) + + t.Parallel() + tt := []struct { + testName string + deleteRequest *app.DeleteApplicationRequest + inputCtx context.Context + + expectedErrorType codes.Code + }{ + // Login User + { + testName: "when user has no project.app.delete permission for app delete request should return permission error", + inputCtx: LoginUserCtx, + deleteRequest: &app.DeleteApplicationRequest{ + ProjectId: p.GetId(), + Id: appToDeleteForLoginUser.GetAppId(), + }, + expectedErrorType: codes.PermissionDenied, + }, + + // Project Owner + { + testName: "when user is ProjectOwner delete app request should succeed", + inputCtx: projectOwnerCtx, + deleteRequest: &app.DeleteApplicationRequest{ + ProjectId: p.GetId(), + Id: appToDeleteForProjectOwner.GetAppId(), + }, + }, + + // Org Owner + { + testName: "when user is OrgOwner delete app request should succeed", + inputCtx: projectOwnerCtx, + deleteRequest: &app.DeleteApplicationRequest{ + ProjectId: p.GetId(), + Id: appToDeleteForOrgOwner.GetAppId(), + }, + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + t.Parallel() + + // When + res, err := instance.Client.AppV2Beta.DeleteApplication(tc.inputCtx, tc.deleteRequest) + + // Then + require.Equal(t, tc.expectedErrorType, status.Code(err)) + if tc.expectedErrorType == codes.OK { + assert.NotZero(t, res.GetDeletionDate()) + } + }) + } +} + +func TestDeactivateApplication(t *testing.T) { + p := instance.CreateProject(IAMOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.Name(), false, false) + + reqForAppNameCreation := &app.CreateApplicationRequest_ApiRequest{ + ApiRequest: &app.CreateAPIApplicationRequest{AuthMethodType: app.APIAuthMethodType_API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT}, + } + + appToDeactivate, appCreateErr := instance.Client.AppV2Beta.CreateApplication(IAMOwnerCtx, &app.CreateApplicationRequest{ + ProjectId: p.GetId(), + Name: gofakeit.AppName(), + CreationRequestType: reqForAppNameCreation, + }) + require.NoError(t, appCreateErr) + + t.Parallel() + + tt := []struct { + testName string + inputCtx context.Context + deleteRequest *app.DeactivateApplicationRequest + + expectedErrorType codes.Code + }{ + { + testName: "when app to deactivate is not found should return not found error", + inputCtx: IAMOwnerCtx, + deleteRequest: &app.DeactivateApplicationRequest{ + ProjectId: p.GetId(), + Id: gofakeit.Sentence(2), + }, + expectedErrorType: codes.NotFound, + }, + { + testName: "when app to deactivate is found should return deactivation time", + inputCtx: IAMOwnerCtx, + deleteRequest: &app.DeactivateApplicationRequest{ + ProjectId: p.GetId(), + Id: appToDeactivate.GetAppId(), + }, + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + t.Parallel() + + // When + res, err := instance.Client.AppV2Beta.DeactivateApplication(tc.inputCtx, tc.deleteRequest) + + // Then + require.Equal(t, tc.expectedErrorType, status.Code(err)) + if tc.expectedErrorType == codes.OK { + assert.NotZero(t, res.GetDeactivationDate()) + } + }) + } +} + +func TestDeactivateApplication_WithDifferentPermissions(t *testing.T) { + p, projectOwnerCtx := getProjectAndProjectContext(t, instance, IAMOwnerCtx) + + appToDeactivateForLoginUser := createAPIApp(t, p.GetId()) + appToDeactivateForPrjectOwner := createAPIApp(t, p.GetId()) + appToDeactivateForOrgOwner := createAPIApp(t, p.GetId()) + + t.Parallel() + + tt := []struct { + testName string + inputCtx context.Context + deleteRequest *app.DeactivateApplicationRequest + + expectedErrorType codes.Code + }{ + // Login User + { + testName: "when user has no project.app.write permission for app deactivate request should return permission error", + inputCtx: IAMOwnerCtx, + deleteRequest: &app.DeactivateApplicationRequest{ + ProjectId: p.GetId(), + Id: appToDeactivateForLoginUser.GetAppId(), + }, + }, + + // Project Owner + { + testName: "when user is ProjectOwner deactivate app request should succeed", + inputCtx: projectOwnerCtx, + deleteRequest: &app.DeactivateApplicationRequest{ + ProjectId: p.GetId(), + Id: appToDeactivateForPrjectOwner.GetAppId(), + }, + }, + + // Org Owner + { + testName: "when user is OrgOwner deactivate app request should succeed", + inputCtx: OrgOwnerCtx, + deleteRequest: &app.DeactivateApplicationRequest{ + ProjectId: p.GetId(), + Id: appToDeactivateForOrgOwner.GetAppId(), + }, + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + t.Parallel() + + // When + res, err := instance.Client.AppV2Beta.DeactivateApplication(tc.inputCtx, tc.deleteRequest) + + // Then + require.Equal(t, tc.expectedErrorType, status.Code(err)) + if tc.expectedErrorType == codes.OK { + assert.NotZero(t, res.GetDeactivationDate()) + } + }) + } +} + +func TestReactivateApplication(t *testing.T) { + p := instance.CreateProject(IAMOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.Name(), false, false) + + reqForAppNameCreation := &app.CreateApplicationRequest_ApiRequest{ + ApiRequest: &app.CreateAPIApplicationRequest{AuthMethodType: app.APIAuthMethodType_API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT}, + } + + appToReactivate, appCreateErr := instance.Client.AppV2Beta.CreateApplication(IAMOwnerCtx, &app.CreateApplicationRequest{ + ProjectId: p.GetId(), + Name: gofakeit.AppName(), + CreationRequestType: reqForAppNameCreation, + }) + require.Nil(t, appCreateErr) + + _, appDeactivateErr := instance.Client.AppV2Beta.DeactivateApplication(IAMOwnerCtx, &app.DeactivateApplicationRequest{ + ProjectId: p.GetId(), + Id: appToReactivate.GetAppId(), + }) + require.Nil(t, appDeactivateErr) + + t.Parallel() + + tt := []struct { + testName string + inputCtx context.Context + reactivateRequest *app.ReactivateApplicationRequest + + expectedErrorType codes.Code + }{ + { + testName: "when app to reactivate is not found should return not found error", + inputCtx: IAMOwnerCtx, + reactivateRequest: &app.ReactivateApplicationRequest{ + ProjectId: p.GetId(), + Id: gofakeit.Sentence(2), + }, + expectedErrorType: codes.NotFound, + }, + { + testName: "when app to reactivate is found should return deactivation time", + inputCtx: IAMOwnerCtx, + reactivateRequest: &app.ReactivateApplicationRequest{ + ProjectId: p.GetId(), + Id: appToReactivate.GetAppId(), + }, + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + t.Parallel() + + // When + res, err := instance.Client.AppV2Beta.ReactivateApplication(tc.inputCtx, tc.reactivateRequest) + + // Then + require.Equal(t, tc.expectedErrorType, status.Code(err)) + if tc.expectedErrorType == codes.OK { + assert.NotZero(t, res.GetReactivationDate()) + } + }) + } +} + +func TestReactivateApplication_WithDifferentPermissions(t *testing.T) { + p, projectOwnerCtx := getProjectAndProjectContext(t, instance, IAMOwnerCtx) + + appToReactivateForLoginUser := createAPIApp(t, p.GetId()) + deactivateApp(t, appToReactivateForLoginUser, p.GetId()) + + appToReactivateForProjectOwner := createAPIApp(t, p.GetId()) + deactivateApp(t, appToReactivateForProjectOwner, p.GetId()) + + appToReactivateForOrgOwner := createAPIApp(t, p.GetId()) + deactivateApp(t, appToReactivateForOrgOwner, p.GetId()) + + t.Parallel() + + tt := []struct { + testName string + inputCtx context.Context + reactivateRequest *app.ReactivateApplicationRequest + + expectedErrorType codes.Code + }{ + // Login User + { + testName: "when user has no project.app.write permission for app reactivate request should return permission error", + inputCtx: LoginUserCtx, + reactivateRequest: &app.ReactivateApplicationRequest{ + ProjectId: p.GetId(), + Id: appToReactivateForLoginUser.GetAppId(), + }, + expectedErrorType: codes.PermissionDenied, + }, + + // Project Owner + { + testName: "when user is ProjectOwner reactivate app request should succeed", + inputCtx: projectOwnerCtx, + reactivateRequest: &app.ReactivateApplicationRequest{ + ProjectId: p.GetId(), + Id: appToReactivateForProjectOwner.GetAppId(), + }, + }, + + // Org Owner + { + testName: "when user is OrgOwner reactivate app request should succeed", + inputCtx: OrgOwnerCtx, + reactivateRequest: &app.ReactivateApplicationRequest{ + ProjectId: p.GetId(), + Id: appToReactivateForOrgOwner.GetAppId(), + }, + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + t.Parallel() + + // When + res, err := instance.Client.AppV2Beta.ReactivateApplication(tc.inputCtx, tc.reactivateRequest) + + // Then + require.Equal(t, tc.expectedErrorType, status.Code(err)) + if tc.expectedErrorType == codes.OK { + assert.NotZero(t, res.GetReactivationDate()) + } + }) + } +} + +func TestRegenerateClientSecret(t *testing.T) { + p := instance.CreateProject(IAMOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.Name(), false, false) + + reqForApiAppCreation := &app.CreateApplicationRequest_ApiRequest{ + ApiRequest: &app.CreateAPIApplicationRequest{AuthMethodType: app.APIAuthMethodType_API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT}, + } + + apiAppToRegen, apiAppCreateErr := instance.Client.AppV2Beta.CreateApplication(IAMOwnerCtx, &app.CreateApplicationRequest{ + ProjectId: p.GetId(), + Name: gofakeit.AppName(), + CreationRequestType: reqForApiAppCreation, + }) + require.Nil(t, apiAppCreateErr) + + reqForOIDCAppCreation := &app.CreateApplicationRequest_OidcRequest{ + OidcRequest: &app.CreateOIDCApplicationRequest{ + RedirectUris: []string{"http://example.com"}, + ResponseTypes: []app.OIDCResponseType{app.OIDCResponseType_OIDC_RESPONSE_TYPE_CODE}, + GrantTypes: []app.OIDCGrantType{app.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE}, + AppType: app.OIDCAppType_OIDC_APP_TYPE_WEB, + AuthMethodType: app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_BASIC, + PostLogoutRedirectUris: []string{"http://example.com/home"}, + Version: app.OIDCVersion_OIDC_VERSION_1_0, + AccessTokenType: app.OIDCTokenType_OIDC_TOKEN_TYPE_JWT, + BackChannelLogoutUri: "http://example.com/logout", + LoginVersion: &app.LoginVersion{ + Version: &app.LoginVersion_LoginV2{ + LoginV2: &app.LoginV2{ + BaseUri: &baseURI, + }, + }, + }, + }, + } + + oidcAppToRegen, oidcAppCreateErr := instance.Client.AppV2Beta.CreateApplication(IAMOwnerCtx, &app.CreateApplicationRequest{ + ProjectId: p.GetId(), + Name: gofakeit.AppName(), + CreationRequestType: reqForOIDCAppCreation, + }) + require.Nil(t, oidcAppCreateErr) + + t.Parallel() + + tt := []struct { + testName string + inputCtx context.Context + regenRequest *app.RegenerateClientSecretRequest + + expectedErrorType codes.Code + oldSecret string + }{ + { + testName: "when app to regen is not expected type should return invalid argument error", + inputCtx: IAMOwnerCtx, + regenRequest: &app.RegenerateClientSecretRequest{ + ProjectId: p.GetId(), + ApplicationId: gofakeit.Sentence(2), + }, + expectedErrorType: codes.InvalidArgument, + }, + { + testName: "when app to regen is not found should return not found error", + inputCtx: IAMOwnerCtx, + regenRequest: &app.RegenerateClientSecretRequest{ + ProjectId: p.GetId(), + ApplicationId: gofakeit.Sentence(2), + AppType: &app.RegenerateClientSecretRequest_IsApi{}, + }, + expectedErrorType: codes.NotFound, + }, + { + testName: "when API app to regen is found should return different secret", + inputCtx: IAMOwnerCtx, + regenRequest: &app.RegenerateClientSecretRequest{ + ProjectId: p.GetId(), + ApplicationId: apiAppToRegen.GetAppId(), + AppType: &app.RegenerateClientSecretRequest_IsApi{}, + }, + oldSecret: apiAppToRegen.GetApiResponse().GetClientSecret(), + }, + { + testName: "when OIDC app to regen is found should return different secret", + inputCtx: IAMOwnerCtx, + regenRequest: &app.RegenerateClientSecretRequest{ + ProjectId: p.GetId(), + ApplicationId: oidcAppToRegen.GetAppId(), + AppType: &app.RegenerateClientSecretRequest_IsOidc{}, + }, + oldSecret: oidcAppToRegen.GetOidcResponse().GetClientSecret(), + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + t.Parallel() + + // When + res, err := instance.Client.AppV2Beta.RegenerateClientSecret(tc.inputCtx, tc.regenRequest) + + // Then + require.Equal(t, tc.expectedErrorType, status.Code(err)) + if tc.expectedErrorType == codes.OK { + assert.NotZero(t, res.GetCreationDate()) + assert.NotEqual(t, tc.oldSecret, res.GetClientSecret()) + } + }) + } + +} + +func TestRegenerateClientSecret_WithDifferentPermissions(t *testing.T) { + p, projectOwnerCtx := getProjectAndProjectContext(t, instance, IAMOwnerCtx) + + apiAppToRegenForLoginUser := createAPIApp(t, p.GetId()) + apiAppToRegenForProjectOwner := createAPIApp(t, p.GetId()) + apiAppToRegenForOrgOwner := createAPIApp(t, p.GetId()) + + oidcAppToRegenForLoginUser := createOIDCApp(t, baseURI, p.GetId()) + oidcAppToRegenForProjectOwner := createOIDCApp(t, baseURI, p.GetId()) + oidcAppToRegenForOrgOwner := createOIDCApp(t, baseURI, p.GetId()) + + t.Parallel() + + tt := []struct { + testName string + inputCtx context.Context + regenRequest *app.RegenerateClientSecretRequest + + expectedErrorType codes.Code + oldSecret string + }{ + // Login user + { + testName: "when user has no project.app.write permission for API app secret regen request should return permission error", + inputCtx: LoginUserCtx, + regenRequest: &app.RegenerateClientSecretRequest{ + ProjectId: p.GetId(), + ApplicationId: apiAppToRegenForLoginUser.GetAppId(), + AppType: &app.RegenerateClientSecretRequest_IsApi{}, + }, + expectedErrorType: codes.PermissionDenied, + }, + { + testName: "when user has no project.app.write permission for OIDC app secret regen request should return permission error", + inputCtx: LoginUserCtx, + regenRequest: &app.RegenerateClientSecretRequest{ + ProjectId: p.GetId(), + ApplicationId: oidcAppToRegenForLoginUser.GetAppId(), + AppType: &app.RegenerateClientSecretRequest_IsOidc{}, + }, + expectedErrorType: codes.PermissionDenied, + }, + + // Project Owner + { + testName: "when user is ProjectOwner regen API app secret request should succeed", + inputCtx: projectOwnerCtx, + regenRequest: &app.RegenerateClientSecretRequest{ + ProjectId: p.GetId(), + ApplicationId: apiAppToRegenForProjectOwner.GetAppId(), + AppType: &app.RegenerateClientSecretRequest_IsApi{}, + }, + oldSecret: apiAppToRegenForProjectOwner.GetApiResponse().GetClientSecret(), + }, + { + testName: "when user is ProjectOwner regen OIDC app secret request should succeed", + inputCtx: projectOwnerCtx, + regenRequest: &app.RegenerateClientSecretRequest{ + ProjectId: p.GetId(), + ApplicationId: oidcAppToRegenForProjectOwner.GetAppId(), + AppType: &app.RegenerateClientSecretRequest_IsOidc{}, + }, + oldSecret: oidcAppToRegenForProjectOwner.GetOidcResponse().GetClientSecret(), + }, + + // Org Owner + { + testName: "when user is OrgOwner regen API app secret request should succeed", + inputCtx: OrgOwnerCtx, + regenRequest: &app.RegenerateClientSecretRequest{ + ProjectId: p.GetId(), + ApplicationId: apiAppToRegenForOrgOwner.GetAppId(), + AppType: &app.RegenerateClientSecretRequest_IsApi{}, + }, + oldSecret: apiAppToRegenForOrgOwner.GetApiResponse().GetClientSecret(), + }, + { + testName: "when user is OrgOwner regen OIDC app secret request should succeed", + inputCtx: OrgOwnerCtx, + regenRequest: &app.RegenerateClientSecretRequest{ + ProjectId: p.GetId(), + ApplicationId: oidcAppToRegenForOrgOwner.GetAppId(), + AppType: &app.RegenerateClientSecretRequest_IsOidc{}, + }, + oldSecret: oidcAppToRegenForOrgOwner.GetOidcResponse().GetClientSecret(), + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + t.Parallel() + + // When + res, err := instance.Client.AppV2Beta.RegenerateClientSecret(tc.inputCtx, tc.regenRequest) + + // Then + require.Equal(t, tc.expectedErrorType, status.Code(err)) + if tc.expectedErrorType == codes.OK { + assert.NotZero(t, res.GetCreationDate()) + assert.NotEqual(t, tc.oldSecret, res.GetClientSecret()) + } + }) + } + +} diff --git a/internal/api/grpc/app/v2beta/integration_test/query_test.go b/internal/api/grpc/app/v2beta/integration_test/query_test.go new file mode 100644 index 0000000000..578fcec138 --- /dev/null +++ b/internal/api/grpc/app/v2beta/integration_test/query_test.go @@ -0,0 +1,575 @@ +//go:build integration + +package instance_test + +import ( + "context" + "fmt" + "slices" + "testing" + "time" + + "github.com/brianvoe/gofakeit/v6" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + "github.com/zitadel/zitadel/internal/integration" + app "github.com/zitadel/zitadel/pkg/grpc/app/v2beta" + "github.com/zitadel/zitadel/pkg/grpc/filter/v2" +) + +func TestGetApplication(t *testing.T) { + p, projectOwnerCtx := getProjectAndProjectContext(t, instance, IAMOwnerCtx) + + apiAppName := gofakeit.AppName() + createdApiApp, errAPIAppCreation := instance.Client.AppV2Beta.CreateApplication(IAMOwnerCtx, &app.CreateApplicationRequest{ + ProjectId: p.GetId(), + Name: apiAppName, + CreationRequestType: &app.CreateApplicationRequest_ApiRequest{ + ApiRequest: &app.CreateAPIApplicationRequest{ + AuthMethodType: app.APIAuthMethodType_API_AUTH_METHOD_TYPE_BASIC, + }, + }, + }) + require.Nil(t, errAPIAppCreation) + + samlAppName := gofakeit.AppName() + createdSAMLApp, errSAMLAppCreation := instance.Client.AppV2Beta.CreateApplication(IAMOwnerCtx, &app.CreateApplicationRequest{ + ProjectId: p.GetId(), + Name: samlAppName, + CreationRequestType: &app.CreateApplicationRequest_SamlRequest{ + SamlRequest: &app.CreateSAMLApplicationRequest{ + LoginVersion: &app.LoginVersion{Version: &app.LoginVersion_LoginV1{LoginV1: &app.LoginV1{}}}, + Metadata: &app.CreateSAMLApplicationRequest_MetadataXml{MetadataXml: samlMetadataGen(gofakeit.URL())}, + }, + }, + }) + require.Nil(t, errSAMLAppCreation) + + oidcAppName := gofakeit.AppName() + createdOIDCApp, errOIDCAppCreation := instance.Client.AppV2Beta.CreateApplication(IAMOwnerCtx, &app.CreateApplicationRequest{ + ProjectId: p.GetId(), + Name: oidcAppName, + CreationRequestType: &app.CreateApplicationRequest_OidcRequest{ + OidcRequest: &app.CreateOIDCApplicationRequest{ + RedirectUris: []string{"http://example.com"}, + ResponseTypes: []app.OIDCResponseType{app.OIDCResponseType_OIDC_RESPONSE_TYPE_CODE}, + GrantTypes: []app.OIDCGrantType{app.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE}, + AppType: app.OIDCAppType_OIDC_APP_TYPE_WEB, + AuthMethodType: app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_BASIC, + PostLogoutRedirectUris: []string{"http://example.com/home"}, + Version: app.OIDCVersion_OIDC_VERSION_1_0, + AccessTokenType: app.OIDCTokenType_OIDC_TOKEN_TYPE_JWT, + BackChannelLogoutUri: "http://example.com/logout", + LoginVersion: &app.LoginVersion{Version: &app.LoginVersion_LoginV2{LoginV2: &app.LoginV2{BaseUri: &baseURI}}}, + }, + }, + }) + require.Nil(t, errOIDCAppCreation) + + t.Parallel() + + tt := []struct { + testName string + inputRequest *app.GetApplicationRequest + inputCtx context.Context + + expectedErrorType codes.Code + expectedAppName string + expectedAppID string + expectedApplicationType string + }{ + { + testName: "when unknown app ID should return not found error", + inputCtx: IAMOwnerCtx, + inputRequest: &app.GetApplicationRequest{ + Id: gofakeit.Sentence(2), + }, + + expectedErrorType: codes.NotFound, + }, + { + testName: "when user has no permission should return membership not found error", + inputCtx: NoPermissionCtx, + inputRequest: &app.GetApplicationRequest{ + Id: createdApiApp.GetAppId(), + }, + + expectedErrorType: codes.NotFound, + }, + { + testName: "when providing API app ID should return valid API app result", + inputCtx: projectOwnerCtx, + inputRequest: &app.GetApplicationRequest{ + Id: createdApiApp.GetAppId(), + }, + + expectedAppName: apiAppName, + expectedAppID: createdApiApp.GetAppId(), + expectedApplicationType: fmt.Sprintf("%T", &app.Application_ApiConfig{}), + }, + { + testName: "when providing SAML app ID should return valid SAML app result", + inputCtx: IAMOwnerCtx, + inputRequest: &app.GetApplicationRequest{ + Id: createdSAMLApp.GetAppId(), + }, + + expectedAppName: samlAppName, + expectedAppID: createdSAMLApp.GetAppId(), + expectedApplicationType: fmt.Sprintf("%T", &app.Application_SamlConfig{}), + }, + { + testName: "when providing OIDC app ID should return valid OIDC app result", + inputCtx: IAMOwnerCtx, + inputRequest: &app.GetApplicationRequest{ + Id: createdOIDCApp.GetAppId(), + }, + + expectedAppName: oidcAppName, + expectedAppID: createdOIDCApp.GetAppId(), + expectedApplicationType: fmt.Sprintf("%T", &app.Application_OidcConfig{}), + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + t.Parallel() + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tc.inputCtx, 30*time.Second) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + // When + res, err := instance.Client.AppV2Beta.GetApplication(tc.inputCtx, tc.inputRequest) + + // Then + require.Equal(t, tc.expectedErrorType, status.Code(err)) + if tc.expectedErrorType == codes.OK { + + assert.Equal(t, tc.expectedAppID, res.GetApp().GetId()) + assert.Equal(t, tc.expectedAppName, res.GetApp().GetName()) + assert.NotZero(t, res.GetApp().GetCreationDate()) + assert.NotZero(t, res.GetApp().GetChangeDate()) + + appType := fmt.Sprintf("%T", res.GetApp().GetConfig()) + assert.Equal(t, tc.expectedApplicationType, appType) + } + }, retryDuration, tick) + }) + } +} + +func TestListApplications(t *testing.T) { + p, projectOwnerCtx := getProjectAndProjectContext(t, instance, IAMOwnerCtx) + + t.Parallel() + + createdApiApp, apiAppName := createAPIAppWithName(t, p.GetId()) + + createdDeactivatedApiApp, deactivatedApiAppName := createAPIAppWithName(t, p.GetId()) + deactivateApp(t, createdDeactivatedApiApp, p.GetId()) + + _, createdSAMLApp, samlAppName := createSAMLAppWithName(t, gofakeit.URL(), p.GetId()) + + createdOIDCApp, oidcAppName := createOIDCAppWithName(t, gofakeit.URL(), p.GetId()) + + type appWithName struct { + app *app.CreateApplicationResponse + name string + } + + // Sorting + appsSortedByName := []appWithName{ + {name: apiAppName, app: createdApiApp}, + {name: deactivatedApiAppName, app: createdDeactivatedApiApp}, + {name: samlAppName, app: createdSAMLApp}, + {name: oidcAppName, app: createdOIDCApp}, + } + slices.SortFunc(appsSortedByName, func(a, b appWithName) int { + if a.name < b.name { + return -1 + } + if a.name > b.name { + return 1 + } + + return 0 + }) + + appsSortedByID := []appWithName{ + {name: apiAppName, app: createdApiApp}, + {name: deactivatedApiAppName, app: createdDeactivatedApiApp}, + {name: samlAppName, app: createdSAMLApp}, + {name: oidcAppName, app: createdOIDCApp}, + } + slices.SortFunc(appsSortedByID, func(a, b appWithName) int { + if a.app.GetAppId() < b.app.GetAppId() { + return -1 + } + if a.app.GetAppId() > b.app.GetAppId() { + return 1 + } + return 0 + }) + + appsSortedByCreationDate := []appWithName{ + {name: apiAppName, app: createdApiApp}, + {name: deactivatedApiAppName, app: createdDeactivatedApiApp}, + {name: samlAppName, app: createdSAMLApp}, + {name: oidcAppName, app: createdOIDCApp}, + } + slices.SortFunc(appsSortedByCreationDate, func(a, b appWithName) int { + aCreationDate := a.app.GetCreationDate().AsTime() + bCreationDate := b.app.GetCreationDate().AsTime() + + if aCreationDate.Before(bCreationDate) { + return -1 + } + if bCreationDate.Before(aCreationDate) { + return 1 + } + + return 0 + }) + + tt := []struct { + testName string + inputRequest *app.ListApplicationsRequest + inputCtx context.Context + + expectedOrderedList []appWithName + expectedOrderedKeys func(keys []appWithName) any + actualOrderedKeys func(keys []*app.Application) any + }{ + { + testName: "when no apps found should return empty list", + inputCtx: IAMOwnerCtx, + inputRequest: &app.ListApplicationsRequest{ + ProjectId: "another-id", + }, + + expectedOrderedList: []appWithName{}, + expectedOrderedKeys: func(keys []appWithName) any { return keys }, + actualOrderedKeys: func(keys []*app.Application) any { return keys }, + }, + { + testName: "when user has no read permission should return empty set", + inputCtx: NoPermissionCtx, + inputRequest: &app.ListApplicationsRequest{ + ProjectId: p.GetId(), + }, + + expectedOrderedList: []appWithName{}, + expectedOrderedKeys: func(keys []appWithName) any { return keys }, + actualOrderedKeys: func(keys []*app.Application) any { return keys }, + }, + { + testName: "when sorting by name should return apps sorted by name in descending order", + inputCtx: IAMOwnerCtx, + inputRequest: &app.ListApplicationsRequest{ + ProjectId: p.GetId(), + SortingColumn: app.AppSorting_APP_SORT_BY_NAME, + Pagination: &filter.PaginationRequest{Asc: true}, + }, + + expectedOrderedList: appsSortedByName, + expectedOrderedKeys: func(apps []appWithName) any { + names := make([]string, len(apps)) + for i, a := range apps { + names[i] = a.name + } + + return names + }, + actualOrderedKeys: func(apps []*app.Application) any { + names := make([]string, len(apps)) + for i, a := range apps { + names[i] = a.GetName() + } + + return names + }, + }, + + { + testName: "when user is project owner should return apps sorted by name in ascending order", + inputCtx: projectOwnerCtx, + inputRequest: &app.ListApplicationsRequest{ + ProjectId: p.GetId(), + SortingColumn: app.AppSorting_APP_SORT_BY_NAME, + Pagination: &filter.PaginationRequest{Asc: true}, + }, + + expectedOrderedList: appsSortedByName, + expectedOrderedKeys: func(apps []appWithName) any { + names := make([]string, len(apps)) + for i, a := range apps { + names[i] = a.name + } + + return names + }, + actualOrderedKeys: func(apps []*app.Application) any { + names := make([]string, len(apps)) + for i, a := range apps { + names[i] = a.GetName() + } + + return names + }, + }, + + { + testName: "when sorting by id should return apps sorted by id in descending order", + inputCtx: IAMOwnerCtx, + inputRequest: &app.ListApplicationsRequest{ + ProjectId: p.GetId(), + SortingColumn: app.AppSorting_APP_SORT_BY_ID, + Pagination: &filter.PaginationRequest{Asc: true}, + }, + expectedOrderedList: appsSortedByID, + expectedOrderedKeys: func(apps []appWithName) any { + ids := make([]string, len(apps)) + for i, a := range apps { + ids[i] = a.app.GetAppId() + } + + return ids + }, + actualOrderedKeys: func(apps []*app.Application) any { + ids := make([]string, len(apps)) + for i, a := range apps { + ids[i] = a.GetId() + } + + return ids + }, + }, + { + testName: "when sorting by creation date should return apps sorted by creation date in descending order", + inputCtx: IAMOwnerCtx, + inputRequest: &app.ListApplicationsRequest{ + ProjectId: p.GetId(), + SortingColumn: app.AppSorting_APP_SORT_BY_CREATION_DATE, + Pagination: &filter.PaginationRequest{Asc: true}, + }, + expectedOrderedList: appsSortedByCreationDate, + expectedOrderedKeys: func(apps []appWithName) any { + creationDates := make([]time.Time, len(apps)) + for i, a := range apps { + creationDates[i] = a.app.GetCreationDate().AsTime() + } + + return creationDates + }, + actualOrderedKeys: func(apps []*app.Application) any { + creationDates := make([]time.Time, len(apps)) + for i, a := range apps { + creationDates[i] = a.GetCreationDate().AsTime() + } + + return creationDates + }, + }, + { + testName: "when filtering by active apps should return active apps only", + inputCtx: IAMOwnerCtx, + inputRequest: &app.ListApplicationsRequest{ + ProjectId: p.GetId(), + Pagination: &filter.PaginationRequest{Asc: true}, + Filters: []*app.ApplicationSearchFilter{ + {Filter: &app.ApplicationSearchFilter_StateFilter{StateFilter: app.AppState_APP_STATE_ACTIVE}}, + }, + }, + expectedOrderedList: slices.DeleteFunc( + slices.Clone(appsSortedByID), + func(a appWithName) bool { return a.name == deactivatedApiAppName }, + ), + expectedOrderedKeys: func(apps []appWithName) any { + creationDates := make([]time.Time, len(apps)) + for i, a := range apps { + creationDates[i] = a.app.GetCreationDate().AsTime() + } + + return creationDates + }, + actualOrderedKeys: func(apps []*app.Application) any { + creationDates := make([]time.Time, len(apps)) + for i, a := range apps { + creationDates[i] = a.GetCreationDate().AsTime() + } + + return creationDates + }, + }, + { + testName: "when filtering by app type should return apps of matching type only", + inputCtx: IAMOwnerCtx, + inputRequest: &app.ListApplicationsRequest{ + ProjectId: p.GetId(), + Pagination: &filter.PaginationRequest{Asc: true}, + Filters: []*app.ApplicationSearchFilter{ + {Filter: &app.ApplicationSearchFilter_OidcAppOnly{}}, + }, + }, + expectedOrderedList: slices.DeleteFunc( + slices.Clone(appsSortedByID), + func(a appWithName) bool { return a.name != oidcAppName }, + ), + expectedOrderedKeys: func(apps []appWithName) any { + creationDates := make([]time.Time, len(apps)) + for i, a := range apps { + creationDates[i] = a.app.GetCreationDate().AsTime() + } + + return creationDates + }, + actualOrderedKeys: func(apps []*app.Application) any { + creationDates := make([]time.Time, len(apps)) + for i, a := range apps { + creationDates[i] = a.GetCreationDate().AsTime() + } + + return creationDates + }, + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + t.Parallel() + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tc.inputCtx, 30*time.Second) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + // When + res, err := instance.Client.AppV2Beta.ListApplications(tc.inputCtx, tc.inputRequest) + + // Then + require.Equal(ttt, codes.OK, status.Code(err)) + + if err == nil { + assert.Len(ttt, res.GetApplications(), len(tc.expectedOrderedList)) + actualOrderedKeys := tc.actualOrderedKeys(res.GetApplications()) + expectedOrderedKeys := tc.expectedOrderedKeys(tc.expectedOrderedList) + assert.ElementsMatch(ttt, expectedOrderedKeys, actualOrderedKeys) + } + }, retryDuration, tick) + }) + } +} + +func TestListApplications_WithPermissionV2(t *testing.T) { + ensureFeaturePermissionV2Enabled(t, instancePermissionV2) + iamOwnerCtx := instancePermissionV2.WithAuthorization(context.Background(), integration.UserTypeIAMOwner) + p, projectOwnerCtx := getProjectAndProjectContext(t, instancePermissionV2, iamOwnerCtx) + _, otherProjectOwnerCtx := getProjectAndProjectContext(t, instancePermissionV2, iamOwnerCtx) + + appName1, appName2, appName3 := gofakeit.AppName(), gofakeit.AppName(), gofakeit.AppName() + reqForAPIAppCreation := &app.CreateApplicationRequest_ApiRequest{ + ApiRequest: &app.CreateAPIApplicationRequest{AuthMethodType: app.APIAuthMethodType_API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT}, + } + + app1, appAPIConfigChangeErr := instancePermissionV2.Client.AppV2Beta.CreateApplication(iamOwnerCtx, &app.CreateApplicationRequest{ + ProjectId: p.GetId(), + Name: appName1, + CreationRequestType: reqForAPIAppCreation, + }) + require.Nil(t, appAPIConfigChangeErr) + + app2, appAPIConfigChangeErr := instancePermissionV2.Client.AppV2Beta.CreateApplication(iamOwnerCtx, &app.CreateApplicationRequest{ + ProjectId: p.GetId(), + Name: appName2, + CreationRequestType: reqForAPIAppCreation, + }) + require.Nil(t, appAPIConfigChangeErr) + + app3, appAPIConfigChangeErr := instancePermissionV2.Client.AppV2Beta.CreateApplication(iamOwnerCtx, &app.CreateApplicationRequest{ + ProjectId: p.GetId(), + Name: appName3, + CreationRequestType: reqForAPIAppCreation, + }) + require.Nil(t, appAPIConfigChangeErr) + + t.Parallel() + + tt := []struct { + testName string + inputRequest *app.ListApplicationsRequest + inputCtx context.Context + + expectedCode codes.Code + expectedAppIDs []string + }{ + { + testName: "when user has no read permission should return empty set", + inputCtx: instancePermissionV2.WithAuthorization(context.Background(), integration.UserTypeNoPermission), + inputRequest: &app.ListApplicationsRequest{ + ProjectId: p.GetId(), + }, + + expectedAppIDs: []string{}, + }, + { + testName: "when projectOwner should return full app list", + inputCtx: projectOwnerCtx, + inputRequest: &app.ListApplicationsRequest{ + ProjectId: p.GetId(), + }, + + expectedCode: codes.OK, + expectedAppIDs: []string{app1.GetAppId(), app2.GetAppId(), app3.GetAppId()}, + }, + { + testName: "when orgOwner should return full app list", + inputCtx: instancePermissionV2.WithAuthorization(context.Background(), integration.UserTypeOrgOwner), + inputRequest: &app.ListApplicationsRequest{ + ProjectId: p.GetId(), + }, + + expectedAppIDs: []string{app1.GetAppId(), app2.GetAppId(), app3.GetAppId()}, + }, + { + testName: "when iamOwner user should return full app list", + inputCtx: iamOwnerCtx, + inputRequest: &app.ListApplicationsRequest{ + ProjectId: p.GetId(), + }, + + expectedAppIDs: []string{app1.GetAppId(), app2.GetAppId(), app3.GetAppId()}, + }, + { + testName: "when other projectOwner user should return empty list", + inputCtx: otherProjectOwnerCtx, + inputRequest: &app.ListApplicationsRequest{ + ProjectId: p.GetId(), + }, + + expectedAppIDs: []string{}, + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + t.Parallel() + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tc.inputCtx, 5*time.Second) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + // When + res, err := instancePermissionV2.Client.AppV2Beta.ListApplications(tc.inputCtx, tc.inputRequest) + + // Then + require.Equal(ttt, tc.expectedCode, status.Code(err)) + + if err == nil { + require.Len(ttt, res.GetApplications(), len(tc.expectedAppIDs)) + + resAppIDs := []string{} + for _, a := range res.GetApplications() { + resAppIDs = append(resAppIDs, a.GetId()) + } + + assert.ElementsMatch(ttt, tc.expectedAppIDs, resAppIDs) + } + }, retryDuration, tick) + }) + } +} diff --git a/internal/api/grpc/app/v2beta/integration_test/server_test.go b/internal/api/grpc/app/v2beta/integration_test/server_test.go new file mode 100644 index 0000000000..6618ab0616 --- /dev/null +++ b/internal/api/grpc/app/v2beta/integration_test/server_test.go @@ -0,0 +1,205 @@ +//go:build integration + +package instance_test + +import ( + "context" + "fmt" + "os" + "testing" + "time" + + "github.com/brianvoe/gofakeit/v6" + "github.com/muhlemmer/gu" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/zitadel/zitadel/internal/integration" + app "github.com/zitadel/zitadel/pkg/grpc/app/v2beta" + "github.com/zitadel/zitadel/pkg/grpc/feature/v2" + project_v2beta "github.com/zitadel/zitadel/pkg/grpc/project/v2beta" +) + +var ( + NoPermissionCtx context.Context + LoginUserCtx context.Context + OrgOwnerCtx context.Context + IAMOwnerCtx context.Context + + instance *integration.Instance + instancePermissionV2 *integration.Instance + + baseURI = "http://example.com" +) + +func TestMain(m *testing.M) { + os.Exit(func() int { + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute) + defer cancel() + + instance = integration.NewInstance(ctx) + instancePermissionV2 = integration.NewInstance(ctx) + + IAMOwnerCtx = instance.WithAuthorization(ctx, integration.UserTypeIAMOwner) + + LoginUserCtx = instance.WithAuthorization(ctx, integration.UserTypeLogin) + OrgOwnerCtx = instance.WithAuthorization(ctx, integration.UserTypeOrgOwner) + NoPermissionCtx = instance.WithAuthorization(ctx, integration.UserTypeNoPermission) + + return m.Run() + }()) +} + +func getProjectAndProjectContext(t *testing.T, inst *integration.Instance, ctx context.Context) (*project_v2beta.CreateProjectResponse, context.Context) { + project := inst.CreateProject(ctx, t, inst.DefaultOrg.GetId(), gofakeit.Name(), false, false) + userResp := inst.CreateMachineUser(ctx) + patResp := inst.CreatePersonalAccessToken(ctx, userResp.GetUserId()) + inst.CreateProjectMembership(t, ctx, project.GetId(), userResp.GetUserId()) + projectOwnerCtx := integration.WithAuthorizationToken(context.Background(), patResp.Token) + + return project, projectOwnerCtx +} + +func samlMetadataGen(entityID string) []byte { + str := fmt.Sprintf(` + + + urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified + + + + +`, + entityID) + + return []byte(str) +} + +func createSAMLAppWithName(t *testing.T, baseURI, projectID string) ([]byte, *app.CreateApplicationResponse, string) { + samlMetas := samlMetadataGen(gofakeit.URL()) + appName := gofakeit.AppName() + + appForSAMLConfigChange, appSAMLConfigChangeErr := instance.Client.AppV2Beta.CreateApplication(IAMOwnerCtx, &app.CreateApplicationRequest{ + ProjectId: projectID, + Name: appName, + CreationRequestType: &app.CreateApplicationRequest_SamlRequest{ + SamlRequest: &app.CreateSAMLApplicationRequest{ + Metadata: &app.CreateSAMLApplicationRequest_MetadataXml{ + MetadataXml: samlMetas, + }, + LoginVersion: &app.LoginVersion{ + Version: &app.LoginVersion_LoginV2{ + LoginV2: &app.LoginV2{ + BaseUri: &baseURI, + }, + }, + }, + }, + }, + }) + require.Nil(t, appSAMLConfigChangeErr) + + return samlMetas, appForSAMLConfigChange, appName +} + +func createSAMLApp(t *testing.T, baseURI, projectID string) ([]byte, *app.CreateApplicationResponse) { + metas, app, _ := createSAMLAppWithName(t, baseURI, projectID) + return metas, app +} + +func createOIDCAppWithName(t *testing.T, baseURI, projectID string) (*app.CreateApplicationResponse, string) { + appName := gofakeit.AppName() + + appForOIDCConfigChange, appOIDCConfigChangeErr := instance.Client.AppV2Beta.CreateApplication(IAMOwnerCtx, &app.CreateApplicationRequest{ + ProjectId: projectID, + Name: appName, + CreationRequestType: &app.CreateApplicationRequest_OidcRequest{ + OidcRequest: &app.CreateOIDCApplicationRequest{ + RedirectUris: []string{"http://example.com"}, + ResponseTypes: []app.OIDCResponseType{app.OIDCResponseType_OIDC_RESPONSE_TYPE_CODE}, + GrantTypes: []app.OIDCGrantType{app.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE}, + AppType: app.OIDCAppType_OIDC_APP_TYPE_WEB, + AuthMethodType: app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_BASIC, + PostLogoutRedirectUris: []string{"http://example.com/home"}, + Version: app.OIDCVersion_OIDC_VERSION_1_0, + AccessTokenType: app.OIDCTokenType_OIDC_TOKEN_TYPE_JWT, + BackChannelLogoutUri: "http://example.com/logout", + LoginVersion: &app.LoginVersion{ + Version: &app.LoginVersion_LoginV2{ + LoginV2: &app.LoginV2{ + BaseUri: &baseURI, + }, + }, + }, + }, + }, + }) + require.Nil(t, appOIDCConfigChangeErr) + + return appForOIDCConfigChange, appName +} + +func createOIDCApp(t *testing.T, baseURI, projctID string) *app.CreateApplicationResponse { + app, _ := createOIDCAppWithName(t, baseURI, projctID) + + return app +} + +func createAPIAppWithName(t *testing.T, projectID string) (*app.CreateApplicationResponse, string) { + appName := gofakeit.AppName() + + reqForAPIAppCreation := &app.CreateApplicationRequest_ApiRequest{ + ApiRequest: &app.CreateAPIApplicationRequest{AuthMethodType: app.APIAuthMethodType_API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT}, + } + + appForAPIConfigChange, appAPIConfigChangeErr := instance.Client.AppV2Beta.CreateApplication(IAMOwnerCtx, &app.CreateApplicationRequest{ + ProjectId: projectID, + Name: appName, + CreationRequestType: reqForAPIAppCreation, + }) + require.Nil(t, appAPIConfigChangeErr) + + return appForAPIConfigChange, appName +} + +func createAPIApp(t *testing.T, projectID string) *app.CreateApplicationResponse { + res, _ := createAPIAppWithName(t, projectID) + return res +} + +func deactivateApp(t *testing.T, appToDeactivate *app.CreateApplicationResponse, projectID string) { + _, appDeactivateErr := instance.Client.AppV2Beta.DeactivateApplication(IAMOwnerCtx, &app.DeactivateApplicationRequest{ + ProjectId: projectID, + Id: appToDeactivate.GetAppId(), + }) + require.Nil(t, appDeactivateErr) +} + +func ensureFeaturePermissionV2Enabled(t *testing.T, instance *integration.Instance) { + ctx := instance.WithAuthorization(context.Background(), integration.UserTypeIAMOwner) + f, err := instance.Client.FeatureV2.GetInstanceFeatures(ctx, &feature.GetInstanceFeaturesRequest{ + Inheritance: true, + }) + require.NoError(t, err) + + if f.PermissionCheckV2.GetEnabled() { + return + } + + _, err = instance.Client.FeatureV2.SetInstanceFeatures(ctx, &feature.SetInstanceFeaturesRequest{ + PermissionCheckV2: gu.Ptr(true), + }) + require.NoError(t, err) + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(ctx, 5*time.Minute) + require.EventuallyWithT(t, func(tt *assert.CollectT) { + f, err := instance.Client.FeatureV2.GetInstanceFeatures(ctx, &feature.GetInstanceFeaturesRequest{Inheritance: true}) + require.NoError(tt, err) + assert.True(tt, f.PermissionCheckV2.GetEnabled()) + }, retryDuration, tick, "timed out waiting for ensuring instance feature") +} diff --git a/internal/api/grpc/app/v2beta/query.go b/internal/api/grpc/app/v2beta/query.go new file mode 100644 index 0000000000..add8af83e6 --- /dev/null +++ b/internal/api/grpc/app/v2beta/query.go @@ -0,0 +1,37 @@ +package app + +import ( + "context" + + "github.com/zitadel/zitadel/internal/api/grpc/app/v2beta/convert" + filter "github.com/zitadel/zitadel/internal/api/grpc/filter/v2" + app "github.com/zitadel/zitadel/pkg/grpc/app/v2beta" +) + +func (s *Server) GetApplication(ctx context.Context, req *app.GetApplicationRequest) (*app.GetApplicationResponse, error) { + res, err := s.query.AppByIDWithPermission(ctx, req.GetId(), false, s.checkPermission) + if err != nil { + return nil, err + } + + return &app.GetApplicationResponse{ + App: convert.AppToPb(res), + }, nil +} + +func (s *Server) ListApplications(ctx context.Context, req *app.ListApplicationsRequest) (*app.ListApplicationsResponse, error) { + queries, err := convert.ListApplicationsRequestToModel(s.systemDefaults, req) + if err != nil { + return nil, err + } + + res, err := s.query.SearchApps(ctx, queries, s.checkPermission) + if err != nil { + return nil, err + } + + return &app.ListApplicationsResponse{ + Applications: convert.AppsToPb(res.Apps), + Pagination: filter.QueryToPaginationPb(queries.SearchRequest, res.SearchResponse), + }, nil +} diff --git a/internal/api/grpc/app/v2beta/server.go b/internal/api/grpc/app/v2beta/server.go new file mode 100644 index 0000000000..8343cbe404 --- /dev/null +++ b/internal/api/grpc/app/v2beta/server.go @@ -0,0 +1,57 @@ +package app + +import ( + "google.golang.org/grpc" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/api/grpc/server" + "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/config/systemdefaults" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/query" + app "github.com/zitadel/zitadel/pkg/grpc/app/v2beta" +) + +var _ app.AppServiceServer = (*Server)(nil) + +type Server struct { + app.UnimplementedAppServiceServer + command *command.Commands + query *query.Queries + systemDefaults systemdefaults.SystemDefaults + 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) { + app.RegisterAppServiceServer(grpcServer, s) +} + +func (s *Server) AppName() string { + return app.AppService_ServiceDesc.ServiceName +} + +func (s *Server) MethodPrefix() string { + return app.AppService_ServiceDesc.ServiceName +} + +func (s *Server) AuthMethods() authz.MethodMapping { + return app.AppService_AuthMethods +} + +func (s *Server) RegisterGateway() server.RegisterGatewayFunc { + return app.RegisterAppServiceHandler +} diff --git a/internal/api/grpc/filter/v2/converter.go b/internal/api/grpc/filter/v2/converter.go index 7a7d7cd8d7..f797ad4bba 100644 --- a/internal/api/grpc/filter/v2/converter.go +++ b/internal/api/grpc/filter/v2/converter.go @@ -48,3 +48,26 @@ func QueryToPaginationPb(request query.SearchRequest, response query.SearchRespo TotalResult: response.Count, } } + +func TextMethodPbToQuery(method filter.TextFilterMethod) query.TextComparison { + switch method { + case filter.TextFilterMethod_TEXT_FILTER_METHOD_EQUALS: + return query.TextEquals + case filter.TextFilterMethod_TEXT_FILTER_METHOD_EQUALS_IGNORE_CASE: + return query.TextEqualsIgnoreCase + case filter.TextFilterMethod_TEXT_FILTER_METHOD_STARTS_WITH: + return query.TextStartsWith + case filter.TextFilterMethod_TEXT_FILTER_METHOD_STARTS_WITH_IGNORE_CASE: + return query.TextStartsWithIgnoreCase + case filter.TextFilterMethod_TEXT_FILTER_METHOD_CONTAINS: + return query.TextContains + case filter.TextFilterMethod_TEXT_FILTER_METHOD_CONTAINS_IGNORE_CASE: + return query.TextContainsIgnoreCase + case filter.TextFilterMethod_TEXT_FILTER_METHOD_ENDS_WITH: + return query.TextEndsWith + case filter.TextFilterMethod_TEXT_FILTER_METHOD_ENDS_WITH_IGNORE_CASE: + return query.TextEndsWithIgnoreCase + default: + return -1 + } +} diff --git a/internal/api/grpc/management/project_application.go b/internal/api/grpc/management/project_application.go index ab49905409..a5526d3cb7 100644 --- a/internal/api/grpc/management/project_application.go +++ b/internal/api/grpc/management/project_application.go @@ -29,7 +29,7 @@ func (s *Server) ListApps(ctx context.Context, req *mgmt_pb.ListAppsRequest) (*m if err != nil { return nil, err } - apps, err := s.query.SearchApps(ctx, queries, false) + apps, err := s.query.SearchApps(ctx, queries, nil) if err != nil { return nil, err } @@ -125,7 +125,7 @@ func (s *Server) AddAPIApp(ctx context.Context, req *mgmt_pb.AddAPIAppRequest) ( } func (s *Server) UpdateApp(ctx context.Context, req *mgmt_pb.UpdateAppRequest) (*mgmt_pb.UpdateAppResponse, error) { - details, err := s.command.ChangeApplication(ctx, req.ProjectId, UpdateAppRequestToDomain(req), authz.GetCtxData(ctx).OrgID) + details, err := s.command.UpdateApplicationName(ctx, req.ProjectId, UpdateAppRequestToDomain(req), authz.GetCtxData(ctx).OrgID) if err != nil { return nil, err } @@ -139,7 +139,7 @@ func (s *Server) UpdateOIDCAppConfig(ctx context.Context, req *mgmt_pb.UpdateOID if err != nil { return nil, err } - config, err := s.command.ChangeOIDCApplication(ctx, oidcApp, authz.GetCtxData(ctx).OrgID) + config, err := s.command.UpdateOIDCApplication(ctx, oidcApp, authz.GetCtxData(ctx).OrgID) if err != nil { return nil, err } @@ -157,7 +157,7 @@ func (s *Server) UpdateSAMLAppConfig(ctx context.Context, req *mgmt_pb.UpdateSAM if err != nil { return nil, err } - config, err := s.command.ChangeSAMLApplication(ctx, samlApp, authz.GetCtxData(ctx).OrgID) + config, err := s.command.UpdateSAMLApplication(ctx, samlApp, authz.GetCtxData(ctx).OrgID) if err != nil { return nil, err } @@ -171,7 +171,7 @@ func (s *Server) UpdateSAMLAppConfig(ctx context.Context, req *mgmt_pb.UpdateSAM } func (s *Server) UpdateAPIAppConfig(ctx context.Context, req *mgmt_pb.UpdateAPIAppConfigRequest) (*mgmt_pb.UpdateAPIAppConfigResponse, error) { - config, err := s.command.ChangeAPIApplication(ctx, UpdateAPIAppConfigRequestToDomain(req), authz.GetCtxData(ctx).OrgID) + config, err := s.command.UpdateAPIApplication(ctx, UpdateAPIAppConfigRequestToDomain(req), authz.GetCtxData(ctx).OrgID) if err != nil { return nil, err } diff --git a/internal/api/grpc/management/project_application_converter.go b/internal/api/grpc/management/project_application_converter.go index 13a0048a5b..186cedc933 100644 --- a/internal/api/grpc/management/project_application_converter.go +++ b/internal/api/grpc/management/project_application_converter.go @@ -4,6 +4,8 @@ import ( "context" "time" + "github.com/muhlemmer/gu" + "github.com/zitadel/zitadel/internal/api/authz" authn_grpc "github.com/zitadel/zitadel/internal/api/grpc/authn" "github.com/zitadel/zitadel/internal/api/grpc/object" @@ -46,24 +48,24 @@ func AddOIDCAppRequestToDomain(req *mgmt_pb.AddOIDCAppRequest) (*domain.OIDCApp, AggregateID: req.ProjectId, }, AppName: req.Name, - OIDCVersion: app_grpc.OIDCVersionToDomain(req.Version), + OIDCVersion: gu.Ptr(app_grpc.OIDCVersionToDomain(req.Version)), RedirectUris: req.RedirectUris, ResponseTypes: app_grpc.OIDCResponseTypesToDomain(req.ResponseTypes), GrantTypes: app_grpc.OIDCGrantTypesToDomain(req.GrantTypes), - ApplicationType: app_grpc.OIDCApplicationTypeToDomain(req.AppType), - AuthMethodType: app_grpc.OIDCAuthMethodTypeToDomain(req.AuthMethodType), + ApplicationType: gu.Ptr(app_grpc.OIDCApplicationTypeToDomain(req.AppType)), + AuthMethodType: gu.Ptr(app_grpc.OIDCAuthMethodTypeToDomain(req.AuthMethodType)), PostLogoutRedirectUris: req.PostLogoutRedirectUris, - DevMode: req.DevMode, - AccessTokenType: app_grpc.OIDCTokenTypeToDomain(req.AccessTokenType), - AccessTokenRoleAssertion: req.AccessTokenRoleAssertion, - IDTokenRoleAssertion: req.IdTokenRoleAssertion, - IDTokenUserinfoAssertion: req.IdTokenUserinfoAssertion, - ClockSkew: req.ClockSkew.AsDuration(), + DevMode: gu.Ptr(req.GetDevMode()), + AccessTokenType: gu.Ptr(app_grpc.OIDCTokenTypeToDomain(req.AccessTokenType)), + AccessTokenRoleAssertion: gu.Ptr(req.GetAccessTokenRoleAssertion()), + IDTokenRoleAssertion: gu.Ptr(req.GetIdTokenRoleAssertion()), + IDTokenUserinfoAssertion: gu.Ptr(req.GetIdTokenUserinfoAssertion()), + ClockSkew: gu.Ptr(req.GetClockSkew().AsDuration()), AdditionalOrigins: req.AdditionalOrigins, - SkipNativeAppSuccessPage: req.SkipNativeAppSuccessPage, - BackChannelLogoutURI: req.GetBackChannelLogoutUri(), - LoginVersion: loginVersion, - LoginBaseURI: loginBaseURI, + SkipNativeAppSuccessPage: gu.Ptr(req.GetSkipNativeAppSuccessPage()), + BackChannelLogoutURI: gu.Ptr(req.GetBackChannelLogoutUri()), + LoginVersion: gu.Ptr(loginVersion), + LoginBaseURI: gu.Ptr(loginBaseURI), }, nil } @@ -78,9 +80,9 @@ func AddSAMLAppRequestToDomain(req *mgmt_pb.AddSAMLAppRequest) (*domain.SAMLApp, }, AppName: req.Name, Metadata: req.GetMetadataXml(), - MetadataURL: req.GetMetadataUrl(), - LoginVersion: loginVersion, - LoginBaseURI: loginBaseURI, + MetadataURL: gu.Ptr(req.GetMetadataUrl()), + LoginVersion: gu.Ptr(loginVersion), + LoginBaseURI: gu.Ptr(loginBaseURI), }, nil } @@ -114,20 +116,20 @@ func UpdateOIDCAppConfigRequestToDomain(app *mgmt_pb.UpdateOIDCAppConfigRequest) RedirectUris: app.RedirectUris, ResponseTypes: app_grpc.OIDCResponseTypesToDomain(app.ResponseTypes), GrantTypes: app_grpc.OIDCGrantTypesToDomain(app.GrantTypes), - ApplicationType: app_grpc.OIDCApplicationTypeToDomain(app.AppType), - AuthMethodType: app_grpc.OIDCAuthMethodTypeToDomain(app.AuthMethodType), + ApplicationType: gu.Ptr(app_grpc.OIDCApplicationTypeToDomain(app.AppType)), + AuthMethodType: gu.Ptr(app_grpc.OIDCAuthMethodTypeToDomain(app.AuthMethodType)), PostLogoutRedirectUris: app.PostLogoutRedirectUris, - DevMode: app.DevMode, - AccessTokenType: app_grpc.OIDCTokenTypeToDomain(app.AccessTokenType), - AccessTokenRoleAssertion: app.AccessTokenRoleAssertion, - IDTokenRoleAssertion: app.IdTokenRoleAssertion, - IDTokenUserinfoAssertion: app.IdTokenUserinfoAssertion, - ClockSkew: app.ClockSkew.AsDuration(), + DevMode: gu.Ptr(app.GetDevMode()), + AccessTokenType: gu.Ptr(app_grpc.OIDCTokenTypeToDomain(app.AccessTokenType)), + AccessTokenRoleAssertion: gu.Ptr(app.GetAccessTokenRoleAssertion()), + IDTokenRoleAssertion: gu.Ptr(app.GetIdTokenRoleAssertion()), + IDTokenUserinfoAssertion: gu.Ptr(app.GetIdTokenUserinfoAssertion()), + ClockSkew: gu.Ptr(app.GetClockSkew().AsDuration()), AdditionalOrigins: app.AdditionalOrigins, - SkipNativeAppSuccessPage: app.SkipNativeAppSuccessPage, - BackChannelLogoutURI: app.BackChannelLogoutUri, - LoginVersion: loginVersion, - LoginBaseURI: loginBaseURI, + SkipNativeAppSuccessPage: gu.Ptr(app.GetSkipNativeAppSuccessPage()), + BackChannelLogoutURI: gu.Ptr(app.GetBackChannelLogoutUri()), + LoginVersion: gu.Ptr(loginVersion), + LoginBaseURI: gu.Ptr(loginBaseURI), }, nil } @@ -142,9 +144,9 @@ func UpdateSAMLAppConfigRequestToDomain(app *mgmt_pb.UpdateSAMLAppConfigRequest) }, AppID: app.AppId, Metadata: app.GetMetadataXml(), - MetadataURL: app.GetMetadataUrl(), - LoginVersion: loginVersion, - LoginBaseURI: loginBaseURI, + MetadataURL: gu.Ptr(app.GetMetadataUrl()), + LoginVersion: gu.Ptr(loginVersion), + LoginBaseURI: gu.Ptr(loginBaseURI), }, nil } diff --git a/internal/authz/repository/eventsourcing/view/application.go b/internal/authz/repository/eventsourcing/view/application.go index 8db8ec8e39..7fa920bcfe 100644 --- a/internal/authz/repository/eventsourcing/view/application.go +++ b/internal/authz/repository/eventsourcing/view/application.go @@ -32,7 +32,7 @@ func (v *View) ApplicationByProjecIDAndAppName(ctx context.Context, projectID, a }, } - apps, err := v.Query.SearchApps(ctx, queries, false) + apps, err := v.Query.SearchApps(ctx, queries, nil) if err != nil { return nil, err } diff --git a/internal/command/permission_checks.go b/internal/command/permission_checks.go index 6bfeaae219..3f978b6618 100644 --- a/internal/command/permission_checks.go +++ b/internal/command/permission_checks.go @@ -85,3 +85,11 @@ func (c *Commands) checkPermissionDeleteProjectGrant(ctx context.Context, resour } return nil } + +func (c *Commands) checkPermissionUpdateApplication(ctx context.Context, resourceOwner, appID string) error { + return c.newPermissionCheck(ctx, domain.PermissionProjectAppWrite, project.AggregateType)(resourceOwner, appID) +} + +func (c *Commands) checkPermissionDeleteApp(ctx context.Context, resourceOwner, appID string) error { + return c.newPermissionCheck(ctx, domain.PermissionProjectAppDelete, project.AggregateType)(resourceOwner, appID) +} diff --git a/internal/command/project_application.go b/internal/command/project_application.go index 0ccf5dc852..465b12e1e1 100644 --- a/internal/command/project_application.go +++ b/internal/command/project_application.go @@ -15,7 +15,7 @@ type AddApp struct { Name string } -func (c *Commands) ChangeApplication(ctx context.Context, projectID string, appChange domain.Application, resourceOwner string) (*domain.ObjectDetails, error) { +func (c *Commands) UpdateApplicationName(ctx context.Context, projectID string, appChange domain.Application, resourceOwner string) (*domain.ObjectDetails, error) { if projectID == "" || appChange.GetAppID() == "" || appChange.GetApplicationName() == "" { return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-4m9vS", "Errors.Project.App.Invalid") } @@ -30,6 +30,13 @@ func (c *Commands) ChangeApplication(ctx context.Context, projectID string, appC if existingApp.Name == appChange.GetApplicationName() { return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-2m8vx", "Errors.NoChangesFound") } + if err := c.eventstore.FilterToQueryReducer(ctx, existingApp); err != nil { + return nil, err + } + if err := c.checkPermissionUpdateApplication(ctx, existingApp.ResourceOwner, existingApp.AggregateID); err != nil { + return nil, err + } + projectAgg := ProjectAggregateFromWriteModel(&existingApp.WriteModel) pushedEvents, err := c.eventstore.Push( ctx, @@ -59,6 +66,13 @@ func (c *Commands) DeactivateApplication(ctx context.Context, projectID, appID, if existingApp.State != domain.AppStateActive { return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-dsh35", "Errors.Project.App.NotActive") } + if err := c.eventstore.FilterToQueryReducer(ctx, existingApp); err != nil { + return nil, err + } + if err := c.checkPermissionUpdateApplication(ctx, existingApp.ResourceOwner, existingApp.AggregateID); err != nil { + return nil, err + } + projectAgg := ProjectAggregateFromWriteModel(&existingApp.WriteModel) pushedEvents, err := c.eventstore.Push(ctx, project.NewApplicationDeactivatedEvent(ctx, projectAgg, appID)) if err != nil { @@ -86,6 +100,11 @@ func (c *Commands) ReactivateApplication(ctx context.Context, projectID, appID, if existingApp.State != domain.AppStateInactive { return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-1n8cM", "Errors.Project.App.NotInactive") } + + if err := c.checkPermissionUpdateApplication(ctx, existingApp.ResourceOwner, existingApp.AggregateID); err != nil { + return nil, err + } + projectAgg := ProjectAggregateFromWriteModel(&existingApp.WriteModel) pushedEvents, err := c.eventstore.Push(ctx, project.NewApplicationReactivatedEvent(ctx, projectAgg, appID)) @@ -111,6 +130,13 @@ func (c *Commands) RemoveApplication(ctx context.Context, projectID, appID, reso if existingApp.State == domain.AppStateUnspecified || existingApp.State == domain.AppStateRemoved { return nil, zerrors.ThrowNotFound(nil, "COMMAND-0po9s", "Errors.Project.App.NotExisting") } + if err := c.eventstore.FilterToQueryReducer(ctx, existingApp); err != nil { + return nil, err + } + if err := c.checkPermissionDeleteApp(ctx, existingApp.ResourceOwner, existingApp.AggregateID); err != nil { + return nil, err + } + projectAgg := ProjectAggregateFromWriteModel(&existingApp.WriteModel) entityID := "" diff --git a/internal/command/project_application_api.go b/internal/command/project_application_api.go index 2832dcf873..82e7d0bde8 100644 --- a/internal/command/project_application_api.go +++ b/internal/command/project_application_api.go @@ -90,16 +90,24 @@ func (c *Commands) AddAPIApplication(ctx context.Context, apiApp *domain.APIApp, return nil, zerrors.ThrowInvalidArgument(nil, "PROJECT-5m9E", "Errors.Project.App.Invalid") } - if _, err := c.checkProjectExists(ctx, apiApp.AggregateID, resourceOwner); err != nil { + projectResOwner, err := c.checkProjectExists(ctx, apiApp.AggregateID, resourceOwner) + if err != nil { return nil, err } + if resourceOwner == "" { + resourceOwner = projectResOwner + } + if !apiApp.IsValid() { return nil, zerrors.ThrowInvalidArgument(nil, "PROJECT-Bff2g", "Errors.Project.App.Invalid") } - appID, err := c.idGenerator.Next() - if err != nil { - return nil, err + appID := apiApp.AppID + if appID == "" { + appID, err = c.idGenerator.Next() + if err != nil { + return nil, err + } } return c.addAPIApplicationWithID(ctx, apiApp, resourceOwner, appID) @@ -112,6 +120,13 @@ func (c *Commands) addAPIApplicationWithID(ctx context.Context, apiApp *domain.A apiApp.AppID = appID addedApplication := NewAPIApplicationWriteModel(apiApp.AggregateID, resourceOwner) + if err := c.eventstore.FilterToQueryReducer(ctx, addedApplication); err != nil { + return nil, err + } + if err := c.checkPermissionUpdateApplication(ctx, addedApplication.ResourceOwner, addedApplication.AggregateID); err != nil { + return nil, err + } + projectAgg := ProjectAggregateFromWriteModel(&addedApplication.WriteModel) events := []eventstore.Command{ @@ -150,7 +165,7 @@ func (c *Commands) addAPIApplicationWithID(ctx context.Context, apiApp *domain.A return result, nil } -func (c *Commands) ChangeAPIApplication(ctx context.Context, apiApp *domain.APIApp, resourceOwner string) (*domain.APIApp, error) { +func (c *Commands) UpdateAPIApplication(ctx context.Context, apiApp *domain.APIApp, resourceOwner string) (*domain.APIApp, error) { if apiApp.AppID == "" || apiApp.AggregateID == "" { return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-1m900", "Errors.Project.App.APIConfigInvalid") } @@ -165,6 +180,13 @@ func (c *Commands) ChangeAPIApplication(ctx context.Context, apiApp *domain.APIA if !existingAPI.IsAPI() { return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-Gnwt3", "Errors.Project.App.IsNotAPI") } + if err := c.eventstore.FilterToQueryReducer(ctx, existingAPI); err != nil { + return nil, err + } + if err := c.checkPermissionUpdateApplication(ctx, existingAPI.ResourceOwner, existingAPI.AggregateID); err != nil { + return nil, err + } + projectAgg := ProjectAggregateFromWriteModel(&existingAPI.WriteModel) changedEvent, hasChanged, err := existingAPI.NewChangedEvent( ctx, @@ -205,6 +227,11 @@ func (c *Commands) ChangeAPIApplicationSecret(ctx context.Context, projectID, ap if !existingAPI.IsAPI() { return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-aeH4", "Errors.Project.App.IsNotAPI") } + + if err := c.checkPermissionUpdateApplication(ctx, existingAPI.ResourceOwner, existingAPI.AggregateID); err != nil { + return nil, err + } + encodedHash, plain, err := c.newHashedSecret(ctx, c.eventstore.Filter) //nolint:staticcheck if err != nil { return nil, err diff --git a/internal/command/project_application_api_test.go b/internal/command/project_application_api_test.go index a6d4349254..53448e1c5e 100644 --- a/internal/command/project_application_api_test.go +++ b/internal/command/project_application_api_test.go @@ -142,6 +142,7 @@ func TestAddAPIConfig(t *testing.T) { } func TestCommandSide_AddAPIApplication(t *testing.T) { + t.Parallel() type fields struct { eventstore func(t *testing.T) *eventstore.Eventstore idGenerator id.Generator @@ -238,6 +239,7 @@ func TestCommandSide_AddAPIApplication(t *testing.T) { domain.PrivateLabelingSettingUnspecified), ), ), + expectFilter(), expectPush( project.NewApplicationAddedEvent(context.Background(), &project.NewAggregate("project1", "org1").Aggregate, @@ -292,6 +294,7 @@ func TestCommandSide_AddAPIApplication(t *testing.T) { domain.PrivateLabelingSettingUnspecified), ), ), + expectFilter(), expectPush( project.NewApplicationAddedEvent(context.Background(), &project.NewAggregate("project1", "org1").Aggregate, @@ -346,6 +349,7 @@ func TestCommandSide_AddAPIApplication(t *testing.T) { domain.PrivateLabelingSettingUnspecified), ), ), + expectFilter(), expectPush( project.NewApplicationAddedEvent(context.Background(), &project.NewAggregate("project1", "org1").Aggregate, @@ -390,6 +394,8 @@ func TestCommandSide_AddAPIApplication(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + t.Parallel() + r := &Commands{ eventstore: tt.fields.eventstore(t), idGenerator: tt.fields.idGenerator, @@ -397,6 +403,7 @@ func TestCommandSide_AddAPIApplication(t *testing.T) { defaultSecretGenerators: &SecretGenerators{ ClientSecret: emptyConfig, }, + checkPermission: newMockPermissionCheckAllowed(), } got, err := r.AddAPIApplication(tt.args.ctx, tt.args.apiApp, tt.args.resourceOwner) if tt.res.err == nil { @@ -413,6 +420,8 @@ func TestCommandSide_AddAPIApplication(t *testing.T) { } func TestCommandSide_ChangeAPIApplication(t *testing.T) { + t.Parallel() + type fields struct { eventstore func(t *testing.T) *eventstore.Eventstore } @@ -516,6 +525,7 @@ func TestCommandSide_ChangeAPIApplication(t *testing.T) { domain.APIAuthMethodTypePrivateKeyJWT), ), ), + expectFilter(), ), }, args: args{ @@ -555,6 +565,7 @@ func TestCommandSide_ChangeAPIApplication(t *testing.T) { domain.APIAuthMethodTypeBasic), ), ), + expectFilter(), expectPush( newAPIAppChangedEvent(context.Background(), "app1", @@ -593,14 +604,17 @@ func TestCommandSide_ChangeAPIApplication(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + t.Parallel() + r := &Commands{ eventstore: tt.fields.eventstore(t), newHashedSecret: mockHashedSecret("secret"), defaultSecretGenerators: &SecretGenerators{ ClientSecret: emptyConfig, }, + checkPermission: newMockPermissionCheckAllowed(), } - got, err := r.ChangeAPIApplication(tt.args.ctx, tt.args.apiApp, tt.args.resourceOwner) + got, err := r.UpdateAPIApplication(tt.args.ctx, tt.args.apiApp, tt.args.resourceOwner) if tt.res.err == nil { assert.NoError(t, err) } @@ -615,6 +629,8 @@ func TestCommandSide_ChangeAPIApplication(t *testing.T) { } func TestCommandSide_ChangeAPIApplicationSecret(t *testing.T) { + t.Parallel() + type fields struct { eventstore func(*testing.T) *eventstore.Eventstore } @@ -734,12 +750,15 @@ func TestCommandSide_ChangeAPIApplicationSecret(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + t.Parallel() + r := &Commands{ eventstore: tt.fields.eventstore(t), newHashedSecret: mockHashedSecret("secret"), defaultSecretGenerators: &SecretGenerators{ ClientSecret: emptyConfig, }, + checkPermission: newMockPermissionCheckAllowed(), } got, err := r.ChangeAPIApplicationSecret(tt.args.ctx, tt.args.projectID, tt.args.appID, tt.args.resourceOwner) if tt.res.err == nil { diff --git a/internal/command/project_application_oidc.go b/internal/command/project_application_oidc.go index 77ef7ff0c7..7f33b6a3cf 100644 --- a/internal/command/project_application_oidc.go +++ b/internal/command/project_application_oidc.go @@ -5,6 +5,8 @@ import ( "strings" "time" + "github.com/muhlemmer/gu" + http_util "github.com/zitadel/zitadel/internal/api/http" "github.com/zitadel/zitadel/internal/command/preparation" "github.com/zitadel/zitadel/internal/domain" @@ -120,6 +122,7 @@ func (c *Commands) AddOIDCAppCommand(app *addOIDCApp) preparation.Validation { } } +// TODO: Combine with AddOIDCApplication and addOIDCApplicationWithID func (c *Commands) AddOIDCApplicationWithID(ctx context.Context, oidcApp *domain.OIDCApp, resourceOwner, appID string) (_ *domain.OIDCApp, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() @@ -142,9 +145,15 @@ func (c *Commands) AddOIDCApplication(ctx context.Context, oidcApp *domain.OIDCA if oidcApp == nil || oidcApp.AggregateID == "" { return nil, zerrors.ThrowInvalidArgument(nil, "PROJECT-34Fm0", "Errors.Project.App.Invalid") } - if _, err := c.checkProjectExists(ctx, oidcApp.AggregateID, resourceOwner); err != nil { + + projectResOwner, err := c.checkProjectExists(ctx, oidcApp.AggregateID, resourceOwner) + if err != nil { return nil, err } + if resourceOwner == "" { + resourceOwner = projectResOwner + } + if oidcApp.AppName == "" || !oidcApp.IsValid() { return nil, zerrors.ThrowInvalidArgument(nil, "PROJECT-1n8df", "Errors.Project.App.Invalid") } @@ -162,6 +171,13 @@ func (c *Commands) addOIDCApplicationWithID(ctx context.Context, oidcApp *domain defer func() { span.EndWithError(err) }() addedApplication := NewOIDCApplicationWriteModel(oidcApp.AggregateID, resourceOwner) + if err := c.eventstore.FilterToQueryReducer(ctx, addedApplication); err != nil { + return nil, err + } + if err := c.checkPermissionUpdateApplication(ctx, addedApplication.ResourceOwner, addedApplication.AggregateID); err != nil { + return nil, err + } + projectAgg := ProjectAggregateFromWriteModel(&addedApplication.WriteModel) oidcApp.AppID = appID @@ -183,27 +199,27 @@ func (c *Commands) addOIDCApplicationWithID(ctx context.Context, oidcApp *domain } events = append(events, project_repo.NewOIDCConfigAddedEvent(ctx, projectAgg, - oidcApp.OIDCVersion, + gu.Value(oidcApp.OIDCVersion), oidcApp.AppID, oidcApp.ClientID, oidcApp.EncodedHash, trimStringSliceWhiteSpaces(oidcApp.RedirectUris), oidcApp.ResponseTypes, oidcApp.GrantTypes, - oidcApp.ApplicationType, - oidcApp.AuthMethodType, + gu.Value(oidcApp.ApplicationType), + gu.Value(oidcApp.AuthMethodType), trimStringSliceWhiteSpaces(oidcApp.PostLogoutRedirectUris), - oidcApp.DevMode, - oidcApp.AccessTokenType, - oidcApp.AccessTokenRoleAssertion, - oidcApp.IDTokenRoleAssertion, - oidcApp.IDTokenUserinfoAssertion, - oidcApp.ClockSkew, + gu.Value(oidcApp.DevMode), + gu.Value(oidcApp.AccessTokenType), + gu.Value(oidcApp.AccessTokenRoleAssertion), + gu.Value(oidcApp.IDTokenRoleAssertion), + gu.Value(oidcApp.IDTokenUserinfoAssertion), + gu.Value(oidcApp.ClockSkew), trimStringSliceWhiteSpaces(oidcApp.AdditionalOrigins), - oidcApp.SkipNativeAppSuccessPage, - strings.TrimSpace(oidcApp.BackChannelLogoutURI), - oidcApp.LoginVersion, - strings.TrimSpace(oidcApp.LoginBaseURI), + gu.Value(oidcApp.SkipNativeAppSuccessPage), + strings.TrimSpace(gu.Value(oidcApp.BackChannelLogoutURI)), + gu.Value(oidcApp.LoginVersion), + strings.TrimSpace(gu.Value(oidcApp.LoginBaseURI)), )) addedApplication.AppID = oidcApp.AppID @@ -226,7 +242,7 @@ func (c *Commands) addOIDCApplicationWithID(ctx context.Context, oidcApp *domain return result, nil } -func (c *Commands) ChangeOIDCApplication(ctx context.Context, oidc *domain.OIDCApp, resourceOwner string) (*domain.OIDCApp, error) { +func (c *Commands) UpdateOIDCApplication(ctx context.Context, oidc *domain.OIDCApp, resourceOwner string) (*domain.OIDCApp, error) { if !oidc.IsValid() || oidc.AppID == "" || oidc.AggregateID == "" { return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-5m9fs", "Errors.Project.App.OIDCConfigInvalid") } @@ -241,7 +257,23 @@ func (c *Commands) ChangeOIDCApplication(ctx context.Context, oidc *domain.OIDCA if !existingOIDC.IsOIDC() { return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-GBr34", "Errors.Project.App.IsNotOIDC") } + if err := c.eventstore.FilterToQueryReducer(ctx, existingOIDC); err != nil { + return nil, err + } + if err := c.checkPermissionUpdateApplication(ctx, existingOIDC.ResourceOwner, existingOIDC.AggregateID); err != nil { + return nil, err + } + projectAgg := ProjectAggregateFromWriteModel(&existingOIDC.WriteModel) + var backChannelLogout, loginBaseURI *string + if oidc.BackChannelLogoutURI != nil { + backChannelLogout = gu.Ptr(strings.TrimSpace(*oidc.BackChannelLogoutURI)) + } + + if oidc.LoginBaseURI != nil { + loginBaseURI = gu.Ptr(strings.TrimSpace(*oidc.LoginBaseURI)) + } + changedEvent, hasChanged, err := existingOIDC.NewChangedEvent( ctx, projectAgg, @@ -261,9 +293,9 @@ func (c *Commands) ChangeOIDCApplication(ctx context.Context, oidc *domain.OIDCA oidc.ClockSkew, trimStringSliceWhiteSpaces(oidc.AdditionalOrigins), oidc.SkipNativeAppSuccessPage, - strings.TrimSpace(oidc.BackChannelLogoutURI), + backChannelLogout, oidc.LoginVersion, - strings.TrimSpace(oidc.LoginBaseURI), + loginBaseURI, ) if err != nil { return nil, err @@ -301,6 +333,11 @@ func (c *Commands) ChangeOIDCApplicationSecret(ctx context.Context, projectID, a if !existingOIDC.IsOIDC() { return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-Ghrh3", "Errors.Project.App.IsNotOIDC") } + + if err := c.checkPermissionUpdateApplication(ctx, existingOIDC.ResourceOwner, existingOIDC.AggregateID); err != nil { + return nil, err + } + encodedHash, plain, err := c.newHashedSecret(ctx, c.eventstore.Filter) //nolint:staticcheck if err != nil { return nil, err diff --git a/internal/command/project_application_oidc_model.go b/internal/command/project_application_oidc_model.go index 603ebdcda2..375bb26f5e 100644 --- a/internal/command/project_application_oidc_model.go +++ b/internal/command/project_application_oidc_model.go @@ -258,77 +258,77 @@ func (wm *OIDCApplicationWriteModel) NewChangedEvent( postLogoutRedirectURIs []string, responseTypes []domain.OIDCResponseType, grantTypes []domain.OIDCGrantType, - appType domain.OIDCApplicationType, - authMethodType domain.OIDCAuthMethodType, - oidcVersion domain.OIDCVersion, - accessTokenType domain.OIDCTokenType, + appType *domain.OIDCApplicationType, + authMethodType *domain.OIDCAuthMethodType, + oidcVersion *domain.OIDCVersion, + accessTokenType *domain.OIDCTokenType, devMode, accessTokenRoleAssertion, idTokenRoleAssertion, - idTokenUserinfoAssertion bool, - clockSkew time.Duration, + idTokenUserinfoAssertion *bool, + clockSkew *time.Duration, additionalOrigins []string, - skipNativeAppSuccessPage bool, - backChannelLogoutURI string, - loginVersion domain.LoginVersion, - loginBaseURI string, + skipNativeAppSuccessPage *bool, + backChannelLogoutURI *string, + loginVersion *domain.LoginVersion, + loginBaseURI *string, ) (*project.OIDCConfigChangedEvent, bool, error) { changes := make([]project.OIDCConfigChanges, 0) var err error - if !slices.Equal(wm.RedirectUris, redirectURIS) { + if redirectURIS != nil && !slices.Equal(wm.RedirectUris, redirectURIS) { changes = append(changes, project.ChangeRedirectURIs(redirectURIS)) } - if !slices.Equal(wm.ResponseTypes, responseTypes) { + if responseTypes != nil && !slices.Equal(wm.ResponseTypes, responseTypes) { changes = append(changes, project.ChangeResponseTypes(responseTypes)) } - if !slices.Equal(wm.GrantTypes, grantTypes) { + if grantTypes != nil && !slices.Equal(wm.GrantTypes, grantTypes) { changes = append(changes, project.ChangeGrantTypes(grantTypes)) } - if wm.ApplicationType != appType { - changes = append(changes, project.ChangeApplicationType(appType)) + if appType != nil && wm.ApplicationType != *appType { + changes = append(changes, project.ChangeApplicationType(*appType)) } - if wm.AuthMethodType != authMethodType { - changes = append(changes, project.ChangeAuthMethodType(authMethodType)) + if authMethodType != nil && wm.AuthMethodType != *authMethodType { + changes = append(changes, project.ChangeAuthMethodType(*authMethodType)) } - if !slices.Equal(wm.PostLogoutRedirectUris, postLogoutRedirectURIs) { + if postLogoutRedirectURIs != nil && !slices.Equal(wm.PostLogoutRedirectUris, postLogoutRedirectURIs) { changes = append(changes, project.ChangePostLogoutRedirectURIs(postLogoutRedirectURIs)) } - if wm.OIDCVersion != oidcVersion { - changes = append(changes, project.ChangeVersion(oidcVersion)) + if oidcVersion != nil && wm.OIDCVersion != *oidcVersion { + changes = append(changes, project.ChangeVersion(*oidcVersion)) } - if wm.DevMode != devMode { - changes = append(changes, project.ChangeDevMode(devMode)) + if devMode != nil && wm.DevMode != *devMode { + changes = append(changes, project.ChangeDevMode(*devMode)) } - if wm.AccessTokenType != accessTokenType { - changes = append(changes, project.ChangeAccessTokenType(accessTokenType)) + if accessTokenType != nil && wm.AccessTokenType != *accessTokenType { + changes = append(changes, project.ChangeAccessTokenType(*accessTokenType)) } - if wm.AccessTokenRoleAssertion != accessTokenRoleAssertion { - changes = append(changes, project.ChangeAccessTokenRoleAssertion(accessTokenRoleAssertion)) + if accessTokenRoleAssertion != nil && wm.AccessTokenRoleAssertion != *accessTokenRoleAssertion { + changes = append(changes, project.ChangeAccessTokenRoleAssertion(*accessTokenRoleAssertion)) } - if wm.IDTokenRoleAssertion != idTokenRoleAssertion { - changes = append(changes, project.ChangeIDTokenRoleAssertion(idTokenRoleAssertion)) + if idTokenRoleAssertion != nil && wm.IDTokenRoleAssertion != *idTokenRoleAssertion { + changes = append(changes, project.ChangeIDTokenRoleAssertion(*idTokenRoleAssertion)) } - if wm.IDTokenUserinfoAssertion != idTokenUserinfoAssertion { - changes = append(changes, project.ChangeIDTokenUserinfoAssertion(idTokenUserinfoAssertion)) + if idTokenUserinfoAssertion != nil && wm.IDTokenUserinfoAssertion != *idTokenUserinfoAssertion { + changes = append(changes, project.ChangeIDTokenUserinfoAssertion(*idTokenUserinfoAssertion)) } - if wm.ClockSkew != clockSkew { - changes = append(changes, project.ChangeClockSkew(clockSkew)) + if clockSkew != nil && wm.ClockSkew != *clockSkew { + changes = append(changes, project.ChangeClockSkew(*clockSkew)) } - if !slices.Equal(wm.AdditionalOrigins, additionalOrigins) { + if additionalOrigins != nil && !slices.Equal(wm.AdditionalOrigins, additionalOrigins) { changes = append(changes, project.ChangeAdditionalOrigins(additionalOrigins)) } - if wm.SkipNativeAppSuccessPage != skipNativeAppSuccessPage { - changes = append(changes, project.ChangeSkipNativeAppSuccessPage(skipNativeAppSuccessPage)) + if skipNativeAppSuccessPage != nil && wm.SkipNativeAppSuccessPage != *skipNativeAppSuccessPage { + changes = append(changes, project.ChangeSkipNativeAppSuccessPage(*skipNativeAppSuccessPage)) } - if wm.BackChannelLogoutURI != backChannelLogoutURI { - changes = append(changes, project.ChangeBackChannelLogoutURI(backChannelLogoutURI)) + if backChannelLogoutURI != nil && wm.BackChannelLogoutURI != *backChannelLogoutURI { + changes = append(changes, project.ChangeBackChannelLogoutURI(*backChannelLogoutURI)) } - if wm.LoginVersion != loginVersion { - changes = append(changes, project.ChangeOIDCLoginVersion(loginVersion)) + if loginVersion != nil && wm.LoginVersion != *loginVersion { + changes = append(changes, project.ChangeOIDCLoginVersion(*loginVersion)) } - if wm.LoginBaseURI != loginBaseURI { - changes = append(changes, project.ChangeOIDCLoginBaseURI(loginBaseURI)) + if loginBaseURI != nil && wm.LoginBaseURI != *loginBaseURI { + changes = append(changes, project.ChangeOIDCLoginBaseURI(*loginBaseURI)) } if len(changes) == 0 { diff --git a/internal/command/project_application_oidc_test.go b/internal/command/project_application_oidc_test.go index d0383b1b29..d728ffca45 100644 --- a/internal/command/project_application_oidc_test.go +++ b/internal/command/project_application_oidc_test.go @@ -5,6 +5,7 @@ import ( "testing" "time" + "github.com/muhlemmer/gu" "github.com/stretchr/testify/assert" "github.com/zitadel/zitadel/internal/api/authz" @@ -401,6 +402,8 @@ func TestAddOIDCApp(t *testing.T) { } func TestCommandSide_AddOIDCApplication(t *testing.T) { + t.Parallel() + type fields struct { eventstore func(t *testing.T) *eventstore.Eventstore idGenerator id.Generator @@ -497,6 +500,7 @@ func TestCommandSide_AddOIDCApplication(t *testing.T) { domain.PrivateLabelingSettingUnspecified), ), ), + expectFilter(), expectPush( project.NewApplicationAddedEvent(context.Background(), &project.NewAggregate("project1", "org1").Aggregate, @@ -538,24 +542,24 @@ func TestCommandSide_AddOIDCApplication(t *testing.T) { AggregateID: "project1", }, AppName: "app", - AuthMethodType: domain.OIDCAuthMethodTypePost, - OIDCVersion: domain.OIDCVersionV1, + AuthMethodType: gu.Ptr(domain.OIDCAuthMethodTypePost), + OIDCVersion: gu.Ptr(domain.OIDCVersionV1), RedirectUris: []string{" https://test.ch "}, ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeCode}, GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode}, - ApplicationType: domain.OIDCApplicationTypeWeb, + ApplicationType: gu.Ptr(domain.OIDCApplicationTypeWeb), PostLogoutRedirectUris: []string{" https://test.ch/logout "}, - DevMode: true, - AccessTokenType: domain.OIDCTokenTypeBearer, - AccessTokenRoleAssertion: true, - IDTokenRoleAssertion: true, - IDTokenUserinfoAssertion: true, - ClockSkew: time.Second * 1, + DevMode: gu.Ptr(true), + AccessTokenType: gu.Ptr(domain.OIDCTokenTypeBearer), + AccessTokenRoleAssertion: gu.Ptr(true), + IDTokenRoleAssertion: gu.Ptr(true), + IDTokenUserinfoAssertion: gu.Ptr(true), + ClockSkew: gu.Ptr(time.Second * 1), AdditionalOrigins: []string{" https://sub.test.ch "}, - SkipNativeAppSuccessPage: true, - BackChannelLogoutURI: " https://test.ch/backchannel ", - LoginVersion: domain.LoginVersion2, - LoginBaseURI: " https://login.test.ch ", + SkipNativeAppSuccessPage: gu.Ptr(true), + BackChannelLogoutURI: gu.Ptr(" https://test.ch/backchannel "), + LoginVersion: gu.Ptr(domain.LoginVersion2), + LoginBaseURI: gu.Ptr(" https://login.test.ch "), }, resourceOwner: "org1", }, @@ -569,24 +573,24 @@ func TestCommandSide_AddOIDCApplication(t *testing.T) { AppName: "app", ClientID: "client1", ClientSecretString: "secret", - AuthMethodType: domain.OIDCAuthMethodTypePost, - OIDCVersion: domain.OIDCVersionV1, + AuthMethodType: gu.Ptr(domain.OIDCAuthMethodTypePost), + OIDCVersion: gu.Ptr(domain.OIDCVersionV1), RedirectUris: []string{"https://test.ch"}, ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeCode}, GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode}, - ApplicationType: domain.OIDCApplicationTypeWeb, + ApplicationType: gu.Ptr(domain.OIDCApplicationTypeWeb), PostLogoutRedirectUris: []string{"https://test.ch/logout"}, - DevMode: true, - AccessTokenType: domain.OIDCTokenTypeBearer, - AccessTokenRoleAssertion: true, - IDTokenRoleAssertion: true, - IDTokenUserinfoAssertion: true, - ClockSkew: time.Second * 1, + DevMode: gu.Ptr(true), + AccessTokenType: gu.Ptr(domain.OIDCTokenTypeBearer), + AccessTokenRoleAssertion: gu.Ptr(true), + IDTokenRoleAssertion: gu.Ptr(true), + IDTokenUserinfoAssertion: gu.Ptr(true), + ClockSkew: gu.Ptr(time.Second * 1), AdditionalOrigins: []string{"https://sub.test.ch"}, - SkipNativeAppSuccessPage: true, - BackChannelLogoutURI: "https://test.ch/backchannel", - LoginVersion: domain.LoginVersion2, - LoginBaseURI: "https://login.test.ch", + SkipNativeAppSuccessPage: gu.Ptr(true), + BackChannelLogoutURI: gu.Ptr("https://test.ch/backchannel"), + LoginVersion: gu.Ptr(domain.LoginVersion2), + LoginBaseURI: gu.Ptr("https://login.test.ch"), State: domain.AppStateActive, Compliance: &domain.Compliance{}, }, @@ -604,6 +608,7 @@ func TestCommandSide_AddOIDCApplication(t *testing.T) { domain.PrivateLabelingSettingUnspecified), ), ), + expectFilter(), expectPush( project.NewApplicationAddedEvent(context.Background(), &project.NewAggregate("project1", "org1").Aggregate, @@ -645,24 +650,24 @@ func TestCommandSide_AddOIDCApplication(t *testing.T) { AggregateID: "project1", }, AppName: "app", - AuthMethodType: domain.OIDCAuthMethodTypePost, - OIDCVersion: domain.OIDCVersionV1, + AuthMethodType: gu.Ptr(domain.OIDCAuthMethodTypePost), + OIDCVersion: gu.Ptr(domain.OIDCVersionV1), RedirectUris: []string{"https://test.ch"}, ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeCode}, GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode}, - ApplicationType: domain.OIDCApplicationTypeWeb, + ApplicationType: gu.Ptr(domain.OIDCApplicationTypeWeb), PostLogoutRedirectUris: []string{"https://test.ch/logout"}, - DevMode: true, - AccessTokenType: domain.OIDCTokenTypeBearer, - AccessTokenRoleAssertion: true, - IDTokenRoleAssertion: true, - IDTokenUserinfoAssertion: true, - ClockSkew: time.Second * 1, + DevMode: gu.Ptr(true), + AccessTokenType: gu.Ptr(domain.OIDCTokenTypeBearer), + AccessTokenRoleAssertion: gu.Ptr(true), + IDTokenRoleAssertion: gu.Ptr(true), + IDTokenUserinfoAssertion: gu.Ptr(true), + ClockSkew: gu.Ptr(time.Second * 1), AdditionalOrigins: []string{"https://sub.test.ch"}, - SkipNativeAppSuccessPage: true, - BackChannelLogoutURI: "https://test.ch/backchannel", - LoginVersion: domain.LoginVersion2, - LoginBaseURI: "https://login.test.ch", + SkipNativeAppSuccessPage: gu.Ptr(true), + BackChannelLogoutURI: gu.Ptr("https://test.ch/backchannel"), + LoginVersion: gu.Ptr(domain.LoginVersion2), + LoginBaseURI: gu.Ptr("https://login.test.ch"), }, resourceOwner: "org1", }, @@ -676,24 +681,24 @@ func TestCommandSide_AddOIDCApplication(t *testing.T) { AppName: "app", ClientID: "client1", ClientSecretString: "secret", - AuthMethodType: domain.OIDCAuthMethodTypePost, - OIDCVersion: domain.OIDCVersionV1, + AuthMethodType: gu.Ptr(domain.OIDCAuthMethodTypePost), + OIDCVersion: gu.Ptr(domain.OIDCVersionV1), RedirectUris: []string{"https://test.ch"}, ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeCode}, GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode}, - ApplicationType: domain.OIDCApplicationTypeWeb, + ApplicationType: gu.Ptr(domain.OIDCApplicationTypeWeb), PostLogoutRedirectUris: []string{"https://test.ch/logout"}, - DevMode: true, - AccessTokenType: domain.OIDCTokenTypeBearer, - AccessTokenRoleAssertion: true, - IDTokenRoleAssertion: true, - IDTokenUserinfoAssertion: true, - ClockSkew: time.Second * 1, + DevMode: gu.Ptr(true), + AccessTokenType: gu.Ptr(domain.OIDCTokenTypeBearer), + AccessTokenRoleAssertion: gu.Ptr(true), + IDTokenRoleAssertion: gu.Ptr(true), + IDTokenUserinfoAssertion: gu.Ptr(true), + ClockSkew: gu.Ptr(time.Second * 1), AdditionalOrigins: []string{"https://sub.test.ch"}, - SkipNativeAppSuccessPage: true, - BackChannelLogoutURI: "https://test.ch/backchannel", - LoginVersion: domain.LoginVersion2, - LoginBaseURI: "https://login.test.ch", + SkipNativeAppSuccessPage: gu.Ptr(true), + BackChannelLogoutURI: gu.Ptr("https://test.ch/backchannel"), + LoginVersion: gu.Ptr(domain.LoginVersion2), + LoginBaseURI: gu.Ptr("https://login.test.ch"), State: domain.AppStateActive, Compliance: &domain.Compliance{}, }, @@ -702,6 +707,7 @@ func TestCommandSide_AddOIDCApplication(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + t.Parallel() c := &Commands{ eventstore: tt.fields.eventstore(t), idGenerator: tt.fields.idGenerator, @@ -709,6 +715,7 @@ func TestCommandSide_AddOIDCApplication(t *testing.T) { defaultSecretGenerators: &SecretGenerators{ ClientSecret: emptyConfig, }, + checkPermission: newMockPermissionCheckAllowed(), } c.setMilestonesCompletedForTest("instanceID") got, err := c.AddOIDCApplication(tt.args.ctx, tt.args.oidcApp, tt.args.resourceOwner) @@ -726,6 +733,7 @@ func TestCommandSide_AddOIDCApplication(t *testing.T) { } func TestCommandSide_ChangeOIDCApplication(t *testing.T) { + t.Parallel() type fields struct { eventstore func(*testing.T) *eventstore.Eventstore } @@ -775,7 +783,7 @@ func TestCommandSide_ChangeOIDCApplication(t *testing.T) { AggregateID: "project1", }, AppID: "", - AuthMethodType: domain.OIDCAuthMethodTypePost, + AuthMethodType: gu.Ptr(domain.OIDCAuthMethodTypePost), GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode}, ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeCode}, }, @@ -797,7 +805,7 @@ func TestCommandSide_ChangeOIDCApplication(t *testing.T) { AggregateID: "", }, AppID: "appid", - AuthMethodType: domain.OIDCAuthMethodTypePost, + AuthMethodType: gu.Ptr(domain.OIDCAuthMethodTypePost), GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode}, ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeCode}, }, @@ -821,7 +829,7 @@ func TestCommandSide_ChangeOIDCApplication(t *testing.T) { AggregateID: "project1", }, AppID: "app1", - AuthMethodType: domain.OIDCAuthMethodTypePost, + AuthMethodType: gu.Ptr(domain.OIDCAuthMethodTypePost), GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode}, ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeCode}, }, @@ -870,6 +878,7 @@ func TestCommandSide_ChangeOIDCApplication(t *testing.T) { ), ), ), + expectFilter(), ), }, args: args{ @@ -880,24 +889,24 @@ func TestCommandSide_ChangeOIDCApplication(t *testing.T) { }, AppID: "app1", AppName: "app", - AuthMethodType: domain.OIDCAuthMethodTypePost, - OIDCVersion: domain.OIDCVersionV1, + AuthMethodType: gu.Ptr(domain.OIDCAuthMethodTypePost), + OIDCVersion: gu.Ptr(domain.OIDCVersionV1), RedirectUris: []string{"https://test.ch"}, ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeCode}, GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode}, - ApplicationType: domain.OIDCApplicationTypeWeb, + ApplicationType: gu.Ptr(domain.OIDCApplicationTypeWeb), PostLogoutRedirectUris: []string{"https://test.ch/logout"}, - DevMode: true, - AccessTokenType: domain.OIDCTokenTypeBearer, - AccessTokenRoleAssertion: true, - IDTokenRoleAssertion: true, - IDTokenUserinfoAssertion: true, - ClockSkew: time.Second * 1, + DevMode: gu.Ptr(true), + AccessTokenType: gu.Ptr(domain.OIDCTokenTypeBearer), + AccessTokenRoleAssertion: gu.Ptr(true), + IDTokenRoleAssertion: gu.Ptr(true), + IDTokenUserinfoAssertion: gu.Ptr(true), + ClockSkew: gu.Ptr(time.Second * 1), AdditionalOrigins: []string{"https://sub.test.ch"}, - SkipNativeAppSuccessPage: true, - BackChannelLogoutURI: "https://test.ch/backchannel", - LoginVersion: domain.LoginVersion2, - LoginBaseURI: "https://login.test.ch", + SkipNativeAppSuccessPage: gu.Ptr(true), + BackChannelLogoutURI: gu.Ptr("https://test.ch/backchannel"), + LoginVersion: gu.Ptr(domain.LoginVersion2), + LoginBaseURI: gu.Ptr("https://login.test.ch"), }, resourceOwner: "org1", }, @@ -944,6 +953,7 @@ func TestCommandSide_ChangeOIDCApplication(t *testing.T) { ), ), ), + expectFilter(), ), }, args: args{ @@ -954,24 +964,24 @@ func TestCommandSide_ChangeOIDCApplication(t *testing.T) { }, AppID: "app1", AppName: "app", - AuthMethodType: domain.OIDCAuthMethodTypePost, - OIDCVersion: domain.OIDCVersionV1, + AuthMethodType: gu.Ptr(domain.OIDCAuthMethodTypePost), + OIDCVersion: gu.Ptr(domain.OIDCVersionV1), RedirectUris: []string{"https://test.ch "}, ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeCode}, GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode}, - ApplicationType: domain.OIDCApplicationTypeWeb, + ApplicationType: gu.Ptr(domain.OIDCApplicationTypeWeb), PostLogoutRedirectUris: []string{" https://test.ch/logout"}, - DevMode: true, - AccessTokenType: domain.OIDCTokenTypeBearer, - AccessTokenRoleAssertion: true, - IDTokenRoleAssertion: true, - IDTokenUserinfoAssertion: true, - ClockSkew: time.Second * 1, + DevMode: gu.Ptr(true), + AccessTokenType: gu.Ptr(domain.OIDCTokenTypeBearer), + AccessTokenRoleAssertion: gu.Ptr(true), + IDTokenRoleAssertion: gu.Ptr(true), + IDTokenUserinfoAssertion: gu.Ptr(true), + ClockSkew: gu.Ptr(time.Second * 1), AdditionalOrigins: []string{" https://sub.test.ch "}, - SkipNativeAppSuccessPage: true, - BackChannelLogoutURI: " https://test.ch/backchannel ", - LoginVersion: domain.LoginVersion2, - LoginBaseURI: " https://login.test.ch ", + SkipNativeAppSuccessPage: gu.Ptr(true), + BackChannelLogoutURI: gu.Ptr(" https://test.ch/backchannel "), + LoginVersion: gu.Ptr(domain.LoginVersion2), + LoginBaseURI: gu.Ptr(" https://login.test.ch "), }, resourceOwner: "org1", }, @@ -980,7 +990,7 @@ func TestCommandSide_ChangeOIDCApplication(t *testing.T) { }, }, { - name: "change oidc app, ok", + name: "partial change oidc app, ok", fields: fields{ eventstore: expectEventstore( expectFilter( @@ -1018,6 +1028,7 @@ func TestCommandSide_ChangeOIDCApplication(t *testing.T) { ), ), ), + expectFilter(), expectPush( newOIDCAppChangedEvent(context.Background(), "app1", @@ -1032,26 +1043,11 @@ func TestCommandSide_ChangeOIDCApplication(t *testing.T) { ObjectRoot: models.ObjectRoot{ AggregateID: "project1", }, - AppID: "app1", - AppName: "app", - AuthMethodType: domain.OIDCAuthMethodTypePost, - OIDCVersion: domain.OIDCVersionV1, - RedirectUris: []string{" https://test-change.ch "}, - ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeCode}, - GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode}, - ApplicationType: domain.OIDCApplicationTypeWeb, - PostLogoutRedirectUris: []string{" https://test-change.ch/logout "}, - DevMode: true, - AccessTokenType: domain.OIDCTokenTypeJWT, - AccessTokenRoleAssertion: false, - IDTokenRoleAssertion: false, - IDTokenUserinfoAssertion: false, - ClockSkew: time.Second * 2, - AdditionalOrigins: []string{"https://sub.test.ch"}, - SkipNativeAppSuccessPage: true, - BackChannelLogoutURI: "https://test.ch/backchannel", - LoginVersion: domain.LoginVersion2, - LoginBaseURI: "https://login.test.ch", + AppID: "app1", + AppName: "app", + AuthMethodType: gu.Ptr(domain.OIDCAuthMethodTypeBasic), + GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode}, + ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeCode}, }, resourceOwner: "org1", }, @@ -1064,24 +1060,24 @@ func TestCommandSide_ChangeOIDCApplication(t *testing.T) { AppID: "app1", ClientID: "client1@project", AppName: "app", - AuthMethodType: domain.OIDCAuthMethodTypePost, - OIDCVersion: domain.OIDCVersionV1, - RedirectUris: []string{"https://test-change.ch"}, + AuthMethodType: gu.Ptr(domain.OIDCAuthMethodTypeBasic), + OIDCVersion: gu.Ptr(domain.OIDCVersionV1), + RedirectUris: []string{"https://test.ch"}, ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeCode}, GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode}, - ApplicationType: domain.OIDCApplicationTypeWeb, - PostLogoutRedirectUris: []string{"https://test-change.ch/logout"}, - DevMode: true, - AccessTokenType: domain.OIDCTokenTypeJWT, - AccessTokenRoleAssertion: false, - IDTokenRoleAssertion: false, - IDTokenUserinfoAssertion: false, - ClockSkew: time.Second * 2, + ApplicationType: gu.Ptr(domain.OIDCApplicationTypeWeb), + PostLogoutRedirectUris: []string{"https://test.ch/logout"}, + DevMode: gu.Ptr(false), + AccessTokenType: gu.Ptr(domain.OIDCTokenTypeBearer), + AccessTokenRoleAssertion: gu.Ptr(true), + IDTokenRoleAssertion: gu.Ptr(true), + IDTokenUserinfoAssertion: gu.Ptr(true), + ClockSkew: gu.Ptr(time.Second * 1), AdditionalOrigins: []string{"https://sub.test.ch"}, - SkipNativeAppSuccessPage: true, - BackChannelLogoutURI: "https://test.ch/backchannel", - LoginVersion: domain.LoginVersion2, - LoginBaseURI: "https://login.test.ch", + SkipNativeAppSuccessPage: gu.Ptr(true), + BackChannelLogoutURI: gu.Ptr("https://test.ch/backchannel"), + LoginVersion: gu.Ptr(domain.LoginVersion1), + LoginBaseURI: gu.Ptr(""), Compliance: &domain.Compliance{}, State: domain.AppStateActive, }, @@ -1090,10 +1086,12 @@ func TestCommandSide_ChangeOIDCApplication(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + // t.Parallel() r := &Commands{ - eventstore: tt.fields.eventstore(t), + eventstore: tt.fields.eventstore(t), + checkPermission: newMockPermissionCheckAllowed(), } - got, err := r.ChangeOIDCApplication(tt.args.ctx, tt.args.oidcApp, tt.args.resourceOwner) + got, err := r.UpdateOIDCApplication(tt.args.ctx, tt.args.oidcApp, tt.args.resourceOwner) if tt.res.err == nil { assert.NoError(t, err) } @@ -1108,6 +1106,8 @@ func TestCommandSide_ChangeOIDCApplication(t *testing.T) { } func TestCommandSide_ChangeOIDCApplicationSecret(t *testing.T) { + t.Parallel() + type fields struct { eventstore func(*testing.T) *eventstore.Eventstore } @@ -1237,36 +1237,40 @@ func TestCommandSide_ChangeOIDCApplicationSecret(t *testing.T) { AppName: "app", ClientID: "client1@project", ClientSecretString: "secret", - AuthMethodType: domain.OIDCAuthMethodTypePost, - OIDCVersion: domain.OIDCVersionV1, + AuthMethodType: gu.Ptr(domain.OIDCAuthMethodTypePost), + OIDCVersion: gu.Ptr(domain.OIDCVersionV1), RedirectUris: []string{"https://test.ch"}, ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeCode}, GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode}, - ApplicationType: domain.OIDCApplicationTypeWeb, + ApplicationType: gu.Ptr(domain.OIDCApplicationTypeWeb), PostLogoutRedirectUris: []string{"https://test.ch/logout"}, - DevMode: true, - AccessTokenType: domain.OIDCTokenTypeBearer, - AccessTokenRoleAssertion: true, - IDTokenRoleAssertion: true, - IDTokenUserinfoAssertion: true, - ClockSkew: time.Second * 1, + DevMode: gu.Ptr(true), + AccessTokenType: gu.Ptr(domain.OIDCTokenTypeBearer), + AccessTokenRoleAssertion: gu.Ptr(true), + IDTokenRoleAssertion: gu.Ptr(true), + IDTokenUserinfoAssertion: gu.Ptr(true), + ClockSkew: gu.Ptr(time.Second * 1), AdditionalOrigins: []string{"https://sub.test.ch"}, - SkipNativeAppSuccessPage: false, - BackChannelLogoutURI: "", - LoginVersion: domain.LoginVersionUnspecified, + SkipNativeAppSuccessPage: gu.Ptr(false), + BackChannelLogoutURI: gu.Ptr(""), + LoginVersion: gu.Ptr(domain.LoginVersionUnspecified), + LoginBaseURI: gu.Ptr(""), State: domain.AppStateActive, }, }, }, } for _, tt := range tests { - t.Run(tt.name, func(*testing.T) { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + r := &Commands{ eventstore: tt.fields.eventstore(t), newHashedSecret: mockHashedSecret("secret"), defaultSecretGenerators: &SecretGenerators{ ClientSecret: emptyConfig, }, + checkPermission: newMockPermissionCheckAllowed(), } got, err := r.ChangeOIDCApplicationSecret(tt.args.ctx, tt.args.projectID, tt.args.appID, tt.args.resourceOwner) if tt.res.err == nil { @@ -1284,16 +1288,7 @@ func TestCommandSide_ChangeOIDCApplicationSecret(t *testing.T) { func newOIDCAppChangedEvent(ctx context.Context, appID, projectID, resourceOwner string) *project.OIDCConfigChangedEvent { changes := []project.OIDCConfigChanges{ - project.ChangeRedirectURIs([]string{"https://test-change.ch"}), - project.ChangePostLogoutRedirectURIs([]string{"https://test-change.ch/logout"}), - project.ChangeDevMode(true), - project.ChangeAccessTokenType(domain.OIDCTokenTypeJWT), - project.ChangeAccessTokenRoleAssertion(false), - project.ChangeIDTokenRoleAssertion(false), - project.ChangeIDTokenUserinfoAssertion(false), - project.ChangeClockSkew(time.Second * 2), - project.ChangeOIDCLoginVersion(domain.LoginVersion2), - project.ChangeOIDCLoginBaseURI("https://login.test.ch"), + project.ChangeAuthMethodType(domain.OIDCAuthMethodTypeBasic), } event, _ := project.NewOIDCConfigChangedEvent(ctx, &project.NewAggregate(projectID, resourceOwner).Aggregate, diff --git a/internal/command/project_application_saml.go b/internal/command/project_application_saml.go index 1a5cefa221..9b1dc9e97a 100644 --- a/internal/command/project_application_saml.go +++ b/internal/command/project_application_saml.go @@ -3,6 +3,7 @@ package command import ( "context" + "github.com/muhlemmer/gu" "github.com/zitadel/saml/pkg/provider/xml" "github.com/zitadel/zitadel/internal/domain" @@ -16,10 +17,22 @@ func (c *Commands) AddSAMLApplication(ctx context.Context, application *domain.S return nil, zerrors.ThrowInvalidArgument(nil, "PROJECT-35Fn0", "Errors.Project.App.Invalid") } - if _, err := c.checkProjectExists(ctx, application.AggregateID, resourceOwner); err != nil { + projectResOwner, err := c.checkProjectExists(ctx, application.AggregateID, resourceOwner) + if err != nil { return nil, err } + if resourceOwner == "" { + resourceOwner = projectResOwner + } + addedApplication := NewSAMLApplicationWriteModel(application.AggregateID, resourceOwner) + if err := c.eventstore.FilterToQueryReducer(ctx, addedApplication); err != nil { + return nil, err + } + if err := c.checkPermissionUpdateApplication(ctx, addedApplication.ResourceOwner, addedApplication.AggregateID); err != nil { + return nil, err + } + projectAgg := ProjectAggregateFromWriteModel(&addedApplication.WriteModel) events, err := c.addSAMLApplication(ctx, projectAgg, application) if err != nil { @@ -49,12 +62,8 @@ func (c *Commands) addSAMLApplication(ctx context.Context, projectAgg *eventstor return nil, zerrors.ThrowInvalidArgument(nil, "PROJECT-1n9df", "Errors.Project.App.Invalid") } - if samlApp.Metadata == nil && samlApp.MetadataURL == "" { - return nil, zerrors.ThrowInvalidArgument(nil, "SAML-podix9", "Errors.Project.App.SAMLMetadataMissing") - } - - if samlApp.MetadataURL != "" { - data, err := xml.ReadMetadataFromURL(c.httpClient, samlApp.MetadataURL) + if samlApp.MetadataURL != nil && *samlApp.MetadataURL != "" { + data, err := xml.ReadMetadataFromURL(c.httpClient, *samlApp.MetadataURL) if err != nil { return nil, zerrors.ThrowInvalidArgument(err, "SAML-wmqlo1", "Errors.Project.App.SAMLMetadataMissing") } @@ -78,14 +87,14 @@ func (c *Commands) addSAMLApplication(ctx context.Context, projectAgg *eventstor samlApp.AppID, string(entity.EntityID), samlApp.Metadata, - samlApp.MetadataURL, - samlApp.LoginVersion, - samlApp.LoginBaseURI, + gu.Value(samlApp.MetadataURL), + gu.Value(samlApp.LoginVersion), + gu.Value(samlApp.LoginBaseURI), ), }, nil } -func (c *Commands) ChangeSAMLApplication(ctx context.Context, samlApp *domain.SAMLApp, resourceOwner string) (*domain.SAMLApp, error) { +func (c *Commands) UpdateSAMLApplication(ctx context.Context, samlApp *domain.SAMLApp, resourceOwner string) (*domain.SAMLApp, error) { if !samlApp.IsValid() || samlApp.AppID == "" || samlApp.AggregateID == "" { return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-5n9fs", "Errors.Project.App.SAMLConfigInvalid") } @@ -100,10 +109,15 @@ func (c *Commands) ChangeSAMLApplication(ctx context.Context, samlApp *domain.SA if !existingSAML.IsSAML() { return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-GBr35", "Errors.Project.App.IsNotSAML") } + + if err := c.checkPermissionUpdateApplication(ctx, existingSAML.ResourceOwner, existingSAML.AggregateID); err != nil { + return nil, err + } + projectAgg := ProjectAggregateFromWriteModel(&existingSAML.WriteModel) - if samlApp.MetadataURL != "" { - data, err := xml.ReadMetadataFromURL(c.httpClient, samlApp.MetadataURL) + if samlApp.MetadataURL != nil && *samlApp.MetadataURL != "" { + data, err := xml.ReadMetadataFromURL(c.httpClient, *samlApp.MetadataURL) if err != nil { return nil, zerrors.ThrowInvalidArgument(err, "SAML-J3kg3", "Errors.Project.App.SAMLMetadataMissing") } diff --git a/internal/command/project_application_saml_model.go b/internal/command/project_application_saml_model.go index f219039b58..f3097914f3 100644 --- a/internal/command/project_application_saml_model.go +++ b/internal/command/project_application_saml_model.go @@ -2,7 +2,7 @@ package command import ( "context" - "reflect" + "slices" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" @@ -170,26 +170,26 @@ func (wm *SAMLApplicationWriteModel) NewChangedEvent( appID string, entityID string, metadata []byte, - metadataURL string, - loginVersion domain.LoginVersion, - loginBaseURI string, + metadataURL *string, + loginVersion *domain.LoginVersion, + loginBaseURI *string, ) (*project.SAMLConfigChangedEvent, bool, error) { changes := make([]project.SAMLConfigChanges, 0) var err error - if !reflect.DeepEqual(wm.Metadata, metadata) { + if metadata != nil && !slices.Equal(wm.Metadata, metadata) { changes = append(changes, project.ChangeMetadata(metadata)) } - if wm.MetadataURL != metadataURL { - changes = append(changes, project.ChangeMetadataURL(metadataURL)) + if metadataURL != nil && wm.MetadataURL != *metadataURL { + changes = append(changes, project.ChangeMetadataURL(*metadataURL)) } if wm.EntityID != entityID { changes = append(changes, project.ChangeEntityID(entityID)) } - if wm.LoginVersion != loginVersion { - changes = append(changes, project.ChangeSAMLLoginVersion(loginVersion)) + if loginVersion != nil && wm.LoginVersion != *loginVersion { + changes = append(changes, project.ChangeSAMLLoginVersion(*loginVersion)) } - if wm.LoginBaseURI != loginBaseURI { - changes = append(changes, project.ChangeSAMLLoginBaseURI(loginBaseURI)) + if loginBaseURI != nil && wm.LoginBaseURI != *loginBaseURI { + changes = append(changes, project.ChangeSAMLLoginBaseURI(*loginBaseURI)) } if len(changes) == 0 { diff --git a/internal/command/project_application_saml_test.go b/internal/command/project_application_saml_test.go index c6f6f7cf21..5d18d9587c 100644 --- a/internal/command/project_application_saml_test.go +++ b/internal/command/project_application_saml_test.go @@ -7,6 +7,7 @@ import ( "net/http" "testing" + "github.com/muhlemmer/gu" "github.com/stretchr/testify/assert" "github.com/zitadel/zitadel/internal/api/authz" @@ -49,6 +50,8 @@ var testMetadataChangedEntityID = []byte(` `) func TestCommandSide_AddSAMLApplication(t *testing.T) { + t.Parallel() + type fields struct { eventstore func(t *testing.T) *eventstore.Eventstore idGenerator id.Generator @@ -117,6 +120,7 @@ func TestCommandSide_AddSAMLApplication(t *testing.T) { domain.PrivateLabelingSettingUnspecified), ), ), + expectFilter(), ), }, args: args{ @@ -134,6 +138,37 @@ func TestCommandSide_AddSAMLApplication(t *testing.T) { err: zerrors.IsErrorInvalidArgument, }, }, + { + name: "empty metas, invalid argument error", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "project", true, true, true, + domain.PrivateLabelingSettingUnspecified), + ), + ), + expectFilter(), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t), + }, + args: args{ + ctx: authz.WithInstanceID(context.Background(), "instanceID"), + samlApp: &domain.SAMLApp{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + }, + AppName: "app", + EntityID: "https://test.com/saml/metadata", + }, + resourceOwner: "org1", + }, + res: res{ + err: zerrors.IsErrorInvalidArgument, + }, + }, { name: "create saml app, metadata not parsable", fields: fields{ @@ -146,6 +181,7 @@ func TestCommandSide_AddSAMLApplication(t *testing.T) { domain.PrivateLabelingSettingUnspecified), ), ), + expectFilter(), ), idGenerator: id_mock.NewIDGeneratorExpectIDs(t), }, @@ -158,7 +194,7 @@ func TestCommandSide_AddSAMLApplication(t *testing.T) { AppName: "app", EntityID: "https://test.com/saml/metadata", Metadata: []byte("test metadata"), - MetadataURL: "", + MetadataURL: gu.Ptr(""), }, resourceOwner: "org1", }, @@ -178,6 +214,7 @@ func TestCommandSide_AddSAMLApplication(t *testing.T) { domain.PrivateLabelingSettingUnspecified), ), ), + expectFilter(), expectPush( project.NewApplicationAddedEvent(context.Background(), &project.NewAggregate("project1", "org1").Aggregate, @@ -206,7 +243,7 @@ func TestCommandSide_AddSAMLApplication(t *testing.T) { AppName: "app", EntityID: "https://test.com/saml/metadata", Metadata: testMetadata, - MetadataURL: "", + MetadataURL: gu.Ptr(""), }, resourceOwner: "org1", }, @@ -216,12 +253,14 @@ func TestCommandSide_AddSAMLApplication(t *testing.T) { AggregateID: "project1", ResourceOwner: "org1", }, - AppID: "app1", - AppName: "app", - EntityID: "https://test.com/saml/metadata", - Metadata: testMetadata, - MetadataURL: "", - State: domain.AppStateActive, + AppID: "app1", + AppName: "app", + EntityID: "https://test.com/saml/metadata", + Metadata: testMetadata, + MetadataURL: gu.Ptr(""), + State: domain.AppStateActive, + LoginVersion: gu.Ptr(domain.LoginVersionUnspecified), + LoginBaseURI: gu.Ptr(""), }, }, }, @@ -237,6 +276,7 @@ func TestCommandSide_AddSAMLApplication(t *testing.T) { domain.PrivateLabelingSettingUnspecified), ), ), + expectFilter(), expectPush( project.NewApplicationAddedEvent(context.Background(), &project.NewAggregate("project1", "org1").Aggregate, @@ -265,9 +305,9 @@ func TestCommandSide_AddSAMLApplication(t *testing.T) { AppName: "app", EntityID: "https://test.com/saml/metadata", Metadata: testMetadata, - MetadataURL: "", - LoginVersion: domain.LoginVersion2, - LoginBaseURI: "https://test.com/login", + MetadataURL: gu.Ptr(""), + LoginVersion: gu.Ptr(domain.LoginVersion2), + LoginBaseURI: gu.Ptr("https://test.com/login"), }, resourceOwner: "org1", }, @@ -281,10 +321,10 @@ func TestCommandSide_AddSAMLApplication(t *testing.T) { AppName: "app", EntityID: "https://test.com/saml/metadata", Metadata: testMetadata, - MetadataURL: "", + MetadataURL: gu.Ptr(""), State: domain.AppStateActive, - LoginVersion: domain.LoginVersion2, - LoginBaseURI: "https://test.com/login", + LoginVersion: gu.Ptr(domain.LoginVersion2), + LoginBaseURI: gu.Ptr("https://test.com/login"), }, }, }, @@ -300,6 +340,7 @@ func TestCommandSide_AddSAMLApplication(t *testing.T) { domain.PrivateLabelingSettingUnspecified), ), ), + expectFilter(), expectPush( project.NewApplicationAddedEvent(context.Background(), &project.NewAggregate("project1", "org1").Aggregate, @@ -329,7 +370,7 @@ func TestCommandSide_AddSAMLApplication(t *testing.T) { AppName: "app", EntityID: "https://test.com/saml/metadata", Metadata: nil, - MetadataURL: "http://localhost:8080/saml/metadata", + MetadataURL: gu.Ptr("http://localhost:8080/saml/metadata"), }, resourceOwner: "org1", }, @@ -339,12 +380,14 @@ func TestCommandSide_AddSAMLApplication(t *testing.T) { AggregateID: "project1", ResourceOwner: "org1", }, - AppID: "app1", - AppName: "app", - EntityID: "https://test.com/saml/metadata", - Metadata: testMetadata, - MetadataURL: "http://localhost:8080/saml/metadata", - State: domain.AppStateActive, + AppID: "app1", + AppName: "app", + EntityID: "https://test.com/saml/metadata", + Metadata: testMetadata, + MetadataURL: gu.Ptr("http://localhost:8080/saml/metadata"), + State: domain.AppStateActive, + LoginVersion: gu.Ptr(domain.LoginVersionUnspecified), + LoginBaseURI: gu.Ptr(""), }, }, }, @@ -360,6 +403,7 @@ func TestCommandSide_AddSAMLApplication(t *testing.T) { domain.PrivateLabelingSettingUnspecified), ), ), + expectFilter(), ), idGenerator: id_mock.NewIDGeneratorExpectIDs(t), httpClient: newTestClient(http.StatusNotFound, nil), @@ -373,7 +417,7 @@ func TestCommandSide_AddSAMLApplication(t *testing.T) { AppName: "app", EntityID: "https://test.com/saml/metadata", Metadata: nil, - MetadataURL: "http://localhost:8080/saml/metadata", + MetadataURL: gu.Ptr("http://localhost:8080/saml/metadata"), }, resourceOwner: "org1", }, @@ -385,10 +429,13 @@ func TestCommandSide_AddSAMLApplication(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + t.Parallel() + c := &Commands{ - eventstore: tt.fields.eventstore(t), - idGenerator: tt.fields.idGenerator, - httpClient: tt.fields.httpClient, + eventstore: tt.fields.eventstore(t), + idGenerator: tt.fields.idGenerator, + httpClient: tt.fields.httpClient, + checkPermission: newMockPermissionCheckAllowed(), } c.setMilestonesCompletedForTest("instanceID") got, err := c.AddSAMLApplication(tt.args.ctx, tt.args.samlApp, tt.args.resourceOwner) @@ -406,6 +453,8 @@ func TestCommandSide_AddSAMLApplication(t *testing.T) { } func TestCommandSide_ChangeSAMLApplication(t *testing.T) { + t.Parallel() + type fields struct { eventstore func(t *testing.T) *eventstore.Eventstore httpClient *http.Client @@ -544,7 +593,7 @@ func TestCommandSide_ChangeSAMLApplication(t *testing.T) { AppID: "app1", EntityID: "https://test.com/saml/metadata", Metadata: nil, - MetadataURL: "http://localhost:8080/saml/metadata", + MetadataURL: gu.Ptr("http://localhost:8080/saml/metadata"), }, resourceOwner: "org1", }, @@ -590,7 +639,7 @@ func TestCommandSide_ChangeSAMLApplication(t *testing.T) { AppID: "app1", EntityID: "https://test.com/saml/metadata", Metadata: testMetadata, - MetadataURL: "", + MetadataURL: gu.Ptr(""), }, resourceOwner: "org1", }, @@ -646,7 +695,7 @@ func TestCommandSide_ChangeSAMLApplication(t *testing.T) { AppName: "app", EntityID: "https://test2.com/saml/metadata", Metadata: nil, - MetadataURL: "http://localhost:8080/saml/metadata", + MetadataURL: gu.Ptr("http://localhost:8080/saml/metadata"), }, resourceOwner: "org1", }, @@ -656,17 +705,19 @@ func TestCommandSide_ChangeSAMLApplication(t *testing.T) { AggregateID: "project1", ResourceOwner: "org1", }, - AppID: "app1", - AppName: "app", - EntityID: "https://test2.com/saml/metadata", - Metadata: testMetadataChangedEntityID, - MetadataURL: "http://localhost:8080/saml/metadata", - State: domain.AppStateActive, + AppID: "app1", + AppName: "app", + EntityID: "https://test2.com/saml/metadata", + Metadata: testMetadataChangedEntityID, + MetadataURL: gu.Ptr("http://localhost:8080/saml/metadata"), + State: domain.AppStateActive, + LoginVersion: gu.Ptr(domain.LoginVersionUnspecified), + LoginBaseURI: gu.Ptr(""), }, }, }, { - name: "change saml app, ok, metadata", + name: "partial change saml app, ok, metadata", fields: fields{ eventstore: expectEventstore( expectFilter( @@ -713,7 +764,7 @@ func TestCommandSide_ChangeSAMLApplication(t *testing.T) { AppName: "app", EntityID: "https://test2.com/saml/metadata", Metadata: testMetadataChangedEntityID, - MetadataURL: "", + MetadataURL: gu.Ptr(""), }, resourceOwner: "org1", }, @@ -723,15 +774,18 @@ func TestCommandSide_ChangeSAMLApplication(t *testing.T) { AggregateID: "project1", ResourceOwner: "org1", }, - AppID: "app1", - AppName: "app", - EntityID: "https://test2.com/saml/metadata", - Metadata: testMetadataChangedEntityID, - MetadataURL: "", - State: domain.AppStateActive, + AppID: "app1", + AppName: "app", + EntityID: "https://test2.com/saml/metadata", + Metadata: testMetadataChangedEntityID, + MetadataURL: gu.Ptr(""), + State: domain.AppStateActive, + LoginVersion: gu.Ptr(domain.LoginVersionUnspecified), + LoginBaseURI: gu.Ptr(""), }, }, - }, { + }, + { name: "change saml app, ok, loginversion", fields: fields{ eventstore: expectEventstore( @@ -781,9 +835,9 @@ func TestCommandSide_ChangeSAMLApplication(t *testing.T) { AppName: "app", EntityID: "https://test2.com/saml/metadata", Metadata: testMetadataChangedEntityID, - MetadataURL: "", - LoginVersion: domain.LoginVersion2, - LoginBaseURI: "https://test.com/login", + MetadataURL: gu.Ptr(""), + LoginVersion: gu.Ptr(domain.LoginVersion2), + LoginBaseURI: gu.Ptr("https://test.com/login"), }, resourceOwner: "org1", }, @@ -797,10 +851,10 @@ func TestCommandSide_ChangeSAMLApplication(t *testing.T) { AppName: "app", EntityID: "https://test2.com/saml/metadata", Metadata: testMetadataChangedEntityID, - MetadataURL: "", + MetadataURL: gu.Ptr(""), State: domain.AppStateActive, - LoginVersion: domain.LoginVersion2, - LoginBaseURI: "https://test.com/login", + LoginVersion: gu.Ptr(domain.LoginVersion2), + LoginBaseURI: gu.Ptr("https://test.com/login"), }, }, }, @@ -808,11 +862,14 @@ func TestCommandSide_ChangeSAMLApplication(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + t.Parallel() + r := &Commands{ - eventstore: tt.fields.eventstore(t), - httpClient: tt.fields.httpClient, + eventstore: tt.fields.eventstore(t), + httpClient: tt.fields.httpClient, + checkPermission: newMockPermissionCheckAllowed(), } - got, err := r.ChangeSAMLApplication(tt.args.ctx, tt.args.samlApp, tt.args.resourceOwner) + got, err := r.UpdateSAMLApplication(tt.args.ctx, tt.args.samlApp, tt.args.resourceOwner) if tt.res.err == nil { assert.NoError(t, err) } diff --git a/internal/command/project_application_test.go b/internal/command/project_application_test.go index 050a41d29f..a67e6886ed 100644 --- a/internal/command/project_application_test.go +++ b/internal/command/project_application_test.go @@ -8,13 +8,16 @@ import ( "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/eventstore/repository/mock" "github.com/zitadel/zitadel/internal/repository/project" "github.com/zitadel/zitadel/internal/zerrors" ) func TestCommandSide_ChangeApplication(t *testing.T) { + t.Parallel() + type fields struct { - eventstore *eventstore.Eventstore + eventstore func(*testing.T) *eventstore.Eventstore } type args struct { ctx context.Context @@ -35,9 +38,7 @@ func TestCommandSide_ChangeApplication(t *testing.T) { { name: "invalid app missing projectid, invalid argument error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(func(mockRepository *mock.MockRepository) {}), }, args: args{ ctx: context.Background(), @@ -55,9 +56,7 @@ func TestCommandSide_ChangeApplication(t *testing.T) { { name: "invalid app missing appid, invalid argument error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(func(mockRepository *mock.MockRepository) {}), }, args: args{ ctx: context.Background(), @@ -74,9 +73,7 @@ func TestCommandSide_ChangeApplication(t *testing.T) { { name: "invalid app missing name, invalid argument error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(func(mockRepository *mock.MockRepository) {}), }, args: args{ ctx: context.Background(), @@ -94,10 +91,7 @@ func TestCommandSide_ChangeApplication(t *testing.T) { { name: "app not existing, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, - expectFilter(), - ), + eventstore: expectEventstore(expectFilter()), }, args: args{ ctx: context.Background(), @@ -115,8 +109,7 @@ func TestCommandSide_ChangeApplication(t *testing.T) { { name: "app name not changed, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher(project.NewApplicationAddedEvent(context.Background(), &project.NewAggregate("project1", "org1").Aggregate, @@ -142,8 +135,14 @@ func TestCommandSide_ChangeApplication(t *testing.T) { { name: "app changed, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher(project.NewApplicationAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "app1", + "app", + )), + ), expectFilter( eventFromEventPusher(project.NewApplicationAddedEvent(context.Background(), &project.NewAggregate("project1", "org1").Aggregate, @@ -179,10 +178,13 @@ func TestCommandSide_ChangeApplication(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + t.Parallel() + r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), + checkPermission: newMockPermissionCheckAllowed(), } - got, err := r.ChangeApplication(tt.args.ctx, tt.args.projectID, tt.args.app, tt.args.resourceOwner) + got, err := r.UpdateApplicationName(tt.args.ctx, tt.args.projectID, tt.args.app, tt.args.resourceOwner) if tt.res.err == nil { assert.NoError(t, err) } @@ -197,8 +199,10 @@ func TestCommandSide_ChangeApplication(t *testing.T) { } func TestCommandSide_DeactivateApplication(t *testing.T) { + t.Parallel() + type fields struct { - eventstore *eventstore.Eventstore + eventstore func(*testing.T) *eventstore.Eventstore } type args struct { ctx context.Context @@ -219,9 +223,7 @@ func TestCommandSide_DeactivateApplication(t *testing.T) { { name: "missing projectid, invalid argument error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(func(mockRepository *mock.MockRepository) {}), }, args: args{ ctx: context.Background(), @@ -236,9 +238,7 @@ func TestCommandSide_DeactivateApplication(t *testing.T) { { name: "missing appid, invalid argument error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(func(mockRepository *mock.MockRepository) {}), }, args: args{ ctx: context.Background(), @@ -253,8 +253,7 @@ func TestCommandSide_DeactivateApplication(t *testing.T) { { name: "app not existing, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), }, @@ -271,8 +270,7 @@ func TestCommandSide_DeactivateApplication(t *testing.T) { { name: "app already inactive, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher(project.NewApplicationAddedEvent(context.Background(), &project.NewAggregate("project1", "org1").Aggregate, @@ -299,8 +297,14 @@ func TestCommandSide_DeactivateApplication(t *testing.T) { { name: "app deactivate, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher(project.NewApplicationAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "app1", + "app", + )), + ), expectFilter( eventFromEventPusher(project.NewApplicationAddedEvent(context.Background(), &project.NewAggregate("project1", "org1").Aggregate, @@ -331,8 +335,11 @@ func TestCommandSide_DeactivateApplication(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + t.Parallel() + r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), + checkPermission: newMockPermissionCheckAllowed(), } got, err := r.DeactivateApplication(tt.args.ctx, tt.args.projectID, tt.args.appID, tt.args.resourceOwner) if tt.res.err == nil { @@ -349,8 +356,10 @@ func TestCommandSide_DeactivateApplication(t *testing.T) { } func TestCommandSide_ReactivateApplication(t *testing.T) { + t.Parallel() + type fields struct { - eventstore *eventstore.Eventstore + eventstore func(*testing.T) *eventstore.Eventstore } type args struct { ctx context.Context @@ -371,9 +380,7 @@ func TestCommandSide_ReactivateApplication(t *testing.T) { { name: "missing projectid, invalid argument error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(func(mockRepository *mock.MockRepository) {}), }, args: args{ ctx: context.Background(), @@ -388,9 +395,7 @@ func TestCommandSide_ReactivateApplication(t *testing.T) { { name: "missing appid, invalid argument error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(func(mockRepository *mock.MockRepository) {}), }, args: args{ ctx: context.Background(), @@ -405,10 +410,7 @@ func TestCommandSide_ReactivateApplication(t *testing.T) { { name: "app not existing, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, - expectFilter(), - ), + eventstore: expectEventstore(expectFilter()), }, args: args{ ctx: context.Background(), @@ -423,8 +425,7 @@ func TestCommandSide_ReactivateApplication(t *testing.T) { { name: "app already active, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher(project.NewApplicationAddedEvent(context.Background(), &project.NewAggregate("project1", "org1").Aggregate, @@ -447,8 +448,7 @@ func TestCommandSide_ReactivateApplication(t *testing.T) { { name: "app reactivate, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher(project.NewApplicationAddedEvent(context.Background(), &project.NewAggregate("project1", "org1").Aggregate, @@ -483,8 +483,11 @@ func TestCommandSide_ReactivateApplication(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + t.Parallel() + r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), + checkPermission: newMockPermissionCheckAllowed(), } got, err := r.ReactivateApplication(tt.args.ctx, tt.args.projectID, tt.args.appID, tt.args.resourceOwner) if tt.res.err == nil { @@ -501,8 +504,10 @@ func TestCommandSide_ReactivateApplication(t *testing.T) { } func TestCommandSide_RemoveApplication(t *testing.T) { + t.Parallel() + type fields struct { - eventstore *eventstore.Eventstore + eventstore func(*testing.T) *eventstore.Eventstore } type args struct { ctx context.Context @@ -523,9 +528,7 @@ func TestCommandSide_RemoveApplication(t *testing.T) { { name: "missing projectid, invalid argument error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(func(mockRepository *mock.MockRepository) {}), }, args: args{ ctx: context.Background(), @@ -540,9 +543,7 @@ func TestCommandSide_RemoveApplication(t *testing.T) { { name: "missing appid, invalid argument error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(func(mockRepository *mock.MockRepository) {}), }, args: args{ ctx: context.Background(), @@ -557,10 +558,7 @@ func TestCommandSide_RemoveApplication(t *testing.T) { { name: "app not existing, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, - expectFilter(), - ), + eventstore: expectEventstore(expectFilter()), }, args: args{ ctx: context.Background(), @@ -575,8 +573,7 @@ func TestCommandSide_RemoveApplication(t *testing.T) { { name: "app remove, entityID, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher(project.NewApplicationAddedEvent(context.Background(), &project.NewAggregate("project1", "org1").Aggregate, @@ -584,6 +581,7 @@ func TestCommandSide_RemoveApplication(t *testing.T) { "app", )), ), + expectFilter(), expectFilter( eventFromEventPusher(project.NewApplicationAddedEvent(context.Background(), &project.NewAggregate("project1", "org1").Aggregate, @@ -625,8 +623,7 @@ func TestCommandSide_RemoveApplication(t *testing.T) { { name: "app remove, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher(project.NewApplicationAddedEvent(context.Background(), &project.NewAggregate("project1", "org1").Aggregate, @@ -636,6 +633,7 @@ func TestCommandSide_RemoveApplication(t *testing.T) { ), // app is not saml, or no saml config available expectFilter(), + expectFilter(), expectPush( project.NewApplicationRemovedEvent(context.Background(), &project.NewAggregate("project1", "org1").Aggregate, @@ -661,8 +659,11 @@ func TestCommandSide_RemoveApplication(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + t.Parallel() + r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), + checkPermission: newMockPermissionCheckAllowed(), } got, err := r.RemoveApplication(tt.args.ctx, tt.args.projectID, tt.args.appID, tt.args.resourceOwner) if tt.res.err == nil { diff --git a/internal/command/project_converter.go b/internal/command/project_converter.go index 01b5a4e63d..e88a1cb75a 100644 --- a/internal/command/project_converter.go +++ b/internal/command/project_converter.go @@ -1,6 +1,8 @@ package command import ( + "github.com/muhlemmer/gu" + "github.com/zitadel/zitadel/internal/domain" ) @@ -35,21 +37,21 @@ func oidcWriteModelToOIDCConfig(writeModel *OIDCApplicationWriteModel) *domain.O RedirectUris: writeModel.RedirectUris, ResponseTypes: writeModel.ResponseTypes, GrantTypes: writeModel.GrantTypes, - ApplicationType: writeModel.ApplicationType, - AuthMethodType: writeModel.AuthMethodType, + ApplicationType: gu.Ptr(writeModel.ApplicationType), + AuthMethodType: gu.Ptr(writeModel.AuthMethodType), PostLogoutRedirectUris: writeModel.PostLogoutRedirectUris, - OIDCVersion: writeModel.OIDCVersion, - DevMode: writeModel.DevMode, - AccessTokenType: writeModel.AccessTokenType, - AccessTokenRoleAssertion: writeModel.AccessTokenRoleAssertion, - IDTokenRoleAssertion: writeModel.IDTokenRoleAssertion, - IDTokenUserinfoAssertion: writeModel.IDTokenUserinfoAssertion, - ClockSkew: writeModel.ClockSkew, + OIDCVersion: gu.Ptr(writeModel.OIDCVersion), + DevMode: gu.Ptr(writeModel.DevMode), + AccessTokenType: gu.Ptr(writeModel.AccessTokenType), + AccessTokenRoleAssertion: gu.Ptr(writeModel.AccessTokenRoleAssertion), + IDTokenRoleAssertion: gu.Ptr(writeModel.IDTokenRoleAssertion), + IDTokenUserinfoAssertion: gu.Ptr(writeModel.IDTokenUserinfoAssertion), + ClockSkew: gu.Ptr(writeModel.ClockSkew), AdditionalOrigins: writeModel.AdditionalOrigins, - SkipNativeAppSuccessPage: writeModel.SkipNativeAppSuccessPage, - BackChannelLogoutURI: writeModel.BackChannelLogoutURI, - LoginVersion: writeModel.LoginVersion, - LoginBaseURI: writeModel.LoginBaseURI, + SkipNativeAppSuccessPage: gu.Ptr(writeModel.SkipNativeAppSuccessPage), + BackChannelLogoutURI: gu.Ptr(writeModel.BackChannelLogoutURI), + LoginVersion: gu.Ptr(writeModel.LoginVersion), + LoginBaseURI: gu.Ptr(writeModel.LoginBaseURI), } } @@ -60,10 +62,10 @@ func samlWriteModelToSAMLConfig(writeModel *SAMLApplicationWriteModel) *domain.S AppName: writeModel.AppName, State: writeModel.State, Metadata: writeModel.Metadata, - MetadataURL: writeModel.MetadataURL, + MetadataURL: gu.Ptr(writeModel.MetadataURL), EntityID: writeModel.EntityID, - LoginVersion: writeModel.LoginVersion, - LoginBaseURI: writeModel.LoginBaseURI, + LoginVersion: gu.Ptr(writeModel.LoginVersion), + LoginBaseURI: gu.Ptr(writeModel.LoginBaseURI), } } @@ -78,15 +80,6 @@ func apiWriteModelToAPIConfig(writeModel *APIApplicationWriteModel) *domain.APIA } } -func roleWriteModelToRole(writeModel *ProjectRoleWriteModel) *domain.ProjectRole { - return &domain.ProjectRole{ - ObjectRoot: writeModelToObjectRoot(writeModel.WriteModel), - Key: writeModel.Key, - DisplayName: writeModel.DisplayName, - Group: writeModel.Group, - } -} - func memberWriteModelToProjectGrantMember(writeModel *ProjectGrantMemberWriteModel) *domain.ProjectGrantMember { return &domain.ProjectGrantMember{ ObjectRoot: writeModelToObjectRoot(writeModel.WriteModel), diff --git a/internal/command/project_model.go b/internal/command/project_model.go index cabceb8500..4c9496b3ad 100644 --- a/internal/command/project_model.go +++ b/internal/command/project_model.go @@ -2,6 +2,7 @@ package command import ( "context" + "slices" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" @@ -120,7 +121,7 @@ func (wm *ProjectWriteModel) NewChangedEvent( } func isProjectStateExists(state domain.ProjectState) bool { - return !hasProjectState(state, domain.ProjectStateRemoved, domain.ProjectStateUnspecified) + return !slices.Contains([]domain.ProjectState{domain.ProjectStateRemoved, domain.ProjectStateUnspecified}, state) } func ProjectAggregateFromWriteModel(wm *eventstore.WriteModel) *eventstore.Aggregate { @@ -130,12 +131,3 @@ func ProjectAggregateFromWriteModel(wm *eventstore.WriteModel) *eventstore.Aggre func ProjectAggregateFromWriteModelWithCTX(ctx context.Context, wm *eventstore.WriteModel) *eventstore.Aggregate { return project.AggregateFromWriteModel(ctx, wm) } - -func hasProjectState(check domain.ProjectState, states ...domain.ProjectState) bool { - for _, state := range states { - if check == state { - return true - } - } - return false -} diff --git a/internal/domain/application_oidc.go b/internal/domain/application_oidc.go index 5d466c689d..10a70a1776 100644 --- a/internal/domain/application_oidc.go +++ b/internal/domain/application_oidc.go @@ -1,6 +1,7 @@ package domain import ( + "slices" "strings" "time" @@ -32,22 +33,22 @@ type OIDCApp struct { RedirectUris []string ResponseTypes []OIDCResponseType GrantTypes []OIDCGrantType - ApplicationType OIDCApplicationType - AuthMethodType OIDCAuthMethodType + ApplicationType *OIDCApplicationType + AuthMethodType *OIDCAuthMethodType PostLogoutRedirectUris []string - OIDCVersion OIDCVersion + OIDCVersion *OIDCVersion Compliance *Compliance - DevMode bool - AccessTokenType OIDCTokenType - AccessTokenRoleAssertion bool - IDTokenRoleAssertion bool - IDTokenUserinfoAssertion bool - ClockSkew time.Duration + DevMode *bool + AccessTokenType *OIDCTokenType + AccessTokenRoleAssertion *bool + IDTokenRoleAssertion *bool + IDTokenUserinfoAssertion *bool + ClockSkew *time.Duration AdditionalOrigins []string - SkipNativeAppSuccessPage bool - BackChannelLogoutURI string - LoginVersion LoginVersion - LoginBaseURI string + SkipNativeAppSuccessPage *bool + BackChannelLogoutURI *string + LoginVersion *LoginVersion + LoginBaseURI *string State AppState } @@ -69,7 +70,7 @@ func (a *OIDCApp) setClientSecret(encodedHash string) { } func (a *OIDCApp) requiresClientSecret() bool { - return a.AuthMethodType == OIDCAuthMethodTypeBasic || a.AuthMethodType == OIDCAuthMethodTypePost + return a.AuthMethodType != nil && (*a.AuthMethodType == OIDCAuthMethodTypeBasic || *a.AuthMethodType == OIDCAuthMethodTypePost) } type OIDCVersion int32 @@ -137,7 +138,7 @@ const ( ) func (a *OIDCApp) IsValid() bool { - if a.ClockSkew > time.Second*5 || a.ClockSkew < time.Second*0 || !a.OriginsValid() { + if (a.ClockSkew != nil && (*a.ClockSkew > time.Second*5 || *a.ClockSkew < time.Second*0)) || !a.OriginsValid() { return false } grantTypes := a.getRequiredGrantTypes() @@ -204,30 +205,25 @@ func ContainsOIDCGrantTypes(shouldContain, list []OIDCGrantType) bool { } func containsOIDCGrantType(grantTypes []OIDCGrantType, grantType OIDCGrantType) bool { - for _, gt := range grantTypes { - if gt == grantType { - return true - } - } - return false + return slices.Contains(grantTypes, grantType) } func (a *OIDCApp) FillCompliance() { a.Compliance = GetOIDCCompliance(a.OIDCVersion, a.ApplicationType, a.GrantTypes, a.ResponseTypes, a.AuthMethodType, a.RedirectUris) } -func GetOIDCCompliance(version OIDCVersion, appType OIDCApplicationType, grantTypes []OIDCGrantType, responseTypes []OIDCResponseType, authMethod OIDCAuthMethodType, redirectUris []string) *Compliance { - switch version { - case OIDCVersionV1: +func GetOIDCCompliance(version *OIDCVersion, appType *OIDCApplicationType, grantTypes []OIDCGrantType, responseTypes []OIDCResponseType, authMethod *OIDCAuthMethodType, redirectUris []string) *Compliance { + if version != nil && *version == OIDCVersionV1 { return GetOIDCV1Compliance(appType, grantTypes, authMethod, redirectUris) } + return &Compliance{ NoneCompliant: true, Problems: []string{"Application.OIDC.UnsupportedVersion"}, } } -func GetOIDCV1Compliance(appType OIDCApplicationType, grantTypes []OIDCGrantType, authMethod OIDCAuthMethodType, redirectUris []string) *Compliance { +func GetOIDCV1Compliance(appType *OIDCApplicationType, grantTypes []OIDCGrantType, authMethod *OIDCAuthMethodType, redirectUris []string) *Compliance { compliance := &Compliance{NoneCompliant: false} checkGrantTypesCombination(compliance, grantTypes) @@ -247,7 +243,7 @@ func checkGrantTypesCombination(compliance *Compliance, grantTypes []OIDCGrantTy } } -func checkRedirectURIs(compliance *Compliance, grantTypes []OIDCGrantType, appType OIDCApplicationType, redirectUris []string) { +func checkRedirectURIs(compliance *Compliance, grantTypes []OIDCGrantType, appType *OIDCApplicationType, redirectUris []string) { // See #5684 for OIDCGrantTypeDeviceCode and redirectUris further explanation if len(redirectUris) == 0 && (!containsOIDCGrantType(grantTypes, OIDCGrantTypeDeviceCode) || (containsOIDCGrantType(grantTypes, OIDCGrantTypeDeviceCode) && containsOIDCGrantType(grantTypes, OIDCGrantTypeAuthorizationCode))) { compliance.NoneCompliant = true @@ -266,53 +262,58 @@ func checkRedirectURIs(compliance *Compliance, grantTypes []OIDCGrantType, appTy } } -func checkApplicationType(compliance *Compliance, appType OIDCApplicationType, authMethod OIDCAuthMethodType) { - switch appType { - case OIDCApplicationTypeNative: - GetOIDCV1NativeApplicationCompliance(compliance, authMethod) - case OIDCApplicationTypeUserAgent: - GetOIDCV1UserAgentApplicationCompliance(compliance, authMethod) +func checkApplicationType(compliance *Compliance, appType *OIDCApplicationType, authMethod *OIDCAuthMethodType) { + if appType != nil { + switch *appType { + case OIDCApplicationTypeNative: + GetOIDCV1NativeApplicationCompliance(compliance, authMethod) + case OIDCApplicationTypeUserAgent: + GetOIDCV1UserAgentApplicationCompliance(compliance, authMethod) + case OIDCApplicationTypeWeb: + return + } } + if compliance.NoneCompliant { compliance.Problems = append([]string{"Application.OIDC.V1.NotCompliant"}, compliance.Problems...) } } -func GetOIDCV1NativeApplicationCompliance(compliance *Compliance, authMethod OIDCAuthMethodType) { - if authMethod != OIDCAuthMethodTypeNone { +func GetOIDCV1NativeApplicationCompliance(compliance *Compliance, authMethod *OIDCAuthMethodType) { + if authMethod != nil && *authMethod != OIDCAuthMethodTypeNone { compliance.NoneCompliant = true compliance.Problems = append(compliance.Problems, "Application.OIDC.V1.Native.AuthMethodType.NotNone") } } -func GetOIDCV1UserAgentApplicationCompliance(compliance *Compliance, authMethod OIDCAuthMethodType) { - if authMethod != OIDCAuthMethodTypeNone { +func GetOIDCV1UserAgentApplicationCompliance(compliance *Compliance, authMethod *OIDCAuthMethodType) { + if authMethod != nil && *authMethod != OIDCAuthMethodTypeNone { compliance.NoneCompliant = true compliance.Problems = append(compliance.Problems, "Application.OIDC.V1.UserAgent.AuthMethodType.NotNone") } } -func CheckRedirectUrisCode(compliance *Compliance, appType OIDCApplicationType, redirectUris []string) { +func CheckRedirectUrisCode(compliance *Compliance, appType *OIDCApplicationType, redirectUris []string) { if urlsAreHttps(redirectUris) { return } if urlContainsPrefix(redirectUris, http) { - if appType == OIDCApplicationTypeUserAgent { + if appType != nil && *appType == OIDCApplicationTypeUserAgent { compliance.NoneCompliant = true compliance.Problems = append(compliance.Problems, "Application.OIDC.V1.Code.RedirectUris.HttpOnlyForWeb") } - if appType == OIDCApplicationTypeNative && !onlyLocalhostIsHttp(redirectUris) { + if appType != nil && *appType == OIDCApplicationTypeNative && !onlyLocalhostIsHttp(redirectUris) { compliance.NoneCompliant = true compliance.Problems = append(compliance.Problems, "Application.OIDC.V1.Native.RedirectUris.MustBeHttpLocalhost") } } - if containsCustom(redirectUris) && appType != OIDCApplicationTypeNative { + if containsCustom(redirectUris) && appType != nil && *appType != OIDCApplicationTypeNative { compliance.NoneCompliant = true compliance.Problems = append(compliance.Problems, "Application.OIDC.V1.Code.RedirectUris.CustomOnlyForNative") } } -func CheckRedirectUrisImplicit(compliance *Compliance, appType OIDCApplicationType, redirectUris []string) { +func CheckRedirectUrisImplicit(compliance *Compliance, appType *OIDCApplicationType, redirectUris []string) { if urlsAreHttps(redirectUris) { return } @@ -321,7 +322,7 @@ func CheckRedirectUrisImplicit(compliance *Compliance, appType OIDCApplicationTy compliance.Problems = append(compliance.Problems, "Application.OIDC.V1.Implicit.RedirectUris.CustomNotAllowed") } if urlContainsPrefix(redirectUris, http) { - if appType == OIDCApplicationTypeNative { + if appType != nil && *appType == OIDCApplicationTypeNative { if !onlyLocalhostIsHttp(redirectUris) { compliance.NoneCompliant = true compliance.Problems = append(compliance.Problems, "Application.OIDC.V1.Native.RedirectUris.MustBeHttpLocalhost") @@ -333,20 +334,20 @@ func CheckRedirectUrisImplicit(compliance *Compliance, appType OIDCApplicationTy } } -func CheckRedirectUrisImplicitAndCode(compliance *Compliance, appType OIDCApplicationType, redirectUris []string) { +func CheckRedirectUrisImplicitAndCode(compliance *Compliance, appType *OIDCApplicationType, redirectUris []string) { if urlsAreHttps(redirectUris) { return } - if containsCustom(redirectUris) && appType != OIDCApplicationTypeNative { + if containsCustom(redirectUris) && appType != nil && *appType != OIDCApplicationTypeNative { compliance.NoneCompliant = true compliance.Problems = append(compliance.Problems, "Application.OIDC.V1.Implicit.RedirectUris.CustomNotAllowed") } if urlContainsPrefix(redirectUris, http) { - if appType == OIDCApplicationTypeUserAgent { + if appType != nil && *appType == OIDCApplicationTypeUserAgent { compliance.NoneCompliant = true compliance.Problems = append(compliance.Problems, "Application.OIDC.V1.Code.RedirectUris.HttpOnlyForWeb") } - if !onlyLocalhostIsHttp(redirectUris) && appType == OIDCApplicationTypeNative { + if !onlyLocalhostIsHttp(redirectUris) && appType != nil && *appType == OIDCApplicationTypeNative { compliance.NoneCompliant = true compliance.Problems = append(compliance.Problems, "Application.OIDC.V1.Native.RedirectUris.MustBeHttpLocalhost") } diff --git a/internal/domain/application_oidc_test.go b/internal/domain/application_oidc_test.go index b3d9488827..4208917cdd 100644 --- a/internal/domain/application_oidc_test.go +++ b/internal/domain/application_oidc_test.go @@ -6,6 +6,8 @@ import ( "testing" "time" + "github.com/muhlemmer/gu" + "github.com/zitadel/zitadel/internal/eventstore/v1/models" ) @@ -25,7 +27,7 @@ func TestApplicationValid(t *testing.T) { ObjectRoot: models.ObjectRoot{AggregateID: "AggregateID"}, AppID: "AppID", AppName: "AppName", - ClockSkew: time.Minute * 1, + ClockSkew: gu.Ptr(time.Minute * 1), ResponseTypes: []OIDCResponseType{OIDCResponseTypeCode}, GrantTypes: []OIDCGrantType{OIDCGrantTypeAuthorizationCode}, }, @@ -39,7 +41,7 @@ func TestApplicationValid(t *testing.T) { ObjectRoot: models.ObjectRoot{AggregateID: "AggregateID"}, AppID: "AppID", AppName: "AppName", - ClockSkew: time.Minute * -1, + ClockSkew: gu.Ptr(time.Minute * -1), ResponseTypes: []OIDCResponseType{OIDCResponseTypeCode}, GrantTypes: []OIDCGrantType{OIDCGrantTypeAuthorizationCode}, }, @@ -190,9 +192,9 @@ func TestApplicationValid(t *testing.T) { func TestGetOIDCV1Compliance(t *testing.T) { type args struct { - appType OIDCApplicationType + appType *OIDCApplicationType grantTypes []OIDCGrantType - authMethod OIDCAuthMethodType + authMethod *OIDCAuthMethodType redirectUris []string } tests := []struct { @@ -266,7 +268,7 @@ func Test_checkGrantTypesCombination(t *testing.T) { func Test_checkRedirectURIs(t *testing.T) { type args struct { grantTypes []OIDCGrantType - appType OIDCApplicationType + appType *OIDCApplicationType redirectUris []string } tests := []struct { @@ -304,7 +306,7 @@ func Test_checkRedirectURIs(t *testing.T) { args: args{ redirectUris: []string{"http://redirect.to/me"}, grantTypes: []OIDCGrantType{OIDCGrantTypeImplicit}, - appType: OIDCApplicationTypeUserAgent, + appType: gu.Ptr(OIDCApplicationTypeUserAgent), }, }, { @@ -316,7 +318,7 @@ func Test_checkRedirectURIs(t *testing.T) { args: args{ redirectUris: []string{"http://redirect.to/me"}, grantTypes: []OIDCGrantType{OIDCGrantTypeAuthorizationCode}, - appType: OIDCApplicationTypeUserAgent, + appType: gu.Ptr(OIDCApplicationTypeUserAgent), }, }, } @@ -338,7 +340,7 @@ func Test_checkRedirectURIs(t *testing.T) { func Test_CheckRedirectUrisImplicitAndCode(t *testing.T) { type args struct { - appType OIDCApplicationType + appType *OIDCApplicationType redirectUris []string } tests := []struct { @@ -356,17 +358,6 @@ func Test_CheckRedirectUrisImplicitAndCode(t *testing.T) { redirectUris: []string{"https://redirect.to/me"}, }, }, - // { - // name: "custom protocol, not native", - // want: &Compliance{ - // NoneCompliant: true, - // Problems: []string{"Application.OIDC.V1.Implicit.RedirectUris.CustomNotAllowed"}, - // }, - // args: args{ - // redirectUris: []string{"protocol://redirect.to/me"}, - // appType: OIDCApplicationTypeWeb, - // }, - // }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -386,7 +377,7 @@ func Test_CheckRedirectUrisImplicitAndCode(t *testing.T) { func TestCheckRedirectUrisImplicitAndCode(t *testing.T) { type args struct { - appType OIDCApplicationType + appType *OIDCApplicationType redirectUris []string } tests := []struct { @@ -402,7 +393,7 @@ func TestCheckRedirectUrisImplicitAndCode(t *testing.T) { { name: "custom protocol not native app", args: args{ - appType: OIDCApplicationTypeWeb, + appType: gu.Ptr(OIDCApplicationTypeWeb), redirectUris: []string{"custom://nirvana.com"}, }, want: &Compliance{ @@ -413,7 +404,7 @@ func TestCheckRedirectUrisImplicitAndCode(t *testing.T) { { name: "http localhost user agent app", args: args{ - appType: OIDCApplicationTypeUserAgent, + appType: gu.Ptr(OIDCApplicationTypeUserAgent), redirectUris: []string{"http://localhost:9009"}, }, want: &Compliance{ @@ -424,7 +415,7 @@ func TestCheckRedirectUrisImplicitAndCode(t *testing.T) { { name: "http, not only localhost native app", args: args{ - appType: OIDCApplicationTypeNative, + appType: gu.Ptr(OIDCApplicationTypeNative), redirectUris: []string{"http://nirvana.com", "http://localhost:9009"}, }, want: &Compliance{ @@ -435,7 +426,7 @@ func TestCheckRedirectUrisImplicitAndCode(t *testing.T) { { name: "not allowed combination", args: args{ - appType: OIDCApplicationTypeNative, + appType: gu.Ptr(OIDCApplicationTypeNative), redirectUris: []string{"https://nirvana.com", "cutom://nirvana.com"}, }, want: &Compliance{ @@ -461,7 +452,7 @@ func TestCheckRedirectUrisImplicitAndCode(t *testing.T) { func TestCheckRedirectUrisImplicit(t *testing.T) { type args struct { - appType OIDCApplicationType + appType *OIDCApplicationType redirectUris []string } tests := []struct { @@ -488,7 +479,7 @@ func TestCheckRedirectUrisImplicit(t *testing.T) { name: "only http protocol, app type native, not only localhost", args: args{ redirectUris: []string{"http://nirvana.com"}, - appType: OIDCApplicationTypeNative, + appType: gu.Ptr(OIDCApplicationTypeNative), }, want: &Compliance{ NoneCompliant: true, @@ -499,7 +490,7 @@ func TestCheckRedirectUrisImplicit(t *testing.T) { name: "only http protocol, app type native, only localhost", args: args{ redirectUris: []string{"http://localhost:8080"}, - appType: OIDCApplicationTypeNative, + appType: gu.Ptr(OIDCApplicationTypeNative), }, want: &Compliance{ NoneCompliant: false, @@ -510,7 +501,7 @@ func TestCheckRedirectUrisImplicit(t *testing.T) { name: "only http protocol, app type web", args: args{ redirectUris: []string{"http://nirvana.com"}, - appType: OIDCApplicationTypeWeb, + appType: gu.Ptr(OIDCApplicationTypeWeb), }, want: &Compliance{ NoneCompliant: true, @@ -535,7 +526,7 @@ func TestCheckRedirectUrisImplicit(t *testing.T) { func TestCheckRedirectUrisCode(t *testing.T) { type args struct { - appType OIDCApplicationType + appType *OIDCApplicationType redirectUris []string } tests := []struct { @@ -552,7 +543,7 @@ func TestCheckRedirectUrisCode(t *testing.T) { name: "custom prefix, app type web", args: args{ redirectUris: []string{"custom://nirvana.com"}, - appType: OIDCApplicationTypeWeb, + appType: gu.Ptr(OIDCApplicationTypeWeb), }, want: &Compliance{ NoneCompliant: true, @@ -563,7 +554,7 @@ func TestCheckRedirectUrisCode(t *testing.T) { name: "only http protocol, app type user agent", args: args{ redirectUris: []string{"http://nirvana.com"}, - appType: OIDCApplicationTypeUserAgent, + appType: gu.Ptr(OIDCApplicationTypeUserAgent), }, want: &Compliance{ NoneCompliant: true, @@ -574,7 +565,7 @@ func TestCheckRedirectUrisCode(t *testing.T) { name: "only http protocol, app type native, only localhost", args: args{ redirectUris: []string{"http://localhost:8080", "http://nirvana.com:8080"}, - appType: OIDCApplicationTypeNative, + appType: gu.Ptr(OIDCApplicationTypeNative), }, want: &Compliance{ NoneCompliant: true, @@ -585,7 +576,7 @@ func TestCheckRedirectUrisCode(t *testing.T) { name: "custom protocol, not native", args: args{ redirectUris: []string{"custom://nirvana.com"}, - appType: OIDCApplicationTypeWeb, + appType: gu.Ptr(OIDCApplicationTypeWeb), }, want: &Compliance{ NoneCompliant: true, diff --git a/internal/domain/application_saml.go b/internal/domain/application_saml.go index de7ef789ee..aff1875c7e 100644 --- a/internal/domain/application_saml.go +++ b/internal/domain/application_saml.go @@ -11,9 +11,9 @@ type SAMLApp struct { AppName string EntityID string Metadata []byte - MetadataURL string - LoginVersion LoginVersion - LoginBaseURI string + MetadataURL *string + LoginVersion *LoginVersion + LoginBaseURI *string State AppState } @@ -31,11 +31,14 @@ func (a *SAMLApp) GetMetadata() []byte { } func (a *SAMLApp) GetMetadataURL() string { - return a.MetadataURL + if a.MetadataURL != nil { + return *a.MetadataURL + } + return "" } func (a *SAMLApp) IsValid() bool { - if a.MetadataURL == "" && a.Metadata == nil { + if (a.MetadataURL == nil || *a.MetadataURL == "") && a.Metadata == nil { return false } return true diff --git a/internal/domain/permission.go b/internal/domain/permission.go index bb569955f5..119e8c2d3e 100644 --- a/internal/domain/permission.go +++ b/internal/domain/permission.go @@ -47,6 +47,9 @@ const ( PermissionProjectRoleWrite = "project.role.write" PermissionProjectRoleRead = "project.role.read" PermissionProjectRoleDelete = "project.role.delete" + PermissionProjectAppWrite = "project.app.write" + PermissionProjectAppDelete = "project.app.delete" + PermissionProjectAppRead = "project.app.read" ) // ProjectPermissionCheck is used as a check for preconditions dependent on application, project, user resourceowner and usergrants. diff --git a/internal/integration/client.go b/internal/integration/client.go index 20c98b5628..326d6fa8b4 100644 --- a/internal/integration/client.go +++ b/internal/integration/client.go @@ -22,6 +22,7 @@ import ( "github.com/zitadel/zitadel/internal/integration/scim" action "github.com/zitadel/zitadel/pkg/grpc/action/v2beta" "github.com/zitadel/zitadel/pkg/grpc/admin" + app "github.com/zitadel/zitadel/pkg/grpc/app/v2beta" "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" @@ -75,6 +76,7 @@ type Client struct { SCIM *scim.Client Projectv2Beta project_v2beta.ProjectServiceClient InstanceV2Beta instance.InstanceServiceClient + AppV2Beta app.AppServiceClient } func NewDefaultClient(ctx context.Context) (*Client, error) { @@ -114,6 +116,7 @@ func newClient(ctx context.Context, target string) (*Client, error) { SCIM: scim.NewScimClient(target), Projectv2Beta: project_v2beta.NewProjectServiceClient(cc), InstanceV2Beta: instance.NewInstanceServiceClient(cc), + AppV2Beta: app.NewAppServiceClient(cc), } return client, client.pollHealth(ctx) } diff --git a/internal/project/model/oidc_config.go b/internal/project/model/oidc_config.go index 50be6c318a..2c482a67a7 100644 --- a/internal/project/model/oidc_config.go +++ b/internal/project/model/oidc_config.go @@ -3,6 +3,8 @@ package model import ( "time" + "github.com/muhlemmer/gu" + "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models" @@ -98,7 +100,7 @@ func GetOIDCCompliance(version OIDCVersion, appType OIDCApplicationType, grantTy for i, grantType := range grantTypes { domainGrantTypes[i] = domain.OIDCGrantType(grantType) } - compliance := domain.GetOIDCV1Compliance(domain.OIDCApplicationType(appType), domainGrantTypes, domain.OIDCAuthMethodType(authMethod), redirectUris) + compliance := domain.GetOIDCV1Compliance(gu.Ptr(domain.OIDCApplicationType(appType)), domainGrantTypes, gu.Ptr(domain.OIDCAuthMethodType(authMethod)), redirectUris) return &Compliance{ NoneCompliant: compliance.NoneCompliant, Problems: compliance.Problems, diff --git a/internal/query/app.go b/internal/query/app.go index bc97c1807e..777c295139 100644 --- a/internal/query/app.go +++ b/internal/query/app.go @@ -5,9 +5,11 @@ import ( "database/sql" _ "embed" "errors" + "slices" "time" sq "github.com/Masterminds/squirrel" + "github.com/muhlemmer/gu" "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/api/authz" @@ -307,6 +309,19 @@ func (q *Queries) AppByProjectAndAppID(ctx context.Context, shouldTriggerBulk bo return app, err } +func (q *Queries) AppByIDWithPermission(ctx context.Context, appID string, activeOnly bool, permissionCheck domain.PermissionCheck) (*App, error) { + app, err := q.AppByID(ctx, appID, activeOnly) + if err != nil { + return nil, err + } + + if err := appCheckPermission(ctx, app.ResourceOwner, app.ProjectID, permissionCheck); err != nil { + return nil, err + } + + return app, nil +} + func (q *Queries) AppByID(ctx context.Context, appID string, activeOnly bool) (app *App, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() @@ -476,11 +491,54 @@ func (q *Queries) AppByOIDCClientID(ctx context.Context, clientID string) (app * return app, err } -func (q *Queries) SearchApps(ctx context.Context, queries *AppSearchQueries, withOwnerRemoved bool) (apps *Apps, err error) { +func (q *Queries) AppByClientID(ctx context.Context, clientID string) (app *App, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + stmt, scan := prepareAppQuery(true) + eq := sq.Eq{ + AppColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), + AppColumnState.identifier(): domain.AppStateActive, + ProjectColumnState.identifier(): domain.ProjectStateActive, + OrgColumnState.identifier(): domain.OrgStateActive, + } + query, args, err := stmt.Where(sq.And{ + eq, + sq.Or{ + sq.Eq{AppOIDCConfigColumnClientID.identifier(): clientID}, + sq.Eq{AppAPIConfigColumnClientID.identifier(): clientID}, + }, + }).ToSql() + if err != nil { + return nil, zerrors.ThrowInternal(err, "QUERY-Dfge2", "Errors.Query.SQLStatement") + } + + err = q.client.QueryRowContext(ctx, func(row *sql.Row) error { + app, err = scan(row) + return err + }, query, args...) + return app, err +} + +func (q *Queries) SearchApps(ctx context.Context, queries *AppSearchQueries, permissionCheck domain.PermissionCheck) (*Apps, error) { + apps, err := q.searchApps(ctx, queries, PermissionV2(ctx, permissionCheck)) + if err != nil { + return nil, err + } + + if permissionCheck != nil && !authz.GetFeatures(ctx).PermissionCheckV2 { + apps.Apps = appsCheckPermission(ctx, apps.Apps, permissionCheck) + } + return apps, nil +} + +func (q *Queries) searchApps(ctx context.Context, queries *AppSearchQueries, isPermissionV2Enabled bool) (apps *Apps, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() query, scan := prepareAppsQuery() + query = appPermissionCheckV2(ctx, query, isPermissionV2Enabled, queries) + eq := sq.Eq{AppColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID()} stmt, args, err := queries.toQuery(query).Where(eq).ToSql() if err != nil { @@ -498,6 +556,21 @@ func (q *Queries) SearchApps(ctx context.Context, queries *AppSearchQueries, wit return apps, err } +func appPermissionCheckV2(ctx context.Context, query sq.SelectBuilder, enabled bool, queries *AppSearchQueries) sq.SelectBuilder { + if !enabled { + return query + } + + join, args := PermissionClause( + ctx, + AppColumnResourceOwner, + domain.PermissionProjectAppRead, + SingleOrgPermissionOption(queries.Queries), + WithProjectsPermissionOption(AppColumnProjectID), + ) + return query.JoinClause(join, args...) +} + func (q *Queries) SearchClientIDs(ctx context.Context, queries *AppSearchQueries, shouldTriggerBulk bool) (ids []string, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() @@ -574,10 +647,25 @@ func (q *Queries) SAMLAppLoginVersion(ctx context.Context, appID string) (loginV return loginVersion, nil } +func appCheckPermission(ctx context.Context, resourceOwner string, projectID string, permissionCheck domain.PermissionCheck) error { + return permissionCheck(ctx, domain.PermissionProjectAppRead, resourceOwner, projectID) +} + +// appsCheckPermission returns only the apps that the user in context has permission to read +func appsCheckPermission(ctx context.Context, apps []*App, permissionCheck domain.PermissionCheck) []*App { + return slices.DeleteFunc(apps, func(app *App) bool { + return permissionCheck(ctx, domain.PermissionProjectAppRead, app.ResourceOwner, app.ProjectID) != nil + }) +} + func NewAppNameSearchQuery(method TextComparison, value string) (SearchQuery, error) { return NewTextQuery(AppColumnName, value, method) } +func NewAppStateSearchQuery(value domain.AppState) (SearchQuery, error) { + return NewNumberQuery(AppColumnState, int(value), NumberEquals) +} + func NewAppProjectIDSearchQuery(id string) (SearchQuery, error) { return NewTextQuery(AppColumnProjectID, id, TextEquals) } @@ -1089,7 +1177,7 @@ func (c sqlOIDCConfig) set(app *App) { if c.loginBaseURI.Valid { app.OIDCConfig.LoginBaseURI = &c.loginBaseURI.String } - compliance := domain.GetOIDCCompliance(app.OIDCConfig.Version, app.OIDCConfig.AppType, app.OIDCConfig.GrantTypes, app.OIDCConfig.ResponseTypes, app.OIDCConfig.AuthMethodType, app.OIDCConfig.RedirectURIs) + compliance := domain.GetOIDCCompliance(gu.Ptr(app.OIDCConfig.Version), gu.Ptr(app.OIDCConfig.AppType), app.OIDCConfig.GrantTypes, app.OIDCConfig.ResponseTypes, gu.Ptr(app.OIDCConfig.AuthMethodType), app.OIDCConfig.RedirectURIs) app.OIDCConfig.ComplianceProblems = compliance.Problems var err error diff --git a/pkg/grpc/app/v2beta/application.go b/pkg/grpc/app/v2beta/application.go new file mode 100644 index 0000000000..bbce4289f9 --- /dev/null +++ b/pkg/grpc/app/v2beta/application.go @@ -0,0 +1,5 @@ +package app + +type ApplicationConfig = isApplication_Config + +type MetaType = isUpdateSAMLApplicationConfigurationRequest_Metadata \ No newline at end of file diff --git a/proto/zitadel/app/v2beta/api.proto b/proto/zitadel/app/v2beta/api.proto new file mode 100644 index 0000000000..9ef09d5ad8 --- /dev/null +++ b/proto/zitadel/app/v2beta/api.proto @@ -0,0 +1,26 @@ +syntax = "proto3"; + +package zitadel.app.v2beta; + +import "protoc-gen-openapiv2/options/annotations.proto"; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/app/v2beta;app"; + +enum APIAuthMethodType { + API_AUTH_METHOD_TYPE_BASIC = 0; + API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT = 1; +} + +message APIConfig { + string client_id = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629023906488334@ZITADEL\""; + description: "generated oauth2/oidc client_id"; + } + ]; + APIAuthMethodType auth_method_type = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "defines how the API passes the login credentials"; + } + ]; +} \ No newline at end of file diff --git a/proto/zitadel/app/v2beta/app.proto b/proto/zitadel/app/v2beta/app.proto new file mode 100644 index 0000000000..f85e3c021d --- /dev/null +++ b/proto/zitadel/app/v2beta/app.proto @@ -0,0 +1,94 @@ +syntax = "proto3"; + +package zitadel.app.v2beta; + +import "zitadel/app/v2beta/oidc.proto"; +import "zitadel/app/v2beta/saml.proto"; +import "zitadel/app/v2beta/api.proto"; +import "zitadel/filter/v2/filter.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; +import "google/protobuf/duration.proto"; +import "google/protobuf/timestamp.proto"; +import "validate/validate.proto"; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/app/v2beta;app"; + +message Application { + string id = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629023906488334\""; + } + ]; + + // The timestamp of the app creation. + google.protobuf.Timestamp creation_date = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; + + // The timestamp of the app update. + google.protobuf.Timestamp change_date = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; + + AppState state = 4 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "current state of the application"; + } + ]; + string name = 5 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"Console\""; + } + ]; + oneof config { + OIDCConfig oidc_config = 6; + APIConfig api_config = 7; + SAMLConfig saml_config = 8; + } +} + +enum AppState { + APP_STATE_UNSPECIFIED = 0; + APP_STATE_ACTIVE = 1; + APP_STATE_INACTIVE = 2; + APP_STATE_REMOVED = 3; +} + +enum AppSorting { + APP_SORT_BY_ID = 0; + APP_SORT_BY_NAME = 1; + APP_SORT_BY_STATE = 2; + APP_SORT_BY_CREATION_DATE = 3; + APP_SORT_BY_CHANGE_DATE = 4; +} + +message ApplicationSearchFilter { + oneof filter { + option (validate.required) = true; + ApplicationNameQuery name_filter = 1; + AppState state_filter = 2; + bool api_app_only = 3; + bool oidc_app_only = 4; + bool saml_app_only = 5; + } +} + +message ApplicationNameQuery { + string name = 1 [ + (validate.rules).string = {max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"Conso\"" + } + ]; + + zitadel.filter.v2.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" + } + ]; +} diff --git a/proto/zitadel/app/v2beta/app_service.proto b/proto/zitadel/app/v2beta/app_service.proto new file mode 100644 index 0000000000..a881022caa --- /dev/null +++ b/proto/zitadel/app/v2beta/app_service.proto @@ -0,0 +1,788 @@ +syntax = "proto3"; + +package zitadel.app.v2beta; + +import "google/api/annotations.proto"; +import "google/api/field_behavior.proto"; +import "google/protobuf/duration.proto"; +import "google/protobuf/struct.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; +import "validate/validate.proto"; +import "zitadel/app/v2beta/login.proto"; +import "zitadel/app/v2beta/oidc.proto"; +import "zitadel/app/v2beta/api.proto"; +import "zitadel/app/v2beta/app.proto"; +import "google/protobuf/timestamp.proto"; +import "zitadel/protoc_gen_zitadel/v2/options.proto"; +import "zitadel/filter/v2/filter.proto"; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/app/v2beta;app"; + +option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { + info: { + title: "Application Service"; + version: "2.0-beta"; + description: "This API is intended to manage apps (SAML, OIDC, etc..) in a ZITADEL instance. This service is in beta state. It can AND will continue breaking until a stable version is released."; + contact:{ + name: "ZITADEL" + url: "https://zitadel.com" + email: "hi@zitadel.com" + } + license: { + name: "Apache 2.0", + url: "https://github.com/zitadel/zitadel/blob/main/LICENSING.md"; + }; + }; + schemes: HTTPS; + schemes: HTTP; + + consumes: "application/json"; + consumes: "application/grpc"; + + produces: "application/json"; + produces: "application/grpc"; + + consumes: "application/grpc-web+proto"; + produces: "application/grpc-web+proto"; + + host: "$CUSTOM-DOMAIN"; + base_path: "/"; + + external_docs: { + description: "Detailed information about ZITADEL", + url: "https://zitadel.com/docs" + } + security_definitions: { + security: { + key: "OAuth2"; + value: { + type: TYPE_OAUTH2; + flow: FLOW_ACCESS_CODE; + authorization_url: "$CUSTOM-DOMAIN/oauth/v2/authorize"; + token_url: "$CUSTOM-DOMAIN/oauth/v2/token"; + scopes: { + scope: { + key: "openid"; + value: "openid"; + } + scope: { + key: "urn:zitadel:iam:org:project:id:zitadel:aud"; + value: "urn:zitadel:iam:org:project:id:zitadel:aud"; + } + } + } + } + } + security: { + security_requirement: { + key: "OAuth2"; + value: { + scope: "openid"; + scope: "urn:zitadel:iam:org:project:id:zitadel:aud"; + } + } + } + responses: { + key: "403"; + value: { + description: "Returned when the user does not have permission to access the resource."; + schema: { + json_schema: { + ref: "#/definitions/rpcStatus"; + } + } + } + } + responses: { + key: "404"; + value: { + description: "Returned when the resource does not exist."; + schema: { + json_schema: { + ref: "#/definitions/rpcStatus"; + } + } + } + } +}; + +// Service to manage apps. +// The service provides methods to create, update, delete and list apps and app keys. +service AppService { + + // Create Application + // + // Create an application. The application can be OIDC, API or SAML type, based on the input. + // + // The user needs to have project.app.write permission + // + // Required permissions: + // - project.app.write + rpc CreateApplication(CreateApplicationRequest) returns (CreateApplicationResponse) { + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "The created application"; + } + }; + }; + + option (google.api.http) = { + post: "/v2beta/applications" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + } + + // Update Application + // + // Changes the configuration of an OIDC, API or SAML type application, as well as + // the application name, based on the input provided. + // + // The user needs to have project.app.write permission + // + // Required permissions: + // - project.app.write + rpc UpdateApplication(UpdateApplicationRequest) returns (UpdateApplicationResponse) { + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "The updated app."; + } + }; + }; + + option (google.api.http) = { + patch: "/v2beta/applications/{id}" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + } + + // Get Application + // + // Retrieves the application matching the provided ID. + // + // The user needs to have project.app.read permission + // + // Required permissions: + // - project.app.read + rpc GetApplication(GetApplicationRequest) returns (GetApplicationResponse) { + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "The fetched app."; + } + }; + }; + + option (google.api.http) = { + get: "/v2beta/applications/{id}" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + } + + // Delete Application + // + // Deletes the application belonging to the input project and matching the provided + // application ID + // + // The user needs to have project.app.delete permission + // + // Required permissions: + // - project.app.delete + rpc DeleteApplication(DeleteApplicationRequest) returns (DeleteApplicationResponse) { + option (google.api.http) = { + delete: "/v2beta/applications/{id}" + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "The time of deletion."; + } + }; + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + } + + // Deactivate Application + // + // Deactivates the application belonging to the input project and matching the provided + // application ID + // + // The user needs to have project.app.write permission + // + // Required permissions: + // - project.app.write + rpc DeactivateApplication(DeactivateApplicationRequest) returns (DeactivateApplicationResponse) { + option (google.api.http) = { + post: "/v2beta/applications/{id}/deactivate" + body: "*" + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "The time of deactivation."; + } + }; + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + } + + // Reactivate Application + // + // Reactivates the application belonging to the input project and matching the provided + // application ID + // + // The user needs to have project.app.write permission + // + // Required permissions: + // - project.app.write + rpc ReactivateApplication(ReactivateApplicationRequest) returns (ReactivateApplicationResponse) { + option (google.api.http) = { + post: "/v2beta/applications/{id}/reactivate" + body: "*" + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "The time of reactivation."; + } + }; + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + } + + + // Regenerate Client Secret + // + // Regenerates the client secret of an API or OIDC application that belongs to the input project. + // + // The user needs to have project.app.write permission + // + // Required permissions: + // - project.app.write + rpc RegenerateClientSecret(RegenerateClientSecretRequest) returns (RegenerateClientSecretResponse) { + option (google.api.http) = { + post: "/v2beta/applications/{application_id}/generate_client_secret" + body: "*" + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "The regenerated client secret."; + } + }; + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + } + + // List Applications + // + // Returns a list of applications matching the input parameters that belong to the provided + // project. + // + // The result can be sorted by app id, name, creation date, change date or state. It can also + // be filtered by app state, app type and app name. + // + // The user needs to have project.app.read permission + // + // Required permissions: + // - project.app.read + rpc ListApplications(ListApplicationsRequest) returns (ListApplicationsResponse) { + option (google.api.http) = { + post: "/v2beta/applications/search" + body: "*" + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "The matching applications"; + } + }; + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + } +} + +message CreateApplicationRequest { + string project_id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; + string id = 2 [(validate.rules).string = {max_len: 200}]; + string name = 3 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"MyApp\""; + } + ]; + oneof creation_request_type { + option (validate.required) = true; + CreateOIDCApplicationRequest oidc_request = 4; + CreateSAMLApplicationRequest saml_request = 5; + CreateAPIApplicationRequest api_request = 6; + } +} + +message CreateApplicationResponse { + string app_id = 1; + // The timestamp of the app creation. + google.protobuf.Timestamp creation_date = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; + + oneof creation_response_type { + CreateOIDCApplicationResponse oidc_response = 3; + CreateSAMLApplicationResponse saml_response = 4; + CreateAPIApplicationResponse api_response = 5; + } +} + +message CreateOIDCApplicationRequest { + // Callback URI of the authorization request where the code or tokens will be sent to + repeated string redirect_uris = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "[\"http://localhost:4200/auth/callback\"]"; + description: "Callback URI of the authorization request where the code or tokens will be sent to"; + } + ]; + repeated OIDCResponseType response_types = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Determines whether a code, id_token token or just id_token will be returned" + } + ]; + repeated OIDCGrantType grant_types = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "The flow type the application uses to gain access"; + } + ]; + OIDCAppType app_type = 4 [ + (validate.rules).enum = {defined_only: true}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Determines the paradigm of the application"; + } + ]; + OIDCAuthMethodType auth_method_type = 5 [ + (validate.rules).enum = {defined_only: true}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Defines how the application passes login credentials"; + } + ]; + + // ZITADEL will redirect to this link after a successful logout + repeated string post_logout_redirect_uris = 6 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "[\"http://localhost:4200/signedout\"]"; + description: "ZITADEL will redirect to this link after a successful logout"; + } + ]; + OIDCVersion version = 7 [(validate.rules).enum = {defined_only: true}]; + bool dev_mode = 8 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Used for development, some checks of the OIDC specification will not be checked."; + } + ]; + OIDCTokenType access_token_type = 9 [ + (validate.rules).enum = {defined_only: true}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Type of the access token returned from ZITADEL"; + } + ]; + bool access_token_role_assertion = 10 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Adds roles to the claims of the access token (only if type == JWT) even if they are not requested by scopes"; + } + ]; + bool id_token_role_assertion = 11 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Adds roles to the claims of the id token even if they are not requested by scopes"; + } + ]; + bool id_token_userinfo_assertion = 12 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Claims of profile, email, address and phone scopes are added to the id token even if an access token is issued. Attention this violates the OIDC specification"; + } + ]; + google.protobuf.Duration clock_skew = 13 [ + (validate.rules).duration = {gte: {}, lte: {seconds: 5}}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Used to compensate time difference of servers. Duration added to the \"exp\" claim and subtracted from \"iat\", \"auth_time\" and \"nbf\" claims"; + example: "\"1s\""; + } + ]; + repeated string additional_origins = 14 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "[\"scheme://localhost:8080\"]"; + description: "Additional origins (other than the redirect_uris) from where the API can be used, provided string has to be an origin (scheme://hostname[:port]) without path, query or fragment"; + } + ]; + bool skip_native_app_success_page = 15 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Skip the successful login page on native apps and directly redirect the user to the callback."; + } + ]; + string back_channel_logout_uri = 16 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "[\"https://example.com/auth/backchannel\"]"; + description: "ZITADEL will use this URI to notify the application about terminated session according to the OIDC Back-Channel Logout (https://openid.net/specs/openid-connect-backchannel-1_0.html)"; + } + ]; + LoginVersion login_version = 17 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Specify the preferred login UI, where the user is redirected to for authentication. If unset, the login UI is chosen by the instance default."; + } + ]; +} + +message CreateOIDCApplicationResponse { + string client_id = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"1035496534033449\""; + description: "generated client id for this config"; + } + ]; + string client_secret = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"gjoq34589uasgh\""; + description: "generated secret for this config"; + } + ]; + bool none_compliant = 3; + repeated OIDCLocalizedMessage compliance_problems = 4; +} + +message CreateSAMLApplicationRequest { + oneof metadata { + option (validate.required) = true; + bytes metadata_xml = 1 [(validate.rules).bytes.max_len = 500000]; + string metadata_url = 2 [(validate.rules).string.max_len = 200]; + } + LoginVersion login_version = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Specify the preferred login UI, where the user is redirected to for authentication. If unset, the login UI is chosen by the instance default."; + } + ]; +} + +message CreateSAMLApplicationResponse {} + +message CreateAPIApplicationRequest { + APIAuthMethodType auth_method_type = 1 [(validate.rules).enum = {defined_only: true}]; +} + +message CreateAPIApplicationResponse { + string client_id = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"3950723409029374\""; + description: "generated secret for this config"; + } + ]; + string client_secret = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"gjoq34589uasgh\""; + description: "generated secret for this config"; + } + ]; +} + +message UpdateApplicationRequest { + string project_id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; + 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: "\"45984352431\""; + } + ]; + string name = 3 [ + (validate.rules).string = {max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"MyApplicationName\""; + min_length: 1; + max_length: 200; + } + ]; + + oneof update_request_type { + UpdateSAMLApplicationConfigurationRequest saml_configuration_request = 4; + UpdateOIDCApplicationConfigurationRequest oidc_configuration_request = 5; + UpdateAPIApplicationConfigurationRequest api_configuration_request = 6; + } +} + +message UpdateApplicationResponse { + // The timestamp of the app update. + google.protobuf.Timestamp change_date = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; +} + +message UpdateSAMLApplicationConfigurationRequest { + oneof metadata { + option (validate.required) = true; + bytes metadata_xml = 1 [(validate.rules).bytes.max_len = 500000]; + string metadata_url = 2 [(validate.rules).string.max_len = 200]; + } + optional LoginVersion login_version = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Specify the preferred login UI, where the user is redirected to for authentication. If unset, the login UI is chosen by the instance default."; + } + ]; +} + +message UpdateOIDCApplicationConfigurationRequest { + repeated string redirect_uris = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "[\"http://localhost:4200/auth/callback\"]"; + description: "Callback URI of the authorization request where the code or tokens will be sent to"; + } + ]; + repeated OIDCResponseType response_types = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Determines whether a code, id_token token or just id_token will be returned" + } + ]; + repeated OIDCGrantType grant_types = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "The flow type the application uses to gain access"; + } + ]; + optional OIDCAppType app_type = 4 [ + (validate.rules).enum = {defined_only: true}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Determines the paradigm of the application"; + } + ]; + optional OIDCAuthMethodType auth_method_type = 5 [ + (validate.rules).enum = {defined_only: true}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Defines how the application passes login credentials"; + } + ]; + repeated string post_logout_redirect_uris = 6 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "[\"http://localhost:4200/signedout\"]"; + description: "ZITADEL will redirect to this link after a successful logout"; + } + ]; + optional OIDCVersion version = 7 [(validate.rules).enum = {defined_only: true}]; + optional bool dev_mode = 8 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Used for development, some checks of the OIDC specification will not be checked."; + } + ]; + optional OIDCTokenType access_token_type = 9 [ + (validate.rules).enum = {defined_only: true}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Type of the access token returned from ZITADEL"; + } + ]; + optional bool access_token_role_assertion = 10 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Adds roles to the claims of the access token (only if type == JWT) even if they are not requested by scopes"; + } + ]; + optional bool id_token_role_assertion = 11 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Adds roles to the claims of the id token even if they are not requested by scopes"; + } + ]; + optional bool id_token_userinfo_assertion = 12 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Claims of profile, email, address and phone scopes are added to the id token even if an access token is issued. Attention this violates the OIDC specification"; + } + ]; + optional google.protobuf.Duration clock_skew = 13 [ + (validate.rules).duration = {gte: {}, lte: {seconds: 5}}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Used to compensate time difference of servers. Duration added to the \"exp\" claim and subtracted from \"iat\", \"auth_time\" and \"nbf\" claims"; + example: "\"1s\""; + } + ]; + repeated string additional_origins = 14 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "[\"scheme://localhost:8080\"]"; + description: "Additional origins (other than the redirect_uris) from where the API can be used, provided string has to be an origin (scheme://hostname[:port]) without path, query or fragment"; + } + ]; + optional bool skip_native_app_success_page = 15 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Skip the successful login page on native apps and directly redirect the user to the callback."; + } + ]; + optional string back_channel_logout_uri = 16 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "[\"https://example.com/auth/backchannel\"]"; + description: "ZITADEL will use this URI to notify the application about terminated session according to the OIDC Back-Channel Logout (https://openid.net/specs/openid-connect-backchannel-1_0.html)"; + } + ]; + optional LoginVersion login_version = 17 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Specify the preferred login UI, where the user is redirected to for authentication. If unset, the login UI is chosen by the instance default."; + } + ]; +} + +message UpdateAPIApplicationConfigurationRequest { + APIAuthMethodType auth_method_type = 1 [(validate.rules).enum = {defined_only: true}]; +} + +message GetApplicationRequest { + 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: "\"45984352431\""; + } + ]; +} + +message GetApplicationResponse { + Application app = 1; +} + +message DeleteApplicationRequest { + string project_id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; + string id = 2 [(validate.rules).string = {min_len: 1, max_len: 200}]; +} + +message DeleteApplicationResponse { + google.protobuf.Timestamp deletion_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message DeactivateApplicationRequest{ + string project_id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; + string id = 2 [(validate.rules).string = {min_len: 1, max_len: 200}]; +} + +message DeactivateApplicationResponse{ + google.protobuf.Timestamp deactivation_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message ReactivateApplicationRequest{ + string project_id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; + string id = 2 [(validate.rules).string = {min_len: 1, max_len: 200}]; +} + +message ReactivateApplicationResponse{ + google.protobuf.Timestamp reactivation_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message RegenerateClientSecretRequest{ + string project_id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; + string application_id = 2 [(validate.rules).string = {min_len: 1, max_len: 200}]; + oneof app_type { + option (validate.required) = true; + bool is_oidc = 3; + bool is_api = 4; + } +} + +message RegenerateClientSecretResponse{ + string client_secret = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"gjoq34589uasgh\""; + description: "generated secret for the client"; + } + ]; + + // The timestamp of the creation of the new client secret + google.protobuf.Timestamp creation_date = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message ListApplicationsRequest { + string project_id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; + + // Pagination and sorting. + zitadel.filter.v2.PaginationRequest pagination = 2; + + //criteria the client is looking for + repeated ApplicationSearchFilter filters = 3; + + AppSorting sorting_column = 4; +} + +message ListApplicationsResponse { + repeated Application applications = 1; + + // Contains the total number of apps matching the query and the applied limit. + zitadel.filter.v2.PaginationResponse pagination = 2; +} \ No newline at end of file diff --git a/proto/zitadel/app/v2beta/login.proto b/proto/zitadel/app/v2beta/login.proto new file mode 100644 index 0000000000..567b4b5167 --- /dev/null +++ b/proto/zitadel/app/v2beta/login.proto @@ -0,0 +1,18 @@ +syntax = "proto3"; + +package zitadel.app.v2beta; +option go_package = "github.com/zitadel/zitadel/pkg/grpc/app/v2beta;app"; + +message LoginVersion { + oneof version { + LoginV1 login_v1 = 1; + LoginV2 login_v2 = 2; + } +} + +message LoginV1 {} + +message LoginV2 { + // Optionally specify a base uri of the login UI. If unspecified the default URI will be used. + optional string base_uri = 1; +} \ No newline at end of file diff --git a/proto/zitadel/app/v2beta/oidc.proto b/proto/zitadel/app/v2beta/oidc.proto new file mode 100644 index 0000000000..7cfd1dcc43 --- /dev/null +++ b/proto/zitadel/app/v2beta/oidc.proto @@ -0,0 +1,166 @@ +syntax = "proto3"; + +package zitadel.app.v2beta; +import "zitadel/app/v2beta/login.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; +import "google/protobuf/duration.proto"; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/app/v2beta;app"; + +message OIDCLocalizedMessage { + string key = 1; + string localized_message = 2; +} + +enum OIDCResponseType { + OIDC_RESPONSE_TYPE_UNSPECIFIED = 0; + OIDC_RESPONSE_TYPE_CODE = 1; + OIDC_RESPONSE_TYPE_ID_TOKEN = 2; + OIDC_RESPONSE_TYPE_ID_TOKEN_TOKEN = 3; +} + +enum OIDCGrantType{ + OIDC_GRANT_TYPE_AUTHORIZATION_CODE = 0; + OIDC_GRANT_TYPE_IMPLICIT = 1; + OIDC_GRANT_TYPE_REFRESH_TOKEN = 2; + OIDC_GRANT_TYPE_DEVICE_CODE = 3; + OIDC_GRANT_TYPE_TOKEN_EXCHANGE = 4; +} + +enum OIDCAppType { + OIDC_APP_TYPE_WEB = 0; + OIDC_APP_TYPE_USER_AGENT = 1; + OIDC_APP_TYPE_NATIVE = 2; +} + +enum OIDCAuthMethodType { + OIDC_AUTH_METHOD_TYPE_BASIC = 0; + OIDC_AUTH_METHOD_TYPE_POST = 1; + OIDC_AUTH_METHOD_TYPE_NONE = 2; + OIDC_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT = 3; +} + +enum OIDCVersion { + OIDC_VERSION_1_0 = 0; +} + +enum OIDCTokenType { + OIDC_TOKEN_TYPE_BEARER = 0; + OIDC_TOKEN_TYPE_JWT = 1; +} + +message OIDCConfig { + repeated string redirect_uris = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "[\"https://console.zitadel.ch/auth/callback\"]"; + description: "Callback URI of the authorization request where the code or tokens will be sent to"; + } + ]; + repeated OIDCResponseType response_types = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Determines whether a code, id_token token or just id_token will be returned" + } + ]; + repeated OIDCGrantType grant_types = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "The flow type the application uses to gain access"; + } + ]; + OIDCAppType app_type = 4 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "determines the paradigm of the application"; + } + ]; + string client_id = 5 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629023906488334@ZITADEL\""; + description: "generated oauth2/oidc client id"; + } + ]; + OIDCAuthMethodType auth_method_type = 6 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "defines how the application passes login credentials"; + } + ]; + repeated string post_logout_redirect_uris = 7 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "[\"https://console.zitadel.ch/logout\"]"; + description: "ZITADEL will redirect to this link after a successful logout"; + } + ]; + OIDCVersion version = 8 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "the OIDC version used by the application"; + } + ]; + bool none_compliant = 9 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "specifies whether the config is OIDC compliant. A production configuration SHOULD be compliant"; + } + ]; + repeated OIDCLocalizedMessage compliance_problems = 10 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "lists the problems for non-compliancy"; + } + ]; + bool dev_mode = 11 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "used for development"; + } + ]; + OIDCTokenType access_token_type = 12 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "type of the access token returned from ZITADEL"; + } + ]; + bool access_token_role_assertion = 13 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "adds roles to the claims of the access token (only if type == JWT) even if they are not requested by scopes"; + } + ]; + bool id_token_role_assertion = 14 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "adds roles to the claims of the id token even if they are not requested by scopes"; + } + ]; + bool id_token_userinfo_assertion = 15 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "claims of profile, email, address and phone scopes are added to the id token even if an access token is issued. Attention this violates the OIDC specification"; + } + ]; + google.protobuf.Duration clock_skew = 16 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Used to compensate time difference of servers. Duration added to the \"exp\" claim and subtracted from \"iat\", \"auth_time\" and \"nbf\" claims"; + // min: "0s"; + // max: "5s"; + } + ]; + repeated string additional_origins = 17 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "[\"https://console.zitadel.ch/auth/callback\"]"; + description: "additional origins (other than the redirect_uris) from where the API can be used"; + } + ]; + repeated string allowed_origins = 18 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "[\"https://console.zitadel.ch/auth/callback\"]"; + description: "all allowed origins from where the API can be used"; + } + ]; + bool skip_native_app_success_page = 19 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Skip the successful login page on native apps and directly redirect the user to the callback."; + } + ]; + string back_channel_logout_uri = 20 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "[\"https://example.com/auth/backchannel\"]"; + description: "ZITADEL will use this URI to notify the application about terminated session according to the OIDC Back-Channel Logout (https://openid.net/specs/openid-connect-backchannel-1_0.html)"; + } + ]; + LoginVersion login_version = 21 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Specify the preferred login UI, where the user is redirected to for authentication. If unset, the login UI is chosen by the instance default."; + } + ]; +} \ No newline at end of file diff --git a/proto/zitadel/app/v2beta/saml.proto b/proto/zitadel/app/v2beta/saml.proto new file mode 100644 index 0000000000..7c85447880 --- /dev/null +++ b/proto/zitadel/app/v2beta/saml.proto @@ -0,0 +1,20 @@ +syntax = "proto3"; + +package zitadel.app.v2beta; + +import "zitadel/app/v2beta/login.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/app/v2beta;app"; + +message SAMLConfig { + oneof metadata{ + bytes metadata_xml = 1; + string metadata_url = 2; + } + LoginVersion login_version = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Specify the preferred login UI, where the user is redirected to for authentication. If unset, the login UI is chosen by the instance default."; + } + ]; +} \ No newline at end of file diff --git a/proto/zitadel/management.proto b/proto/zitadel/management.proto index d633fbe8c5..74d5dcf60b 100644 --- a/proto/zitadel/management.proto +++ b/proto/zitadel/management.proto @@ -3287,6 +3287,7 @@ service ManagementService { }; } + // Deprecated: Use [GetApplication](/apis/resources/application_service_v2/application-service-get-application.api.mdx) instead to fetch an app rpc GetAppByID(GetAppByIDRequest) returns (GetAppByIDResponse) { option (google.api.http) = { get: "/projects/{project_id}/apps/{app_id}" @@ -3309,9 +3310,11 @@ service ManagementService { required: false; }; }; + deprecated: true; }; } + // Deprecated: Use [ListApplications](/apis/resources/application_service_v2/application-service-list-applications.api.mdx) instead to list applications rpc ListApps(ListAppsRequest) returns (ListAppsResponse) { option (google.api.http) = { post: "/projects/{project_id}/apps/_search" @@ -3335,6 +3338,7 @@ service ManagementService { required: false; }; }; + deprecated: true; }; } @@ -3363,6 +3367,7 @@ service ManagementService { }; } + // Deprecated: Use [CreateApplication](/apis/resources/application_service_v2/application-service-create-application.api.mdx) instead to create an OIDC application rpc AddOIDCApp(AddOIDCAppRequest) returns (AddOIDCAppResponse) { option (google.api.http) = { post: "/projects/{project_id}/apps/oidc" @@ -3386,62 +3391,74 @@ service ManagementService { required: false; }; }; + deprecated: true; }; } - rpc AddSAMLApp(AddSAMLAppRequest) returns (AddSAMLAppResponse) { - option (google.api.http) = { - post: "/projects/{project_id}/apps/saml" - body: "*" - }; - - option (zitadel.v1.auth_option) = { - permission: "project.app.write" - check_field_name: "ProjectId" - }; - - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - tags: "Applications"; - summary: "Create Application (SAML)"; - description: "Create a new SAML client. Returns an entity ID" - parameters: { - headers: { - name: "x-zitadel-orgid"; - description: "The default is always the organization of the requesting user. If you like to change/get objects of another organization include the header. Make sure the requesting user has permission to access the requested data."; - type: STRING, - required: false; - }; - }; - }; - } - - rpc AddAPIApp(AddAPIAppRequest) returns (AddAPIAppResponse) { - option (google.api.http) = { - post: "/projects/{project_id}/apps/api" - body: "*" - }; - - option (zitadel.v1.auth_option) = { - permission: "project.app.write" - check_field_name: "ProjectId" + // Deprecated: Use [CreateApplication](/apis/resources/application_service_v2/application-service-create-application.api.mdx) instead to create a SAML application + rpc AddSAMLApp(AddSAMLAppRequest) returns (AddSAMLAppResponse) { + option (google.api.http) = { + post: "/projects/{project_id}/apps/saml" + body: "*" }; - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - tags: "Applications"; - summary: "Create Application (API)"; - description: "Create a new API client. The client id will be generated and returned in the response. Depending on the chosen configuration also a secret will be generated and returned." - parameters: { - headers: { - name: "x-zitadel-orgid"; - description: "The default is always the organization of the requesting user. If you like to change/get objects of another organization include the header. Make sure the requesting user has permission to access the requested data."; - type: STRING, - required: false; - }; - }; - }; + option (zitadel.v1.auth_option) = { + permission: "project.app.write" + check_field_name: "ProjectId" + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + tags: "Applications"; + summary: "Create Application (SAML)"; + description: "Create a new SAML client. Returns an entity ID" + parameters: { + headers: { + name: "x-zitadel-orgid"; + description: "The default is always the organization of the requesting user. If you like to change/get objects of another organization include the header. Make sure the requesting user has permission to access the requested data."; + type: STRING, + required: false; + }; + }; + deprecated: true; + }; + } + + // Create Application (API) + // + // Create a new API client. The client id will be generated and returned in the response. + // Depending on the chosen configuration also a secret will be generated and returned. + // + // Deprecated: Use [CreateApplication](/apis/resources/application_service_v2/application-service-create-application.api.mdx) instead to create an API application + rpc AddAPIApp(AddAPIAppRequest) returns (AddAPIAppResponse) { + option (google.api.http) = { + post: "/projects/{project_id}/apps/api" + body: "*" + }; + + option (zitadel.v1.auth_option) = { + permission: "project.app.write" + check_field_name: "ProjectId" + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + tags: "Applications"; + summary: "Create Application (API)"; + description: "Create a new API client. The client id will be generated and returned in the response. Depending on the chosen configuration also a secret will be generated and returned." + parameters: { + headers: { + name: "x-zitadel-orgid"; + description: "The default is always the organization of the requesting user. If you like to change/get objects of another organization include the header. Make sure the requesting user has permission to access the requested data."; + type: STRING, + required: false; + }; + }; + deprecated: true; + }; } // Changes application + // + // Deprecated: Use [PatchApplication](/apis/resources/application_service_v2/application-service-patch-application.api.mdx) instead to update the generic params of an app rpc UpdateApp(UpdateAppRequest) returns (UpdateAppResponse) { option (google.api.http) = { put: "/projects/{project_id}/apps/{app_id}" @@ -3465,9 +3482,11 @@ service ManagementService { required: false; }; }; + deprecated: true; }; } + // Deprecated: Use [PatchApplication](/apis/resources/application_service_v2/application-service-patch-application.api.mdx) instead to update the config of an OIDC app rpc UpdateOIDCAppConfig(UpdateOIDCAppConfigRequest) returns (UpdateOIDCAppConfigResponse) { option (google.api.http) = { put: "/projects/{project_id}/apps/{app_id}/oidc_config" @@ -3491,61 +3510,67 @@ service ManagementService { required: false; }; }; + deprecated: true }; } - rpc UpdateSAMLAppConfig(UpdateSAMLAppConfigRequest) returns (UpdateSAMLAppConfigResponse) { - option (google.api.http) = { - put: "/projects/{project_id}/apps/{app_id}/saml_config" - body: "*" - }; - - option (zitadel.v1.auth_option) = { - permission: "project.app.write" - check_field_name: "ProjectId" - }; - - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - tags: "Applications"; - summary: "Update SAML Application Config"; - description: "Update the SAML specific configuration of an application." - parameters: { - headers: { - name: "x-zitadel-orgid"; - description: "The default is always the organization of the requesting user. If you like to change/get objects of another organization include the header. Make sure the requesting user has permission to access the requested data."; - type: STRING, - required: false; - }; - }; - }; - } - - rpc UpdateAPIAppConfig(UpdateAPIAppConfigRequest) returns (UpdateAPIAppConfigResponse) { - option (google.api.http) = { - put: "/projects/{project_id}/apps/{app_id}/api_config" - body: "*" - }; + // Deprecated: Use [PatchApplication](/apis/resources/application_service_v2/application-service-patch-application.api.mdx) instead to update the config of a SAML app + rpc UpdateSAMLAppConfig(UpdateSAMLAppConfigRequest) returns (UpdateSAMLAppConfigResponse) { + option (google.api.http) = { + put: "/projects/{project_id}/apps/{app_id}/saml_config" + body: "*" + }; option (zitadel.v1.auth_option) = { - permission: "project.app.write" - check_field_name: "ProjectId" + permission: "project.app.write" + check_field_name: "ProjectId" }; - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - tags: "Applications"; - summary: "Update API Application Config"; - description: "Update the OIDC-specific configuration of an application." - parameters: { - headers: { - name: "x-zitadel-orgid"; - description: "The default is always the organization of the requesting user. If you like to change/get objects of another organization include the header. Make sure the requesting user has permission to access the requested data."; - type: STRING, - required: false; - }; - }; - }; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + tags: "Applications"; + summary: "Update SAML Application Config"; + description: "Update the SAML specific configuration of an application." + parameters: { + headers: { + name: "x-zitadel-orgid"; + description: "The default is always the organization of the requesting user. If you like to change/get objects of another organization include the header. Make sure the requesting user has permission to access the requested data."; + type: STRING, + required: false; + }; + }; + deprecated: true; + }; } + // Deprecated: Use [PatchApplication](/apis/resources/application_service_v2/application-service-patch-application.api.mdx) instead to update the config of an API app + rpc UpdateAPIAppConfig(UpdateAPIAppConfigRequest) returns (UpdateAPIAppConfigResponse) { + option (google.api.http) = { + put: "/projects/{project_id}/apps/{app_id}/api_config" + body: "*" + }; + + option (zitadel.v1.auth_option) = { + permission: "project.app.write" + check_field_name: "ProjectId" + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + tags: "Applications"; + summary: "Update API Application Config"; + description: "Update the OIDC-specific configuration of an application." + parameters: { + headers: { + name: "x-zitadel-orgid"; + description: "The default is always the organization of the requesting user. If you like to change/get objects of another organization include the header. Make sure the requesting user has permission to access the requested data."; + type: STRING, + required: false; + }; + }; + deprecated: true; + }; + } + + // Deprecated: Use [DeactivateApplication](/apis/resources/application_service_v2/application-service-deactivate-application.api.mdx) instead to deactivate an app rpc DeactivateApp(DeactivateAppRequest) returns (DeactivateAppResponse) { option (google.api.http) = { post: "/projects/{project_id}/apps/{app_id}/_deactivate" @@ -3569,9 +3594,11 @@ service ManagementService { required: false; }; }; + deprecated: true; }; } + // Deprecated: Use [ReactivateApplication](/apis/resources/application_service_v2/application-service-reactivate-application.api.mdx) instead to reactivate an app rpc ReactivateApp(ReactivateAppRequest) returns (ReactivateAppResponse) { option (google.api.http) = { post: "/projects/{project_id}/apps/{app_id}/_reactivate" @@ -3595,9 +3622,11 @@ service ManagementService { required: false; }; }; + deprecated: true; }; } + // Deprecated: Use [DeleteApplication](/apis/resources/application_service_v2/application-service-delete-application.api.mdx) instead to delete an app rpc RemoveApp(RemoveAppRequest) returns (RemoveAppResponse) { option (google.api.http) = { delete: "/projects/{project_id}/apps/{app_id}" @@ -3620,9 +3649,11 @@ service ManagementService { required: false; }; }; + deprecated: true; }; } + // Deprecated: Use [RegenerateClientSecret](/apis/resources/application_service_v2/application-service-regenerate-client-secret.api.mdx) instead to regenerate an OIDC app client secret rpc RegenerateOIDCClientSecret(RegenerateOIDCClientSecretRequest) returns (RegenerateOIDCClientSecretResponse) { option (google.api.http) = { post: "/projects/{project_id}/apps/{app_id}/oidc_config/_generate_client_secret" @@ -3646,9 +3677,11 @@ service ManagementService { required: false; }; }; + deprecated: true; }; } + // Deprecated: Use [RegenerateClientSecret](/apis/resources/application_service_v2/application-service-regenerate-client-secret.api.mdx) instead to regenerate an API app client secret rpc RegenerateAPIClientSecret(RegenerateAPIClientSecretRequest) returns (RegenerateAPIClientSecretResponse) { option (google.api.http) = { post: "/projects/{project_id}/apps/{app_id}/api_config/_generate_client_secret" @@ -3672,6 +3705,7 @@ service ManagementService { required: false; }; }; + deprecated: true; }; } From 14b45b58ebc955f7ee2b1161b74bd2f57ed07916 Mon Sep 17 00:00:00 2001 From: Florian Forster Date: Fri, 27 Jun 2025 13:46:21 -0700 Subject: [PATCH 3/3] chore: add inkeep search and ai to docs (#10119) --- docs/docusaurus.config.js | 111 ++- docs/package.json | 1 + docs/yarn.lock | 1776 ++++++++++++++++++++++++++++++------- 3 files changed, 1508 insertions(+), 380 deletions(-) diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js index 43830eafd0..abf5c742a5 100644 --- a/docs/docusaurus.config.js +++ b/docs/docusaurus.config.js @@ -71,13 +71,13 @@ module.exports = { label: "🚀 Quick Start", docId: "guides/start/quickstart", position: "left", - }, + }, { type: "doc", label: "Documentation", docId: "guides/overview", position: "left", - }, + }, { type: "doc", label: "APIs", @@ -174,20 +174,25 @@ module.exports = { { label: "Status", href: "https://status.zitadel.com/", - } + }, ], }, ], copyright: `Copyright © ${new Date().getFullYear()} ZITADEL Docs - Built with Docusaurus.`, }, - algolia: { - appId: "8H6ZKXENLO", - apiKey: "124fe1c102a184bc6fc70c75dc84f96f", - indexName: "zitadel", - selector: "div#", - }, prism: { - additionalLanguages: ["csharp", "dart", "groovy", "regex", "java", "php", "python", "protobuf", "json", "bash"], + additionalLanguages: [ + "csharp", + "dart", + "groovy", + "regex", + "java", + "php", + "python", + "protobuf", + "json", + "bash", + ], }, colorMode: { defaultMode: "dark", @@ -196,9 +201,9 @@ module.exports = { }, codeblock: { showGithubLink: true, - githubLinkLabel: 'View on GitHub', + githubLinkLabel: "View on GitHub", showRunmeLink: false, - runmeLinkLabel: 'Checkout via Runme' + runmeLinkLabel: "Checkout via Runme", }, }, presets: [ @@ -213,19 +218,33 @@ module.exports = { showLastUpdateTime: true, editUrl: "https://github.com/zitadel/zitadel/edit/main/docs/", remarkPlugins: [require("mdx-mermaid")], - - docItemComponent: '@theme/ApiItem' + + docItemComponent: "@theme/ApiItem", }, theme: { customCss: require.resolve("./src/css/custom.css"), }, - }) + }), ], - ], plugins: [ [ - 'docusaurus-plugin-openapi-docs', + "@inkeep/cxkit-docusaurus", + { + SearchBar: { + baseSettings: { + apiKey: process.env.INKEEP_API_KEY, + primaryBrandColor: "#ff2069", + organizationDisplayName: "ZITADEL", + }, + }, + SearchSettings: { + tabs: ["All", "Docs", "GitHub", "Forums", "Discord"], + }, + }, + ], + [ + "docusaurus-plugin-openapi-docs", { id: "apiDocs", docsPluginId: "classic", @@ -263,7 +282,8 @@ module.exports = { }, }, user_v2: { - specPath: ".artifacts/openapi/zitadel/user/v2/user_service.swagger.json", + specPath: + ".artifacts/openapi/zitadel/user/v2/user_service.swagger.json", outputDir: "docs/apis/resources/user_service_v2", sidebarOptions: { groupPathsBy: "tag", @@ -271,7 +291,8 @@ module.exports = { }, }, session_v2: { - specPath: ".artifacts/openapi/zitadel/session/v2/session_service.swagger.json", + specPath: + ".artifacts/openapi/zitadel/session/v2/session_service.swagger.json", outputDir: "docs/apis/resources/session_service_v2", sidebarOptions: { groupPathsBy: "tag", @@ -279,7 +300,8 @@ module.exports = { }, }, oidc_v2: { - specPath: ".artifacts/openapi/zitadel/oidc/v2/oidc_service.swagger.json", + specPath: + ".artifacts/openapi/zitadel/oidc/v2/oidc_service.swagger.json", outputDir: "docs/apis/resources/oidc_service_v2", sidebarOptions: { groupPathsBy: "tag", @@ -287,7 +309,8 @@ module.exports = { }, }, saml_v2: { - specPath: ".artifacts/openapi/zitadel/saml/v2/saml_service.swagger.json", + specPath: + ".artifacts/openapi/zitadel/saml/v2/saml_service.swagger.json", outputDir: "docs/apis/resources/saml_service_v2", sidebarOptions: { groupPathsBy: "tag", @@ -295,7 +318,8 @@ module.exports = { }, }, settings_v2: { - specPath: ".artifacts/openapi/zitadel/settings/v2/settings_service.swagger.json", + specPath: + ".artifacts/openapi/zitadel/settings/v2/settings_service.swagger.json", outputDir: "docs/apis/resources/settings_service_v2", sidebarOptions: { groupPathsBy: "tag", @@ -303,31 +327,35 @@ module.exports = { }, }, action_v2: { - specPath: ".artifacts/openapi/zitadel/action/v2beta/action_service.swagger.json", + specPath: + ".artifacts/openapi/zitadel/action/v2beta/action_service.swagger.json", outputDir: "docs/apis/resources/action_service_v2", sidebarOptions: { - groupPathsBy: "tag", - categoryLinkSource: "auto", + groupPathsBy: "tag", + categoryLinkSource: "auto", }, }, webkey_v2: { - specPath: ".artifacts/openapi/zitadel/webkey/v2beta/webkey_service.swagger.json", + specPath: + ".artifacts/openapi/zitadel/webkey/v2beta/webkey_service.swagger.json", outputDir: "docs/apis/resources/webkey_service_v2", sidebarOptions: { - groupPathsBy: "tag", - categoryLinkSource: "auto", + groupPathsBy: "tag", + categoryLinkSource: "auto", }, }, feature_v2: { - specPath: ".artifacts/openapi/zitadel/feature/v2/feature_service.swagger.json", + specPath: + ".artifacts/openapi/zitadel/feature/v2/feature_service.swagger.json", outputDir: "docs/apis/resources/feature_service_v2", sidebarOptions: { - groupPathsBy: "tag", - categoryLinkSource: "auto", + groupPathsBy: "tag", + categoryLinkSource: "auto", }, }, org_v2: { - specPath: ".artifacts/openapi/zitadel/org/v2/org_service.swagger.json", + specPath: + ".artifacts/openapi/zitadel/org/v2/org_service.swagger.json", outputDir: "docs/apis/resources/org_service_v2", sidebarOptions: { groupPathsBy: "tag", @@ -335,7 +363,8 @@ module.exports = { }, }, idp_v2: { - specPath: ".artifacts/openapi/zitadel/idp/v2/idp_service.swagger.json", + specPath: + ".artifacts/openapi/zitadel/idp/v2/idp_service.swagger.json", outputDir: "docs/apis/resources/idp_service_v2", sidebarOptions: { groupPathsBy: "tag", @@ -343,7 +372,8 @@ module.exports = { }, }, org_v2beta: { - specPath: ".artifacts/openapi/zitadel/org/v2beta/org_service.swagger.json", + specPath: + ".artifacts/openapi/zitadel/org/v2beta/org_service.swagger.json", outputDir: "docs/apis/resources/org_service_v2beta", sidebarOptions: { groupPathsBy: "tag", @@ -351,7 +381,8 @@ module.exports = { }, }, project_v2beta: { - specPath: ".artifacts/openapi/zitadel/project/v2beta/project_service.swagger.json", + specPath: + ".artifacts/openapi/zitadel/project/v2beta/project_service.swagger.json", outputDir: "docs/apis/resources/project_service_v2", sidebarOptions: { groupPathsBy: "tag", @@ -359,7 +390,8 @@ module.exports = { }, }, instance_v2: { - specPath: ".artifacts/openapi/zitadel/instance/v2beta/instance_service.swagger.json", + specPath: + ".artifacts/openapi/zitadel/instance/v2beta/instance_service.swagger.json", outputDir: "docs/apis/resources/instance_service_v2", sidebarOptions: { groupPathsBy: "tag", @@ -382,13 +414,16 @@ module.exports = { }; }, ], - themes: [ "docusaurus-theme-github-codeblock", "docusaurus-theme-openapi-docs"], + themes: [ + "docusaurus-theme-github-codeblock", + "docusaurus-theme-openapi-docs", + ], future: { v4: false, // Disabled because of some problems related to https://github.com/facebook/docusaurus/issues/11040 experimental_faster: { swcJsLoader: false, // Disabled because of memory usage > 8GB which is a problem on vercel default runners swcJsMinimizer: true, - swcHtmlMinimizer : true, + swcHtmlMinimizer: true, lightningCssMinimizer: true, mdxCrossCompilerCache: true, ssgWorkerThreads: false, // Disabled because of some problems related to https://github.com/facebook/docusaurus/issues/11040 diff --git a/docs/package.json b/docs/package.json index 2c9eb8bb84..2e1214f378 100644 --- a/docs/package.json +++ b/docs/package.json @@ -29,6 +29,7 @@ "@docusaurus/theme-search-algolia": "^3.8.1", "@headlessui/react": "^1.7.4", "@heroicons/react": "^2.0.13", + "@inkeep/cxkit-docusaurus": "^0.5.89", "autoprefixer": "^10.4.13", "clsx": "^1.2.1", "docusaurus-plugin-image-zoom": "^3.0.1", diff --git a/docs/yarn.lock b/docs/yarn.lock index c933386f97..c48c5b8bd6 100644 --- a/docs/yarn.lock +++ b/docs/yarn.lock @@ -29,126 +29,126 @@ resolved "https://registry.yarnpkg.com/@algolia/autocomplete-shared/-/autocomplete-shared-1.17.9.tgz#5f38868f7cb1d54b014b17a10fc4f7e79d427fa8" integrity sha512-iDf05JDQ7I0b7JEA/9IektxN/80a2MZ1ToohfmNS3rfeuQnIKI3IJlIafD0xu4StbtQTghx9T3Maa97ytkXenQ== -"@algolia/client-abtesting@5.25.0": - version "5.25.0" - resolved "https://registry.yarnpkg.com/@algolia/client-abtesting/-/client-abtesting-5.25.0.tgz#012204f1614e1a71366fb1e117c8f195186ff081" - integrity sha512-1pfQulNUYNf1Tk/svbfjfkLBS36zsuph6m+B6gDkPEivFmso/XnRgwDvjAx80WNtiHnmeNjIXdF7Gos8+OLHqQ== +"@algolia/client-abtesting@5.29.0": + version "5.29.0" + resolved "https://registry.yarnpkg.com/@algolia/client-abtesting/-/client-abtesting-5.29.0.tgz#af9928f3b206cc5224e30256ea27d4e4d6023f22" + integrity sha512-AM/6LYMSTnZvAT5IarLEKjYWOdV+Fb+LVs8JRq88jn8HH6bpVUtjWdOZXqX1hJRXuCAY8SdQfb7F8uEiMNXdYQ== dependencies: - "@algolia/client-common" "5.25.0" - "@algolia/requester-browser-xhr" "5.25.0" - "@algolia/requester-fetch" "5.25.0" - "@algolia/requester-node-http" "5.25.0" + "@algolia/client-common" "5.29.0" + "@algolia/requester-browser-xhr" "5.29.0" + "@algolia/requester-fetch" "5.29.0" + "@algolia/requester-node-http" "5.29.0" -"@algolia/client-analytics@5.25.0": - version "5.25.0" - resolved "https://registry.yarnpkg.com/@algolia/client-analytics/-/client-analytics-5.25.0.tgz#eba015bfafb3dbb82712c9160a00717a5974ff71" - integrity sha512-AFbG6VDJX/o2vDd9hqncj1B6B4Tulk61mY0pzTtzKClyTDlNP0xaUiEKhl6E7KO9I/x0FJF5tDCm0Hn6v5x18A== +"@algolia/client-analytics@5.29.0": + version "5.29.0" + resolved "https://registry.yarnpkg.com/@algolia/client-analytics/-/client-analytics-5.29.0.tgz#d71b2f6e6c77c390343ee0ab73806378adb295eb" + integrity sha512-La34HJh90l0waw3wl5zETO8TuukeUyjcXhmjYZL3CAPLggmKv74mobiGRIb+mmBENybiFDXf/BeKFLhuDYWMMQ== dependencies: - "@algolia/client-common" "5.25.0" - "@algolia/requester-browser-xhr" "5.25.0" - "@algolia/requester-fetch" "5.25.0" - "@algolia/requester-node-http" "5.25.0" + "@algolia/client-common" "5.29.0" + "@algolia/requester-browser-xhr" "5.29.0" + "@algolia/requester-fetch" "5.29.0" + "@algolia/requester-node-http" "5.29.0" -"@algolia/client-common@5.25.0": - version "5.25.0" - resolved "https://registry.yarnpkg.com/@algolia/client-common/-/client-common-5.25.0.tgz#2def8947efe849266057d92f67d1b8d83de0c005" - integrity sha512-il1zS/+Rc6la6RaCdSZ2YbJnkQC6W1wiBO8+SH+DE6CPMWBU6iDVzH0sCKSAtMWl9WBxoN6MhNjGBnCv9Yy2bA== +"@algolia/client-common@5.29.0": + version "5.29.0" + resolved "https://registry.yarnpkg.com/@algolia/client-common/-/client-common-5.29.0.tgz#0908e90c5dc881be08eab4e595bf981e23525474" + integrity sha512-T0lzJH/JiCxQYtCcnWy7Jf1w/qjGDXTi2npyF9B9UsTvXB97GRC6icyfXxe21mhYvhQcaB1EQ/J2575FXxi2rA== -"@algolia/client-insights@5.25.0": - version "5.25.0" - resolved "https://registry.yarnpkg.com/@algolia/client-insights/-/client-insights-5.25.0.tgz#b87df8614b96c4cc9c9aa7765cce07fa70864fa8" - integrity sha512-blbjrUH1siZNfyCGeq0iLQu00w3a4fBXm0WRIM0V8alcAPo7rWjLbMJMrfBtzL9X5ic6wgxVpDADXduGtdrnkw== +"@algolia/client-insights@5.29.0": + version "5.29.0" + resolved "https://registry.yarnpkg.com/@algolia/client-insights/-/client-insights-5.29.0.tgz#80ca3c3d16ff2fa78b3a6a091a10ae508977dffa" + integrity sha512-A39F1zmHY9aev0z4Rt3fTLcGN5AG1VsVUkVWy6yQG5BRDScktH+U5m3zXwThwniBTDV1HrPgiGHZeWb67GkR2Q== dependencies: - "@algolia/client-common" "5.25.0" - "@algolia/requester-browser-xhr" "5.25.0" - "@algolia/requester-fetch" "5.25.0" - "@algolia/requester-node-http" "5.25.0" + "@algolia/client-common" "5.29.0" + "@algolia/requester-browser-xhr" "5.29.0" + "@algolia/requester-fetch" "5.29.0" + "@algolia/requester-node-http" "5.29.0" -"@algolia/client-personalization@5.25.0": - version "5.25.0" - resolved "https://registry.yarnpkg.com/@algolia/client-personalization/-/client-personalization-5.25.0.tgz#74b041f0e7d91e1009c131c8d716c34e4d45c30f" - integrity sha512-aywoEuu1NxChBcHZ1pWaat0Plw7A8jDMwjgRJ00Mcl7wGlwuPt5dJ/LTNcg3McsEUbs2MBNmw0ignXBw9Tbgow== +"@algolia/client-personalization@5.29.0": + version "5.29.0" + resolved "https://registry.yarnpkg.com/@algolia/client-personalization/-/client-personalization-5.29.0.tgz#1bc8882fe889ad25132794b7beecf1cfc0783acc" + integrity sha512-ibxmh2wKKrzu5du02gp8CLpRMeo+b/75e4ORct98CT7mIxuYFXowULwCd6cMMkz/R0LpKXIbTUl15UL5soaiUQ== dependencies: - "@algolia/client-common" "5.25.0" - "@algolia/requester-browser-xhr" "5.25.0" - "@algolia/requester-fetch" "5.25.0" - "@algolia/requester-node-http" "5.25.0" + "@algolia/client-common" "5.29.0" + "@algolia/requester-browser-xhr" "5.29.0" + "@algolia/requester-fetch" "5.29.0" + "@algolia/requester-node-http" "5.29.0" -"@algolia/client-query-suggestions@5.25.0": - version "5.25.0" - resolved "https://registry.yarnpkg.com/@algolia/client-query-suggestions/-/client-query-suggestions-5.25.0.tgz#e92d935d9e2994f790d43c64d3518d81070a3888" - integrity sha512-a/W2z6XWKjKjIW1QQQV8PTTj1TXtaKx79uR3NGBdBdGvVdt24KzGAaN7sCr5oP8DW4D3cJt44wp2OY/fZcPAVA== +"@algolia/client-query-suggestions@5.29.0": + version "5.29.0" + resolved "https://registry.yarnpkg.com/@algolia/client-query-suggestions/-/client-query-suggestions-5.29.0.tgz#784001417cee2ffde376f10074a477eef1eb095d" + integrity sha512-VZq4/AukOoJC2WSwF6J5sBtt+kImOoBwQc1nH3tgI+cxJBg7B77UsNC+jT6eP2dQCwGKBBRTmtPLUTDDnHpMgA== dependencies: - "@algolia/client-common" "5.25.0" - "@algolia/requester-browser-xhr" "5.25.0" - "@algolia/requester-fetch" "5.25.0" - "@algolia/requester-node-http" "5.25.0" + "@algolia/client-common" "5.29.0" + "@algolia/requester-browser-xhr" "5.29.0" + "@algolia/requester-fetch" "5.29.0" + "@algolia/requester-node-http" "5.29.0" -"@algolia/client-search@5.25.0": - version "5.25.0" - resolved "https://registry.yarnpkg.com/@algolia/client-search/-/client-search-5.25.0.tgz#dc38ca1015f2f4c9f5053a4517f96fb28a2117f8" - integrity sha512-9rUYcMIBOrCtYiLX49djyzxqdK9Dya/6Z/8sebPn94BekT+KLOpaZCuc6s0Fpfq7nx5J6YY5LIVFQrtioK9u0g== +"@algolia/client-search@5.29.0": + version "5.29.0" + resolved "https://registry.yarnpkg.com/@algolia/client-search/-/client-search-5.29.0.tgz#91c9a036b6677d954cd87d9262850f73f145bf81" + integrity sha512-cZ0Iq3OzFUPpgszzDr1G1aJV5UMIZ4VygJ2Az252q4Rdf5cQMhYEIKArWY/oUjMhQmosM8ygOovNq7gvA9CdCg== dependencies: - "@algolia/client-common" "5.25.0" - "@algolia/requester-browser-xhr" "5.25.0" - "@algolia/requester-fetch" "5.25.0" - "@algolia/requester-node-http" "5.25.0" + "@algolia/client-common" "5.29.0" + "@algolia/requester-browser-xhr" "5.29.0" + "@algolia/requester-fetch" "5.29.0" + "@algolia/requester-node-http" "5.29.0" "@algolia/events@^4.0.1": version "4.0.1" resolved "https://registry.yarnpkg.com/@algolia/events/-/events-4.0.1.tgz#fd39e7477e7bc703d7f893b556f676c032af3950" integrity sha512-FQzvOCgoFXAbf5Y6mYozw2aj5KCJoA3m4heImceldzPSMbdyS4atVjJzXKMsfX3wnZTFYwkkt8/z8UesLHlSBQ== -"@algolia/ingestion@1.25.0": - version "1.25.0" - resolved "https://registry.yarnpkg.com/@algolia/ingestion/-/ingestion-1.25.0.tgz#4d13c56dda0a05c7bacb0e3ef5866292dfd86ed5" - integrity sha512-jJeH/Hk+k17Vkokf02lkfYE4A+EJX+UgnMhTLR/Mb+d1ya5WhE+po8p5a/Nxb6lo9OLCRl6w3Hmk1TX1e9gVbQ== +"@algolia/ingestion@1.29.0": + version "1.29.0" + resolved "https://registry.yarnpkg.com/@algolia/ingestion/-/ingestion-1.29.0.tgz#9d7f30a7161b1cb612309f8240aa471faac8a21f" + integrity sha512-scBXn0wO5tZCxmO6evfa7A3bGryfyOI3aoXqSQBj5SRvNYXaUlFWQ/iKI70gRe/82ICwE0ICXbHT/wIvxOW7vw== dependencies: - "@algolia/client-common" "5.25.0" - "@algolia/requester-browser-xhr" "5.25.0" - "@algolia/requester-fetch" "5.25.0" - "@algolia/requester-node-http" "5.25.0" + "@algolia/client-common" "5.29.0" + "@algolia/requester-browser-xhr" "5.29.0" + "@algolia/requester-fetch" "5.29.0" + "@algolia/requester-node-http" "5.29.0" -"@algolia/monitoring@1.25.0": - version "1.25.0" - resolved "https://registry.yarnpkg.com/@algolia/monitoring/-/monitoring-1.25.0.tgz#d59360cfe556338519d05a9d8107147e9dbcb020" - integrity sha512-Ls3i1AehJ0C6xaHe7kK9vPmzImOn5zBg7Kzj8tRYIcmCWVyuuFwCIsbuIIz/qzUf1FPSWmw0TZrGeTumk2fqXg== +"@algolia/monitoring@1.29.0": + version "1.29.0" + resolved "https://registry.yarnpkg.com/@algolia/monitoring/-/monitoring-1.29.0.tgz#919f86b7c53f1ea7c78f4c0ed9bd7917c1ca3a67" + integrity sha512-FGWWG9jLFhsKB7YiDjM2dwQOYnWu//7Oxrb2vT96N7+s+hg1mdHHfHNRyEudWdxd4jkMhBjeqNA21VbTiOIPVg== dependencies: - "@algolia/client-common" "5.25.0" - "@algolia/requester-browser-xhr" "5.25.0" - "@algolia/requester-fetch" "5.25.0" - "@algolia/requester-node-http" "5.25.0" + "@algolia/client-common" "5.29.0" + "@algolia/requester-browser-xhr" "5.29.0" + "@algolia/requester-fetch" "5.29.0" + "@algolia/requester-node-http" "5.29.0" -"@algolia/recommend@5.25.0": - version "5.25.0" - resolved "https://registry.yarnpkg.com/@algolia/recommend/-/recommend-5.25.0.tgz#b96f12c85aa74a0326982c7801fcd4a610b420f4" - integrity sha512-79sMdHpiRLXVxSjgw7Pt4R1aNUHxFLHiaTDnN2MQjHwJ1+o3wSseb55T9VXU4kqy3m7TUme3pyRhLk5ip/S4Mw== +"@algolia/recommend@5.29.0": + version "5.29.0" + resolved "https://registry.yarnpkg.com/@algolia/recommend/-/recommend-5.29.0.tgz#8f2e5fe2e43e6d1dfa488b4c095404e46d0e1b0c" + integrity sha512-xte5+mpdfEARAu61KXa4ewpjchoZuJlAlvQb8ptK6hgHlBHDnYooy1bmOFpokaAICrq/H9HpoqNUX71n+3249A== dependencies: - "@algolia/client-common" "5.25.0" - "@algolia/requester-browser-xhr" "5.25.0" - "@algolia/requester-fetch" "5.25.0" - "@algolia/requester-node-http" "5.25.0" + "@algolia/client-common" "5.29.0" + "@algolia/requester-browser-xhr" "5.29.0" + "@algolia/requester-fetch" "5.29.0" + "@algolia/requester-node-http" "5.29.0" -"@algolia/requester-browser-xhr@5.25.0": - version "5.25.0" - resolved "https://registry.yarnpkg.com/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.25.0.tgz#c194fa5f49206b9343e6646c41bfbca2a3f2ac54" - integrity sha512-JLaF23p1SOPBmfEqozUAgKHQrGl3z/Z5RHbggBu6s07QqXXcazEsub5VLonCxGVqTv6a61AAPr8J1G5HgGGjEw== +"@algolia/requester-browser-xhr@5.29.0": + version "5.29.0" + resolved "https://registry.yarnpkg.com/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.29.0.tgz#c3cec914716160d3d972ff09b3b35093916cb5bb" + integrity sha512-og+7Em75aPHhahEUScq2HQ3J7ULN63Levtd87BYMpn6Im5d5cNhaC4QAUsXu6LWqxRPgh4G+i+wIb6tVhDhg2A== dependencies: - "@algolia/client-common" "5.25.0" + "@algolia/client-common" "5.29.0" -"@algolia/requester-fetch@5.25.0": - version "5.25.0" - resolved "https://registry.yarnpkg.com/@algolia/requester-fetch/-/requester-fetch-5.25.0.tgz#231a2d0da2397d141f80b8f28e2cb6e3d219d38d" - integrity sha512-rtzXwqzFi1edkOF6sXxq+HhmRKDy7tz84u0o5t1fXwz0cwx+cjpmxu/6OQKTdOJFS92JUYHsG51Iunie7xbqfQ== +"@algolia/requester-fetch@5.29.0": + version "5.29.0" + resolved "https://registry.yarnpkg.com/@algolia/requester-fetch/-/requester-fetch-5.29.0.tgz#3d885d73ab116c4c1ae88e7e6fb3b022cba45ce8" + integrity sha512-JCxapz7neAy8hT/nQpCvOrI5JO8VyQ1kPvBiaXWNC1prVq0UMYHEL52o1BsPvtXfdQ7BVq19OIq6TjOI06mV/w== dependencies: - "@algolia/client-common" "5.25.0" + "@algolia/client-common" "5.29.0" -"@algolia/requester-node-http@5.25.0": - version "5.25.0" - resolved "https://registry.yarnpkg.com/@algolia/requester-node-http/-/requester-node-http-5.25.0.tgz#0ce13c550890de21c558b04381535d2d245a3725" - integrity sha512-ZO0UKvDyEFvyeJQX0gmZDQEvhLZ2X10K+ps6hViMo1HgE2V8em00SwNsQ+7E/52a+YiBkVWX61pJJJE44juDMQ== +"@algolia/requester-node-http@5.29.0": + version "5.29.0" + resolved "https://registry.yarnpkg.com/@algolia/requester-node-http/-/requester-node-http-5.29.0.tgz#9e8fb975c392ba1a99b8774856cfc892ed17819e" + integrity sha512-lVBD81RBW5VTdEYgnzCz7Pf9j2H44aymCP+/eHGJu4vhU+1O8aKf3TVBgbQr5UM6xoe8IkR/B112XY6YIG2vtg== dependencies: - "@algolia/client-common" "5.25.0" + "@algolia/client-common" "5.29.0" "@alloc/quick-lru@^5.2.0": version "5.2.0" @@ -217,9 +217,9 @@ integrity sha512-qJzAIcv03PyaWqxRgO4mSU3lihncDT296vnyuE2O8uA4w3UHWI4S3hgeZd1L8W1Bft40w9JxJ2b412iDUFFRhw== "@babel/compat-data@^7.27.2": - version "7.27.3" - resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.27.3.tgz#cc49c2ac222d69b889bf34c795f537c0c6311111" - integrity sha512-V42wFfx1ymFte+ecf6iXghnnP8kWTO+ZLXIyZq+1LAXHHvTZdVxicn4yiVYdYMGaCO3tmqub11AorKkv+iodqw== + version "7.27.5" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.27.5.tgz#7d0658ec1a8420fc866d1df1b03bea0e79934c82" + integrity sha512-KiRAp/VoJaWkkte84TvUd9qjdbZAdiqyvMxrGl1N6vzFogKmaLgoM3L1kgtLicp2HP5fBJS8JrZKLVIZGVJAVg== "@babel/core@^7.21.3": version "7.24.7" @@ -243,19 +243,19 @@ semver "^6.3.1" "@babel/core@^7.25.9": - version "7.27.3" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.27.3.tgz#d7d05502bccede3cab36373ed142e6a1df554c2f" - integrity sha512-hyrN8ivxfvJ4i0fIJuV4EOlV0WDMz5Ui4StRTgVaAvWeiRCilXgwVvxJKtFQ3TKtHgJscB2YiXKGNJuVwhQMtA== + version "7.27.4" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.27.4.tgz#cc1fc55d0ce140a1828d1dd2a2eba285adbfb3ce" + integrity sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g== dependencies: "@ampproject/remapping" "^2.2.0" "@babel/code-frame" "^7.27.1" "@babel/generator" "^7.27.3" "@babel/helper-compilation-targets" "^7.27.2" "@babel/helper-module-transforms" "^7.27.3" - "@babel/helpers" "^7.27.3" - "@babel/parser" "^7.27.3" + "@babel/helpers" "^7.27.4" + "@babel/parser" "^7.27.4" "@babel/template" "^7.27.2" - "@babel/traverse" "^7.27.3" + "@babel/traverse" "^7.27.4" "@babel/types" "^7.27.3" convert-source-map "^2.0.0" debug "^4.1.0" @@ -274,11 +274,11 @@ jsesc "^2.5.1" "@babel/generator@^7.25.9", "@babel/generator@^7.27.3": - version "7.27.3" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.27.3.tgz#ef1c0f7cfe3b5fc8cbb9f6cc69f93441a68edefc" - integrity sha512-xnlJYj5zepml8NXtjkG0WquFUv8RskFqyFcVgTBp5k+NaA/8uw/K+OSVf8AMGw5e9HKP2ETd5xpK5MLZQD6b4Q== + version "7.27.5" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.27.5.tgz#3eb01866b345ba261b04911020cbe22dd4be8c8c" + integrity sha512-ZGhA37l0e/g2s1Cnzdix0O3aLYm66eF8aufiVteOgnwxgnRP8GoyMj7VWsgWnQbVKXyge7hqrFh2K2TQM6t1Hw== dependencies: - "@babel/parser" "^7.27.3" + "@babel/parser" "^7.27.5" "@babel/types" "^7.27.3" "@jridgewell/gen-mapping" "^0.3.5" "@jridgewell/trace-mapping" "^0.3.25" @@ -628,13 +628,13 @@ "@babel/template" "^7.27.0" "@babel/types" "^7.27.0" -"@babel/helpers@^7.27.3": - version "7.27.3" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.27.3.tgz#387d65d279290e22fe7a47a8ffcd2d0c0184edd0" - integrity sha512-h/eKy9agOya1IGuLaZ9tEUgz+uIRXcbtOhRtUyyMf8JFmn1iT13vnl/IGVWSkdOCG/pC57U4S1jnAabAavTMwg== +"@babel/helpers@^7.27.4": + version "7.27.6" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.27.6.tgz#6456fed15b2cb669d2d1fabe84b66b34991d812c" + integrity sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug== dependencies: "@babel/template" "^7.27.2" - "@babel/types" "^7.27.3" + "@babel/types" "^7.27.6" "@babel/highlight@^7.24.7": version "7.24.7" @@ -658,10 +658,10 @@ dependencies: "@babel/types" "^7.27.0" -"@babel/parser@^7.27.2", "@babel/parser@^7.27.3": - version "7.27.3" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.27.3.tgz#1b7533f0d908ad2ac545c4d05cbe2fb6dc8cfaaf" - integrity sha512-xyYxRj6+tLNDTWi0KCBcZ9V7yg3/lwL9DWh9Uwh/RIVlIfFidggcgxKX3GCXwCiswwcGRawBKbEg2LG/Y8eJhw== +"@babel/parser@^7.27.2", "@babel/parser@^7.27.4", "@babel/parser@^7.27.5": + version "7.27.5" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.27.5.tgz#ed22f871f110aa285a6fd934a0efed621d118826" + integrity sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg== dependencies: "@babel/types" "^7.27.3" @@ -983,9 +983,9 @@ "@babel/helper-plugin-utils" "^7.24.7" "@babel/plugin-transform-block-scoping@^7.27.1": - version "7.27.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.27.3.tgz#a21f37e222dc0a7b91c3784fa3bd4edf8d7a6dc1" - integrity sha512-+F8CnfhuLhwUACIJMLWnjz6zvzYM2r0yeIHKlbgfw7ml8rOMJsXNXV/hyRcb3nb493gRs4WvYpQAndWj/qQmkQ== + version "7.27.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.27.5.tgz#98c37485d815533623d992fd149af3e7b3140157" + integrity sha512-JF6uE2s67f0y2RZcm2kpAUEbD50vH62TyWVebxwHAlbSdM49VqPz8t4a1uIjp4NIOIZ4xzLfjY5emt/RCyC7TQ== dependencies: "@babel/helper-plugin-utils" "^7.27.1" @@ -1595,9 +1595,9 @@ regenerator-transform "^0.15.2" "@babel/plugin-transform-regenerator@^7.27.1": - version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.27.1.tgz#0a471df9213416e44cd66bf67176b66f65768401" - integrity sha512-B19lbbL7PMrKr52BNPjCqg1IyNUIjTcxKj8uX9zHO+PmWN93s19NDr/f69mIkEp2x9nmDJ08a7lgHaTTzvW7mw== + version "7.27.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.27.5.tgz#0c01f4e0e4cced15f68ee14b9c76dac9813850c7" + integrity sha512-uhB8yHerfe3MWnuLAhEbeQ4afVoqv8BQsPqrTv7e/jZ9y00kJL6l9a/f4OWaKxotmjzewfEyXE1vgDJenkQ2/Q== dependencies: "@babel/helper-plugin-utils" "^7.27.1" @@ -1624,9 +1624,9 @@ "@babel/helper-plugin-utils" "^7.27.1" "@babel/plugin-transform-runtime@^7.25.9": - version "7.27.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.27.3.tgz#ad35f1eff5ba18a5e23f7270e939fb5a59d3ec0b" - integrity sha512-bA9ZL5PW90YwNgGfjg6U+7Qh/k3zCEQJ06BFgAGRp/yMjw9hP9UGbGPtx3KSOkHGljEPCCxaE+PH4fUR2h1sDw== + version "7.27.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.27.4.tgz#dee5c5db6543313d1ae1b4b1ec122ff1e77352b9" + integrity sha512-D68nR5zxU64EUzV8i7T3R5XP0Xhrou/amNnddsRQssx6GrTLdZl1rLxyjtVZBd+v/NVX4AbTPOB5aU8thAZV1A== dependencies: "@babel/helper-module-imports" "^7.27.1" "@babel/helper-plugin-utils" "^7.27.1" @@ -2013,13 +2013,13 @@ integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA== "@babel/runtime-corejs3@^7.25.9": - version "7.27.3" - resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.27.3.tgz#b971a4a0a376171e266629152e74ef50e9931f79" - integrity sha512-ZYcgrwb+dkWNcDlsTe4fH1CMdqMDSJ5lWFd1by8Si2pI54XcQjte/+ViIPqAk7EAWisaUxvQ89grv+bNX2x8zg== + version "7.27.6" + resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.27.6.tgz#97644153808a62898e7c05f3361501417db3c48b" + integrity sha512-vDVrlmRAY8z9Ul/HxT+8ceAru95LQgkSKiXkSYZvqtbkPSfhZJgpRp45Cldbh1GJ1kxzQkI70AqyrTI58KpaWQ== dependencies: core-js-pure "^3.30.2" -"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.3", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.15.4", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2": +"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.3", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.15.4", "@babel/runtime@^7.20.13", "@babel/runtime@^7.23.2", "@babel/runtime@^7.26.0", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2": version "7.27.0" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.27.0.tgz#fbee7cf97c709518ecc1f590984481d5460d4762" integrity sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw== @@ -2027,9 +2027,9 @@ regenerator-runtime "^0.14.0" "@babel/runtime@^7.25.9": - version "7.27.3" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.27.3.tgz#10491113799fb8d77e1d9273384d5d68deeea8f6" - integrity sha512-7EYtGezsdiDMyY80+65EzwiGmcJqpmcZCojSXaRgdrBaGtWTgDZKq69cPIVped6MkIM78cTQ2GOiEYjwOlG4xw== + version "7.27.6" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.27.6.tgz#ec4070a04d76bae8ddbb10770ba55714a417b7c6" + integrity sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q== "@babel/template@^7.24.7": version "7.24.7" @@ -2074,14 +2074,14 @@ debug "^4.3.1" globals "^11.1.0" -"@babel/traverse@^7.25.9", "@babel/traverse@^7.27.1", "@babel/traverse@^7.27.3": - version "7.27.3" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.27.3.tgz#8b62a6c2d10f9d921ba7339c90074708509cffae" - integrity sha512-lId/IfN/Ye1CIu8xG7oKBHXd2iNb2aW1ilPszzGcJug6M8RCKfVNcYhpI5+bMvFYjK7lXIM0R+a+6r8xhHp2FQ== +"@babel/traverse@^7.25.9", "@babel/traverse@^7.27.1", "@babel/traverse@^7.27.3", "@babel/traverse@^7.27.4": + version "7.27.4" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.27.4.tgz#b0045ac7023c8472c3d35effd7cc9ebd638da6ea" + integrity sha512-oNcu2QbHqts9BtOWJosOVJapWjBDSxGCpFvikNR5TGDYDQf3JwpIoMzIKrvfoti93cLfPJEG4tH9SPVeyCGgdA== dependencies: "@babel/code-frame" "^7.27.1" "@babel/generator" "^7.27.3" - "@babel/parser" "^7.27.3" + "@babel/parser" "^7.27.4" "@babel/template" "^7.27.2" "@babel/types" "^7.27.3" debug "^4.3.1" @@ -2104,10 +2104,10 @@ "@babel/helper-string-parser" "^7.25.9" "@babel/helper-validator-identifier" "^7.25.9" -"@babel/types@^7.27.1", "@babel/types@^7.27.3": - version "7.27.3" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.27.3.tgz#c0257bedf33aad6aad1f406d35c44758321eb3ec" - integrity sha512-Y1GkI4ktrtvmawoSq+4FCVHNryea6uR+qUQy0AGxLSsjCX0nVmkYQMBLHDkXZuo5hGx7eYdnIaslsdBFm7zbUw== +"@babel/types@^7.27.1", "@babel/types@^7.27.3", "@babel/types@^7.27.6": + version "7.27.6" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.27.6.tgz#a434ca7add514d4e646c80f7375c0aa2befc5535" + integrity sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q== dependencies: "@babel/helper-string-parser" "^7.27.1" "@babel/helper-validator-identifier" "^7.27.1" @@ -3072,6 +3072,33 @@ resolved "https://registry.yarnpkg.com/@faker-js/faker/-/faker-5.5.3.tgz#18e3af6b8eae7984072bbeb0c0858474d7c4cefe" integrity sha512-R11tGE6yIFwqpaIqcfkcg7AICXzFg14+5h5v0TfF/9+RMDL6jhzCy/pxHVOfbALGdtVYdt6JdR21tuxEgl34dw== +"@floating-ui/core@^1.6.0": + version "1.6.9" + resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.6.9.tgz#64d1da251433019dafa091de9b2886ff35ec14e6" + integrity sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw== + dependencies: + "@floating-ui/utils" "^0.2.9" + +"@floating-ui/dom@^1.0.0": + version "1.6.13" + resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.6.13.tgz#a8a938532aea27a95121ec16e667a7cbe8c59e34" + integrity sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w== + dependencies: + "@floating-ui/core" "^1.6.0" + "@floating-ui/utils" "^0.2.9" + +"@floating-ui/react-dom@^2.0.0": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-2.1.2.tgz#a1349bbf6a0e5cb5ded55d023766f20a4d439a31" + integrity sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A== + dependencies: + "@floating-ui/dom" "^1.0.0" + +"@floating-ui/utils@^0.2.9": + version "0.2.9" + resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.9.tgz#50dea3616bc8191fb8e112283b49eaff03e78429" + integrity sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg== + "@hapi/hoek@^9.0.0", "@hapi/hoek@^9.3.0": version "9.3.0" resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.3.0.tgz#8368869dcb735be2e7f5cb7647de78e167a251fb" @@ -3121,6 +3148,104 @@ local-pkg "^1.0.0" mlly "^1.7.4" +"@inkeep/cxkit-color-mode@0.5.89": + version "0.5.89" + resolved "https://registry.yarnpkg.com/@inkeep/cxkit-color-mode/-/cxkit-color-mode-0.5.89.tgz#4a5471b3dc453262ef0277908c30e108b1095331" + integrity sha512-h89/i67uEiJh0Bqf/dt9nJWv3IjCnmu96nkomocZ3evPKrLeBq13IygFIxtvmvXfx1QBBdmAD+x933rc5RcgFA== + +"@inkeep/cxkit-docusaurus@^0.5.89": + version "0.5.89" + resolved "https://registry.yarnpkg.com/@inkeep/cxkit-docusaurus/-/cxkit-docusaurus-0.5.89.tgz#aff4035fe2bb69401d1677393dcd2943bf3e0d1f" + integrity sha512-z4OxGLoPVbk6ZcKW5i/MrGJUx6Wyc5zh2mxOt2IHTQgFL3XiArCKds0R8jSxSi8SB/a9j5Wm/X0FinT+or9NKA== + dependencies: + "@inkeep/cxkit-react" "0.5.89" + merge-anything "5.1.7" + path "^0.12.7" + +"@inkeep/cxkit-primitives@0.5.89": + version "0.5.89" + resolved "https://registry.yarnpkg.com/@inkeep/cxkit-primitives/-/cxkit-primitives-0.5.89.tgz#1f8252f18754aab2c28dd0e1436381b4323d2c0b" + integrity sha512-ugC80ivuimKmzcm8RktAL9C7YHss7CneerbHnNy+uipCx9ZcIY5dRfgMqIdoLG6fL7fhSm6E0QElGL8YjjfN6g== + dependencies: + "@inkeep/cxkit-color-mode" "0.5.89" + "@inkeep/cxkit-theme" "0.5.89" + "@inkeep/cxkit-types" "0.5.89" + "@radix-ui/primitive" "^1.1.1" + "@radix-ui/react-avatar" "1.1.2" + "@radix-ui/react-checkbox" "1.1.3" + "@radix-ui/react-compose-refs" "^1.1.1" + "@radix-ui/react-context" "^1.1.1" + "@radix-ui/react-dismissable-layer" "^1.1.5" + "@radix-ui/react-focus-guards" "^1.1.1" + "@radix-ui/react-focus-scope" "^1.1.2" + "@radix-ui/react-hover-card" "^1.1.6" + "@radix-ui/react-id" "^1.1.0" + "@radix-ui/react-popover" "1.1.6" + "@radix-ui/react-portal" "^1.1.4" + "@radix-ui/react-presence" "^1.1.2" + "@radix-ui/react-primitive" "^2.0.2" + "@radix-ui/react-scroll-area" "1.2.2" + "@radix-ui/react-select" "^2.1.7" + "@radix-ui/react-slot" "^1.2.0" + "@radix-ui/react-tabs" "^1.1.4" + "@radix-ui/react-tooltip" "1.1.6" + "@radix-ui/react-use-callback-ref" "^1.1.0" + "@radix-ui/react-use-controllable-state" "^1.1.0" + "@zag-js/focus-trap" "^1.7.0" + "@zag-js/presence" "^1.13.1" + "@zag-js/react" "^1.13.1" + altcha-lib "^1.2.0" + aria-hidden "^1.2.4" + dequal "^2.0.3" + humps "2.0.1" + lucide-react "^0.503.0" + marked "^15.0.9" + merge-anything "5.1.7" + openai "4.78.1" + prism-react-renderer "2.4.1" + react-error-boundary "^6.0.0" + react-hook-form "7.54.2" + react-markdown "9.0.3" + react-remove-scroll "^2.7.1" + react-svg "16.3.0" + react-textarea-autosize "8.5.7" + rehype-raw "7.0.0" + remark-gfm "^4.0.1" + unist-util-visit "^5.0.0" + use-sync-external-store "^1.4.0" + +"@inkeep/cxkit-react@0.5.89": + version "0.5.89" + resolved "https://registry.yarnpkg.com/@inkeep/cxkit-react/-/cxkit-react-0.5.89.tgz#4bc37852bc6161ed4dc5b44b3ceb8beddf49f6f8" + integrity sha512-v86J6xe86kgKfDzlNGZSGHQ3PB8KJ46ra8xnVV4RrRjp7kPGiR7MvGfalFcSiaSEQzWiAcKAl4gya/AF/J/OZw== + dependencies: + "@inkeep/cxkit-styled" "0.5.89" + "@radix-ui/react-use-controllable-state" "^1.1.0" + lucide-react "^0.503.0" + +"@inkeep/cxkit-styled@0.5.89": + version "0.5.89" + resolved "https://registry.yarnpkg.com/@inkeep/cxkit-styled/-/cxkit-styled-0.5.89.tgz#e113ee393f5055457281a52cfb8221dd14f70661" + integrity sha512-w9V3vYuq4ytluow16RO/0/V1s9PSBqOjZdvATdn+jy06gUn5ClNAiJ79m34fB3Ep0Y6o2m+obujX4njw4A+LPw== + dependencies: + "@inkeep/cxkit-primitives" "0.5.89" + class-variance-authority "0.7.1" + clsx "2.1.1" + merge-anything "5.1.7" + tailwind-merge "2.6.0" + +"@inkeep/cxkit-theme@0.5.89": + version "0.5.89" + resolved "https://registry.yarnpkg.com/@inkeep/cxkit-theme/-/cxkit-theme-0.5.89.tgz#b1f9f7be2a87f25b8c6b2c4eb654b998b6bd7cc8" + integrity sha512-Yji2OCDi2buYZQXY4tw93U6W3ZFDaNw7wgGLP+vTyRZaFBMGwW52zNOjewz2UL1jNGl6ublHXmWM+kjIC4b5SQ== + dependencies: + colorjs.io "0.5.2" + +"@inkeep/cxkit-types@0.5.89": + version "0.5.89" + resolved "https://registry.yarnpkg.com/@inkeep/cxkit-types/-/cxkit-types-0.5.89.tgz#f8db85cca7c8dbb72c6a035882435b5e0e86ca76" + integrity sha512-zz6945Ex9kSpIUeZaVAX4h6HeCaOt28BzZyuprQWXIpzvAlFKuKDNV1Zm5umEglaiGSx+T9J6WViy3PoRXwTtA== + "@isaacs/cliui@^8.0.2": version "8.0.2" resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550" @@ -3238,10 +3363,10 @@ dependencies: "@types/mdx" "^2.0.0" -"@mermaid-js/parser@^0.4.0": - version "0.4.0" - resolved "https://registry.yarnpkg.com/@mermaid-js/parser/-/parser-0.4.0.tgz#c1de1f5669f8fcbd0d0c9d124927d36ddc00d8a6" - integrity sha512-wla8XOWvQAwuqy+gxiZqY+c7FokraOTHRWMsbB4AgRx9Sy7zKslNyejy7E+a77qHfey5GXw/ik3IXv/NHMJgaA== +"@mermaid-js/parser@^0.5.0": + version "0.5.0" + resolved "https://registry.yarnpkg.com/@mermaid-js/parser/-/parser-0.5.0.tgz#63d676e930b0cfd6abfeadee46fb228761438ce6" + integrity sha512-AiaN7+VjXC+3BYE+GwNezkpjIcCI2qIMB/K4S2/vMWe0q/XJCBbx5+K7iteuz7VyltX9iAK4FmVTvGc9kjOV4w== dependencies: langium "3.3.1" @@ -3429,6 +3554,551 @@ resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.25.tgz#f077fdc0b5d0078d30893396ff4827a13f99e817" integrity sha512-j7P6Rgr3mmtdkeDGTe0E/aYyWEWVtc5yFXtHCRHs28/jptDEWfaVOc5T7cblqy1XKPPfCxJc/8DwQ5YgLOZOVQ== +"@radix-ui/number@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@radix-ui/number/-/number-1.1.0.tgz#1e95610461a09cdf8bb05c152e76ca1278d5da46" + integrity sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ== + +"@radix-ui/number@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/number/-/number-1.1.1.tgz#7b2c9225fbf1b126539551f5985769d0048d9090" + integrity sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g== + +"@radix-ui/primitive@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/primitive/-/primitive-1.1.1.tgz#fc169732d755c7fbad33ba8d0cd7fd10c90dc8e3" + integrity sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA== + +"@radix-ui/primitive@1.1.2", "@radix-ui/primitive@^1.1.1": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@radix-ui/primitive/-/primitive-1.1.2.tgz#83f415c4425f21e3d27914c12b3272a32e3dae65" + integrity sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA== + +"@radix-ui/react-arrow@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-arrow/-/react-arrow-1.1.1.tgz#2103721933a8bfc6e53bbfbdc1aaad5fc8ba0dd7" + integrity sha512-NaVpZfmv8SKeZbn4ijN2V3jlHA9ngBG16VnIIm22nUR0Yk8KUALyBxT3KYEUnNuch9sTE8UTsS3whzBgKOL30w== + dependencies: + "@radix-ui/react-primitive" "2.0.1" + +"@radix-ui/react-arrow@1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@radix-ui/react-arrow/-/react-arrow-1.1.2.tgz#30c0d574d7bb10eed55cd7007b92d38b03c6b2ab" + integrity sha512-G+KcpzXHq24iH0uGG/pF8LyzpFJYGD4RfLjCIBfGdSLXvjLHST31RUiRVrupIBMvIppMgSzQ6l66iAxl03tdlg== + dependencies: + "@radix-ui/react-primitive" "2.0.2" + +"@radix-ui/react-arrow@1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@radix-ui/react-arrow/-/react-arrow-1.1.3.tgz#8926eb1d87f73c2e047eac96703949f168c85861" + integrity sha512-2dvVU4jva0qkNZH6HHWuSz5FN5GeU5tymvCgutF8WaXz9WnD1NgUhy73cqzkjkN4Zkn8lfTPv5JIfrC221W+Nw== + dependencies: + "@radix-ui/react-primitive" "2.0.3" + +"@radix-ui/react-avatar@1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@radix-ui/react-avatar/-/react-avatar-1.1.2.tgz#24af4c66bb5271460a4a6b74c4f4f9d4789d3d90" + integrity sha512-GaC7bXQZ5VgZvVvsJ5mu/AEbjYLnhhkoidOboC50Z6FFlLA03wG2ianUoH+zgDQ31/9gCF59bE4+2bBgTyMiig== + dependencies: + "@radix-ui/react-context" "1.1.1" + "@radix-ui/react-primitive" "2.0.1" + "@radix-ui/react-use-callback-ref" "1.1.0" + "@radix-ui/react-use-layout-effect" "1.1.0" + +"@radix-ui/react-checkbox@1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@radix-ui/react-checkbox/-/react-checkbox-1.1.3.tgz#0e2ab913fddf3c88603625f7a9457d73882c8a32" + integrity sha512-HD7/ocp8f1B3e6OHygH0n7ZKjONkhciy1Nh0yuBgObqThc3oyx+vuMfFHKAknXRHHWVE9XvXStxJFyjUmB8PIw== + dependencies: + "@radix-ui/primitive" "1.1.1" + "@radix-ui/react-compose-refs" "1.1.1" + "@radix-ui/react-context" "1.1.1" + "@radix-ui/react-presence" "1.1.2" + "@radix-ui/react-primitive" "2.0.1" + "@radix-ui/react-use-controllable-state" "1.1.0" + "@radix-ui/react-use-previous" "1.1.0" + "@radix-ui/react-use-size" "1.1.0" + +"@radix-ui/react-collection@1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@radix-ui/react-collection/-/react-collection-1.1.3.tgz#cfd46dcea5a8ab064d91798feeb46faba4032930" + integrity sha512-mM2pxoQw5HJ49rkzwOs7Y6J4oYH22wS8BfK2/bBxROlI4xuR0c4jEenQP63LlTlDkO6Buj2Vt+QYAYcOgqtrXA== + dependencies: + "@radix-ui/react-compose-refs" "1.1.2" + "@radix-ui/react-context" "1.1.2" + "@radix-ui/react-primitive" "2.0.3" + "@radix-ui/react-slot" "1.2.0" + +"@radix-ui/react-compose-refs@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz#6f766faa975f8738269ebb8a23bad4f5a8d2faec" + integrity sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw== + +"@radix-ui/react-compose-refs@1.1.2", "@radix-ui/react-compose-refs@^1.1.1": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz#a2c4c47af6337048ee78ff6dc0d090b390d2bb30" + integrity sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg== + +"@radix-ui/react-context@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-context/-/react-context-1.1.1.tgz#82074aa83a472353bb22e86f11bcbd1c61c4c71a" + integrity sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q== + +"@radix-ui/react-context@1.1.2", "@radix-ui/react-context@^1.1.1": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@radix-ui/react-context/-/react-context-1.1.2.tgz#61628ef269a433382c364f6f1e3788a6dc213a36" + integrity sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA== + +"@radix-ui/react-direction@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-direction/-/react-direction-1.1.0.tgz#a7d39855f4d077adc2a1922f9c353c5977a09cdc" + integrity sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg== + +"@radix-ui/react-direction@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-direction/-/react-direction-1.1.1.tgz#39e5a5769e676c753204b792fbe6cf508e550a14" + integrity sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw== + +"@radix-ui/react-dismissable-layer@1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.3.tgz#4ee0f0f82d53bf5bd9db21665799bb0d1bad5ed8" + integrity sha512-onrWn/72lQoEucDmJnr8uczSNTujT0vJnA/X5+3AkChVPowr8n1yvIKIabhWyMQeMvvmdpsvcyDqx3X1LEXCPg== + dependencies: + "@radix-ui/primitive" "1.1.1" + "@radix-ui/react-compose-refs" "1.1.1" + "@radix-ui/react-primitive" "2.0.1" + "@radix-ui/react-use-callback-ref" "1.1.0" + "@radix-ui/react-use-escape-keydown" "1.1.0" + +"@radix-ui/react-dismissable-layer@1.1.5": + version "1.1.5" + resolved "https://registry.yarnpkg.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.5.tgz#96dde2be078c694a621e55e047406c58cd5fe774" + integrity sha512-E4TywXY6UsXNRhFrECa5HAvE5/4BFcGyfTyK36gP+pAW1ed7UTK4vKwdr53gAJYwqbfCWC6ATvJa3J3R/9+Qrg== + dependencies: + "@radix-ui/primitive" "1.1.1" + "@radix-ui/react-compose-refs" "1.1.1" + "@radix-ui/react-primitive" "2.0.2" + "@radix-ui/react-use-callback-ref" "1.1.0" + "@radix-ui/react-use-escape-keydown" "1.1.0" + +"@radix-ui/react-dismissable-layer@1.1.6", "@radix-ui/react-dismissable-layer@^1.1.5": + version "1.1.6" + resolved "https://registry.yarnpkg.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.6.tgz#e72c156cac7b07614fe8e3a039ab7081ce413686" + integrity sha512-7gpgMT2gyKym9Jz2ZhlRXSg2y6cNQIK8d/cqBZ0RBCaps8pFryCWXiUKI+uHGFrhMrbGUP7U6PWgiXzIxoyF3Q== + dependencies: + "@radix-ui/primitive" "1.1.2" + "@radix-ui/react-compose-refs" "1.1.2" + "@radix-ui/react-primitive" "2.0.3" + "@radix-ui/react-use-callback-ref" "1.1.1" + "@radix-ui/react-use-escape-keydown" "1.1.1" + +"@radix-ui/react-focus-guards@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.1.tgz#8635edd346304f8b42cae86b05912b61aef27afe" + integrity sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg== + +"@radix-ui/react-focus-guards@1.1.2", "@radix-ui/react-focus-guards@^1.1.1": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.2.tgz#4ec9a7e50925f7fb661394460045b46212a33bed" + integrity sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA== + +"@radix-ui/react-focus-scope@1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.2.tgz#c0a4519cd95c772606a82fc5b96226cd7fdd2602" + integrity sha512-zxwE80FCU7lcXUGWkdt6XpTTCKPitG1XKOwViTxHVKIJhZl9MvIl2dVHeZENCWD9+EdWv05wlaEkRXUykU27RA== + dependencies: + "@radix-ui/react-compose-refs" "1.1.1" + "@radix-ui/react-primitive" "2.0.2" + "@radix-ui/react-use-callback-ref" "1.1.0" + +"@radix-ui/react-focus-scope@1.1.3", "@radix-ui/react-focus-scope@^1.1.2": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.3.tgz#eac83a3aac700db17650b41b30724deffac5b28a" + integrity sha512-4XaDlq0bPt7oJwR+0k0clCiCO/7lO7NKZTAaJBYxDNQT/vj4ig0/UvctrRscZaFREpRvUTkpKR96ov1e6jptQg== + dependencies: + "@radix-ui/react-compose-refs" "1.1.2" + "@radix-ui/react-primitive" "2.0.3" + "@radix-ui/react-use-callback-ref" "1.1.1" + +"@radix-ui/react-hover-card@^1.1.6": + version "1.1.7" + resolved "https://registry.yarnpkg.com/@radix-ui/react-hover-card/-/react-hover-card-1.1.7.tgz#01b2f956daeb8a1193ccdb36c9c00943120bf2d4" + integrity sha512-HwM03kP8psrv21J1+9T/hhxi0f5rARVbqIZl9+IAq13l4j4fX+oGIuxisukZZmebO7J35w9gpoILvtG8bbph0w== + dependencies: + "@radix-ui/primitive" "1.1.2" + "@radix-ui/react-compose-refs" "1.1.2" + "@radix-ui/react-context" "1.1.2" + "@radix-ui/react-dismissable-layer" "1.1.6" + "@radix-ui/react-popper" "1.2.3" + "@radix-ui/react-portal" "1.1.5" + "@radix-ui/react-presence" "1.1.3" + "@radix-ui/react-primitive" "2.0.3" + "@radix-ui/react-use-controllable-state" "1.1.1" + +"@radix-ui/react-id@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-id/-/react-id-1.1.0.tgz#de47339656594ad722eb87f94a6b25f9cffae0ed" + integrity sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA== + dependencies: + "@radix-ui/react-use-layout-effect" "1.1.0" + +"@radix-ui/react-id@1.1.1", "@radix-ui/react-id@^1.1.0": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-id/-/react-id-1.1.1.tgz#1404002e79a03fe062b7e3864aa01e24bd1471f7" + integrity sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg== + dependencies: + "@radix-ui/react-use-layout-effect" "1.1.1" + +"@radix-ui/react-popover@1.1.6": + version "1.1.6" + resolved "https://registry.yarnpkg.com/@radix-ui/react-popover/-/react-popover-1.1.6.tgz#699634dbc7899429f657bb590d71fb3ca0904087" + integrity sha512-NQouW0x4/GnkFJ/pRqsIS3rM/k97VzKnVb2jB7Gq7VEGPy5g7uNV1ykySFt7eWSp3i2uSGFwaJcvIRJBAHmmFg== + dependencies: + "@radix-ui/primitive" "1.1.1" + "@radix-ui/react-compose-refs" "1.1.1" + "@radix-ui/react-context" "1.1.1" + "@radix-ui/react-dismissable-layer" "1.1.5" + "@radix-ui/react-focus-guards" "1.1.1" + "@radix-ui/react-focus-scope" "1.1.2" + "@radix-ui/react-id" "1.1.0" + "@radix-ui/react-popper" "1.2.2" + "@radix-ui/react-portal" "1.1.4" + "@radix-ui/react-presence" "1.1.2" + "@radix-ui/react-primitive" "2.0.2" + "@radix-ui/react-slot" "1.1.2" + "@radix-ui/react-use-controllable-state" "1.1.0" + aria-hidden "^1.2.4" + react-remove-scroll "^2.6.3" + +"@radix-ui/react-popper@1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-popper/-/react-popper-1.2.1.tgz#2fc66cfc34f95f00d858924e3bee54beae2dff0a" + integrity sha512-3kn5Me69L+jv82EKRuQCXdYyf1DqHwD2U/sxoNgBGCB7K9TRc3bQamQ+5EPM9EvyPdli0W41sROd+ZU1dTCztw== + dependencies: + "@floating-ui/react-dom" "^2.0.0" + "@radix-ui/react-arrow" "1.1.1" + "@radix-ui/react-compose-refs" "1.1.1" + "@radix-ui/react-context" "1.1.1" + "@radix-ui/react-primitive" "2.0.1" + "@radix-ui/react-use-callback-ref" "1.1.0" + "@radix-ui/react-use-layout-effect" "1.1.0" + "@radix-ui/react-use-rect" "1.1.0" + "@radix-ui/react-use-size" "1.1.0" + "@radix-ui/rect" "1.1.0" + +"@radix-ui/react-popper@1.2.2": + version "1.2.2" + resolved "https://registry.yarnpkg.com/@radix-ui/react-popper/-/react-popper-1.2.2.tgz#d2e1ee5a9b24419c5936a1b7f6f472b7b412b029" + integrity sha512-Rvqc3nOpwseCyj/rgjlJDYAgyfw7OC1tTkKn2ivhaMGcYt8FSBlahHOZak2i3QwkRXUXgGgzeEe2RuqeEHuHgA== + dependencies: + "@floating-ui/react-dom" "^2.0.0" + "@radix-ui/react-arrow" "1.1.2" + "@radix-ui/react-compose-refs" "1.1.1" + "@radix-ui/react-context" "1.1.1" + "@radix-ui/react-primitive" "2.0.2" + "@radix-ui/react-use-callback-ref" "1.1.0" + "@radix-ui/react-use-layout-effect" "1.1.0" + "@radix-ui/react-use-rect" "1.1.0" + "@radix-ui/react-use-size" "1.1.0" + "@radix-ui/rect" "1.1.0" + +"@radix-ui/react-popper@1.2.3": + version "1.2.3" + resolved "https://registry.yarnpkg.com/@radix-ui/react-popper/-/react-popper-1.2.3.tgz#3b6ef3388fd209bb46341e1e40125b75f07f1304" + integrity sha512-iNb9LYUMkne9zIahukgQmHlSBp9XWGeQQ7FvUGNk45ywzOb6kQa+Ca38OphXlWDiKvyneo9S+KSJsLfLt8812A== + dependencies: + "@floating-ui/react-dom" "^2.0.0" + "@radix-ui/react-arrow" "1.1.3" + "@radix-ui/react-compose-refs" "1.1.2" + "@radix-ui/react-context" "1.1.2" + "@radix-ui/react-primitive" "2.0.3" + "@radix-ui/react-use-callback-ref" "1.1.1" + "@radix-ui/react-use-layout-effect" "1.1.1" + "@radix-ui/react-use-rect" "1.1.1" + "@radix-ui/react-use-size" "1.1.1" + "@radix-ui/rect" "1.1.1" + +"@radix-ui/react-portal@1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@radix-ui/react-portal/-/react-portal-1.1.3.tgz#b0ea5141103a1671b715481b13440763d2ac4440" + integrity sha512-NciRqhXnGojhT93RPyDaMPfLH3ZSl4jjIFbZQ1b/vxvZEdHsBZ49wP9w8L3HzUQwep01LcWtkUvm0OVB5JAHTw== + dependencies: + "@radix-ui/react-primitive" "2.0.1" + "@radix-ui/react-use-layout-effect" "1.1.0" + +"@radix-ui/react-portal@1.1.4": + version "1.1.4" + resolved "https://registry.yarnpkg.com/@radix-ui/react-portal/-/react-portal-1.1.4.tgz#ff5401ff63c8a825c46eea96d3aef66074b8c0c8" + integrity sha512-sn2O9k1rPFYVyKd5LAJfo96JlSGVFpa1fS6UuBJfrZadudiw5tAmru+n1x7aMRQ84qDM71Zh1+SzK5QwU0tJfA== + dependencies: + "@radix-ui/react-primitive" "2.0.2" + "@radix-ui/react-use-layout-effect" "1.1.0" + +"@radix-ui/react-portal@1.1.5", "@radix-ui/react-portal@^1.1.4": + version "1.1.5" + resolved "https://registry.yarnpkg.com/@radix-ui/react-portal/-/react-portal-1.1.5.tgz#50ed6bee2d895c9a9dfc28625f24b8483b74d431" + integrity sha512-ps/67ZqsFm+Mb6lSPJpfhRLrVL2i2fntgCmGMqqth4eaGUf+knAuuRtWVJrNjUhExgmdRqftSgzpf0DF0n6yXA== + dependencies: + "@radix-ui/react-primitive" "2.0.3" + "@radix-ui/react-use-layout-effect" "1.1.1" + +"@radix-ui/react-presence@1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@radix-ui/react-presence/-/react-presence-1.1.2.tgz#bb764ed8a9118b7ec4512da5ece306ded8703cdc" + integrity sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg== + dependencies: + "@radix-ui/react-compose-refs" "1.1.1" + "@radix-ui/react-use-layout-effect" "1.1.0" + +"@radix-ui/react-presence@1.1.3", "@radix-ui/react-presence@^1.1.2": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@radix-ui/react-presence/-/react-presence-1.1.3.tgz#ce3400caec9892ceb862f96ddaa2add080c09b90" + integrity sha512-IrVLIhskYhH3nLvtcBLQFZr61tBG7wx7O3kEmdzcYwRGAEBmBicGGL7ATzNgruYJ3xBTbuzEEq9OXJM3PAX3tA== + dependencies: + "@radix-ui/react-compose-refs" "1.1.2" + "@radix-ui/react-use-layout-effect" "1.1.1" + +"@radix-ui/react-primitive@2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-primitive/-/react-primitive-2.0.1.tgz#6d9efc550f7520135366f333d1e820cf225fad9e" + integrity sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg== + dependencies: + "@radix-ui/react-slot" "1.1.1" + +"@radix-ui/react-primitive@2.0.2": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@radix-ui/react-primitive/-/react-primitive-2.0.2.tgz#ac8b7854d87b0d7af388d058268d9a7eb64ca8ef" + integrity sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w== + dependencies: + "@radix-ui/react-slot" "1.1.2" + +"@radix-ui/react-primitive@2.0.3", "@radix-ui/react-primitive@^2.0.2": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@radix-ui/react-primitive/-/react-primitive-2.0.3.tgz#13c654dc4754558870a9c769f6febe5980a1bad8" + integrity sha512-Pf/t/GkndH7CQ8wE2hbkXA+WyZ83fhQQn5DDmwDiDo6AwN/fhaH8oqZ0jRjMrO2iaMhDi6P1HRx6AZwyMinY1g== + dependencies: + "@radix-ui/react-slot" "1.2.0" + +"@radix-ui/react-roving-focus@1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.3.tgz#c992b9d30c795f5f5a668853db8f4a6e07b7284d" + integrity sha512-ufbpLUjZiOg4iYgb2hQrWXEPYX6jOLBbR27bDyAff5GYMRrCzcze8lukjuXVUQvJ6HZe8+oL+hhswDcjmcgVyg== + dependencies: + "@radix-ui/primitive" "1.1.2" + "@radix-ui/react-collection" "1.1.3" + "@radix-ui/react-compose-refs" "1.1.2" + "@radix-ui/react-context" "1.1.2" + "@radix-ui/react-direction" "1.1.1" + "@radix-ui/react-id" "1.1.1" + "@radix-ui/react-primitive" "2.0.3" + "@radix-ui/react-use-callback-ref" "1.1.1" + "@radix-ui/react-use-controllable-state" "1.1.1" + +"@radix-ui/react-scroll-area@1.2.2": + version "1.2.2" + resolved "https://registry.yarnpkg.com/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.2.tgz#28e34fd4d83e9de5d987c5e8914a7bd8be9546a5" + integrity sha512-EFI1N/S3YxZEW/lJ/H1jY3njlvTd8tBmgKEn4GHi51+aMm94i6NmAJstsm5cu3yJwYqYc93gpCPm21FeAbFk6g== + dependencies: + "@radix-ui/number" "1.1.0" + "@radix-ui/primitive" "1.1.1" + "@radix-ui/react-compose-refs" "1.1.1" + "@radix-ui/react-context" "1.1.1" + "@radix-ui/react-direction" "1.1.0" + "@radix-ui/react-presence" "1.1.2" + "@radix-ui/react-primitive" "2.0.1" + "@radix-ui/react-use-callback-ref" "1.1.0" + "@radix-ui/react-use-layout-effect" "1.1.0" + +"@radix-ui/react-select@^2.1.7": + version "2.1.7" + resolved "https://registry.yarnpkg.com/@radix-ui/react-select/-/react-select-2.1.7.tgz#68561488ca54cad07352b3f2c2d29e0da28bbaa0" + integrity sha512-exzGIRtc7S8EIM2KjFg+7lJZsH7O7tpaBaJbBNVDnOZNhtoQ2iV+iSNfi2Wth0m6h3trJkMVvzAehB3c6xj/3Q== + dependencies: + "@radix-ui/number" "1.1.1" + "@radix-ui/primitive" "1.1.2" + "@radix-ui/react-collection" "1.1.3" + "@radix-ui/react-compose-refs" "1.1.2" + "@radix-ui/react-context" "1.1.2" + "@radix-ui/react-direction" "1.1.1" + "@radix-ui/react-dismissable-layer" "1.1.6" + "@radix-ui/react-focus-guards" "1.1.2" + "@radix-ui/react-focus-scope" "1.1.3" + "@radix-ui/react-id" "1.1.1" + "@radix-ui/react-popper" "1.2.3" + "@radix-ui/react-portal" "1.1.5" + "@radix-ui/react-primitive" "2.0.3" + "@radix-ui/react-slot" "1.2.0" + "@radix-ui/react-use-callback-ref" "1.1.1" + "@radix-ui/react-use-controllable-state" "1.1.1" + "@radix-ui/react-use-layout-effect" "1.1.1" + "@radix-ui/react-use-previous" "1.1.1" + "@radix-ui/react-visually-hidden" "1.1.3" + aria-hidden "^1.2.4" + react-remove-scroll "^2.6.3" + +"@radix-ui/react-slot@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-1.1.1.tgz#ab9a0ffae4027db7dc2af503c223c978706affc3" + integrity sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g== + dependencies: + "@radix-ui/react-compose-refs" "1.1.1" + +"@radix-ui/react-slot@1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-1.1.2.tgz#daffff7b2bfe99ade63b5168407680b93c00e1c6" + integrity sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ== + dependencies: + "@radix-ui/react-compose-refs" "1.1.1" + +"@radix-ui/react-slot@1.2.0", "@radix-ui/react-slot@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-1.2.0.tgz#57727fc186ddb40724ccfbe294e1a351d92462ba" + integrity sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w== + dependencies: + "@radix-ui/react-compose-refs" "1.1.2" + +"@radix-ui/react-tabs@^1.1.4": + version "1.1.4" + resolved "https://registry.yarnpkg.com/@radix-ui/react-tabs/-/react-tabs-1.1.4.tgz#2e43f3ef3450143281e7c1491da1e5d7941b9826" + integrity sha512-fuHMHWSf5SRhXke+DbHXj2wVMo+ghVH30vhX3XVacdXqDl+J4XWafMIGOOER861QpBx1jxgwKXL2dQnfrsd8MQ== + dependencies: + "@radix-ui/primitive" "1.1.2" + "@radix-ui/react-context" "1.1.2" + "@radix-ui/react-direction" "1.1.1" + "@radix-ui/react-id" "1.1.1" + "@radix-ui/react-presence" "1.1.3" + "@radix-ui/react-primitive" "2.0.3" + "@radix-ui/react-roving-focus" "1.1.3" + "@radix-ui/react-use-controllable-state" "1.1.1" + +"@radix-ui/react-tooltip@1.1.6": + version "1.1.6" + resolved "https://registry.yarnpkg.com/@radix-ui/react-tooltip/-/react-tooltip-1.1.6.tgz#eab98e9a5c876ef0abfae3cfeee229870528ed06" + integrity sha512-TLB5D8QLExS1uDn7+wH/bjEmRurNMTzNrtq7IjaS4kjion9NtzsTGkvR5+i7yc9q01Pi2KMM2cN3f8UG4IvvXA== + dependencies: + "@radix-ui/primitive" "1.1.1" + "@radix-ui/react-compose-refs" "1.1.1" + "@radix-ui/react-context" "1.1.1" + "@radix-ui/react-dismissable-layer" "1.1.3" + "@radix-ui/react-id" "1.1.0" + "@radix-ui/react-popper" "1.2.1" + "@radix-ui/react-portal" "1.1.3" + "@radix-ui/react-presence" "1.1.2" + "@radix-ui/react-primitive" "2.0.1" + "@radix-ui/react-slot" "1.1.1" + "@radix-ui/react-use-controllable-state" "1.1.0" + "@radix-ui/react-visually-hidden" "1.1.1" + +"@radix-ui/react-use-callback-ref@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz#bce938ca413675bc937944b0d01ef6f4a6dc5bf1" + integrity sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw== + +"@radix-ui/react-use-callback-ref@1.1.1", "@radix-ui/react-use-callback-ref@^1.1.0": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz#62a4dba8b3255fdc5cc7787faeac1c6e4cc58d40" + integrity sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg== + +"@radix-ui/react-use-controllable-state@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.0.tgz#1321446857bb786917df54c0d4d084877aab04b0" + integrity sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw== + dependencies: + "@radix-ui/react-use-callback-ref" "1.1.0" + +"@radix-ui/react-use-controllable-state@1.1.1", "@radix-ui/react-use-controllable-state@^1.1.0": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.1.tgz#ec9c572072a6f269df7435c1652fbeebabe0f0c1" + integrity sha512-YnEXIy8/ga01Y1PN0VfaNH//MhA91JlEGVBDxDzROqwrAtG5Yr2QGEPz8A/rJA3C7ZAHryOYGaUv8fLSW2H/mg== + dependencies: + "@radix-ui/react-use-callback-ref" "1.1.1" + +"@radix-ui/react-use-escape-keydown@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz#31a5b87c3b726504b74e05dac1edce7437b98754" + integrity sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw== + dependencies: + "@radix-ui/react-use-callback-ref" "1.1.0" + +"@radix-ui/react-use-escape-keydown@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz#b3fed9bbea366a118f40427ac40500aa1423cc29" + integrity sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g== + dependencies: + "@radix-ui/react-use-callback-ref" "1.1.1" + +"@radix-ui/react-use-layout-effect@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz#3c2c8ce04827b26a39e442ff4888d9212268bd27" + integrity sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w== + +"@radix-ui/react-use-layout-effect@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz#0c4230a9eed49d4589c967e2d9c0d9d60a23971e" + integrity sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ== + +"@radix-ui/react-use-previous@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-previous/-/react-use-previous-1.1.0.tgz#d4dd37b05520f1d996a384eb469320c2ada8377c" + integrity sha512-Z/e78qg2YFnnXcW88A4JmTtm4ADckLno6F7OXotmkQfeuCVaKuYzqAATPhVzl3delXE7CxIV8shofPn3jPc5Og== + +"@radix-ui/react-use-previous@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz#1a1ad5568973d24051ed0af687766f6c7cb9b5b5" + integrity sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ== + +"@radix-ui/react-use-rect@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-rect/-/react-use-rect-1.1.0.tgz#13b25b913bd3e3987cc9b073a1a164bb1cf47b88" + integrity sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ== + dependencies: + "@radix-ui/rect" "1.1.0" + +"@radix-ui/react-use-rect@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz#01443ca8ed071d33023c1113e5173b5ed8769152" + integrity sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w== + dependencies: + "@radix-ui/rect" "1.1.1" + +"@radix-ui/react-use-size@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-size/-/react-use-size-1.1.0.tgz#b4dba7fbd3882ee09e8d2a44a3eed3a7e555246b" + integrity sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw== + dependencies: + "@radix-ui/react-use-layout-effect" "1.1.0" + +"@radix-ui/react-use-size@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz#6de276ffbc389a537ffe4316f5b0f24129405b37" + integrity sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ== + dependencies: + "@radix-ui/react-use-layout-effect" "1.1.1" + +"@radix-ui/react-visually-hidden@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.1.1.tgz#f7b48c1af50dfdc366e92726aee6d591996c5752" + integrity sha512-vVfA2IZ9q/J+gEamvj761Oq1FpWgCDaNOOIfbPVp2MVPLEomUr5+Vf7kJGwQ24YxZSlQVar7Bes8kyTo5Dshpg== + dependencies: + "@radix-ui/react-primitive" "2.0.1" + +"@radix-ui/react-visually-hidden@1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.1.3.tgz#f704c49121859941a8bb50ff1e4f156058cacd0b" + integrity sha512-oXSF3ZQRd5fvomd9hmUCb2EHSZbPp3ZSHAHJJU/DlF9XoFkJBBW8RHU/E8WEH+RbSfJd/QFA0sl8ClJXknBwHQ== + dependencies: + "@radix-ui/react-primitive" "2.0.3" + +"@radix-ui/rect@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@radix-ui/rect/-/rect-1.1.0.tgz#f817d1d3265ac5415dadc67edab30ae196696438" + integrity sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg== + +"@radix-ui/rect@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/rect/-/rect-1.1.1.tgz#78244efe12930c56fd255d7923865857c41ac8cb" + integrity sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw== + "@redocly/ajv@^8.11.0": version "8.11.0" resolved "https://registry.yarnpkg.com/@redocly/ajv/-/ajv-8.11.0.tgz#2fad322888dc0113af026e08fceb3e71aae495ae" @@ -3691,152 +4361,152 @@ "@svgr/plugin-jsx" "8.1.0" "@svgr/plugin-svgo" "8.1.0" -"@swc/core-darwin-arm64@1.11.29": - version "1.11.29" - resolved "https://registry.yarnpkg.com/@swc/core-darwin-arm64/-/core-darwin-arm64-1.11.29.tgz#bf66e3f4f00e6fe9d95e8a33f780e6c40fca946d" - integrity sha512-whsCX7URzbuS5aET58c75Dloby3Gtj/ITk2vc4WW6pSDQKSPDuONsIcZ7B2ng8oz0K6ttbi4p3H/PNPQLJ4maQ== +"@swc/core-darwin-arm64@1.12.6": + version "1.12.6" + resolved "https://registry.yarnpkg.com/@swc/core-darwin-arm64/-/core-darwin-arm64-1.12.6.tgz#3d9166720df2dc00fa3b6cf90fce3e77a442a43e" + integrity sha512-yLiw+XzG+MilfFh0ON7qt67bfIr7UxB9JprhYReVOmLTBDmDVQSC3T4/vIuc+GwlX08ydnHy0ud4lIjTNW4uWg== -"@swc/core-darwin-x64@1.11.29": - version "1.11.29" - resolved "https://registry.yarnpkg.com/@swc/core-darwin-x64/-/core-darwin-x64-1.11.29.tgz#0a77d2d79ef2c789f9d40a86784bbf52c5f9877f" - integrity sha512-S3eTo/KYFk+76cWJRgX30hylN5XkSmjYtCBnM4jPLYn7L6zWYEPajsFLmruQEiTEDUg0gBEWLMNyUeghtswouw== +"@swc/core-darwin-x64@1.12.6": + version "1.12.6" + resolved "https://registry.yarnpkg.com/@swc/core-darwin-x64/-/core-darwin-x64-1.12.6.tgz#2bbc32c56f8bccf6958b73d46bdd5670aa31f4d9" + integrity sha512-qwg8ux5x5Gd1LmSUtL4s9mbyfzAjr5M6OtjO281dKHwc/GYiSc4j1urb2jNSo9FcMkfT78oAOW2L6HQiWv+j1A== -"@swc/core-linux-arm-gnueabihf@1.11.29": - version "1.11.29" - resolved "https://registry.yarnpkg.com/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.11.29.tgz#80fa3a6a36034ffdbbba73e26c8f27cb13111a33" - integrity sha512-o9gdshbzkUMG6azldHdmKklcfrcMx+a23d/2qHQHPDLUPAN+Trd+sDQUYArK5Fcm7TlpG4sczz95ghN0DMkM7g== +"@swc/core-linux-arm-gnueabihf@1.12.6": + version "1.12.6" + resolved "https://registry.yarnpkg.com/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.12.6.tgz#45d8bdef987c4f7fbc5b14640374f0e77904f304" + integrity sha512-pnkqH59JXBZu+MedaykMAC2or7tlUKeya7GKjzub+hkwxBP0ywWoFd+QYEdzp7QSziOt1VIHc4Wb9iZ2EfnzmA== -"@swc/core-linux-arm64-gnu@1.11.29": - version "1.11.29" - resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.11.29.tgz#42da87f445bc3e26da01d494246884006d9b9a1a" - integrity sha512-sLoaciOgUKQF1KX9T6hPGzvhOQaJn+3DHy4LOHeXhQqvBgr+7QcZ+hl4uixPKTzxk6hy6Hb0QOvQEdBAAR1gXw== +"@swc/core-linux-arm64-gnu@1.12.6": + version "1.12.6" + resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.12.6.tgz#f69616d4269e11cb93348437687e08f49f58cc36" + integrity sha512-h8+Ltx0NSEzIFHetkOYoQ+UQ59unYLuJ4wF6kCpxzS4HskRLjcngr1HgN0F/PRpptnrmJUPVQmfms/vjN8ndAQ== -"@swc/core-linux-arm64-musl@1.11.29": - version "1.11.29" - resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.11.29.tgz#c9cec610525dc9e9b11ef26319db3780812dfa54" - integrity sha512-PwjB10BC0N+Ce7RU/L23eYch6lXFHz7r3NFavIcwDNa/AAqywfxyxh13OeRy+P0cg7NDpWEETWspXeI4Ek8otw== +"@swc/core-linux-arm64-musl@1.12.6": + version "1.12.6" + resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.12.6.tgz#3277154e7b60213c0fa11e415c8a90b41563d76e" + integrity sha512-GZu3MnB/5qtBxKEH46hgVDaplEe4mp3ZmQ1O2UpFCv/u/Ji3Gar5w5g2wHCZoT5AOouAhP1bh7IAEqjG/fbVfg== -"@swc/core-linux-x64-gnu@1.11.29": - version "1.11.29" - resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.11.29.tgz#1cda2df38a4ab8905ba6ac3aa16e4ad710b6f2de" - integrity sha512-i62vBVoPaVe9A3mc6gJG07n0/e7FVeAvdD9uzZTtGLiuIfVfIBta8EMquzvf+POLycSk79Z6lRhGPZPJPYiQaA== +"@swc/core-linux-x64-gnu@1.12.6": + version "1.12.6" + resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.12.6.tgz#7d04d9437011396bf54c4ab0686c4db65a1283f8" + integrity sha512-WwJLQFzMW9ufVjM6k3le4HUgBFNunyt2oghjcgn2YjnKj0Ka2LrrBHCxfS7lgFSCQh/shib2wIlKXUnlTEWQJw== -"@swc/core-linux-x64-musl@1.11.29": - version "1.11.29" - resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.11.29.tgz#5d634efff33f47c8d6addd84291ab606903d1cfd" - integrity sha512-YER0XU1xqFdK0hKkfSVX1YIyCvMDI7K07GIpefPvcfyNGs38AXKhb2byySDjbVxkdl4dycaxxhRyhQ2gKSlsFQ== +"@swc/core-linux-x64-musl@1.12.6": + version "1.12.6" + resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.12.6.tgz#2f3e586eda3ee5d08b6b8de7714623d5a44ceb3c" + integrity sha512-rVGPNpI/sm8VVAhnB09Z/23OJP3ymouv6F4z4aYDbq/2JIwxqgpnl8gtMYP+Jw3XqabaFNjQmPiL15TvKCQaxQ== -"@swc/core-win32-arm64-msvc@1.11.29": - version "1.11.29" - resolved "https://registry.yarnpkg.com/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.11.29.tgz#bc54f2e3f8f180113b7a092b1ee1eaaab24df62b" - integrity sha512-po+WHw+k9g6FAg5IJ+sMwtA/fIUL3zPQ4m/uJgONBATCVnDDkyW6dBA49uHNVtSEvjvhuD8DVWdFP847YTcITw== +"@swc/core-win32-arm64-msvc@1.12.6": + version "1.12.6" + resolved "https://registry.yarnpkg.com/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.12.6.tgz#70211534238ed941efaf0a1b34310e937bd4afa7" + integrity sha512-EKDJ1+8vaIlJGMl2yvd2HklV4GNbpKKwNQcUQid6j91tFYz4/aByw+9vh/sDVG7ZNqdmdywSnLRo317UTt0zFg== -"@swc/core-win32-ia32-msvc@1.11.29": - version "1.11.29" - resolved "https://registry.yarnpkg.com/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.11.29.tgz#f1df344c06283643d1fe66c6931b350347b73722" - integrity sha512-h+NjOrbqdRBYr5ItmStmQt6x3tnhqgwbj9YxdGPepbTDamFv7vFnhZR0YfB3jz3UKJ8H3uGJ65Zw1VsC+xpFkg== +"@swc/core-win32-ia32-msvc@1.12.6": + version "1.12.6" + resolved "https://registry.yarnpkg.com/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.12.6.tgz#d3584da07b47904b547baced8b00c9e4d32110e7" + integrity sha512-jnULikZkR2fpZgFUQs7NsNIztavM1JdX+8Y+8FsfChBvMvziKxXtvUPGjeVJ8nzU1wgMnaeilJX9vrwuDGkA0Q== -"@swc/core-win32-x64-msvc@1.11.29": - version "1.11.29" - resolved "https://registry.yarnpkg.com/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.11.29.tgz#a6f9dc1df66c8db96d70091abedd78cc52544724" - integrity sha512-Q8cs2BDV9wqDvqobkXOYdC+pLUSEpX/KvI0Dgfun1F+LzuLotRFuDhrvkU9ETJA6OnD2+Fn/ieHgloiKA/Mn/g== +"@swc/core-win32-x64-msvc@1.12.6": + version "1.12.6" + resolved "https://registry.yarnpkg.com/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.12.6.tgz#8eff8c0d9e7c3b91bd9427e67483be90070b0f7d" + integrity sha512-jL2Dcdcc/QZiQnwByP1uIE4k/mTlapzUng7owtLD2tSBBi1d+jPIdXIefdv+nccYJKRA+lKG3rRB6Tk9GrC7Kg== "@swc/core@^1.7.39": - version "1.11.29" - resolved "https://registry.yarnpkg.com/@swc/core/-/core-1.11.29.tgz#bce20113c47fcd6251d06262b8b8c063f8e86a20" - integrity sha512-g4mThMIpWbNhV8G2rWp5a5/Igv8/2UFRJx2yImrLGMgrDDYZIopqZ/z0jZxDgqNA1QDx93rpwNF7jGsxVWcMlA== + version "1.12.6" + resolved "https://registry.yarnpkg.com/@swc/core/-/core-1.12.6.tgz#1bf204f7afc59fde6cb2cef067de23af232d6ff6" + integrity sha512-TEpta6Gi02X1b2yDIzBOIr7dFprvq9jD8RbEVI2OcMrwklbCUx0Dz9TrAnklSOwRvYvH5JjCx8ht9E94oWiG7A== dependencies: "@swc/counter" "^0.1.3" - "@swc/types" "^0.1.21" + "@swc/types" "^0.1.23" optionalDependencies: - "@swc/core-darwin-arm64" "1.11.29" - "@swc/core-darwin-x64" "1.11.29" - "@swc/core-linux-arm-gnueabihf" "1.11.29" - "@swc/core-linux-arm64-gnu" "1.11.29" - "@swc/core-linux-arm64-musl" "1.11.29" - "@swc/core-linux-x64-gnu" "1.11.29" - "@swc/core-linux-x64-musl" "1.11.29" - "@swc/core-win32-arm64-msvc" "1.11.29" - "@swc/core-win32-ia32-msvc" "1.11.29" - "@swc/core-win32-x64-msvc" "1.11.29" + "@swc/core-darwin-arm64" "1.12.6" + "@swc/core-darwin-x64" "1.12.6" + "@swc/core-linux-arm-gnueabihf" "1.12.6" + "@swc/core-linux-arm64-gnu" "1.12.6" + "@swc/core-linux-arm64-musl" "1.12.6" + "@swc/core-linux-x64-gnu" "1.12.6" + "@swc/core-linux-x64-musl" "1.12.6" + "@swc/core-win32-arm64-msvc" "1.12.6" + "@swc/core-win32-ia32-msvc" "1.12.6" + "@swc/core-win32-x64-msvc" "1.12.6" "@swc/counter@^0.1.3": version "0.1.3" resolved "https://registry.yarnpkg.com/@swc/counter/-/counter-0.1.3.tgz#cc7463bd02949611c6329596fccd2b0ec782b0e9" integrity sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ== -"@swc/html-darwin-arm64@1.11.29": - version "1.11.29" - resolved "https://registry.yarnpkg.com/@swc/html-darwin-arm64/-/html-darwin-arm64-1.11.29.tgz#7bd6d10115ffe155ecd757387b5aff318b02b5a0" - integrity sha512-q53kn/HI0n/+pecsOB2gxqITbRAhtBG7VI520SIWuCGXHPsTQ/1VOrhLMNvyfw1xVhRyFal7BpAvfGUORCl0sw== +"@swc/html-darwin-arm64@1.12.6": + version "1.12.6" + resolved "https://registry.yarnpkg.com/@swc/html-darwin-arm64/-/html-darwin-arm64-1.12.6.tgz#dff9ee656cd1a4ac0a4a2637c4e9a058ce64b42a" + integrity sha512-McW4JsF5wFB5KmHyAaty94kw2hHLbYtrIQvVlshbXM3lpY+rDO0KnS74CcIiAD46p7knV0Y6Xuhint8K3rYfkg== -"@swc/html-darwin-x64@1.11.29": - version "1.11.29" - resolved "https://registry.yarnpkg.com/@swc/html-darwin-x64/-/html-darwin-x64-1.11.29.tgz#ccafed56081932ffaa51be1788cef88eb9a144d1" - integrity sha512-YfQPjh5WoDqOxsA7vDOOSnxEPc1Ki4SuZ0ufR4t8jYdMOFsU3AhZQ/sgBZLpTzegBTutUn7/7yy8VSoFngeR7Q== +"@swc/html-darwin-x64@1.12.6": + version "1.12.6" + resolved "https://registry.yarnpkg.com/@swc/html-darwin-x64/-/html-darwin-x64-1.12.6.tgz#194456c256cb5f949af24cfaf1740fb20098285a" + integrity sha512-Fh/bPNdnSNeJ7GrRAe/BqERWV9hbIyZktoMlvkMipz2NPTdadIwXjd8fscVDc6S5j1DigiSp2Mnf0rZgH6Xnhw== -"@swc/html-linux-arm-gnueabihf@1.11.29": - version "1.11.29" - resolved "https://registry.yarnpkg.com/@swc/html-linux-arm-gnueabihf/-/html-linux-arm-gnueabihf-1.11.29.tgz#e784a1a0f69034e9dd52a9019ff80f0d5eb91433" - integrity sha512-dC3aEv1mqAUkY9TiZWOE2IcYpvxJzw0LdvkDzGW5072JSlZZYQMqq2Llwg63LIp6qBlj1JLHMLnBqk7Ubatmjw== +"@swc/html-linux-arm-gnueabihf@1.12.6": + version "1.12.6" + resolved "https://registry.yarnpkg.com/@swc/html-linux-arm-gnueabihf/-/html-linux-arm-gnueabihf-1.12.6.tgz#aeeeb4b27c06d0f813bbebd6ed48e471fb5873b8" + integrity sha512-F0Z2Fmvdw4vTmmJyFZaGMklrZkrtT9A5d8K1Ez2f7SZwhU09e2cgi49PCHL7wBfc5MBItnugdVJKYi/V6O/Jsg== -"@swc/html-linux-arm64-gnu@1.11.29": - version "1.11.29" - resolved "https://registry.yarnpkg.com/@swc/html-linux-arm64-gnu/-/html-linux-arm64-gnu-1.11.29.tgz#ef015d81d3a011d6c273a428eee130b3aac790b7" - integrity sha512-seo+lCiBUggTR9NsHE4qVC+7+XIfLHK7yxWiIsXb8nNAXDcqVZ0Rxv8O1Y1GTeJfUlcCt1koahCG2AeyWpYFBg== +"@swc/html-linux-arm64-gnu@1.12.6": + version "1.12.6" + resolved "https://registry.yarnpkg.com/@swc/html-linux-arm64-gnu/-/html-linux-arm64-gnu-1.12.6.tgz#5aaf98ff9bced40c70a8662dde73db3a1bf21991" + integrity sha512-2S9hXG5EvDMHdjeiVANft+mZ+dRUrqUqKEAM0GehxsnG/ITT4uTolI3u/upMo7t1leOMWcz85hJZqDbVtfyP5Q== -"@swc/html-linux-arm64-musl@1.11.29": - version "1.11.29" - resolved "https://registry.yarnpkg.com/@swc/html-linux-arm64-musl/-/html-linux-arm64-musl-1.11.29.tgz#b20e4b442287367c4c1d62db2b8065106542f432" - integrity sha512-bK8K6t3hHgaZZ1vMNaZ+8x42EWJPEX1Dx4zi6ulMhKa1uan+DjW5SiMlUg0an16fFSYfE+r9oFC4cFEbGP1o4Q== +"@swc/html-linux-arm64-musl@1.12.6": + version "1.12.6" + resolved "https://registry.yarnpkg.com/@swc/html-linux-arm64-musl/-/html-linux-arm64-musl-1.12.6.tgz#7f658b07b41ca7910d1af980988549d638666ebc" + integrity sha512-RqKvGk4V2HpEObFva1AbhhEpvH8VrRI1sRjHZW7I0kTWZZkg13tJXSmGhIAfUgJFGWvvVSoZ/8TSyRq8Ju6Pvw== -"@swc/html-linux-x64-gnu@1.11.29": - version "1.11.29" - resolved "https://registry.yarnpkg.com/@swc/html-linux-x64-gnu/-/html-linux-x64-gnu-1.11.29.tgz#c45505b3e22c02dc8bdef8b3da48ba855527a62c" - integrity sha512-34tSms5TkRUCr+J6uuSE/11ECcfIpp5R1ODuIgxZRUd/u88pQGKzLVNLWGPLw4b3cZSjnAn+PFJl7BtaYl0UyQ== +"@swc/html-linux-x64-gnu@1.12.6": + version "1.12.6" + resolved "https://registry.yarnpkg.com/@swc/html-linux-x64-gnu/-/html-linux-x64-gnu-1.12.6.tgz#b32b9f6fa65ef322d499e35ac0d1599ec425ad93" + integrity sha512-nZzjhrya4VFfT2jX2EYe+FF1EzeghHAB5wyOASFN35CxOpJMhr/04COu5uRggZGYD+19s1LrLelKhSOBAPDrOw== -"@swc/html-linux-x64-musl@1.11.29": - version "1.11.29" - resolved "https://registry.yarnpkg.com/@swc/html-linux-x64-musl/-/html-linux-x64-musl-1.11.29.tgz#86116e7db3cf02b1a627bc94c374d26ce6f9d68a" - integrity sha512-oJLLrX94ccaniWdQt8PH6K2u8aN/ehBo/YPg84LycFtaud/k73Fa1kh6Neq8vbWI4CugIWTl4LXWoHm+l+QYeA== +"@swc/html-linux-x64-musl@1.12.6": + version "1.12.6" + resolved "https://registry.yarnpkg.com/@swc/html-linux-x64-musl/-/html-linux-x64-musl-1.12.6.tgz#ac03730723528385f71b2a30b635002eea4f23e1" + integrity sha512-hJdSZw5lo+Ws355gs6M2cV4QTbRmc6Ide7kUYMoSQQFyZVc05am2sulPLfOTHmzV8BW3QZ3apO7wcKTzJ/yBbQ== -"@swc/html-win32-arm64-msvc@1.11.29": - version "1.11.29" - resolved "https://registry.yarnpkg.com/@swc/html-win32-arm64-msvc/-/html-win32-arm64-msvc-1.11.29.tgz#cf7e57b8c0b52f7f93abc307b0cb78d8213b3c13" - integrity sha512-nw4TCFfA4YV6jicRdicJZPKW+ihOZPMKEG/4bj1/6HqXw1T2pXI070ASOLE0KOHYuoyV/jConEHfIjlU0olneA== +"@swc/html-win32-arm64-msvc@1.12.6": + version "1.12.6" + resolved "https://registry.yarnpkg.com/@swc/html-win32-arm64-msvc/-/html-win32-arm64-msvc-1.12.6.tgz#10fd59442feda8e3d0ce6dfccd3bef6fc1e01440" + integrity sha512-l7kFWXr4/A5joeJBSft8oGMVxXOORu6oKMSNk0SU9kFlSaqmQM9sFXW8Mny7P5bvJoNb/fGjnJ3o7BmSjwu3ow== -"@swc/html-win32-ia32-msvc@1.11.29": - version "1.11.29" - resolved "https://registry.yarnpkg.com/@swc/html-win32-ia32-msvc/-/html-win32-ia32-msvc-1.11.29.tgz#61c91409c3fcdf942891c02aa2a2eac1892d1907" - integrity sha512-rO6X4qOofGpKV8pyZ7VblJn+J3PHEqeWHJkJfzwP7c04Flr1oLyuLbTU8lwf8enXrTAZqitHZs+OpofKcUwHEw== +"@swc/html-win32-ia32-msvc@1.12.6": + version "1.12.6" + resolved "https://registry.yarnpkg.com/@swc/html-win32-ia32-msvc/-/html-win32-ia32-msvc-1.12.6.tgz#aaa89f639059c1263855c571d6678ca8c5ce8e8a" + integrity sha512-4PQysHukXaGUbP9af6DdqEIuNHMShUj5xQrVZ9M/JNV77JuX8RhTTc8Nq4IzGvCepS77gJnKg2nUbKEOt0vHaQ== -"@swc/html-win32-x64-msvc@1.11.29": - version "1.11.29" - resolved "https://registry.yarnpkg.com/@swc/html-win32-x64-msvc/-/html-win32-x64-msvc-1.11.29.tgz#0e78b507d2bf28315487655852008cdecfe84535" - integrity sha512-GSCihzBItEPJAeLzkAtw0ZGbxRGMsGt1Z1ugo0uHva1R3Eybkqu9qoax1tGAON+EJzeiHRqphhNgh8MVDpnKnQ== +"@swc/html-win32-x64-msvc@1.12.6": + version "1.12.6" + resolved "https://registry.yarnpkg.com/@swc/html-win32-x64-msvc/-/html-win32-x64-msvc-1.12.6.tgz#632091341d39231c813fab1beabab4408d6ffe33" + integrity sha512-dNg1qIzriAUQkSwWQP+b7GK09zU126VYt9Eng4RlLzdvZYO1EWrnvTLGgvAADyLk8ELvrDUJ1joaaKzFzZXVOQ== "@swc/html@^1.7.39": - version "1.11.29" - resolved "https://registry.yarnpkg.com/@swc/html/-/html-1.11.29.tgz#6e9e1b8ea65baa0d6f25cb883565a5e7d22d2858" - integrity sha512-Tsk/o6Eo3lDvHPGjLqVwXGEdC1bemGzByPWx/TrF5N7qEsanRblPeRcJzLl6LbWa80pRYIRB6T4VqdXXZqklaw== + version "1.12.6" + resolved "https://registry.yarnpkg.com/@swc/html/-/html-1.12.6.tgz#faf01ad0594287680bdb495b424da0f232f81216" + integrity sha512-Qki6Ci6f16BWJhEz5gNB/2QAsSIYvvIjLYUNsrmo1P//By7SF42oDZcu7jPLpsdlMK+qGH9n37be+HZFj9Zn5w== dependencies: "@swc/counter" "^0.1.3" optionalDependencies: - "@swc/html-darwin-arm64" "1.11.29" - "@swc/html-darwin-x64" "1.11.29" - "@swc/html-linux-arm-gnueabihf" "1.11.29" - "@swc/html-linux-arm64-gnu" "1.11.29" - "@swc/html-linux-arm64-musl" "1.11.29" - "@swc/html-linux-x64-gnu" "1.11.29" - "@swc/html-linux-x64-musl" "1.11.29" - "@swc/html-win32-arm64-msvc" "1.11.29" - "@swc/html-win32-ia32-msvc" "1.11.29" - "@swc/html-win32-x64-msvc" "1.11.29" + "@swc/html-darwin-arm64" "1.12.6" + "@swc/html-darwin-x64" "1.12.6" + "@swc/html-linux-arm-gnueabihf" "1.12.6" + "@swc/html-linux-arm64-gnu" "1.12.6" + "@swc/html-linux-arm64-musl" "1.12.6" + "@swc/html-linux-x64-gnu" "1.12.6" + "@swc/html-linux-x64-musl" "1.12.6" + "@swc/html-win32-arm64-msvc" "1.12.6" + "@swc/html-win32-ia32-msvc" "1.12.6" + "@swc/html-win32-x64-msvc" "1.12.6" -"@swc/types@^0.1.21": - version "0.1.21" - resolved "https://registry.yarnpkg.com/@swc/types/-/types-0.1.21.tgz#6fcadbeca1d8bc89e1ab3de4948cef12344a38c0" - integrity sha512-2YEtj5HJVbKivud9N4bpPBAyZhj4S2Ipe5LkUG94alTpr7in/GU/EARgPAd3BwU+YOmFVJC2+kjqhGRi3r0ZpQ== +"@swc/types@^0.1.23": + version "0.1.23" + resolved "https://registry.yarnpkg.com/@swc/types/-/types-0.1.23.tgz#7eabf88b9cfd929253859c562ae95982ee04b4e8" + integrity sha512-u1iIVZV9Q0jxY+yM2vw/hZGDNudsN85bBpTqzAQ9rzkxW9D+e3aEM4Han+ow518gSewkXgjmEK0BD79ZcNVgPw== dependencies: "@swc/counter" "^0.1.3" @@ -3847,6 +4517,15 @@ dependencies: defer-to-connect "^2.0.1" +"@tanem/svg-injector@^10.1.68": + version "10.1.68" + resolved "https://registry.yarnpkg.com/@tanem/svg-injector/-/svg-injector-10.1.68.tgz#0bd08da3c4184b055a6fe16909037c96f49e3cd1" + integrity sha512-UkJajeR44u73ujtr5GVSbIlELDWD/mzjqWe54YMK61ljKxFcJoPd9RBSaO7xj02ISCWUqJW99GjrS+sVF0UnrA== + dependencies: + "@babel/runtime" "^7.23.2" + content-type "^1.0.5" + tslib "^2.6.2" + "@tanstack/react-virtual@^3.0.0-beta.60": version "3.5.1" resolved "https://registry.yarnpkg.com/@tanstack/react-virtual/-/react-virtual-3.5.1.tgz#1ce466f530a10f781871360ed2bf7ff83e664f85" @@ -4147,9 +4826,9 @@ integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw== "@types/estree@^1.0.6": - version "1.0.7" - resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.7.tgz#4158d3105276773d5b7695cd4834b1722e4f37a8" - integrity sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ== + version "1.0.8" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.8.tgz#958b91c991b1867ced318bedea0e215ee050726e" + integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w== "@types/express-serve-static-core@*", "@types/express-serve-static-core@^4.17.33": version "4.19.3" @@ -4283,6 +4962,14 @@ resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.34.tgz#10964ba0dee6ac4cd462e2795b6bebd407303433" integrity sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g== +"@types/node-fetch@^2.6.4": + version "2.6.12" + resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.12.tgz#8ab5c3ef8330f13100a7479e2cd56d3386830a03" + integrity sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA== + dependencies: + "@types/node" "*" + form-data "^4.0.0" + "@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" @@ -4302,6 +4989,13 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.45.tgz#2c0fafd78705e7a18b7906b5201a522719dc5190" integrity sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw== +"@types/node@^18.11.18": + version "18.19.86" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.86.tgz#a7e1785289c343155578b9d84a0e3e924deb948b" + integrity sha512-fifKayi175wLyKyc5qUfyENhQ1dCNI1UNjp653d8kuYcPQN5JhX3dGuP/XmvPTg/xRBn1VTLpbmi+H/Mr7tLfQ== + dependencies: + undici-types "~5.26.4" + "@types/parse5@^6.0.0": version "6.0.3" resolved "https://registry.yarnpkg.com/@types/parse5/-/parse5-6.0.3.tgz#705bb349e789efa06f43f128cef51240753424cb" @@ -4317,6 +5011,11 @@ resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.12.tgz#12bb1e2be27293c1406acb6af1c3f3a1481d98c6" integrity sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q== +"@types/prop-types@^15.7.14": + version "15.7.14" + resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.14.tgz#1433419d73b2a7ebfc6918dcefd2ec0d5cd698f2" + integrity sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ== + "@types/qs@*": version "6.9.15" resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.15.tgz#adde8a060ec9c305a82de1babc1056e73bd64dce" @@ -4712,6 +5411,87 @@ resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d" integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== +"@zag-js/core@1.17.1": + version "1.17.1" + resolved "https://registry.yarnpkg.com/@zag-js/core/-/core-1.17.1.tgz#1d47e8117352cb42b3de0dd2672189a8e0bf955b" + integrity sha512-68jh6R87QLMYrtntu34eSF9JJXRXd+/l5Mpaz/InEOwA9sjxuyJIESqO578IpI2GAqk+cE1sUTKhhPmkzeTq3g== + dependencies: + "@zag-js/dom-query" "1.17.1" + "@zag-js/utils" "1.17.1" + +"@zag-js/dom-query@1.10.0": + version "1.10.0" + resolved "https://registry.yarnpkg.com/@zag-js/dom-query/-/dom-query-1.10.0.tgz#62d5cdb887297c7522bde3e86ddb67cedf1cfad2" + integrity sha512-UQM4pHPPwpPNyuIcaDvuTjI4ntvBCV0oatpd+OcOW8NdUc2VVcPzL4cN6q1h+Q9s0Rpi+q77X0x6t9c1QWj1Iw== + dependencies: + "@zag-js/types" "1.10.0" + +"@zag-js/dom-query@1.17.1": + version "1.17.1" + resolved "https://registry.yarnpkg.com/@zag-js/dom-query/-/dom-query-1.17.1.tgz#38a8496869fb4fd1e02b6734d5f59e52d17bfc71" + integrity sha512-fwwzEKLPq3kAZVkkPBdskL4Ge4aHRAGqBLfAHCKioQNgvKYGRTzqmGA6ijls9ESULUWf0M2ogKstuUtY19PopA== + dependencies: + "@zag-js/types" "1.17.1" + +"@zag-js/focus-trap@^1.7.0": + version "1.10.0" + resolved "https://registry.yarnpkg.com/@zag-js/focus-trap/-/focus-trap-1.10.0.tgz#c292010997ce09581aeb1729f9151a80aa4cf141" + integrity sha512-6+SPzXws7BurUb5AxHD6RoygInvPkGhleJmClQadeFhOlOdZdaeqwZjnoA3WoH/15V4NfUnoIzy72Su36D8RmA== + dependencies: + "@zag-js/dom-query" "1.10.0" + +"@zag-js/presence@^1.13.1": + version "1.17.1" + resolved "https://registry.yarnpkg.com/@zag-js/presence/-/presence-1.17.1.tgz#9fee698db453fa49c743a175910b2ba107a9bed5" + integrity sha512-2b9/4gs/ZuTpplqNjTARWjEgqkV8pMjcrH5u/fFng2cm5JRhcPrgWDSeOiahKOCdWj8x+f5EkNVvBOqs4Bmcsw== + dependencies: + "@zag-js/core" "1.17.1" + "@zag-js/dom-query" "1.17.1" + "@zag-js/types" "1.17.1" + +"@zag-js/react@^1.13.1": + version "1.17.1" + resolved "https://registry.yarnpkg.com/@zag-js/react/-/react-1.17.1.tgz#917f6fd739a9e54e73578f03813e2de96cc919c2" + integrity sha512-hgIpkHpfJByWMtaBvrJQNxBsEghFDWDWRx/JcG5cv+0VDS3bdT2U6b4AWRq6/6CMI1a2bXodgxXrgXj0t1UofQ== + dependencies: + "@zag-js/core" "1.17.1" + "@zag-js/store" "1.17.1" + "@zag-js/types" "1.17.1" + "@zag-js/utils" "1.17.1" + +"@zag-js/store@1.17.1": + version "1.17.1" + resolved "https://registry.yarnpkg.com/@zag-js/store/-/store-1.17.1.tgz#d7833045c56169f028324d64b8fd3dc2f78df22a" + integrity sha512-01iHhN08QezWTgouaAQdOW/WQUieTBv3Abl3QeGPtQ1UC8oygG84zea1uF+FzqxhT/KtWvI2AT0zRaw368aqVQ== + dependencies: + proxy-compare "3.0.1" + +"@zag-js/types@1.10.0": + version "1.10.0" + resolved "https://registry.yarnpkg.com/@zag-js/types/-/types-1.10.0.tgz#d6f0d406d06cc954622b0234d2c2aeab64999ffd" + integrity sha512-HlM+EHYPLPaHgmuf2Bg5isNy2Kv30nwaANbkcMhVQYi8OfrTraxUQbTDXk3hb56qFmW1HQCMZzt1L7aS2qlOyQ== + dependencies: + csstype "3.1.3" + +"@zag-js/types@1.17.1": + version "1.17.1" + resolved "https://registry.yarnpkg.com/@zag-js/types/-/types-1.17.1.tgz#ce75409a9a89431f790038fd145cc9353d5fa236" + integrity sha512-KEPko1DK19hEMfM5IPKTZQtpf4HC3X56qwckezRX1yk+/vGotVUxdjRIrv+pcITjlFAoQQO9TiiZv2UiiVrFGA== + dependencies: + csstype "3.1.3" + +"@zag-js/utils@1.17.1": + version "1.17.1" + resolved "https://registry.yarnpkg.com/@zag-js/utils/-/utils-1.17.1.tgz#0015f9a160877672a75a2ed0419c289fb5fcb22d" + integrity sha512-+w/Kx7uZufg3cD6I5bQ8iSoeY3qSarPpUwrxz6FCOxJ86IAmf3ActqFC2pJ6DQCdHdkWINaKKchb4GNt8ld7KQ== + +abort-controller@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" + integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg== + dependencies: + event-target-shim "^5.0.0" + accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.8: version "1.3.8" resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" @@ -4743,9 +5523,9 @@ acorn@^8.0.0, acorn@^8.0.4, acorn@^8.11.0, acorn@^8.7.1, acorn@^8.8.2: integrity sha512-RTvkC4w+KNXrM39/lWCUaG0IbRkWdCv7W/IOW9oU6SawyxulvkQy5HQPVTKxEjczcUvapcrw3cFx/60VN/NRNw== acorn@^8.14.0: - version "8.14.1" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.14.1.tgz#721d5dc10f7d5b5609a891773d47731796935dfb" - integrity sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg== + version "8.15.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.15.0.tgz#a360898bc415edaac46c8241f6383975b930b816" + integrity sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg== address@^1.0.1: version "1.2.2" @@ -4759,6 +5539,13 @@ agent-base@6: dependencies: debug "4" +agentkeepalive@^4.2.1: + version "4.6.0" + resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-4.6.0.tgz#35f73e94b3f40bf65f105219c623ad19c136ea6a" + integrity sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ== + dependencies: + humanize-ms "^1.2.1" + aggregate-error@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a" @@ -4822,30 +5609,30 @@ ajv@^8.0.0, ajv@^8.9.0: uri-js "^4.4.1" algoliasearch-helper@^3.22.6: - version "3.25.0" - resolved "https://registry.yarnpkg.com/algoliasearch-helper/-/algoliasearch-helper-3.25.0.tgz#15cc79ad7909db66b8bb5a5a9c38b40e3941fa2f" - integrity sha512-vQoK43U6HXA9/euCqLjvyNdM4G2Fiu/VFp4ae0Gau9sZeIKBPvUPnXfLYAe65Bg7PFuw03coeu5K6lTPSXRObw== + version "3.26.0" + resolved "https://registry.yarnpkg.com/algoliasearch-helper/-/algoliasearch-helper-3.26.0.tgz#d6e283396a9fc5bf944f365dc3b712570314363f" + integrity sha512-Rv2x3GXleQ3ygwhkhJubhhYGsICmShLAiqtUuJTUkr9uOCOXyF2E71LVT4XDnVffbknv8XgScP4U0Oxtgm+hIw== dependencies: "@algolia/events" "^4.0.1" algoliasearch@^5.14.2, algoliasearch@^5.17.1: - version "5.25.0" - resolved "https://registry.yarnpkg.com/algoliasearch/-/algoliasearch-5.25.0.tgz#7337b097deadeca0e6e985c0f8724abea189994f" - integrity sha512-n73BVorL4HIwKlfJKb4SEzAYkR3Buwfwbh+MYxg2mloFph2fFGV58E90QTzdbfzWrLn4HE5Czx/WTjI8fcHaMg== + version "5.29.0" + resolved "https://registry.yarnpkg.com/algoliasearch/-/algoliasearch-5.29.0.tgz#0feae8e0a71fced857be4e97c434ef9dce89783b" + integrity sha512-E2l6AlTWGznM2e7vEE6T6hzObvEyXukxMOlBmVlMyixZyK1umuO/CiVc6sDBbzVH0oEviCE5IfVY1oZBmccYPQ== dependencies: - "@algolia/client-abtesting" "5.25.0" - "@algolia/client-analytics" "5.25.0" - "@algolia/client-common" "5.25.0" - "@algolia/client-insights" "5.25.0" - "@algolia/client-personalization" "5.25.0" - "@algolia/client-query-suggestions" "5.25.0" - "@algolia/client-search" "5.25.0" - "@algolia/ingestion" "1.25.0" - "@algolia/monitoring" "1.25.0" - "@algolia/recommend" "5.25.0" - "@algolia/requester-browser-xhr" "5.25.0" - "@algolia/requester-fetch" "5.25.0" - "@algolia/requester-node-http" "5.25.0" + "@algolia/client-abtesting" "5.29.0" + "@algolia/client-analytics" "5.29.0" + "@algolia/client-common" "5.29.0" + "@algolia/client-insights" "5.29.0" + "@algolia/client-personalization" "5.29.0" + "@algolia/client-query-suggestions" "5.29.0" + "@algolia/client-search" "5.29.0" + "@algolia/ingestion" "1.29.0" + "@algolia/monitoring" "1.29.0" + "@algolia/recommend" "5.29.0" + "@algolia/requester-browser-xhr" "5.29.0" + "@algolia/requester-fetch" "5.29.0" + "@algolia/requester-node-http" "5.29.0" allof-merge@^0.6.6: version "0.6.6" @@ -4854,6 +5641,11 @@ allof-merge@^0.6.6: dependencies: json-crawl "^0.5.3" +altcha-lib@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/altcha-lib/-/altcha-lib-1.2.0.tgz#a8b874ace261751473686adc5cc210be7449ba0d" + integrity sha512-S5WF8QLNRaM1hvK24XPhOLfu9is2EBCvH7+nv50sM5CaIdUCqQCd0WV/qm/ZZFGTdSoKLuDp+IapZxBLvC+SNg== + ansi-align@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/ansi-align/-/ansi-align-3.0.1.tgz#0cdf12e111ace773a86e9a1fad1225c43cb19a59" @@ -4932,6 +5724,13 @@ argparse@^2.0.1: resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== +aria-hidden@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/aria-hidden/-/aria-hidden-1.2.4.tgz#b78e383fdbc04d05762c78b4a25a501e736c4522" + integrity sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A== + dependencies: + tslib "^2.0.0" + array-flatten@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" @@ -4957,6 +5756,11 @@ async@3.2.4: resolved "https://registry.yarnpkg.com/async/-/async-3.2.4.tgz#2d22e00f8cddeb5fde5dd33522b56d1cf569a81c" integrity sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ== +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== + at-least-node@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2" @@ -5312,10 +6116,10 @@ caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001599, caniuse-lite@^1.0.30001629: resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001702.tgz" integrity sha512-LoPe/D7zioC0REI5W73PeR1e1MLCipRGq/VkovJnd6Df+QVqT+vT33OXCp8QUd7kA7RZrHWxb1B36OQKI/0gOA== -caniuse-lite@^1.0.30001702, caniuse-lite@^1.0.30001716, caniuse-lite@^1.0.30001718: - version "1.0.30001718" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001718.tgz#dae13a9c80d517c30c6197515a96131c194d8f82" - integrity sha512-AflseV1ahcSunK53NfEs9gFWgOEmzr0f+kaMFA4xiLZlr9Hzt7HxcSpIFcnNCUkz6R6dWKa54rUz3HUmI3nVcw== +caniuse-lite@^1.0.30001702, caniuse-lite@^1.0.30001718: + version "1.0.30001724" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001724.tgz#312e163553dd70d2c0fb603d74810c85d8ed94a0" + integrity sha512-WqJo7p0TbHDOythNTqYujmaJTvtYRZrjpP8TCvH6Vb9CYJerJNKamKzIWOM4BkQatWj9H2lYulpdAQNBe7QhNA== ccount@^2.0.0: version "2.0.1" @@ -5455,6 +6259,13 @@ ci-info@^3.2.0: resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.9.0.tgz#4279a62028a7b1f262f3473fc9605f5e218c59b4" integrity sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ== +class-variance-authority@0.7.1: + version "0.7.1" + resolved "https://registry.yarnpkg.com/class-variance-authority/-/class-variance-authority-0.7.1.tgz#4008a798a0e4553a781a57ac5177c9fb5d043787" + integrity sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg== + dependencies: + clsx "^2.1.1" + clean-css@^5.2.2, clean-css@^5.3.3, clean-css@~5.3.2: version "5.3.3" resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-5.3.3.tgz#b330653cd3bd6b75009cc25c714cae7b93351ccd" @@ -5504,16 +6315,16 @@ clone-deep@^4.0.1: kind-of "^6.0.2" shallow-clone "^3.0.0" +clsx@2.1.1, clsx@^2.0.0, clsx@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999" + integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA== + clsx@^1.1.1, clsx@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12" integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg== -clsx@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999" - integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA== - collapse-white-space@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/collapse-white-space/-/collapse-white-space-2.1.0.tgz#640257174f9f42c740b40f3b55ee752924feefca" @@ -5558,11 +6369,23 @@ colorette@^2.0.10: resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a" integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w== +colorjs.io@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/colorjs.io/-/colorjs.io-0.5.2.tgz#63b20139b007591ebc3359932bef84628eb3fcef" + integrity sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw== + combine-promises@^1.1.0: version "1.2.0" resolved "https://registry.yarnpkg.com/combine-promises/-/combine-promises-1.2.0.tgz#5f2e68451862acf85761ded4d9e2af7769c2ca6a" integrity sha512-VcQB1ziGD0NXrhKxiwyNbCDmRzs/OShMs2GqW2DlU2A/Sd0nQxE1oWDAE5O0ygSx5mgQOn9eIFh7yKPgFRVkPQ== +combined-stream@^1.0.8: + version "1.0.8" + 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" + comma-separated-tokens@^2.0.0: version "2.0.3" resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz#4e89c9458acb61bc8fef19f4529973b2392839ee" @@ -5698,7 +6521,7 @@ content-disposition@0.5.4: dependencies: safe-buffer "5.2.1" -content-type@~1.0.4, content-type@~1.0.5: +content-type@^1.0.5, content-type@~1.0.4, content-type@~1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== @@ -5750,11 +6573,11 @@ core-js-compat@^3.31.0, core-js-compat@^3.36.1: browserslist "^4.23.0" core-js-compat@^3.40.0: - version "3.42.0" - resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.42.0.tgz#ce19c29706ee5806e26d3cb3c542d4cfc0ed51bb" - integrity sha512-bQasjMfyDGyaeWKBIu33lHh9qlSR0MFE/Nmc6nMjf/iU9b3rSMdAYz1Baxrv4lPdGUsTqZudHA4jIGSJy0SWZQ== + version "3.43.0" + resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.43.0.tgz#055587369c458795ef316f65e0aabb808fb15840" + integrity sha512-2GML2ZsCc5LR7hZYz4AXmjQw8zuy2T//2QntwdnpuYI7jteT6GVYJL7F6C2C57R7gSYrcqVW3lAALefdbhBLDA== dependencies: - browserslist "^4.24.4" + browserslist "^4.25.0" core-js-pure@^3.30.2: version "3.37.1" @@ -5919,9 +6742,9 @@ css-what@^6.0.1, css-what@^6.1.0: integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw== cssdb@^8.3.0: - version "8.3.0" - resolved "https://registry.yarnpkg.com/cssdb/-/cssdb-8.3.0.tgz#940becad497b8509ad822a28fb0cfe54c969ccfe" - integrity sha512-c7bmItIg38DgGjSwDPZOYF/2o0QU/sSgkWOMyl8votOfgFuyiFKWPesmCGEsrGLxEA9uL540cp8LdaGEjUGsZQ== + version "8.3.1" + resolved "https://registry.yarnpkg.com/cssdb/-/cssdb-8.3.1.tgz#0ac96395b7092ffee14563e948cf43c2019b051e" + integrity sha512-XnDRQMXucLueX92yDe0LPKupXetWoFOgawr4O4X41l5TltgK2NVbJJVDnnOywDYfW1sTJ28AcXGKOqdRKwCcmQ== cssesc@^3.0.0: version "3.0.0" @@ -5997,7 +6820,7 @@ csso@^5.0.5: dependencies: css-tree "~2.2.0" -csstype@^3.0.2: +csstype@3.1.3, csstype@^3.0.2: version "3.1.3" resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81" integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw== @@ -6404,6 +7227,11 @@ delaunator@5: dependencies: robust-predicates "^3.0.2" +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== + depd@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" @@ -6414,7 +7242,7 @@ depd@~1.1.2: resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ== -dequal@^2.0.0: +dequal@^2.0.0, dequal@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be" integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== @@ -6434,6 +7262,11 @@ detect-libc@^2.0.3: resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.4.tgz#f04715b8ba815e53b4d8109655b6508a6865a7e8" integrity sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA== +detect-node-es@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/detect-node-es/-/detect-node-es-1.1.0.tgz#163acdf643330caa0b4cd7c21e7ee7755d6fa493" + integrity sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ== + detect-node@^2.0.4: version "2.1.0" resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.1.0.tgz#c9c70775a49c3d03bc2c06d9a73be550f978f8b1" @@ -6601,7 +7434,7 @@ domhandler@^5.0.2, domhandler@^5.0.3: dependencies: domelementtype "^2.3.0" -dompurify@^3.2.4: +dompurify@^3.2.5: version "3.2.6" resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.2.6.tgz#ca040a6ad2b88e2a92dc45f38c79f84a714a1cad" integrity sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ== @@ -6670,11 +7503,6 @@ electron-to-chromium@^1.4.796: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.803.tgz#cf55808a5ee12e2a2778bbe8cdc941ef87c2093b" integrity sha512-61H9mLzGOCLLVsnLiRzCbc63uldP0AniRYPV3hbGVtONA1pI7qSGILdbofR7A8TMbOypDocEAjH/e+9k1QIe3g== -electron-to-chromium@^1.5.149: - version "1.5.158" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.158.tgz#e5f01fc7fdf810d9d223e30593e0839c306276d4" - integrity sha512-9vcp2xHhkvraY6AHw2WMi+GDSLPX42qe2xjYaVoZqFRJiOcilVQFq9mZmpuHEQpzlgGDelKlV7ZiGcmMsc8WxQ== - electron-to-chromium@^1.5.160: version "1.5.172" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.172.tgz#fe1d99028d8d6321668d0f1fed61d99ac896259c" @@ -6726,9 +7554,9 @@ enhanced-resolve@^5.17.0: tapable "^2.2.0" enhanced-resolve@^5.17.1: - version "5.18.1" - resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz#728ab082f8b7b6836de51f1637aab5d3b9568faf" - integrity sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg== + version "5.18.2" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.18.2.tgz#7903c5b32ffd4b2143eeb4b92472bd68effd5464" + integrity sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ== dependencies: graceful-fs "^4.2.4" tapable "^2.2.0" @@ -6779,6 +7607,16 @@ es-object-atoms@^1.0.0, es-object-atoms@^1.1.1: dependencies: es-errors "^1.3.0" +es-set-tostringtag@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz#f31dbbe0c183b00a6d26eb6325c810c0fd18bd4d" + integrity sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA== + dependencies: + es-errors "^1.3.0" + get-intrinsic "^1.2.6" + has-tostringtag "^1.0.2" + hasown "^2.0.2" + es6-promise@^3.2.1: version "3.3.1" resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-3.3.1.tgz#a08cdde84ccdbf34d027a1451bc91d4bcd28a613" @@ -6955,6 +7793,11 @@ eval@^0.1.8: "@types/node" "*" require-like ">= 0.1.1" +event-target-shim@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" + integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== + eventemitter3@^4.0.0, eventemitter3@^4.0.4: version "4.0.7" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" @@ -7023,9 +7866,9 @@ express@^4.17.3: vary "~1.1.2" exsolve@^1.0.1: - version "1.0.5" - resolved "https://registry.yarnpkg.com/exsolve/-/exsolve-1.0.5.tgz#1f5b6b4fe82ad6b28a173ccb955a635d77859dcf" - integrity sha512-pz5dvkYYKQ1AHVrgOzBKWeP4u4FRb3a6DNK2ucr0OoNwYIU4QWsJ+NM36LLzORT+z845MzKHHhpXiUF5nvQoJg== + version "1.0.7" + resolved "https://registry.yarnpkg.com/exsolve/-/exsolve-1.0.7.tgz#3b74e4c7ca5c5f9a19c3626ca857309fa99f9e9e" + integrity sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw== extend-shallow@^2.0.1: version "2.0.1" @@ -7200,16 +8043,39 @@ foreground-child@^3.1.0: cross-spawn "^7.0.0" signal-exit "^4.0.1" +form-data-encoder@1.7.2: + version "1.7.2" + resolved "https://registry.yarnpkg.com/form-data-encoder/-/form-data-encoder-1.7.2.tgz#1f1ae3dccf58ed4690b86d87e4f57c654fbab040" + integrity sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A== + form-data-encoder@^2.1.2: version "2.1.4" resolved "https://registry.yarnpkg.com/form-data-encoder/-/form-data-encoder-2.1.4.tgz#261ea35d2a70d48d30ec7a9603130fa5515e9cd5" integrity sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw== +form-data@^4.0.0: + version "4.0.2" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.2.tgz#35cabbdd30c3ce73deb2c42d3c8d3ed9ca51794c" + integrity sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + es-set-tostringtag "^2.1.0" + mime-types "^2.1.12" + format@^0.2.0: version "0.2.2" resolved "https://registry.yarnpkg.com/format/-/format-0.2.2.tgz#d6170107e9efdc4ed30c9dc39016df942b5cb58b" integrity sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww== +formdata-node@^4.3.2: + version "4.4.1" + resolved "https://registry.yarnpkg.com/formdata-node/-/formdata-node-4.4.1.tgz#23f6a5cb9cb55315912cbec4ff7b0f59bbd191e2" + integrity sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ== + dependencies: + node-domexception "1.0.0" + web-streams-polyfill "4.0.0-beta.3" + forwarded@0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" @@ -7290,7 +8156,7 @@ get-intrinsic@^1.1.3, get-intrinsic@^1.2.4: has-symbols "^1.0.3" hasown "^2.0.0" -get-intrinsic@^1.2.5, get-intrinsic@^1.3.0: +get-intrinsic@^1.2.5, get-intrinsic@^1.2.6, get-intrinsic@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01" integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ== @@ -7306,6 +8172,11 @@ get-intrinsic@^1.2.5, get-intrinsic@^1.3.0: hasown "^2.0.2" math-intrinsics "^1.1.0" +get-nonce@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/get-nonce/-/get-nonce-1.0.1.tgz#fdf3f0278073820d2ce9426c18f07481b1e0cdf3" + integrity sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q== + get-own-enumerable-property-symbols@^3.0.0: version "3.0.2" resolved "https://registry.yarnpkg.com/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz#b5fde77f22cbe35f390b4e089922c50bce6ef664" @@ -7523,6 +8394,13 @@ has-symbols@^1.1.0: resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338" integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ== +has-tostringtag@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc" + integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw== + dependencies: + has-symbols "^1.0.3" + has-yarn@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/has-yarn/-/has-yarn-3.0.0.tgz#c3c21e559730d1d3b57e28af1f30d06fac38147d" @@ -7821,6 +8699,11 @@ html-tags@^3.3.1: resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-3.3.1.tgz#a04026a18c882e4bba8a01a3d39cfe465d40b5ce" integrity sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ== +html-url-attributes@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/html-url-attributes/-/html-url-attributes-3.0.1.tgz#83b052cd5e437071b756cd74ae70f708870c2d87" + integrity sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ== + html-void-elements@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/html-void-elements/-/html-void-elements-2.0.1.tgz#29459b8b05c200b6c5ee98743c41b979d577549f" @@ -7949,6 +8832,18 @@ human-signals@^2.1.0: 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.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" + +humps@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/humps/-/humps-2.0.1.tgz#dd02ea6081bd0568dc5d073184463957ba9ef9aa" + integrity sha512-E0eIbrFWUhwfXJmsbdjRQFQPrl5pTEoKlz163j1mTqqUnU9PgR4AgB8AIITzuB3vLBdxZXyZ9TDIrwB2OASz4g== + iconv-lite@0.4.24: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" @@ -7989,9 +8884,9 @@ immer@^9.0.21: integrity sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA== immutable@^5.0.2: - version "5.1.2" - resolved "https://registry.yarnpkg.com/immutable/-/immutable-5.1.2.tgz#e8169476414505e5a4fa650107b65e1227d16d4b" - integrity sha512-qHKXW1q6liAk1Oys6umoaZbDRqjcjgSrbnrifHsfsttza7zcvRAsL7mMV6xWcyhwQy7Xj5v4hhbr6b+iDYwlmQ== + version "5.1.3" + resolved "https://registry.yarnpkg.com/immutable/-/immutable-5.1.3.tgz#e6486694c8b76c37c063cca92399fa64098634d4" + integrity sha512-+chQdDfvscSF1SJqv2gn4SRO2ZyS3xL3r7IW/wWEEzrzLisnOlKiQu5ytC/BVNcS15C39WT2Hg/bjKjDMcu+zg== import-fresh@^3.3.0: version "3.3.0" @@ -8244,6 +9139,11 @@ is-typedarray@^1.0.0: resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" integrity sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA== +is-what@^4.1.8: + version "4.1.16" + resolved "https://registry.yarnpkg.com/is-what/-/is-what-4.1.16.tgz#1ad860a19da8b4895ad5495da3182ce2acdd7a6f" + integrity sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A== + is-wsl@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271" @@ -8715,6 +9615,11 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" +lucide-react@^0.503.0: + version "0.503.0" + resolved "https://registry.yarnpkg.com/lucide-react/-/lucide-react-0.503.0.tgz#4ac55b262fa613f9497531c9df50ea0e883d2de2" + integrity sha512-HGGkdlPWQ0vTF8jJ5TdIqhQXZi6uh3LnNgfZ8MHiuxFfX3RZeA79r2MW2tHAZKlAVfoNE8esm3p+O6VkIvpj6w== + markdown-extensions@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/markdown-extensions/-/markdown-extensions-2.0.0.tgz#34bebc83e9938cae16e0e017e4a9814a8330d3c4" @@ -8732,7 +9637,7 @@ markdown-table@^3.0.0: resolved "https://registry.yarnpkg.com/markdown-table/-/markdown-table-3.0.3.tgz#e6331d30e493127e031dd385488b5bd326e4a6bd" integrity sha512-Z1NL3Tb1M9wH4XESsCDEksWoKTdlUafKc4pt0GRwjUyXaCFZ+dc3g2erqB6zm3szA2IUSi7VnPI+o/9jnxh9hw== -marked@^15.0.7: +marked@^15.0.7, marked@^15.0.9: version "15.0.12" resolved "https://registry.yarnpkg.com/marked/-/marked-15.0.12.tgz#30722c7346e12d0a2d0207ab9b0c4f0102d86c4e" integrity sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA== @@ -9194,6 +10099,13 @@ memoize-one@^5.1.1: resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e" integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q== +merge-anything@5.1.7: + version "5.1.7" + resolved "https://registry.yarnpkg.com/merge-anything/-/merge-anything-5.1.7.tgz#94f364d2b0cf21ac76067b5120e429353b3525d7" + integrity sha512-eRtbOb1N5iyH0tkQDAoQ4Ipsp/5qSR79Dzrz8hEPxRX10RWWR/iQXdoKmBSRCThY1Fh5EhISDtpSc93fpxUniQ== + dependencies: + is-what "^4.1.8" + merge-descriptors@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" @@ -9210,13 +10122,13 @@ merge2@^1.3.0, merge2@^1.4.1: integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== mermaid@>=11.6.0: - version "11.6.0" - resolved "https://registry.yarnpkg.com/mermaid/-/mermaid-11.6.0.tgz#eee45cdc3087be561a19faf01745596d946bb575" - integrity sha512-PE8hGUy1LDlWIHWBP05SFdqUHGmRcCcK4IzpOKPE35eOw+G9zZgcnMpyunJVUEOgb//KBORPjysKndw8bFLuRg== + version "11.7.0" + resolved "https://registry.yarnpkg.com/mermaid/-/mermaid-11.7.0.tgz#53f319147632db15e499c5ccb72b24b277a00bae" + integrity sha512-/1/5R0rt0Z1Ak0CuznAnCF3HtQgayRXUz6SguzOwN4L+DuCobz0UxnQ+ZdTSZ3AugKVVh78tiVmsHpHWV25TCw== dependencies: "@braintree/sanitize-url" "^7.0.4" "@iconify/utils" "^2.1.33" - "@mermaid-js/parser" "^0.4.0" + "@mermaid-js/parser" "^0.5.0" "@types/d3" "^7.4.3" cytoscape "^3.29.3" cytoscape-cose-bilkent "^4.1.0" @@ -9225,7 +10137,7 @@ mermaid@>=11.6.0: d3-sankey "^0.12.3" dagre-d3-es "7.0.11" dayjs "^1.11.13" - dompurify "^3.2.4" + dompurify "^3.2.5" katex "^0.16.9" khroma "^2.1.0" lodash-es "^4.17.21" @@ -10041,7 +10953,7 @@ mime-types@2.1.31: dependencies: mime-db "1.48.0" -mime-types@2.1.35, mime-types@^2.1.27, mime-types@^2.1.31, mime-types@~2.1.17, mime-types@~2.1.24, mime-types@~2.1.34: +mime-types@2.1.35, mime-types@^2.1.12, mime-types@^2.1.27, mime-types@^2.1.31, mime-types@~2.1.17, mime-types@~2.1.24, mime-types@~2.1.34: version "2.1.35" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== @@ -10147,7 +11059,7 @@ ms@2.1.2: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== -ms@2.1.3, ms@^2.1.3: +ms@2.1.3, ms@^2.0.0, ms@^2.1.3: version "2.1.3" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== @@ -10207,6 +11119,11 @@ node-addon-api@^7.0.0: resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-7.1.1.tgz#1aba6693b0f255258a049d621329329322aad558" integrity sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ== +node-domexception@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/node-domexception/-/node-domexception-1.0.0.tgz#6888db46a1f71c0b76b3f7555016b63fe64766e5" + integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ== + node-emoji@^2.1.0: version "2.1.3" resolved "https://registry.yarnpkg.com/node-emoji/-/node-emoji-2.1.3.tgz#93cfabb5cc7c3653aa52f29d6ffb7927d8047c06" @@ -10231,7 +11148,7 @@ node-fetch@2.6.7: dependencies: whatwg-url "^5.0.0" -node-fetch@^2.6.1: +node-fetch@^2.6.1, node-fetch@^2.6.7: version "2.7.0" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== @@ -10435,6 +11352,19 @@ open@^8.0.9, open@^8.4.0: is-docker "^2.1.1" is-wsl "^2.2.0" +openai@4.78.1: + version "4.78.1" + resolved "https://registry.yarnpkg.com/openai/-/openai-4.78.1.tgz#44c3b195d239891be9c9c53722539ad8a1fcc5f2" + integrity sha512-drt0lHZBd2lMyORckOXFPQTmnGLWSLt8VK0W9BhOKWpMFBEoHMoz5gxMPmVq5icp+sOrsbMnsmZTVHUlKvD1Ow== + dependencies: + "@types/node" "^18.11.18" + "@types/node-fetch" "^2.6.4" + abort-controller "^3.0.0" + agentkeepalive "^4.2.1" + form-data-encoder "1.7.2" + formdata-node "^4.3.2" + node-fetch "^2.6.7" + openapi-to-postmanv2@^4.21.0: version "4.21.0" resolved "https://registry.yarnpkg.com/openapi-to-postmanv2/-/openapi-to-postmanv2-4.21.0.tgz#4bc5b19ccbd1514c2b3466268a7f5dd64b61f535" @@ -10690,7 +11620,7 @@ path-type@^4.0.0: resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== -path@0.12.7: +path@0.12.7, path@^0.12.7: version "0.12.7" resolved "https://registry.yarnpkg.com/path/-/path-0.12.7.tgz#d4dc2a506c4ce2197eb481ebfcd5b36c0140b10f" integrity sha512-aXXC6s+1w7otVF9UletFkFcDsJeO7lSZBPUQhtb5O0xJe8LtYhj/GxldoL09bBj9+ZmE2hNoHqQSFMN5fikh4Q== @@ -11471,6 +12401,14 @@ pretty-time@^1.1.0: resolved "https://registry.yarnpkg.com/pretty-time/-/pretty-time-1.1.0.tgz#ffb7429afabb8535c346a34e41873adf3d74dd0e" integrity sha512-28iF6xPQrP8Oa6uxE6a1biz+lWeTOAPKggvjB8HAs6nVMKZwf5bG++632Dx614hIWgUPkgivRfG+a8uAXGTIbA== +prism-react-renderer@2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/prism-react-renderer/-/prism-react-renderer-2.4.1.tgz#ac63b7f78e56c8f2b5e76e823a976d5ede77e35f" + integrity sha512-ey8Ls/+Di31eqzUxC46h8MksNuGx/n0AAC8uKpwFau4RPDYLuE3EXTp8N8G2vX2N7UC/+IXeNUnlWBGGcAG+Ig== + dependencies: + "@types/prismjs" "^1.26.0" + clsx "^2.0.0" + prism-react-renderer@^2.0.6, prism-react-renderer@^2.3.0: version "2.3.1" resolved "https://registry.yarnpkg.com/prism-react-renderer/-/prism-react-renderer-2.3.1.tgz#e59e5450052ede17488f6bc85de1553f584ff8d5" @@ -11534,6 +12472,11 @@ proxy-addr@~2.0.7: forwarded "0.2.0" ipaddr.js "1.9.1" +proxy-compare@3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/proxy-compare/-/proxy-compare-3.0.1.tgz#3262cff3a25a6dedeaa299f6cf2369d6f7588a94" + integrity sha512-V9plBAt3qjMlS1+nC8771KNf6oJ12gExvaxnNzN/9yVRLdTv/lc+oJlnSzrdYDAvBfTStPCoiaCOTmTs0adv7Q== + proxy-from-env@1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" @@ -11680,6 +12623,13 @@ react-dom@^18.2.0: loose-envify "^1.1.0" scheduler "^0.23.2" +react-error-boundary@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/react-error-boundary/-/react-error-boundary-6.0.0.tgz#a9e552146958fa77d873b587aa6a5e97544ee954" + integrity sha512-gdlJjD7NWr0IfkPlaREN2d9uUZUlksrfOx7SX62VRerwXbMY6ftGCIZua1VG1aXFNOimhISsTq+Owp725b9SiA== + dependencies: + "@babel/runtime" "^7.12.5" + react-fast-compare@^3.0.1, react-fast-compare@^3.2.0: version "3.2.2" resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.2.tgz#929a97a532304ce9fee4bcae44234f1ce2c21d49" @@ -11701,6 +12651,11 @@ react-helmet-async@^1.3.0, "react-helmet-async@npm:@slorber/react-helmet-async@1 react-fast-compare "^3.2.0" shallowequal "^1.1.0" +react-hook-form@7.54.2: + version "7.54.2" + resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.54.2.tgz#8c26ed54c71628dff57ccd3c074b1dd377cfb211" + integrity sha512-eHpAUgUjWbZocoQYUHposymRb4ZP6d0uwUnooL2uOybA9/3tPUvoAKqEWK1WaSiTxxOfTpffNZP7QwlnM3/gEg== + react-hook-form@^7.43.8: version "7.52.0" resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.52.0.tgz#e52b33043e283719586b9dd80f6d51b68dd3999c" @@ -11759,6 +12714,22 @@ react-magic-dropzone@^1.0.1: resolved "https://registry.yarnpkg.com/react-magic-dropzone/-/react-magic-dropzone-1.0.1.tgz#bfd25b77b57e7a04aaef0a28910563b707ee54df" integrity sha512-0BIROPARmXHpk4AS3eWBOsewxoM5ndk2psYP/JmbCq8tz3uR2LIV1XiroZ9PKrmDRMctpW+TvsBCtWasuS8vFA== +react-markdown@9.0.3: + version "9.0.3" + resolved "https://registry.yarnpkg.com/react-markdown/-/react-markdown-9.0.3.tgz#c12bf60dad05e9bf650b86bcc612d80636e8456e" + integrity sha512-Yk7Z94dbgYTOrdk41Z74GoKA7rThnsbbqBTRYuxoe08qvfQ9tJVhmAKw6BJS/ZORG7kTy/s1QvYzSuaoBA1qfw== + dependencies: + "@types/hast" "^3.0.0" + devlop "^1.0.0" + hast-util-to-jsx-runtime "^2.0.0" + html-url-attributes "^3.0.0" + mdast-util-to-hast "^13.0.0" + remark-parse "^11.0.0" + remark-rehype "^11.0.0" + unified "^11.0.0" + unist-util-visit "^5.0.0" + vfile "^6.0.0" + react-markdown@^8.0.1: version "8.0.7" resolved "https://registry.yarnpkg.com/react-markdown/-/react-markdown-8.0.7.tgz#c8dbd1b9ba5f1c5e7e5f2a44de465a3caafdf89b" @@ -11813,6 +12784,36 @@ react-redux@^7.2.0: prop-types "^15.7.2" react-is "^17.0.2" +react-remove-scroll-bar@^2.3.7: + version "2.3.8" + resolved "https://registry.yarnpkg.com/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz#99c20f908ee467b385b68a3469b4a3e750012223" + integrity sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q== + dependencies: + react-style-singleton "^2.2.2" + tslib "^2.0.0" + +react-remove-scroll@^2.6.3: + version "2.6.3" + resolved "https://registry.yarnpkg.com/react-remove-scroll/-/react-remove-scroll-2.6.3.tgz#df02cde56d5f2731e058531f8ffd7f9adec91ac2" + integrity sha512-pnAi91oOk8g8ABQKGF5/M9qxmmOPxaAnopyTHYfqYEwJhyFrbbBtHuSgtKEoH0jpcxx5o3hXqH1mNd9/Oi+8iQ== + dependencies: + react-remove-scroll-bar "^2.3.7" + react-style-singleton "^2.2.3" + tslib "^2.1.0" + use-callback-ref "^1.3.3" + use-sidecar "^1.1.3" + +react-remove-scroll@^2.7.1: + version "2.7.1" + resolved "https://registry.yarnpkg.com/react-remove-scroll/-/react-remove-scroll-2.7.1.tgz#d2101d414f6d81d7d3bf033f3c1cb4785789f753" + integrity sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA== + dependencies: + react-remove-scroll-bar "^2.3.7" + react-style-singleton "^2.2.3" + tslib "^2.1.0" + use-callback-ref "^1.3.3" + use-sidecar "^1.1.3" + react-router-config@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/react-router-config/-/react-router-config-5.1.1.tgz#0f4263d1a80c6b2dc7b9c1902c9526478194a988" @@ -11848,6 +12849,33 @@ react-router@5.3.4, react-router@^5.3.4: tiny-invariant "^1.0.2" tiny-warning "^1.0.0" +react-style-singleton@^2.2.2, react-style-singleton@^2.2.3: + version "2.2.3" + resolved "https://registry.yarnpkg.com/react-style-singleton/-/react-style-singleton-2.2.3.tgz#4265608be69a4d70cfe3047f2c6c88b2c3ace388" + integrity sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ== + dependencies: + get-nonce "^1.0.0" + tslib "^2.0.0" + +react-svg@16.3.0: + version "16.3.0" + resolved "https://registry.yarnpkg.com/react-svg/-/react-svg-16.3.0.tgz#de7a4bb6ee2d465c1ff7125ec27414ac27e907d7" + integrity sha512-MvoQbITgkmpPJYwDTNdiUyoncJFfoa0D86WzoZuMQ9c/ORJURPR6rPMnXDsLOWDCAyXuV9nKZhQhGyP0HZ0MVQ== + dependencies: + "@babel/runtime" "^7.26.0" + "@tanem/svg-injector" "^10.1.68" + "@types/prop-types" "^15.7.14" + prop-types "^15.8.1" + +react-textarea-autosize@8.5.7: + version "8.5.7" + resolved "https://registry.yarnpkg.com/react-textarea-autosize/-/react-textarea-autosize-8.5.7.tgz#b2bf1913383a05ffef7fbc89c2ea21ba8133b023" + integrity sha512-2MqJ3p0Jh69yt9ktFIaZmORHXw4c4bxSIhCeWiFwmJ9EYKgLmuNII3e9c9b2UO+ijl4StnpZdqpxNIhTdHvqtQ== + dependencies: + "@babel/runtime" "^7.20.13" + use-composed-ref "^1.3.0" + use-latest "^1.2.1" + react@^18.2.0: version "18.3.1" resolved "https://registry.yarnpkg.com/react/-/react-18.3.1.tgz#49ab892009c53933625bd16b2533fc754cab2891" @@ -12008,6 +13036,15 @@ regjsparser@^0.9.1: dependencies: jsesc "~0.5.0" +rehype-raw@7.0.0, rehype-raw@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/rehype-raw/-/rehype-raw-7.0.0.tgz#59d7348fd5dbef3807bbaa1d443efd2dd85ecee4" + integrity sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww== + dependencies: + "@types/hast" "^3.0.0" + hast-util-raw "^9.0.0" + vfile "^6.0.0" + rehype-raw@^6.1.1: version "6.1.1" resolved "https://registry.yarnpkg.com/rehype-raw/-/rehype-raw-6.1.1.tgz#81bbef3793bd7abacc6bf8335879d1b6c868c9d4" @@ -12017,15 +13054,6 @@ rehype-raw@^6.1.1: hast-util-raw "^7.2.0" unified "^10.0.0" -rehype-raw@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/rehype-raw/-/rehype-raw-7.0.0.tgz#59d7348fd5dbef3807bbaa1d443efd2dd85ecee4" - integrity sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww== - dependencies: - "@types/hast" "^3.0.0" - hast-util-raw "^9.0.0" - vfile "^6.0.0" - relateurl@^0.2.7: version "0.2.7" resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9" @@ -12084,6 +13112,18 @@ remark-gfm@^4.0.0: remark-stringify "^11.0.0" unified "^11.0.0" +remark-gfm@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/remark-gfm/-/remark-gfm-4.0.1.tgz#33227b2a74397670d357bf05c098eaf8513f0d6b" + integrity sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg== + dependencies: + "@types/mdast" "^4.0.0" + mdast-util-gfm "^3.0.0" + micromark-extension-gfm "^3.0.0" + remark-parse "^11.0.0" + remark-stringify "^11.0.0" + unified "^11.0.0" + remark-mdx@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/remark-mdx/-/remark-mdx-3.0.1.tgz#8f73dd635c1874e44426e243f72c0977cf60e212" @@ -12304,9 +13344,9 @@ sass-loader@^16.0.2: neo-async "^2.6.2" sass@^1.80.4: - version "1.89.0" - resolved "https://registry.yarnpkg.com/sass/-/sass-1.89.0.tgz#6df72360c5c3ec2a9833c49adafe57b28206752d" - integrity sha512-ld+kQU8YTdGNjOLfRWBzewJpU5cwEv/h5yyqlSeJcj6Yh8U4TDA9UA5FPicqDz/xgRPWRSYIQNiFks21TbA9KQ== + version "1.89.2" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.89.2.tgz#a771716aeae774e2b529f72c0ff2dfd46c9de10e" + integrity sha512-xCmtksBKd/jdJ9Bt9p7nPKiuqrlBMBuuGkQlkhZjjQk3Ty48lv93k5Dq6OPkKt4XwxDJ7tvlfrTa1MPA9bf+QA== dependencies: chokidar "^4.0.0" immutable "^5.0.2" @@ -12994,6 +14034,11 @@ swc-loader@^0.2.6: dependencies: "@swc/counter" "^0.1.3" +tailwind-merge@2.6.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/tailwind-merge/-/tailwind-merge-2.6.0.tgz#ac5fb7e227910c038d458f396b7400d93a3142d5" + integrity sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA== + tailwindcss@^3.2.4: version "3.4.4" resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.4.4.tgz#351d932273e6abfa75ce7d226b5bf3a6cb257c05" @@ -13081,9 +14126,9 @@ terser@^5.10.0, terser@^5.15.1, terser@^5.26.0: source-map-support "~0.5.20" terser@^5.31.1: - version "5.40.0" - resolved "https://registry.yarnpkg.com/terser/-/terser-5.40.0.tgz#839a80db42bfee8340085f44ea99b5cba36c55c8" - integrity sha512-cfeKl/jjwSR5ar7d0FGmave9hFGJT8obyo0z+CrQOylLDbk7X81nPU6vq9VORa5jU30SkDnT2FXjLbR8HLP+xA== + version "5.43.1" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.43.1.tgz#88387f4f9794ff1a29e7ad61fb2932e25b4fdb6d" + integrity sha512-+6erLbBm0+LROX2sPXlUYx/ux5PyE9K/a92Wrt6oA+WDAoFTdpHE5tCYCI5PNzq2y8df4rA+QgHLJuR4jNymsg== dependencies: "@jridgewell/source-map" "^0.3.3" acorn "^8.14.0" @@ -13130,9 +14175,9 @@ tinyexec@^1.0.1: integrity sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw== tinypool@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/tinypool/-/tinypool-1.0.2.tgz#706193cc532f4c100f66aa00b01c42173d9051b2" - integrity sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA== + version "1.1.1" + resolved "https://registry.yarnpkg.com/tinypool/-/tinypool-1.1.1.tgz#059f2d042bd37567fbc017d3d426bdd2a2612591" + integrity sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg== to-fast-properties@^2.0.0: version "2.0.0" @@ -13191,7 +14236,12 @@ ts-interface-checker@^0.1.9: resolved "https://registry.yarnpkg.com/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz#784fd3d679722bc103b1b4b8030bcddb5db2a699" integrity sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA== -tslib@^2.0.3, tslib@^2.6.0: +tslib@^2.0.0, tslib@^2.6.2: + version "2.8.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" + integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== + +tslib@^2.0.3, tslib@^2.1.0, tslib@^2.6.0: version "2.6.3" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.3.tgz#0438f810ad7a9edcde7a241c3d80db693c8cbfe0" integrity sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ== @@ -13486,11 +14536,48 @@ url@^0.11.1: punycode "^1.4.1" qs "^6.12.3" +use-callback-ref@^1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/use-callback-ref/-/use-callback-ref-1.3.3.tgz#98d9fab067075841c5b2c6852090d5d0feabe2bf" + integrity sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg== + dependencies: + tslib "^2.0.0" + +use-composed-ref@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/use-composed-ref/-/use-composed-ref-1.4.0.tgz#09e023bf798d005286ad85cd20674bdf5770653b" + integrity sha512-djviaxuOOh7wkj0paeO1Q/4wMZ8Zrnag5H6yBvzN7AKKe8beOaED9SF5/ByLqsku8NP4zQqsvM2u3ew/tJK8/w== + use-editable@^2.3.3: version "2.3.3" resolved "https://registry.yarnpkg.com/use-editable/-/use-editable-2.3.3.tgz#a292fe9ba4c291cd28d1cc2728c75a5fc8d9a33f" integrity sha512-7wVD2JbfAFJ3DK0vITvXBdpd9JAz5BcKAAolsnLBuBn6UDDwBGuCIAGvR3yA2BNKm578vAMVHFCWaOcA+BhhiA== +use-isomorphic-layout-effect@^1.1.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.2.0.tgz#afb292eb284c39219e8cb8d3d62d71999361a21d" + integrity sha512-q6ayo8DWoPZT0VdG4u3D3uxcgONP3Mevx2i2b0434cwWBoL+aelL1DzkXI6w3PhTZzUeR2kaVlZn70iCiseP6w== + +use-latest@^1.2.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/use-latest/-/use-latest-1.3.0.tgz#549b9b0d4c1761862072f0899c6f096eb379137a" + integrity sha512-mhg3xdm9NaM8q+gLT8KryJPnRFOz1/5XPBhmDEVZK1webPzDjrPk7f/mbpeLqTgB9msytYWANxgALOCJKnLvcQ== + dependencies: + use-isomorphic-layout-effect "^1.1.1" + +use-sidecar@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/use-sidecar/-/use-sidecar-1.1.3.tgz#10e7fd897d130b896e2c546c63a5e8233d00efdb" + integrity sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ== + dependencies: + detect-node-es "^1.1.0" + tslib "^2.0.0" + +use-sync-external-store@^1.4.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz#55122e2a3edd2a6c106174c27485e0fd59bcfca0" + integrity sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A== + util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" @@ -13699,6 +14786,11 @@ web-namespaces@^2.0.0: resolved "https://registry.yarnpkg.com/web-namespaces/-/web-namespaces-2.0.1.tgz#1010ff7c650eccb2592cebeeaf9a1b253fd40692" integrity sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ== +web-streams-polyfill@4.0.0-beta.3: + version "4.0.0-beta.3" + resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz#2898486b74f5156095e473efe989dcf185047a38" + integrity sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug== + webidl-conversions@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"