diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 6796658f6f..4100347d6d 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -50,7 +50,6 @@ jobs: with: working-directory: e2e browser: ${{ matrix.browser }} - command: npm run e2e config-file: cypress.config.ts - uses: actions/upload-artifact@v4 diff --git a/.gitignore b/.gitignore index 4a8a762ef0..c08cbae77f 100644 --- a/.gitignore +++ b/.gitignore @@ -31,6 +31,7 @@ sandbox.go google-credentials key.json .keys/* +load-test/.keys # dumps .backups diff --git a/cmd/defaults.yaml b/cmd/defaults.yaml index 0f56ddb8ba..76f2fb7fbb 100644 --- a/cmd/defaults.yaml +++ b/cmd/defaults.yaml @@ -339,6 +339,9 @@ OIDC: AuthMethodPrivateKeyJWT: true # ZITADEL_OIDC_AUTHMETHODPRIVATEKEYJWT GrantTypeRefreshToken: true # ZITADEL_OIDC_GRANTTYPEREFRESHTOKEN RequestObjectSupported: true # ZITADEL_OIDC_REQUESTOBJECTSUPPORTED + + # Deprecated: The signing algorithm is determined by the generated keys. + # Use the web keys resource to generate keys with different algorithms. SigningKeyAlgorithm: RS256 # ZITADEL_OIDC_SIGNINGKEYALGORITHM # Sets the default values for lifetime and expiration for OIDC # This default can be overwritten in the default instance configuration and for each instance during runtime @@ -349,10 +352,10 @@ OIDC: DefaultRefreshTokenIdleExpiration: 720h # ZITADEL_OIDC_DEFAULTREFRESHTOKENIDLEEXPIRATION # 2160h are 90 days, three months DefaultRefreshTokenExpiration: 2160h # ZITADEL_OIDC_DEFAULTREFRESHTOKENEXPIRATION - Cache: - MaxAge: 12h # ZITADEL_OIDC_CACHE_MAXAGE - # 168h is 7 days, one week - SharedMaxAge: 168h # ZITADEL_OIDC_CACHE_SHAREDMAXAGE + + # HTTP Cache-Control max-age header value to set on the jwks endpoint. + # Only used when the web keys feature is enabled. 0 sets a no-store value. + JWKSCacheControlMaxAge: 5m # ZITADEL_OIDC_JWKSCACHECONTROLMAXAGE CustomEndpoints: Auth: Path: /oauth/v2/authorize # ZITADEL_OIDC_CUSTOMENDPOINTS_AUTH_PATH diff --git a/cmd/start/start.go b/cmd/start/start.go index 0ecff76a9b..47bc33ca42 100644 --- a/cmd/start/start.go +++ b/cmd/start/start.go @@ -45,13 +45,14 @@ import ( org_v2 "github.com/zitadel/zitadel/internal/api/grpc/org/v2" org_v2beta "github.com/zitadel/zitadel/internal/api/grpc/org/v2beta" action_v3_alpha "github.com/zitadel/zitadel/internal/api/grpc/resources/action/v3alpha" + user_v3_alpha "github.com/zitadel/zitadel/internal/api/grpc/resources/user/v3alpha" + userschema_v3_alpha "github.com/zitadel/zitadel/internal/api/grpc/resources/userschema/v3alpha" "github.com/zitadel/zitadel/internal/api/grpc/resources/webkey/v3" session_v2 "github.com/zitadel/zitadel/internal/api/grpc/session/v2" session_v2beta "github.com/zitadel/zitadel/internal/api/grpc/session/v2beta" settings_v2 "github.com/zitadel/zitadel/internal/api/grpc/settings/v2" settings_v2beta "github.com/zitadel/zitadel/internal/api/grpc/settings/v2beta" "github.com/zitadel/zitadel/internal/api/grpc/system" - user_schema_v3_alpha "github.com/zitadel/zitadel/internal/api/grpc/user/schema/v3alpha" user_v2 "github.com/zitadel/zitadel/internal/api/grpc/user/v2" user_v2beta "github.com/zitadel/zitadel/internal/api/grpc/user/v2beta" http_util "github.com/zitadel/zitadel/internal/api/http" @@ -444,7 +445,10 @@ func startAPIs( if err := apis.RegisterService(ctx, action_v3_alpha.CreateServer(config.SystemDefaults, commands, queries, domain.AllFunctions, apis.ListGrpcMethods, apis.ListGrpcServices)); err != nil { return nil, err } - if err := apis.RegisterService(ctx, user_schema_v3_alpha.CreateServer(commands, queries)); err != nil { + if err := apis.RegisterService(ctx, userschema_v3_alpha.CreateServer(config.SystemDefaults, commands, queries)); err != nil { + return nil, err + } + if err := apis.RegisterService(ctx, user_v3_alpha.CreateServer(commands, keys.User)); err != nil { return nil, err } if err := apis.RegisterService(ctx, webkey.CreateServer(commands, queries)); err != nil { diff --git a/console/package.json b/console/package.json index 524ebe833f..1631a206d6 100644 --- a/console/package.json +++ b/console/package.json @@ -55,21 +55,21 @@ }, "devDependencies": { "@angular-devkit/build-angular": "^16.2.2", - "@angular-eslint/builder": "16.2.0", - "@angular-eslint/eslint-plugin": "16.2.0", - "@angular-eslint/eslint-plugin-template": "16.2.0", + "@angular-eslint/builder": "18.3.0", + "@angular-eslint/eslint-plugin": "18.0.0", + "@angular-eslint/eslint-plugin-template": "18.0.0", "@angular-eslint/schematics": "16.2.0", - "@angular-eslint/template-parser": "16.2.0", + "@angular-eslint/template-parser": "18.3.0", "@angular/cli": "^16.2.14", "@angular/compiler-cli": "^16.2.5", - "@angular/language-service": "^16.2.5", - "@bufbuild/buf": "^1.34.0", + "@angular/language-service": "^18.2.2", + "@bufbuild/buf": "^1.39.0", "@types/file-saver": "^2.0.7", "@types/google-protobuf": "^3.15.3", "@types/jasmine": "~5.1.4", "@types/jasminewd2": "~2.0.13", "@types/jsonwebtoken": "^9.0.6", - "@types/node": "^20.7.0", + "@types/node": "^22.5.2", "@types/opentype.js": "^1.3.8", "@types/qrcode": "^1.5.2", "@types/uuid": "^10.0.0", @@ -85,7 +85,7 @@ "karma-jasmine": "^5.1.0", "karma-jasmine-html-reporter": "^2.1.0", "prettier": "^3.1.1", - "prettier-plugin-organize-imports": "^3.2.4", + "prettier-plugin-organize-imports": "^4.0.0", "protractor": "~7.0.0", "typescript": "^5.1.6" } diff --git a/console/src/app/modules/header/header.component.html b/console/src/app/modules/header/header.component.html index a8c438ae4d..2a167731ec 100644 --- a/console/src/app/modules/header/header.component.html +++ b/console/src/app/modules/header/header.component.html @@ -168,9 +168,11 @@ - - {{ customLinkText }} - + + + {{ pP.customLinkText }} + + {{ 'MENU.DOCUMENTATION' | translate }} diff --git a/console/src/app/modules/header/header.component.ts b/console/src/app/modules/header/header.component.ts index 720d40cb98..457e2bbe51 100644 --- a/console/src/app/modules/header/header.component.ts +++ b/console/src/app/modules/header/header.component.ts @@ -9,7 +9,6 @@ import { BreadcrumbService, BreadcrumbType } from 'src/app/services/breadcrumb.s import { GrpcAuthService } from 'src/app/services/grpc-auth.service'; import { ManagementService } from 'src/app/services/mgmt.service'; import { ActionKeysType } from '../action-keys/action-keys.component'; -import { GetPrivacyPolicyResponse } from 'src/app/proto/generated/zitadel/management_pb'; @Component({ selector: 'cnsl-header', @@ -32,8 +31,6 @@ export class HeaderComponent implements OnDestroy { public BreadcrumbType: any = BreadcrumbType; public ActionKeysType: any = ActionKeysType; public docsLink = 'https://zitadel.com/docs'; - public customLink = ''; - public customLinkText = ''; public positions: ConnectedPosition[] = [ new ConnectionPositionPair({ originX: 'start', originY: 'bottom' }, { overlayX: 'start', overlayY: 'top' }, 0, 10), @@ -50,25 +47,7 @@ export class HeaderComponent implements OnDestroy { public mgmtService: ManagementService, public breadcrumbService: BreadcrumbService, public router: Router, - ) { - this.loadData(); - } - - public async loadData(): Promise { - const getData = (): Promise => { - return this.mgmtService.getPrivacyPolicy(); - }; - - getData() - .then((resp) => { - if (resp.policy) { - this.docsLink = resp.policy.docsLink; - this.customLink = resp.policy.customLink; - this.customLinkText = resp.policy.customLinkText; - } - }) - .catch(() => {}); - } + ) {} public ngOnDestroy() { this.destroy$.next(); diff --git a/console/src/app/modules/policies/login-texts/helper.ts b/console/src/app/modules/policies/login-texts/helper.ts index 355b267061..b0016955f2 100644 --- a/console/src/app/modules/policies/login-texts/helper.ts +++ b/console/src/app/modules/policies/login-texts/helper.ts @@ -15,7 +15,6 @@ import { InitPasswordDoneScreenText, InitPasswordScreenText, LinkingUserDoneScreenText, - LinkingUserPromptScreenText, LoginScreenText, LogoutDoneScreenText, MFAProvidersText, @@ -377,12 +376,5 @@ export function mapRequestValues(map: Partial, req: Req): Req { r34.setUsernameLabel(map.externalRegistrationUserOverviewText?.usernameLabel ?? ''); req.setExternalRegistrationUserOverviewText(r34); - const r35 = new LinkingUserPromptScreenText(); - r35.setTitle(map.linkingUserPromptText?.title ?? ''); - r35.setDescription(map.linkingUserPromptText?.description ?? ''); - r35.setLinkButtonText(map.linkingUserPromptText?.linkButtonText ?? ''); - r35.setOtherButtonText(map.linkingUserPromptText?.otherButtonText ?? ''); - req.setLinkingUserPromptText(r35); - return req; } diff --git a/console/src/app/modules/policies/login-texts/login-texts.component.ts b/console/src/app/modules/policies/login-texts/login-texts.component.ts index 695e757f52..b0a846cb65 100644 --- a/console/src/app/modules/policies/login-texts/login-texts.component.ts +++ b/console/src/app/modules/policies/login-texts/login-texts.component.ts @@ -41,7 +41,6 @@ const KeyNamesArray = [ 'initPasswordText', 'initializeDoneText', 'initializeUserText', - 'linkingUserPromptText', 'linkingUserDoneText', 'loginText', 'logoutText', diff --git a/console/src/app/modules/policies/notification-sms-provider/notification-sms-provider.component.html b/console/src/app/modules/policies/notification-sms-provider/notification-sms-provider.component.html index b8a46bb342..228b343a7c 100644 --- a/console/src/app/modules/policies/notification-sms-provider/notification-sms-provider.component.html +++ b/console/src/app/modules/policies/notification-sms-provider/notification-sms-provider.component.html @@ -5,7 +5,7 @@ -
+

Twilio

diff --git a/console/src/app/modules/settings-list/settings-list.component.html b/console/src/app/modules/settings-list/settings-list.component.html index f60427a300..0392a03849 100644 --- a/console/src/app/modules/settings-list/settings-list.component.html +++ b/console/src/app/modules/settings-list/settings-list.component.html @@ -1,4 +1,5 @@ l.id === changes['selectedId'].currentValue) ? changes['selectedId'].currentValue : ''; - } else { + } + } + + ngOnInit(): void { + if (!this.currentSetting) { this.currentSetting = this.settingsList ? this.settingsList[0].id : ''; } } diff --git a/console/src/app/modules/sidenav/sidenav.component.html b/console/src/app/modules/sidenav/sidenav.component.html index 9f37a28056..d12a3e1c24 100644 --- a/console/src/app/modules/sidenav/sidenav.component.html +++ b/console/src/app/modules/sidenav/sidenav.component.html @@ -5,8 +5,8 @@

{{ description }}

diff --git a/console/src/app/modules/sidenav/sidenav.component.ts b/console/src/app/modules/sidenav/sidenav.component.ts index b5d072e5fc..dc7948a256 100644 --- a/console/src/app/modules/sidenav/sidenav.component.ts +++ b/console/src/app/modules/sidenav/sidenav.component.ts @@ -1,4 +1,4 @@ -import { Component, Input, OnInit } from '@angular/core'; +import { Component, forwardRef, Input, OnInit } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { ActivatedRoute, Router } from '@angular/router'; @@ -19,9 +19,9 @@ export interface SidenavSetting { selector: 'cnsl-sidenav', templateUrl: './sidenav.component.html', styleUrls: ['./sidenav.component.scss'], - providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: SidenavComponent, multi: true }], + providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => SidenavComponent), multi: true }], }) -export class SidenavComponent implements ControlValueAccessor, OnInit { +export class SidenavComponent implements ControlValueAccessor { @Input() public title: string = ''; @Input() public description: string = ''; @Input() public indented: boolean = false; @@ -35,12 +35,6 @@ export class SidenavComponent implements ControlValueAccessor, OnInit { private route: ActivatedRoute, ) {} - ngOnInit(): void { - if (!this.value) { - this.value = this.settingsList[0].id; - } - } - private onChange = (current: string | undefined) => {}; private onTouch = (current: string | undefined) => {}; @@ -51,7 +45,7 @@ export class SidenavComponent implements ControlValueAccessor, OnInit { set value(setting: string | undefined) { this.currentSetting = setting; - if (setting || setting === undefined) { + if (setting || setting === undefined || setting === '') { this.onChange(setting); this.onTouch(setting); } diff --git a/console/src/app/pages/projects/owned-projects/owned-project-detail/owned-project-detail.component.ts b/console/src/app/pages/projects/owned-projects/owned-project-detail/owned-project-detail.component.ts index fa0081bad0..021b0dc2b0 100644 --- a/console/src/app/pages/projects/owned-projects/owned-project-detail/owned-project-detail.component.ts +++ b/console/src/app/pages/projects/owned-projects/owned-project-detail/owned-project-detail.component.ts @@ -61,7 +61,7 @@ export class OwnedProjectDetailComponent implements OnInit { public refreshChanges$: EventEmitter = new EventEmitter(); public settingsList: SidenavSetting[] = [GENERAL, ROLES, PROJECTGRANTS, GRANTS]; - public currentSetting: string | undefined = ''; + public currentSetting: string = ''; constructor( public translate: TranslateService, private route: ActivatedRoute, @@ -72,12 +72,11 @@ export class OwnedProjectDetailComponent implements OnInit { private router: Router, private breadcrumbService: BreadcrumbService, ) { + this.currentSetting = 'general'; route.queryParams.pipe(take(1)).subscribe((params: Params) => { const { id } = params; if (id) { this.currentSetting = id; - } else { - this.currentSetting = 'general'; } }); } diff --git a/console/src/assets/i18n/bg.json b/console/src/assets/i18n/bg.json index e8b8ade072..3548608b5b 100644 --- a/console/src/assets/i18n/bg.json +++ b/console/src/assets/i18n/bg.json @@ -1627,7 +1627,6 @@ "initPasswordText": "Инициализиране на парола", "initializeDoneText": "Инициализирането на потребителя е готово", "initializeUserText": "Инициализирайте потребителя", - "linkingUserPromptText": "Потребителският промпт за свързване", "linkingUserDoneText": "Свързването на потребителя е готово", "loginText": "Влизам", "logoutText": "Излез от профила си", diff --git a/console/src/assets/i18n/cs.json b/console/src/assets/i18n/cs.json index 8cdbc69e64..18d9e9f274 100644 --- a/console/src/assets/i18n/cs.json +++ b/console/src/assets/i18n/cs.json @@ -1628,7 +1628,6 @@ "initPasswordText": "Inicializace hesla", "initializeDoneText": "Inicializace uživatele dokončena", "initializeUserText": "Inicializace uživatele", - "linkingUserPromptText": "Uživatelský propojovací text", "linkingUserDoneText": "Propojení uživatele dokončeno", "loginText": "Přihlášení", "logoutText": "Odhlášení", diff --git a/console/src/assets/i18n/de.json b/console/src/assets/i18n/de.json index 2ffdb74e8f..7d86f1a611 100644 --- a/console/src/assets/i18n/de.json +++ b/console/src/assets/i18n/de.json @@ -1628,7 +1628,6 @@ "initPasswordText": "Passwort Initialisierung", "initializeDoneText": "Benutzereinrichtung erfolgreich", "initializeUserText": "Benutzereinrichtung", - "linkingUserPromptText": "Aufforderung zur Benutzerverlinkung", "linkingUserDoneText": "Benutzerverlinkung erfolgreich", "loginText": "Anmelden", "logoutText": "Abmelden", diff --git a/console/src/assets/i18n/en.json b/console/src/assets/i18n/en.json index fefbb5bfb5..2c4efd5fd6 100644 --- a/console/src/assets/i18n/en.json +++ b/console/src/assets/i18n/en.json @@ -1628,7 +1628,6 @@ "initPasswordText": "Initialize password", "initializeDoneText": "Initialize user done", "initializeUserText": "Initialize user", - "linkingUserPromptText": "Linking user prompt", "linkingUserDoneText": "Linking user done", "loginText": "Login", "logoutText": "Logout", diff --git a/console/src/assets/i18n/es.json b/console/src/assets/i18n/es.json index 61867dbc76..5e6d090eb7 100644 --- a/console/src/assets/i18n/es.json +++ b/console/src/assets/i18n/es.json @@ -1629,7 +1629,6 @@ "initPasswordText": "Inicializar contraseña", "initializeDoneText": "Inicializar usuario, hecho", "initializeUserText": "Inicializar usuario", - "linkingUserPromptText": "Mensaje de enlace de usuario", "linkingUserDoneText": "Vinculación de usuario, hecho", "loginText": "Iniciar sesión", "logoutText": "Cerrar sesión", diff --git a/console/src/assets/i18n/fr.json b/console/src/assets/i18n/fr.json index fb250f06ca..ecd76cc51b 100644 --- a/console/src/assets/i18n/fr.json +++ b/console/src/assets/i18n/fr.json @@ -1628,7 +1628,6 @@ "initPasswordText": "Initialiser le mot de passe", "initializeDoneText": "Initialiser l'utilisateur terminé", "initializeUserText": "Initialiser l'utilisateur", - "linkingUserPromptText": "Message de liaison utilisateur", "linkingUserDoneText": "Lier l'utilisateur fait", "loginText": "Connexion", "logoutText": "Déconnexion", diff --git a/console/src/assets/i18n/it.json b/console/src/assets/i18n/it.json index 96206462c0..2e1d5b8edc 100644 --- a/console/src/assets/i18n/it.json +++ b/console/src/assets/i18n/it.json @@ -1628,7 +1628,6 @@ "initPasswordText": "Inizializzazione della password", "initializeDoneText": "Inizializzazione utente finita", "initializeUserText": "Inizializzazione utente", - "linkingUserPromptText": "Testo di promemoria per collegare l'utente", "linkingUserDoneText": "Collegamento dell'utente finito", "loginText": "Accesso", "logoutText": "Logout", diff --git a/console/src/assets/i18n/ja.json b/console/src/assets/i18n/ja.json index aa89b52a8b..523a553a22 100644 --- a/console/src/assets/i18n/ja.json +++ b/console/src/assets/i18n/ja.json @@ -1624,7 +1624,6 @@ "initPasswordText": "パスワードを初期化する", "initializeDoneText": "ユーザーの初期化が完了しました", "initializeUserText": "ユーザーを初期化する", - "linkingUserPromptText": "ユーザーのリンクプロンプト", "linkingUserDoneText": "ユーザーのリンクが完了しました", "loginText": "ログイン", "logoutText": "ログアウト", diff --git a/console/src/assets/i18n/mk.json b/console/src/assets/i18n/mk.json index 847e221267..2f925e40c4 100644 --- a/console/src/assets/i18n/mk.json +++ b/console/src/assets/i18n/mk.json @@ -1629,7 +1629,6 @@ "initPasswordText": "Иницијализација на лозинка", "initializeDoneText": "Иницијализацијата на корисникот е завршена", "initializeUserText": "Иницијализација на корисник", - "linkingUserPromptText": "Поврзување на кориснички промпт", "linkingUserDoneText": "Поврзувањето на корисникот е завршено", "loginText": "Најава", "logoutText": "Одјава", diff --git a/console/src/assets/i18n/nl.json b/console/src/assets/i18n/nl.json index 9e494b3dc1..2832d43211 100644 --- a/console/src/assets/i18n/nl.json +++ b/console/src/assets/i18n/nl.json @@ -1628,7 +1628,6 @@ "initPasswordText": "Initialiseer wachtwoord", "initializeDoneText": "Gebruiker initialisatie voltooid", "initializeUserText": "Initialiseer gebruiker", - "linkingUserPromptText": "Gebruiker koppelingsprompt", "linkingUserDoneText": "Gebruiker koppeling voltooid", "loginText": "Login", "logoutText": "Uitloggen", diff --git a/console/src/assets/i18n/pl.json b/console/src/assets/i18n/pl.json index 3caf7f39c1..70f0b18d70 100644 --- a/console/src/assets/i18n/pl.json +++ b/console/src/assets/i18n/pl.json @@ -1627,7 +1627,6 @@ "initPasswordText": "Inicjalizacja hasła", "initializeDoneText": "Inicjalizacja użytkownika zakończona", "initializeUserText": "Inicjalizacja użytkownika", - "linkingUserPromptText": "Komunikat o łączeniu użytkowników", "linkingUserDoneText": "Łączenie użytkownika zakończone", "loginText": "Zaloguj się", "logoutText": "Wyloguj się", diff --git a/console/src/assets/i18n/pt.json b/console/src/assets/i18n/pt.json index c40f635357..0010158ed3 100644 --- a/console/src/assets/i18n/pt.json +++ b/console/src/assets/i18n/pt.json @@ -1629,7 +1629,6 @@ "initPasswordText": "Inicialização de senha", "initializeDoneText": "Inicialização de usuário concluída", "initializeUserText": "Inicializaçãode usuário", - "linkingUserPromptText": "Prompt de usuário para vinculação", "linkingUserDoneText": "Vinculação de usuário concluída", "loginText": "Login", "logoutText": "Logout", diff --git a/console/src/assets/i18n/ru.json b/console/src/assets/i18n/ru.json index 532aa2ba9d..98a715981b 100644 --- a/console/src/assets/i18n/ru.json +++ b/console/src/assets/i18n/ru.json @@ -1693,7 +1693,6 @@ "initPasswordText": "Инициализировать пароль", "initializeDoneText": "Инициализация пользователя выполнена", "initializeUserText": "Инициализировать пользователя", - "linkingUserPromptText": "Текст приглашения к привязке пользователя", "linkingUserDoneText": "Привязка пользователя выполнена", "loginText": "Вход", "logoutText": "Выход", diff --git a/console/src/assets/i18n/sv.json b/console/src/assets/i18n/sv.json index 13333a4c32..894b87c2cb 100644 --- a/console/src/assets/i18n/sv.json +++ b/console/src/assets/i18n/sv.json @@ -1632,7 +1632,6 @@ "initPasswordText": "Initiera lösenord", "initializeDoneText": "Initiera användare klart", "initializeUserText": "Initiera användare", - "linkingUserPromptText": "Länka användarprompt", "linkingUserDoneText": "Länka användare klart", "loginText": "Inloggning", "logoutText": "Utloggning", diff --git a/console/src/assets/i18n/zh.json b/console/src/assets/i18n/zh.json index d273750ad6..dabf8261f8 100644 --- a/console/src/assets/i18n/zh.json +++ b/console/src/assets/i18n/zh.json @@ -1627,7 +1627,6 @@ "initPasswordText": "初始化密码", "initializeDoneText": "初始化用户完成", "initializeUserText": "初始化用户", - "linkingUserPromptText": "用户链接提示", "linkingUserDoneText": "链接用户完成", "loginText": "登录", "logoutText": "登出", diff --git a/console/yarn.lock b/console/yarn.lock index 9ed63fac2b..eec0cc2438 100644 --- a/console/yarn.lock +++ b/console/yarn.lock @@ -129,19 +129,26 @@ ora "5.4.1" rxjs "7.8.1" -"@angular-eslint/builder@16.2.0": - version "16.2.0" - resolved "https://registry.yarnpkg.com/@angular-eslint/builder/-/builder-16.2.0.tgz#ae5ddc2c658c54918186efba55a6cebb801880c0" - integrity sha512-SZjXOi3YIjuX2CocuRsR2QH6k1ca9lRO6IMm0YIYMmBPFCRP2KFHkL6aQnXM6DSaymQNN2TXfpuvUd45NxhU1w== - dependencies: - "@nx/devkit" "16.5.1" - nx "16.5.1" +"@angular-eslint/builder@18.3.0": + version "18.3.0" + resolved "https://registry.yarnpkg.com/@angular-eslint/builder/-/builder-18.3.0.tgz#e4a62f45c1d2c37572be4018ec2eefd515d1aacc" + integrity sha512-httEQyqyBw3+0CRtAa7muFxHrauRfkEfk/jmrh5fn2Eiu+I53hAqFPgrwVi1V6AP/kj2zbAiWhd5xM3pMJdoRQ== "@angular-eslint/bundled-angular-compiler@16.2.0": version "16.2.0" resolved "https://registry.yarnpkg.com/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-16.2.0.tgz#09d0637d738850a2c6f0523f19632e992f790102" integrity sha512-ct9orDYxkMl2+uvM7UBfgV28Dq57V4dEs+Drh7cD673JIMa6sXbgmd0QEtm8W3cmyK/jcTzmuoufxbH7hOxd6g== +"@angular-eslint/bundled-angular-compiler@18.0.0": + version "18.0.0" + resolved "https://registry.yarnpkg.com/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-18.0.0.tgz#b95769124ccbfed6a313e0b0b56c4c7fd90eef30" + integrity sha512-c5XNfpWN6vfMoZpnLLeras7nUIVI10ofJu3W3s1s1NpCjP67kY84SPYRJIND1LemVewMQ+mhnP4xJnqvJxC1tA== + +"@angular-eslint/bundled-angular-compiler@18.3.0": + version "18.3.0" + resolved "https://registry.yarnpkg.com/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-18.3.0.tgz#086bf5d529e60a864bbcf3d448ccb9544bfd9b86" + integrity sha512-v/59FxUKnMzymVce99gV43huxoqXWMb85aKvzlNvLN+ScDu6ZE4YMiTQNpfapVL2lkxhs0uwB3jH17EYd5TcsA== + "@angular-eslint/eslint-plugin-template@16.2.0": version "16.2.0" resolved "https://registry.yarnpkg.com/@angular-eslint/eslint-plugin-template/-/eslint-plugin-template-16.2.0.tgz#5d1dd0f450020c9bc8d9cbd5fcbf173b15ff3bd3" @@ -154,6 +161,17 @@ aria-query "5.3.0" axobject-query "3.2.1" +"@angular-eslint/eslint-plugin-template@18.0.0": + version "18.0.0" + resolved "https://registry.yarnpkg.com/@angular-eslint/eslint-plugin-template/-/eslint-plugin-template-18.0.0.tgz#d355504af560487b3177fc8ecf62fee292a8e29b" + integrity sha512-KN32zW5eutRLumjJNGM77pZ4dpQe/PlffU2fGGVagHSDRrjaEqBmJ/khecUHjz3+VxYLbVWBM2skfb5jC4Lr2g== + dependencies: + "@angular-eslint/bundled-angular-compiler" "18.0.0" + "@angular-eslint/utils" "18.0.0" + "@typescript-eslint/utils" "8.0.0-alpha.20" + aria-query "5.3.0" + axobject-query "4.0.0" + "@angular-eslint/eslint-plugin@16.2.0": version "16.2.0" resolved "https://registry.yarnpkg.com/@angular-eslint/eslint-plugin/-/eslint-plugin-16.2.0.tgz#2d61d087d208f347c9c472ecd9b0eee1fae1b21b" @@ -162,6 +180,15 @@ "@angular-eslint/utils" "16.2.0" "@typescript-eslint/utils" "5.62.0" +"@angular-eslint/eslint-plugin@18.0.0": + version "18.0.0" + resolved "https://registry.yarnpkg.com/@angular-eslint/eslint-plugin/-/eslint-plugin-18.0.0.tgz#67982243625f66a8fb0141d34df6c44855bd6977" + integrity sha512-XhsIR28HiFOg3qbyjr0ZFBvOeFSXowbriFn8pAuiUjYoLJEtNZzPA1Ih/J0Ky5ZXYwcSJbZRQdNR/q1INQEFqA== + dependencies: + "@angular-eslint/bundled-angular-compiler" "18.0.0" + "@angular-eslint/utils" "18.0.0" + "@typescript-eslint/utils" "8.0.0-alpha.20" + "@angular-eslint/schematics@16.2.0": version "16.2.0" resolved "https://registry.yarnpkg.com/@angular-eslint/schematics/-/schematics-16.2.0.tgz#587321a0813beede1ec7fe50413ca089bb4f6bb7" @@ -175,13 +202,13 @@ strip-json-comments "3.1.1" tmp "0.2.1" -"@angular-eslint/template-parser@16.2.0": - version "16.2.0" - resolved "https://registry.yarnpkg.com/@angular-eslint/template-parser/-/template-parser-16.2.0.tgz#eccd1a2424b001a585107ec4db8eda726bdc9a6d" - integrity sha512-v2jVKTy2wN7iM9nHpBkxLn2wfL8jSl4IlPrXThIqj8No2VHtpLQZPKuXbGPUXQX05VS2Mj5feScQ36ZVGS8Rbw== +"@angular-eslint/template-parser@18.3.0": + version "18.3.0" + resolved "https://registry.yarnpkg.com/@angular-eslint/template-parser/-/template-parser-18.3.0.tgz#c64e7e5a5dba9599d23fb3a0ac2e1aef101eeeec" + integrity sha512-1mUquqcnugI4qsoxcYZKZ6WMi6RPelDcJZg2YqGyuaIuhWmi3ZqJZLErSSpjP60+TbYZu7wM8Kchqa1bwJtEaQ== dependencies: - "@angular-eslint/bundled-angular-compiler" "16.2.0" - eslint-scope "^7.0.0" + "@angular-eslint/bundled-angular-compiler" "18.3.0" + eslint-scope "^8.0.2" "@angular-eslint/utils@16.2.0": version "16.2.0" @@ -191,6 +218,14 @@ "@angular-eslint/bundled-angular-compiler" "16.2.0" "@typescript-eslint/utils" "5.62.0" +"@angular-eslint/utils@18.0.0": + version "18.0.0" + resolved "https://registry.yarnpkg.com/@angular-eslint/utils/-/utils-18.0.0.tgz#1ddedf84d3ff5275387d35b22a974f54f5eb33f2" + integrity sha512-ygOlsC5HrknbI8Ah5pa6tGtrpxB0W4UqzZG9Ii7whoWs7OjkBTIbeNy/qaWv1e45MR2/Ytd5BSWK17w0Poyz8w== + dependencies: + "@angular-eslint/bundled-angular-compiler" "18.0.0" + "@typescript-eslint/utils" "8.0.0-alpha.20" + "@angular/animations@^16.2.5": version "16.2.12" resolved "https://registry.yarnpkg.com/@angular/animations/-/animations-16.2.12.tgz#27744d8176e09e70e0f6d837c3abcfcee843a936" @@ -283,10 +318,10 @@ dependencies: tslib "^2.3.0" -"@angular/language-service@^16.2.5": - version "16.2.12" - resolved "https://registry.yarnpkg.com/@angular/language-service/-/language-service-16.2.12.tgz#e81d9667ec96eac214b0dd54275bdfb835db3f3f" - integrity sha512-sZwB+ZEjChx9EYcqPaS4OnhC/q5RcedZjIdM9mCxuU/MtseURRYRI/8Hnm1RHo9qyc5PmsQpg7p9Vp/5hXLUjw== +"@angular/language-service@^18.2.2": + version "18.2.2" + resolved "https://registry.yarnpkg.com/@angular/language-service/-/language-service-18.2.2.tgz#8a6b3f224871cb4b1dd5d76a43a1c3884d14aa62" + integrity sha512-aROQNQeLf+o+F5OVvE/9BUe/Tpv8pjzmrZlogBbic5cb4IqSNhR4RjxbgIyXBO/6bhLCZwqfmMqRbW2J2xqMkg== "@angular/material-moment-adapter@^16.2.4": version "16.2.14" @@ -1427,47 +1462,47 @@ "@babel/helper-validator-identifier" "^7.24.7" to-fast-properties "^2.0.0" -"@bufbuild/buf-darwin-arm64@1.35.1": - version "1.35.1" - resolved "https://registry.yarnpkg.com/@bufbuild/buf-darwin-arm64/-/buf-darwin-arm64-1.35.1.tgz#7d7567180df771e94cc95267276da7966be7b90a" - integrity sha512-Yy+sk+8sg3LDvMSZLGUIoMCkZajkQSZkdxO96mpqJagKlEYPLGTtakVFCVNX9KgK/sv1bd9sU55iMGXE3+eIYw== +"@bufbuild/buf-darwin-arm64@1.39.0": + version "1.39.0" + resolved "https://registry.yarnpkg.com/@bufbuild/buf-darwin-arm64/-/buf-darwin-arm64-1.39.0.tgz#0ab8453dc7fc7694e5bd39c69d934edc51b81c81" + integrity sha512-Ptl0uAGssLxQTzoZhGwv1FFTbzUfcstIpEwMhN+XrwiuqsSxOg9eq/n3yXoci5VJsHokjDUHnWkR3y+j5P/5KA== -"@bufbuild/buf-darwin-x64@1.35.1": - version "1.35.1" - resolved "https://registry.yarnpkg.com/@bufbuild/buf-darwin-x64/-/buf-darwin-x64-1.35.1.tgz#05b6dc00944aa2150acf67f4c6f1d8592312f0de" - integrity sha512-LcscoNTCHFeb5y9sitw4w6HWZtJ4Ja/MDBCUU9A8/OGHJSESV0JjhbvVHGNOIsKUbPq5p/SVjYA/Ab/wlmmpaA== +"@bufbuild/buf-darwin-x64@1.39.0": + version "1.39.0" + resolved "https://registry.yarnpkg.com/@bufbuild/buf-darwin-x64/-/buf-darwin-x64-1.39.0.tgz#9c9a211c8039b8cb89b45bf44f338edf82d5e506" + integrity sha512-XNCuy9sjQwVJ4NIZqxaTIyzUtlyquSkp/Uuoh5W5thJ3nzZ5RSgvXKF5iXHhZmesrfRGApktwoCx5Am8runsfQ== -"@bufbuild/buf-linux-aarch64@1.35.1": - version "1.35.1" - resolved "https://registry.yarnpkg.com/@bufbuild/buf-linux-aarch64/-/buf-linux-aarch64-1.35.1.tgz#fb26dbe860229759c224a3c91d5e77dab1874113" - integrity sha512-bPeiSURl8WFxCdawtJjAjUOMqknVTw763NLIDcbYSH1/wTiUbM5QeXCORRlHKXtMGM89SYU5AatcY9UhQ+sn9g== +"@bufbuild/buf-linux-aarch64@1.39.0": + version "1.39.0" + resolved "https://registry.yarnpkg.com/@bufbuild/buf-linux-aarch64/-/buf-linux-aarch64-1.39.0.tgz#9778732efbdbbfe02ec821017cc2392ce4a0153f" + integrity sha512-Am+hrw94awp/eY027ROXwRQBuwAzOpQ/4zI4dgmgsyhzeWZ8w1LWC8z2SSr8T2cqd0cm52KxtoWMW+B3b2qzbw== -"@bufbuild/buf-linux-x64@1.35.1": - version "1.35.1" - resolved "https://registry.yarnpkg.com/@bufbuild/buf-linux-x64/-/buf-linux-x64-1.35.1.tgz#552399c5f42dbbef21968771e6364306c4667313" - integrity sha512-n6ziazYjNH9H1JjHiacGi20rIyZuKnsHjF8qWisO8KGajhnS/7tpq0VzYdorqqWyJ1TcnLBWHj+dWYuGay9Nag== +"@bufbuild/buf-linux-x64@1.39.0": + version "1.39.0" + resolved "https://registry.yarnpkg.com/@bufbuild/buf-linux-x64/-/buf-linux-x64-1.39.0.tgz#d7ca62c4f506c60011f5a97ca2e8683aa26693b0" + integrity sha512-JXVkHoMrTvmpseqdoQPJJ6MRV7/vlloYtvXHHACEzVytYjljOYCNoVET/E5gLBco/edeXFMNc40cCi1KgL3rSw== -"@bufbuild/buf-win32-arm64@1.35.1": - version "1.35.1" - resolved "https://registry.yarnpkg.com/@bufbuild/buf-win32-arm64/-/buf-win32-arm64-1.35.1.tgz#efd0a6a2159a135173becfbe362651a4a4e1dd4d" - integrity sha512-3B65+iA1i/LDjJBseEpAvrkEI7VJqrvW39PyYVkIXSHHT917O+n95g74pn38A0XkggN5lEibLEkipBMDUfwMew== +"@bufbuild/buf-win32-arm64@1.39.0": + version "1.39.0" + resolved "https://registry.yarnpkg.com/@bufbuild/buf-win32-arm64/-/buf-win32-arm64-1.39.0.tgz#efdaf1eca30445f04124c6d829a46a676e6b1dc3" + integrity sha512-akdGW02mo04wbLfjNMBQqxC4mPQ/L/vTU8/o79I67GSxyFYt7bKifvYIYhAA39C2gibHyB7ZLmoeRPbaU8wbYA== -"@bufbuild/buf-win32-x64@1.35.1": - version "1.35.1" - resolved "https://registry.yarnpkg.com/@bufbuild/buf-win32-x64/-/buf-win32-x64-1.35.1.tgz#24cd639b4b692233c4ba6004263933b433a2ff13" - integrity sha512-iafrcs+1FMlD+3ZjI1kVBHGOluT6YcoAUETrGMbQjRha6dL5s2Ldr0G7zCKLIT13yEKG5QTyP8z8gVEpk8C8wg== +"@bufbuild/buf-win32-x64@1.39.0": + version "1.39.0" + resolved "https://registry.yarnpkg.com/@bufbuild/buf-win32-x64/-/buf-win32-x64-1.39.0.tgz#09f2b0290818d826847689d6149f8fb0def4ac4b" + integrity sha512-jos08UMg9iUZsGjPrNpLXP+FNk6q6GizO+bjee/GcI0kSijIzXYMg14goQr0TKlvqs/+IRAM5vZIokQBYlAENQ== -"@bufbuild/buf@^1.34.0": - version "1.35.1" - resolved "https://registry.yarnpkg.com/@bufbuild/buf/-/buf-1.35.1.tgz#46a700b94b463919f21313962e539f63448c7d90" - integrity sha512-POtbb4wRhvgCmmClnuaQTpkHL4ukhFItuS/AaD7QDY0kamn4ExNJz4XlHG5jeJODaQ1Wq3f9qn7UIgUr6CUODw== +"@bufbuild/buf@^1.39.0": + version "1.39.0" + resolved "https://registry.yarnpkg.com/@bufbuild/buf/-/buf-1.39.0.tgz#65884f55d072b93122959c92b389c1d7d8ab510b" + integrity sha512-lm7xb9pc7X04rRjCQ69o9byAAZ7/dsUQGoH+iJ9uBSXQWiwQ1Ts8gneBnuUVsAH2vdW73NFBpmNQGE9XtFauVQ== optionalDependencies: - "@bufbuild/buf-darwin-arm64" "1.35.1" - "@bufbuild/buf-darwin-x64" "1.35.1" - "@bufbuild/buf-linux-aarch64" "1.35.1" - "@bufbuild/buf-linux-x64" "1.35.1" - "@bufbuild/buf-win32-arm64" "1.35.1" - "@bufbuild/buf-win32-x64" "1.35.1" + "@bufbuild/buf-darwin-arm64" "1.39.0" + "@bufbuild/buf-darwin-x64" "1.39.0" + "@bufbuild/buf-linux-aarch64" "1.39.0" + "@bufbuild/buf-linux-x64" "1.39.0" + "@bufbuild/buf-win32-arm64" "1.39.0" + "@bufbuild/buf-win32-x64" "1.39.0" "@colors/colors@1.5.0": version "1.5.0" @@ -1712,7 +1747,7 @@ resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz#786c5f41f043b07afb1af37683d7c33668858f6d" integrity sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ== -"@eslint-community/eslint-utils@^4.2.0": +"@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0": version "4.4.0" resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" integrity sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA== @@ -3063,19 +3098,12 @@ dependencies: "@types/node" "*" -"@types/node@*", "@types/node@>=10.0.0", "@types/node@>=13.7.0": - version "22.0.0" - resolved "https://registry.yarnpkg.com/@types/node/-/node-22.0.0.tgz#04862a2a71e62264426083abe1e27e87cac05a30" - integrity sha512-VT7KSYudcPOzP5Q0wfbowyNLaVR8QWUdw+088uFWwfvpY6uCWaXpqV6ieLAu9WBcnTa7H4Z5RLK8I5t2FuOcqw== +"@types/node@*", "@types/node@>=10.0.0", "@types/node@>=13.7.0", "@types/node@^22.5.2": + version "22.5.2" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.5.2.tgz#e42344429702e69e28c839a7e16a8262a8086793" + integrity sha512-acJsPTEqYqulZS/Yp/S3GgeE6GZ0qYODUR8aVr/DkhHQ8l9nd4j5x1/ZJy9/gHrRlFMqkO6i0I3E27Alu4jjPg== dependencies: - undici-types "~6.11.1" - -"@types/node@^20.7.0": - version "20.14.13" - resolved "https://registry.yarnpkg.com/@types/node/-/node-20.14.13.tgz#bf4fe8959ae1c43bc284de78bd6c01730933736b" - integrity sha512-+bHoGiZb8UiQ0+WEtmph2IWQCjIqg8MDZMAV+ppRRhUZnquF5mQkP/9vpSwJClEiSM/C7fZZExPzfU0vJTyp8w== - dependencies: - undici-types "~5.26.4" + undici-types "~6.19.2" "@types/normalize-package-data@^2.4.1": version "2.4.4" @@ -3208,6 +3236,14 @@ "@typescript-eslint/types" "5.62.0" "@typescript-eslint/visitor-keys" "5.62.0" +"@typescript-eslint/scope-manager@8.0.0-alpha.20": + version "8.0.0-alpha.20" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.0.0-alpha.20.tgz#2f953a8f62e87d65b7a5d19800f7c996e0fe8b11" + integrity sha512-+Ncj0Q6DT8ZHYNp8h5RndW4Siv52kiPpHEz/i8Sj2rh2y8ZCc5pKSHSslk+eZi0Bdj+/+swNOmDNcL2CrlaEnA== + dependencies: + "@typescript-eslint/types" "8.0.0-alpha.20" + "@typescript-eslint/visitor-keys" "8.0.0-alpha.20" + "@typescript-eslint/type-utils@5.62.0": version "5.62.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz#286f0389c41681376cdad96b309cedd17d70346a" @@ -3223,6 +3259,11 @@ resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.62.0.tgz#258607e60effa309f067608931c3df6fed41fd2f" integrity sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ== +"@typescript-eslint/types@8.0.0-alpha.20": + version "8.0.0-alpha.20" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.0.0-alpha.20.tgz#f6d6ed7789178934fcdc67a0796191580f505730" + integrity sha512-xpU1rMQfnnNZxpHN6YUfr18sGOMcpC9hvt54fupcU6N1qMCagEtkRt1U15x086oJAgAITJGa67454ffAoCxv/w== + "@typescript-eslint/typescript-estree@5.62.0": version "5.62.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz#7d17794b77fabcac615d6a48fb143330d962eb9b" @@ -3236,6 +3277,20 @@ semver "^7.3.7" tsutils "^3.21.0" +"@typescript-eslint/typescript-estree@8.0.0-alpha.20": + version "8.0.0-alpha.20" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.0.0-alpha.20.tgz#f495288215150f64af97896f2c1a8cf44197d09c" + integrity sha512-VQ8Mf8upDCuf0uMTjX/Pdw3gvCZomkG43nuThUuzhK3YFwFmIDTqx0ZWSsYJkVGfll0WrXgIua+rKSP/n6NBWw== + dependencies: + "@typescript-eslint/types" "8.0.0-alpha.20" + "@typescript-eslint/visitor-keys" "8.0.0-alpha.20" + debug "^4.3.4" + globby "^11.1.0" + is-glob "^4.0.3" + minimatch "^9.0.4" + semver "^7.6.0" + ts-api-utils "^1.3.0" + "@typescript-eslint/utils@5.62.0": version "5.62.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.62.0.tgz#141e809c71636e4a75daa39faed2fb5f4b10df86" @@ -3250,6 +3305,16 @@ eslint-scope "^5.1.1" semver "^7.3.7" +"@typescript-eslint/utils@8.0.0-alpha.20": + version "8.0.0-alpha.20" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.0.0-alpha.20.tgz#f8e7b6d282714e9e34e891eab2daf8d9b76db5a3" + integrity sha512-0aMhjDTvIrkGkHqyM0ZByAwR8BV1f2HhKdYyjtxko8S/Ca4PGjOIjub6VoF+bQwCRxEuV8viNUld78rqm9jqLA== + dependencies: + "@eslint-community/eslint-utils" "^4.4.0" + "@typescript-eslint/scope-manager" "8.0.0-alpha.20" + "@typescript-eslint/types" "8.0.0-alpha.20" + "@typescript-eslint/typescript-estree" "8.0.0-alpha.20" + "@typescript-eslint/visitor-keys@5.62.0": version "5.62.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz#2174011917ce582875954ffe2f6912d5931e353e" @@ -3258,6 +3323,14 @@ "@typescript-eslint/types" "5.62.0" eslint-visitor-keys "^3.3.0" +"@typescript-eslint/visitor-keys@8.0.0-alpha.20": + version "8.0.0-alpha.20" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.0.0-alpha.20.tgz#bffce2fa485fd99b071a4a51fec8ed6ad7a8d1a3" + integrity sha512-ej06rfct0kalfJgIR8nTR7dF1mgfF83hkylrYas7IAElHfgw4zx99BUGa6VrnHZ1PkxdJBp5PgcO2FmmlOoaRQ== + dependencies: + "@typescript-eslint/types" "8.0.0-alpha.20" + eslint-visitor-keys "^3.4.3" + "@ungap/structured-clone@^1.2.0": version "1.2.0" resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" @@ -3780,9 +3853,9 @@ aws4@^1.8.0: integrity sha512-3AungXC4I8kKsS9PuS4JH2nc+0bVY/mjgrephHTIi8fpEeGsTHBUJeosp0Wc1myYMElmD0B3Oc4XL/HVJ4PV2g== axios@^1.0.0: - version "1.7.2" - resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.2.tgz#b625db8a7051fbea61c35a3cbb3a1daa7b9c7621" - integrity sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw== + version "1.7.4" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.4.tgz#4c8ded1b43683c8dd362973c393f3ede24052aa2" + integrity sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw== dependencies: follow-redirects "^1.15.6" form-data "^4.0.0" @@ -3802,6 +3875,13 @@ axobject-query@3.2.1: dependencies: dequal "^2.0.3" +axobject-query@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-4.0.0.tgz#04a4c90dce33cc5d606c76d6216e3b250ff70dab" + integrity sha512-+60uv1hiVFhHZeO+Lz0RYzsVHy5Wr1ayX0mwda9KPDVLNJgZ1T9Ny7VmFbLDzxsH0D87I86vgj3gFrjTJUYznw== + dependencies: + dequal "^2.0.3" + babel-loader@9.1.3: version "9.1.3" resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-9.1.3.tgz#3d0e01b4e69760cc694ee306fe16d358aa1c6f9a" @@ -5042,7 +5122,7 @@ eslint-scope@5.1.1, eslint-scope@^5.1.1: esrecurse "^4.3.0" estraverse "^4.1.1" -eslint-scope@^7.0.0, eslint-scope@^7.2.2: +eslint-scope@^7.2.2: version "7.2.2" resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.2.2.tgz#deb4f92563390f32006894af62a22dba1c46423f" integrity sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg== @@ -5050,6 +5130,14 @@ eslint-scope@^7.0.0, eslint-scope@^7.2.2: esrecurse "^4.3.0" estraverse "^5.2.0" +eslint-scope@^8.0.2: + version "8.0.2" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-8.0.2.tgz#5cbb33d4384c9136083a71190d548158fe128f94" + integrity sha512-6E4xmrTw5wtxnLA5wYL3WDfhZ/1bUBGOXV0zQvVRDOtrR8D0p6W7fs3JweNYhwRYeGvd/1CKX2se0/2s7Q/nJA== + dependencies: + esrecurse "^4.3.0" + estraverse "^5.2.0" + eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4.3: version "3.4.3" resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" @@ -6967,9 +7055,9 @@ methods@~1.1.2: integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== micromatch@^4.0.2, micromatch@^4.0.4: - version "4.0.7" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.7.tgz#33e8190d9fe474a9895525f5618eee136d46c2e5" - integrity sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q== + version "4.0.8" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" + integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== dependencies: braces "^3.0.3" picomatch "^2.3.1" @@ -7910,10 +7998,10 @@ prelude-ls@^1.2.1: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== -prettier-plugin-organize-imports@^3.2.4: - version "3.2.4" - resolved "https://registry.yarnpkg.com/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-3.2.4.tgz#77967f69d335e9c8e6e5d224074609309c62845e" - integrity sha512-6m8WBhIp0dfwu0SkgfOxJqh+HpdyfqSSLfKKRZSFbDuEQXDDndb8fTpRWkUrX/uBenkex3MgnVk0J3b3Y5byog== +prettier-plugin-organize-imports@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-4.0.0.tgz#a69acf024ea3c8eb650c81f664693826ca853534" + integrity sha512-vnKSdgv9aOlqKeEFGhf9SCBsTyzDSyScy1k7E0R1Uo4L0cTcOV7c1XQaT7jfXIOc/p08WLBfN2QUQA9zDSZMxA== prettier@^3.1.1: version "3.3.3" @@ -8506,7 +8594,7 @@ semver@^6.3.0, semver@^6.3.1: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== -semver@^7.0.0, semver@^7.1.1, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8, semver@^7.5.3: +semver@^7.0.0, semver@^7.1.1, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8, semver@^7.5.3, semver@^7.6.0: version "7.6.3" resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143" integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== @@ -9157,6 +9245,11 @@ tree-kill@1.2.2: resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc" integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A== +ts-api-utils@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.3.0.tgz#4b490e27129f1e8e686b45cc4ab63714dc60eea1" + integrity sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ== + tsconfig-paths@^4.1.2: version "4.2.0" resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz#ef78e19039133446d244beac0fd6a1632e2d107c" @@ -9254,15 +9347,10 @@ ua-parser-js@^0.7.30: resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.38.tgz#f497d8a4dc1fec6e854e5caa4b2f9913422ef054" integrity sha512-fYmIy7fKTSFAhG3fuPlubeGaMoAd6r0rSnfEsO5nEY55i26KSLt9EH7PLQiiqPUhNqYIJvSkTy1oArIcXAbPbA== -undici-types@~5.26.4: - version "5.26.5" - resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" - integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== - -undici-types@~6.11.1: - version "6.11.1" - resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.11.1.tgz#432ea6e8efd54a48569705a699e62d8f4981b197" - integrity sha512-mIDEX2ek50x0OlRgxryxsenE5XaQD4on5U2inY7RApK3SOJpofyw7uW2AyfMKkhAxXIceo2DeWGVGwyvng1GNQ== +undici-types@~6.19.2: + version "6.19.8" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.19.8.tgz#35111c9d1437ab83a7cdc0abae2f26d88eda0a02" + integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw== unicode-canonical-property-names-ecmascript@^2.0.0: version "2.0.0" diff --git a/docs/docs/apis/openidoauth/endpoints.mdx b/docs/docs/apis/openidoauth/endpoints.mdx index 304ac3c539..ea6b1a081d 100644 --- a/docs/docs/apis/openidoauth/endpoints.mdx +++ b/docs/docs/apis/openidoauth/endpoints.mdx @@ -665,6 +665,8 @@ No parameters are needed apart from the user agent cookie, but you can provide t The `post_logout_redirect_uri` will be checked against the previously registered uris of the client provided by the `azp` claim of the `id_token_hint` or the `client_id` parameter. If both parameters are provided, they must be equal. +If neither an `id_token_hint` nor a `client_id` parameter is provided, the `post_logout_redirect_uri` will be ignored. + ## jwks_uri `{your_domain}/oauth/v2/keys` diff --git a/docs/docs/examples/login/vue.mdx b/docs/docs/examples/login/vue.mdx index fa1a5ee41e..481af212c5 100644 --- a/docs/docs/examples/login/vue.mdx +++ b/docs/docs/examples/login/vue.mdx @@ -85,20 +85,20 @@ https://github.com/zitadel/zitadel-vue/blob/main/src/main.ts The restricted admin view will only be shown if the user is authenticated and has the role "admin" in the apps project in ZITADEL. ```ts reference -https://github.com/zitadel/zitadel-vue/blob/main/src/views/Admin.vue +https://github.com/zitadel/zitadel-vue/blob/main/src/views/AdminView.vue ``` The restricted login view is shown to all authenticated users. It prints all the information it gets from the token and from the user info endpoint. ```ts reference -https://github.com/zitadel/zitadel-vue/blob/main/src/views/Login.vue +https://github.com/zitadel/zitadel-vue/blob/main/src/views/LoginView.vue ``` The public no access view is shown to authenticated users who navigate to a page they don't have access to based on their roles. ```ts reference -https://github.com/zitadel/zitadel-vue/blob/main/src/views/NoAccess.vue +https://github.com/zitadel/zitadel-vue/blob/main/src/views/NoAccessView.vue ``` ### Add protected routes to your new pages as well as a Signout link @@ -126,4 +126,4 @@ Now that you have enabled authentication, you are ready to call add authorizatio To do this, [refer to the API docs](/apis/introduction) or check out [the ZITADEL Console code on GitHub](https://github.com/zitadel/zitadel) which uses gRPC to access data. For more information on how to create an Vue application, you can refer to [Vue](https://vuejs.org/guide/quick-start.html). -If you want to learn more about the libraries wrapped by [@zitadel/vue](https://www.npmjs.com/package/@zitadel/vue), [read the docs for vue-oidc-client](https://github.com/soukoku/vue-oidc-client/wiki/V1-Docs). \ No newline at end of file +If you want to learn more about the libraries wrapped by [@zitadel/vue](https://www.npmjs.com/package/@zitadel/vue), [read the docs for vue-oidc-client](https://github.com/soukoku/vue-oidc-client/wiki/V1-Docs). diff --git a/docs/docs/guides/integrate/onboarding/b2b.mdx b/docs/docs/guides/integrate/onboarding/b2b.mdx index f6528d8ad6..a976da71e3 100644 --- a/docs/docs/guides/integrate/onboarding/b2b.mdx +++ b/docs/docs/guides/integrate/onboarding/b2b.mdx @@ -208,12 +208,12 @@ curl -L -X POST 'https://$CUSTOM-DOMAIN/admin/v1/orgs/_setup' \ Detailed description of [Setup Organization](/docs/apis/resources/admin/admin-service-set-up-org#setup-organization) If you need to add custom data to either the organization or the user you can use the metadata. -Metadata is a key value construct that allows you to store any additional information to the ressources. +Metadata is a key value construct that allows you to store any additional information to the resources. The set organization metadata request allows you to add one key value pair to an organization: [Set Organization Metadata](/docs/apis/resources/mgmt/management-service-set-org-metadata) If you have more than one field, you can use the bulk add request: [Bulk Set Organization Metadata](/docs/apis/resources/mgmt/management-service-bulk-set-org-metadata) -The same requests also exist on the user ressource: +The same requests also exist on the user resource: [Set User Metadata](/docs/apis/resources/mgmt/management-service-set-user-metadata) [Bulk Set User Metadata](/docs/apis/resources/mgmt/management-service-bulk-set-user-metadata) diff --git a/docs/docs/guides/integrate/zitadel-apis/event-api.md b/docs/docs/guides/integrate/zitadel-apis/event-api.md index fcc5649ccc..0aacafed95 100644 --- a/docs/docs/guides/integrate/zitadel-apis/event-api.md +++ b/docs/docs/guides/integrate/zitadel-apis/event-api.md @@ -129,7 +129,7 @@ curl --request POST \ "limit": 1000, "event_types": [ "user.token.added", - "user.refresh.token.added + "user.refresh.token.added" ] }' ``` diff --git a/docs/docs/self-hosting/manage/production.md b/docs/docs/self-hosting/manage/production.md index 51c6a787a3..cdd2a8e965 100644 --- a/docs/docs/self-hosting/manage/production.md +++ b/docs/docs/self-hosting/manage/production.md @@ -54,11 +54,33 @@ Tracing: ZITADEL follows the principles that guide cloud-native and twelve factor applications. Logs are a stream of time-ordered events collected from all running processes. -ZITADEL processes write the following events to the standard output: +[ZITADEL is configurable](#default-zitadel-logging-config) to write the following events to the standard output: -- Runtime Logs: Define the log level and record format [in the Log configuration section](https://github.com/zitadel/zitadel/blob/main/cmd/defaults.yaml#L1-L4) -- Access Logs: Enable logging all HTTP and gRPC responses from the ZITADEL binary [in the LogStore section](https://github.com/zitadel/zitadel/blob/main/cmd/defaults.yaml#L366) -- Actions Exectution Logs: Actions can emit custom logs at different levels. For example, a log record can be emitted each time a user is created or authenticated. If you don't want to have these logs in STDOUT, you can disable this [in the LogStore section](https://github.com/zitadel/zitadel/blob/main/cmd/defaults.yaml#L387) . +- Runtime Logs: Define the log level and record format in the `Log` configuration section. +- Access Logs: Enable logging all HTTP and gRPC responses from the ZITADEL binary by setting `LogStore.Access.Stdout.Enabled` to true. +- Actions Execution Logs: Actions can emit custom logs at different levels. For example, a log record can be emitted each time a user is created or authenticated. If you don't want to have these logs in STDOUT, you can disable this by setting `LogStore.Execution.Stdout.Enabled` to true. + +### Default ZITADEL Logging Config + +```yaml +Log: + Level: info # ZITADEL_LOG_LEVEL + Formatter: + Format: text # ZITADEL_LOG_FORMATTER_FORMAT + +LogStore: + Access: + Stdout: + # If enabled, all access logs are printed to the binary's standard output + Enabled: false # ZITADEL_LOGSTORE_ACCESS_STDOUT_ENABLED + Execution: + Stdout: + # If enabled, all execution logs are printed to the binary's standard output + Enabled: true # ZITADEL_LOGSTORE_EXECUTION_STDOUT_ENABLED + +``` + +### Why ZITADEL does not write logs to files Log file management should not be in each business apps responsibility. Instead, your execution environment should provide tooling for managing logs in a generic way. diff --git a/docs/docs/support/advisory/a10011.md b/docs/docs/support/advisory/a10011.md index 992e4202c2..431fdb762c 100644 --- a/docs/docs/support/advisory/a10011.md +++ b/docs/docs/support/advisory/a10011.md @@ -4,13 +4,13 @@ title: Technical Advisory 10011 ## Date and Version -Version: 2.60.0 +Version: 2.59.0 -Date: TBD +Date: 2024-08-19 ## Description -Version 2.60.0 allows more combinations in the identity provider options. As of now, **automatic creation** and **automatic linking options** were only considered if the corresponding **allowed option** (account creation / linking allowed) was enabled. +Version 2.59.0 allows more combinations in the identity provider options. As of now, **automatic creation** and **automatic linking options** were only considered if the corresponding **allowed option** (account creation / linking allowed) was enabled. Starting with this release, this is no longer needed and allows administrators to address cases, where only an **automatic creation** is allowed, but users themselves should not be allowed to **manually** create new accounts using an identity provider or edit the information during the process. Also, allowing users to only link to the proposed existing account is now possible with an enabled **automatic linking option**, while disabling **account linking allowed**. @@ -18,7 +18,7 @@ Also, allowing users to only link to the proposed existing account is now possib ## Statement This change was tracked in the following PR: -[feat(idp): provide auto only options](https://github.com/zitadel/zitadel/pull/8420), which was released in Version [2.60.0](https://github.com/zitadel/zitadel/releases/tag/v2.60.0) +[feat(idp): provide auto only options](https://github.com/zitadel/zitadel/pull/8420), which was released in Version [2.59.0](https://github.com/zitadel/zitadel/releases/tag/v2.59.0) ## Mitigation diff --git a/docs/docs/support/technical_advisory.mdx b/docs/docs/support/technical_advisory.mdx index a252803fe0..6e5c6ac519 100644 --- a/docs/docs/support/technical_advisory.mdx +++ b/docs/docs/support/technical_advisory.mdx @@ -185,10 +185,10 @@ We understand that these advisories may include breaking changes, and we aim to Identity Provider options: allow "auto" only Breaking Behavior Change - Version 2.60.0 allows more combinations in the identity provider options. Due to this there might be unexpected behavior changes. + Version 2.59.0 allows more combinations in the identity provider options. Due to this there might be unexpected behavior changes. - 2.53.0 - 2024-05-28 + 2.59.0 + 2024-08-19 diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js index a07f6e8c78..0318ad074a 100644 --- a/docs/docusaurus.config.js +++ b/docs/docusaurus.config.js @@ -317,7 +317,7 @@ module.exports = { }, }, user_schema_v3: { - specPath: ".artifacts/openapi/zitadel/user/schema/v3alpha/user_schema_service.swagger.json", + specPath: ".artifacts/openapi/zitadel/resources/userschema/v3alpha/user_schema_service.swagger.json", outputDir: "docs/apis/resources/user_schema_service_v3", sidebarOptions: { groupPathsBy: "tag", @@ -325,7 +325,7 @@ module.exports = { }, }, user_v3: { - specPath: ".artifacts/openapi/zitadel/user/v3alpha/user_service.swagger.json", + specPath: ".artifacts/openapi/zitadel/resources/user/v3alpha/user_service.swagger.json", outputDir: "docs/apis/resources/user_service_v3", sidebarOptions: { groupPathsBy: "tag", diff --git a/docs/yarn.lock b/docs/yarn.lock index 668f590f28..00d8b00c75 100644 --- a/docs/yarn.lock +++ b/docs/yarn.lock @@ -4790,9 +4790,9 @@ elkjs@^0.9.0: integrity sha512-f/ZeWvW/BCXbhGEf1Ujp29EASo/lk1FDnETgNKwJrsVvGZhUWCZyg3xLJjAsxfOmt8KjswHmI5EwCQcPMpOYhQ== elliptic@^6.5.3, elliptic@^6.5.5: - version "6.5.5" - resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.5.tgz#c715e09f78b6923977610d4c2346d6ce22e6dded" - integrity sha512-7EjbcmUm17NQFu4Pmgmq2olYMj8nwMnpcddByChSUjArp8F5DQWcIcpriwO4ZToLNAJig0yiyjswfyGNje/ixw== + version "6.5.7" + resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.7.tgz#8ec4da2cb2939926a1b9a73619d768207e647c8b" + integrity sha512-ESVCtTwiA+XhY3wyh24QqRGBoP3rEdDUl3EDUUo9tft074fi19IrdpH7hLCMMP3CIj7jb3W96rn8lt/BqIlt5Q== dependencies: bn.js "^4.11.9" brorand "^1.1.0" diff --git a/e2e/cypress.config.ts b/e2e/cypress.config.ts index 1e3f8e5bab..16d017c872 100644 --- a/e2e/cypress.config.ts +++ b/e2e/cypress.config.ts @@ -61,6 +61,7 @@ export default defineConfig({ baseUrl: baseUrl(), experimentalRunAllSpecs: true, experimentalOriginDependencies: true, + pageLoadTimeout: 180000, setupNodeEvents(on, config) { startWebhookEventHandler() diff --git a/e2e/cypress/e2e/applications/applications.cy.ts b/e2e/cypress/e2e/applications/applications.cy.ts index ec5932165d..3546fe3e6f 100644 --- a/e2e/cypress/e2e/applications/applications.cy.ts +++ b/e2e/cypress/e2e/applications/applications.cy.ts @@ -26,13 +26,16 @@ describe('applications', () => { it('add web pkce app', () => { cy.get('[data-e2e="app-card-add"]').should('be.visible').click(); - cy.get('[formcontrolname="name"]').focus().type(testPKCEAppName); + cy.get('[formcontrolname="name"]').focus().should('be.enabled').type(testPKCEAppName); cy.get('[for="WEB"]').click(); cy.get('[data-e2e="continue-button-nameandtype"]').click(); cy.get('[for="PKCE"]').should('be.visible').click(); cy.get('[data-e2e="continue-button-authmethod"]').click(); - cy.get('[data-e2e="redirect-uris"] input').focus().type('http://localhost:3000/api/auth/callback/zitadel'); - cy.get('[data-e2e="postlogout-uris"] input').focus().type('http://localhost:3000'); + cy.get('[data-e2e="redirect-uris"] input') + .focus() + .should('be.enabled') + .type('http://localhost:3000/api/auth/callback/zitadel'); + cy.get('[data-e2e="postlogout-uris"] input').focus().should('be.enabled').type('http://localhost:3000'); cy.get('[data-e2e="continue-button-redirecturis"]').click(); cy.get('[data-e2e="create-button"]').click(); cy.get('[id*=overlay]').should('exist'); @@ -56,7 +59,7 @@ describe('applications', () => { it('add device code app', () => { cy.get('[data-e2e="app-card-add"]').should('be.visible').click(); - cy.get('[formcontrolname="name"]').focus().type(testDEVICECODEAppName); + cy.get('[formcontrolname="name"]').focus().should('be.enabled').type(testDEVICECODEAppName); cy.get('[for="N"]').click(); cy.get('[data-e2e="continue-button-nameandtype"]').click(); cy.get('[for="DEVICECODE"]').should('be.visible').click(); diff --git a/e2e/cypress/e2e/humans/humans.cy.ts b/e2e/cypress/e2e/humans/humans.cy.ts index d59dace0ec..a018db60cf 100644 --- a/e2e/cypress/e2e/humans/humans.cy.ts +++ b/e2e/cypress/e2e/humans/humans.cy.ts @@ -28,14 +28,14 @@ describe('humans', () => { it('should add a user', () => { cy.get('[data-e2e="create-user-button"]').should('be.visible').click(); cy.url().should('contain', 'users/create'); - cy.get('[formcontrolname="email"]').type('dummy@dummy.com'); + cy.get('[formcontrolname="email"]').should('be.enabled').type('dummy@dummy.com'); //force needed due to the prefilled username prefix - cy.get('[formcontrolname="userName"]').type(user.addName); - cy.get('[formcontrolname="firstName"]').type('e2ehumanfirstname'); - cy.get('[formcontrolname="lastName"]').type('e2ehumanlastname'); + cy.get('[formcontrolname="userName"]').should('be.enabled').type(user.addName); + cy.get('[formcontrolname="firstName"]').should('be.enabled').type('e2ehumanfirstname'); + cy.get('[formcontrolname="lastName"]').should('be.enabled').type('e2ehumanlastname'); cy.get('mat-select[data-cy="country-calling-code"]').click(); cy.contains('mat-option', 'Switzerland').scrollIntoView().click(); - cy.get('[formcontrolname="phone"]').type('123456789'); + cy.get('[formcontrolname="phone"]').should('be.enabled').type('123456789'); cy.get('[data-e2e="create-button"]').click({ force: true }); cy.shouldConfirmSuccess(); let loginName = user.addName; @@ -58,7 +58,7 @@ describe('humans', () => { it('should delete a human user', () => { const rowSelector = `tr:contains(${user.removeName})`; cy.get(rowSelector).find('[data-e2e="enabled-delete-button"]').click({ force: true }); - cy.get('[data-e2e="confirm-dialog-input"]').focus().type(user.removeName); + cy.get('[data-e2e="confirm-dialog-input"]').focus().should('be.enabled').type(user.removeName); cy.get('[data-e2e="confirm-dialog-button"]').click(); cy.shouldConfirmSuccess(); cy.shouldNotExist({ diff --git a/e2e/cypress/e2e/instance/settings/notifications.cy.ts b/e2e/cypress/e2e/instance/settings/notifications.cy.ts index ca7af80d46..99e68a4a0e 100644 --- a/e2e/cypress/e2e/instance/settings/notifications.cy.ts +++ b/e2e/cypress/e2e/instance/settings/notifications.cy.ts @@ -1,3 +1,10 @@ +import { v4 as uuidv4 } from 'uuid'; + +import { Context } from '../../../support/commands'; +import { ensureOrgExists } from '../../../support/api/orgs'; +import { activateSMTPProvider, ensureSMTPProviderExists } from '../../../support/api/smtp'; +import { ensureSMSProviderDoesntExist, ensureSMSProviderExists } from '../../../support/api/sms'; + const notificationPath = `/instance?id=notifications`; const smtpPath = `/instance?id=smtpprovider`; const smsPath = `/instance?id=smsprovider`; @@ -6,6 +13,11 @@ beforeEach(() => { cy.context().as('ctx'); }); +type SMTPProvider = { + description: string; + rowSelector: string; +}; + describe('instance notifications', () => { describe('notification settings', () => { it(`should show notification settings`, () => { @@ -15,243 +27,195 @@ describe('instance notifications', () => { }); describe('smtp settings', () => { - it(`should show SMTP provider settings`, () => { - cy.visit(smtpPath); - cy.contains('SMTP Provider'); + beforeEach(() => { + const description = `mailgun-${uuidv4()}`; + cy.wrap({ description, rowSelector: `tr:contains('${description}')` }).as('provider'); }); + it(`should add Mailgun SMTP provider settings`, () => { - let rowSelector = `a:contains('Mailgun')`; - cy.visit(smtpPath); - cy.get(rowSelector).click(); - cy.get('[formcontrolname="hostAndPort"]').should('have.value', 'smtp.mailgun.org:587'); - cy.get('[formcontrolname="user"]').clear().type('user@example.com'); - cy.get('[formcontrolname="password"]').clear().type('password'); - cy.get('[data-e2e="continue-to-2nd-form"]').click(); - cy.get('[formcontrolname="senderAddress"]').clear().type('sender1@example.com'); - cy.get('[formcontrolname="senderName"]').clear().type('Test1'); - cy.get('[formcontrolname="replyToAddress"]').clear().type('replyto1@example.com'); - cy.get('[data-e2e="continue-button"]').click(); - cy.get('[data-e2e="create-button"]').click(); - cy.shouldConfirmSuccess(); - cy.get('[data-e2e="close-button"]').click(); - cy.get('tr').contains('mailgun'); - cy.get('tr').contains('smtp.mailgun.org:587'); - cy.get('tr').contains('sender1@example.com'); - }); - it(`should change Mailgun SMTP provider settings`, () => { - let rowSelector = `tr:contains('mailgun')`; - cy.visit(smtpPath); - cy.get(rowSelector).click(); - cy.get('[formcontrolname="hostAndPort"]').should('have.value', 'smtp.mailgun.org:587'); - cy.get('[formcontrolname="user"]').should('have.value', 'user@example.com'); - cy.get('[formcontrolname="user"]').clear().type('change@example.com'); - cy.get('[data-e2e="continue-to-2nd-form"]').click(); - cy.get('[formcontrolname="senderAddress"]').should('have.value', 'sender1@example.com'); - cy.get('[formcontrolname="senderName"]').should('have.value', 'Test1'); - cy.get('[formcontrolname="replyToAddress"]').should('have.value', 'replyto1@example.com'); - cy.get('[formcontrolname="senderAddress"]').clear().type('senderchange1@example.com'); - cy.get('[formcontrolname="senderName"]').clear().type('Change1'); - cy.get('[data-e2e="continue-button"]').click(); - cy.get('[data-e2e="create-button"]').click(); - cy.shouldConfirmSuccess(); - cy.get('[data-e2e="close-button"]').click(); - rowSelector = `tr:contains('mailgun')`; - cy.get(rowSelector).contains('mailgun'); - cy.get(rowSelector).contains('smtp.mailgun.org:587'); - cy.get(rowSelector).contains('senderchange1@example.com'); - }); - it(`should activate Mailgun SMTP provider settings`, () => { - let rowSelector = `tr:contains('smtp.mailgun.org:587')`; - cy.visit(smtpPath); - cy.get(rowSelector).find('[data-e2e="activate-provider-button"]').click({ force: true }); - cy.get('[data-e2e="confirm-dialog-button"]').click(); - cy.shouldConfirmSuccess(); - rowSelector = `tr:contains('smtp.mailgun.org:587')`; - cy.get(rowSelector).find('[data-e2e="active-provider"]'); - cy.get(rowSelector).contains('mailgun'); - cy.get(rowSelector).contains('smtp.mailgun.org:587'); - cy.get(rowSelector).contains('senderchange1@example.com'); - }); - it(`should add Mailjet SMTP provider settings`, () => { - let rowSelector = `a:contains('Mailjet')`; - cy.visit(smtpPath); - cy.get(rowSelector).click(); - cy.get('[formcontrolname="hostAndPort"]').should('have.value', 'in-v3.mailjet.com:587'); - cy.get('[formcontrolname="user"]').clear().type('user@example.com'); - cy.get('[formcontrolname="password"]').clear().type('password'); - cy.get('[data-e2e="continue-to-2nd-form"]').click(); - cy.get('[formcontrolname="senderAddress"]').clear().type('sender2@example.com'); - cy.get('[formcontrolname="senderName"]').clear().type('Test2'); - cy.get('[formcontrolname="replyToAddress"]').clear().type('replyto2@example.com'); - cy.get('[data-e2e="continue-button"]').click(); - cy.get('[data-e2e="create-button"]').click(); - cy.shouldConfirmSuccess(); - cy.get('[data-e2e="close-button"]').click(); - rowSelector = `tr:contains('mailjet')`; - cy.get(rowSelector).contains('mailjet'); - cy.get(rowSelector).contains('in-v3.mailjet.com:587'); - cy.get(rowSelector).contains('sender2@example.com'); - }); - it(`should activate Mailjet SMTP provider settings an disable Mailgun`, () => { - let rowSelector = `tr:contains('in-v3.mailjet.com:587')`; - cy.visit(smtpPath); - cy.get(rowSelector).find('[data-e2e="activate-provider-button"]').click({ force: true }); - cy.get('[data-e2e="confirm-dialog-button"]').click(); - cy.shouldConfirmSuccess(); - cy.get(rowSelector).find('[data-e2e="active-provider"]'); - cy.get(rowSelector).contains('mailjet'); - cy.get(rowSelector).contains('in-v3.mailjet.com:587'); - cy.get(rowSelector).contains('sender2@example.com'); - rowSelector = `tr:contains('mailgun')`; - cy.get(rowSelector).find('[data-e2e="active-provider"]').should('not.exist'); - }); - it(`should deactivate Mailjet SMTP provider`, () => { - let rowSelector = `tr:contains('mailjet')`; - cy.visit(smtpPath); - cy.get(rowSelector).find('[data-e2e="deactivate-provider-button"]').click({ force: true }); - cy.get('[data-e2e="confirm-dialog-button"]').click(); - cy.shouldConfirmSuccess(); - rowSelector = `tr:contains('mailjet')`; - cy.get(rowSelector).find('[data-e2e="active-provider"]').should('not.exist'); - rowSelector = `tr:contains('mailgun')`; - cy.get(rowSelector).find('[data-e2e="active-provider"]').should('not.exist'); - }); - it(`should delete Mailjet SMTP provider`, () => { - let rowSelector = `tr:contains('mailjet')`; - cy.visit(smtpPath); - cy.get(rowSelector).find('[data-e2e="delete-provider-button"]').click({ force: true }); - cy.get('[data-e2e="confirm-dialog-input"]').focus().type('Test2'); - cy.get('[data-e2e="confirm-dialog-button"]').click(); - cy.shouldConfirmSuccess(); - rowSelector = `tr:contains('mailjet')`; - cy.get(rowSelector).should('not.exist'); - }); - it(`should delete Mailgun SMTP provider`, () => { - let rowSelector = `tr:contains('mailgun')`; - cy.visit(smtpPath); - cy.get(rowSelector).find('[data-e2e="delete-provider-button"]').click({ force: true }); - cy.get('[data-e2e="confirm-dialog-input"]').focus().type('Change1'); - cy.get('[data-e2e="confirm-dialog-button"]').click(); - cy.shouldConfirmSuccess(); - rowSelector = `tr:contains('mailgun')`; - cy.get(rowSelector).should('not.exist'); + cy.get('@provider').then((provider) => { + cy.visit(smtpPath); + cy.get(`a:contains('Mailgun')`).click(); + cy.get('[formcontrolname="description"]').should('be.enabled').clear().type(provider.description); + cy.get('[formcontrolname="hostAndPort"]').should('have.value', 'smtp.mailgun.org:587'); + cy.get('[formcontrolname="user"]').should('be.enabled').clear().type('user@example.com'); + cy.get('[formcontrolname="password"]').should('be.enabled').clear().type('password'); + cy.get('[data-e2e="continue-to-2nd-form"]').should('be.enabled').click(); + cy.get('[formcontrolname="senderAddress"]').should('be.enabled').clear().type('sender1@example.com'); + cy.get('[formcontrolname="senderName"]').should('be.enabled').clear().type('Test1'); + cy.get('[formcontrolname="replyToAddress"]').should('be.enabled').clear().type('replyto1@example.com'); + cy.get('[data-e2e="continue-button"]').should('be.enabled').click(); + cy.get('[data-e2e="create-button"]').should('be.enabled').click(); + cy.shouldConfirmSuccess(); + cy.get('[data-e2e="close-button"]').should('be.enabled').click(); + cy.get(provider.rowSelector).contains('smtp.mailgun.org:587'); + cy.get(provider.rowSelector).contains('sender1@example.com'); + }); }); + it(`should add Mailgun SMTP provider settings and activate it using wizard`, () => { - let rowSelector = `a:contains('Mailgun')`; - cy.visit(smtpPath); - cy.get(rowSelector).click(); - cy.get('[formcontrolname="hostAndPort"]').should('have.value', 'smtp.mailgun.org:587'); - cy.get('[formcontrolname="user"]').clear().type('user@example.com'); - cy.get('[formcontrolname="password"]').clear().type('password'); - cy.get('[data-e2e="continue-to-2nd-form"]').click(); - cy.get('[formcontrolname="senderAddress"]').clear().type('sender1@example.com'); - cy.get('[formcontrolname="senderName"]').clear().type('Test1'); - cy.get('[formcontrolname="replyToAddress"]').clear().type('replyto1@example.com'); - cy.get('[data-e2e="continue-button"]').click(); - cy.get('[data-e2e="create-button"]').click(); - cy.shouldConfirmSuccess(); - cy.get('[data-e2e="activate-button"]').click(); - cy.shouldConfirmSuccess(); - cy.get('[data-e2e="close-button"]').click(); - rowSelector = `tr:contains('smtp.mailgun.org:587')`; - cy.get(rowSelector).find('[data-e2e="active-provider"]'); - cy.get(rowSelector).contains('mailgun'); - cy.get(rowSelector).contains('smtp.mailgun.org:587'); - cy.get(rowSelector).contains('sender1@example.com'); + cy.get('@provider').then((provider) => { + cy.visit(smtpPath); + cy.get(`a:contains('Mailgun')`).click(); + cy.get('[formcontrolname="description"]').should('be.enabled').clear().type(provider.description); + cy.get('[formcontrolname="hostAndPort"]').should('have.value', 'smtp.mailgun.org:587'); + cy.get('[formcontrolname="user"]').should('be.enabled').clear().type('user@example.com'); + cy.get('[formcontrolname="password"]').should('be.enabled').clear().type('password'); + cy.get('[data-e2e="continue-to-2nd-form"]').should('be.enabled').click(); + cy.get('[formcontrolname="senderAddress"]').should('be.enabled').clear().type('sender1@example.com'); + cy.get('[formcontrolname="senderName"]').should('be.enabled').clear().type('Test1'); + cy.get('[formcontrolname="replyToAddress"]').should('be.enabled').clear().type('replyto1@example.com'); + cy.get('[data-e2e="continue-button"]').should('be.enabled').click(); + cy.get('[data-e2e="create-button"]').click(); + cy.shouldConfirmSuccess(); + cy.get('[data-e2e="activate-button"]').click(); + cy.shouldConfirmSuccess(); + cy.get('[data-e2e="close-button"]').click(); + cy.get(provider.rowSelector).find('[data-e2e="active-provider"]'); + cy.get(provider.rowSelector).contains('smtp.mailgun.org:587'); + cy.get(provider.rowSelector).contains('sender1@example.com'); + }); }); - it(`should add Mailgun SMTP provider settings and deactivate it using wizard`, () => { - let rowSelector = `tr:contains('mailgun')`; - cy.visit(smtpPath); - cy.get(rowSelector).click(); - cy.get('[data-e2e="continue-to-2nd-form"]').click(); - cy.get('[data-e2e="continue-button"]').click(); - cy.get('[data-e2e="create-button"]').click(); - cy.shouldConfirmSuccess(); - cy.get('[data-e2e="deactivate-button"]').click(); - cy.shouldConfirmSuccess(); - cy.get('[data-e2e="close-button"]').click(); - rowSelector = `tr:contains('mailgun')`; - cy.get(rowSelector).find('[data-e2e="active-provider"]').should('not.exist'); + + describe('with inactive existing', () => { + beforeEach(() => { + cy.get('@ctx').then((ctx) => { + cy.get('@provider').then(({ description }) => { + ensureSMTPProviderExists(ctx.api, description); + }); + }); + cy.visit(smtpPath); + }); + + it(`should change Mailgun SMTP provider settings`, () => { + cy.get('@provider').then(({ rowSelector }) => { + cy.get(rowSelector).click(); + cy.get('[data-e2e="continue-to-2nd-form"]').click(); + cy.get('[formcontrolname="senderAddress"]').should('be.enabled').clear().type('senderchange1@example.com'); + cy.get('[formcontrolname="senderName"]').clear().type('Change1'); + cy.get('[data-e2e="continue-button"]').click(); + cy.get('[data-e2e="create-button"]').click(); + cy.shouldConfirmSuccess(); + cy.get('[data-e2e="close-button"]').click(); + cy.get(rowSelector).contains('senderchange1@example.com'); + }); + }); + it(`should activate Mailgun SMTP provider settings`, () => { + cy.get('@provider').then(({ rowSelector }) => { + cy.get(rowSelector).find('[data-e2e="activate-provider-button"]').click({ force: true }); + cy.get('[data-e2e="confirm-dialog-button"]').click(); + cy.shouldConfirmSuccess(); + cy.get(rowSelector).find('[data-e2e="active-provider"]'); + }); + }); + + it(`should delete Mailgun SMTP provider`, () => { + cy.get('@provider').then(({ rowSelector }) => { + cy.get(rowSelector).find('[data-e2e="delete-provider-button"]').click({ force: true }); + cy.get('[data-e2e="confirm-dialog-input"]').focus().should('be.enabled').type('A Sender'); + cy.get('[data-e2e="confirm-dialog-button"]').click(); + cy.shouldConfirmSuccess(); + cy.get(rowSelector).should('not.exist'); + }); + }); }); - it(`should delete Mailgun SMTP provider`, () => { - let rowSelector = `tr:contains('mailgun')`; - cy.visit(smtpPath); - cy.get(rowSelector).find('[data-e2e="delete-provider-button"]').click({ force: true }); - cy.get('[data-e2e="confirm-dialog-input"]').focus().type('Test1'); - cy.get('[data-e2e="confirm-dialog-button"]').click(); - cy.shouldConfirmSuccess(); - rowSelector = `tr:contains('mailgun')`; - cy.get(rowSelector).should('not.exist'); + describe('with active existing', () => { + beforeEach(() => { + cy.get('@ctx').then((ctx) => { + cy.get('@provider').then(({ description }) => { + ensureSMTPProviderExists(ctx.api, description).then((providerId) => { + activateSMTPProvider(ctx.api, providerId); + }); + }); + }); + cy.pause(); + cy.visit(smtpPath); + }); + + it(`should deactivate an existing Mailgun SMTP provider using wizard`, () => { + cy.get('@provider').then(({ rowSelector }) => { + cy.get(rowSelector).click(); + cy.get('[data-e2e="continue-to-2nd-form"]').click(); + cy.get('[data-e2e="continue-button"]').click(); + cy.get('[data-e2e="create-button"]').click(); + cy.shouldConfirmSuccess(); + cy.get('[data-e2e="deactivate-button"]').click(); + cy.shouldConfirmSuccess(); + cy.get('[data-e2e="close-button"]').click(); + cy.get(rowSelector).find('[data-e2e="active-provider"]').should('not.exist'); + }); + }); }); }); describe('sms settings', () => { - it(`should show SMS provider settings`, () => { - cy.visit(smsPath); - cy.contains('SMS Settings'); + beforeEach(() => { + cy.wrap(`twilio-${uuidv4()}`).as('uniqueSid'); }); - it(`should add SMS provider`, () => { - cy.visit(smsPath); - cy.get('[data-e2e="new-twilio-button"]').click(); - cy.get('[formcontrolname="sid"]').clear().type('test'); - cy.get('[formcontrolname="token"]').clear().type('token'); - cy.get('[formcontrolname="senderNumber"]').clear().type('2312123132'); - cy.get('[data-e2e="save-sms-settings-button"]').click(); - cy.shouldConfirmSuccess(); - cy.get('h4').contains('Twilio'); - cy.get('.state').contains('Inactive'); + describe('without existing', () => { + beforeEach(() => { + cy.get('@ctx').then((ctx) => { + ensureSMSProviderDoesntExist(ctx.api); + }); + }); + + it(`should add SMS provider`, () => { + cy.visit(smsPath); + cy.get('[data-e2e="new-twilio-button"]').click(); + cy.get('[formcontrolname="sid"]').should('be.enabled').clear().type('test'); + cy.get('[formcontrolname="token"]').should('be.enabled').clear().type('token'); + cy.get('[formcontrolname="senderNumber"]').should('be.enabled').clear().type('2312123132'); + cy.get('[data-e2e="save-sms-settings-button"]').click(); + cy.shouldConfirmSuccess(); + cy.get('h4').contains('Twilio'); + cy.get('.state').contains('Inactive'); + }); }); - it(`should activate SMS provider`, () => { - cy.visit(smsPath); - cy.get('h4').contains('Twilio'); - cy.get('.state').contains('Inactive'); - cy.get('[data-e2e="activate-sms-provider-button"]').click(); - cy.shouldConfirmSuccess(); - cy.get('.state').contains('Active'); - }); + describe('with inactive existing', () => { + beforeEach(() => { + cy.get('@ctx').then((ctx) => { + ensureSMSProviderExists(ctx.api); + cy.visit(smsPath); + }); + }); - it(`should edit SMS provider`, () => { - cy.visit(smsPath); - cy.get('h4').contains('Twilio'); - cy.get('.state').contains('Active'); - cy.get('[data-e2e="new-twilio-button"]').click(); - cy.get('[formcontrolname="sid"]').should('have.value', 'test'); - cy.get('[formcontrolname="senderNumber"]').should('have.value', '2312123132'); - cy.get('[formcontrolname="sid"]').clear().type('test2'); - cy.get('[formcontrolname="senderNumber"]').clear().type('6666666666'); - cy.get('[data-e2e="save-sms-settings-button"]').click(); - cy.shouldConfirmSuccess(); - }); + it(`should activate SMS provider`, () => { + cy.get('h4').contains('Twilio'); + cy.get('.state').contains('Inactive'); + cy.get('[data-e2e="activate-sms-provider-button"]').click(); + cy.shouldConfirmSuccess(); + cy.get('.state').contains('Active'); + }); - it(`should contain edited values`, () => { - cy.visit(smsPath); - cy.get('h4').contains('Twilio'); - cy.get('.state').contains('Active'); - cy.get('[data-e2e="new-twilio-button"]').click(); - cy.get('[formcontrolname="sid"]').should('have.value', 'test2'); - cy.get('[formcontrolname="senderNumber"]').should('have.value', '6666666666'); - }); + it(`should edit SMS provider`, () => { + cy.get('h4').contains('Twilio'); + cy.get('[data-e2e="new-twilio-button"]').click(); + cy.get('[formcontrolname="sid"]').should('be.enabled').clear().type('test2'); + cy.get('[formcontrolname="senderNumber"]').should('be.enabled').clear().type('6666666666'); + cy.get('[data-e2e="save-sms-settings-button"]').click(); + cy.shouldConfirmSuccess(); + cy.get('[data-e2e="new-twilio-button"]').click(); + cy.get('[formcontrolname="sid"]').should('have.value', 'test2'); + cy.get('[formcontrolname="senderNumber"]').should('have.value', '6666666666'); + }); - it(`should edit SMS provider token`, () => { - cy.visit(smsPath); - cy.get('h4').contains('Twilio'); - cy.get('.state').contains('Active'); - cy.get('[data-e2e="new-twilio-button"]').click(); - cy.get('[data-e2e="edit-sms-token-button"]').click(); - cy.get('[data-e2e="notification-setting-password"]').clear().type('newsupertoken'); - cy.get('[data-e2e="save-notification-setting-password-button"]').click(); - cy.shouldConfirmSuccess(); - }); + it(`should edit SMS provider token`, () => { + cy.get('h4').contains('Twilio'); + cy.get('[data-e2e="new-twilio-button"]').click(); + cy.get('[data-e2e="edit-sms-token-button"]').click(); + cy.get('[data-e2e="notification-setting-password"]').should('be.enabled').clear().type('newsupertoken'); + cy.get('[data-e2e="save-notification-setting-password-button"]').click(); + cy.shouldConfirmSuccess(); + }); - it(`should remove SMS provider`, () => { - cy.visit(smsPath); - cy.get('h4').contains('Twilio'); - cy.get('.state').contains('Active'); - cy.get('[data-e2e="remove-sms-provider-button"]').click(); - cy.get('[data-e2e="confirm-dialog-button"]').click(); - cy.shouldConfirmSuccess(); + it(`should remove SMS provider`, () => { + cy.get('h4').contains('Twilio'); + cy.get('[data-e2e="remove-sms-provider-button"]').click(); + cy.get('[data-e2e="confirm-dialog-button"]').click(); + cy.shouldConfirmSuccess(); + }); }); }); }); diff --git a/e2e/cypress/e2e/instance/settings/secret-generator.cy.ts b/e2e/cypress/e2e/instance/settings/secret-generator.cy.ts index 8ea2253e84..29316894eb 100644 --- a/e2e/cypress/e2e/instance/settings/secret-generator.cy.ts +++ b/e2e/cypress/e2e/instance/settings/secret-generator.cy.ts @@ -101,7 +101,7 @@ describe('instance secret generators', () => { it(`Initialization Mail should update settings`, () => { cy.visit(secretGeneratorSettingsPath); cy.wait(1000); - cy.get('input[id="length1"]').clear().type('64'); + cy.get('input[id="length1"]').should('be.enabled').clear().type('64'); cy.get('mat-slide-toggle#includeLowerLetters1 button').click(); cy.get('button[id="saveSecretGenerator1"]').click(); cy.wait(1000); @@ -116,7 +116,7 @@ describe('instance secret generators', () => { it(`Email verification should update settings`, () => { cy.visit(secretGeneratorSettingsPath); cy.wait(1000); - cy.get('input[id="length2"]').clear().type('64'); + cy.get('input[id="length2"]').should('be.enabled').clear().type('64'); cy.get('mat-slide-toggle#includeUpperLetters2 button').click(); cy.get('button[id="saveSecretGenerator2"]').click(); cy.wait(1000); @@ -131,7 +131,7 @@ describe('instance secret generators', () => { it(`Phone verification should update settings`, () => { cy.visit(secretGeneratorSettingsPath); cy.wait(1000); - cy.get('input[id="expiry3"]').clear().type('10'); + cy.get('input[id="expiry3"]').should('be.enabled').clear().type('10'); cy.get('mat-slide-toggle#includeSymbols3 button').click(); cy.get('button[id="saveSecretGenerator3"]').click(); cy.wait(1000); @@ -146,7 +146,7 @@ describe('instance secret generators', () => { it(`Password Reset should update settings`, () => { cy.visit(secretGeneratorSettingsPath); cy.wait(1000); - cy.get('input[id="expiry4"]').clear().type('5'); + cy.get('input[id="expiry4"]').should('be.enabled').clear().type('5'); cy.get('mat-slide-toggle#includeDigits4 button').click(); cy.get('button[id="saveSecretGenerator4"]').click(); cy.wait(1000); @@ -161,7 +161,7 @@ describe('instance secret generators', () => { it(`Passwordless Initialization should update settings`, () => { cy.visit(secretGeneratorSettingsPath); cy.wait(1000); - cy.get('input[id="length5"]').clear().type('64'); + cy.get('input[id="length5"]').should('be.enabled').clear().type('64'); cy.get('mat-slide-toggle#includeDigits5 button').click(); cy.get('button[id="saveSecretGenerator5"]').click(); cy.wait(1000); @@ -176,8 +176,8 @@ describe('instance secret generators', () => { it(`App Secret should update settings`, () => { cy.visit(secretGeneratorSettingsPath); cy.wait(1000); - cy.get('input[id="length6"]').clear().type('32'); - cy.get('input[id="expiry6"]').clear().type('120'); + cy.get('input[id="length6"]').should('be.enabled').clear().type('32'); + cy.get('input[id="expiry6"]').should('be.enabled').clear().type('120'); cy.get('mat-slide-toggle#includeUpperLetters6 button').click(); cy.get('button[id="saveSecretGenerator6"]').click(); cy.wait(1000); @@ -192,7 +192,7 @@ describe('instance secret generators', () => { it(`One Time Password (OTP) - SMS should update settings`, () => { cy.visit(secretGeneratorSettingsPath); cy.wait(1000); - cy.get('input[id="expiry7"]').clear().type('120'); + cy.get('input[id="expiry7"]').should('be.enabled').clear().type('120'); cy.get('mat-slide-toggle#includeLowerLetters7 button').click(); cy.get('button[id="saveSecretGenerator7"]').click(); cy.wait(1000); @@ -207,8 +207,8 @@ describe('instance secret generators', () => { it(`One Time Password (OTP) should update settings`, () => { cy.visit(secretGeneratorSettingsPath); cy.wait(1000); - cy.get('input[id="length8"]').clear().type('12'); - cy.get('input[id="expiry8"]').clear().type('90'); + cy.get('input[id="length8"]').should('be.enabled').clear().type('12'); + cy.get('input[id="expiry8"]').should('be.enabled').clear().type('90'); cy.get('mat-slide-toggle#includeDigits8 button').click(); cy.get('mat-slide-toggle#includeSymbols8 button').click(); cy.get('button[id="saveSecretGenerator8"]').click(); diff --git a/e2e/cypress/e2e/machines/machines.cy.ts b/e2e/cypress/e2e/machines/machines.cy.ts index b0ca005e6d..504dc88df2 100644 --- a/e2e/cypress/e2e/machines/machines.cy.ts +++ b/e2e/cypress/e2e/machines/machines.cy.ts @@ -29,9 +29,9 @@ describe('machines', () => { cy.get('[data-e2e="create-user-button"]').should('be.visible').click(); cy.url().should('contain', 'users/create-machine'); //force needed due to the prefilled username prefix - cy.get('[formcontrolname="userName"]').type(machine.addName); - cy.get('[formcontrolname="name"]').type('e2emachinename'); - cy.get('[formcontrolname="description"]').type('e2emachinedescription'); + cy.get('[formcontrolname="userName"]').should('be.enabled').type(machine.addName); + cy.get('[formcontrolname="name"]').should('be.enabled').type('e2emachinename'); + cy.get('[formcontrolname="description"]').should('be.enabled').type('e2emachinedescription'); cy.get('[data-e2e="create-button"]').click(); cy.shouldConfirmSuccess(); let loginName = machine.addName; @@ -58,7 +58,7 @@ describe('machines', () => { it('should delete a machine', () => { const rowSelector = `tr:contains(${machine.removeName})`; cy.get(rowSelector).find('[data-e2e="enabled-delete-button"]').click({ force: true }); - cy.get('[data-e2e="confirm-dialog-input"]').focus().type(loginName); + cy.get('[data-e2e="confirm-dialog-input"]').focus().should('be.enabled').type(loginName); cy.get('[data-e2e="confirm-dialog-button"]').click(); cy.shouldConfirmSuccess(); cy.shouldNotExist({ diff --git a/e2e/cypress/e2e/organization/organizations.cy.ts b/e2e/cypress/e2e/organization/organizations.cy.ts index 773ac956ee..ab08c3a47c 100644 --- a/e2e/cypress/e2e/organization/organizations.cy.ts +++ b/e2e/cypress/e2e/organization/organizations.cy.ts @@ -18,7 +18,7 @@ describe('organizations', () => { describe('add and delete org', () => { it('should create an org', () => { cy.visit(orgsPathCreate); - cy.get('[data-e2e="org-name-input"]').focus().clear().type(newOrg); + cy.get('[data-e2e="org-name-input"]').focus().clear().should('be.enabled').type(newOrg); cy.get('[data-e2e="create-org-button"]').click(); cy.contains('tr', newOrg); }); @@ -30,7 +30,7 @@ describe('organizations', () => { cy.wait(1000); cy.get('[data-e2e="actions"]').click(); cy.get('[data-e2e="delete"]', { timeout: 1000 }).should('be.visible').click(); - cy.get('[data-e2e="confirm-dialog-input"]').focus().clear().type(newOrg); + cy.get('[data-e2e="confirm-dialog-input"]').focus().clear().should('be.enabled').type(newOrg); cy.get('[data-e2e="confirm-dialog-button"]').click(); cy.shouldConfirmSuccess(); cy.contains('tr', newOrg).should('not.exist'); @@ -49,7 +49,7 @@ describe('organizations', () => { cy.get('[data-e2e="actions"]').click(); cy.get('[data-e2e="rename"]', { timeout: 1000 }).should('be.visible').click(); - cy.get('[data-e2e="name"]').focus().clear().type(testOrgNameChange); + cy.get('[data-e2e="name"]').focus().clear().should('be.enabled').type(testOrgNameChange); cy.get('[data-e2e="dialog-submit"]').click(); cy.shouldConfirmSuccess(); cy.visit(orgPath); diff --git a/e2e/cypress/e2e/permissions/permissions.cy.ts b/e2e/cypress/e2e/permissions/permissions.cy.ts index d1755aa69a..a123d7533d 100644 --- a/e2e/cypress/e2e/permissions/permissions.cy.ts +++ b/e2e/cypress/e2e/permissions/permissions.cy.ts @@ -45,7 +45,7 @@ describe('permissions', () => { it('should add a manager', () => { cy.get('[data-e2e="add-member-button"]').click(); - cy.get('[data-e2e="add-member-input"]').type(testManagerUsername); + cy.get('[data-e2e="add-member-input"]').should('be.enabled').type(testManagerUsername); cy.get('[data-e2e="user-option"]').first().click(); cy.contains('[data-e2e="role-checkbox"]', roles[0]).click(); cy.get('[data-e2e="confirm-add-member-button"]').click(); @@ -174,9 +174,9 @@ describe('permissions', () => { it('should add a role', () => { cy.get('[data-e2e="sidenav-element-roles"]').click(); cy.get('[data-e2e="add-new-role"]').click(); - cy.get('[formcontrolname="key"]').type(testRoleName); - cy.get('[formcontrolname="displayName"]').type('e2eroleundertestdisplay'); - cy.get('[formcontrolname="group"]').type('e2eroleundertestgroup'); + cy.get('[formcontrolname="key"]').should('be.enabled').type(testRoleName); + cy.get('[formcontrolname="displayName"]').should('be.enabled').type('e2eroleundertestdisplay'); + cy.get('[formcontrolname="group"]').should('be.enabled').type('e2eroleundertestgroup'); cy.get('[data-e2e="save-button"]').click(); cy.shouldConfirmSuccess(); cy.contains('tr', testRoleName); diff --git a/e2e/cypress/e2e/projects/projects.cy.ts b/e2e/cypress/e2e/projects/projects.cy.ts index 36af2cd22e..a0b8552e4e 100644 --- a/e2e/cypress/e2e/projects/projects.cy.ts +++ b/e2e/cypress/e2e/projects/projects.cy.ts @@ -1,29 +1,29 @@ import { Context } from 'support/commands'; -import { ensureProjectDoesntExist, ensureProjectExists } from '../../support/api/projects'; +import { ensureProjectDoesntExist, ensureProjectExists, ensureRoleExists } from '../../support/api/projects'; import { ensureOrgExists } from 'support/api/orgs'; +import { ensureProjectGrantDoesntExist, ensureProjectGrantExists } from '../../support/api/grants'; describe('projects', () => { beforeEach(() => { cy.context().as('ctx'); }); - const defaultOrg = 'e2eorgnewdefault'; + const foreignOrg = 'e2eorgnewdefault'; const testProjectNameCreate = 'e2eprojectcreate'; const testProjectNameDelete = 'e2eprojectdelete'; + const testProjectRole = 'e2eprojectrole'; describe('add project', () => { beforeEach(`ensure it doesn't exist already`, () => { cy.get('@ctx').then((ctx) => { - ensureOrgExists(ctx, defaultOrg).then(() => { - ensureProjectDoesntExist(ctx.api, testProjectNameCreate); - cy.visit(`/projects`); - }); + ensureProjectDoesntExist(ctx.api, testProjectNameCreate); + cy.visit(`/projects`); }); }); it('should add a project', () => { cy.get('.add-project-button').click({ force: true }); - cy.get('input').type(testProjectNameCreate); + cy.get('input').should('be.enabled').type(testProjectNameCreate); cy.get('[data-e2e="continue-button"]').click(); cy.shouldConfirmSuccess(); }); @@ -32,41 +32,54 @@ describe('projects', () => { }); describe('create project grant', () => { - const testRoleName = 'e2eroleundertestname'; - beforeEach('ensure it exists', () => { cy.get('@ctx').then((ctx) => { ensureProjectExists(ctx.api, testProjectNameCreate).as('projectId'); - cy.get('@projectId').then((projectId) => { - cy.visit(`/projects/${projectId}`); - }); }); }); it('should add a role', () => { + const testRoleName = 'e2eroleundertestname'; + cy.get('@projectId').then((projectId) => { + cy.visit(`/projects/${projectId}`); + }); cy.get('[data-e2e="sidenav-element-roles"]').click(); cy.get('[data-e2e="add-new-role"]').click(); - cy.get('[formcontrolname="key"]').should('be.enabled').type(testRoleName); - cy.get('[formcontrolname="displayName"]').type('e2eroleundertestdisplay'); - cy.get('[formcontrolname="group"]').type('e2eroleundertestgroup'); + cy.get('[data-e2e="role-key-input"]').should('be.enabled').type(testRoleName); + cy.get('[formcontrolname="displayName"]').should('be.enabled').type('e2eroleundertestdisplay'); + cy.get('[formcontrolname="group"]').should('be.enabled').type('e2eroleundertestgroup'); cy.get('[data-e2e="save-button"]').click(); cy.shouldConfirmSuccess(); cy.contains('tr', testRoleName); }); - it('should add a project grant', () => { - const rowSelector = `tr:contains(${testRoleName})`; + describe('with existing role, without project grant', () => { + beforeEach(() => { + cy.get('@ctx').then((ctx) => { + cy.get('@projectId').then((projectId) => { + ensureOrgExists(ctx, foreignOrg).then((foreignOrgID) => { + ensureRoleExists(ctx.api, projectId, testProjectRole); + ensureProjectGrantDoesntExist(ctx, projectId, foreignOrgID); + cy.visit(`/projects/${projectId}`); + }); + }); + }); + }); - cy.get('[data-e2e="sidenav-element-projectgrants"]').click(); - cy.get('[data-e2e="create-project-grant-button"]').click(); - cy.get('[data-e2e="add-org-input"]').type(defaultOrg); - cy.get('mat-option').contains(defaultOrg).click(); - cy.get('button').should('be.enabled'); - cy.get('[data-e2e="project-grant-continue"]').first().click(); - cy.get(rowSelector).find('input').click({ force: true }); - cy.get('[data-e2e="save-project-grant-button"]').click(); - cy.contains('tr', defaultOrg); - cy.contains('tr', testRoleName); + it('should add a project grant', () => { + const rowSelector = `tr:contains(${testProjectRole})`; + + cy.get('[data-e2e="sidenav-element-projectgrants"]').click(); + cy.get('[data-e2e="create-project-grant-button"]').click(); + cy.get('[data-e2e="add-org-input"]').should('be.enabled').type(foreignOrg); + cy.get('mat-option').contains(foreignOrg).click(); + cy.get('button').should('be.enabled'); + cy.get('[data-e2e="project-grant-continue"]').first().click(); + cy.get(rowSelector).find('input').click({ force: true }); + cy.get('[data-e2e="save-project-grant-button"]').click(); + cy.contains('tr', foreignOrg); + cy.contains('tr', testProjectRole); + }); }); }); @@ -84,7 +97,7 @@ describe('projects', () => { cy.get('[data-e2e="toggle-grid"]').click(); cy.get('[data-e2e="timestamp"]'); cy.get(rowSelector).find('[data-e2e="delete-project-button"]').click({ force: true }); - cy.get('[data-e2e="confirm-dialog-input"]').focus().type(testProjectNameDelete); + cy.get('[data-e2e="confirm-dialog-input"]').focus().should('be.enabled').type(testProjectNameDelete); cy.get('[data-e2e="confirm-dialog-button"]').click(); cy.shouldConfirmSuccess(); cy.shouldNotExist({ @@ -96,7 +109,7 @@ describe('projects', () => { it('removes the project from grid view', () => { const cardSelector = `[data-e2e="grid-card"]:contains(${testProjectNameDelete})`; cy.get(cardSelector).find('[data-e2e="delete-project-button"]').click({ force: true }); - cy.get('[data-e2e="confirm-dialog-input"]').focus().type(testProjectNameDelete); + cy.get('[data-e2e="confirm-dialog-input"]').focus().should('be.enabled').type(testProjectNameDelete); cy.get('[data-e2e="confirm-dialog-button"]').click(); cy.shouldConfirmSuccess(); cy.shouldNotExist({ diff --git a/e2e/cypress/e2e/settings/external-links-settings.cy.ts b/e2e/cypress/e2e/settings/external-links-settings.cy.ts index ee23a2ad65..70ee137dd6 100644 --- a/e2e/cypress/e2e/settings/external-links-settings.cy.ts +++ b/e2e/cypress/e2e/settings/external-links-settings.cy.ts @@ -17,7 +17,6 @@ describe('external link settings', () => { }); describe('instance', () => { - beforeEach(`visit`, () => { cy.visit(`/instance?id=privacypolicy`); }); @@ -94,5 +93,4 @@ describe('external link settings', () => { cy.get('[formcontrolname="docsLink"]').should('value', docsLink); }); }); -}) - +}); diff --git a/e2e/cypress/e2e/settings/oidc-settings.cy.ts b/e2e/cypress/e2e/settings/oidc-settings.cy.ts index dc6a8e8a03..42d68dcead 100644 --- a/e2e/cypress/e2e/settings/oidc-settings.cy.ts +++ b/e2e/cypress/e2e/settings/oidc-settings.cy.ts @@ -22,15 +22,25 @@ describe('oidc settings', () => { }); it(`should update oidc settings`, () => { - cy.get('[formcontrolname="accessTokenLifetime"]').should('value', accessTokenPrecondition).clear().type('2'); - cy.get('[formcontrolname="idTokenLifetime"]').should('value', idTokenPrecondition).clear().type('24'); + cy.get('[formcontrolname="accessTokenLifetime"]') + .should('value', accessTokenPrecondition) + .clear() + .should('be.enabled') + .type('2'); + cy.get('[formcontrolname="idTokenLifetime"]') + .should('value', idTokenPrecondition) + .clear() + .should('be.enabled') + .type('24'); cy.get('[formcontrolname="refreshTokenExpiration"]') .should('value', refreshTokenExpirationPrecondition) .clear() + .should('be.enabled') .type('30'); cy.get('[formcontrolname="refreshTokenIdleExpiration"]') .should('value', refreshTokenIdleExpirationPrecondition) .clear() + .should('be.enabled') .type('7'); cy.get('[data-e2e="save-button"]').click(); cy.shouldConfirmSuccess(); diff --git a/e2e/cypress/support/api/grants.ts b/e2e/cypress/support/api/grants.ts index 5f6f5f4760..aefa662bab 100644 --- a/e2e/cypress/support/api/grants.ts +++ b/e2e/cypress/support/api/grants.ts @@ -1,6 +1,7 @@ import { Context } from 'support/commands'; -import { ensureItemExists } from './ensure'; +import { ensureItemDoesntExist, ensureItemExists } from './ensure'; import { getOrgUnderTest } from './orgs'; +import { API, Entity } from './types'; export function ensureProjectGrantExists(ctx: Context, foreignOrgId: string, foreignProjectId: string) { return getOrgUnderTest(ctx).then((orgUnderTest) => { @@ -16,3 +17,16 @@ export function ensureProjectGrantExists(ctx: Context, foreignOrgId: string, for ); }); } + +export function ensureProjectGrantDoesntExist(ctx: Context, projectId: number, foreignOrgId: string) { + return getOrgUnderTest(ctx).then((orgUnderTest) => { + console.log('removing grant to foreignOrgId', foreignOrgId, 'in orgUnderTest', orgUnderTest, 'projectId', projectId); + return ensureItemDoesntExist( + ctx.api, + `${ctx.api.mgmtBaseURL}/projectgrants/_search`, + (grant: any) => grant.grantedOrgId == foreignOrgId && grant.projectId == projectId, + (grant: any) => `${ctx.api.mgmtBaseURL}/projects/${projectId}/grants/${grant.grantId}`, + orgUnderTest.toString(), + ); + }); +} diff --git a/e2e/cypress/support/api/projects.ts b/e2e/cypress/support/api/projects.ts index ac368c7755..e54af29b12 100644 --- a/e2e/cypress/support/api/projects.ts +++ b/e2e/cypress/support/api/projects.ts @@ -23,7 +23,11 @@ export function ensureProjectDoesntExist(api: API, projectName: string, orgId?: } class ResourceType { - constructor(public resourcePath: string, public compareProperty: string, public identifierProperty: string) {} + constructor( + public resourcePath: string, + public compareProperty: string, + public identifierProperty: string, + ) {} } export const Apps = new ResourceType('apps', 'name', 'id'); @@ -47,19 +51,16 @@ export function ensureProjectResourceDoesntExist( ); } -export function ensureApplicationExists(api: API, projectId: number, appName: string) { +export function ensureRoleExists(api: API, projectId: number, roleName: string) { return ensureItemExists( api, - `${api.mgmtBaseURL}/projects/${projectId}/${Apps.resourcePath}/_search`, - (resource: any) => resource.name === appName, - `${api.mgmtBaseURL}/projects/${projectId}/${Apps.resourcePath}/oidc`, + `${api.mgmtBaseURL}/projects/${projectId}/${Roles.resourcePath}/_search`, + (resource: any) => resource.key === roleName, + `${api.mgmtBaseURL}/projects/${projectId}/${Roles.resourcePath}`, { - name: appName, - redirectUris: ['https://e2eredirecturl.org'], - responseTypes: ['OIDC_RESPONSE_TYPE_CODE'], - grantTypes: ['OIDC_GRANT_TYPE_AUTHORIZATION_CODE'], - authMethodType: 'OIDC_AUTH_METHOD_TYPE_NONE', - postLogoutRedirectUris: ['https://e2elogoutredirecturl.org'], + name: roleName, + roleKey: roleName, + displayName: roleName, }, ); } diff --git a/e2e/cypress/support/api/sms.ts b/e2e/cypress/support/api/sms.ts new file mode 100644 index 0000000000..324414c3b9 --- /dev/null +++ b/e2e/cypress/support/api/sms.ts @@ -0,0 +1,28 @@ +import { ensureItemDoesntExist, ensureItemExists } from './ensure'; +import { API, Entity } from './types'; +import { ensureSMTPProviderExists } from './smtp'; + +export function ensureSMSProviderExists(api: API) { + // remove and create + ensureSMSProviderDoesntExist(api); + return ensureItemExists( + api, + `${api.adminBaseURL}/sms/_search`, + ({ twilio: { sid: foundSid } }: any) => foundSid === 'initial-sid', + `${api.adminBaseURL}/sms/twilio`, + { + sid: 'initial-sid', + senderNumber: 'initial-senderNumber', + token: 'initial-token', + }, + ); +} + +export function ensureSMSProviderDoesntExist(api: API) { + return ensureItemDoesntExist( + api, + `${api.adminBaseURL}/sms/_search`, + (provider: any) => !!provider, + (provider) => `${api.adminBaseURL}/sms/${provider.id}`, + ); +} diff --git a/e2e/cypress/support/api/smtp.ts b/e2e/cypress/support/api/smtp.ts new file mode 100644 index 0000000000..78b7393976 --- /dev/null +++ b/e2e/cypress/support/api/smtp.ts @@ -0,0 +1,32 @@ +import { ensureItemDoesntExist, ensureItemExists } from './ensure'; +import { API, Entity } from './types'; + +export function ensureSMTPProviderExists(api: API, providerDescription: string) { + return ensureItemExists( + api, + `${api.adminBaseURL}/smtp/_search`, + (provider: any) => { + return provider.description === providerDescription; + }, + `${api.adminBaseURL}/smtp`, + { + name: providerDescription, + description: providerDescription, + senderAddress: 'a@sender.com', + senderName: 'A Sender', + host: 'smtp.host.com:587', + user: 'smtpuser', + }, + ); +} + +export function activateSMTPProvider(api: API, providerId: string) { + return cy.request({ + method: 'POST', + url: `${api.adminBaseURL}/smtp/${providerId}/_activate`, + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${api.token}`, + }, + }); +} diff --git a/e2e/package.json b/e2e/package.json index 4624e938b6..01c2995907 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -17,18 +17,18 @@ }, "private": true, "dependencies": { - "@types/pg": "^8.6.6", - "cypress-wait-until": "^1.7.2", - "jsonwebtoken": "^8.5.1", + "@types/pg": "^8.11.6", + "cypress-wait-until": "^3.0.2", + "jsonwebtoken": "^9.0.2", "mochawesome": "^7.1.3", - "pg": "^8.8.0", - "prettier": "^2.7.1", - "typescript": "^4.8.4", - "uuid": "^9.0.0", + "pg": "^8.12.0", + "prettier": "^3.3.3", + "typescript": "^5.5.4", + "uuid": "^10.0.0", "wait-on": "^7.2.0" }, "devDependencies": { - "@types/node": "^18.8.3", - "cypress": "^13.3.1" + "@types/node": "^22.3.0", + "cypress": "^13.13.3" } } diff --git a/e2e/yarn.lock b/e2e/yarn.lock index 84669a7fe1..90befd4063 100644 --- a/e2e/yarn.lock +++ b/e2e/yarn.lock @@ -7,7 +7,7 @@ resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ== -"@cypress/request@^3.0.0": +"@cypress/request@^3.0.1": version "3.0.1" resolved "https://registry.yarnpkg.com/@cypress/request/-/request-3.0.1.tgz#72d7d5425236a2413bd3d8bb66d02d9dc3168960" integrity sha512-TWivJlJi8ZDx2wGOw1dbLuHJKUYX7bWySw377nlnGOW3hP9/MUKIsEdXT/YngWxVdgNCHRBmFlBipE+5/2ZZlQ== @@ -68,20 +68,17 @@ resolved "https://registry.yarnpkg.com/@sideway/pinpoint/-/pinpoint-2.0.0.tgz#cff8ffadc372ad29fd3f78277aeb29e632cc70df" integrity sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ== -"@types/node@*": - version "20.7.0" - resolved "https://registry.yarnpkg.com/@types/node/-/node-20.7.0.tgz#c03de4572f114a940bc2ca909a33ddb2b925e470" - integrity sha512-zI22/pJW2wUZOVyguFaUL1HABdmSVxpXrzIqkjsHmyUjNhPoWM1CKfvVuXfetHhIok4RY573cqS0mZ1SJEnoTg== +"@types/node@*", "@types/node@^22.3.0": + version "22.3.0" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.3.0.tgz#7f8da0e2b72c27c4f9bd3cb5ef805209d04d4f9e" + integrity sha512-nrWpWVaDZuaVc5X84xJ0vNrLvomM205oQyLsRt7OHNZbSHslcWsvgFR7O7hire2ZonjLrWBbedmotmIlJDVd6g== + dependencies: + undici-types "~6.18.2" -"@types/node@^18.8.3": - version "18.18.0" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.18.0.tgz#bd19d5133a6e5e2d0152ec079ac27c120e7f1763" - integrity sha512-3xA4X31gHT1F1l38ATDIL9GpRLdwVhnEFC8Uikv5ZLlXATwrCYyPq7ZWHxzxc3J/30SUiwiYT+bQe0/XvKlWbw== - -"@types/pg@^8.6.6": - version "8.10.3" - resolved "https://registry.yarnpkg.com/@types/pg/-/pg-8.10.3.tgz#39b3acba4f313a65c8fbb4b241fcb21cc1ba4126" - integrity sha512-BACzsw64lCZesclRpZGu55tnqgFAYcrCBP92xLh1KLypZLCOsvJTSTgaoFVTy3lCys/aZTQzfeDxtjwrvdzL2g== +"@types/pg@^8.11.6": + version "8.11.6" + resolved "https://registry.yarnpkg.com/@types/pg/-/pg-8.11.6.tgz#a2d0fb0a14b53951a17df5197401569fb9c0c54b" + integrity sha512-/2WmmBXHLsfRqzfHW7BNZ8SbYzE8OSk7i3WjFYvfgRHj7S1xj+16Je5fUKv3lVdVzk/zn9TXOqf+avFCFIE0yQ== dependencies: "@types/node" "*" pg-protocol "*" @@ -93,14 +90,14 @@ integrity sha512-0kSuKjAS0TrGLJ0M/+8MaFkGsQhZpB6pxOmvS3K8FYI72K//YmdfoW9X2qPsAKh1mkwxGD5zib9s1FIFed6E8g== "@types/sizzle@^2.3.2": - version "2.3.4" - resolved "https://registry.yarnpkg.com/@types/sizzle/-/sizzle-2.3.4.tgz#cd6531924f60834fa4a1b8081f9eecf9bb1117f0" - integrity sha512-jA2llq2zNkg8HrALI7DtWzhALcVH0l7i89yhY3iBdOz6cBPeACoFq+fkQrjHA39t1hnSFOboZ7A/AY5MMZSlag== + version "2.3.8" + resolved "https://registry.yarnpkg.com/@types/sizzle/-/sizzle-2.3.8.tgz#518609aefb797da19bf222feb199e8f653ff7627" + integrity sha512-0vWLNK2D5MT9dg0iOo8GlKguPAU02QjmZitPEsXRuJXU/OGIOt9vT9Fc26wtYuavLxtO45v9PGleoL9Z0k1LHg== "@types/yauzl@^2.9.1": - version "2.10.1" - resolved "https://registry.yarnpkg.com/@types/yauzl/-/yauzl-2.10.1.tgz#4e8f299f0934d60f36c74f59cb5a8483fd786691" - integrity sha512-CHzgNU3qYBnp/O4S3yv2tXPlvMTq0YWSTVg2/JYLqWZGHwwgJGAwd00poay/11asPq8wLFwHzubyInqHIFmmiw== + version "2.10.3" + resolved "https://registry.yarnpkg.com/@types/yauzl/-/yauzl-2.10.3.tgz#e9b2808b4f109504a03cda958259876f61017999" + integrity sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q== dependencies: "@types/node" "*" @@ -159,9 +156,9 @@ astral-regex@^2.0.0: integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== async@^3.2.0: - version "3.2.4" - resolved "https://registry.yarnpkg.com/async/-/async-3.2.4.tgz#2d22e00f8cddeb5fde5dd33522b56d1cf569a81c" - integrity sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ== + version "3.2.5" + resolved "https://registry.yarnpkg.com/async/-/async-3.2.5.tgz#ebd52a8fdaf7a2289a24df399f8d8485c8a46b66" + integrity sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg== asynckit@^0.4.0: version "0.4.0" @@ -179,24 +176,19 @@ aws-sign2@~0.7.0: integrity sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA== aws4@^1.8.0: - version "1.12.0" - resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.12.0.tgz#ce1c9d143389679e253b314241ea9aa5cec980d3" - integrity sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg== + version "1.13.1" + resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.13.1.tgz#bb5f8b8a20739f6ae1caeaf7eea2c7913df8048e" + integrity sha512-u5w79Rd7SU4JaIlA/zFqG+gOiuq25q5VLyZ8E+ijJeILuTxVzZgp2CaGw/UTw6pXYN9XMO9yiqj/nEHmhTG5CA== axios@^1.6.1: - version "1.6.8" - resolved "https://registry.yarnpkg.com/axios/-/axios-1.6.8.tgz#66d294951f5d988a00e87a0ffb955316a619ea66" - integrity sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ== + version "1.7.4" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.4.tgz#4c8ded1b43683c8dd362973c393f3ede24052aa2" + integrity sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw== dependencies: follow-redirects "^1.15.6" form-data "^4.0.0" proxy-from-env "^1.1.0" -balanced-match@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" - integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== - base64-js@^1.3.1: version "1.5.1" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" @@ -219,14 +211,6 @@ bluebird@^3.7.2: resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== -brace-expansion@^1.1.7: - version "1.1.11" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" - integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== - dependencies: - balanced-match "^1.0.0" - concat-map "0.0.1" - buffer-crc32@~0.2.3: version "0.2.13" resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" @@ -237,11 +221,6 @@ buffer-equal-constant-time@1.0.1: resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" integrity sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA== -buffer-writer@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/buffer-writer/-/buffer-writer-2.0.0.tgz#ce7eb81a38f7829db09c873f2fbb792c0c98ec04" - integrity sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw== - buffer@^5.7.1: version "5.7.1" resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" @@ -255,13 +234,16 @@ cachedir@^2.3.0: resolved "https://registry.yarnpkg.com/cachedir/-/cachedir-2.4.0.tgz#7fef9cf7367233d7c88068fe6e34ed0d355a610d" integrity sha512-9EtFOZR8g22CL7BWjJ9BUx1+A/djkofnyW3aOXZORNW2kxoUpx2h+uN2cOqwPmFhnpVmxg+KW2OjOSgChTEvsQ== -call-bind@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" - integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA== +call-bind@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" + integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w== dependencies: - function-bind "^1.1.1" - get-intrinsic "^1.0.2" + es-define-property "^1.0.0" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + set-function-length "^1.2.1" caseless@~0.12.0: version "0.12.0" @@ -282,9 +264,9 @@ check-more-types@^2.24.0: integrity sha512-Pj779qHxV2tuapviy1bSZNEL1maXr13bPYpsvSDB68HlYcYuhlDrmGd63i0JHMCLKzc7rUSNIrpdJlhVlNwrxA== ci-info@^3.2.0: - version "3.8.0" - resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.8.0.tgz#81408265a5380c929f0bc665d62256628ce9ef91" - integrity sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw== + version "3.9.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.9.0.tgz#4279a62028a7b1f262f3473fc9605f5e218c59b4" + integrity sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ== clean-stack@^2.0.0: version "2.2.0" @@ -299,9 +281,9 @@ cli-cursor@^3.1.0: restore-cursor "^3.1.0" cli-table3@~0.6.1: - version "0.6.3" - resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.3.tgz#61ab765aac156b52f222954ffc607a6f01dbeeb2" - integrity sha512-w5Jac5SykAeZJKntOxJCrm63Eg5/4dhMWIcuTbo9rpE+brgaSZo0RuNJZeOyMgsUdhDeojvgyQLmjI+K50ZGyg== + version "0.6.5" + resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.5.tgz#013b91351762739c16a9567c21a04632e449bf2f" + integrity sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ== dependencies: string-width "^4.2.0" optionalDependencies: @@ -358,11 +340,6 @@ common-tags@^1.8.0: resolved "https://registry.yarnpkg.com/common-tags/-/common-tags-1.8.2.tgz#94ebb3c076d26032745fd54face7f688ef5ac9c6" integrity sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA== -concat-map@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" - integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== - core-util-is@1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" @@ -377,17 +354,17 @@ cross-spawn@^7.0.0: shebang-command "^2.0.0" which "^2.0.1" -cypress-wait-until@^1.7.2: - version "1.7.2" - resolved "https://registry.yarnpkg.com/cypress-wait-until/-/cypress-wait-until-1.7.2.tgz#7f534dd5a11c89b65359e7a0210f20d3dfc22107" - integrity sha512-uZ+M8/MqRcpf+FII/UZrU7g1qYZ4aVlHcgyVopnladyoBrpoaMJ4PKZDrdOJ05H5RHbr7s9Tid635X3E+ZLU/Q== +cypress-wait-until@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/cypress-wait-until/-/cypress-wait-until-3.0.2.tgz#c90dddfa4c46a2c422f5b91d486531c560bae46e" + integrity sha512-iemies796dD5CgjG5kV0MnpEmKSH+s7O83ZoJLVzuVbZmm4lheMsZqAVT73hlMx4QlkwhxbyUzhOBUOZwoOe0w== -cypress@^13.3.1: - version "13.7.2" - resolved "https://registry.yarnpkg.com/cypress/-/cypress-13.7.2.tgz#61e841382abb20e0a9a063086ee0d850af3ef6bc" - integrity sha512-FF5hFI5wlRIHY8urLZjJjj/YvfCBrRpglbZCLr/cYcL9MdDe0+5usa8kTIrDHthlEc9lwihbkb5dmwqBDNS2yw== +cypress@^13.13.3: + version "13.13.3" + resolved "https://registry.yarnpkg.com/cypress/-/cypress-13.13.3.tgz#21ee054bb4e00b3858f2e33b4f8f4e69128470a9" + integrity sha512-hUxPrdbJXhUOTzuML+y9Av7CKoYznbD83pt8g3klgpioEha0emfx4WNIuVRx0C76r0xV2MIwAW9WYiXfVJYFQw== dependencies: - "@cypress/request" "^3.0.0" + "@cypress/request" "^3.0.1" "@cypress/xvfb" "^1.2.4" "@types/sinonjs__fake-timers" "8.1.1" "@types/sizzle" "^2.3.2" @@ -426,7 +403,7 @@ cypress@^13.3.1: request-progress "^3.0.0" semver "^7.5.3" supports-color "^8.1.1" - tmp "~0.2.1" + tmp "~0.2.3" untildify "^4.0.0" yauzl "^2.10.0" @@ -443,9 +420,9 @@ dateformat@^4.5.1: integrity sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA== dayjs@^1.10.4: - version "1.11.10" - resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.10.tgz#68acea85317a6e164457d6d6947564029a6a16a0" - integrity sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ== + version "1.11.12" + resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.12.tgz#5245226cc7f40a15bf52e0b99fd2a04669ccac1d" + integrity sha512-Rt2g+nTbLlDWZTwwrIXjy9MeiZmSDI375FvZs72ngxx8PDC6YXOeR3q5LAuPzjZQxhiWdRKac7RKV+YyQYfYIg== debug@^3.1.0: version "3.2.7" @@ -455,21 +432,30 @@ debug@^3.1.0: ms "^2.1.1" debug@^4.1.1, debug@^4.3.4: - version "4.3.4" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" - integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== + version "4.3.6" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.6.tgz#2ab2c38fbaffebf8aa95fdfe6d88438c7a13c52b" + integrity sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg== dependencies: ms "2.1.2" +define-data-property@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" + integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + gopd "^1.0.1" + 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== diff@^5.0.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/diff/-/diff-5.1.0.tgz#bc52d298c5ea8df9194800224445ed43ffc87e40" - integrity sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw== + version "5.2.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-5.2.0.tgz#26ded047cd1179b78b9537d5ef725503ce1ae531" + integrity sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A== ecc-jsbn@~0.1.1: version "0.1.2" @@ -506,10 +492,22 @@ enquirer@^2.3.6: ansi-colors "^4.1.1" strip-ansi "^6.0.1" +es-define-property@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845" + integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ== + dependencies: + get-intrinsic "^1.2.4" + +es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== + escalade@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" - integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== + version "3.1.2" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.2.tgz#54076e9ab29ea5bf3d8f1ed62acffbb88272df27" + integrity sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA== escape-html@^1.0.3: version "1.0.3" @@ -635,35 +633,31 @@ fs-extra@^9.1.0: jsonfile "^6.0.1" universalify "^2.0.0" -fs.realpath@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" - integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== - fsu@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/fsu/-/fsu-1.1.1.tgz#bd36d3579907c59d85b257a75b836aa9e0c31834" integrity sha512-xQVsnjJ/5pQtcKh+KjUoZGzVWn4uNkchxTF6Lwjr4Gf7nQr8fmUfhKJ62zE77+xQg9xnxi5KUps7XGs+VC986A== -function-bind@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" - integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== get-caller-file@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== -get-intrinsic@^1.0.2: - version "1.2.1" - resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.1.tgz#d295644fed4505fc9cde952c37ee12b477a83d82" - integrity sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw== +get-intrinsic@^1.1.3, get-intrinsic@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd" + integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== dependencies: - function-bind "^1.1.1" - has "^1.0.3" + es-errors "^1.3.0" + function-bind "^1.1.2" has-proto "^1.0.1" has-symbols "^1.0.3" + hasown "^2.0.0" get-stream@^5.0.0, get-stream@^5.1.0: version "5.2.0" @@ -686,18 +680,6 @@ getpass@^0.1.1: dependencies: assert-plus "^1.0.0" -glob@^7.1.3: - version "7.2.3" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" - integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.1.1" - once "^1.3.0" - path-is-absolute "^1.0.0" - global-dirs@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-3.0.1.tgz#0c488971f066baceda21447aecb1a8b911d22485" @@ -705,6 +687,13 @@ global-dirs@^3.0.0: dependencies: ini "2.0.0" +gopd@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" + integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== + dependencies: + get-intrinsic "^1.1.3" + graceful-fs@^4.1.6, graceful-fs@^4.2.0: version "4.2.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" @@ -715,22 +704,29 @@ has-flag@^4.0.0: resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== +has-property-descriptors@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" + integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== + dependencies: + es-define-property "^1.0.0" + has-proto@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.1.tgz#1885c1305538958aff469fef37937c22795408e0" - integrity sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg== + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.3.tgz#b31ddfe9b0e6e9914536a6ab286426d0214f77fd" + integrity sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q== has-symbols@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== -has@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" - integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== +hasown@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== dependencies: - function-bind "^1.1.1" + function-bind "^1.1.2" http-signature@~1.3.6: version "1.3.6" @@ -756,19 +752,6 @@ indent-string@^4.0.0: resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== -inflight@^1.0.4: - version "1.0.6" - resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" - integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== - dependencies: - once "^1.3.0" - wrappy "1" - -inherits@2: - version "2.0.4" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" - integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== - ini@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ini/-/ini-2.0.0.tgz#e5fd556ecdd5726be978fa1001862eacb0a94bc5" @@ -825,9 +808,9 @@ isstream@~0.1.2: integrity sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g== joi@^17.11.0: - version "17.12.3" - resolved "https://registry.yarnpkg.com/joi/-/joi-17.12.3.tgz#944646979cd3b460178547b12ba37aca8482f63d" - integrity sha512-2RRziagf555owrm9IRVtdKynOBeITiDpuZqIpgwqXShPncPKNiRQoiGsl/T8SQdq+8ugRzH2LqY67irr2y/d+g== + version "17.13.3" + resolved "https://registry.yarnpkg.com/joi/-/joi-17.13.3.tgz#0f5cc1169c999b30d344366d384b12d92558bcec" + integrity sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA== dependencies: "@hapi/hoek" "^9.3.0" "@hapi/topo" "^5.1.0" @@ -864,10 +847,10 @@ jsonfile@^6.0.1: optionalDependencies: graceful-fs "^4.1.6" -jsonwebtoken@^8.5.1: - version "8.5.1" - resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#00e71e0b8df54c2121a1f26137df2280673bcc0d" - integrity sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w== +jsonwebtoken@^9.0.2: + version "9.0.2" + resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz#65ff91f4abef1784697d40952bb1998c504caaf3" + integrity sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ== dependencies: jws "^3.2.2" lodash.includes "^4.3.0" @@ -878,7 +861,7 @@ jsonwebtoken@^8.5.1: lodash.isstring "^4.0.1" lodash.once "^4.0.0" ms "^2.1.1" - semver "^5.6.0" + semver "^7.5.4" jsprim@^2.0.2: version "2.0.2" @@ -1006,13 +989,6 @@ loose-envify@^1.4.0: dependencies: js-tokens "^3.0.0 || ^4.0.0" -lru-cache@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" - integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== - dependencies: - yallist "^4.0.0" - merge-stream@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" @@ -1035,13 +1011,6 @@ mimic-fn@^2.1.0: resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== -minimatch@^3.1.1: - version "3.1.2" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" - integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== - dependencies: - brace-expansion "^1.1.7" - minimist@^1.2.8: version "1.2.8" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" @@ -1103,17 +1072,17 @@ object-assign@^4.1.1: resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== -object-inspect@^1.9.0: - version "1.12.3" - resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.3.tgz#ba62dffd67ee256c8c086dfae69e016cd1f198b9" - integrity sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g== +object-inspect@^1.13.1: + version "1.13.2" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.2.tgz#dea0088467fb991e67af4058147a24824a3043ff" + integrity sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g== obuf@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e" integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg== -once@^1.3.0, once@^1.3.1, once@^1.4.0: +once@^1.3.1, once@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== @@ -1144,16 +1113,6 @@ p-map@^4.0.0: dependencies: aggregate-error "^3.0.0" -packet-reader@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/packet-reader/-/packet-reader-1.0.0.tgz#9238e5480dedabacfe1fe3f2771063f164157d74" - integrity sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ== - -path-is-absolute@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" - integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== - path-key@^3.0.0, path-key@^3.1.0: version "3.1.1" resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" @@ -1174,10 +1133,10 @@ pg-cloudflare@^1.1.1: resolved "https://registry.yarnpkg.com/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz#e6d5833015b170e23ae819e8c5d7eaedb472ca98" integrity sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q== -pg-connection-string@^2.6.2: - version "2.6.2" - resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.6.2.tgz#713d82053de4e2bd166fab70cd4f26ad36aab475" - integrity sha512-ch6OwaeaPYcova4kKZ15sbJ2hKb/VP48ZD2gE7i1J+L4MspCtBMAx8nMgz7bksc7IojCIIWuEhHibSMFH8m8oA== +pg-connection-string@^2.6.4: + version "2.6.4" + resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.6.4.tgz#f543862adfa49fa4e14bc8a8892d2a84d754246d" + integrity sha512-v+Z7W/0EO707aNMaAEfiGnGL9sxxumwLl2fJvCQtMn9Fxsg+lPpPkdcyBSv/KFgpGdYkMfn+EI1Or2EHjpgLCA== pg-int8@1.0.1: version "1.0.1" @@ -1189,15 +1148,15 @@ pg-numeric@1.0.2: resolved "https://registry.yarnpkg.com/pg-numeric/-/pg-numeric-1.0.2.tgz#816d9a44026086ae8ae74839acd6a09b0636aa3a" integrity sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw== -pg-pool@^3.6.1: - version "3.6.1" - resolved "https://registry.yarnpkg.com/pg-pool/-/pg-pool-3.6.1.tgz#5a902eda79a8d7e3c928b77abf776b3cb7d351f7" - integrity sha512-jizsIzhkIitxCGfPRzJn1ZdcosIt3pz9Sh3V01fm1vZnbnCMgmGl5wvGGdNN2EL9Rmb0EcFoCkixH4Pu+sP9Og== +pg-pool@^3.6.2: + version "3.6.2" + resolved "https://registry.yarnpkg.com/pg-pool/-/pg-pool-3.6.2.tgz#3a592370b8ae3f02a7c8130d245bc02fa2c5f3f2" + integrity sha512-Htjbg8BlwXqSBQ9V8Vjtc+vzf/6fVUuak/3/XXKA9oxZprwW3IMDQTGHP+KDmVL7rtd+R1QjbnCFPuTHm3G4hg== -pg-protocol@*, pg-protocol@^1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/pg-protocol/-/pg-protocol-1.6.0.tgz#4c91613c0315349363af2084608db843502f8833" - integrity sha512-M+PDm637OY5WM307051+bsDia5Xej6d9IR4GwJse1qA1DIhiKlksvrneZOYQq42OM+spubpcNYEo2FcKQrDk+Q== +pg-protocol@*, pg-protocol@^1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/pg-protocol/-/pg-protocol-1.6.1.tgz#21333e6d83b01faaebfe7a33a7ad6bfd9ed38cb3" + integrity sha512-jPIlvgoD63hrEuihvIg+tJhoGjUsLPn6poJY9N5CnlPd91c2T18T/9zBtLxZSb1EhYxBRoZJtzScCaWlYLtktg== pg-types@^2.1.0: version "2.2.0" @@ -1211,28 +1170,26 @@ pg-types@^2.1.0: postgres-interval "^1.1.0" pg-types@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/pg-types/-/pg-types-4.0.1.tgz#31857e89d00a6c66b06a14e907c3deec03889542" - integrity sha512-hRCSDuLII9/LE3smys1hRHcu5QGcLs9ggT7I/TCs0IE+2Eesxi9+9RWAAwZ0yaGjxoWICF/YHLOEjydGujoJ+g== + version "4.0.2" + resolved "https://registry.yarnpkg.com/pg-types/-/pg-types-4.0.2.tgz#399209a57c326f162461faa870145bb0f918b76d" + integrity sha512-cRL3JpS3lKMGsKaWndugWQoLOCoP+Cic8oseVcbr0qhPzYD5DWXK+RZ9LY9wxRf7RQia4SCwQlXk0q6FCPrVng== dependencies: pg-int8 "1.0.1" pg-numeric "1.0.2" postgres-array "~3.0.1" postgres-bytea "~3.0.0" - postgres-date "~2.0.1" + postgres-date "~2.1.0" postgres-interval "^3.0.0" postgres-range "^1.1.1" -pg@^8.8.0: - version "8.11.3" - resolved "https://registry.yarnpkg.com/pg/-/pg-8.11.3.tgz#d7db6e3fe268fcedd65b8e4599cda0b8b4bf76cb" - integrity sha512-+9iuvG8QfaaUrrph+kpF24cXkH1YOOUeArRNYIxq1viYHZagBxrTno7cecY1Fa44tJeZvaoG+Djpkc3JwehN5g== +pg@^8.12.0: + version "8.12.0" + resolved "https://registry.yarnpkg.com/pg/-/pg-8.12.0.tgz#9341724db571022490b657908f65aee8db91df79" + integrity sha512-A+LHUSnwnxrnL/tZ+OLfqR1SxLN3c/pgDztZ47Rpbsd4jUytsTtwQo/TLPRzPJMp/1pbhYVhH9cuSZLAajNfjQ== dependencies: - buffer-writer "2.0.0" - packet-reader "1.0.0" - pg-connection-string "^2.6.2" - pg-pool "^3.6.1" - pg-protocol "^1.6.0" + pg-connection-string "^2.6.4" + pg-pool "^3.6.2" + pg-protocol "^1.6.1" pg-types "^2.1.0" pgpass "1.x" optionalDependencies: @@ -1277,10 +1234,10 @@ postgres-date@~1.0.4: resolved "https://registry.yarnpkg.com/postgres-date/-/postgres-date-1.0.7.tgz#51bc086006005e5061c591cee727f2531bf641a8" integrity sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q== -postgres-date@~2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/postgres-date/-/postgres-date-2.0.1.tgz#638b62e5c33764c292d37b08f5257ecb09231457" - integrity sha512-YtMKdsDt5Ojv1wQRvUhnyDJNSr2dGIC96mQVKz7xufp07nfuFONzdaowrMHjlAzY6GDLd4f+LUHHAAM1h4MdUw== +postgres-date@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/postgres-date/-/postgres-date-2.1.0.tgz#b85d3c1fb6fb3c6c8db1e9942a13a3bf625189d0" + integrity sha512-K7Juri8gtgXVcDfZttFKVmhglp7epKb1K4pgrkLxehjqkrgPhfG6OO8LHLkfaqkbpjNRnra018XwAr1yQFWGcA== postgres-interval@^1.1.0: version "1.2.0" @@ -1295,14 +1252,14 @@ postgres-interval@^3.0.0: integrity sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw== postgres-range@^1.1.1: - version "1.1.3" - resolved "https://registry.yarnpkg.com/postgres-range/-/postgres-range-1.1.3.tgz#9ccd7b01ca2789eb3c2e0888b3184225fa859f76" - integrity sha512-VdlZoocy5lCP0c/t66xAfclglEapXPCIVhqqJRncYpvbCgImF0w67aPKfbqUMr72tO2k5q0TdTZwCLjPTI6C9g== + version "1.1.4" + resolved "https://registry.yarnpkg.com/postgres-range/-/postgres-range-1.1.4.tgz#a59c5f9520909bcec5e63e8cf913a92e4c952863" + integrity sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w== -prettier@^2.7.1: - version "2.8.8" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da" - integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q== +prettier@^3.3.3: + version "3.3.3" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.3.3.tgz#30c54fe0be0d8d12e6ae61dbb10109ea00d53105" + integrity sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew== pretty-bytes@^5.6.0: version "5.6.0" @@ -1347,9 +1304,9 @@ pump@^3.0.0: once "^1.3.1" punycode@^2.1.1: - version "2.3.0" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f" - integrity sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA== + version "2.3.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" + integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== qs@6.10.4: version "6.10.4" @@ -1394,16 +1351,9 @@ restore-cursor@^3.1.0: signal-exit "^3.0.2" rfdc@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.0.tgz#d0b7c441ab2720d05dc4cf26e01c89631d9da08b" - integrity sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA== - -rimraf@^3.0.0: - version "3.0.2" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" - integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== - dependencies: - glob "^7.1.3" + version "1.4.1" + resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.4.1.tgz#778f76c4fb731d93414e8f925fbecf64cce7f6ca" + integrity sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA== rxjs@^7.5.1, rxjs@^7.8.1: version "7.8.1" @@ -1422,17 +1372,22 @@ safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== -semver@^5.6.0: - version "5.7.2" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8" - integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g== +semver@^7.5.3, semver@^7.5.4: + version "7.6.3" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143" + integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== -semver@^7.5.3: - version "7.5.4" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" - integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== +set-function-length@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" + integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== dependencies: - lru-cache "^6.0.0" + define-data-property "^1.1.4" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + gopd "^1.0.1" + has-property-descriptors "^1.0.2" shebang-command@^2.0.0: version "2.0.0" @@ -1447,13 +1402,14 @@ shebang-regex@^3.0.0: integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== side-channel@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" - integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw== + version "1.0.6" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2" + integrity sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA== dependencies: - call-bind "^1.0.0" - get-intrinsic "^1.0.2" - object-inspect "^1.9.0" + call-bind "^1.0.7" + es-errors "^1.3.0" + get-intrinsic "^1.2.4" + object-inspect "^1.13.1" signal-exit@^3.0.2: version "3.0.7" @@ -1484,9 +1440,9 @@ split2@^4.1.0: integrity sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg== sshpk@^1.14.1: - version "1.17.0" - resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.17.0.tgz#578082d92d4fe612b13007496e543fa0fbcbe4c5" - integrity sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ== + version "1.18.0" + resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.18.0.tgz#1663e55cddf4d688b86a46b77f0d5fe363aba028" + integrity sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ== dependencies: asn1 "~0.2.3" assert-plus "^1.0.0" @@ -1546,26 +1502,24 @@ tcomb@^3.0.0, tcomb@^3.2.17: integrity sha512-di2Hd1DB2Zfw6StGv861JoAF5h/uQVu/QJp2g8KVbtfKnoHdBQl5M32YWq6mnSYBQ1vFFrns5B1haWJL7rKaOQ== throttleit@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-1.0.0.tgz#9e785836daf46743145a5984b6268d828528ac6c" - integrity sha512-rkTVqu6IjfQ/6+uNuuc3sZek4CEYxTJom3IktzgdSxcZqdARuebbA/f4QmAxMQIxqq9ZLEUkSYqvuk1I6VKq4g== + version "1.0.1" + resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-1.0.1.tgz#304ec51631c3b770c65c6c6f76938b384000f4d5" + integrity sha512-vDZpf9Chs9mAdfY046mcPt8fg5QSZr37hEH4TXYBnDF+izxgrbRGUAAaBvIk/fJm9aOFCGFd1EsNg5AZCbnQCQ== through@^2.3.8: version "2.3.8" resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== -tmp@~0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.1.tgz#8457fc3037dcf4719c251367a1af6500ee1ccf14" - integrity sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ== - dependencies: - rimraf "^3.0.0" +tmp@~0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.3.tgz#eb783cc22bc1e8bebd0671476d46ea4eb32a79ae" + integrity sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w== tough-cookie@^4.1.3: - version "4.1.3" - resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.1.3.tgz#97b9adb0728b42280aa3d814b6b999b2ff0318bf" - integrity sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw== + version "4.1.4" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.1.4.tgz#945f1461b45b5a8c76821c33ea49c3ac192c1b36" + integrity sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag== dependencies: psl "^1.1.33" punycode "^2.1.1" @@ -1573,9 +1527,9 @@ tough-cookie@^4.1.3: url-parse "^1.5.3" tslib@^2.1.0: - version "2.6.2" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" - integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== + version "2.6.3" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.3.tgz#0438f810ad7a9edcde7a241c3d80db693c8cbfe0" + integrity sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ== tunnel-agent@^0.6.0: version "0.6.0" @@ -1594,10 +1548,15 @@ type-fest@^0.21.3: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== -typescript@^4.8.4: - version "4.9.5" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a" - integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g== +typescript@^5.5.4: + version "5.5.4" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.5.4.tgz#d9852d6c82bad2d2eda4fd74a5762a8f5909e9ba" + integrity sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q== + +undici-types@~6.18.2: + version "6.18.2" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.18.2.tgz#8b678cf939d4fc9ec56be3c68ed69c619dee28b0" + integrity sha512-5ruQbENj95yDYJNS3TvcaxPMshV7aizdv/hWYjGIKoANWKjhWNBsr2YEuYZKodQulB1b8l7ILOuDQep3afowQQ== universalify@^0.2.0: version "0.2.0" @@ -1605,9 +1564,9 @@ universalify@^0.2.0: integrity sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg== universalify@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" - integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== + version "2.0.1" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d" + integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw== untildify@^4.0.0: version "4.0.0" @@ -1622,20 +1581,20 @@ url-parse@^1.5.3: querystringify "^2.1.1" requires-port "^1.0.0" +uuid@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-10.0.0.tgz#5a95aa454e6e002725c79055fd42aaba30ca6294" + integrity sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ== + uuid@^8.3.2: version "8.3.2" resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== -uuid@^9.0.0: - version "9.0.1" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30" - integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA== - validator@^13.6.0: - version "13.11.0" - resolved "https://registry.yarnpkg.com/validator/-/validator-13.11.0.tgz#23ab3fd59290c61248364eabf4067f04955fbb1b" - integrity sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ== + version "13.12.0" + resolved "https://registry.yarnpkg.com/validator/-/validator-13.12.0.tgz#7d78e76ba85504da3fee4fd1922b385914d4b35f" + integrity sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg== verror@1.10.0: version "1.10.0" @@ -1697,11 +1656,6 @@ y18n@^5.0.5: resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== -yallist@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" - integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== - yargs-parser@^21.1.1: version "21.1.1" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" diff --git a/go.mod b/go.mod index dae92417fa..49fe4df244 100644 --- a/go.mod +++ b/go.mod @@ -59,9 +59,9 @@ require ( github.com/superseriousbusiness/exifremove v0.0.0-20210330092427-6acd27eac203 github.com/ttacon/libphonenumber v1.2.1 github.com/zitadel/logging v0.6.0 - github.com/zitadel/oidc/v3 v3.26.1 + github.com/zitadel/oidc/v3 v3.28.1 github.com/zitadel/passwap v0.6.0 - github.com/zitadel/saml v0.1.3 + github.com/zitadel/saml v0.2.0 github.com/zitadel/schema v1.3.0 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 @@ -78,8 +78,8 @@ require ( golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 golang.org/x/net v0.26.0 golang.org/x/oauth2 v0.22.0 - golang.org/x/sync v0.7.0 - golang.org/x/text v0.16.0 + golang.org/x/sync v0.8.0 + golang.org/x/text v0.17.0 google.golang.org/api v0.187.0 google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 google.golang.org/grpc v1.65.0 diff --git a/go.sum b/go.sum index 16c1046d56..416161b726 100644 --- a/go.sum +++ b/go.sum @@ -723,12 +723,12 @@ github.com/zenazn/goji v1.0.1 h1:4lbD8Mx2h7IvloP7r2C0D6ltZP6Ufip8Hn0wmSK5LR8= github.com/zenazn/goji v1.0.1/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= github.com/zitadel/logging v0.6.0 h1:t5Nnt//r+m2ZhhoTmoPX+c96pbMarqJvW1Vq6xFTank= github.com/zitadel/logging v0.6.0/go.mod h1:Y4CyAXHpl3Mig6JOszcV5Rqqsojj+3n7y2F591Mp/ow= -github.com/zitadel/oidc/v3 v3.26.1 h1:/4wi2gxHByI9YYEjqcwEUx5GjsfDk8reudNP1Cp5Hgo= -github.com/zitadel/oidc/v3 v3.26.1/go.mod h1:ZwBEqSviCpJVZiYashzo53bEGRGXi7amE5Q8PpQg9IM= +github.com/zitadel/oidc/v3 v3.28.1 h1:PsbFm5CzEMQq9HBXUNJ8yvnWmtVYxpwV5Cinj7TTsHo= +github.com/zitadel/oidc/v3 v3.28.1/go.mod h1:WmDFu3dZ9YNKrIoZkmxjGG8QyUR4PbbhsVVSY+rpojM= github.com/zitadel/passwap v0.6.0 h1:m9F3epFC0VkBXu25rihSLGyHvWiNlCzU5kk8RoI+SXQ= github.com/zitadel/passwap v0.6.0/go.mod h1:kqAiJ4I4eZvm3Y6oAk6hlEqlZZOkjMHraGXF90GG7LI= -github.com/zitadel/saml v0.1.3 h1:LI4DOCVyyU1qKPkzs3vrGcA5J3H4pH3+CL9zr9ShkpM= -github.com/zitadel/saml v0.1.3/go.mod h1:MdkjyU3mwnTuh4lNnhPG+RyZL/VfzD72wUG/eWWBaXc= +github.com/zitadel/saml v0.2.0 h1:vv7r+Xz43eAPCb+fImMaospD+TWRZQDkb78AbSJRcL4= +github.com/zitadel/saml v0.2.0/go.mod h1:QqKcguOt7mMVI6tkEfpkyzwnYRdlmn3kYQj3VTPUw1g= github.com/zitadel/schema v1.3.0 h1:kQ9W9tvIwZICCKWcMvCEweXET1OcOyGEuFbHs4o5kg0= github.com/zitadel/schema v1.3.0/go.mod h1:NptN6mkBDFvERUCvZHlvWmmME+gmZ44xzwRXwhzsbtc= go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= @@ -871,8 +871,8 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -932,8 +932,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= diff --git a/internal/api/grpc/admin/custom_text_converter.go b/internal/api/grpc/admin/custom_text_converter.go index 4bb76b3617..a7471525d9 100644 --- a/internal/api/grpc/admin/custom_text_converter.go +++ b/internal/api/grpc/admin/custom_text_converter.go @@ -172,7 +172,6 @@ func SetLoginTextToDomain(req *admin_pb.SetCustomLoginTextsRequest) *domain.Cust result.RegistrationUser = text.RegistrationUserScreenTextPbToDomain(req.RegistrationUserText) result.ExternalRegistrationUserOverview = text.ExternalRegistrationUserOverviewScreenTextPbToDomain(req.ExternalRegistrationUserOverviewText) result.RegistrationOrg = text.RegistrationOrgScreenTextPbToDomain(req.RegistrationOrgText) - result.LinkingUserPrompt = text.LinkingUserPromptScreenTextPbToDomain(req.LinkingUserPromptText) result.LinkingUsersDone = text.LinkingUserDoneScreenTextPbToDomain(req.LinkingUserDoneText) result.ExternalNotFound = text.ExternalUserNotFoundScreenTextPbToDomain(req.ExternalUserNotFoundText) result.LoginSuccess = text.SuccessLoginScreenTextPbToDomain(req.SuccessLoginText) diff --git a/internal/api/grpc/admin/export.go b/internal/api/grpc/admin/export.go index 8394ae78d9..408a1a59fe 100644 --- a/internal/api/grpc/admin/export.go +++ b/internal/api/grpc/admin/export.go @@ -468,7 +468,7 @@ func (s *Server) getUserLinks(ctx context.Context, orgID string) (_ []*idp_pb.ID if err != nil { return nil, err } - idpUserLinks, err := s.query.IDPUserLinks(ctx, &query.IDPUserLinksSearchQuery{Queries: []query.SearchQuery{userLinksResourceOwner}}, false) + idpUserLinks, err := s.query.IDPUserLinks(ctx, &query.IDPUserLinksSearchQuery{Queries: []query.SearchQuery{userLinksResourceOwner}}, nil) if err != nil { return nil, err } @@ -1063,7 +1063,6 @@ func (s *Server) getCustomLoginTexts(ctx context.Context, org string, languages RegistrationUserText: text_grpc.RegistrationUserScreenTextToPb(text.RegistrationUser), ExternalRegistrationUserOverviewText: text_grpc.ExternalRegistrationUserOverviewScreenTextToPb(text.ExternalRegistrationUserOverview), RegistrationOrgText: text_grpc.RegistrationOrgScreenTextToPb(text.RegistrationOrg), - LinkingUserPromptText: text_grpc.LinkingUserPromptScreenTextToPb(text.LinkingUserPrompt), LinkingUserDoneText: text_grpc.LinkingUserDoneScreenTextToPb(text.LinkingUsersDone), ExternalUserNotFoundText: text_grpc.ExternalUserNotFoundScreenTextToPb(text.ExternalNotFound), SuccessLoginText: text_grpc.SuccessLoginScreenTextToPb(text.LoginSuccess), diff --git a/internal/api/grpc/admin/idp.go b/internal/api/grpc/admin/idp.go index 5528a94dbb..d0907d6e1f 100644 --- a/internal/api/grpc/admin/idp.go +++ b/internal/api/grpc/admin/idp.go @@ -112,7 +112,7 @@ func (s *Server) RemoveIDP(ctx context.Context, req *admin_pb.RemoveIDPRequest) } userLinks, err := s.query.IDPUserLinks(ctx, &query.IDPUserLinksSearchQuery{ Queries: []query.SearchQuery{idpQuery}, - }, true) + }, nil) if err != nil { return nil, err } diff --git a/internal/api/grpc/auth/idp.go b/internal/api/grpc/auth/idp.go index 9d1759ae8b..57018b04c6 100644 --- a/internal/api/grpc/auth/idp.go +++ b/internal/api/grpc/auth/idp.go @@ -13,7 +13,7 @@ func (s *Server) ListMyLinkedIDPs(ctx context.Context, req *auth_pb.ListMyLinked if err != nil { return nil, err } - links, err := s.query.IDPUserLinks(ctx, q, false) + links, err := s.query.IDPUserLinks(ctx, q, nil) if err != nil { return nil, err } diff --git a/internal/api/grpc/auth/multi_factor.go b/internal/api/grpc/auth/multi_factor.go index ed3cf421d5..0771ded35e 100644 --- a/internal/api/grpc/auth/multi_factor.go +++ b/internal/api/grpc/auth/multi_factor.go @@ -26,7 +26,7 @@ func (s *Server) ListMyAuthFactors(ctx context.Context, _ *auth_pb.ListMyAuthFac if err != nil { return nil, err } - authMethods, err := s.query.SearchUserAuthMethods(ctx, query, false) + authMethods, err := s.query.SearchUserAuthMethods(ctx, query, nil) if err != nil { return nil, err } diff --git a/internal/api/grpc/auth/passwordless.go b/internal/api/grpc/auth/passwordless.go index ddc0a331c4..87f310e8d2 100644 --- a/internal/api/grpc/auth/passwordless.go +++ b/internal/api/grpc/auth/passwordless.go @@ -30,7 +30,7 @@ func (s *Server) ListMyPasswordless(ctx context.Context, _ *auth_pb.ListMyPasswo if err != nil { return nil, err } - authMethods, err := s.query.SearchUserAuthMethods(ctx, query, false) + authMethods, err := s.query.SearchUserAuthMethods(ctx, query, nil) if err != nil { return nil, err } diff --git a/internal/api/grpc/feature/v2/converter.go b/internal/api/grpc/feature/v2/converter.go index 3d35694bdd..533baaf534 100644 --- a/internal/api/grpc/feature/v2/converter.go +++ b/internal/api/grpc/feature/v2/converter.go @@ -43,6 +43,7 @@ func instanceFeaturesToCommand(req *feature_pb.SetInstanceFeaturesRequest) *comm Actions: req.Actions, ImprovedPerformance: improvedPerformanceListToDomain(req.ImprovedPerformance), WebKey: req.WebKey, + DebugOIDCParentError: req.DebugOidcParentError, } } @@ -57,6 +58,7 @@ func instanceFeaturesToPb(f *query.InstanceFeatures) *feature_pb.GetInstanceFeat Actions: featureSourceToFlagPb(&f.Actions), ImprovedPerformance: featureSourceToImprovedPerformanceFlagPb(&f.ImprovedPerformance), WebKey: featureSourceToFlagPb(&f.WebKey), + DebugOidcParentError: featureSourceToFlagPb(&f.DebugOIDCParentError), } } diff --git a/internal/api/grpc/feature/v2/converter_test.go b/internal/api/grpc/feature/v2/converter_test.go index e6335145b0..f88b96ea5a 100644 --- a/internal/api/grpc/feature/v2/converter_test.go +++ b/internal/api/grpc/feature/v2/converter_test.go @@ -217,6 +217,10 @@ func Test_instanceFeaturesToPb(t *testing.T) { Enabled: true, Source: feature_pb.Source_SOURCE_INSTANCE, }, + DebugOidcParentError: &feature_pb.FeatureFlag{ + Enabled: false, + Source: feature_pb.Source_SOURCE_UNSPECIFIED, + }, } got := instanceFeaturesToPb(arg) assert.Equal(t, want, got) diff --git a/internal/api/grpc/feature/v2beta/converter.go b/internal/api/grpc/feature/v2beta/converter.go index 16654d1e6b..63738672e6 100644 --- a/internal/api/grpc/feature/v2beta/converter.go +++ b/internal/api/grpc/feature/v2beta/converter.go @@ -43,6 +43,7 @@ func instanceFeaturesToCommand(req *feature_pb.SetInstanceFeaturesRequest) *comm Actions: req.Actions, ImprovedPerformance: improvedPerformanceListToDomain(req.ImprovedPerformance), WebKey: req.WebKey, + DebugOIDCParentError: req.DebugOidcParentError, } } @@ -57,6 +58,7 @@ func instanceFeaturesToPb(f *query.InstanceFeatures) *feature_pb.GetInstanceFeat Actions: featureSourceToFlagPb(&f.Actions), ImprovedPerformance: featureSourceToImprovedPerformanceFlagPb(&f.ImprovedPerformance), WebKey: featureSourceToFlagPb(&f.WebKey), + DebugOidcParentError: featureSourceToFlagPb(&f.DebugOIDCParentError), } } diff --git a/internal/api/grpc/feature/v2beta/converter_test.go b/internal/api/grpc/feature/v2beta/converter_test.go index b8a69f86a8..80be160077 100644 --- a/internal/api/grpc/feature/v2beta/converter_test.go +++ b/internal/api/grpc/feature/v2beta/converter_test.go @@ -217,6 +217,10 @@ func Test_instanceFeaturesToPb(t *testing.T) { Enabled: true, Source: feature_pb.Source_SOURCE_INSTANCE, }, + DebugOidcParentError: &feature_pb.FeatureFlag{ + Enabled: false, + Source: feature_pb.Source_SOURCE_UNSPECIFIED, + }, } got := instanceFeaturesToPb(arg) assert.Equal(t, want, got) diff --git a/internal/api/grpc/management/custom_text_converter.go b/internal/api/grpc/management/custom_text_converter.go index aa5aa05a67..06dfed6a8d 100644 --- a/internal/api/grpc/management/custom_text_converter.go +++ b/internal/api/grpc/management/custom_text_converter.go @@ -171,7 +171,6 @@ func SetLoginCustomTextToDomain(req *mgmt_pb.SetCustomLoginTextsRequest) *domain result.RegistrationUser = text.RegistrationUserScreenTextPbToDomain(req.RegistrationUserText) result.ExternalRegistrationUserOverview = text.ExternalRegistrationUserOverviewScreenTextPbToDomain(req.ExternalRegistrationUserOverviewText) result.RegistrationOrg = text.RegistrationOrgScreenTextPbToDomain(req.RegistrationOrgText) - result.LinkingUserPrompt = text.LinkingUserPromptScreenTextPbToDomain(req.LinkingUserPromptText) result.LinkingUsersDone = text.LinkingUserDoneScreenTextPbToDomain(req.LinkingUserDoneText) result.ExternalNotFound = text.ExternalUserNotFoundScreenTextPbToDomain(req.ExternalUserNotFoundText) result.LoginSuccess = text.SuccessLoginScreenTextPbToDomain(req.SuccessLoginText) diff --git a/internal/api/grpc/management/idp.go b/internal/api/grpc/management/idp.go index 66b659d1ea..9ef7b833a8 100644 --- a/internal/api/grpc/management/idp.go +++ b/internal/api/grpc/management/idp.go @@ -91,7 +91,7 @@ func (s *Server) RemoveOrgIDP(ctx context.Context, req *mgmt_pb.RemoveOrgIDPRequ } userLinks, err := s.query.IDPUserLinks(ctx, &query.IDPUserLinksSearchQuery{ Queries: []query.SearchQuery{idpQuery}, - }, true) + }, nil) if err != nil { return nil, err } diff --git a/internal/api/grpc/management/user.go b/internal/api/grpc/management/user.go index 981e7823ab..dac651af81 100644 --- a/internal/api/grpc/management/user.go +++ b/internal/api/grpc/management/user.go @@ -608,7 +608,7 @@ func (s *Server) ListHumanAuthFactors(ctx context.Context, req *mgmt_pb.ListHuma if err != nil { return nil, err } - authMethods, err := s.query.SearchUserAuthMethods(ctx, query, false) + authMethods, err := s.query.SearchUserAuthMethods(ctx, query, nil) if err != nil { return nil, err } @@ -671,7 +671,7 @@ func (s *Server) ListHumanPasswordless(ctx context.Context, req *mgmt_pb.ListHum if err != nil { return nil, err } - authMethods, err := s.query.SearchUserAuthMethods(ctx, query, false) + authMethods, err := s.query.SearchUserAuthMethods(ctx, query, nil) if err != nil { return nil, err } @@ -892,7 +892,7 @@ func (s *Server) ListHumanLinkedIDPs(ctx context.Context, req *mgmt_pb.ListHuman if err != nil { return nil, err } - res, err := s.query.IDPUserLinks(ctx, queries, false) + res, err := s.query.IDPUserLinks(ctx, queries, nil) if err != nil { return nil, err } diff --git a/internal/api/grpc/resources/user/v3alpha/server.go b/internal/api/grpc/resources/user/v3alpha/server.go new file mode 100644 index 0000000000..e18f017453 --- /dev/null +++ b/internal/api/grpc/resources/user/v3alpha/server.go @@ -0,0 +1,51 @@ +package user + +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/crypto" + user "github.com/zitadel/zitadel/pkg/grpc/resources/user/v3alpha" +) + +var _ user.ZITADELUsersServer = (*Server)(nil) + +type Server struct { + user.UnimplementedZITADELUsersServer + command *command.Commands + userCodeAlg crypto.EncryptionAlgorithm +} + +type Config struct{} + +func CreateServer( + command *command.Commands, + userCodeAlg crypto.EncryptionAlgorithm, +) *Server { + return &Server{ + command: command, + userCodeAlg: userCodeAlg, + } +} + +func (s *Server) RegisterServer(grpcServer *grpc.Server) { + user.RegisterZITADELUsersServer(grpcServer, s) +} + +func (s *Server) AppName() string { + return user.ZITADELUsers_ServiceDesc.ServiceName +} + +func (s *Server) MethodPrefix() string { + return user.ZITADELUsers_ServiceDesc.ServiceName +} + +func (s *Server) AuthMethods() authz.MethodMapping { + return user.ZITADELUsers_AuthMethods +} + +func (s *Server) RegisterGateway() server.RegisterGatewayFunc { + return user.RegisterZITADELUsersHandler +} diff --git a/internal/api/grpc/resources/user/v3alpha/server_integration_test.go b/internal/api/grpc/resources/user/v3alpha/server_integration_test.go new file mode 100644 index 0000000000..9043e7894a --- /dev/null +++ b/internal/api/grpc/resources/user/v3alpha/server_integration_test.go @@ -0,0 +1,72 @@ +//go:build integration + +package user_test + +import ( + "context" + "os" + "testing" + "time" + + "github.com/muhlemmer/gu" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/zitadel/zitadel/internal/integration" + "github.com/zitadel/zitadel/pkg/grpc/feature/v2" + user "github.com/zitadel/zitadel/pkg/grpc/resources/user/v3alpha" +) + +var ( + IAMOwnerCTX, SystemCTX context.Context + UserCTX context.Context + Tester *integration.Tester + Client user.ZITADELUsersClient +) + +func TestMain(m *testing.M) { + os.Exit(func() int { + ctx, _, cancel := integration.Contexts(time.Hour) + defer cancel() + + Tester = integration.NewTester(ctx) + defer Tester.Done() + + IAMOwnerCTX = Tester.WithAuthorization(ctx, integration.IAMOwner) + SystemCTX = Tester.WithAuthorization(ctx, integration.SystemUser) + UserCTX = Tester.WithAuthorization(ctx, integration.Login) + Client = Tester.Client.UserV3Alpha + return m.Run() + }()) +} + +func ensureFeatureEnabled(t *testing.T, iamOwnerCTX context.Context) { + f, err := Tester.Client.FeatureV2.GetInstanceFeatures(iamOwnerCTX, &feature.GetInstanceFeaturesRequest{ + Inheritance: true, + }) + require.NoError(t, err) + if f.UserSchema.GetEnabled() { + return + } + _, err = Tester.Client.FeatureV2.SetInstanceFeatures(iamOwnerCTX, &feature.SetInstanceFeaturesRequest{ + UserSchema: gu.Ptr(true), + }) + require.NoError(t, err) + retryDuration := time.Minute + if ctxDeadline, ok := iamOwnerCTX.Deadline(); ok { + retryDuration = time.Until(ctxDeadline) + } + require.EventuallyWithT(t, + func(ttt *assert.CollectT) { + f, err := Tester.Client.FeatureV2.GetInstanceFeatures(iamOwnerCTX, &feature.GetInstanceFeaturesRequest{ + Inheritance: true, + }) + require.NoError(ttt, err) + if f.UserSchema.GetEnabled() { + return + } + }, + retryDuration, + 100*time.Millisecond, + "timed out waiting for ensuring instance feature") +} diff --git a/internal/api/grpc/resources/user/v3alpha/user.go b/internal/api/grpc/resources/user/v3alpha/user.go new file mode 100644 index 0000000000..ede2f122aa --- /dev/null +++ b/internal/api/grpc/resources/user/v3alpha/user.go @@ -0,0 +1,66 @@ +package user + +import ( + "context" + + "github.com/muhlemmer/gu" + + "github.com/zitadel/zitadel/internal/api/authz" + resource_object "github.com/zitadel/zitadel/internal/api/grpc/resources/object/v3alpha" + "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/zerrors" + object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha" + "github.com/zitadel/zitadel/pkg/grpc/resources/user/v3alpha" +) + +func (s *Server) CreateUser(ctx context.Context, req *user.CreateUserRequest) (_ *user.CreateUserResponse, err error) { + if err := checkUserSchemaEnabled(ctx); err != nil { + return nil, err + } + schemauser, err := createUserRequestToCreateSchemaUser(ctx, req) + if err != nil { + return nil, err + } + + if err := s.command.CreateSchemaUser(ctx, schemauser, s.userCodeAlg); err != nil { + return nil, err + } + return &user.CreateUserResponse{ + Details: resource_object.DomainToDetailsPb(schemauser.Details, object.OwnerType_OWNER_TYPE_ORG, schemauser.ResourceOwner), + EmailCode: gu.Ptr(schemauser.ReturnCodeEmail), + PhoneCode: gu.Ptr(schemauser.ReturnCodePhone), + }, nil +} + +func createUserRequestToCreateSchemaUser(ctx context.Context, req *user.CreateUserRequest) (*command.CreateSchemaUser, error) { + data, err := req.GetUser().GetData().MarshalJSON() + if err != nil { + return nil, err + } + return &command.CreateSchemaUser{ + ResourceOwner: authz.GetCtxData(ctx).OrgID, + SchemaID: req.GetUser().GetSchemaId(), + ID: req.GetUser().GetUserId(), + Data: data, + }, nil +} + +func (s *Server) DeleteUser(ctx context.Context, req *user.DeleteUserRequest) (_ *user.DeleteUserResponse, err error) { + if err := checkUserSchemaEnabled(ctx); err != nil { + return nil, err + } + details, err := s.command.DeleteSchemaUser(ctx, req.GetUserId()) + if err != nil { + return nil, err + } + return &user.DeleteUserResponse{ + Details: resource_object.DomainToDetailsPb(details, object.OwnerType_OWNER_TYPE_ORG, details.ResourceOwner), + }, nil +} + +func checkUserSchemaEnabled(ctx context.Context) error { + if authz.GetInstance(ctx).Features().UserSchema { + return nil + } + return zerrors.ThrowPreconditionFailed(nil, "TODO", "Errors.UserSchema.NotEnabled") +} diff --git a/internal/api/grpc/resources/user/v3alpha/user_integration_test.go b/internal/api/grpc/resources/user/v3alpha/user_integration_test.go new file mode 100644 index 0000000000..bad87d7e56 --- /dev/null +++ b/internal/api/grpc/resources/user/v3alpha/user_integration_test.go @@ -0,0 +1,354 @@ +//go:build integration + +package user_test + +import ( + "context" + "testing" + + "github.com/brianvoe/gofakeit/v6" + "github.com/stretchr/testify/require" + "github.com/zitadel/logging" + "google.golang.org/protobuf/types/known/structpb" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/integration" + object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha" + resource_object "github.com/zitadel/zitadel/pkg/grpc/resources/object/v3alpha" + user "github.com/zitadel/zitadel/pkg/grpc/resources/user/v3alpha" +) + +func TestServer_CreateUser(t *testing.T) { + ensureFeatureEnabled(t, IAMOwnerCTX) + schema := []byte(`{ + "$schema": "urn:zitadel:schema:v1", + "type": "object", + "properties": { + "name": { + "type": "string" + } + } + }`) + schemaResp := Tester.CreateUserSchema(IAMOwnerCTX, schema) + permissionSchema := []byte(`{ + "$schema": "urn:zitadel:schema:v1", + "type": "object", + "properties": { + "name": { + "urn:zitadel:schema:permission": { + "owner": "r", + "self": "r" + }, + "type": "string" + } + } + }`) + permissionSchemaResp := Tester.CreateUserSchema(IAMOwnerCTX, permissionSchema) + orgResp := Tester.CreateOrganization(IAMOwnerCTX, gofakeit.Name(), gofakeit.Email()) + + type res struct { + want *resource_object.Details + returnCodeEmail bool + returnCodePhone bool + } + tests := []struct { + name string + ctx context.Context + req *user.CreateUserRequest + res res + wantErr bool + }{ + { + name: "user create, no schemaID", + ctx: IAMOwnerCTX, + req: &user.CreateUserRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: orgResp.GetOrganizationId(), + }, + }, + User: &user.CreateUser{Data: unmarshalJSON("{\"name\": \"user\"}")}, + }, + wantErr: true, + }, + { + name: "user create, no context", + ctx: context.Background(), + req: &user.CreateUserRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: orgResp.GetOrganizationId(), + }, + }, + User: &user.CreateUser{ + SchemaId: schemaResp.GetDetails().GetId(), + Data: unmarshalJSON("{\"name\": \"user\"}"), + }, + }, + wantErr: true, + }, + { + name: "user create, no permission", + ctx: UserCTX, + req: &user.CreateUserRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: orgResp.GetOrganizationId(), + }, + }, + User: &user.CreateUser{ + SchemaId: schemaResp.GetDetails().GetId(), + Data: unmarshalJSON("{\"name\": \"user\"}"), + }, + }, + wantErr: true, + }, + { + name: "user create, invalid schema permission, owner", + ctx: IAMOwnerCTX, + req: &user.CreateUserRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: orgResp.GetOrganizationId(), + }, + }, + User: &user.CreateUser{ + SchemaId: permissionSchemaResp.GetDetails().GetId(), + Data: unmarshalJSON("{\"name\": \"user\"}"), + }, + }, + wantErr: true, + }, + { + name: "user create, no user data", + ctx: IAMOwnerCTX, + req: &user.CreateUserRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: orgResp.GetOrganizationId(), + }, + }, + User: &user.CreateUser{ + SchemaId: schemaResp.GetDetails().GetId(), + }, + }, + res: res{ + want: &resource_object.Details{ + Changed: timestamppb.Now(), + Owner: &object.Owner{ + Type: object.OwnerType_OWNER_TYPE_ORG, + Id: orgResp.GetOrganizationId(), + }, + }, + }, + }, + { + name: "user create, ok", + ctx: IAMOwnerCTX, + req: &user.CreateUserRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: orgResp.GetOrganizationId(), + }, + }, + User: &user.CreateUser{ + SchemaId: schemaResp.GetDetails().GetId(), + Data: unmarshalJSON("{\"name\": \"user\"}"), + }, + }, + res: res{ + want: &resource_object.Details{ + Changed: timestamppb.Now(), + Owner: &object.Owner{ + Type: object.OwnerType_OWNER_TYPE_ORG, + Id: orgResp.GetOrganizationId(), + }, + }, + }, + }, { + name: "user create, full contact, ok", + ctx: IAMOwnerCTX, + req: &user.CreateUserRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: orgResp.GetOrganizationId(), + }, + }, + User: &user.CreateUser{ + SchemaId: schemaResp.GetDetails().GetId(), + Data: unmarshalJSON("{\"name\": \"user\"}"), + Contact: &user.SetContact{ + Email: &user.SetEmail{ + Address: gofakeit.Email(), + Verification: &user.SetEmail_ReturnCode{ReturnCode: &user.ReturnEmailVerificationCode{}}, + }, + Phone: &user.SetPhone{ + Number: gofakeit.Phone(), + Verification: &user.SetPhone_ReturnCode{ReturnCode: &user.ReturnPhoneVerificationCode{}}, + }, + }, + }, + }, + res: res{ + want: &resource_object.Details{ + Changed: timestamppb.Now(), + Owner: &object.Owner{ + Type: object.OwnerType_OWNER_TYPE_ORG, + Id: orgResp.GetOrganizationId(), + }, + }, + returnCodePhone: true, + returnCodeEmail: true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Tester.Client.UserV3Alpha.CreateUser(tt.ctx, tt.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + integration.AssertResourceDetails(t, tt.res.want, got.Details) + if tt.res.returnCodeEmail { + require.NotNil(t, got.EmailCode) + } + if tt.res.returnCodePhone { + require.NotNil(t, got.PhoneCode) + } + }) + } +} + +func TestServer_DeleteUser(t *testing.T) { + ensureFeatureEnabled(t, IAMOwnerCTX) + schema := []byte(`{ + "$schema": "urn:zitadel:schema:v1", + "type": "object", + "properties": { + "name": { + "type": "string" + } + } + }`) + schemaResp := Tester.CreateUserSchema(IAMOwnerCTX, schema) + orgResp := Tester.CreateOrganization(IAMOwnerCTX, gofakeit.Name(), gofakeit.Email()) + + tests := []struct { + name string + ctx context.Context + dep func(ctx context.Context, req *user.DeleteUserRequest) error + req *user.DeleteUserRequest + want *resource_object.Details + wantErr bool + }{ + { + name: "user delete, no userID", + ctx: IAMOwnerCTX, + req: &user.DeleteUserRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: orgResp.GetOrganizationId(), + }, + }, + UserId: "", + }, + wantErr: true, + }, + { + name: "user delete, not existing", + ctx: IAMOwnerCTX, + req: &user.DeleteUserRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: orgResp.GetOrganizationId(), + }, + }, + UserId: "notexisting", + }, + wantErr: true, + }, + { + name: "user delete, no context", + ctx: context.Background(), + dep: func(ctx context.Context, req *user.DeleteUserRequest) error { + userResp := Tester.CreateSchemaUser(IAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}")) + req.UserId = userResp.GetDetails().GetId() + return nil + }, + req: &user.DeleteUserRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: orgResp.GetOrganizationId(), + }, + }, + }, + wantErr: true, + }, + { + name: "user delete, no permission", + ctx: UserCTX, + dep: func(ctx context.Context, req *user.DeleteUserRequest) error { + userResp := Tester.CreateSchemaUser(IAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}")) + req.UserId = userResp.GetDetails().GetId() + return nil + }, + req: &user.DeleteUserRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: orgResp.GetOrganizationId(), + }, + }, + }, + wantErr: true, + }, + { + name: "user delete, ok", + ctx: IAMOwnerCTX, + dep: func(ctx context.Context, req *user.DeleteUserRequest) error { + userResp := Tester.CreateSchemaUser(ctx, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}")) + req.UserId = userResp.GetDetails().GetId() + return nil + }, + req: &user.DeleteUserRequest{ + Organization: &object.Organization{ + Property: &object.Organization_OrgId{ + OrgId: orgResp.GetOrganizationId(), + }, + }, + }, + want: &resource_object.Details{ + Changed: timestamppb.Now(), + Owner: &object.Owner{ + Type: object.OwnerType_OWNER_TYPE_ORG, + Id: orgResp.GetOrganizationId(), + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.dep != nil { + err := tt.dep(tt.ctx, tt.req) + require.NoError(t, err) + } + got, err := Tester.Client.UserV3Alpha.DeleteUser(tt.ctx, tt.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + integration.AssertResourceDetails(t, tt.want, got.Details) + }) + } +} + +func unmarshalJSON(data string) *structpb.Struct { + user := new(structpb.Struct) + err := user.UnmarshalJSON([]byte(data)) + if err != nil { + logging.OnError(err).Fatal("unmarshalling user json") + } + return user +} diff --git a/internal/api/grpc/resources/userschema/v3alpha/query.go b/internal/api/grpc/resources/userschema/v3alpha/query.go new file mode 100644 index 0000000000..b457c71c9c --- /dev/null +++ b/internal/api/grpc/resources/userschema/v3alpha/query.go @@ -0,0 +1,255 @@ +package userschema + +import ( + "context" + + "google.golang.org/protobuf/types/known/structpb" + + resource_object "github.com/zitadel/zitadel/internal/api/grpc/resources/object/v3alpha" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/query" + "github.com/zitadel/zitadel/internal/zerrors" + object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha" + schema "github.com/zitadel/zitadel/pkg/grpc/resources/userschema/v3alpha" +) + +func (s *Server) SearchUserSchemas(ctx context.Context, req *schema.SearchUserSchemasRequest) (*schema.SearchUserSchemasResponse, error) { + if err := checkUserSchemaEnabled(ctx); err != nil { + return nil, err + } + queries, err := s.searchUserSchemaToModel(req) + if err != nil { + return nil, err + } + res, err := s.query.SearchUserSchema(ctx, queries) + if err != nil { + return nil, err + } + userSchemas, err := userSchemasToPb(res.UserSchemas) + if err != nil { + return nil, err + } + return &schema.SearchUserSchemasResponse{ + Details: resource_object.ToSearchDetailsPb(queries.SearchRequest, res.SearchResponse), + Result: userSchemas, + }, nil +} + +func (s *Server) GetUserSchema(ctx context.Context, req *schema.GetUserSchemaRequest) (*schema.GetUserSchemaResponse, error) { + if err := checkUserSchemaEnabled(ctx); err != nil { + return nil, err + } + res, err := s.query.GetUserSchemaByID(ctx, req.GetId()) + if err != nil { + return nil, err + } + userSchema, err := userSchemaToPb(res) + if err != nil { + return nil, err + } + return &schema.GetUserSchemaResponse{ + UserSchema: userSchema, + }, nil +} + +func (s *Server) searchUserSchemaToModel(req *schema.SearchUserSchemasRequest) (*query.UserSchemaSearchQueries, error) { + offset, limit, asc, err := resource_object.SearchQueryPbToQuery(s.systemDefaults, req.Query) + if err != nil { + return nil, err + } + queries, err := userSchemaFiltersToQuery(req.Filters, 0) // start at level 0 + if err != nil { + return nil, err + } + return &query.UserSchemaSearchQueries{ + SearchRequest: query.SearchRequest{ + Offset: offset, + Limit: limit, + Asc: asc, + SortingColumn: userSchemaFieldNameToSortingColumn(req.SortingColumn), + }, + Queries: queries, + }, nil +} + +func userSchemaFieldNameToSortingColumn(field *schema.FieldName) query.Column { + if field == nil { + return query.UserSchemaCreationDateCol + } + switch *field { + case schema.FieldName_FIELD_NAME_TYPE: + return query.UserSchemaTypeCol + case schema.FieldName_FIELD_NAME_STATE: + return query.UserSchemaStateCol + case schema.FieldName_FIELD_NAME_REVISION: + return query.UserSchemaRevisionCol + case schema.FieldName_FIELD_NAME_CHANGE_DATE: + return query.UserSchemaChangeDateCol + case schema.FieldName_FIELD_NAME_CREATION_DATE: + return query.UserSchemaCreationDateCol + case schema.FieldName_FIELD_NAME_UNSPECIFIED: + return query.UserSchemaIDCol + default: + return query.UserSchemaIDCol + } +} + +func userSchemasToPb(schemas []*query.UserSchema) (_ []*schema.GetUserSchema, err error) { + userSchemas := make([]*schema.GetUserSchema, len(schemas)) + for i, userSchema := range schemas { + userSchemas[i], err = userSchemaToPb(userSchema) + if err != nil { + return nil, err + } + } + return userSchemas, nil +} + +func userSchemaToPb(userSchema *query.UserSchema) (*schema.GetUserSchema, error) { + s := new(structpb.Struct) + if err := s.UnmarshalJSON(userSchema.Schema); err != nil { + return nil, err + } + return &schema.GetUserSchema{ + Details: resource_object.DomainToDetailsPb(&userSchema.ObjectDetails, object.OwnerType_OWNER_TYPE_INSTANCE, userSchema.ResourceOwner), + Config: &schema.UserSchema{ + Type: userSchema.Type, + DataType: &schema.UserSchema_Schema{ + Schema: s, + }, + PossibleAuthenticators: authenticatorTypesToPb(userSchema.PossibleAuthenticators), + }, + State: userSchemaStateToPb(userSchema.State), + Revision: userSchema.Revision, + }, nil +} + +func authenticatorTypesToPb(authenticators []domain.AuthenticatorType) []schema.AuthenticatorType { + authTypes := make([]schema.AuthenticatorType, len(authenticators)) + for i, authenticator := range authenticators { + authTypes[i] = authenticatorTypeToPb(authenticator) + } + return authTypes +} + +func authenticatorTypeToPb(authenticator domain.AuthenticatorType) schema.AuthenticatorType { + switch authenticator { + case domain.AuthenticatorTypeUsername: + return schema.AuthenticatorType_AUTHENTICATOR_TYPE_USERNAME + case domain.AuthenticatorTypePassword: + return schema.AuthenticatorType_AUTHENTICATOR_TYPE_PASSWORD + case domain.AuthenticatorTypeWebAuthN: + return schema.AuthenticatorType_AUTHENTICATOR_TYPE_WEBAUTHN + case domain.AuthenticatorTypeTOTP: + return schema.AuthenticatorType_AUTHENTICATOR_TYPE_TOTP + case domain.AuthenticatorTypeOTPEmail: + return schema.AuthenticatorType_AUTHENTICATOR_TYPE_OTP_EMAIL + case domain.AuthenticatorTypeOTPSMS: + return schema.AuthenticatorType_AUTHENTICATOR_TYPE_OTP_SMS + case domain.AuthenticatorTypeAuthenticationKey: + return schema.AuthenticatorType_AUTHENTICATOR_TYPE_AUTHENTICATION_KEY + case domain.AuthenticatorTypeIdentityProvider: + return schema.AuthenticatorType_AUTHENTICATOR_TYPE_IDENTITY_PROVIDER + case domain.AuthenticatorTypeUnspecified: + return schema.AuthenticatorType_AUTHENTICATOR_TYPE_UNSPECIFIED + default: + return schema.AuthenticatorType_AUTHENTICATOR_TYPE_UNSPECIFIED + } +} + +func userSchemaStateToPb(state domain.UserSchemaState) schema.State { + switch state { + case domain.UserSchemaStateActive: + return schema.State_STATE_ACTIVE + case domain.UserSchemaStateInactive: + return schema.State_STATE_INACTIVE + case domain.UserSchemaStateUnspecified, + domain.UserSchemaStateDeleted: + return schema.State_STATE_UNSPECIFIED + default: + return schema.State_STATE_UNSPECIFIED + } +} + +func userSchemaStateToDomain(state schema.State) domain.UserSchemaState { + switch state { + case schema.State_STATE_ACTIVE: + return domain.UserSchemaStateActive + case schema.State_STATE_INACTIVE: + return domain.UserSchemaStateInactive + case schema.State_STATE_UNSPECIFIED: + return domain.UserSchemaStateUnspecified + default: + return domain.UserSchemaStateUnspecified + } +} + +func userSchemaFiltersToQuery(queries []*schema.SearchFilter, level uint8) (_ []query.SearchQuery, err error) { + q := make([]query.SearchQuery, len(queries)) + for i, query := range queries { + q[i], err = userSchemaFilterToQuery(query, level) + if err != nil { + return nil, err + } + } + return q, nil +} + +func userSchemaFilterToQuery(query *schema.SearchFilter, level uint8) (query.SearchQuery, error) { + if level > 20 { + // can't go deeper than 20 levels of nesting. + return nil, zerrors.ThrowInvalidArgument(nil, "SCHEMA-zsQ97", "Errors.Query.TooManyNestingLevels") + } + switch q := query.Filter.(type) { + case *schema.SearchFilter_StateFilter: + return stateQueryToQuery(q.StateFilter) + case *schema.SearchFilter_TypeFilter: + return typeQueryToQuery(q.TypeFilter) + case *schema.SearchFilter_IdFilter: + return idQueryToQuery(q.IdFilter) + case *schema.SearchFilter_OrFilter: + return orQueryToQuery(q.OrFilter, level) + case *schema.SearchFilter_AndFilter: + return andQueryToQuery(q.AndFilter, level) + case *schema.SearchFilter_NotFilter: + return notQueryToQuery(q.NotFilter, level) + default: + return nil, zerrors.ThrowInvalidArgument(nil, "SCHEMA-vR9nC", "List.Query.Invalid") + } +} + +func stateQueryToQuery(q *schema.StateFilter) (query.SearchQuery, error) { + return query.NewUserSchemaStateSearchQuery(userSchemaStateToDomain(q.GetState())) +} + +func typeQueryToQuery(q *schema.TypeFilter) (query.SearchQuery, error) { + return query.NewUserSchemaTypeSearchQuery(q.GetType(), resource_object.TextMethodPbToQuery(q.GetMethod())) +} + +func idQueryToQuery(q *schema.IDFilter) (query.SearchQuery, error) { + return query.NewUserSchemaIDSearchQuery(q.GetId(), resource_object.TextMethodPbToQuery(q.GetMethod())) +} + +func orQueryToQuery(q *schema.OrFilter, level uint8) (query.SearchQuery, error) { + mappedQueries, err := userSchemaFiltersToQuery(q.GetQueries(), level+1) + if err != nil { + return nil, err + } + return query.NewUserOrSearchQuery(mappedQueries) +} + +func andQueryToQuery(q *schema.AndFilter, level uint8) (query.SearchQuery, error) { + mappedQueries, err := userSchemaFiltersToQuery(q.GetQueries(), level+1) + if err != nil { + return nil, err + } + return query.NewUserAndSearchQuery(mappedQueries) +} + +func notQueryToQuery(q *schema.NotFilter, level uint8) (query.SearchQuery, error) { + mappedQuery, err := userSchemaFilterToQuery(q.GetFilter(), level+1) + if err != nil { + return nil, err + } + return query.NewUserNotSearchQuery(mappedQuery) +} diff --git a/internal/api/grpc/resources/userschema/v3alpha/query_integration_test.go b/internal/api/grpc/resources/userschema/v3alpha/query_integration_test.go new file mode 100644 index 0000000000..38e9317872 --- /dev/null +++ b/internal/api/grpc/resources/userschema/v3alpha/query_integration_test.go @@ -0,0 +1,317 @@ +//go:build integration + +package userschema_test + +import ( + "context" + "testing" + "time" + + "github.com/brianvoe/gofakeit/v6" + "github.com/muhlemmer/gu" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/structpb" + + "github.com/zitadel/zitadel/internal/api/grpc" + "github.com/zitadel/zitadel/internal/integration" + object "github.com/zitadel/zitadel/pkg/grpc/resources/object/v3alpha" + schema "github.com/zitadel/zitadel/pkg/grpc/resources/userschema/v3alpha" +) + +func TestServer_ListUserSchemas(t *testing.T) { + ensureFeatureEnabled(t, IAMOwnerCTX) + + userSchema := new(structpb.Struct) + err := userSchema.UnmarshalJSON([]byte(`{ + "$schema": "urn:zitadel:schema:v1", + "type": "object", + "properties": {} + }`)) + require.NoError(t, err) + type args struct { + ctx context.Context + req *schema.SearchUserSchemasRequest + prepare func(request *schema.SearchUserSchemasRequest, resp *schema.SearchUserSchemasResponse) error + } + tests := []struct { + name string + args args + want *schema.SearchUserSchemasResponse + wantErr bool + }{ + { + name: "missing permission", + args: args{ + ctx: Tester.WithAuthorization(context.Background(), integration.OrgOwner), + req: &schema.SearchUserSchemasRequest{}, + }, + wantErr: true, + }, + { + name: "not found, error", + args: args{ + ctx: IAMOwnerCTX, + req: &schema.SearchUserSchemasRequest{ + Filters: []*schema.SearchFilter{ + { + Filter: &schema.SearchFilter_IdFilter{ + IdFilter: &schema.IDFilter{ + Id: "notexisting", + }, + }, + }, + }, + }, + }, + want: &schema.SearchUserSchemasResponse{ + Details: &object.ListDetails{ + TotalResult: 0, + AppliedLimit: 100, + }, + Result: []*schema.GetUserSchema{}, + }, + }, + { + name: "single (id), ok", + args: args{ + ctx: IAMOwnerCTX, + req: &schema.SearchUserSchemasRequest{}, + prepare: func(request *schema.SearchUserSchemasRequest, resp *schema.SearchUserSchemasResponse) error { + schemaType := gofakeit.Name() + createResp := Tester.CreateUserSchemaEmptyWithType(IAMOwnerCTX, schemaType) + request.Filters = []*schema.SearchFilter{ + { + Filter: &schema.SearchFilter_IdFilter{ + IdFilter: &schema.IDFilter{ + Id: createResp.GetDetails().GetId(), + Method: object.TextFilterMethod_TEXT_FILTER_METHOD_EQUALS, + }, + }, + }, + } + resp.Result[0].Config.Type = schemaType + resp.Result[0].Details = createResp.GetDetails() + // as schema is freshly created, the changed date is the created date + resp.Result[0].Details.Created = resp.Result[0].Details.GetChanged() + resp.Details.Timestamp = resp.Result[0].Details.GetChanged() + return nil + }, + }, + want: &schema.SearchUserSchemasResponse{ + Details: &object.ListDetails{ + TotalResult: 1, + AppliedLimit: 100, + }, + Result: []*schema.GetUserSchema{ + { + State: schema.State_STATE_ACTIVE, + Revision: 1, + Config: &schema.UserSchema{ + Type: "", + DataType: &schema.UserSchema_Schema{ + Schema: userSchema, + }, + PossibleAuthenticators: nil, + }, + }, + }, + }, + }, + { + name: "multiple (type), ok", + args: args{ + ctx: IAMOwnerCTX, + req: &schema.SearchUserSchemasRequest{}, + prepare: func(request *schema.SearchUserSchemasRequest, resp *schema.SearchUserSchemasResponse) error { + schemaType := gofakeit.Name() + schemaType1 := schemaType + "_1" + schemaType2 := schemaType + "_2" + createResp := Tester.CreateUserSchemaEmptyWithType(IAMOwnerCTX, schemaType1) + createResp2 := Tester.CreateUserSchemaEmptyWithType(IAMOwnerCTX, schemaType2) + + request.SortingColumn = gu.Ptr(schema.FieldName_FIELD_NAME_TYPE) + request.Query = &object.SearchQuery{Desc: false} + request.Filters = []*schema.SearchFilter{ + { + Filter: &schema.SearchFilter_TypeFilter{ + TypeFilter: &schema.TypeFilter{ + Type: schemaType, + Method: object.TextFilterMethod_TEXT_FILTER_METHOD_STARTS_WITH, + }, + }, + }, + } + + resp.Result[0].Config.Type = schemaType1 + resp.Result[0].Details = createResp.GetDetails() + resp.Result[1].Config.Type = schemaType2 + resp.Result[1].Details = createResp2.GetDetails() + return nil + }, + }, + want: &schema.SearchUserSchemasResponse{ + Details: &object.ListDetails{ + TotalResult: 2, + AppliedLimit: 100, + }, + Result: []*schema.GetUserSchema{ + { + State: schema.State_STATE_ACTIVE, + Revision: 1, + Config: &schema.UserSchema{ + DataType: &schema.UserSchema_Schema{ + Schema: userSchema, + }, + }, + }, + { + State: schema.State_STATE_ACTIVE, + Revision: 1, + Config: &schema.UserSchema{ + DataType: &schema.UserSchema_Schema{ + Schema: userSchema, + }, + }, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.args.prepare != nil { + err := tt.args.prepare(tt.args.req, tt.want) + require.NoError(t, err) + } + + retryDuration := 20 * time.Second + if ctxDeadline, ok := IAMOwnerCTX.Deadline(); ok { + retryDuration = time.Until(ctxDeadline) + } + + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + got, err := Client.SearchUserSchemas(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(ttt, err) + return + } + assert.NoError(ttt, err) + + // always first check length, otherwise its failed anyway + assert.Len(ttt, got.Result, len(tt.want.Result)) + for i := range tt.want.Result { + want := tt.want.Result[i] + got := got.Result[i] + + integration.AssertResourceDetails(t, want.GetDetails(), got.GetDetails()) + want.Details = got.Details + grpc.AllFieldsEqual(t, want.ProtoReflect(), got.ProtoReflect(), grpc.CustomMappers) + } + integration.AssertListDetails(t, tt.want, got) + }, retryDuration, time.Millisecond*100, "timeout waiting for expected user schema result") + }) + } +} + +func TestServer_GetUserSchema(t *testing.T) { + ensureFeatureEnabled(t, IAMOwnerCTX) + + userSchema := new(structpb.Struct) + err := userSchema.UnmarshalJSON([]byte(`{ + "$schema": "urn:zitadel:schema:v1", + "type": "object", + "properties": {} + }`)) + require.NoError(t, err) + type args struct { + ctx context.Context + req *schema.GetUserSchemaRequest + prepare func(request *schema.GetUserSchemaRequest, resp *schema.GetUserSchemaResponse) error + } + tests := []struct { + name string + args args + want *schema.GetUserSchemaResponse + wantErr bool + }{ + { + name: "missing permission", + args: args{ + ctx: Tester.WithAuthorization(context.Background(), integration.OrgOwner), + req: &schema.GetUserSchemaRequest{}, + prepare: func(request *schema.GetUserSchemaRequest, resp *schema.GetUserSchemaResponse) error { + schemaType := gofakeit.Name() + createResp := Tester.CreateUserSchemaEmptyWithType(IAMOwnerCTX, schemaType) + request.Id = createResp.GetDetails().GetId() + return nil + }, + }, + wantErr: true, + }, + { + name: "not existing, error", + args: args{ + ctx: IAMOwnerCTX, + req: &schema.GetUserSchemaRequest{ + Id: "notexisting", + }, + }, + wantErr: true, + }, + { + name: "get, ok", + args: args{ + ctx: IAMOwnerCTX, + req: &schema.GetUserSchemaRequest{}, + prepare: func(request *schema.GetUserSchemaRequest, resp *schema.GetUserSchemaResponse) error { + schemaType := gofakeit.Name() + createResp := Tester.CreateUserSchemaEmptyWithType(IAMOwnerCTX, schemaType) + request.Id = createResp.GetDetails().GetId() + + resp.UserSchema.Config.Type = schemaType + resp.UserSchema.Details = createResp.GetDetails() + return nil + }, + }, + want: &schema.GetUserSchemaResponse{ + UserSchema: &schema.GetUserSchema{ + State: schema.State_STATE_ACTIVE, + Revision: 1, + Config: &schema.UserSchema{ + DataType: &schema.UserSchema_Schema{ + Schema: userSchema, + }, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.args.prepare != nil { + err := tt.args.prepare(tt.args.req, tt.want) + require.NoError(t, err) + } + + retryDuration := 5 * time.Second + if ctxDeadline, ok := IAMOwnerCTX.Deadline(); ok { + retryDuration = time.Until(ctxDeadline) + } + + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + got, err := Client.GetUserSchema(tt.args.ctx, tt.args.req) + if tt.wantErr { + assert.Error(t, err, "Error: "+err.Error()) + } else { + assert.NoError(t, err) + wantSchema := tt.want.GetUserSchema() + gotSchema := got.GetUserSchema() + integration.AssertResourceDetails(t, wantSchema.GetDetails(), gotSchema.GetDetails()) + tt.want.UserSchema.Details = got.GetUserSchema().GetDetails() + grpc.AllFieldsEqual(t, tt.want.ProtoReflect(), got.ProtoReflect(), grpc.CustomMappers) + } + }, retryDuration, time.Millisecond*100, "timeout waiting for expected user schema result") + }) + } +} diff --git a/internal/api/grpc/resources/userschema/v3alpha/server.go b/internal/api/grpc/resources/userschema/v3alpha/server.go new file mode 100644 index 0000000000..1674a0838a --- /dev/null +++ b/internal/api/grpc/resources/userschema/v3alpha/server.go @@ -0,0 +1,65 @@ +package userschema + +import ( + "context" + + "google.golang.org/grpc" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/api/grpc/server" + "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/config/systemdefaults" + "github.com/zitadel/zitadel/internal/query" + "github.com/zitadel/zitadel/internal/zerrors" + schema "github.com/zitadel/zitadel/pkg/grpc/resources/userschema/v3alpha" +) + +var _ schema.ZITADELUserSchemasServer = (*Server)(nil) + +type Server struct { + schema.UnimplementedZITADELUserSchemasServer + systemDefaults systemdefaults.SystemDefaults + command *command.Commands + query *query.Queries +} + +type Config struct{} + +func CreateServer( + systemDefaults systemdefaults.SystemDefaults, + command *command.Commands, + query *query.Queries, +) *Server { + return &Server{ + systemDefaults: systemDefaults, + command: command, + query: query, + } +} + +func (s *Server) RegisterServer(grpcServer *grpc.Server) { + schema.RegisterZITADELUserSchemasServer(grpcServer, s) +} + +func (s *Server) AppName() string { + return schema.ZITADELUserSchemas_ServiceDesc.ServiceName +} + +func (s *Server) MethodPrefix() string { + return schema.ZITADELUserSchemas_ServiceDesc.ServiceName +} + +func (s *Server) AuthMethods() authz.MethodMapping { + return schema.ZITADELUserSchemas_AuthMethods +} + +func (s *Server) RegisterGateway() server.RegisterGatewayFunc { + return schema.RegisterZITADELUserSchemasHandler +} + +func checkUserSchemaEnabled(ctx context.Context) error { + if authz.GetInstance(ctx).Features().UserSchema { + return nil + } + return zerrors.ThrowPreconditionFailed(nil, "SCHEMA-SFjk3", "Errors.UserSchema.NotEnabled") +} diff --git a/internal/api/grpc/resources/userschema/v3alpha/server_integration_test.go b/internal/api/grpc/resources/userschema/v3alpha/server_integration_test.go new file mode 100644 index 0000000000..7dbdd04cbb --- /dev/null +++ b/internal/api/grpc/resources/userschema/v3alpha/server_integration_test.go @@ -0,0 +1,71 @@ +//go:build integration + +package userschema_test + +import ( + "context" + "os" + "testing" + "time" + + "github.com/muhlemmer/gu" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/zitadel/zitadel/internal/integration" + "github.com/zitadel/zitadel/pkg/grpc/feature/v2" + schema "github.com/zitadel/zitadel/pkg/grpc/resources/userschema/v3alpha" +) + +var ( + IAMOwnerCTX, SystemCTX context.Context + Tester *integration.Tester + Client schema.ZITADELUserSchemasClient +) + +func TestMain(m *testing.M) { + os.Exit(func() int { + ctx, _, cancel := integration.Contexts(5 * time.Minute) + defer cancel() + + Tester = integration.NewTester(ctx) + defer Tester.Done() + + IAMOwnerCTX = Tester.WithAuthorization(ctx, integration.IAMOwner) + SystemCTX = Tester.WithAuthorization(ctx, integration.SystemUser) + Client = Tester.Client.UserSchemaV3 + + return m.Run() + }()) +} + +func ensureFeatureEnabled(t *testing.T, iamOwnerCTX context.Context) { + f, err := Tester.Client.FeatureV2.GetInstanceFeatures(iamOwnerCTX, &feature.GetInstanceFeaturesRequest{ + Inheritance: true, + }) + require.NoError(t, err) + if f.UserSchema.GetEnabled() { + return + } + _, err = Tester.Client.FeatureV2.SetInstanceFeatures(iamOwnerCTX, &feature.SetInstanceFeaturesRequest{ + UserSchema: gu.Ptr(true), + }) + require.NoError(t, err) + retryDuration := time.Minute + if ctxDeadline, ok := iamOwnerCTX.Deadline(); ok { + retryDuration = time.Until(ctxDeadline) + } + require.EventuallyWithT(t, + func(ttt *assert.CollectT) { + f, err := Tester.Client.FeatureV2.GetInstanceFeatures(iamOwnerCTX, &feature.GetInstanceFeaturesRequest{ + Inheritance: true, + }) + require.NoError(ttt, err) + if f.UserSchema.GetEnabled() { + return + } + }, + retryDuration, + 100*time.Millisecond, + "timed out waiting for ensuring instance feature") +} diff --git a/internal/api/grpc/resources/userschema/v3alpha/userschema.go b/internal/api/grpc/resources/userschema/v3alpha/userschema.go new file mode 100644 index 0000000000..7044c79c51 --- /dev/null +++ b/internal/api/grpc/resources/userschema/v3alpha/userschema.go @@ -0,0 +1,159 @@ +package userschema + +import ( + "context" + + "github.com/muhlemmer/gu" + + "github.com/zitadel/zitadel/internal/api/authz" + resource_object "github.com/zitadel/zitadel/internal/api/grpc/resources/object/v3alpha" + "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/domain" + object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha" + schema "github.com/zitadel/zitadel/pkg/grpc/resources/userschema/v3alpha" +) + +func (s *Server) CreateUserSchema(ctx context.Context, req *schema.CreateUserSchemaRequest) (*schema.CreateUserSchemaResponse, error) { + if err := checkUserSchemaEnabled(ctx); err != nil { + return nil, err + } + instanceID := authz.GetInstance(ctx).InstanceID() + userSchema, err := createUserSchemaToCommand(req, instanceID) + if err != nil { + return nil, err + } + + if err := s.command.CreateUserSchema(ctx, userSchema); err != nil { + return nil, err + } + return &schema.CreateUserSchemaResponse{ + Details: resource_object.DomainToDetailsPb(userSchema.Details, object.OwnerType_OWNER_TYPE_INSTANCE, instanceID), + }, nil +} + +func (s *Server) PatchUserSchema(ctx context.Context, req *schema.PatchUserSchemaRequest) (*schema.PatchUserSchemaResponse, error) { + if err := checkUserSchemaEnabled(ctx); err != nil { + return nil, err + } + instanceID := authz.GetInstance(ctx).InstanceID() + userSchema, err := patchUserSchemaToCommand(req, instanceID) + if err != nil { + return nil, err + } + if err := s.command.ChangeUserSchema(ctx, userSchema); err != nil { + return nil, err + } + return &schema.PatchUserSchemaResponse{ + Details: resource_object.DomainToDetailsPb(userSchema.Details, object.OwnerType_OWNER_TYPE_INSTANCE, instanceID), + }, nil +} + +func (s *Server) DeactivateUserSchema(ctx context.Context, req *schema.DeactivateUserSchemaRequest) (*schema.DeactivateUserSchemaResponse, error) { + if err := checkUserSchemaEnabled(ctx); err != nil { + return nil, err + } + instanceID := authz.GetInstance(ctx).InstanceID() + details, err := s.command.DeactivateUserSchema(ctx, req.GetId(), instanceID) + if err != nil { + return nil, err + } + return &schema.DeactivateUserSchemaResponse{ + Details: resource_object.DomainToDetailsPb(details, object.OwnerType_OWNER_TYPE_INSTANCE, instanceID), + }, nil +} + +func (s *Server) ReactivateUserSchema(ctx context.Context, req *schema.ReactivateUserSchemaRequest) (*schema.ReactivateUserSchemaResponse, error) { + if err := checkUserSchemaEnabled(ctx); err != nil { + return nil, err + } + instanceID := authz.GetInstance(ctx).InstanceID() + details, err := s.command.ReactivateUserSchema(ctx, req.GetId(), instanceID) + if err != nil { + return nil, err + } + return &schema.ReactivateUserSchemaResponse{ + Details: resource_object.DomainToDetailsPb(details, object.OwnerType_OWNER_TYPE_INSTANCE, instanceID), + }, nil +} + +func (s *Server) DeleteUserSchema(ctx context.Context, req *schema.DeleteUserSchemaRequest) (*schema.DeleteUserSchemaResponse, error) { + if err := checkUserSchemaEnabled(ctx); err != nil { + return nil, err + } + instanceID := authz.GetInstance(ctx).InstanceID() + details, err := s.command.DeleteUserSchema(ctx, req.GetId(), instanceID) + if err != nil { + return nil, err + } + return &schema.DeleteUserSchemaResponse{ + Details: resource_object.DomainToDetailsPb(details, object.OwnerType_OWNER_TYPE_INSTANCE, instanceID), + }, nil +} + +func createUserSchemaToCommand(req *schema.CreateUserSchemaRequest, resourceOwner string) (*command.CreateUserSchema, error) { + schema, err := req.GetUserSchema().GetSchema().MarshalJSON() + if err != nil { + return nil, err + } + return &command.CreateUserSchema{ + ResourceOwner: resourceOwner, + Type: req.GetUserSchema().GetType(), + Schema: schema, + PossibleAuthenticators: authenticatorsToDomain(req.GetUserSchema().GetPossibleAuthenticators()), + }, nil +} + +func patchUserSchemaToCommand(req *schema.PatchUserSchemaRequest, resourceOwner string) (*command.ChangeUserSchema, error) { + schema, err := req.GetUserSchema().GetSchema().MarshalJSON() + if err != nil { + return nil, err + } + + var ty *string + if req.GetUserSchema() != nil && req.GetUserSchema().GetType() != "" { + ty = gu.Ptr(req.GetUserSchema().GetType()) + } + return &command.ChangeUserSchema{ + ID: req.GetId(), + ResourceOwner: resourceOwner, + Type: ty, + Schema: schema, + PossibleAuthenticators: authenticatorsToDomain(req.GetUserSchema().GetPossibleAuthenticators()), + }, nil +} + +func authenticatorsToDomain(authenticators []schema.AuthenticatorType) []domain.AuthenticatorType { + if authenticators == nil { + return nil + } + types := make([]domain.AuthenticatorType, len(authenticators)) + for i, authenticator := range authenticators { + types[i] = authenticatorTypeToDomain(authenticator) + } + return types +} + +func authenticatorTypeToDomain(authenticator schema.AuthenticatorType) domain.AuthenticatorType { + switch authenticator { + case schema.AuthenticatorType_AUTHENTICATOR_TYPE_UNSPECIFIED: + return domain.AuthenticatorTypeUnspecified + case schema.AuthenticatorType_AUTHENTICATOR_TYPE_USERNAME: + return domain.AuthenticatorTypeUsername + case schema.AuthenticatorType_AUTHENTICATOR_TYPE_PASSWORD: + return domain.AuthenticatorTypePassword + case schema.AuthenticatorType_AUTHENTICATOR_TYPE_WEBAUTHN: + return domain.AuthenticatorTypeWebAuthN + case schema.AuthenticatorType_AUTHENTICATOR_TYPE_TOTP: + return domain.AuthenticatorTypeTOTP + case schema.AuthenticatorType_AUTHENTICATOR_TYPE_OTP_EMAIL: + return domain.AuthenticatorTypeOTPEmail + case schema.AuthenticatorType_AUTHENTICATOR_TYPE_OTP_SMS: + return domain.AuthenticatorTypeOTPSMS + case schema.AuthenticatorType_AUTHENTICATOR_TYPE_AUTHENTICATION_KEY: + return domain.AuthenticatorTypeAuthenticationKey + case schema.AuthenticatorType_AUTHENTICATOR_TYPE_IDENTITY_PROVIDER: + return domain.AuthenticatorTypeIdentityProvider + default: + return domain.AuthenticatorTypeUnspecified + } +} diff --git a/internal/api/grpc/resources/userschema/v3alpha/userschema_integration_test.go b/internal/api/grpc/resources/userschema/v3alpha/userschema_integration_test.go new file mode 100644 index 0000000000..f907d0f56e --- /dev/null +++ b/internal/api/grpc/resources/userschema/v3alpha/userschema_integration_test.go @@ -0,0 +1,827 @@ +//go:build integration + +package userschema_test + +import ( + "context" + "testing" + + "github.com/brianvoe/gofakeit/v6" + "github.com/muhlemmer/gu" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/structpb" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/integration" + object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha" + resource_object "github.com/zitadel/zitadel/pkg/grpc/resources/object/v3alpha" + schema "github.com/zitadel/zitadel/pkg/grpc/resources/userschema/v3alpha" +) + +func TestServer_CreateUserSchema(t *testing.T) { + ensureFeatureEnabled(t, IAMOwnerCTX) + + tests := []struct { + name string + ctx context.Context + req *schema.CreateUserSchemaRequest + want *schema.CreateUserSchemaResponse + wantErr bool + }{ + { + name: "missing permission, error", + ctx: Tester.WithAuthorization(context.Background(), integration.OrgOwner), + req: &schema.CreateUserSchemaRequest{ + UserSchema: &schema.UserSchema{ + Type: gofakeit.Name(), + }, + }, + wantErr: true, + }, + { + name: "empty type", + ctx: IAMOwnerCTX, + req: &schema.CreateUserSchemaRequest{ + UserSchema: &schema.UserSchema{ + Type: "", + }, + }, + wantErr: true, + }, + { + name: "empty schema, error", + ctx: IAMOwnerCTX, + req: &schema.CreateUserSchemaRequest{ + UserSchema: &schema.UserSchema{ + Type: gofakeit.Name(), + }, + }, + wantErr: true, + }, + { + name: "invalid schema, error", + ctx: IAMOwnerCTX, + req: &schema.CreateUserSchemaRequest{ + UserSchema: &schema.UserSchema{ + Type: gofakeit.Name(), + DataType: &schema.UserSchema_Schema{ + Schema: func() *structpb.Struct { + s := new(structpb.Struct) + err := s.UnmarshalJSON([]byte(` + { + "type": "object", + "properties": { + "name": { + "type": "string", + "required": true + }, + "description": { + "type": "string" + } + } + } + `)) + require.NoError(t, err) + return s + }(), + }, + }, + }, + wantErr: true, + }, + { + name: "no authenticators, ok", + ctx: IAMOwnerCTX, + req: &schema.CreateUserSchemaRequest{ + UserSchema: &schema.UserSchema{ + Type: gofakeit.Name(), + DataType: &schema.UserSchema_Schema{ + Schema: func() *structpb.Struct { + s := new(structpb.Struct) + err := s.UnmarshalJSON([]byte(` + { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string" + } + }, + "required": ["name"] + } + `)) + require.NoError(t, err) + return s + }(), + }, + }, + }, + want: &schema.CreateUserSchemaResponse{ + Details: &resource_object.Details{ + Changed: timestamppb.Now(), + Owner: &object.Owner{ + Type: object.OwnerType_OWNER_TYPE_INSTANCE, + Id: Tester.Instance.InstanceID(), + }, + }, + }, + }, + { + name: "invalid authenticator, error", + ctx: IAMOwnerCTX, + req: &schema.CreateUserSchemaRequest{ + UserSchema: &schema.UserSchema{ + Type: gofakeit.Name(), + DataType: &schema.UserSchema_Schema{ + Schema: func() *structpb.Struct { + s := new(structpb.Struct) + err := s.UnmarshalJSON([]byte(` + { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string" + } + }, + "required": ["name"] + } + `)) + require.NoError(t, err) + return s + }(), + }, + PossibleAuthenticators: []schema.AuthenticatorType{ + schema.AuthenticatorType_AUTHENTICATOR_TYPE_UNSPECIFIED, + }, + }, + }, + wantErr: true, + }, + { + name: "with authenticator, ok", + ctx: IAMOwnerCTX, + req: &schema.CreateUserSchemaRequest{ + UserSchema: &schema.UserSchema{ + Type: gofakeit.Name(), + DataType: &schema.UserSchema_Schema{ + Schema: func() *structpb.Struct { + s := new(structpb.Struct) + err := s.UnmarshalJSON([]byte(` + { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string" + } + }, + "required": ["name"] + } + `)) + require.NoError(t, err) + return s + }(), + }, + PossibleAuthenticators: []schema.AuthenticatorType{ + schema.AuthenticatorType_AUTHENTICATOR_TYPE_USERNAME, + }, + }, + }, + want: &schema.CreateUserSchemaResponse{ + Details: &resource_object.Details{ + Changed: timestamppb.Now(), + Owner: &object.Owner{ + Type: object.OwnerType_OWNER_TYPE_INSTANCE, + Id: Tester.Instance.InstanceID(), + }, + }, + }, + }, + { + name: "with invalid permission, error", + ctx: IAMOwnerCTX, + req: &schema.CreateUserSchemaRequest{ + UserSchema: &schema.UserSchema{ + Type: gofakeit.Name(), + DataType: &schema.UserSchema_Schema{ + Schema: func() *structpb.Struct { + s := new(structpb.Struct) + err := s.UnmarshalJSON([]byte(` + { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string", + "urn:zitadel:schema:permission": "read" + } + }, + "required": ["name"] + } + `)) + require.NoError(t, err) + return s + }(), + }, + PossibleAuthenticators: []schema.AuthenticatorType{ + schema.AuthenticatorType_AUTHENTICATOR_TYPE_USERNAME, + }, + }, + }, + wantErr: true, + }, + { + name: "with valid permission, ok", + ctx: IAMOwnerCTX, + req: &schema.CreateUserSchemaRequest{ + UserSchema: &schema.UserSchema{ + Type: gofakeit.Name(), + DataType: &schema.UserSchema_Schema{ + Schema: func() *structpb.Struct { + s := new(structpb.Struct) + err := s.UnmarshalJSON([]byte(` + { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string", + "urn:zitadel:schema:permission": { + "owner": "rw", + "self": "r" + } + } + }, + "required": ["name"] + } + `)) + require.NoError(t, err) + return s + }(), + }, + PossibleAuthenticators: []schema.AuthenticatorType{ + schema.AuthenticatorType_AUTHENTICATOR_TYPE_USERNAME, + }, + }, + }, + want: &schema.CreateUserSchemaResponse{ + Details: &resource_object.Details{ + Changed: timestamppb.Now(), + Owner: &object.Owner{ + Type: object.OwnerType_OWNER_TYPE_INSTANCE, + Id: Tester.Instance.InstanceID(), + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Client.CreateUserSchema(tt.ctx, tt.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + + integration.AssertResourceDetails(t, tt.want.GetDetails(), got.GetDetails()) + }) + } +} + +func TestServer_UpdateUserSchema(t *testing.T) { + ensureFeatureEnabled(t, IAMOwnerCTX) + + type args struct { + ctx context.Context + req *schema.PatchUserSchemaRequest + } + tests := []struct { + name string + prepare func(request *schema.PatchUserSchemaRequest) error + args args + want *schema.PatchUserSchemaResponse + wantErr bool + }{ + { + name: "missing permission, error", + prepare: func(request *schema.PatchUserSchemaRequest) error { + schemaID := Tester.CreateUserSchemaEmpty(IAMOwnerCTX).GetDetails().GetId() + request.Id = schemaID + return nil + }, + args: args{ + ctx: Tester.WithAuthorization(context.Background(), integration.OrgOwner), + req: &schema.PatchUserSchemaRequest{ + UserSchema: &schema.PatchUserSchema{ + Type: gu.Ptr(gofakeit.Name()), + }, + }, + }, + wantErr: true, + }, + { + name: "missing id, error", + prepare: func(request *schema.PatchUserSchemaRequest) error { + return nil + }, + args: args{ + ctx: IAMOwnerCTX, + req: &schema.PatchUserSchemaRequest{}, + }, + wantErr: true, + }, + { + name: "not existing, error", + prepare: func(request *schema.PatchUserSchemaRequest) error { + request.Id = "notexisting" + return nil + }, + args: args{ + ctx: IAMOwnerCTX, + req: &schema.PatchUserSchemaRequest{}, + }, + wantErr: true, + }, + { + name: "empty type, error", + prepare: func(request *schema.PatchUserSchemaRequest) error { + schemaID := Tester.CreateUserSchemaEmpty(IAMOwnerCTX).GetDetails().GetId() + request.Id = schemaID + return nil + }, + args: args{ + ctx: IAMOwnerCTX, + req: &schema.PatchUserSchemaRequest{ + UserSchema: &schema.PatchUserSchema{ + Type: gu.Ptr(""), + }, + }, + }, + wantErr: true, + }, + { + name: "update type, ok", + prepare: func(request *schema.PatchUserSchemaRequest) error { + schemaID := Tester.CreateUserSchemaEmpty(IAMOwnerCTX).GetDetails().GetId() + request.Id = schemaID + return nil + }, + args: args{ + ctx: IAMOwnerCTX, + req: &schema.PatchUserSchemaRequest{ + UserSchema: &schema.PatchUserSchema{ + Type: gu.Ptr(gofakeit.Name()), + }, + }, + }, + want: &schema.PatchUserSchemaResponse{ + Details: &resource_object.Details{ + Changed: timestamppb.Now(), + Owner: &object.Owner{ + Type: object.OwnerType_OWNER_TYPE_INSTANCE, + Id: Tester.Instance.InstanceID(), + }, + }, + }, + }, + { + name: "empty schema, ok", + prepare: func(request *schema.PatchUserSchemaRequest) error { + schemaID := Tester.CreateUserSchemaEmpty(IAMOwnerCTX).GetDetails().GetId() + request.Id = schemaID + return nil + }, + args: args{ + ctx: IAMOwnerCTX, + req: &schema.PatchUserSchemaRequest{ + UserSchema: &schema.PatchUserSchema{ + DataType: &schema.PatchUserSchema_Schema{}, + }, + }, + }, + want: &schema.PatchUserSchemaResponse{ + Details: &resource_object.Details{ + Changed: timestamppb.Now(), + Owner: &object.Owner{ + Type: object.OwnerType_OWNER_TYPE_INSTANCE, + Id: Tester.Instance.InstanceID(), + }, + }, + }, + }, + { + name: "invalid schema, error", + prepare: func(request *schema.PatchUserSchemaRequest) error { + schemaID := Tester.CreateUserSchemaEmpty(IAMOwnerCTX).GetDetails().GetId() + request.Id = schemaID + return nil + }, + args: args{ + ctx: IAMOwnerCTX, + req: &schema.PatchUserSchemaRequest{ + UserSchema: &schema.PatchUserSchema{ + DataType: &schema.PatchUserSchema_Schema{ + Schema: func() *structpb.Struct { + s := new(structpb.Struct) + err := s.UnmarshalJSON([]byte(` + { + "$schema": "urn:zitadel:schema:v1", + "type": "object", + "properties": { + "name": { + "type": "string", + "required": true + }, + "description": { + "type": "string" + } + } + } + `)) + require.NoError(t, err) + return s + }(), + }, + }, + }, + }, + wantErr: true, + }, + { + name: "update schema, ok", + prepare: func(request *schema.PatchUserSchemaRequest) error { + schemaID := Tester.CreateUserSchemaEmpty(IAMOwnerCTX).GetDetails().GetId() + request.Id = schemaID + return nil + }, + args: args{ + ctx: IAMOwnerCTX, + req: &schema.PatchUserSchemaRequest{ + UserSchema: &schema.PatchUserSchema{ + DataType: &schema.PatchUserSchema_Schema{ + Schema: func() *structpb.Struct { + s := new(structpb.Struct) + err := s.UnmarshalJSON([]byte(` + { + "$schema": "urn:zitadel:schema:v1", + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string" + } + }, + "required": ["name"] + } + `)) + require.NoError(t, err) + return s + }(), + }, + }, + }, + }, + want: &schema.PatchUserSchemaResponse{ + Details: &resource_object.Details{ + Changed: timestamppb.Now(), + Owner: &object.Owner{ + Type: object.OwnerType_OWNER_TYPE_INSTANCE, + Id: Tester.Instance.InstanceID(), + }, + }, + }, + }, + { + name: "invalid authenticator, error", + prepare: func(request *schema.PatchUserSchemaRequest) error { + schemaID := Tester.CreateUserSchemaEmpty(IAMOwnerCTX).GetDetails().GetId() + request.Id = schemaID + return nil + }, + args: args{ + ctx: IAMOwnerCTX, + req: &schema.PatchUserSchemaRequest{ + UserSchema: &schema.PatchUserSchema{ + PossibleAuthenticators: []schema.AuthenticatorType{ + schema.AuthenticatorType_AUTHENTICATOR_TYPE_UNSPECIFIED, + }, + }, + }, + }, + wantErr: true, + }, + { + name: "update authenticator, ok", + prepare: func(request *schema.PatchUserSchemaRequest) error { + schemaID := Tester.CreateUserSchemaEmpty(IAMOwnerCTX).GetDetails().GetId() + request.Id = schemaID + return nil + }, + args: args{ + ctx: IAMOwnerCTX, + req: &schema.PatchUserSchemaRequest{ + UserSchema: &schema.PatchUserSchema{ + PossibleAuthenticators: []schema.AuthenticatorType{ + schema.AuthenticatorType_AUTHENTICATOR_TYPE_USERNAME, + }, + }, + }, + }, + want: &schema.PatchUserSchemaResponse{ + Details: &resource_object.Details{ + Changed: timestamppb.Now(), + Owner: &object.Owner{ + Type: object.OwnerType_OWNER_TYPE_INSTANCE, + Id: Tester.Instance.InstanceID(), + }, + }, + }, + }, + { + name: "inactive, error", + prepare: func(request *schema.PatchUserSchemaRequest) error { + schemaID := Tester.CreateUserSchemaEmpty(IAMOwnerCTX).GetDetails().GetId() + _, err := Client.DeactivateUserSchema(IAMOwnerCTX, &schema.DeactivateUserSchemaRequest{ + Id: schemaID, + }) + require.NoError(t, err) + request.Id = schemaID + return nil + }, + args: args{ + ctx: IAMOwnerCTX, + req: &schema.PatchUserSchemaRequest{ + UserSchema: &schema.PatchUserSchema{ + Type: gu.Ptr(gofakeit.Name()), + }, + }, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.prepare(tt.args.req) + require.NoError(t, err) + + got, err := Client.PatchUserSchema(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + integration.AssertResourceDetails(t, tt.want.GetDetails(), got.GetDetails()) + }) + } +} + +func TestServer_DeactivateUserSchema(t *testing.T) { + ensureFeatureEnabled(t, IAMOwnerCTX) + + type args struct { + ctx context.Context + req *schema.DeactivateUserSchemaRequest + prepare func(request *schema.DeactivateUserSchemaRequest) error + } + tests := []struct { + name string + args args + want *schema.DeactivateUserSchemaResponse + wantErr bool + }{ + { + name: "not existing, error", + args: args{ + IAMOwnerCTX, + &schema.DeactivateUserSchemaRequest{ + Id: "notexisting", + }, + func(request *schema.DeactivateUserSchemaRequest) error { return nil }, + }, + wantErr: true, + }, + { + name: "active, ok", + args: args{ + IAMOwnerCTX, + &schema.DeactivateUserSchemaRequest{}, + func(request *schema.DeactivateUserSchemaRequest) error { + schemaID := Tester.CreateUserSchemaEmpty(IAMOwnerCTX).GetDetails().GetId() + request.Id = schemaID + return nil + }, + }, + want: &schema.DeactivateUserSchemaResponse{ + Details: &resource_object.Details{ + Changed: timestamppb.Now(), + Owner: &object.Owner{ + Type: object.OwnerType_OWNER_TYPE_INSTANCE, + Id: Tester.Instance.InstanceID(), + }, + }, + }, + }, + { + name: "inactive, error", + args: args{ + IAMOwnerCTX, + &schema.DeactivateUserSchemaRequest{}, + func(request *schema.DeactivateUserSchemaRequest) error { + schemaID := Tester.CreateUserSchemaEmpty(IAMOwnerCTX).GetDetails().GetId() + request.Id = schemaID + _, err := Client.DeactivateUserSchema(IAMOwnerCTX, &schema.DeactivateUserSchemaRequest{ + Id: schemaID, + }) + return err + }, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.args.prepare(tt.args.req) + require.NoError(t, err) + + got, err := Client.DeactivateUserSchema(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + integration.AssertResourceDetails(t, tt.want.GetDetails(), got.GetDetails()) + }) + } +} + +func TestServer_ReactivateUserSchema(t *testing.T) { + ensureFeatureEnabled(t, IAMOwnerCTX) + + type args struct { + ctx context.Context + req *schema.ReactivateUserSchemaRequest + prepare func(request *schema.ReactivateUserSchemaRequest) error + } + tests := []struct { + name string + args args + want *schema.ReactivateUserSchemaResponse + wantErr bool + }{ + { + name: "not existing, error", + args: args{ + IAMOwnerCTX, + &schema.ReactivateUserSchemaRequest{ + Id: "notexisting", + }, + func(request *schema.ReactivateUserSchemaRequest) error { return nil }, + }, + wantErr: true, + }, + { + name: "active, error", + args: args{ + ctx: IAMOwnerCTX, + req: &schema.ReactivateUserSchemaRequest{}, + prepare: func(request *schema.ReactivateUserSchemaRequest) error { + schemaID := Tester.CreateUserSchemaEmpty(IAMOwnerCTX).GetDetails().GetId() + request.Id = schemaID + return nil + }, + }, + wantErr: true, + }, + { + name: "inactive, ok", + args: args{ + ctx: IAMOwnerCTX, + req: &schema.ReactivateUserSchemaRequest{}, + prepare: func(request *schema.ReactivateUserSchemaRequest) error { + schemaID := Tester.CreateUserSchemaEmpty(IAMOwnerCTX).GetDetails().GetId() + request.Id = schemaID + _, err := Client.DeactivateUserSchema(IAMOwnerCTX, &schema.DeactivateUserSchemaRequest{ + Id: schemaID, + }) + return err + }, + }, + want: &schema.ReactivateUserSchemaResponse{ + Details: &resource_object.Details{ + Changed: timestamppb.Now(), + Owner: &object.Owner{ + Type: object.OwnerType_OWNER_TYPE_INSTANCE, + Id: Tester.Instance.InstanceID(), + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.args.prepare(tt.args.req) + require.NoError(t, err) + + got, err := Client.ReactivateUserSchema(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + integration.AssertResourceDetails(t, tt.want.GetDetails(), got.GetDetails()) + }) + } +} + +func TestServer_DeleteUserSchema(t *testing.T) { + ensureFeatureEnabled(t, IAMOwnerCTX) + + type args struct { + ctx context.Context + req *schema.DeleteUserSchemaRequest + prepare func(request *schema.DeleteUserSchemaRequest) error + } + tests := []struct { + name string + args args + want *schema.DeleteUserSchemaResponse + wantErr bool + }{ + { + name: "not existing, error", + args: args{ + IAMOwnerCTX, + &schema.DeleteUserSchemaRequest{ + Id: "notexisting", + }, + func(request *schema.DeleteUserSchemaRequest) error { return nil }, + }, + wantErr: true, + }, + { + name: "delete, ok", + args: args{ + ctx: IAMOwnerCTX, + req: &schema.DeleteUserSchemaRequest{}, + prepare: func(request *schema.DeleteUserSchemaRequest) error { + schemaID := Tester.CreateUserSchemaEmpty(IAMOwnerCTX).GetDetails().GetId() + request.Id = schemaID + return nil + }, + }, + want: &schema.DeleteUserSchemaResponse{ + Details: &resource_object.Details{ + Changed: timestamppb.Now(), + Owner: &object.Owner{ + Type: object.OwnerType_OWNER_TYPE_INSTANCE, + Id: Tester.Instance.InstanceID(), + }, + }, + }, + }, + { + name: "deleted, error", + args: args{ + ctx: IAMOwnerCTX, + req: &schema.DeleteUserSchemaRequest{}, + prepare: func(request *schema.DeleteUserSchemaRequest) error { + schemaID := Tester.CreateUserSchemaEmpty(IAMOwnerCTX).GetDetails().GetId() + request.Id = schemaID + _, err := Client.DeleteUserSchema(IAMOwnerCTX, &schema.DeleteUserSchemaRequest{ + Id: schemaID, + }) + return err + }, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.args.prepare(tt.args.req) + require.NoError(t, err) + + got, err := Client.DeleteUserSchema(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + integration.AssertResourceDetails(t, tt.want.GetDetails(), got.GetDetails()) + }) + } +} diff --git a/internal/api/grpc/text/custom_text.go b/internal/api/grpc/text/custom_text.go index 82e77cbbcc..28899278c6 100644 --- a/internal/api/grpc/text/custom_text.go +++ b/internal/api/grpc/text/custom_text.go @@ -64,7 +64,6 @@ func CustomLoginTextToPb(text *domain.CustomLoginText) *text_pb.LoginCustomText RegistrationUserText: RegistrationUserScreenTextToPb(text.RegistrationUser), ExternalRegistrationUserOverviewText: ExternalRegistrationUserOverviewScreenTextToPb(text.ExternalRegistrationUserOverview), RegistrationOrgText: RegistrationOrgScreenTextToPb(text.RegistrationOrg), - LinkingUserPromptText: LinkingUserPromptScreenTextToPb(text.LinkingUserPrompt), LinkingUserDoneText: LinkingUserDoneScreenTextToPb(text.LinkingUsersDone), ExternalUserNotFoundText: ExternalUserNotFoundScreenTextToPb(text.ExternalNotFound), SuccessLoginText: SuccessLoginScreenTextToPb(text.LoginSuccess), @@ -424,15 +423,6 @@ func LinkingUserDoneScreenTextToPb(text domain.LinkingUserDoneScreenText) *text_ } } -func LinkingUserPromptScreenTextToPb(text domain.LinkingUserPromptScreenText) *text_pb.LinkingUserPromptScreenText { - return &text_pb.LinkingUserPromptScreenText{ - Title: text.Title, - Description: text.Description, - LinkButtonText: text.LinkButtonText, - OtherButtonText: text.OtherButtonText, - } -} - func ExternalUserNotFoundScreenTextToPb(text domain.ExternalUserNotFoundScreenText) *text_pb.ExternalUserNotFoundScreenText { return &text_pb.ExternalUserNotFoundScreenText{ Title: text.Title, @@ -902,15 +892,6 @@ func RegistrationOrgScreenTextPbToDomain(text *text_pb.RegistrationOrgScreenText } } -func LinkingUserPromptScreenTextPbToDomain(text *text_pb.LinkingUserPromptScreenText) domain.LinkingUserPromptScreenText { - return domain.LinkingUserPromptScreenText{ - Title: text.GetTitle(), - Description: text.GetDescription(), - LinkButtonText: text.GetLinkButtonText(), - OtherButtonText: text.GetOtherButtonText(), - } -} - func LinkingUserDoneScreenTextPbToDomain(text *text_pb.LinkingUserDoneScreenText) domain.LinkingUserDoneScreenText { if text == nil { return domain.LinkingUserDoneScreenText{} diff --git a/internal/api/grpc/user/schema/v3alpha/schema.go b/internal/api/grpc/user/schema/v3alpha/schema.go deleted file mode 100644 index 73342f7024..0000000000 --- a/internal/api/grpc/user/schema/v3alpha/schema.go +++ /dev/null @@ -1,386 +0,0 @@ -package schema - -import ( - "context" - - "google.golang.org/protobuf/types/known/structpb" - - "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/grpc/object/v2" - "github.com/zitadel/zitadel/internal/command" - "github.com/zitadel/zitadel/internal/domain" - "github.com/zitadel/zitadel/internal/query" - "github.com/zitadel/zitadel/internal/zerrors" - schema "github.com/zitadel/zitadel/pkg/grpc/user/schema/v3alpha" -) - -func (s *Server) CreateUserSchema(ctx context.Context, req *schema.CreateUserSchemaRequest) (*schema.CreateUserSchemaResponse, error) { - if err := checkUserSchemaEnabled(ctx); err != nil { - return nil, err - } - userSchema, err := createUserSchemaToCommand(req, authz.GetInstance(ctx).InstanceID()) - if err != nil { - return nil, err - } - id, details, err := s.command.CreateUserSchema(ctx, userSchema) - if err != nil { - return nil, err - } - return &schema.CreateUserSchemaResponse{ - Id: id, - Details: object.DomainToDetailsPb(details), - }, nil -} - -func (s *Server) UpdateUserSchema(ctx context.Context, req *schema.UpdateUserSchemaRequest) (*schema.UpdateUserSchemaResponse, error) { - if err := checkUserSchemaEnabled(ctx); err != nil { - return nil, err - } - userSchema, err := updateUserSchemaToCommand(req, authz.GetInstance(ctx).InstanceID()) - if err != nil { - return nil, err - } - details, err := s.command.UpdateUserSchema(ctx, userSchema) - if err != nil { - return nil, err - } - return &schema.UpdateUserSchemaResponse{ - Details: object.DomainToDetailsPb(details), - }, nil -} - -func (s *Server) DeactivateUserSchema(ctx context.Context, req *schema.DeactivateUserSchemaRequest) (*schema.DeactivateUserSchemaResponse, error) { - if err := checkUserSchemaEnabled(ctx); err != nil { - return nil, err - } - details, err := s.command.DeactivateUserSchema(ctx, req.GetId(), authz.GetInstance(ctx).InstanceID()) - if err != nil { - return nil, err - } - return &schema.DeactivateUserSchemaResponse{ - Details: object.DomainToDetailsPb(details), - }, nil -} - -func (s *Server) ReactivateUserSchema(ctx context.Context, req *schema.ReactivateUserSchemaRequest) (*schema.ReactivateUserSchemaResponse, error) { - if err := checkUserSchemaEnabled(ctx); err != nil { - return nil, err - } - details, err := s.command.ReactivateUserSchema(ctx, req.GetId(), authz.GetInstance(ctx).InstanceID()) - if err != nil { - return nil, err - } - return &schema.ReactivateUserSchemaResponse{ - Details: object.DomainToDetailsPb(details), - }, nil -} - -func (s *Server) DeleteUserSchema(ctx context.Context, req *schema.DeleteUserSchemaRequest) (*schema.DeleteUserSchemaResponse, error) { - if err := checkUserSchemaEnabled(ctx); err != nil { - return nil, err - } - details, err := s.command.DeleteUserSchema(ctx, req.GetId(), authz.GetInstance(ctx).InstanceID()) - if err != nil { - return nil, err - } - return &schema.DeleteUserSchemaResponse{ - Details: object.DomainToDetailsPb(details), - }, nil -} - -func (s *Server) ListUserSchemas(ctx context.Context, req *schema.ListUserSchemasRequest) (*schema.ListUserSchemasResponse, error) { - if err := checkUserSchemaEnabled(ctx); err != nil { - return nil, err - } - queries, err := listUserSchemaToQuery(req) - if err != nil { - return nil, err - } - res, err := s.query.SearchUserSchema(ctx, queries) - if err != nil { - return nil, err - } - userSchemas, err := userSchemasToPb(res.UserSchemas) - if err != nil { - return nil, err - } - return &schema.ListUserSchemasResponse{ - Details: object.ToListDetails(res.SearchResponse), - Result: userSchemas, - }, nil -} - -func (s *Server) GetUserSchemaByID(ctx context.Context, req *schema.GetUserSchemaByIDRequest) (*schema.GetUserSchemaByIDResponse, error) { - if err := checkUserSchemaEnabled(ctx); err != nil { - return nil, err - } - res, err := s.query.GetUserSchemaByID(ctx, req.GetId()) - if err != nil { - return nil, err - } - userSchema, err := userSchemaToPb(res) - if err != nil { - return nil, err - } - return &schema.GetUserSchemaByIDResponse{ - Schema: userSchema, - }, nil -} - -func userSchemasToPb(schemas []*query.UserSchema) (_ []*schema.UserSchema, err error) { - userSchemas := make([]*schema.UserSchema, len(schemas)) - for i, userSchema := range schemas { - userSchemas[i], err = userSchemaToPb(userSchema) - if err != nil { - return nil, err - } - } - return userSchemas, nil -} - -func userSchemaToPb(userSchema *query.UserSchema) (*schema.UserSchema, error) { - s := new(structpb.Struct) - if err := s.UnmarshalJSON(userSchema.Schema); err != nil { - return nil, err - } - return &schema.UserSchema{ - Id: userSchema.ID, - Details: object.DomainToDetailsPb(&userSchema.ObjectDetails), - Type: userSchema.Type, - State: userSchemaStateToPb(userSchema.State), - Revision: userSchema.Revision, - Schema: s, - PossibleAuthenticators: authenticatorTypesToPb(userSchema.PossibleAuthenticators), - }, nil -} - -func authenticatorTypesToPb(authenticators []domain.AuthenticatorType) []schema.AuthenticatorType { - authTypes := make([]schema.AuthenticatorType, len(authenticators)) - for i, authenticator := range authenticators { - authTypes[i] = authenticatorTypeToPb(authenticator) - } - return authTypes -} - -func authenticatorTypeToPb(authenticator domain.AuthenticatorType) schema.AuthenticatorType { - switch authenticator { - case domain.AuthenticatorTypeUsername: - return schema.AuthenticatorType_AUTHENTICATOR_TYPE_USERNAME - case domain.AuthenticatorTypePassword: - return schema.AuthenticatorType_AUTHENTICATOR_TYPE_PASSWORD - case domain.AuthenticatorTypeWebAuthN: - return schema.AuthenticatorType_AUTHENTICATOR_TYPE_WEBAUTHN - case domain.AuthenticatorTypeTOTP: - return schema.AuthenticatorType_AUTHENTICATOR_TYPE_TOTP - case domain.AuthenticatorTypeOTPEmail: - return schema.AuthenticatorType_AUTHENTICATOR_TYPE_OTP_EMAIL - case domain.AuthenticatorTypeOTPSMS: - return schema.AuthenticatorType_AUTHENTICATOR_TYPE_OTP_SMS - case domain.AuthenticatorTypeAuthenticationKey: - return schema.AuthenticatorType_AUTHENTICATOR_TYPE_AUTHENTICATION_KEY - case domain.AuthenticatorTypeIdentityProvider: - return schema.AuthenticatorType_AUTHENTICATOR_TYPE_IDENTITY_PROVIDER - case domain.AuthenticatorTypeUnspecified: - return schema.AuthenticatorType_AUTHENTICATOR_TYPE_UNSPECIFIED - default: - return schema.AuthenticatorType_AUTHENTICATOR_TYPE_UNSPECIFIED - } -} - -func userSchemaStateToPb(state domain.UserSchemaState) schema.State { - switch state { - case domain.UserSchemaStateActive: - return schema.State_STATE_ACTIVE - case domain.UserSchemaStateInactive: - return schema.State_STATE_INACTIVE - case domain.UserSchemaStateUnspecified, - domain.UserSchemaStateDeleted: - return schema.State_STATE_UNSPECIFIED - default: - return schema.State_STATE_UNSPECIFIED - } -} - -func listUserSchemaToQuery(req *schema.ListUserSchemasRequest) (*query.UserSchemaSearchQueries, error) { - offset, limit, asc := object.ListQueryToQuery(req.Query) - queries, err := userSchemaQueriesToQuery(req.Queries, 0) // start at level 0 - if err != nil { - return nil, err - } - return &query.UserSchemaSearchQueries{ - SearchRequest: query.SearchRequest{ - Offset: offset, - Limit: limit, - Asc: asc, - SortingColumn: userSchemaFieldNameToSortingColumn(req.SortingColumn), - }, - Queries: queries, - }, nil -} - -func userSchemaFieldNameToSortingColumn(column schema.FieldName) query.Column { - switch column { - case schema.FieldName_FIELD_NAME_TYPE: - return query.UserSchemaTypeCol - case schema.FieldName_FIELD_NAME_STATE: - return query.UserSchemaStateCol - case schema.FieldName_FIELD_NAME_REVISION: - return query.UserSchemaRevisionCol - case schema.FieldName_FIELD_NAME_CHANGE_DATE: - return query.UserSchemaChangeDateCol - case schema.FieldName_FIELD_NAME_UNSPECIFIED: - return query.UserSchemaIDCol - default: - return query.UserSchemaIDCol - } -} - -func checkUserSchemaEnabled(ctx context.Context) error { - if authz.GetInstance(ctx).Features().UserSchema { - return nil - } - return zerrors.ThrowPreconditionFailed(nil, "SCHEMA-SFjk3", "Errors.UserSchema.NotEnabled") -} - -func createUserSchemaToCommand(req *schema.CreateUserSchemaRequest, resourceOwner string) (*command.CreateUserSchema, error) { - schema, err := req.GetSchema().MarshalJSON() - if err != nil { - return nil, err - } - return &command.CreateUserSchema{ - ResourceOwner: resourceOwner, - Type: req.GetType(), - Schema: schema, - PossibleAuthenticators: authenticatorsToDomain(req.GetPossibleAuthenticators()), - }, nil -} - -func updateUserSchemaToCommand(req *schema.UpdateUserSchemaRequest, resourceOwner string) (*command.UpdateUserSchema, error) { - schema, err := req.GetSchema().MarshalJSON() - if err != nil { - return nil, err - } - return &command.UpdateUserSchema{ - ID: req.GetId(), - ResourceOwner: resourceOwner, - Type: req.Type, - Schema: schema, - PossibleAuthenticators: authenticatorsToDomain(req.GetPossibleAuthenticators()), - }, nil -} - -func authenticatorsToDomain(authenticators []schema.AuthenticatorType) []domain.AuthenticatorType { - types := make([]domain.AuthenticatorType, len(authenticators)) - for i, authenticator := range authenticators { - types[i] = authenticatorTypeToDomain(authenticator) - } - return types -} - -func authenticatorTypeToDomain(authenticator schema.AuthenticatorType) domain.AuthenticatorType { - switch authenticator { - case schema.AuthenticatorType_AUTHENTICATOR_TYPE_UNSPECIFIED: - return domain.AuthenticatorTypeUnspecified - case schema.AuthenticatorType_AUTHENTICATOR_TYPE_USERNAME: - return domain.AuthenticatorTypeUsername - case schema.AuthenticatorType_AUTHENTICATOR_TYPE_PASSWORD: - return domain.AuthenticatorTypePassword - case schema.AuthenticatorType_AUTHENTICATOR_TYPE_WEBAUTHN: - return domain.AuthenticatorTypeWebAuthN - case schema.AuthenticatorType_AUTHENTICATOR_TYPE_TOTP: - return domain.AuthenticatorTypeTOTP - case schema.AuthenticatorType_AUTHENTICATOR_TYPE_OTP_EMAIL: - return domain.AuthenticatorTypeOTPEmail - case schema.AuthenticatorType_AUTHENTICATOR_TYPE_OTP_SMS: - return domain.AuthenticatorTypeOTPSMS - case schema.AuthenticatorType_AUTHENTICATOR_TYPE_AUTHENTICATION_KEY: - return domain.AuthenticatorTypeAuthenticationKey - case schema.AuthenticatorType_AUTHENTICATOR_TYPE_IDENTITY_PROVIDER: - return domain.AuthenticatorTypeIdentityProvider - default: - return domain.AuthenticatorTypeUnspecified - } -} - -func userSchemaStateToDomain(state schema.State) domain.UserSchemaState { - switch state { - case schema.State_STATE_ACTIVE: - return domain.UserSchemaStateActive - case schema.State_STATE_INACTIVE: - return domain.UserSchemaStateInactive - case schema.State_STATE_UNSPECIFIED: - return domain.UserSchemaStateUnspecified - default: - return domain.UserSchemaStateUnspecified - } -} - -func userSchemaQueriesToQuery(queries []*schema.SearchQuery, level uint8) (_ []query.SearchQuery, err error) { - q := make([]query.SearchQuery, len(queries)) - for i, query := range queries { - q[i], err = userSchemaQueryToQuery(query, level) - if err != nil { - return nil, err - } - } - return q, nil -} - -func userSchemaQueryToQuery(query *schema.SearchQuery, level uint8) (query.SearchQuery, error) { - if level > 20 { - // can't go deeper than 20 levels of nesting. - return nil, zerrors.ThrowInvalidArgument(nil, "SCHEMA-zsQ97", "Errors.Query.TooManyNestingLevels") - } - switch q := query.Query.(type) { - case *schema.SearchQuery_StateQuery: - return stateQueryToQuery(q.StateQuery) - case *schema.SearchQuery_TypeQuery: - return typeQueryToQuery(q.TypeQuery) - case *schema.SearchQuery_IdQuery: - return idQueryToQuery(q.IdQuery) - case *schema.SearchQuery_OrQuery: - return orQueryToQuery(q.OrQuery, level) - case *schema.SearchQuery_AndQuery: - return andQueryToQuery(q.AndQuery, level) - case *schema.SearchQuery_NotQuery: - return notQueryToQuery(q.NotQuery, level) - default: - return nil, zerrors.ThrowInvalidArgument(nil, "SCHEMA-vR9nC", "List.Query.Invalid") - } -} - -func stateQueryToQuery(q *schema.StateQuery) (query.SearchQuery, error) { - return query.NewUserSchemaStateSearchQuery(userSchemaStateToDomain(q.GetState())) -} - -func typeQueryToQuery(q *schema.TypeQuery) (query.SearchQuery, error) { - return query.NewUserSchemaTypeSearchQuery(q.GetType(), object.TextMethodToQuery(q.GetMethod())) -} - -func idQueryToQuery(q *schema.IDQuery) (query.SearchQuery, error) { - return query.NewUserSchemaIDSearchQuery(q.GetId(), object.TextMethodToQuery(q.GetMethod())) -} - -func orQueryToQuery(q *schema.OrQuery, level uint8) (query.SearchQuery, error) { - mappedQueries, err := userSchemaQueriesToQuery(q.GetQueries(), level+1) - if err != nil { - return nil, err - } - return query.NewUserOrSearchQuery(mappedQueries) -} - -func andQueryToQuery(q *schema.AndQuery, level uint8) (query.SearchQuery, error) { - mappedQueries, err := userSchemaQueriesToQuery(q.GetQueries(), level+1) - if err != nil { - return nil, err - } - return query.NewUserAndSearchQuery(mappedQueries) -} - -func notQueryToQuery(q *schema.NotQuery, level uint8) (query.SearchQuery, error) { - mappedQuery, err := userSchemaQueryToQuery(q.GetQuery(), level+1) - if err != nil { - return nil, err - } - return query.NewUserNotSearchQuery(mappedQuery) -} diff --git a/internal/api/grpc/user/schema/v3alpha/schema_integration_test.go b/internal/api/grpc/user/schema/v3alpha/schema_integration_test.go deleted file mode 100644 index 5cf279144d..0000000000 --- a/internal/api/grpc/user/schema/v3alpha/schema_integration_test.go +++ /dev/null @@ -1,1109 +0,0 @@ -//go:build integration - -package schema_test - -import ( - "context" - "fmt" - "os" - "testing" - "time" - - "github.com/muhlemmer/gu" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "google.golang.org/protobuf/types/known/structpb" - "google.golang.org/protobuf/types/known/timestamppb" - - "github.com/zitadel/zitadel/internal/api/grpc" - "github.com/zitadel/zitadel/internal/integration" - "github.com/zitadel/zitadel/pkg/grpc/feature/v2" - "github.com/zitadel/zitadel/pkg/grpc/object/v2" - schema "github.com/zitadel/zitadel/pkg/grpc/user/schema/v3alpha" -) - -var ( - CTX context.Context - Tester *integration.Tester - Client schema.UserSchemaServiceClient -) - -func TestMain(m *testing.M) { - os.Exit(func() int { - ctx, _, cancel := integration.Contexts(5 * time.Minute) - defer cancel() - - Tester = integration.NewTester(ctx) - defer Tester.Done() - - CTX = Tester.WithAuthorization(ctx, integration.IAMOwner) - Client = Tester.Client.UserSchemaV3 - - return m.Run() - }()) -} - -func ensureFeatureEnabled(t *testing.T) { - f, err := Tester.Client.FeatureV2.GetInstanceFeatures(CTX, &feature.GetInstanceFeaturesRequest{ - Inheritance: true, - }) - require.NoError(t, err) - if f.UserSchema.GetEnabled() { - return - } - _, err = Tester.Client.FeatureV2.SetInstanceFeatures(CTX, &feature.SetInstanceFeaturesRequest{ - UserSchema: gu.Ptr(true), - }) - require.NoError(t, err) - retryDuration := time.Minute - if ctxDeadline, ok := CTX.Deadline(); ok { - retryDuration = time.Until(ctxDeadline) - } - require.EventuallyWithT(t, - func(ttt *assert.CollectT) { - f, err := Tester.Client.FeatureV2.GetInstanceFeatures(CTX, &feature.GetInstanceFeaturesRequest{ - Inheritance: true, - }) - require.NoError(ttt, err) - if f.UserSchema.GetEnabled() { - return - } - }, - retryDuration, - 100*time.Millisecond, - "timed out waiting for ensuring instance feature") -} - -func TestServer_CreateUserSchema(t *testing.T) { - ensureFeatureEnabled(t) - - tests := []struct { - name string - ctx context.Context - req *schema.CreateUserSchemaRequest - want *schema.CreateUserSchemaResponse - wantErr bool - }{ - { - name: "missing permission, error", - ctx: Tester.WithAuthorization(context.Background(), integration.OrgOwner), - req: &schema.CreateUserSchemaRequest{ - Type: fmt.Sprint(time.Now().UnixNano() + 1), - }, - wantErr: true, - }, - { - name: "empty type", - ctx: CTX, - req: &schema.CreateUserSchemaRequest{ - Type: "", - }, - wantErr: true, - }, - { - name: "empty schema, error", - ctx: CTX, - req: &schema.CreateUserSchemaRequest{ - Type: fmt.Sprint(time.Now().UnixNano() + 1), - }, - wantErr: true, - }, - { - name: "invalid schema, error", - ctx: CTX, - req: &schema.CreateUserSchemaRequest{ - Type: fmt.Sprint(time.Now().UnixNano() + 1), - DataType: &schema.CreateUserSchemaRequest_Schema{ - Schema: func() *structpb.Struct { - s := new(structpb.Struct) - err := s.UnmarshalJSON([]byte(` - { - "type": "object", - "properties": { - "name": { - "type": "string", - "required": true - }, - "description": { - "type": "string" - } - } - } - `)) - require.NoError(t, err) - return s - }(), - }, - }, - wantErr: true, - }, - { - name: "no authenticators, ok", - ctx: CTX, - req: &schema.CreateUserSchemaRequest{ - Type: fmt.Sprint(time.Now().UnixNano() + 1), - DataType: &schema.CreateUserSchemaRequest_Schema{ - Schema: func() *structpb.Struct { - s := new(structpb.Struct) - err := s.UnmarshalJSON([]byte(` - { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "description": { - "type": "string" - } - }, - "required": ["name"] - } - `)) - require.NoError(t, err) - return s - }(), - }, - }, - want: &schema.CreateUserSchemaResponse{ - Details: &object.Details{ - ChangeDate: timestamppb.Now(), - ResourceOwner: Tester.Instance.InstanceID(), - }, - }, - }, - { - name: "invalid authenticator, error", - ctx: CTX, - req: &schema.CreateUserSchemaRequest{ - Type: fmt.Sprint(time.Now().UnixNano() + 1), - DataType: &schema.CreateUserSchemaRequest_Schema{ - Schema: func() *structpb.Struct { - s := new(structpb.Struct) - err := s.UnmarshalJSON([]byte(` - { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "description": { - "type": "string" - } - }, - "required": ["name"] - } - `)) - require.NoError(t, err) - return s - }(), - }, - PossibleAuthenticators: []schema.AuthenticatorType{ - schema.AuthenticatorType_AUTHENTICATOR_TYPE_UNSPECIFIED, - }, - }, - wantErr: true, - }, - { - name: "with authenticator, ok", - ctx: CTX, - req: &schema.CreateUserSchemaRequest{ - Type: fmt.Sprint(time.Now().UnixNano() + 1), - DataType: &schema.CreateUserSchemaRequest_Schema{ - Schema: func() *structpb.Struct { - s := new(structpb.Struct) - err := s.UnmarshalJSON([]byte(` - { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "description": { - "type": "string" - } - }, - "required": ["name"] - } - `)) - require.NoError(t, err) - return s - }(), - }, - PossibleAuthenticators: []schema.AuthenticatorType{ - schema.AuthenticatorType_AUTHENTICATOR_TYPE_USERNAME, - }, - }, - want: &schema.CreateUserSchemaResponse{ - Details: &object.Details{ - ChangeDate: timestamppb.Now(), - ResourceOwner: Tester.Instance.InstanceID(), - }, - }, - }, - { - name: "with invalid permission, error", - ctx: CTX, - req: &schema.CreateUserSchemaRequest{ - Type: fmt.Sprint(time.Now().UnixNano() + 1), - DataType: &schema.CreateUserSchemaRequest_Schema{ - Schema: func() *structpb.Struct { - s := new(structpb.Struct) - err := s.UnmarshalJSON([]byte(` - { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "description": { - "type": "string", - "urn:zitadel:schema:permission": "read" - } - }, - "required": ["name"] - } - `)) - require.NoError(t, err) - return s - }(), - }, - PossibleAuthenticators: []schema.AuthenticatorType{ - schema.AuthenticatorType_AUTHENTICATOR_TYPE_USERNAME, - }, - }, - wantErr: true, - }, - { - name: "with valid permission, ok", - ctx: CTX, - req: &schema.CreateUserSchemaRequest{ - Type: fmt.Sprint(time.Now().UnixNano() + 1), - DataType: &schema.CreateUserSchemaRequest_Schema{ - Schema: func() *structpb.Struct { - s := new(structpb.Struct) - err := s.UnmarshalJSON([]byte(` - { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "description": { - "type": "string", - "urn:zitadel:schema:permission": { - "owner": "rw", - "self": "r" - } - } - }, - "required": ["name"] - } - `)) - require.NoError(t, err) - return s - }(), - }, - PossibleAuthenticators: []schema.AuthenticatorType{ - schema.AuthenticatorType_AUTHENTICATOR_TYPE_USERNAME, - }, - }, - want: &schema.CreateUserSchemaResponse{ - Details: &object.Details{ - ChangeDate: timestamppb.Now(), - ResourceOwner: Tester.Instance.InstanceID(), - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := Client.CreateUserSchema(tt.ctx, tt.req) - if tt.wantErr { - require.Error(t, err) - return - } - require.NoError(t, err) - - integration.AssertDetails(t, tt.want, got) - assert.NotEmpty(t, got.GetId()) - }) - } -} - -func TestServer_UpdateUserSchema(t *testing.T) { - ensureFeatureEnabled(t) - - type args struct { - ctx context.Context - req *schema.UpdateUserSchemaRequest - } - tests := []struct { - name string - prepare func(request *schema.UpdateUserSchemaRequest) error - args args - want *schema.UpdateUserSchemaResponse - wantErr bool - }{ - { - name: "missing permission, error", - prepare: func(request *schema.UpdateUserSchemaRequest) error { - schemaID := Tester.CreateUserSchema(CTX, t).GetId() - request.Id = schemaID - return nil - }, - args: args{ - ctx: Tester.WithAuthorization(context.Background(), integration.OrgOwner), - req: &schema.UpdateUserSchemaRequest{ - Type: gu.Ptr(fmt.Sprint(time.Now().UnixNano() + 1)), - }, - }, - wantErr: true, - }, - { - name: "missing id, error", - prepare: func(request *schema.UpdateUserSchemaRequest) error { - return nil - }, - args: args{ - ctx: CTX, - req: &schema.UpdateUserSchemaRequest{}, - }, - wantErr: true, - }, - { - name: "not existing, error", - prepare: func(request *schema.UpdateUserSchemaRequest) error { - request.Id = "notexisting" - return nil - }, - args: args{ - ctx: CTX, - req: &schema.UpdateUserSchemaRequest{}, - }, - wantErr: true, - }, - { - name: "empty type, error", - prepare: func(request *schema.UpdateUserSchemaRequest) error { - schemaID := Tester.CreateUserSchema(CTX, t).GetId() - request.Id = schemaID - return nil - }, - args: args{ - ctx: CTX, - req: &schema.UpdateUserSchemaRequest{ - Type: gu.Ptr(""), - }, - }, - wantErr: true, - }, - { - name: "update type, ok", - prepare: func(request *schema.UpdateUserSchemaRequest) error { - schemaID := Tester.CreateUserSchema(CTX, t).GetId() - request.Id = schemaID - return nil - }, - args: args{ - ctx: CTX, - req: &schema.UpdateUserSchemaRequest{ - Type: gu.Ptr(fmt.Sprint(time.Now().UnixNano() + 1)), - }, - }, - want: &schema.UpdateUserSchemaResponse{ - Details: &object.Details{ - ChangeDate: timestamppb.Now(), - ResourceOwner: Tester.Instance.InstanceID(), - }, - }, - }, - { - name: "empty schema, ok", - prepare: func(request *schema.UpdateUserSchemaRequest) error { - schemaID := Tester.CreateUserSchema(CTX, t).GetId() - request.Id = schemaID - return nil - }, - args: args{ - ctx: CTX, - req: &schema.UpdateUserSchemaRequest{ - DataType: &schema.UpdateUserSchemaRequest_Schema{}, - }, - }, - want: &schema.UpdateUserSchemaResponse{ - Details: &object.Details{ - ChangeDate: timestamppb.Now(), - ResourceOwner: Tester.Instance.InstanceID(), - }, - }, - }, - { - name: "invalid schema, error", - prepare: func(request *schema.UpdateUserSchemaRequest) error { - schemaID := Tester.CreateUserSchema(CTX, t).GetId() - request.Id = schemaID - return nil - }, - args: args{ - ctx: CTX, - req: &schema.UpdateUserSchemaRequest{ - DataType: &schema.UpdateUserSchemaRequest_Schema{ - Schema: func() *structpb.Struct { - s := new(structpb.Struct) - err := s.UnmarshalJSON([]byte(` - { - "$schema": "urn:zitadel:schema:v1", - "type": "object", - "properties": { - "name": { - "type": "string", - "required": true - }, - "description": { - "type": "string" - } - } - } - `)) - require.NoError(t, err) - return s - }(), - }, - }, - }, - wantErr: true, - }, - { - name: "update schema, ok", - prepare: func(request *schema.UpdateUserSchemaRequest) error { - schemaID := Tester.CreateUserSchema(CTX, t).GetId() - request.Id = schemaID - return nil - }, - args: args{ - ctx: CTX, - req: &schema.UpdateUserSchemaRequest{ - DataType: &schema.UpdateUserSchemaRequest_Schema{ - Schema: func() *structpb.Struct { - s := new(structpb.Struct) - err := s.UnmarshalJSON([]byte(` - { - "$schema": "urn:zitadel:schema:v1", - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "description": { - "type": "string" - } - }, - "required": ["name"] - } - `)) - require.NoError(t, err) - return s - }(), - }, - }, - }, - want: &schema.UpdateUserSchemaResponse{ - Details: &object.Details{ - ChangeDate: timestamppb.Now(), - ResourceOwner: Tester.Instance.InstanceID(), - }, - }, - }, - { - name: "invalid authenticator, error", - prepare: func(request *schema.UpdateUserSchemaRequest) error { - schemaID := Tester.CreateUserSchema(CTX, t).GetId() - request.Id = schemaID - return nil - }, - args: args{ - ctx: CTX, - req: &schema.UpdateUserSchemaRequest{ - PossibleAuthenticators: []schema.AuthenticatorType{ - schema.AuthenticatorType_AUTHENTICATOR_TYPE_UNSPECIFIED, - }, - }, - }, - wantErr: true, - }, - { - name: "update authenticator, ok", - prepare: func(request *schema.UpdateUserSchemaRequest) error { - schemaID := Tester.CreateUserSchema(CTX, t).GetId() - request.Id = schemaID - return nil - }, - args: args{ - ctx: CTX, - req: &schema.UpdateUserSchemaRequest{ - PossibleAuthenticators: []schema.AuthenticatorType{ - schema.AuthenticatorType_AUTHENTICATOR_TYPE_USERNAME, - }, - }, - }, - want: &schema.UpdateUserSchemaResponse{ - Details: &object.Details{ - ChangeDate: timestamppb.Now(), - ResourceOwner: Tester.Instance.InstanceID(), - }, - }, - }, - { - name: "inactive, error", - prepare: func(request *schema.UpdateUserSchemaRequest) error { - schemaID := Tester.CreateUserSchema(CTX, t).GetId() - _, err := Client.DeactivateUserSchema(CTX, &schema.DeactivateUserSchemaRequest{ - Id: schemaID, - }) - require.NoError(t, err) - request.Id = schemaID - return nil - }, - args: args{ - ctx: CTX, - req: &schema.UpdateUserSchemaRequest{ - Type: gu.Ptr(fmt.Sprint(time.Now().UnixNano() + 1)), - }, - }, - wantErr: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - err := tt.prepare(tt.args.req) - require.NoError(t, err) - - got, err := Client.UpdateUserSchema(tt.args.ctx, tt.args.req) - if tt.wantErr { - require.Error(t, err) - return - } - require.NoError(t, err) - integration.AssertDetails(t, tt.want, got) - }) - } -} - -func TestServer_DeactivateUserSchema(t *testing.T) { - ensureFeatureEnabled(t) - - type args struct { - ctx context.Context - req *schema.DeactivateUserSchemaRequest - prepare func(request *schema.DeactivateUserSchemaRequest) error - } - tests := []struct { - name string - args args - want *schema.DeactivateUserSchemaResponse - wantErr bool - }{ - { - name: "not existing, error", - args: args{ - CTX, - &schema.DeactivateUserSchemaRequest{ - Id: "notexisting", - }, - func(request *schema.DeactivateUserSchemaRequest) error { return nil }, - }, - wantErr: true, - }, - { - name: "active, ok", - args: args{ - CTX, - &schema.DeactivateUserSchemaRequest{}, - func(request *schema.DeactivateUserSchemaRequest) error { - schemaID := Tester.CreateUserSchema(CTX, t).GetId() - request.Id = schemaID - return nil - }, - }, - want: &schema.DeactivateUserSchemaResponse{ - Details: &object.Details{ - ChangeDate: timestamppb.Now(), - ResourceOwner: Tester.Instance.InstanceID(), - }, - }, - }, - { - name: "inactive, error", - args: args{ - CTX, - &schema.DeactivateUserSchemaRequest{}, - func(request *schema.DeactivateUserSchemaRequest) error { - schemaID := Tester.CreateUserSchema(CTX, t).GetId() - request.Id = schemaID - _, err := Client.DeactivateUserSchema(CTX, &schema.DeactivateUserSchemaRequest{ - Id: schemaID, - }) - return err - }, - }, - wantErr: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - err := tt.args.prepare(tt.args.req) - require.NoError(t, err) - - got, err := Client.DeactivateUserSchema(tt.args.ctx, tt.args.req) - if tt.wantErr { - require.Error(t, err) - return - } - require.NoError(t, err) - integration.AssertDetails(t, tt.want, got) - }) - } -} - -func TestServer_ReactivateUserSchema(t *testing.T) { - ensureFeatureEnabled(t) - - type args struct { - ctx context.Context - req *schema.ReactivateUserSchemaRequest - prepare func(request *schema.ReactivateUserSchemaRequest) error - } - tests := []struct { - name string - args args - want *schema.ReactivateUserSchemaResponse - wantErr bool - }{ - { - name: "not existing, error", - args: args{ - CTX, - &schema.ReactivateUserSchemaRequest{ - Id: "notexisting", - }, - func(request *schema.ReactivateUserSchemaRequest) error { return nil }, - }, - wantErr: true, - }, - { - name: "active, error", - args: args{ - ctx: CTX, - req: &schema.ReactivateUserSchemaRequest{}, - prepare: func(request *schema.ReactivateUserSchemaRequest) error { - schemaID := Tester.CreateUserSchema(CTX, t).GetId() - request.Id = schemaID - return nil - }, - }, - wantErr: true, - }, - { - name: "inactive, ok", - args: args{ - ctx: CTX, - req: &schema.ReactivateUserSchemaRequest{}, - prepare: func(request *schema.ReactivateUserSchemaRequest) error { - schemaID := Tester.CreateUserSchema(CTX, t).GetId() - request.Id = schemaID - _, err := Client.DeactivateUserSchema(CTX, &schema.DeactivateUserSchemaRequest{ - Id: schemaID, - }) - return err - }, - }, - want: &schema.ReactivateUserSchemaResponse{ - Details: &object.Details{ - ChangeDate: timestamppb.Now(), - ResourceOwner: Tester.Instance.InstanceID(), - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - err := tt.args.prepare(tt.args.req) - require.NoError(t, err) - - got, err := Client.ReactivateUserSchema(tt.args.ctx, tt.args.req) - if tt.wantErr { - require.Error(t, err) - } else { - require.NoError(t, err) - } - integration.AssertDetails(t, tt.want, got) - }) - } -} - -func TestServer_DeleteUserSchema(t *testing.T) { - ensureFeatureEnabled(t) - - type args struct { - ctx context.Context - req *schema.DeleteUserSchemaRequest - prepare func(request *schema.DeleteUserSchemaRequest) error - } - tests := []struct { - name string - args args - want *schema.DeleteUserSchemaResponse - wantErr bool - }{ - { - name: "not existing, error", - args: args{ - CTX, - &schema.DeleteUserSchemaRequest{ - Id: "notexisting", - }, - func(request *schema.DeleteUserSchemaRequest) error { return nil }, - }, - wantErr: true, - }, - { - name: "delete, ok", - args: args{ - ctx: CTX, - req: &schema.DeleteUserSchemaRequest{}, - prepare: func(request *schema.DeleteUserSchemaRequest) error { - schemaID := Tester.CreateUserSchema(CTX, t).GetId() - request.Id = schemaID - return nil - }, - }, - want: &schema.DeleteUserSchemaResponse{ - Details: &object.Details{ - ChangeDate: timestamppb.Now(), - ResourceOwner: Tester.Instance.InstanceID(), - }, - }, - }, - { - name: "deleted, error", - args: args{ - ctx: CTX, - req: &schema.DeleteUserSchemaRequest{}, - prepare: func(request *schema.DeleteUserSchemaRequest) error { - schemaID := Tester.CreateUserSchema(CTX, t).GetId() - request.Id = schemaID - _, err := Client.DeleteUserSchema(CTX, &schema.DeleteUserSchemaRequest{ - Id: schemaID, - }) - return err - }, - }, - wantErr: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - err := tt.args.prepare(tt.args.req) - require.NoError(t, err) - - got, err := Client.DeleteUserSchema(tt.args.ctx, tt.args.req) - if tt.wantErr { - require.Error(t, err) - return - } - require.NoError(t, err) - integration.AssertDetails(t, tt.want, got) - }) - } -} - -func TestServer_GetUserSchemaByID(t *testing.T) { - userSchema := new(structpb.Struct) - err := userSchema.UnmarshalJSON([]byte(`{ - "$schema": "urn:zitadel:schema:v1", - "type": "object", - "properties": {} - }`)) - require.NoError(t, err) - type args struct { - ctx context.Context - req *schema.GetUserSchemaByIDRequest - prepare func(request *schema.GetUserSchemaByIDRequest, resp *schema.GetUserSchemaByIDResponse) error - } - tests := []struct { - name string - args args - want *schema.GetUserSchemaByIDResponse - wantErr bool - }{ - { - name: "missing permission", - args: args{ - ctx: Tester.WithAuthorization(context.Background(), integration.OrgOwner), - req: &schema.GetUserSchemaByIDRequest{}, - prepare: func(request *schema.GetUserSchemaByIDRequest, resp *schema.GetUserSchemaByIDResponse) error { - schemaType := fmt.Sprint(time.Now().UnixNano() + 1) - createResp := Tester.CreateUserSchemaWithType(CTX, t, schemaType) - request.Id = createResp.GetId() - return nil - }, - }, - wantErr: true, - }, - { - name: "not existing, error", - args: args{ - ctx: CTX, - req: &schema.GetUserSchemaByIDRequest{ - Id: "notexisting", - }, - }, - wantErr: true, - }, - { - name: "get, ok", - args: args{ - ctx: CTX, - req: &schema.GetUserSchemaByIDRequest{}, - prepare: func(request *schema.GetUserSchemaByIDRequest, resp *schema.GetUserSchemaByIDResponse) error { - schemaType := fmt.Sprint(time.Now().UnixNano() + 1) - createResp := Tester.CreateUserSchemaWithType(CTX, t, schemaType) - request.Id = createResp.GetId() - - resp.Schema.Id = createResp.GetId() - resp.Schema.Type = schemaType - resp.Schema.Details = &object.Details{ - Sequence: createResp.GetDetails().GetSequence(), - ChangeDate: createResp.GetDetails().GetChangeDate(), - ResourceOwner: createResp.GetDetails().GetResourceOwner(), - } - return nil - }, - }, - want: &schema.GetUserSchemaByIDResponse{ - Schema: &schema.UserSchema{ - State: schema.State_STATE_ACTIVE, - Revision: 1, - Schema: userSchema, - PossibleAuthenticators: nil, - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ensureFeatureEnabled(t) - if tt.args.prepare != nil { - err := tt.args.prepare(tt.args.req, tt.want) - require.NoError(t, err) - } - - retryDuration := 5 * time.Second - if ctxDeadline, ok := CTX.Deadline(); ok { - retryDuration = time.Until(ctxDeadline) - } - - require.EventuallyWithT(t, func(ttt *assert.CollectT) { - got, err := Client.GetUserSchemaByID(tt.args.ctx, tt.args.req) - if tt.wantErr { - require.Error(ttt, err) - return - } - assert.NoError(ttt, err) - - integration.AssertDetails(t, tt.want.GetSchema(), got.GetSchema()) - grpc.AllFieldsEqual(t, tt.want.ProtoReflect(), got.ProtoReflect(), grpc.CustomMappers) - - }, retryDuration, time.Millisecond*100, "timeout waiting for expected user schema result") - }) - } -} - -func TestServer_ListUserSchemas(t *testing.T) { - userSchema := new(structpb.Struct) - err := userSchema.UnmarshalJSON([]byte(`{ - "$schema": "urn:zitadel:schema:v1", - "type": "object", - "properties": {} - }`)) - require.NoError(t, err) - type args struct { - ctx context.Context - req *schema.ListUserSchemasRequest - prepare func(request *schema.ListUserSchemasRequest, resp *schema.ListUserSchemasResponse) error - } - tests := []struct { - name string - args args - want *schema.ListUserSchemasResponse - wantErr bool - }{ - { - name: "missing permission", - args: args{ - ctx: Tester.WithAuthorization(context.Background(), integration.OrgOwner), - req: &schema.ListUserSchemasRequest{}, - }, - wantErr: true, - }, - { - name: "not found, error", - args: args{ - ctx: CTX, - req: &schema.ListUserSchemasRequest{ - Queries: []*schema.SearchQuery{ - { - Query: &schema.SearchQuery_IdQuery{ - IdQuery: &schema.IDQuery{ - Id: "notexisting", - }, - }, - }, - }, - }, - }, - want: &schema.ListUserSchemasResponse{ - Details: &object.ListDetails{ - TotalResult: 0, - }, - Result: []*schema.UserSchema{}, - }, - }, - { - name: "single (id), ok", - args: args{ - ctx: CTX, - req: &schema.ListUserSchemasRequest{}, - prepare: func(request *schema.ListUserSchemasRequest, resp *schema.ListUserSchemasResponse) error { - schemaType := fmt.Sprint(time.Now().UnixNano() + 1) - createResp := Tester.CreateUserSchemaWithType(CTX, t, schemaType) - request.Queries = []*schema.SearchQuery{ - { - Query: &schema.SearchQuery_IdQuery{ - IdQuery: &schema.IDQuery{ - Id: createResp.GetId(), - Method: object.TextQueryMethod_TEXT_QUERY_METHOD_EQUALS, - }, - }, - }, - } - - resp.Result[0].Id = createResp.GetId() - resp.Result[0].Type = schemaType - resp.Result[0].Details = &object.Details{ - Sequence: createResp.GetDetails().GetSequence(), - ChangeDate: createResp.GetDetails().GetChangeDate(), - ResourceOwner: createResp.GetDetails().GetResourceOwner(), - } - return nil - }, - }, - want: &schema.ListUserSchemasResponse{ - Details: &object.ListDetails{ - TotalResult: 1, - }, - Result: []*schema.UserSchema{ - { - State: schema.State_STATE_ACTIVE, - Revision: 1, - Schema: userSchema, - PossibleAuthenticators: nil, - }, - }, - }, - }, - { - name: "multiple (type), ok", - args: args{ - ctx: CTX, - req: &schema.ListUserSchemasRequest{}, - prepare: func(request *schema.ListUserSchemasRequest, resp *schema.ListUserSchemasResponse) error { - schemaType := fmt.Sprint(time.Now().UnixNano()) - schemaType1 := schemaType + "_1" - schemaType2 := schemaType + "_2" - createResp := Tester.CreateUserSchemaWithType(CTX, t, schemaType1) - createResp2 := Tester.CreateUserSchemaWithType(CTX, t, schemaType2) - - request.SortingColumn = schema.FieldName_FIELD_NAME_TYPE - request.Query = &object.ListQuery{Asc: true} - request.Queries = []*schema.SearchQuery{ - { - Query: &schema.SearchQuery_TypeQuery{ - TypeQuery: &schema.TypeQuery{ - Type: schemaType, - Method: object.TextQueryMethod_TEXT_QUERY_METHOD_STARTS_WITH, - }, - }, - }, - } - - resp.Result[0].Id = createResp.GetId() - resp.Result[0].Type = schemaType1 - resp.Result[0].Details = &object.Details{ - Sequence: createResp.GetDetails().GetSequence(), - ChangeDate: createResp.GetDetails().GetChangeDate(), - ResourceOwner: createResp.GetDetails().GetResourceOwner(), - } - resp.Result[1].Id = createResp2.GetId() - resp.Result[1].Type = schemaType2 - resp.Result[1].Details = &object.Details{ - Sequence: createResp2.GetDetails().GetSequence(), - ChangeDate: createResp2.GetDetails().GetChangeDate(), - ResourceOwner: createResp2.GetDetails().GetResourceOwner(), - } - return nil - }, - }, - want: &schema.ListUserSchemasResponse{ - Details: &object.ListDetails{ - TotalResult: 2, - }, - Result: []*schema.UserSchema{ - { - State: schema.State_STATE_ACTIVE, - Revision: 1, - Schema: userSchema, - PossibleAuthenticators: nil, - }, - { - State: schema.State_STATE_ACTIVE, - Revision: 1, - Schema: userSchema, - PossibleAuthenticators: nil, - }, - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ensureFeatureEnabled(t) - if tt.args.prepare != nil { - err := tt.args.prepare(tt.args.req, tt.want) - require.NoError(t, err) - } - - retryDuration := 20 * time.Second - if ctxDeadline, ok := CTX.Deadline(); ok { - retryDuration = time.Until(ctxDeadline) - } - - require.EventuallyWithT(t, func(ttt *assert.CollectT) { - got, err := Client.ListUserSchemas(tt.args.ctx, tt.args.req) - if tt.wantErr { - require.Error(ttt, err) - return - } - assert.NoError(ttt, err) - - // always first check length, otherwise its failed anyway - assert.Len(ttt, got.Result, len(tt.want.Result)) - for i := range tt.want.Result { - // - grpc.AllFieldsEqual(t, tt.want.Result[i].ProtoReflect(), got.Result[i].ProtoReflect(), grpc.CustomMappers) - } - integration.AssertListDetails(t, tt.want, got) - }, retryDuration, time.Millisecond*100, "timeout waiting for expected user schema result") - }) - } -} diff --git a/internal/api/grpc/user/schema/v3alpha/server.go b/internal/api/grpc/user/schema/v3alpha/server.go deleted file mode 100644 index c02bb7c629..0000000000 --- a/internal/api/grpc/user/schema/v3alpha/server.go +++ /dev/null @@ -1,51 +0,0 @@ -package schema - -import ( - "google.golang.org/grpc" - - "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/grpc/server" - "github.com/zitadel/zitadel/internal/command" - "github.com/zitadel/zitadel/internal/query" - schema "github.com/zitadel/zitadel/pkg/grpc/user/schema/v3alpha" -) - -var _ schema.UserSchemaServiceServer = (*Server)(nil) - -type Server struct { - schema.UnimplementedUserSchemaServiceServer - command *command.Commands - query *query.Queries -} - -type Config struct{} - -func CreateServer( - command *command.Commands, - query *query.Queries, -) *Server { - return &Server{ - command: command, - query: query, - } -} - -func (s *Server) RegisterServer(grpcServer *grpc.Server) { - schema.RegisterUserSchemaServiceServer(grpcServer, s) -} - -func (s *Server) AppName() string { - return schema.UserSchemaService_ServiceDesc.ServiceName -} - -func (s *Server) MethodPrefix() string { - return schema.UserSchemaService_ServiceDesc.ServiceName -} - -func (s *Server) AuthMethods() authz.MethodMapping { - return schema.UserSchemaService_AuthMethods -} - -func (s *Server) RegisterGateway() server.RegisterGatewayFunc { - return schema.RegisterUserSchemaServiceHandler -} diff --git a/internal/api/grpc/user/v2/idp_link.go b/internal/api/grpc/user/v2/idp_link.go index 5567ab24a2..bef40617cf 100644 --- a/internal/api/grpc/user/v2/idp_link.go +++ b/internal/api/grpc/user/v2/idp_link.go @@ -30,11 +30,10 @@ func (s *Server) ListIDPLinks(ctx context.Context, req *user.ListIDPLinksRequest if err != nil { return nil, err } - res, err := s.query.IDPUserLinks(ctx, queries, false) + res, err := s.query.IDPUserLinks(ctx, queries, s.checkPermission) if err != nil { return nil, err } - res.RemoveNoPermission(ctx, s.checkPermission) return &user.ListIDPLinksResponse{ Result: IDPLinksToPb(res.Links), Details: object.ToListDetails(res.SearchResponse), diff --git a/internal/api/grpc/user/v2/idp_link_integration_test.go b/internal/api/grpc/user/v2/idp_link_integration_test.go index 6b85c80f98..99658b3024 100644 --- a/internal/api/grpc/user/v2/idp_link_integration_test.go +++ b/internal/api/grpc/user/v2/idp_link_integration_test.go @@ -122,6 +122,16 @@ func TestServer_ListIDPLinks(t *testing.T) { want *user.ListIDPLinksResponse wantErr bool }{ + { + name: "list links, missing userID", + args: args{ + IamCTX, + &user.ListIDPLinksRequest{ + UserId: "", + }, + }, + wantErr: true, + }, { name: "list links, no permission", args: args{ @@ -130,13 +140,7 @@ func TestServer_ListIDPLinks(t *testing.T) { UserId: userOrgResp.GetUserId(), }, }, - want: &user.ListIDPLinksResponse{ - Details: &object.ListDetails{ - TotalResult: 0, - Timestamp: timestamppb.Now(), - }, - Result: []*user.IDPLink{}, - }, + wantErr: true, }, { name: "list links, no permission, org", @@ -146,13 +150,7 @@ func TestServer_ListIDPLinks(t *testing.T) { UserId: userOrgResp.GetUserId(), }, }, - want: &user.ListIDPLinksResponse{ - Details: &object.ListDetails{ - TotalResult: 0, - Timestamp: timestamppb.Now(), - }, - Result: []*user.IDPLink{}, - }, + wantErr: true, }, { name: "list idp links, org, ok", diff --git a/internal/api/grpc/user/v2/passkey.go b/internal/api/grpc/user/v2/passkey.go index bf539e252b..145c1e5716 100644 --- a/internal/api/grpc/user/v2/passkey.go +++ b/internal/api/grpc/user/v2/passkey.go @@ -142,8 +142,7 @@ func (s *Server) ListPasskeys(ctx context.Context, req *user.ListPasskeysRequest if err != nil { return nil, err } - authMethods, err := s.query.SearchUserAuthMethods(ctx, query, false) - authMethods.RemoveNoPermission(ctx, s.checkPermission) + authMethods, err := s.query.SearchUserAuthMethods(ctx, query, s.checkPermission) if err != nil { return nil, err } diff --git a/internal/api/grpc/user/v2/passkey_integration_test.go b/internal/api/grpc/user/v2/passkey_integration_test.go index 027005c438..12d3a6622b 100644 --- a/internal/api/grpc/user/v2/passkey_integration_test.go +++ b/internal/api/grpc/user/v2/passkey_integration_test.go @@ -471,6 +471,16 @@ func TestServer_ListPasskeys(t *testing.T) { want *user.ListPasskeysResponse wantErr bool }{ + { + name: "list passkeys, no userID", + args: args{ + IamCTX, + &user.ListPasskeysRequest{ + UserId: "", + }, + }, + wantErr: true, + }, { name: "list passkeys, no permission", args: args{ @@ -479,18 +489,12 @@ func TestServer_ListPasskeys(t *testing.T) { UserId: userIDVerified, }, }, - want: &user.ListPasskeysResponse{ - Details: &object.ListDetails{ - TotalResult: 0, - Timestamp: timestamppb.Now(), - }, - Result: []*user.Passkey{}, - }, + wantErr: true, }, { name: "list passkeys, none", args: args{ - UserCTX, + IamCTX, &user.ListPasskeysRequest{ UserId: userIDWithout, }, @@ -506,7 +510,7 @@ func TestServer_ListPasskeys(t *testing.T) { { name: "list passkeys, registered", args: args{ - UserCTX, + IamCTX, &user.ListPasskeysRequest{ UserId: userIDRegistered, }, diff --git a/internal/api/grpc/user/v2/query.go b/internal/api/grpc/user/v2/query.go index d40e4d47d9..564d4c1c0a 100644 --- a/internal/api/grpc/user/v2/query.go +++ b/internal/api/grpc/user/v2/query.go @@ -6,7 +6,6 @@ import ( "github.com/muhlemmer/gu" "google.golang.org/protobuf/types/known/timestamppb" - "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/grpc/object/v2" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/query" @@ -15,15 +14,10 @@ import ( ) func (s *Server) GetUserByID(ctx context.Context, req *user.GetUserByIDRequest) (_ *user.GetUserByIDResponse, err error) { - resp, err := s.query.GetUserByID(ctx, true, req.GetUserId()) + resp, err := s.query.GetUserByIDWithPermission(ctx, true, req.GetUserId(), s.checkPermission) if err != nil { return nil, err } - if authz.GetCtxData(ctx).UserID != req.GetUserId() { - if err := s.checkPermission(ctx, domain.PermissionUserRead, resp.ResourceOwner, req.GetUserId()); err != nil { - return nil, err - } - } return &user.GetUserByIDResponse{ Details: object.DomainToDetailsPb(&domain.ObjectDetails{ Sequence: resp.Sequence, diff --git a/internal/api/grpc/user/v2/user.go b/internal/api/grpc/user/v2/user.go index 9ad83dfea3..e46f6d3cb8 100644 --- a/internal/api/grpc/user/v2/user.go +++ b/internal/api/grpc/user/v2/user.go @@ -421,7 +421,7 @@ func (s *Server) checkLinkedExternalUser(ctx context.Context, idpID, externalUse queries := []query.SearchQuery{ idQuery, externalIDQuery, } - links, err := s.query.IDPUserLinks(ctx, &query.IDPUserLinksSearchQuery{Queries: queries}, false) + links, err := s.query.IDPUserLinks(ctx, &query.IDPUserLinksSearchQuery{Queries: queries}, nil) if err != nil { return "", err } diff --git a/internal/api/grpc/user/v2beta/query.go b/internal/api/grpc/user/v2beta/query.go index 28e0a0c2e7..4567259d15 100644 --- a/internal/api/grpc/user/v2beta/query.go +++ b/internal/api/grpc/user/v2beta/query.go @@ -6,7 +6,6 @@ import ( "github.com/muhlemmer/gu" "google.golang.org/protobuf/types/known/timestamppb" - "github.com/zitadel/zitadel/internal/api/authz" object "github.com/zitadel/zitadel/internal/api/grpc/object/v2beta" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/query" @@ -15,15 +14,10 @@ import ( ) func (s *Server) GetUserByID(ctx context.Context, req *user.GetUserByIDRequest) (_ *user.GetUserByIDResponse, err error) { - resp, err := s.query.GetUserByID(ctx, true, req.GetUserId()) + resp, err := s.query.GetUserByIDWithPermission(ctx, true, req.GetUserId(), s.checkPermission) if err != nil { return nil, err } - if authz.GetCtxData(ctx).UserID != req.GetUserId() { - if err := s.checkPermission(ctx, domain.PermissionUserRead, resp.ResourceOwner, req.GetUserId()); err != nil { - return nil, err - } - } return &user.GetUserByIDResponse{ Details: object.DomainToDetailsPb(&domain.ObjectDetails{ Sequence: resp.Sequence, diff --git a/internal/api/grpc/user/v2beta/user.go b/internal/api/grpc/user/v2beta/user.go index 8e3151a0b0..b6029a0d6f 100644 --- a/internal/api/grpc/user/v2beta/user.go +++ b/internal/api/grpc/user/v2beta/user.go @@ -434,7 +434,7 @@ func (s *Server) checkLinkedExternalUser(ctx context.Context, idpID, externalUse queries := []query.SearchQuery{ idQuery, externalIDQuery, } - links, err := s.query.IDPUserLinks(ctx, &query.IDPUserLinksSearchQuery{Queries: queries}, false) + links, err := s.query.IDPUserLinks(ctx, &query.IDPUserLinksSearchQuery{Queries: queries}, nil) if err != nil { return "", err } diff --git a/internal/api/http/middleware/cache_interceptor.go b/internal/api/http/middleware/cache_interceptor.go index 04dbf2b6c9..de0c0df618 100644 --- a/internal/api/http/middleware/cache_interceptor.go +++ b/internal/api/http/middleware/cache_interceptor.go @@ -108,6 +108,11 @@ func (c *Cache) serializeHeaders(w http.ResponseWriter) { control := make([]string, 0, 6) pragma := false + // Do not overwrite cache-control header if set by business logic. + if w.Header().Get(http_utils.CacheControl) != "" { + return + } + if c.Cacheability != CacheabilityNotSet { control = append(control, string(c.Cacheability)) control = append(control, fmt.Sprintf("max-age=%v", c.MaxAge.Seconds())) diff --git a/internal/api/idp/idp.go b/internal/api/idp/idp.go index b058ba5c2a..3d46f029da 100644 --- a/internal/api/idp/idp.go +++ b/internal/api/idp/idp.go @@ -420,7 +420,7 @@ func (h *Handler) checkExternalUser(ctx context.Context, idpID, externalUserID s queries := []query.SearchQuery{ idQuery, externalIDQuery, } - links, err := h.queries.IDPUserLinks(ctx, &query.IDPUserLinksSearchQuery{Queries: queries}, false) + links, err := h.queries.IDPUserLinks(ctx, &query.IDPUserLinksSearchQuery{Queries: queries}, nil) if err != nil { return "", err } diff --git a/internal/api/oidc/access_token.go b/internal/api/oidc/access_token.go index 4ac1646edf..66da6e3ccf 100644 --- a/internal/api/oidc/access_token.go +++ b/internal/api/oidc/access_token.go @@ -52,7 +52,9 @@ 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) + verifier := op.NewAccessTokenVerifier(op.IssuerFromContext(ctx), s.accessTokenKeySet, + op.WithSupportedAccessTokenSigningAlgorithms(supportedSigningAlgs(ctx)...), + ) claims, err := op.VerifyAccessToken[*oidc.AccessTokenClaims](ctx, tkn, verifier) if err != nil { return nil, zerrors.ThrowPermissionDenied(err, "OIDC-Eib8e", "token is not valid or has expired") diff --git a/internal/api/oidc/auth_request.go b/internal/api/oidc/auth_request.go index c99e4dd124..dd765a3045 100644 --- a/internal/api/oidc/auth_request.go +++ b/internal/api/oidc/auth_request.go @@ -277,7 +277,7 @@ func (o *OPStorage) RevokeToken(ctx context.Context, token, userID, clientID str if zerrors.IsPreconditionFailed(err) { return oidc.ErrInvalidClient().WithDescription("token was not issued for this client") } - return oidc.ErrServerError().WithParent(err) + return oidc.ErrServerError().WithParent(err).WithReturnParentToClient(authz.GetFeatures(ctx).DebugOIDCParentError) } return o.revokeTokenV1(ctx, token, userID, clientID) @@ -293,14 +293,14 @@ func (o *OPStorage) revokeTokenV1(ctx context.Context, token, userID, clientID s if err == nil || zerrors.IsNotFound(err) { return nil } - return oidc.ErrServerError().WithParent(err) + return oidc.ErrServerError().WithParent(err).WithReturnParentToClient(authz.GetFeatures(ctx).DebugOIDCParentError) } accessToken, err := o.repo.TokenByIDs(ctx, userID, token) if err != nil { if zerrors.IsNotFound(err) { return nil } - return oidc.ErrServerError().WithParent(err) + return oidc.ErrServerError().WithParent(err).WithReturnParentToClient(authz.GetFeatures(ctx).DebugOIDCParentError) } if accessToken.ApplicationID != clientID { return oidc.ErrInvalidClient().WithDescription("token was not issued for this client") @@ -309,7 +309,7 @@ func (o *OPStorage) revokeTokenV1(ctx context.Context, token, userID, clientID s if err == nil || zerrors.IsNotFound(err) { return nil } - return oidc.ErrServerError().WithParent(err) + return oidc.ErrServerError().WithParent(err).WithReturnParentToClient(authz.GetFeatures(ctx).DebugOIDCParentError) } func (o *OPStorage) GetRefreshTokenInfo(ctx context.Context, clientID string, token string) (userID string, tokenID string, err error) { diff --git a/internal/api/oidc/client.go b/internal/api/oidc/client.go index 8dc7c58ad5..01e8203b51 100644 --- a/internal/api/oidc/client.go +++ b/internal/api/oidc/client.go @@ -975,13 +975,13 @@ func (s *Server) VerifyClient(ctx context.Context, r *op.Request[op.ClientCreden return s.clientCredentialsAuth(ctx, r.Data.ClientID, r.Data.ClientSecret) } - clientID, assertion, err := clientIDFromCredentials(r.Data) + clientID, assertion, err := clientIDFromCredentials(ctx, r.Data) if err != nil { return nil, err } client, err := s.query.GetOIDCClientByID(ctx, clientID, assertion) if zerrors.IsNotFound(err) { - return nil, oidc.ErrInvalidClient().WithParent(err).WithDescription("client not found") + return nil, oidc.ErrInvalidClient().WithParent(err).WithReturnParentToClient(authz.GetFeatures(ctx).DebugOIDCParentError).WithDescription("client not found") } if err != nil { return nil, err // defaults to server error @@ -1019,7 +1019,7 @@ func (s *Server) verifyClientAssertion(ctx context.Context, client *query.OIDCCl } verifier := op.NewJWTProfileVerifierKeySet(keySetMap(client.PublicKeys), op.IssuerFromContext(ctx), time.Hour, client.ClockSkew) if _, err := op.VerifyJWTAssertion(ctx, assertion, verifier); err != nil { - return oidc.ErrInvalidClient().WithParent(err).WithDescription("invalid assertion") + return oidc.ErrInvalidClient().WithParent(err).WithReturnParentToClient(authz.GetFeatures(ctx).DebugOIDCParentError).WithDescription("invalid assertion") } return nil } @@ -1035,10 +1035,11 @@ func (s *Server) verifyClientSecret(ctx context.Context, client *query.OIDCClien updated, err := s.hasher.Verify(client.HashedSecret, secret) spanPasswordComparison.EndWithError(err) if err != nil { - s.command.OIDCSecretCheckFailed(ctx, client.AppID, client.ProjectID, client.Settings.ResourceOwner) - return oidc.ErrInvalidClient().WithParent(err).WithDescription("invalid secret") + return oidc.ErrInvalidClient().WithParent(err).WithReturnParentToClient(authz.GetFeatures(ctx).DebugOIDCParentError).WithDescription("invalid secret") + } + if updated != "" { + s.command.OIDCUpdateSecret(ctx, client.AppID, client.ProjectID, client.Settings.ResourceOwner, updated) } - s.command.OIDCSecretCheckSucceeded(ctx, client.AppID, client.ProjectID, client.Settings.ResourceOwner, updated) return nil } diff --git a/internal/api/oidc/client_converter.go b/internal/api/oidc/client_converter.go index 1a9d86afb6..c84049d2ad 100644 --- a/internal/api/oidc/client_converter.go +++ b/internal/api/oidc/client_converter.go @@ -1,6 +1,7 @@ package oidc import ( + "context" "slices" "strings" "time" @@ -8,6 +9,7 @@ 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/query" @@ -218,11 +220,11 @@ func removeScopeWithPrefix(scopes []string, scopePrefix ...string) []string { return newScopeList } -func clientIDFromCredentials(cc *op.ClientCredentials) (clientID string, assertion bool, err error) { +func clientIDFromCredentials(ctx context.Context, cc *op.ClientCredentials) (clientID string, assertion bool, err error) { if cc.ClientAssertion != "" { claims := new(oidc.JWTTokenRequest) if _, err := oidc.ParseToken(cc.ClientAssertion, claims); err != nil { - return "", false, oidc.ErrInvalidClient().WithParent(err) + return "", false, oidc.ErrInvalidClient().WithParent(err).WithReturnParentToClient(authz.GetFeatures(ctx).DebugOIDCParentError) } return claims.Issuer, true, nil } diff --git a/internal/api/oidc/client_credentials.go b/internal/api/oidc/client_credentials.go index b2821bcb70..b8c2a10a59 100644 --- a/internal/api/oidc/client_credentials.go +++ b/internal/api/oidc/client_credentials.go @@ -8,6 +8,7 @@ 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/query" "github.com/zitadel/zitadel/internal/telemetry/tracing" "github.com/zitadel/zitadel/internal/zerrors" @@ -37,7 +38,7 @@ func (c *clientCredentialsRequest) GetScopes() []string { func (s *Server) clientCredentialsAuth(ctx context.Context, clientID, clientSecret string) (op.Client, error) { user, err := s.query.GetUserByLoginName(ctx, false, clientID) if zerrors.IsNotFound(err) { - return nil, oidc.ErrInvalidClient().WithParent(err).WithDescription("client not found") + return nil, oidc.ErrInvalidClient().WithParent(err).WithReturnParentToClient(authz.GetFeatures(ctx).DebugOIDCParentError).WithDescription("client not found") } if err != nil { return nil, err // defaults to server error diff --git a/internal/api/oidc/introspect.go b/internal/api/oidc/introspect.go index 99602393c5..868676c1f1 100644 --- a/internal/api/oidc/introspect.go +++ b/internal/api/oidc/introspect.go @@ -156,18 +156,18 @@ func (s *Server) introspectionClientAuth(ctx context.Context, cc *op.ClientCrede if cc.ClientAssertion != "" { verifier := op.NewJWTProfileVerifierKeySet(keySetMap(client.PublicKeys), op.IssuerFromContext(ctx), time.Hour, time.Second) if _, err := op.VerifyJWTAssertion(ctx, cc.ClientAssertion, verifier); err != nil { - return "", "", false, oidc.ErrUnauthorizedClient().WithParent(err) + return "", "", false, oidc.ErrUnauthorizedClient().WithParent(err).WithReturnParentToClient(authz.GetFeatures(ctx).DebugOIDCParentError) } return client.ClientID, client.ProjectID, client.ProjectRoleAssertion, nil } if client.HashedSecret != "" { if err := s.introspectionClientSecretAuth(ctx, client, cc.ClientSecret); err != nil { - return "", "", false, oidc.ErrUnauthorizedClient().WithParent(err) + return "", "", false, oidc.ErrUnauthorizedClient().WithParent(err).WithReturnParentToClient(authz.GetFeatures(ctx).DebugOIDCParentError) } return client.ClientID, client.ProjectID, client.ProjectRoleAssertion, nil } - return "", "", false, oidc.ErrUnauthorizedClient().WithParent(errNoClientSecret) + return "", "", false, oidc.ErrUnauthorizedClient().WithParent(errNoClientSecret).WithReturnParentToClient(authz.GetFeatures(ctx).DebugOIDCParentError) }() span.EndWithError(err) @@ -183,17 +183,13 @@ func (s *Server) introspectionClientAuth(ctx context.Context, cc *op.ClientCrede var errNoAppType = errors.New("introspection client without app type") func (s *Server) introspectionClientSecretAuth(ctx context.Context, client *query.IntrospectionClient, secret string) error { - var ( - successCommand func(ctx context.Context, appID, projectID, resourceOwner, updated string) - failedCommand func(ctx context.Context, appID, projectID, resourceOwner string) - ) + var updateCommand func(ctx context.Context, appID, projectID, resourceOwner, updated string) + switch client.AppType { case query.AppTypeAPI: - successCommand = s.command.APISecretCheckSucceeded - failedCommand = s.command.APISecretCheckFailed + updateCommand = s.command.APIUpdateSecret case query.AppTypeOIDC: - successCommand = s.command.OIDCSecretCheckSucceeded - failedCommand = s.command.OIDCSecretCheckFailed + updateCommand = s.command.OIDCUpdateSecret default: return zerrors.ThrowInternal(errNoAppType, "OIDC-ooD5Ot", "Errors.Internal") } @@ -202,23 +198,24 @@ func (s *Server) introspectionClientSecretAuth(ctx context.Context, client *quer updated, err := s.hasher.Verify(client.HashedSecret, secret) spanPasswordComparison.EndWithError(err) if err != nil { - failedCommand(ctx, client.AppID, client.ProjectID, client.ResourceOwner) return err } - successCommand(ctx, client.AppID, client.ProjectID, client.ResourceOwner, updated) + if updated != "" { + updateCommand(ctx, client.AppID, client.ProjectID, client.ResourceOwner, updated) + } return nil } // clientFromCredentials parses the client ID early, // and makes a single query for the client for either auth methods. func (s *Server) clientFromCredentials(ctx context.Context, cc *op.ClientCredentials) (client *query.IntrospectionClient, err error) { - clientID, assertion, err := clientIDFromCredentials(cc) + clientID, assertion, err := clientIDFromCredentials(ctx, cc) if err != nil { return nil, err } client, err = s.query.GetIntrospectionClientByID(ctx, clientID, assertion) if errors.Is(err, sql.ErrNoRows) { - return nil, oidc.ErrUnauthorizedClient().WithParent(err) + return nil, oidc.ErrUnauthorizedClient().WithParent(err).WithReturnParentToClient(authz.GetFeatures(ctx).DebugOIDCParentError) } // any other error is regarded internal and should not be reported back to the client. return client, err diff --git a/internal/api/oidc/key.go b/internal/api/oidc/key.go index 2db5baf832..a7e156fe78 100644 --- a/internal/api/oidc/key.go +++ b/internal/api/oidc/key.go @@ -3,16 +3,19 @@ package oidc import ( "context" "fmt" + "slices" "sync" "sync/atomic" "time" "github.com/go-jose/go-jose/v4" "github.com/jonboulle/clockwork" + "github.com/muhlemmer/gu" "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" @@ -22,14 +25,33 @@ import ( "github.com/zitadel/zitadel/internal/zerrors" ) -type cachedPublicKey struct { - lastUse atomic.Int64 // unix micro time. - query.PublicKey +var supportedWebKeyAlgs = []string{ + string(jose.EdDSA), + string(jose.RS256), + string(jose.RS384), + string(jose.RS512), + string(jose.ES256), + string(jose.ES384), + string(jose.ES512), } -func newCachedPublicKey(key query.PublicKey, now time.Time) *cachedPublicKey { +func supportedSigningAlgs(ctx context.Context) []string { + if authz.GetFeatures(ctx).WebKey { + return supportedWebKeyAlgs + } + return []string{string(jose.RS256)} +} + +type cachedPublicKey struct { + lastUse atomic.Int64 // unix micro time. + expiry *time.Time // expiry may be nil if the key does not expire. + webKey *jose.JSONWebKey +} + +func newCachedPublicKey(key *jose.JSONWebKey, expiry *time.Time, now time.Time) *cachedPublicKey { cachedKey := &cachedPublicKey{ - PublicKey: key, + expiry: expiry, + webKey: key, } cachedKey.setLastUse(now) return cachedKey @@ -53,14 +75,17 @@ func (c *cachedPublicKey) expired(now time.Time, validity time.Duration) bool { type publicKeyCache struct { mtx sync.RWMutex instanceKeys map[string]map[string]*cachedPublicKey - queryKey func(ctx context.Context, keyID string) (query.PublicKey, error) - clock clockwork.Clock + + // queryKey returns a public web key. + // If the key does not have expiry, Time may be nil. + queryKey func(ctx context.Context, keyID string) (*jose.JSONWebKey, *time.Time, error) + clock clockwork.Clock } // newPublicKeyCache initializes a keySetCache starts a purging Go routine. // The purge routine deletes all public keys that are older than maxAge. // When the passed context is done, the purge routine will terminate. -func newPublicKeyCache(background context.Context, maxAge time.Duration, queryKey func(ctx context.Context, keyID string) (query.PublicKey, error)) *publicKeyCache { +func newPublicKeyCache(background context.Context, maxAge time.Duration, queryKey func(ctx context.Context, keyID string) (*jose.JSONWebKey, *time.Time, error)) *publicKeyCache { k := &publicKeyCache{ instanceKeys: make(map[string]map[string]*cachedPublicKey), queryKey: queryKey, @@ -119,11 +144,11 @@ func (k *publicKeyCache) getKey(ctx context.Context, keyID string) (_ *cachedPub if ok { key.setLastUse(k.clock.Now()) } else { - newKey, err := k.queryKey(ctx, keyID) + newKey, expiry, err := k.queryKey(ctx, keyID) if err != nil { return nil, err } - key = newCachedPublicKey(newKey, k.clock.Now()) + key = newCachedPublicKey(newKey, expiry, k.clock.Now()) k.setKey(instanceID, keyID, key) } @@ -144,10 +169,10 @@ func (k *publicKeyCache) verifySignature(ctx context.Context, jws *jose.JSONWebS if err != nil { return nil, err } - if checkKeyExpiry && key.Expiry().Before(k.clock.Now()) { + if checkKeyExpiry && key.expiry != nil && key.expiry.Before(k.clock.Now()) { return nil, zerrors.ThrowInvalidArgument(err, "QUERY-ciF4k", "Errors.Key.ExpireBeforeNow") } - return jws.Verify(jsonWebkey(key)) + return jws.Verify(key.webKey) } type oidcKeySet struct { @@ -423,3 +448,68 @@ func retry(retryable func() error) (err error) { } return err } + +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 +} + +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(), + }) + } +} + +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) + if err != nil { + return nil, nil, err + } + return jsonWebkey(pubKey), gu.Ptr(pubKey.Expiry()), nil + } +} diff --git a/internal/api/oidc/key_test.go b/internal/api/oidc/key_test.go index 3f84722a9b..b495b4132a 100644 --- a/internal/api/oidc/key_test.go +++ b/internal/api/oidc/key_test.go @@ -2,12 +2,14 @@ package oidc import ( "context" + "crypto/rand" "errors" "testing" "time" "github.com/go-jose/go-jose/v4" "github.com/jonboulle/clockwork" + "github.com/muhlemmer/gu" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -51,36 +53,45 @@ func (k *publicKey) Key() any { var ( clock = clockwork.NewFakeClock() - keyDB = map[string]*publicKey{ + keyDB = map[string]struct { + webKey *jose.JSONWebKey + expiry *time.Time + }{ "key1": { - id: "key1", - alg: "alg", - use: crypto.KeyUsageSigning, - seq: 1, - expiry: clock.Now().Add(time.Minute), + webKey: &jose.JSONWebKey{ + Key: "abc", + KeyID: "key1", + Algorithm: "alg", + Use: "sig", + }, + expiry: gu.Ptr(clock.Now().Add(time.Minute)), }, "key2": { - id: "key2", - alg: "alg", - use: crypto.KeyUsageSigning, - seq: 3, - expiry: clock.Now().Add(10 * time.Hour), + webKey: &jose.JSONWebKey{ + Key: "def", + KeyID: "key1", + Algorithm: "alg", + Use: "sig", + }, + expiry: gu.Ptr(clock.Now().Add(10 * time.Hour)), }, "exp1": { - id: "key2", - alg: "alg", - use: crypto.KeyUsageSigning, - seq: 4, - expiry: clock.Now().Add(-time.Hour), + webKey: &jose.JSONWebKey{ + Key: "ghi", + KeyID: "exp1", + Algorithm: "alg", + Use: "sig", + }, + expiry: gu.Ptr(clock.Now().Add(-time.Hour)), }, } ) -func queryKeyDB(_ context.Context, keyID string) (query.PublicKey, error) { +func queryKeyDB(_ context.Context, keyID string) (*jose.JSONWebKey, *time.Time, error) { if key, ok := keyDB[keyID]; ok { - return key, nil + return key.webKey, key.expiry, nil } - return nil, errors.New("not found") + return nil, nil, errors.New("not found") } func Test_publicKeyCache(t *testing.T) { @@ -102,7 +113,7 @@ func Test_publicKeyCache(t *testing.T) { got, err := cache.getKey(ctx, "key1") require.NoError(t, err) require.NotNil(t, got) - assert.Equal(t, keyDB["key1"], got.PublicKey) + assert.Equal(t, keyDB["key1"].webKey, got.webKey) // move time forward clock.Advance(15 * time.Minute) @@ -122,7 +133,7 @@ func Test_publicKeyCache(t *testing.T) { got, err = cache.getKey(ctx, "key2") require.NoError(t, err) require.NotNil(t, got) - assert.Equal(t, keyDB["key2"], got.PublicKey) + assert.Equal(t, keyDB["key2"].webKey, got.webKey) // move time forward clock.Advance(15 * time.Minute) @@ -140,7 +151,7 @@ func Test_publicKeyCache(t *testing.T) { got, err = cache.getKey(ctx, "key2") require.NoError(t, err) require.NotNil(t, got) - assert.Equal(t, keyDB["key2"], got.PublicKey) + assert.Equal(t, keyDB["key2"].webKey, got.webKey) // move time forward clock.Advance(2 * time.Hour) @@ -266,3 +277,126 @@ func Test_keySetMap_VerifySignature(t *testing.T) { }) } } + +func Test_appendPublicKeysToWebKeySet(t *testing.T) { + keys := [...][]byte{ + make([]byte, 32), + make([]byte, 32), + } + for _, key := range keys { + _, err := rand.Read(key) + require.NoError(t, err) + } + + type args struct { + keyset *jose.JSONWebKeySet + pubkeys *query.PublicKeys + } + tests := []struct { + name string + args args + want *jose.JSONWebKeySet + }{ + { + name: "nil pubkeys", + args: args{ + keyset: &jose.JSONWebKeySet{ + Keys: []jose.JSONWebKey{ + { + Key: keys[0], + KeyID: "key0", + Algorithm: "XYZ", + Use: crypto.KeyUsageSigning.String(), + }, + }, + }, + pubkeys: nil, + }, + want: &jose.JSONWebKeySet{ + Keys: []jose.JSONWebKey{ + { + Key: keys[0], + KeyID: "key0", + Algorithm: "XYZ", + Use: crypto.KeyUsageSigning.String(), + }, + }, + }, + }, + { + name: "empty pubkeys", + args: args{ + keyset: &jose.JSONWebKeySet{ + Keys: []jose.JSONWebKey{ + { + Key: keys[0], + KeyID: "key0", + Algorithm: "XYZ", + Use: crypto.KeyUsageSigning.String(), + }, + }, + }, + pubkeys: &query.PublicKeys{ + Keys: []query.PublicKey{}, + }, + }, + want: &jose.JSONWebKeySet{ + Keys: []jose.JSONWebKey{ + { + Key: keys[0], + KeyID: "key0", + Algorithm: "XYZ", + Use: crypto.KeyUsageSigning.String(), + }, + }, + }, + }, + { + name: "append pubkeys", + args: args{ + keyset: &jose.JSONWebKeySet{ + Keys: []jose.JSONWebKey{ + { + Key: keys[0], + KeyID: "key0", + Algorithm: "XYZ", + Use: crypto.KeyUsageSigning.String(), + }, + }, + }, + pubkeys: &query.PublicKeys{ + Keys: []query.PublicKey{ + &publicKey{ + id: "key1", + key: keys[1], + alg: "XYZ", + use: crypto.KeyUsageSigning, + }, + }, + }, + }, + want: &jose.JSONWebKeySet{ + Keys: []jose.JSONWebKey{ + { + Key: keys[0], + KeyID: "key0", + Algorithm: "XYZ", + Use: crypto.KeyUsageSigning.String(), + }, + { + Key: keys[1], + KeyID: "key1", + Algorithm: "XYZ", + Use: crypto.KeyUsageSigning.String(), + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + appendPublicKeysToWebKeySet(tt.args.keyset, tt.args.pubkeys) + assert.Equal(t, tt.want, tt.args.keyset) + }) + } +} diff --git a/internal/api/oidc/keys_integration_test.go b/internal/api/oidc/keys_integration_test.go new file mode 100644 index 0000000000..a78e4fc1b8 --- /dev/null +++ b/internal/api/oidc/keys_integration_test.go @@ -0,0 +1,115 @@ +//go:build integration + +package oidc_test + +import ( + "crypto/rsa" + "encoding/json" + "net/http" + "testing" + "time" + + "github.com/go-jose/go-jose/v4" + "github.com/stretchr/testify/assert" + "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/pkg/grpc/feature/v2" + oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2" +) + +func TestServer_Keys(t *testing.T) { + // TODO: isolated instance + + clientID, _ := createClient(t) + authRequestID := createAuthRequest(t, clientID, redirectURI, oidc.ScopeOpenID, oidc.ScopeOfflineAccess, zitadelAudienceScope) + sessionID, sessionToken, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTXLOGIN, User.GetUserId()) + linkResp, err := Tester.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{ + AuthRequestId: authRequestID, + CallbackKind: &oidc_pb.CreateCallbackRequest_Session{ + Session: &oidc_pb.Session{ + SessionId: sessionID, + SessionToken: sessionToken, + }, + }, + }) + require.NoError(t, err) + + // code exchange so we are sure there is 1 legacy key pair. + code := assertCodeResponse(t, linkResp.GetCallbackUrl()) + _, err = exchangeTokens(t, clientID, code, redirectURI) + require.NoError(t, err) + + issuer := http_util.BuildHTTP(Tester.Config.ExternalDomain, Tester.Config.Port, Tester.Config.ExternalSecure) + discovery, err := client.Discover(CTX, issuer, http.DefaultClient) + require.NoError(t, err) + + tests := []struct { + name string + webKeyFeature bool + 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 + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ensureWebKeyFeature(t, tt.webKeyFeature) + + assert.EventuallyWithT(t, func(ttt *assert.CollectT) { + resp, err := http.Get(discovery.JwksURI) + require.NoError(ttt, err) + require.Equal(ttt, resp.StatusCode, http.StatusOK) + defer resp.Body.Close() + + got := new(jose.JSONWebKeySet) + err = json.NewDecoder(resp.Body).Decode(got) + require.NoError(ttt, err) + + assert.Len(t, got.Keys, tt.wantLen) + for _, key := range got.Keys { + _, ok := key.Key.(*rsa.PublicKey) + require.True(ttt, ok) + require.NotEmpty(ttt, key.KeyID) + require.Equal(ttt, key.Algorithm, string(jose.RS256)) + require.Equal(ttt, key.Use, crypto.KeyUsageSigning.String()) + } + + 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) + + }, time.Minute, time.Second/10) + }) + + } +} + +func ensureWebKeyFeature(t *testing.T, set bool) { + _, err := Tester.Client.FeatureV2.SetInstanceFeatures(CTXIAM, &feature.SetInstanceFeaturesRequest{ + WebKey: proto.Bool(set), + }) + require.NoError(t, err) + + t.Cleanup(func() { + _, err := Tester.Client.FeatureV2.SetInstanceFeatures(CTXIAM, &feature.SetInstanceFeaturesRequest{ + WebKey: proto.Bool(false), + }) + require.NoError(t, err) + }) +} diff --git a/internal/api/oidc/op.go b/internal/api/oidc/op.go index 67bc1765a5..c8dafb50f3 100644 --- a/internal/api/oidc/op.go +++ b/internal/api/oidc/op.go @@ -36,8 +36,7 @@ type Config struct { DefaultIdTokenLifetime time.Duration DefaultRefreshTokenIdleExpiration time.Duration DefaultRefreshTokenExpiration time.Duration - UserAgentCookieConfig *middleware.UserAgentCookieConfig - Cache *middleware.CacheConfig + JWKSCacheControlMaxAge time.Duration CustomEndpoints *EndpointConfig DeviceAuth *DeviceAuthorizationConfig DefaultLoginURLV2 string @@ -79,6 +78,27 @@ type OPStorage struct { assetAPIPrefix func(ctx context.Context) string } +// Provider is used to overload certain [op.Provider] methods +type Provider struct { + *op.Provider + accessTokenKeySet oidc.KeySet + idTokenHintKeySet oidc.KeySet +} + +// 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)..., + )) +} + +// 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)..., + )) +} + func NewServer( ctx context.Context, config Config, @@ -101,14 +121,11 @@ func NewServer( return nil, zerrors.ThrowInternal(err, "OIDC-EGrqd", "cannot create op config: %w") } storage := newStorage(config, command, query, repo, encryptionAlg, es, projections) - keyCache := newPublicKeyCache(ctx, config.PublicKeyCacheMaxAge, query.GetPublicKeyByID) + keyCache := newPublicKeyCache(ctx, config.PublicKeyCacheMaxAge, queryKeyFunc(query)) accessTokenKeySet := newOidcKeySet(keyCache, withKeyExpiryCheck(true)) idTokenHintKeySet := newOidcKeySet(keyCache) - options := []op.Option{ - op.WithAccessTokenKeySet(accessTokenKeySet), - op.WithIDTokenHintKeySet(idTokenHintKeySet), - } + var options []op.Option if !externalSecure { options = append(options, op.WithAllowInsecure()) } @@ -126,7 +143,11 @@ func NewServer( return nil, zerrors.ThrowInternal(err, "OIDC-Aij4e", "cannot create secret hasher") } server := &Server{ - LegacyServer: op.NewLegacyServer(provider, endpoints(config.CustomEndpoints)), + LegacyServer: op.NewLegacyServer(&Provider{ + Provider: provider, + accessTokenKeySet: accessTokenKeySet, + idTokenHintKeySet: idTokenHintKeySet, + }, endpoints(config.CustomEndpoints)), repo: repo, query: query, command: command, @@ -137,6 +158,7 @@ func NewServer( defaultLogoutURLV2: config.DefaultLogoutURLV2, defaultAccessTokenLifetime: config.DefaultAccessTokenLifetime, defaultIdTokenLifetime: config.DefaultIdTokenLifetime, + jwksCacheControlMaxAge: config.JWKSCacheControlMaxAge, fallbackLogger: fallbackLogger, hasher: hasher, signingKeyAlgorithm: config.SigningKeyAlgorithm, diff --git a/internal/api/oidc/server.go b/internal/api/oidc/server.go index b0a062b74d..15628cad8a 100644 --- a/internal/api/oidc/server.go +++ b/internal/api/oidc/server.go @@ -10,6 +10,7 @@ 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/auth/repository" "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/crypto" @@ -33,6 +34,7 @@ type Server struct { defaultLogoutURLV2 string defaultAccessTokenLifetime time.Duration defaultIdTokenLifetime time.Duration + jwksCacheControlMaxAge time.Duration fallbackLogger *slog.Logger hasher *crypto.Hasher @@ -119,7 +121,7 @@ func (s *Server) Discovery(ctx context.Context, r *op.Request[struct{}]) (_ *op. }() restrictions, err := s.query.GetInstanceRestrictions(ctx) if err != nil { - return nil, op.NewStatusError(oidc.ErrServerError().WithParent(err).WithDescription("internal server error"), http.StatusInternalServerError) + return nil, op.NewStatusError(oidc.ErrServerError().WithParent(err).WithReturnParentToClient(authz.GetFeatures(ctx).DebugOIDCParentError).WithDescription("internal server error"), http.StatusInternalServerError) } allowedLanguages := restrictions.AllowedLanguages if len(allowedLanguages) == 0 { @@ -128,13 +130,6 @@ func (s *Server) Discovery(ctx context.Context, r *op.Request[struct{}]) (_ *op. return op.NewResponse(s.createDiscoveryConfig(ctx, allowedLanguages)), nil } -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) }() - - return s.LegacyServer.Keys(ctx, r) -} - func (s *Server) VerifyAuthRequest(ctx context.Context, r *op.Request[oidc.AuthRequest]) (_ *op.ClientRequest[oidc.AuthRequest], err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() @@ -172,6 +167,7 @@ func (s *Server) EndSession(ctx context.Context, r *op.Request[oidc.EndSessionRe func (s *Server) createDiscoveryConfig(ctx context.Context, supportedUILocales oidc.Locales) *oidc.DiscoveryConfiguration { issuer := op.IssuerFromContext(ctx) + return &oidc.DiscoveryConfiguration{ Issuer: issuer, AuthorizationEndpoint: s.Endpoints().Authorization.Absolute(issuer), @@ -191,7 +187,7 @@ func (s *Server) createDiscoveryConfig(ctx context.Context, supportedUILocales o }, GrantTypesSupported: op.GrantTypes(s.Provider()), SubjectTypesSupported: op.SubjectTypes(s.Provider()), - IDTokenSigningAlgValuesSupported: []string{s.signingKeyAlgorithm}, + IDTokenSigningAlgValuesSupported: supportedSigningAlgs(ctx), 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 19404933ba..47f24fcdee 100644 --- a/internal/api/oidc/server_test.go +++ b/internal/api/oidc/server_test.go @@ -7,6 +7,8 @@ import ( "github.com/stretchr/testify/assert" "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/feature" "golang.org/x/text/language" ) @@ -30,6 +32,7 @@ func TestServer_createDiscoveryConfig(t *testing.T) { fields{ LegacyServer: op.NewLegacyServer( func() *op.Provider { + //nolint:staticcheck provider, _ := op.NewForwardedOpenIDProvider("path", &op.Config{ CodeMethodS256: true, @@ -107,6 +110,92 @@ func TestServer_createDiscoveryConfig(t *testing.T) { 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", + 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: supportedWebKeyAlgs, + 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: "", + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/internal/api/oidc/token.go b/internal/api/oidc/token.go index be3a30ed73..56ed225902 100644 --- a/internal/api/oidc/token.go +++ b/internal/api/oidc/token.go @@ -12,9 +12,11 @@ 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" + "github.com/zitadel/zitadel/internal/zerrors" ) /* @@ -75,22 +77,43 @@ func (s *Server) getSignerOnce() signerFunc { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() + if authz.GetFeatures(ctx).WebKey { + var webKey *jose.JSONWebKey + webKey, err = s.query.GetActiveSigningWebKey(ctx) + if err != nil { + return + } + signer, signAlg, err = signerFromWebKey(webKey) + return + } + var signingKey op.SigningKey signingKey, err = s.Provider().Storage().SigningKey(ctx) if err != nil { return } signAlg = signingKey.SignatureAlgorithm() - signer, err = op.SignerFromKey(signingKey) - if err != nil { - return - } }) return signer, signAlg, err } } +func signerFromWebKey(signingKey *jose.JSONWebKey) (jose.Signer, jose.SignatureAlgorithm, error) { + signAlg := jose.SignatureAlgorithm(signingKey.Algorithm) + signer, err := jose.NewSigner( + jose.SigningKey{ + Algorithm: signAlg, + Key: signingKey, + }, + (&jose.SignerOptions{}).WithType("JWT"), + ) + if err != nil { + return nil, "", zerrors.ThrowInternal(err, "OIDC-oaF0s", "Errors.Internal") + } + return signer, signAlg, nil +} + // userInfoFunc is a getter function that allows add-hoc retrieval of a user. type userInfoFunc func(ctx context.Context, roleAssertion bool, triggerType domain.TriggerType) (*oidc.UserInfo, error) diff --git a/internal/api/oidc/token_device.go b/internal/api/oidc/token_device.go index b574af1260..464e9e46ae 100644 --- a/internal/api/oidc/token_device.go +++ b/internal/api/oidc/token_device.go @@ -7,6 +7,7 @@ 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" @@ -29,7 +30,7 @@ func (s *Server) DeviceToken(ctx context.Context, r *op.ClientRequest[oidc.Devic return response(s.accessTokenResponseFromSession(ctx, client, session, "", client.client.ProjectID, client.client.ProjectRoleAssertion, client.client.AccessTokenRoleAssertion, client.client.IDTokenRoleAssertion, client.client.IDTokenUserinfoAssertion)) } if errors.Is(err, context.DeadlineExceeded) { - return nil, oidc.ErrSlowDown().WithParent(err) + return nil, oidc.ErrSlowDown().WithParent(err).WithReturnParentToClient(authz.GetFeatures(ctx).DebugOIDCParentError) } var target command.DeviceAuthStateError @@ -42,5 +43,5 @@ func (s *Server) DeviceToken(ctx context.Context, r *op.ClientRequest[oidc.Devic return nil, oidc.ErrExpiredDeviceCode() } } - return nil, oidc.ErrAccessDenied().WithParent(err) + return nil, oidc.ErrAccessDenied().WithParent(err).WithReturnParentToClient(authz.GetFeatures(ctx).DebugOIDCParentError) } diff --git a/internal/api/oidc/token_exchange.go b/internal/api/oidc/token_exchange.go index c29b7eb80b..c94621a4d2 100644 --- a/internal/api/oidc/token_exchange.go +++ b/internal/api/oidc/token_exchange.go @@ -55,7 +55,7 @@ func (s *Server) tokenExchange(ctx context.Context, r *op.ClientRequest[oidc.Tok subjectToken, err := s.verifyExchangeToken(ctx, client, r.Data.SubjectToken, r.Data.SubjectTokenType, oidc.AllTokenTypes...) if err != nil { - return nil, oidc.ErrInvalidRequest().WithParent(err).WithDescription("subject_token invalid") + return nil, oidc.ErrInvalidRequest().WithParent(err).WithReturnParentToClient(authz.GetFeatures(ctx).DebugOIDCParentError).WithDescription("subject_token invalid") } actorToken := subjectToken // see [createExchangeTokens] comment. @@ -65,7 +65,7 @@ func (s *Server) tokenExchange(ctx context.Context, r *op.ClientRequest[oidc.Tok } actorToken, err = s.verifyExchangeToken(ctx, client, r.Data.ActorToken, r.Data.ActorTokenType, oidc.AccessTokenType, oidc.IDTokenType, oidc.RefreshTokenType) if err != nil { - return nil, oidc.ErrInvalidRequest().WithParent(err).WithDescription("actor_token invalid") + return nil, oidc.ErrInvalidRequest().WithParent(err).WithReturnParentToClient(authz.GetFeatures(ctx).DebugOIDCParentError).WithDescription("actor_token invalid") } ctx = authz.SetCtxData(ctx, authz.CtxData{ UserID: actorToken.userID, diff --git a/internal/api/oidc/userinfo.go b/internal/api/oidc/userinfo.go index 93dffb623a..e023d7a63d 100644 --- a/internal/api/oidc/userinfo.go +++ b/internal/api/oidc/userinfo.go @@ -42,7 +42,7 @@ func (s *Server) UserInfo(ctx context.Context, r *op.Request[oidc.UserInfoReques token, err := s.verifyAccessToken(ctx, r.Data.AccessToken) if err != nil { - return nil, op.NewStatusError(oidc.ErrAccessDenied().WithDescription("access token invalid").WithParent(err), http.StatusUnauthorized) + return nil, op.NewStatusError(oidc.ErrAccessDenied().WithDescription("access token invalid").WithParent(err).WithReturnParentToClient(authz.GetFeatures(ctx).DebugOIDCParentError), http.StatusUnauthorized) } var ( diff --git a/internal/api/oidc/userinfo_integration_test.go b/internal/api/oidc/userinfo_integration_test.go index ad95defd6f..e938078078 100644 --- a/internal/api/oidc/userinfo_integration_test.go +++ b/internal/api/oidc/userinfo_integration_test.go @@ -36,6 +36,7 @@ func TestServer_UserInfo(t *testing.T) { name string legacy bool trigger bool + webKey bool }{ { name: "legacy enabled", @@ -51,6 +52,17 @@ func TestServer_UserInfo(t *testing.T) { legacy: false, 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", + legacy: false, + trigger: false, + webKey: true, + }, } for _, tt := range tests { @@ -58,6 +70,7 @@ func TestServer_UserInfo(t *testing.T) { _, err := Tester.Client.FeatureV2.SetInstanceFeatures(iamOwnerCTX, &feature.SetInstanceFeaturesRequest{ OidcLegacyIntrospection: &tt.legacy, OidcTriggerIntrospectionProjections: &tt.trigger, + WebKey: &tt.webKey, }) require.NoError(t, err) testServer_UserInfo(t) diff --git a/internal/api/ui/login/custom_action.go b/internal/api/ui/login/custom_action.go index 7beda6133f..6e8054943e 100644 --- a/internal/api/ui/login/custom_action.go +++ b/internal/api/ui/login/custom_action.go @@ -479,7 +479,7 @@ func (l *Login) resourceOwnerOfUserIDPLink(ctx context.Context, idpConfigID stri queries := []query.SearchQuery{ idQuery, externalIDQuery, } - links, err := l.query.IDPUserLinks(ctx, &query.IDPUserLinksSearchQuery{Queries: queries}, false) + links, err := l.query.IDPUserLinks(ctx, &query.IDPUserLinksSearchQuery{Queries: queries}, nil) if err != nil { return "", err } diff --git a/internal/api/ui/login/external_provider_handler.go b/internal/api/ui/login/external_provider_handler.go index a462f06a7d..1f835c855f 100644 --- a/internal/api/ui/login/external_provider_handler.go +++ b/internal/api/ui/login/external_provider_handler.go @@ -455,9 +455,9 @@ func (l *Login) handleExternalUserAuthenticated( // checkAutoLinking checks if a user with the provided information (username or email) already exists within ZITADEL. // The decision, which information will be checked is based on the IdP template option. // The function returns a boolean whether a user was found or not. +// If single a user was found, it will be automatically linked. func (l *Login) checkAutoLinking(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, provider *query.IDPTemplate, externalUser *domain.ExternalUser) bool { queries := make([]query.SearchQuery, 0, 2) - var user *query.NotifyUser switch provider.AutoLinking { case domain.AutoLinkingOptionUnspecified: // is auto linking is disable, we shouldn't even get here, but in case we do we can directly return @@ -472,7 +472,7 @@ func (l *Login) checkAutoLinking(w http.ResponseWriter, r *http.Request, authReq if err != nil { return false } - l.renderLinkingUserPrompt(w, r, authReq, user, nil) + l.autoLinkUser(w, r, authReq, user) return true } // If a specific org has been requested, we'll check the provided username against usernames (of that org). @@ -501,10 +501,22 @@ func (l *Login) checkAutoLinking(w http.ResponseWriter, r *http.Request, authReq if err != nil { return false } - l.renderLinkingUserPrompt(w, r, authReq, user, nil) + l.autoLinkUser(w, r, authReq, user) return true } +func (l *Login) autoLinkUser(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, user *query.NotifyUser) { + if err := l.authRepo.SelectUser(r.Context(), authReq.ID, user.ID, authReq.AgentID); err != nil { + l.renderError(w, r, authReq, err) + return + } + if err := l.authRepo.LinkExternalUsers(r.Context(), authReq.ID, authReq.AgentID, domain.BrowserInfoFromRequest(r)); err != nil { + l.renderError(w, r, authReq, err) + return + } + l.renderNextStep(w, r, authReq) +} + // externalUserNotExisting is called if an externalAuthentication couldn't find a corresponding externalID // possible solutions are: // @@ -846,7 +858,7 @@ func (l *Login) updateExternalUsername(ctx context.Context, user *query.User, ex if err != nil { return err } - links, err := l.query.IDPUserLinks(ctx, &query.IDPUserLinksSearchQuery{Queries: []query.SearchQuery{externalIDQuery, idpIDQuery, userIDQuery}}, false) + links, err := l.query.IDPUserLinks(ctx, &query.IDPUserLinksSearchQuery{Queries: []query.SearchQuery{externalIDQuery, idpIDQuery, userIDQuery}}, nil) if err != nil || len(links.Links) == 0 { return err } @@ -1326,6 +1338,6 @@ func (l *Login) getUserLinks(ctx context.Context, userID, idpID string) (*query. userIDQuery, idpIDQuery, }, - }, false, + }, nil, ) } diff --git a/internal/api/ui/login/link_prompt_handler.go b/internal/api/ui/login/link_prompt_handler.go deleted file mode 100644 index 30ac186298..0000000000 --- a/internal/api/ui/login/link_prompt_handler.go +++ /dev/null @@ -1,62 +0,0 @@ -package login - -import ( - "net/http" - - "github.com/zitadel/zitadel/internal/domain" - "github.com/zitadel/zitadel/internal/query" -) - -const ( - tmplLinkingUserPrompt = "link_user_prompt" -) - -type linkingUserPromptData struct { - userData - Username string - Linking domain.AutoLinkingOption - UserID string -} - -type linkingUserPromptFormData struct { - OtherUser bool `schema:"other"` - UserID string `schema:"userID"` -} - -func (l *Login) renderLinkingUserPrompt(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, user *query.NotifyUser, err error) { - var errID, errMessage string - if err != nil { - errID, errMessage = l.getErrorMessage(r, err) - } - translator := l.getTranslator(r.Context(), authReq) - identification := user.PreferredLoginName - // hide the suffix in case the option is set and the auth request has been started with the primary domain scope - if authReq.RequestedOrgDomain && authReq.LabelPolicy != nil && authReq.LabelPolicy.HideLoginNameSuffix { - identification = user.Username - } - data := &linkingUserPromptData{ - Username: identification, - UserID: user.ID, - userData: l.getUserData(r, authReq, translator, "LinkingUserPrompt.Title", "LinkingUserPrompt.Description", errID, errMessage), - } - l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplLinkingUserPrompt], data, nil) -} - -func (l *Login) handleLinkingUserPrompt(w http.ResponseWriter, r *http.Request) { - data := new(linkingUserPromptFormData) - authReq, err := l.ensureAuthRequestAndParseData(r, data) - if err != nil { - l.renderLogin(w, r, authReq, err) - return - } - if data.OtherUser { - l.renderExternalNotFoundOption(w, r, authReq, nil, nil, nil, nil) - return - } - err = l.authRepo.SelectUser(r.Context(), authReq.ID, data.UserID, authReq.AgentID) - if err != nil { - l.renderLogin(w, r, authReq, err) - return - } - l.renderNextStep(w, r, authReq) -} diff --git a/internal/api/ui/login/renderer.go b/internal/api/ui/login/renderer.go index 462e3fed93..3cfcb60cf0 100644 --- a/internal/api/ui/login/renderer.go +++ b/internal/api/ui/login/renderer.go @@ -83,7 +83,6 @@ func CreateRenderer(pathPrefix string, staticStorage static.Storage, cookieName tmplLDAPLogin: "ldap_login.html", tmplDeviceAuthUserCode: "device_usercode.html", tmplDeviceAuthAction: "device_action.html", - tmplLinkingUserPrompt: "link_user_prompt.html", } funcs := map[string]interface{}{ "resourceUrl": func(file string) string { diff --git a/internal/api/ui/login/router.go b/internal/api/ui/login/router.go index 1e5a297b06..a815784af5 100644 --- a/internal/api/ui/login/router.go +++ b/internal/api/ui/login/router.go @@ -124,6 +124,5 @@ func CreateRouter(login *Login, interceptors ...mux.MiddlewareFunc) *mux.Router router.SkipClean(true).Handle("", http.RedirectHandler(HandlerPrefix+"/", http.StatusMovedPermanently)) router.HandleFunc(EndpointDeviceAuth, login.handleDeviceAuthUserCode).Methods(http.MethodGet, http.MethodPost) router.HandleFunc(EndpointDeviceAuthAction, login.handleDeviceAuthAction).Methods(http.MethodGet, http.MethodPost) - router.HandleFunc(EndpointLinkingUserPrompt, login.handleLinkingUserPrompt).Methods(http.MethodPost) return router } diff --git a/internal/api/ui/login/static/templates/link_user_prompt.html b/internal/api/ui/login/static/templates/link_user_prompt.html deleted file mode 100644 index 9d6ed36cd1..0000000000 --- a/internal/api/ui/login/static/templates/link_user_prompt.html +++ /dev/null @@ -1,37 +0,0 @@ -{{template "main-top" .}} - -
-

{{t "LinkingUserPrompt.Title"}}

-

- {{t "LinkingUserPrompt.Description"}}
- {{.Username}} -

-
- - -
- - {{ .CSRF }} - - - - - {{template "error-message" .}} - -
- - - - - - -
- -
- - - - - - -{{template "main-bottom" .}} diff --git a/internal/auth/repository/eventsourcing/eventstore/auth_request.go b/internal/auth/repository/eventsourcing/eventstore/auth_request.go index bef602ff03..c49ade2a0a 100644 --- a/internal/auth/repository/eventsourcing/eventstore/auth_request.go +++ b/internal/auth/repository/eventsourcing/eventstore/auth_request.go @@ -100,7 +100,7 @@ type idpProviderViewProvider interface { } type idpUserLinksProvider interface { - IDPUserLinks(ctx context.Context, queries *query.IDPUserLinksSearchQuery, withOwnerRemoved bool) (*query.IDPUserLinks, error) + IDPUserLinks(ctx context.Context, queries *query.IDPUserLinksSearchQuery, permissionCheck domain.PermissionCheck) (*query.IDPUserLinks, error) } type userEventProvider interface { @@ -1000,7 +1000,7 @@ func (repo *AuthRequestRepo) checkExternalUserLogin(ctx context.Context, request } queries = append(queries, orgIDQuery) } - links, err := repo.Query.IDPUserLinks(ctx, &query.IDPUserLinksSearchQuery{Queries: queries}, false) + links, err := repo.Query.IDPUserLinks(ctx, &query.IDPUserLinksSearchQuery{Queries: queries}, nil) if err != nil { return err } @@ -1200,7 +1200,7 @@ func checkExternalIDPsOfUser(ctx context.Context, idpUserLinksProvider idpUserLi if err != nil { return nil, err } - return idpUserLinksProvider.IDPUserLinks(ctx, &query.IDPUserLinksSearchQuery{Queries: []query.SearchQuery{userIDQuery}}, false) + return idpUserLinksProvider.IDPUserLinks(ctx, &query.IDPUserLinksSearchQuery{Queries: []query.SearchQuery{userIDQuery}}, nil) } func (repo *AuthRequestRepo) usersForUserSelection(ctx context.Context, request *domain.AuthRequest) ([]domain.UserSelection, error) { diff --git a/internal/auth/repository/eventsourcing/eventstore/auth_request_test.go b/internal/auth/repository/eventsourcing/eventstore/auth_request_test.go index 7308d6ce13..14d4955825 100644 --- a/internal/auth/repository/eventsourcing/eventstore/auth_request_test.go +++ b/internal/auth/repository/eventsourcing/eventstore/auth_request_test.go @@ -298,7 +298,7 @@ type mockIDPUserLinks struct { idps []*query.IDPUserLink } -func (m *mockIDPUserLinks) IDPUserLinks(ctx context.Context, queries *query.IDPUserLinksSearchQuery, withOwnerRemoved bool) (*query.IDPUserLinks, error) { +func (m *mockIDPUserLinks) IDPUserLinks(ctx context.Context, queries *query.IDPUserLinksSearchQuery, permissionCheck domain.PermissionCheck) (*query.IDPUserLinks, error) { return &query.IDPUserLinks{Links: m.idps}, nil } diff --git a/internal/authz/repository/eventsourcing/eventstore/token_verifier.go b/internal/authz/repository/eventsourcing/eventstore/token_verifier.go index c9286d61dd..1a50c141d6 100644 --- a/internal/authz/repository/eventsourcing/eventstore/token_verifier.go +++ b/internal/authz/repository/eventsourcing/eventstore/token_verifier.go @@ -4,6 +4,7 @@ import ( "context" "encoding/base64" "fmt" + "slices" "strings" "time" @@ -327,28 +328,39 @@ 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) ([]byte, error) { - keySet, err := o.Queries.ActivePublicKeys(ctx, time.Now()) +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()) if err != nil { return nil, fmt.Errorf("error fetching keys: %w", err) } + appendPublicKeysToWebKeySet(keySet, legacyKeySet) keyID, alg := oidc.GetKeyIDAndAlg(jws) - key, err := oidc.FindMatchingKey(keyID, oidc.KeyUseSignature, alg, jsonWebKeys(keySet.Keys)...) + key, err := oidc.FindMatchingKey(keyID, oidc.KeyUseSignature, alg, keySet.Keys...) if err != nil { return nil, fmt.Errorf("invalid signature: %w", err) } return jws.Verify(&key) } -func jsonWebKeys(keys []query.PublicKey) []jose.JSONWebKey { - webKeys := make([]jose.JSONWebKey, len(keys)) - for i, key := range keys { - webKeys[i] = jose.JSONWebKey{ +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(), - Key: key.Key(), - } + }) } - return webKeys } diff --git a/internal/command/custom_login_text.go b/internal/command/custom_login_text.go index 6d4223c4ef..de430537af 100644 --- a/internal/command/custom_login_text.go +++ b/internal/command/custom_login_text.go @@ -42,7 +42,6 @@ func (c *Commands) createAllLoginTextEvents(ctx context.Context, agg *eventstore events = append(events, c.createRegistrationUserEvents(ctx, agg, existingText, text, defaultText)...) events = append(events, c.createExternalRegistrationUserOverviewEvents(ctx, agg, existingText, text, defaultText)...) events = append(events, c.createRegistrationOrgEvents(ctx, agg, existingText, text, defaultText)...) - events = append(events, c.createLinkingUserPromptEvents(ctx, agg, existingText, text, defaultText)...) events = append(events, c.createLinkingUserDoneEvents(ctx, agg, existingText, text, defaultText)...) events = append(events, c.createExternalUserNotFoundEvents(ctx, agg, existingText, text, defaultText)...) events = append(events, c.createSuccessLoginEvents(ctx, agg, existingText, text, defaultText)...) @@ -984,27 +983,6 @@ func (c *Commands) createRegistrationOrgEvents(ctx context.Context, agg *eventst return events } -func (c *Commands) createLinkingUserPromptEvents(ctx context.Context, agg *eventstore.Aggregate, existingText *CustomLoginTextReadModel, text *domain.CustomLoginText, defaultText bool) []eventstore.Command { - events := make([]eventstore.Command, 0) - event := c.createCustomLoginTextEvent(ctx, agg, domain.LoginKeyLinkingUserPromptTitle, existingText.LinkingUserPromptTitle, text.LinkingUserPrompt.Title, text.Language, defaultText) - if event != nil { - events = append(events, event) - } - event = c.createCustomLoginTextEvent(ctx, agg, domain.LoginKeyLinkingUserPromptDescription, existingText.LinkingUserPromptDescription, text.LinkingUserPrompt.Description, text.Language, defaultText) - if event != nil { - events = append(events, event) - } - event = c.createCustomLoginTextEvent(ctx, agg, domain.LoginKeyLinkingUserPromptLinkButtonText, existingText.LinkingUserPromptLinkButtonText, text.LinkingUserPrompt.LinkButtonText, text.Language, defaultText) - if event != nil { - events = append(events, event) - } - event = c.createCustomLoginTextEvent(ctx, agg, domain.LoginKeyLinkingUserPromptOtherButtonText, existingText.LinkingUserPromptOtherButtonText, text.LinkingUserPrompt.OtherButtonText, text.Language, defaultText) - if event != nil { - events = append(events, event) - } - return events -} - func (c *Commands) createLinkingUserDoneEvents(ctx context.Context, agg *eventstore.Aggregate, existingText *CustomLoginTextReadModel, text *domain.CustomLoginText, defaultText bool) []eventstore.Command { events := make([]eventstore.Command, 0) event := c.createCustomLoginTextEvent(ctx, agg, domain.LoginKeyLinkingUserDoneTitle, existingText.LinkingUserDoneTitle, text.LinkingUsersDone.Title, text.Language, defaultText) diff --git a/internal/command/custom_login_text_model.go b/internal/command/custom_login_text_model.go index 36ca04a62b..9197656110 100644 --- a/internal/command/custom_login_text_model.go +++ b/internal/command/custom_login_text_model.go @@ -422,10 +422,6 @@ func (wm *CustomLoginTextReadModel) Reduce() error { wm.handleRegistrationOrgScreenSetEvent(e) continue } - if strings.HasPrefix(e.Key, domain.LoginKeyLinkingUserPrompt) { - wm.handleLinkingUserPromptScreenSetEvent(e) - continue - } if strings.HasPrefix(e.Key, domain.LoginKeyLinkingUserDone) { wm.handleLinkingUserDoneScreenSetEvent(e) continue @@ -566,10 +562,6 @@ func (wm *CustomLoginTextReadModel) Reduce() error { wm.handleRegistrationOrgScreenRemoveEvent(e) continue } - if strings.HasPrefix(e.Key, domain.LoginKeyLinkingUserPrompt) { - wm.handleLinkingUserPromptRemoveEvent(e) - continue - } if strings.HasPrefix(e.Key, domain.LoginKeyLinkingUserDone) { wm.handleLinkingUserDoneRemoveEvent(e) continue @@ -2345,25 +2337,6 @@ func (wm *CustomLoginTextReadModel) handleRegistrationOrgScreenRemoveEvent(e *po } } -func (wm *CustomLoginTextReadModel) handleLinkingUserPromptScreenSetEvent(e *policy.CustomTextSetEvent) { - if e.Key == domain.LoginKeyLinkingUserPromptTitle { - wm.LinkingUserPromptTitle = e.Text - return - } - if e.Key == domain.LoginKeyLinkingUserPromptDescription { - wm.LinkingUserPromptDescription = e.Text - return - } - if e.Key == domain.LoginKeyLinkingUserPromptLinkButtonText { - wm.LinkingUserPromptLinkButtonText = e.Text - return - } - if e.Key == domain.LoginKeyLinkingUserPromptOtherButtonText { - wm.LinkingUserPromptOtherButtonText = e.Text - return - } -} - func (wm *CustomLoginTextReadModel) handleLinkingUserDoneScreenSetEvent(e *policy.CustomTextSetEvent) { if e.Key == domain.LoginKeyLinkingUserDoneTitle { wm.LinkingUserDoneTitle = e.Text @@ -2383,25 +2356,6 @@ func (wm *CustomLoginTextReadModel) handleLinkingUserDoneScreenSetEvent(e *polic } } -func (wm *CustomLoginTextReadModel) handleLinkingUserPromptRemoveEvent(e *policy.CustomTextRemovedEvent) { - if e.Key == domain.LoginKeyLinkingUserPromptTitle { - wm.LinkingUserPromptTitle = "" - return - } - if e.Key == domain.LoginKeyLinkingUserPromptDescription { - wm.LinkingUserPromptDescription = "" - return - } - if e.Key == domain.LoginKeyLinkingUserPromptLinkButtonText { - wm.LinkingUserPromptLinkButtonText = "" - return - } - if e.Key == domain.LoginKeyLinkingUserPromptOtherButtonText { - wm.LinkingUserPromptOtherButtonText = "" - return - } -} - func (wm *CustomLoginTextReadModel) handleLinkingUserDoneRemoveEvent(e *policy.CustomTextRemovedEvent) { if e.Key == domain.LoginKeyLinkingUserDoneTitle { wm.LinkingUserDoneTitle = "" diff --git a/internal/command/instance_custom_login_text_test.go b/internal/command/instance_custom_login_text_test.go index 9e460a2f75..0822cd2bd9 100644 --- a/internal/command/instance_custom_login_text_test.go +++ b/internal/command/instance_custom_login_text_test.go @@ -678,18 +678,6 @@ func TestCommandSide_SetCustomIAMLoginText(t *testing.T) { instance.NewCustomTextSetEvent(context.Background(), &instance.NewAggregate("INSTANCE").Aggregate, domain.LoginCustomText, domain.LoginKeyRegisterOrgSaveButtonText, "SaveButtonText", language.English, ), - instance.NewCustomTextSetEvent(context.Background(), - &instance.NewAggregate("INSTANCE").Aggregate, domain.LoginCustomText, domain.LoginKeyLinkingUserPromptTitle, "Title", language.English, - ), - instance.NewCustomTextSetEvent(context.Background(), - &instance.NewAggregate("INSTANCE").Aggregate, domain.LoginCustomText, domain.LoginKeyLinkingUserPromptDescription, "Description", language.English, - ), - instance.NewCustomTextSetEvent(context.Background(), - &instance.NewAggregate("INSTANCE").Aggregate, domain.LoginCustomText, domain.LoginKeyLinkingUserPromptLinkButtonText, "LinkButtonText", language.English, - ), - instance.NewCustomTextSetEvent(context.Background(), - &instance.NewAggregate("INSTANCE").Aggregate, domain.LoginCustomText, domain.LoginKeyLinkingUserPromptOtherButtonText, "OtherButtonText", language.English, - ), instance.NewCustomTextSetEvent(context.Background(), &instance.NewAggregate("INSTANCE").Aggregate, domain.LoginCustomText, domain.LoginKeyLinkingUserDoneTitle, "Title", language.English, ), @@ -1024,12 +1012,6 @@ func TestCommandSide_SetCustomIAMLoginText(t *testing.T) { PrivacyLinkText: "PrivacyLinkText", SaveButtonText: "SaveButtonText", }, - LinkingUserPrompt: domain.LinkingUserPromptScreenText{ - Title: "Title", - Description: "Description", - LinkButtonText: "LinkButtonText", - OtherButtonText: "OtherButtonText", - }, LinkingUsersDone: domain.LinkingUserDoneScreenText{ Title: "Title", Description: "Description", @@ -2260,30 +2242,6 @@ func TestCommandSide_SetCustomIAMLoginText(t *testing.T) { &instance.NewAggregate("INSTANCE").Aggregate, domain.LoginCustomText, domain.LoginKeyRegisterOrgSaveButtonText, "SaveButtonText", language.English, ), ), - eventFromEventPusherWithInstanceID( - "INSTANCE", - instance.NewCustomTextSetEvent(context.Background(), - &instance.NewAggregate("INSTANCE").Aggregate, domain.LoginCustomText, domain.LoginKeyLinkingUserPromptTitle, "Title", language.English, - ), - ), - eventFromEventPusherWithInstanceID( - "INSTANCE", - instance.NewCustomTextSetEvent(context.Background(), - &instance.NewAggregate("INSTANCE").Aggregate, domain.LoginCustomText, domain.LoginKeyLinkingUserPromptDescription, "Description", language.English, - ), - ), - eventFromEventPusherWithInstanceID( - "INSTANCE", - instance.NewCustomTextSetEvent(context.Background(), - &instance.NewAggregate("INSTANCE").Aggregate, domain.LoginCustomText, domain.LoginKeyLinkingUserPromptLinkButtonText, "LinkButtonText", language.English, - ), - ), - eventFromEventPusherWithInstanceID( - "INSTANCE", - instance.NewCustomTextSetEvent(context.Background(), - &instance.NewAggregate("INSTANCE").Aggregate, domain.LoginCustomText, domain.LoginKeyLinkingUserPromptOtherButtonText, "OtherButtonText", language.English, - ), - ), eventFromEventPusherWithInstanceID( "INSTANCE", instance.NewCustomTextSetEvent(context.Background(), @@ -3021,18 +2979,6 @@ func TestCommandSide_SetCustomIAMLoginText(t *testing.T) { instance.NewCustomTextRemovedEvent(context.Background(), &instance.NewAggregate("INSTANCE").Aggregate, domain.LoginCustomText, domain.LoginKeyRegisterOrgSaveButtonText, language.English, ), - instance.NewCustomTextRemovedEvent(context.Background(), - &instance.NewAggregate("INSTANCE").Aggregate, domain.LoginCustomText, domain.LoginKeyLinkingUserPromptTitle, language.English, - ), - instance.NewCustomTextRemovedEvent(context.Background(), - &instance.NewAggregate("INSTANCE").Aggregate, domain.LoginCustomText, domain.LoginKeyLinkingUserPromptDescription, language.English, - ), - instance.NewCustomTextRemovedEvent(context.Background(), - &instance.NewAggregate("INSTANCE").Aggregate, domain.LoginCustomText, domain.LoginKeyLinkingUserPromptLinkButtonText, language.English, - ), - instance.NewCustomTextRemovedEvent(context.Background(), - &instance.NewAggregate("INSTANCE").Aggregate, domain.LoginCustomText, domain.LoginKeyLinkingUserPromptOtherButtonText, language.English, - ), instance.NewCustomTextRemovedEvent(context.Background(), &instance.NewAggregate("INSTANCE").Aggregate, domain.LoginCustomText, domain.LoginKeyLinkingUserDoneTitle, language.English, ), @@ -3141,7 +3087,6 @@ func TestCommandSide_SetCustomIAMLoginText(t *testing.T) { RegistrationUser: domain.RegistrationUserScreenText{}, ExternalRegistrationUserOverview: domain.ExternalRegistrationUserOverviewScreenText{}, RegistrationOrg: domain.RegistrationOrgScreenText{}, - LinkingUserPrompt: domain.LinkingUserPromptScreenText{}, LinkingUsersDone: domain.LinkingUserDoneScreenText{}, ExternalNotFound: domain.ExternalUserNotFoundScreenText{}, LoginSuccess: domain.SuccessLoginScreenText{}, @@ -4344,30 +4289,6 @@ func TestCommandSide_SetCustomIAMLoginText(t *testing.T) { &instance.NewAggregate("INSTANCE").Aggregate, domain.LoginCustomText, domain.LoginKeyRegisterOrgSaveButtonText, "SaveButtonText", language.English, ), ), - eventFromEventPusherWithInstanceID( - "INSTANCE", - instance.NewCustomTextSetEvent(context.Background(), - &instance.NewAggregate("INSTANCE").Aggregate, domain.LoginCustomText, domain.LoginKeyLinkingUserPromptTitle, "Title", language.English, - ), - ), - eventFromEventPusherWithInstanceID( - "INSTANCE", - instance.NewCustomTextSetEvent(context.Background(), - &instance.NewAggregate("INSTANCE").Aggregate, domain.LoginCustomText, domain.LoginKeyLinkingUserPromptDescription, "Description", language.English, - ), - ), - eventFromEventPusherWithInstanceID( - "INSTANCE", - instance.NewCustomTextSetEvent(context.Background(), - &instance.NewAggregate("INSTANCE").Aggregate, domain.LoginCustomText, domain.LoginKeyLinkingUserPromptLinkButtonText, "LinkButtonText", language.English, - ), - ), - eventFromEventPusherWithInstanceID( - "INSTANCE", - instance.NewCustomTextSetEvent(context.Background(), - &instance.NewAggregate("INSTANCE").Aggregate, domain.LoginCustomText, domain.LoginKeyLinkingUserPromptOtherButtonText, "OtherButtonText", language.English, - ), - ), eventFromEventPusherWithInstanceID( "INSTANCE", instance.NewCustomTextSetEvent(context.Background(), @@ -5695,30 +5616,6 @@ func TestCommandSide_SetCustomIAMLoginText(t *testing.T) { &instance.NewAggregate("INSTANCE").Aggregate, domain.LoginCustomText, domain.LoginKeyRegisterOrgSaveButtonText, language.English, ), ), - eventFromEventPusherWithInstanceID( - "INSTANCE", - instance.NewCustomTextRemovedEvent(context.Background(), - &instance.NewAggregate("INSTANCE").Aggregate, domain.LoginCustomText, domain.LoginKeyLinkingUserPromptTitle, language.English, - ), - ), - eventFromEventPusherWithInstanceID( - "INSTANCE", - instance.NewCustomTextRemovedEvent(context.Background(), - &instance.NewAggregate("INSTANCE").Aggregate, domain.LoginCustomText, domain.LoginKeyLinkingUserPromptDescription, language.English, - ), - ), - eventFromEventPusherWithInstanceID( - "INSTANCE", - instance.NewCustomTextRemovedEvent(context.Background(), - &instance.NewAggregate("INSTANCE").Aggregate, domain.LoginCustomText, domain.LoginKeyLinkingUserPromptLinkButtonText, language.English, - ), - ), - eventFromEventPusherWithInstanceID( - "INSTANCE", - instance.NewCustomTextRemovedEvent(context.Background(), - &instance.NewAggregate("INSTANCE").Aggregate, domain.LoginCustomText, domain.LoginKeyLinkingUserPromptOtherButtonText, language.English, - ), - ), eventFromEventPusherWithInstanceID( "INSTANCE", instance.NewCustomTextRemovedEvent(context.Background(), @@ -6456,18 +6353,6 @@ func TestCommandSide_SetCustomIAMLoginText(t *testing.T) { instance.NewCustomTextSetEvent(context.Background(), &instance.NewAggregate("INSTANCE").Aggregate, domain.LoginCustomText, domain.LoginKeyRegisterOrgSaveButtonText, "SaveButtonText", language.English, ), - instance.NewCustomTextSetEvent(context.Background(), - &instance.NewAggregate("INSTANCE").Aggregate, domain.LoginCustomText, domain.LoginKeyLinkingUserPromptTitle, "Title", language.English, - ), - instance.NewCustomTextSetEvent(context.Background(), - &instance.NewAggregate("INSTANCE").Aggregate, domain.LoginCustomText, domain.LoginKeyLinkingUserPromptDescription, "Description", language.English, - ), - instance.NewCustomTextSetEvent(context.Background(), - &instance.NewAggregate("INSTANCE").Aggregate, domain.LoginCustomText, domain.LoginKeyLinkingUserPromptLinkButtonText, "LinkButtonText", language.English, - ), - instance.NewCustomTextSetEvent(context.Background(), - &instance.NewAggregate("INSTANCE").Aggregate, domain.LoginCustomText, domain.LoginKeyLinkingUserPromptOtherButtonText, "OtherButtonText", language.English, - ), instance.NewCustomTextSetEvent(context.Background(), &instance.NewAggregate("INSTANCE").Aggregate, domain.LoginCustomText, domain.LoginKeyLinkingUserDoneTitle, "Title", language.English, ), @@ -6802,12 +6687,6 @@ func TestCommandSide_SetCustomIAMLoginText(t *testing.T) { PrivacyLinkText: "PrivacyLinkText", SaveButtonText: "SaveButtonText", }, - LinkingUserPrompt: domain.LinkingUserPromptScreenText{ - Title: "Title", - Description: "Description", - LinkButtonText: "LinkButtonText", - OtherButtonText: "OtherButtonText", - }, LinkingUsersDone: domain.LinkingUserDoneScreenText{ Title: "Title", Description: "Description", diff --git a/internal/command/instance_features.go b/internal/command/instance_features.go index e6e448da9e..4fcf87b670 100644 --- a/internal/command/instance_features.go +++ b/internal/command/instance_features.go @@ -24,6 +24,7 @@ type InstanceFeatures struct { Actions *bool ImprovedPerformance []feature.ImprovedPerformanceType WebKey *bool + DebugOIDCParentError *bool } func (m *InstanceFeatures) isEmpty() bool { @@ -35,7 +36,8 @@ func (m *InstanceFeatures) isEmpty() bool { m.Actions == nil && // nil check to allow unset improvements m.ImprovedPerformance == nil && - m.WebKey == nil + m.WebKey == nil && + m.DebugOIDCParentError == nil } func (c *Commands) SetInstanceFeatures(ctx context.Context, f *InstanceFeatures) (*domain.ObjectDetails, error) { diff --git a/internal/command/instance_features_model.go b/internal/command/instance_features_model.go index bdb46d2e04..417b14dba7 100644 --- a/internal/command/instance_features_model.go +++ b/internal/command/instance_features_model.go @@ -68,6 +68,7 @@ func (m *InstanceFeaturesWriteModel) Query() *eventstore.SearchQueryBuilder { feature_v2.InstanceActionsEventType, feature_v2.InstanceImprovedPerformanceEventType, feature_v2.InstanceWebKeyEventType, + feature_v2.InstanceDebugOIDCParentErrorEventType, ). Builder().ResourceOwner(m.ResourceOwner) } @@ -104,6 +105,9 @@ func reduceInstanceFeature(features *InstanceFeatures, key feature.Key, value an case feature.KeyWebKey: v := value.(bool) features.WebKey = &v + case feature.KeyDebugOIDCParentError: + v := value.(bool) + features.DebugOIDCParentError = &v } } @@ -118,5 +122,6 @@ func (wm *InstanceFeaturesWriteModel) setCommands(ctx context.Context, f *Instan cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.Actions, f.Actions, feature_v2.InstanceActionsEventType) cmds = appendFeatureSliceUpdate(ctx, cmds, aggregate, wm.ImprovedPerformance, f.ImprovedPerformance, feature_v2.InstanceImprovedPerformanceEventType) cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.WebKey, f.WebKey, feature_v2.InstanceWebKeyEventType) + cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.DebugOIDCParentError, f.DebugOIDCParentError, feature_v2.InstanceDebugOIDCParentErrorEventType) return cmds } diff --git a/internal/command/org_custom_login_text_test.go b/internal/command/org_custom_login_text_test.go index 0e4be6ceec..cc05dae437 100644 --- a/internal/command/org_custom_login_text_test.go +++ b/internal/command/org_custom_login_text_test.go @@ -1087,26 +1087,6 @@ func TestCommandSide_SetCustomOrgLoginText(t *testing.T) { &org.NewAggregate("org1").Aggregate, domain.LoginCustomText, domain.LoginKeyRegisterOrgSaveButtonText, "SaveButtonText", language.English, ), ), - eventFromEventPusher( - org.NewCustomTextSetEvent(context.Background(), - &org.NewAggregate("org1").Aggregate, domain.LoginCustomText, domain.LoginKeyLinkingUserPromptTitle, "Title", language.English, - ), - ), - eventFromEventPusher( - org.NewCustomTextSetEvent(context.Background(), - &org.NewAggregate("org1").Aggregate, domain.LoginCustomText, domain.LoginKeyLinkingUserPromptDescription, "Description", language.English, - ), - ), - eventFromEventPusher( - org.NewCustomTextSetEvent(context.Background(), - &org.NewAggregate("org1").Aggregate, domain.LoginCustomText, domain.LoginKeyLinkingUserPromptLinkButtonText, "LinkButtonText", language.English, - ), - ), - eventFromEventPusher( - org.NewCustomTextSetEvent(context.Background(), - &org.NewAggregate("org1").Aggregate, domain.LoginCustomText, domain.LoginKeyLinkingUserPromptOtherButtonText, "OtherButtonText", language.English, - ), - ), eventFromEventPusher( org.NewCustomTextSetEvent(context.Background(), &org.NewAggregate("org1").Aggregate, domain.LoginCustomText, domain.LoginKeyLinkingUserDoneTitle, "Title", language.English, @@ -1489,12 +1469,6 @@ func TestCommandSide_SetCustomOrgLoginText(t *testing.T) { PrivacyLinkText: "PrivacyLinkText", SaveButtonText: "SaveButtonText", }, - LinkingUserPrompt: domain.LinkingUserPromptScreenText{ - Title: "Title", - Description: "Description", - LinkButtonText: "LinkButtonText", - OtherButtonText: "OtherButtonText", - }, LinkingUsersDone: domain.LinkingUserDoneScreenText{ Title: "Title", Description: "Description", @@ -2523,26 +2497,6 @@ func TestCommandSide_SetCustomOrgLoginText(t *testing.T) { &org.NewAggregate("org1").Aggregate, domain.LoginCustomText, domain.LoginKeyRegisterOrgSaveButtonText, "SaveButtonText", language.English, ), ), - eventFromEventPusher( - org.NewCustomTextSetEvent(context.Background(), - &org.NewAggregate("org1").Aggregate, domain.LoginCustomText, domain.LoginKeyLinkingUserPromptTitle, "Title", language.English, - ), - ), - eventFromEventPusher( - org.NewCustomTextSetEvent(context.Background(), - &org.NewAggregate("org1").Aggregate, domain.LoginCustomText, domain.LoginKeyLinkingUserPromptDescription, "Description", language.English, - ), - ), - eventFromEventPusher( - org.NewCustomTextSetEvent(context.Background(), - &org.NewAggregate("org1").Aggregate, domain.LoginCustomText, domain.LoginKeyLinkingUserPromptLinkButtonText, "LinkButtonText", language.English, - ), - ), - eventFromEventPusher( - org.NewCustomTextSetEvent(context.Background(), - &org.NewAggregate("org1").Aggregate, domain.LoginCustomText, domain.LoginKeyLinkingUserPromptOtherButtonText, "OtherButtonText", language.English, - ), - ), eventFromEventPusher( org.NewCustomTextSetEvent(context.Background(), &org.NewAggregate("org1").Aggregate, domain.LoginCustomText, domain.LoginKeyLinkingUserDoneTitle, "Title", language.English, @@ -3253,18 +3207,6 @@ func TestCommandSide_SetCustomOrgLoginText(t *testing.T) { org.NewCustomTextRemovedEvent(context.Background(), &org.NewAggregate("org1").Aggregate, domain.LoginCustomText, domain.LoginKeyRegisterOrgSaveButtonText, language.English, ), - org.NewCustomTextRemovedEvent(context.Background(), - &org.NewAggregate("org1").Aggregate, domain.LoginCustomText, domain.LoginKeyLinkingUserPromptTitle, language.English, - ), - org.NewCustomTextRemovedEvent(context.Background(), - &org.NewAggregate("org1").Aggregate, domain.LoginCustomText, domain.LoginKeyLinkingUserPromptDescription, language.English, - ), - org.NewCustomTextRemovedEvent(context.Background(), - &org.NewAggregate("org1").Aggregate, domain.LoginCustomText, domain.LoginKeyLinkingUserPromptLinkButtonText, language.English, - ), - org.NewCustomTextRemovedEvent(context.Background(), - &org.NewAggregate("org1").Aggregate, domain.LoginCustomText, domain.LoginKeyLinkingUserPromptOtherButtonText, language.English, - ), org.NewCustomTextRemovedEvent(context.Background(), &org.NewAggregate("org1").Aggregate, domain.LoginCustomText, domain.LoginKeyLinkingUserDoneTitle, language.English, ), @@ -3374,7 +3316,6 @@ func TestCommandSide_SetCustomOrgLoginText(t *testing.T) { ExternalRegistrationUserOverview: domain.ExternalRegistrationUserOverviewScreenText{}, RegistrationUser: domain.RegistrationUserScreenText{}, RegistrationOrg: domain.RegistrationOrgScreenText{}, - LinkingUserPrompt: domain.LinkingUserPromptScreenText{}, LinkingUsersDone: domain.LinkingUserDoneScreenText{}, ExternalNotFound: domain.ExternalUserNotFoundScreenText{}, LoginSuccess: domain.SuccessLoginScreenText{}, @@ -4374,26 +4315,6 @@ func TestCommandSide_SetCustomOrgLoginText(t *testing.T) { &org.NewAggregate("org1").Aggregate, domain.LoginCustomText, domain.LoginKeyRegisterOrgSaveButtonText, "SaveButtonText", language.English, ), ), - eventFromEventPusher( - org.NewCustomTextSetEvent(context.Background(), - &org.NewAggregate("org1").Aggregate, domain.LoginCustomText, domain.LoginKeyLinkingUserPromptTitle, "Title", language.English, - ), - ), - eventFromEventPusher( - org.NewCustomTextSetEvent(context.Background(), - &org.NewAggregate("org1").Aggregate, domain.LoginCustomText, domain.LoginKeyLinkingUserPromptDescription, "Description", language.English, - ), - ), - eventFromEventPusher( - org.NewCustomTextSetEvent(context.Background(), - &org.NewAggregate("org1").Aggregate, domain.LoginCustomText, domain.LoginKeyLinkingUserPromptLinkButtonText, "LinkButtonText", language.English, - ), - ), - eventFromEventPusher( - org.NewCustomTextSetEvent(context.Background(), - &org.NewAggregate("org1").Aggregate, domain.LoginCustomText, domain.LoginKeyLinkingUserPromptOtherButtonText, "OtherButtonText", language.English, - ), - ), eventFromEventPusher( org.NewCustomTextSetEvent(context.Background(), &org.NewAggregate("org1").Aggregate, domain.LoginCustomText, domain.LoginKeyLinkingUserDoneTitle, "Title", language.English, @@ -5494,26 +5415,6 @@ func TestCommandSide_SetCustomOrgLoginText(t *testing.T) { &org.NewAggregate("org1").Aggregate, domain.LoginCustomText, domain.LoginKeyRegisterOrgSaveButtonText, language.English, ), ), - eventFromEventPusher( - org.NewCustomTextRemovedEvent(context.Background(), - &org.NewAggregate("org1").Aggregate, domain.LoginCustomText, domain.LoginKeyLinkingUserPromptTitle, language.English, - ), - ), - eventFromEventPusher( - org.NewCustomTextRemovedEvent(context.Background(), - &org.NewAggregate("org1").Aggregate, domain.LoginCustomText, domain.LoginKeyLinkingUserPromptDescription, language.English, - ), - ), - eventFromEventPusher( - org.NewCustomTextRemovedEvent(context.Background(), - &org.NewAggregate("org1").Aggregate, domain.LoginCustomText, domain.LoginKeyLinkingUserPromptLinkButtonText, language.English, - ), - ), - eventFromEventPusher( - org.NewCustomTextRemovedEvent(context.Background(), - &org.NewAggregate("org1").Aggregate, domain.LoginCustomText, domain.LoginKeyLinkingUserPromptOtherButtonText, language.English, - ), - ), eventFromEventPusher( org.NewCustomTextRemovedEvent(context.Background(), &org.NewAggregate("org1").Aggregate, domain.LoginCustomText, domain.LoginKeyLinkingUserDoneTitle, language.English, @@ -6224,18 +6125,6 @@ func TestCommandSide_SetCustomOrgLoginText(t *testing.T) { org.NewCustomTextSetEvent(context.Background(), &org.NewAggregate("org1").Aggregate, domain.LoginCustomText, domain.LoginKeyRegisterOrgSaveButtonText, "SaveButtonText", language.English, ), - org.NewCustomTextSetEvent(context.Background(), - &org.NewAggregate("org1").Aggregate, domain.LoginCustomText, domain.LoginKeyLinkingUserPromptTitle, "Title", language.English, - ), - org.NewCustomTextSetEvent(context.Background(), - &org.NewAggregate("org1").Aggregate, domain.LoginCustomText, domain.LoginKeyLinkingUserPromptDescription, "Description", language.English, - ), - org.NewCustomTextSetEvent(context.Background(), - &org.NewAggregate("org1").Aggregate, domain.LoginCustomText, domain.LoginKeyLinkingUserPromptLinkButtonText, "LinkButtonText", language.English, - ), - org.NewCustomTextSetEvent(context.Background(), - &org.NewAggregate("org1").Aggregate, domain.LoginCustomText, domain.LoginKeyLinkingUserPromptOtherButtonText, "OtherButtonText", language.English, - ), org.NewCustomTextSetEvent(context.Background(), &org.NewAggregate("org1").Aggregate, domain.LoginCustomText, domain.LoginKeyLinkingUserDoneTitle, "Title", language.English, ), @@ -6570,12 +6459,6 @@ func TestCommandSide_SetCustomOrgLoginText(t *testing.T) { PrivacyLinkText: "PrivacyLinkText", SaveButtonText: "SaveButtonText", }, - LinkingUserPrompt: domain.LinkingUserPromptScreenText{ - Title: "Title", - Description: "Description", - LinkButtonText: "LinkButtonText", - OtherButtonText: "OtherButtonText", - }, LinkingUsersDone: domain.LinkingUserDoneScreenText{ Title: "Title", Description: "Description", diff --git a/internal/command/project_application_api.go b/internal/command/project_application_api.go index b1ad53f893..e3718b5010 100644 --- a/internal/command/project_application_api.go +++ b/internal/command/project_application_api.go @@ -250,22 +250,18 @@ func (c *Commands) VerifyAPIClientSecret(ctx context.Context, projectID, appID, ctx, spanPasswordComparison := tracing.NewNamedSpan(ctx, "passwap.Verify") updated, err := c.secretHasher.Verify(app.HashedSecret, secret) spanPasswordComparison.EndWithError(err) - if err == nil { - c.apiSecretCheckSucceeded(ctx, projectAgg, app.AppID, updated) - return err + if err != nil { + return zerrors.ThrowInvalidArgument(err, "COMMAND-SADfg", "Errors.Project.App.ClientSecretInvalid") } - c.apiSecretCheckFailed(ctx, projectAgg, app.AppID) - return zerrors.ThrowInvalidArgument(err, "COMMAND-SADfg", "Errors.Project.App.ClientSecretInvalid") + if updated != "" { + c.apiUpdateSecret(ctx, projectAgg, app.AppID, updated) + } + return nil } -func (c *Commands) APISecretCheckSucceeded(ctx context.Context, appID, projectID, resourceOwner, updated string) { +func (c *Commands) APIUpdateSecret(ctx context.Context, appID, projectID, resourceOwner, updated string) { agg := project_repo.NewAggregate(projectID, resourceOwner) - c.apiSecretCheckSucceeded(ctx, &agg.Aggregate, appID, updated) -} - -func (c *Commands) APISecretCheckFailed(ctx context.Context, appID, projectID, resourceOwner string) { - agg := project_repo.NewAggregate(projectID, resourceOwner) - c.apiSecretCheckFailed(ctx, &agg.Aggregate, appID) + c.apiUpdateSecret(ctx, &agg.Aggregate, appID, updated) } func (c *Commands) getAPIAppWriteModel(ctx context.Context, projectID, appID, resourceOwner string) (_ *APIApplicationWriteModel, err error) { @@ -280,17 +276,6 @@ func (c *Commands) getAPIAppWriteModel(ctx context.Context, projectID, appID, re return appWriteModel, nil } -func (c *Commands) apiSecretCheckSucceeded(ctx context.Context, agg *eventstore.Aggregate, appID, updated string) { - cmds := append( - make([]eventstore.Command, 0, 2), - project_repo.NewAPIConfigSecretCheckSucceededEvent(ctx, agg, appID), - ) - if updated != "" { - cmds = append(cmds, project_repo.NewAPIConfigSecretHashUpdatedEvent(ctx, agg, appID, updated)) - } - c.asyncPush(ctx, cmds...) -} - -func (c *Commands) apiSecretCheckFailed(ctx context.Context, agg *eventstore.Aggregate, appID string) { - c.asyncPush(ctx, project_repo.NewAPIConfigSecretCheckFailedEvent(ctx, agg, appID)) +func (c *Commands) apiUpdateSecret(ctx context.Context, agg *eventstore.Aggregate, appID, updated string) { + c.asyncPush(ctx, project_repo.NewAPIConfigSecretHashUpdatedEvent(ctx, agg, appID, updated)) } diff --git a/internal/command/project_application_api_test.go b/internal/command/project_application_api_test.go index 8463f9ff99..2702c00b39 100644 --- a/internal/command/project_application_api_test.go +++ b/internal/command/project_application_api_test.go @@ -837,9 +837,6 @@ func TestCommands_VerifyAPIClientSecret(t *testing.T) { project.NewAPIConfigAddedEvent(context.Background(), &agg.Aggregate, "appID", "clientID", hashedSecret, domain.APIAuthMethodTypePrivateKeyJWT), ), ), - expectPush( - project.NewAPIConfigSecretCheckSucceededEvent(context.Background(), &agg.Aggregate, "appID"), - ), ), }, { @@ -854,9 +851,6 @@ func TestCommands_VerifyAPIClientSecret(t *testing.T) { project.NewAPIConfigAddedEvent(context.Background(), &agg.Aggregate, "appID", "clientID", hashedSecret, domain.APIAuthMethodTypePrivateKeyJWT), ), ), - expectPush( - project.NewAPIConfigSecretCheckFailedEvent(context.Background(), &agg.Aggregate, "appID"), - ), ), wantErr: zerrors.ThrowInvalidArgument(err, "COMMAND-SADfg", "Errors.Project.App.ClientSecretInvalid"), }, diff --git a/internal/command/project_application_oidc.go b/internal/command/project_application_oidc.go index 0d270a1919..1f1ec184f3 100644 --- a/internal/command/project_application_oidc.go +++ b/internal/command/project_application_oidc.go @@ -331,22 +331,18 @@ func (c *Commands) VerifyOIDCClientSecret(ctx context.Context, projectID, appID, ctx, spanPasswordComparison := tracing.NewNamedSpan(ctx, "passwap.Verify") updated, err := c.secretHasher.Verify(app.HashedSecret, secret) spanPasswordComparison.EndWithError(err) - if err == nil { - c.oidcSecretCheckSucceeded(ctx, projectAgg, appID, updated) - return nil + if err != nil { + return zerrors.ThrowInvalidArgument(err, "COMMAND-Bz542", "Errors.Project.App.ClientSecretInvalid") } - c.oidcSecretCheckFailed(ctx, projectAgg, appID) - return zerrors.ThrowInvalidArgument(err, "COMMAND-Bz542", "Errors.Project.App.ClientSecretInvalid") + if updated != "" { + c.oidcUpdateSecret(ctx, projectAgg, appID, updated) + } + return nil } -func (c *Commands) OIDCSecretCheckSucceeded(ctx context.Context, appID, projectID, resourceOwner, updated string) { +func (c *Commands) OIDCUpdateSecret(ctx context.Context, appID, projectID, resourceOwner, updated string) { agg := project_repo.NewAggregate(projectID, resourceOwner) - c.oidcSecretCheckSucceeded(ctx, &agg.Aggregate, appID, updated) -} - -func (c *Commands) OIDCSecretCheckFailed(ctx context.Context, appID, projectID, resourceOwner string) { - agg := project_repo.NewAggregate(projectID, resourceOwner) - c.oidcSecretCheckFailed(ctx, &agg.Aggregate, appID) + c.oidcUpdateSecret(ctx, &agg.Aggregate, appID, updated) } func (c *Commands) getOIDCAppWriteModel(ctx context.Context, projectID, appID, resourceOwner string) (_ *OIDCApplicationWriteModel, err error) { @@ -382,17 +378,6 @@ func trimStringSliceWhiteSpaces(slice []string) []string { return slice } -func (c *Commands) oidcSecretCheckSucceeded(ctx context.Context, agg *eventstore.Aggregate, appID, updated string) { - cmds := append( - make([]eventstore.Command, 0, 2), - project_repo.NewOIDCConfigSecretCheckSucceededEvent(ctx, agg, appID), - ) - if updated != "" { - cmds = append(cmds, project_repo.NewOIDCConfigSecretHashUpdatedEvent(ctx, agg, appID, updated)) - } - c.asyncPush(ctx, cmds...) -} - -func (c *Commands) oidcSecretCheckFailed(ctx context.Context, agg *eventstore.Aggregate, appID string) { - c.asyncPush(ctx, project_repo.NewOIDCConfigSecretCheckFailedEvent(ctx, agg, appID)) +func (c *Commands) oidcUpdateSecret(ctx context.Context, agg *eventstore.Aggregate, appID, updated string) { + c.asyncPush(ctx, project_repo.NewOIDCConfigSecretHashUpdatedEvent(ctx, agg, appID, updated)) } diff --git a/internal/command/project_application_oidc_test.go b/internal/command/project_application_oidc_test.go index 1e4106be08..13bf359597 100644 --- a/internal/command/project_application_oidc_test.go +++ b/internal/command/project_application_oidc_test.go @@ -1363,9 +1363,6 @@ func TestCommands_VerifyOIDCClientSecret(t *testing.T) { ), ), ), - expectPush( - project.NewOIDCConfigSecretCheckSucceededEvent(context.Background(), &agg.Aggregate, "appID"), - ), ), }, { @@ -1400,9 +1397,6 @@ func TestCommands_VerifyOIDCClientSecret(t *testing.T) { ), ), ), - expectPush( - project.NewOIDCConfigSecretCheckFailedEvent(context.Background(), &agg.Aggregate, "appID"), - ), ), wantErr: zerrors.ThrowInvalidArgument(err, "COMMAND-Bz542", "Errors.Project.App.ClientSecretInvalid"), }, diff --git a/internal/command/user_schema.go b/internal/command/user_schema.go index 507e0caced..7b53c9e165 100644 --- a/internal/command/user_schema.go +++ b/internal/command/user_schema.go @@ -12,6 +12,8 @@ import ( ) type CreateUserSchema struct { + Details *domain.ObjectDetails + ResourceOwner string Type string Schema json.RawMessage @@ -33,7 +35,9 @@ func (s *CreateUserSchema) Valid() error { return nil } -type UpdateUserSchema struct { +type ChangeUserSchema struct { + Details *domain.ObjectDetails + ID string ResourceOwner string Type *string @@ -41,7 +45,7 @@ type UpdateUserSchema struct { PossibleAuthenticators []domain.AuthenticatorType } -func (s *UpdateUserSchema) Valid() error { +func (s *ChangeUserSchema) Valid() error { if s.ID == "" { return zerrors.ThrowInvalidArgument(nil, "COMMA-H5421", "Errors.IDMissing") } @@ -59,40 +63,43 @@ func (s *UpdateUserSchema) Valid() error { return nil } -func (c *Commands) CreateUserSchema(ctx context.Context, userSchema *CreateUserSchema) (string, *domain.ObjectDetails, error) { +func (c *Commands) CreateUserSchema(ctx context.Context, userSchema *CreateUserSchema) error { if err := userSchema.Valid(); err != nil { - return "", nil, err + return err } if userSchema.ResourceOwner == "" { - return "", nil, zerrors.ThrowInvalidArgument(nil, "COMMA-J3hhj", "Errors.ResourceOwnerMissing") + return zerrors.ThrowInvalidArgument(nil, "COMMA-J3hhj", "Errors.ResourceOwnerMissing") } id, err := c.idGenerator.Next() if err != nil { - return "", nil, err + return err } - writeModel := NewUserSchemaWriteModel(id, userSchema.ResourceOwner) - err = c.pushAppendAndReduce(ctx, writeModel, + writeModel, err := c.getSchemaWriteModelByID(ctx, userSchema.ResourceOwner, id) + if err != nil { + return err + } + if err := c.pushAppendAndReduce(ctx, writeModel, schema.NewCreatedEvent(ctx, UserSchemaAggregateFromWriteModel(&writeModel.WriteModel), userSchema.Type, userSchema.Schema, userSchema.PossibleAuthenticators, ), - ) - if err != nil { - return "", nil, err + ); err != nil { + return err } - return id, writeModelToObjectDetails(&writeModel.WriteModel), nil + userSchema.Details = writeModelToObjectDetails(&writeModel.WriteModel) + return nil } -func (c *Commands) UpdateUserSchema(ctx context.Context, userSchema *UpdateUserSchema) (*domain.ObjectDetails, error) { +func (c *Commands) ChangeUserSchema(ctx context.Context, userSchema *ChangeUserSchema) error { if err := userSchema.Valid(); err != nil { - return nil, err + return err } - writeModel := NewUserSchemaWriteModel(userSchema.ID, userSchema.ResourceOwner) - if err := c.eventstore.FilterToQueryReducer(ctx, writeModel); err != nil { - return nil, err + writeModel, err := c.getSchemaWriteModelByID(ctx, userSchema.ResourceOwner, userSchema.ID) + if err != nil { + return err } if writeModel.State != domain.UserSchemaStateActive { - return nil, zerrors.ThrowPreconditionFailed(nil, "COMMA-HB3e1", "Errors.UserSchema.NotActive") + return zerrors.ThrowPreconditionFailed(nil, "COMMA-HB3e1", "Errors.UserSchema.NotActive") } updatedEvent := writeModel.NewUpdatedEvent( ctx, @@ -102,29 +109,30 @@ func (c *Commands) UpdateUserSchema(ctx context.Context, userSchema *UpdateUserS userSchema.PossibleAuthenticators, ) if updatedEvent == nil { - return writeModelToObjectDetails(&writeModel.WriteModel), nil + userSchema.Details = writeModelToObjectDetails(&writeModel.WriteModel) + return nil } if err := c.pushAppendAndReduce(ctx, writeModel, updatedEvent); err != nil { - return nil, err + return err } - return writeModelToObjectDetails(&writeModel.WriteModel), nil + userSchema.Details = writeModelToObjectDetails(&writeModel.WriteModel) + return nil } func (c *Commands) DeactivateUserSchema(ctx context.Context, id, resourceOwner string) (*domain.ObjectDetails, error) { if id == "" { return nil, zerrors.ThrowInvalidArgument(nil, "COMMA-Vvf3w", "Errors.IDMissing") } - writeModel := NewUserSchemaWriteModel(id, resourceOwner) - if err := c.eventstore.FilterToQueryReducer(ctx, writeModel); err != nil { + writeModel, err := c.getSchemaWriteModelByID(ctx, resourceOwner, id) + if err != nil { return nil, err } if writeModel.State != domain.UserSchemaStateActive { return nil, zerrors.ThrowPreconditionFailed(nil, "COMMA-E4t4z", "Errors.UserSchema.NotActive") } - err := c.pushAppendAndReduce(ctx, writeModel, + if err := c.pushAppendAndReduce(ctx, writeModel, schema.NewDeactivatedEvent(ctx, UserSchemaAggregateFromWriteModel(&writeModel.WriteModel)), - ) - if err != nil { + ); err != nil { return nil, err } return writeModelToObjectDetails(&writeModel.WriteModel), nil @@ -134,17 +142,16 @@ func (c *Commands) ReactivateUserSchema(ctx context.Context, id, resourceOwner s if id == "" { return nil, zerrors.ThrowInvalidArgument(nil, "COMMA-wq3Gw", "Errors.IDMissing") } - writeModel := NewUserSchemaWriteModel(id, resourceOwner) - if err := c.eventstore.FilterToQueryReducer(ctx, writeModel); err != nil { + writeModel, err := c.getSchemaWriteModelByID(ctx, resourceOwner, id) + if err != nil { return nil, err } if writeModel.State != domain.UserSchemaStateInactive { return nil, zerrors.ThrowPreconditionFailed(nil, "COMMA-DGzh5", "Errors.UserSchema.NotInactive") } - err := c.pushAppendAndReduce(ctx, writeModel, + if err := c.pushAppendAndReduce(ctx, writeModel, schema.NewReactivatedEvent(ctx, UserSchemaAggregateFromWriteModel(&writeModel.WriteModel)), - ) - if err != nil { + ); err != nil { return nil, err } return writeModelToObjectDetails(&writeModel.WriteModel), nil @@ -154,18 +161,17 @@ func (c *Commands) DeleteUserSchema(ctx context.Context, id, resourceOwner strin if id == "" { return nil, zerrors.ThrowInvalidArgument(nil, "COMMA-E22gg", "Errors.IDMissing") } - writeModel := NewUserSchemaWriteModel(id, resourceOwner) - if err := c.eventstore.FilterToQueryReducer(ctx, writeModel); err != nil { + writeModel, err := c.getSchemaWriteModelByID(ctx, resourceOwner, id) + if err != nil { return nil, err } if !writeModel.Exists() { return nil, zerrors.ThrowPreconditionFailed(nil, "COMMA-Grg41", "Errors.UserSchema.NotExists") } // TODO: check for users based on that schema; this is only possible with / after https://github.com/zitadel/zitadel/issues/7308 - err := c.pushAppendAndReduce(ctx, writeModel, + if err := c.pushAppendAndReduce(ctx, writeModel, schema.NewDeletedEvent(ctx, UserSchemaAggregateFromWriteModel(&writeModel.WriteModel), writeModel.SchemaType), - ) - if err != nil { + ); err != nil { return nil, err } return writeModelToObjectDetails(&writeModel.WriteModel), nil @@ -178,3 +184,11 @@ func validateUserSchema(userSchema json.RawMessage) error { } return nil } + +func (c *Commands) getSchemaWriteModelByID(ctx context.Context, resourceOwner, id string) (*UserSchemaWriteModel, error) { + writeModel := NewUserSchemaWriteModel(resourceOwner, id, "") + if err := c.eventstore.FilterToQueryReducer(ctx, writeModel); err != nil { + return nil, err + } + return writeModel, nil +} diff --git a/internal/command/user_schema_model.go b/internal/command/user_schema_model.go index ccb5fbf27b..e8df80479a 100644 --- a/internal/command/user_schema_model.go +++ b/internal/command/user_schema_model.go @@ -19,14 +19,16 @@ type UserSchemaWriteModel struct { Schema json.RawMessage PossibleAuthenticators []domain.AuthenticatorType State domain.UserSchemaState + Revision uint64 } -func NewUserSchemaWriteModel(schemaID, resourceOwner string) *UserSchemaWriteModel { +func NewUserSchemaWriteModel(resourceOwner, schemaID, ty string) *UserSchemaWriteModel { return &UserSchemaWriteModel{ WriteModel: eventstore.WriteModel{ AggregateID: schemaID, ResourceOwner: resourceOwner, }, + SchemaType: ty, } } @@ -38,10 +40,14 @@ func (wm *UserSchemaWriteModel) Reduce() error { wm.Schema = e.Schema wm.PossibleAuthenticators = e.PossibleAuthenticators wm.State = domain.UserSchemaStateActive + wm.Revision = 1 case *schema.UpdatedEvent: if e.SchemaType != nil { wm.SchemaType = *e.SchemaType } + if e.SchemaRevision != nil { + wm.Revision = *e.SchemaRevision + } if len(e.Schema) > 0 { wm.Schema = e.Schema } @@ -60,7 +66,7 @@ func (wm *UserSchemaWriteModel) Reduce() error { } func (wm *UserSchemaWriteModel) Query() *eventstore.SearchQueryBuilder { - return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). + query := eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). ResourceOwner(wm.ResourceOwner). AddQuery(). AggregateTypes(schema.AggregateType). @@ -71,8 +77,13 @@ func (wm *UserSchemaWriteModel) Query() *eventstore.SearchQueryBuilder { schema.DeactivatedType, schema.ReactivatedType, schema.DeletedType, - ). - Builder() + ) + + if wm.SchemaType != "" { + query = query.EventData(map[string]interface{}{"schemaType": wm.SchemaType}) + } + + return query.Builder() } func (wm *UserSchemaWriteModel) NewUpdatedEvent( ctx context.Context, @@ -87,6 +98,8 @@ func (wm *UserSchemaWriteModel) NewUpdatedEvent( } if !bytes.Equal(wm.Schema, userSchema) { changes = append(changes, schema.ChangeSchema(userSchema)) + // change revision if the content of the schema changed + changes = append(changes, schema.IncreaseRevision(wm.Revision)) } if len(possibleAuthenticators) > 0 && slices.Compare(wm.PossibleAuthenticators, possibleAuthenticators) != 0 { changes = append(changes, schema.ChangePossibleAuthenticators(possibleAuthenticators)) diff --git a/internal/command/user_schema_test.go b/internal/command/user_schema_test.go index 084963116a..e9f06bff2b 100644 --- a/internal/command/user_schema_test.go +++ b/internal/command/user_schema_test.go @@ -27,7 +27,6 @@ func TestCommands_CreateUserSchema(t *testing.T) { userSchema *CreateUserSchema } type res struct { - id string details *domain.ObjectDetails err error } @@ -107,6 +106,7 @@ func TestCommands_CreateUserSchema(t *testing.T) { "empty user schema created", fields{ eventstore: expectEventstore( + expectFilter(), expectPush( schema.NewCreatedEvent( context.Background(), @@ -131,8 +131,8 @@ func TestCommands_CreateUserSchema(t *testing.T) { }, }, res{ - id: "id1", details: &domain.ObjectDetails{ + ID: "id1", ResourceOwner: "instanceID", }, }, @@ -141,6 +141,7 @@ func TestCommands_CreateUserSchema(t *testing.T) { "user schema created", fields{ eventstore: expectEventstore( + expectFilter(), expectPush( schema.NewCreatedEvent( context.Background(), @@ -181,8 +182,8 @@ func TestCommands_CreateUserSchema(t *testing.T) { }, }, res{ - id: "id1", details: &domain.ObjectDetails{ + ID: "id1", ResourceOwner: "instanceID", }, }, @@ -220,6 +221,7 @@ func TestCommands_CreateUserSchema(t *testing.T) { "user schema with permission created", fields{ eventstore: expectEventstore( + expectFilter(), expectPush( schema.NewCreatedEvent( context.Background(), @@ -266,8 +268,8 @@ func TestCommands_CreateUserSchema(t *testing.T) { }, }, res{ - id: "id1", details: &domain.ObjectDetails{ + ID: "id1", ResourceOwner: "instanceID", }, }, @@ -279,21 +281,20 @@ func TestCommands_CreateUserSchema(t *testing.T) { eventstore: tt.fields.eventstore(t), idGenerator: tt.fields.idGenerator, } - gotID, gotDetails, err := c.CreateUserSchema(tt.args.ctx, tt.args.userSchema) - assert.Equal(t, tt.res.id, gotID) - assertObjectDetails(t, tt.res.details, gotDetails) + err := c.CreateUserSchema(tt.args.ctx, tt.args.userSchema) + assertObjectDetails(t, tt.res.details, tt.args.userSchema.Details) assert.ErrorIs(t, err, tt.res.err) }) } } -func TestCommands_UpdateUserSchema(t *testing.T) { +func TestCommands_ChangeUserSchema(t *testing.T) { type fields struct { eventstore func(t *testing.T) *eventstore.Eventstore } type args struct { ctx context.Context - userSchema *UpdateUserSchema + userSchema *ChangeUserSchema } type res struct { details *domain.ObjectDetails @@ -312,7 +313,7 @@ func TestCommands_UpdateUserSchema(t *testing.T) { }, args{ ctx: authz.NewMockContext("instanceID", "", ""), - userSchema: &UpdateUserSchema{}, + userSchema: &ChangeUserSchema{}, }, res{ err: zerrors.ThrowInvalidArgument(nil, "COMMA-H5421", "Errors.IDMissing"), @@ -325,7 +326,7 @@ func TestCommands_UpdateUserSchema(t *testing.T) { }, args{ ctx: authz.NewMockContext("instanceID", "", ""), - userSchema: &UpdateUserSchema{ + userSchema: &ChangeUserSchema{ ID: "id1", Type: gu.Ptr(""), }, @@ -341,7 +342,7 @@ func TestCommands_UpdateUserSchema(t *testing.T) { }, args{ ctx: authz.NewMockContext("instanceID", "", ""), - userSchema: &UpdateUserSchema{ + userSchema: &ChangeUserSchema{ ID: "id1", }, }, @@ -356,7 +357,7 @@ func TestCommands_UpdateUserSchema(t *testing.T) { }, args{ ctx: authz.NewMockContext("instanceID", "", ""), - userSchema: &UpdateUserSchema{ + userSchema: &ChangeUserSchema{ ID: "id1", Schema: json.RawMessage(`{ "properties": { @@ -379,7 +380,7 @@ func TestCommands_UpdateUserSchema(t *testing.T) { }, args{ ctx: authz.NewMockContext("instanceID", "", ""), - userSchema: &UpdateUserSchema{ + userSchema: &ChangeUserSchema{ ID: "id1", Schema: json.RawMessage(`{}`), PossibleAuthenticators: []domain.AuthenticatorType{ @@ -400,7 +401,7 @@ func TestCommands_UpdateUserSchema(t *testing.T) { }, args{ ctx: authz.NewMockContext("instanceID", "", ""), - userSchema: &UpdateUserSchema{ + userSchema: &ChangeUserSchema{ ID: "id1", Type: gu.Ptr("type"), Schema: json.RawMessage(`{}`), @@ -432,7 +433,7 @@ func TestCommands_UpdateUserSchema(t *testing.T) { }, args{ ctx: authz.NewMockContext("instanceID", "", ""), - userSchema: &UpdateUserSchema{ + userSchema: &ChangeUserSchema{ ID: "id1", Type: gu.Ptr("type"), Schema: json.RawMessage(`{}`), @@ -473,7 +474,7 @@ func TestCommands_UpdateUserSchema(t *testing.T) { }, args{ ctx: authz.NewMockContext("instanceID", "", ""), - userSchema: &UpdateUserSchema{ + userSchema: &ChangeUserSchema{ ID: "id1", Schema: json.RawMessage(`{}`), Type: gu.Ptr("newType"), @@ -515,7 +516,9 @@ func TestCommands_UpdateUserSchema(t *testing.T) { schema.NewUpdatedEvent( context.Background(), &schema.NewAggregate("id1", "instanceID").Aggregate, - []schema.Changes{schema.ChangeSchema(json.RawMessage(`{ + []schema.Changes{ + schema.IncreaseRevision(1), + schema.ChangeSchema(json.RawMessage(`{ "$schema": "urn:zitadel:schema:v1", "type": "object", "properties": { @@ -539,7 +542,7 @@ func TestCommands_UpdateUserSchema(t *testing.T) { }, args{ ctx: authz.NewMockContext("instanceID", "", ""), - userSchema: &UpdateUserSchema{ + userSchema: &ChangeUserSchema{ ID: "id1", Schema: json.RawMessage(`{ "$schema": "urn:zitadel:schema:v1", @@ -597,7 +600,7 @@ func TestCommands_UpdateUserSchema(t *testing.T) { }, args{ ctx: authz.NewMockContext("instanceID", "", ""), - userSchema: &UpdateUserSchema{ + userSchema: &ChangeUserSchema{ ID: "id1", Schema: json.RawMessage(`{}`), PossibleAuthenticators: []domain.AuthenticatorType{ @@ -618,9 +621,9 @@ func TestCommands_UpdateUserSchema(t *testing.T) { c := &Commands{ eventstore: tt.fields.eventstore(t), } - got, err := c.UpdateUserSchema(tt.args.ctx, tt.args.userSchema) + err := c.ChangeUserSchema(tt.args.ctx, tt.args.userSchema) assert.ErrorIs(t, err, tt.res.err) - assertObjectDetails(t, tt.res.details, got) + assertObjectDetails(t, tt.res.details, tt.args.userSchema.Details) }) } } diff --git a/internal/command/user_v3.go b/internal/command/user_v3.go new file mode 100644 index 0000000000..f2cacd6cb0 --- /dev/null +++ b/internal/command/user_v3.go @@ -0,0 +1,220 @@ +package command + +import ( + "bytes" + "context" + "encoding/json" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/crypto" + "github.com/zitadel/zitadel/internal/domain" + domain_schema "github.com/zitadel/zitadel/internal/domain/schema" + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/repository/user/schemauser" + "github.com/zitadel/zitadel/internal/zerrors" +) + +type CreateSchemaUser struct { + Details *domain.ObjectDetails + ResourceOwner string + + SchemaID string + schemaRevision uint64 + + ID string + Data json.RawMessage + + Email *Email + ReturnCodeEmail string + Phone *Phone + ReturnCodePhone string +} + +func (s *CreateSchemaUser) Valid(ctx context.Context, c *Commands) (err error) { + if s.ResourceOwner == "" { + return zerrors.ThrowInvalidArgument(nil, "COMMAND-urEJKa1tJM", "Errors.ResourceOwnerMissing") + } + if s.SchemaID == "" { + return zerrors.ThrowInvalidArgument(nil, "COMMAND-TFo06JgnF2", "Errors.UserSchema.ID.Missing") + } + + schemaWriteModel, err := c.getSchemaWriteModelByID(ctx, "", s.SchemaID) + if err != nil { + return err + } + if !schemaWriteModel.Exists() { + return zerrors.ThrowPreconditionFailed(nil, "COMMAND-N9QOuN4F7o", "Errors.UserSchema.NotExists") + } + s.schemaRevision = schemaWriteModel.Revision + + if s.ID == "" { + s.ID, err = c.idGenerator.Next() + if err != nil { + return err + } + } + + // get role for permission check in schema through extension + role, err := c.getSchemaRoleForWrite(ctx, s.ResourceOwner, s.ID) + if err != nil { + return err + } + + schema, err := domain_schema.NewSchema(role, bytes.NewReader(schemaWriteModel.Schema)) + if err != nil { + return err + } + + var v interface{} + if err := json.Unmarshal(s.Data, &v); err != nil { + return zerrors.ThrowInvalidArgument(nil, "COMMAND-7o3ZGxtXUz", "Errors.User.Invalid") + } + + if err := schema.Validate(v); err != nil { + return zerrors.ThrowPreconditionFailed(nil, "COMMAND-SlKXqLSeL6", "Errors.UserSchema.Data.Invalid") + } + + if s.Email != nil && s.Email.Address != "" { + if err := s.Email.Validate(); err != nil { + return err + } + } + + if s.Phone != nil && s.Phone.Number != "" { + if s.Phone.Number, err = s.Phone.Number.Normalize(); err != nil { + return err + } + } + + return nil +} + +func (c *Commands) getSchemaRoleForWrite(ctx context.Context, resourceOwner, userID string) (domain_schema.Role, error) { + if userID == authz.GetCtxData(ctx).UserID { + return domain_schema.RoleSelf, nil + } + if err := c.checkPermission(ctx, domain.PermissionUserWrite, resourceOwner, userID); err != nil { + return domain_schema.RoleUnspecified, err + } + return domain_schema.RoleOwner, nil +} + +func (c *Commands) CreateSchemaUser(ctx context.Context, user *CreateSchemaUser, alg crypto.EncryptionAlgorithm) (err error) { + if err := user.Valid(ctx, c); err != nil { + return err + } + + writeModel, err := c.getSchemaUserExists(ctx, user.ResourceOwner, user.ID) + if err != nil { + return err + } + if writeModel.Exists() { + return zerrors.ThrowPreconditionFailed(nil, "COMMAND-Nn8CRVlkeZ", "Errors.User.AlreadyExists") + } + + userAgg := UserV3AggregateFromWriteModel(&writeModel.WriteModel) + events := []eventstore.Command{ + schemauser.NewCreatedEvent(ctx, + userAgg, + user.SchemaID, user.schemaRevision, user.Data, + ), + } + if user.Email != nil { + events, user.ReturnCodeEmail, err = c.updateSchemaUserEmail(ctx, events, userAgg, user.Email, alg) + if err != nil { + return err + } + } + if user.Phone != nil { + events, user.ReturnCodePhone, err = c.updateSchemaUserPhone(ctx, events, userAgg, user.Phone, alg) + if err != nil { + return err + } + } + + if err := c.pushAppendAndReduce(ctx, writeModel, events...); err != nil { + return err + } + user.Details = writeModelToObjectDetails(&writeModel.WriteModel) + return nil +} + +func (c *Commands) DeleteSchemaUser(ctx context.Context, id string) (*domain.ObjectDetails, error) { + if id == "" { + return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-Vs4wJCME7T", "Errors.IDMissing") + } + writeModel, err := c.getSchemaUserExists(ctx, "", id) + if err != nil { + return nil, err + } + if !writeModel.Exists() { + return nil, zerrors.ThrowNotFound(nil, "COMMAND-syHyCsGmvM", "Errors.User.NotFound") + } + if err := c.checkPermissionDeleteUser(ctx, writeModel.ResourceOwner, writeModel.AggregateID); err != nil { + return nil, err + } + if err := c.pushAppendAndReduce(ctx, writeModel, + schemauser.NewDeletedEvent(ctx, UserV3AggregateFromWriteModel(&writeModel.WriteModel)), + ); err != nil { + return nil, err + } + return writeModelToObjectDetails(&writeModel.WriteModel), nil +} + +func (c *Commands) updateSchemaUserEmail(ctx context.Context, events []eventstore.Command, agg *eventstore.Aggregate, email *Email, alg crypto.EncryptionAlgorithm) (_ []eventstore.Command, plainCode string, err error) { + + events = append(events, schemauser.NewEmailUpdatedEvent(ctx, + agg, + email.Address, + )) + if email.Verified { + events = append(events, schemauser.NewEmailVerifiedEvent(ctx, agg)) + } else { + cryptoCode, err := c.newEmailCode(ctx, c.eventstore.Filter, alg) //nolint:staticcheck + if err != nil { + return nil, "", err + } + if email.ReturnCode { + plainCode = cryptoCode.Plain + } + events = append(events, schemauser.NewEmailCodeAddedEvent(ctx, agg, + cryptoCode.Crypted, + cryptoCode.Expiry, + email.URLTemplate, + email.ReturnCode, + )) + } + return events, plainCode, nil +} + +func (c *Commands) updateSchemaUserPhone(ctx context.Context, events []eventstore.Command, agg *eventstore.Aggregate, phone *Phone, alg crypto.EncryptionAlgorithm) (_ []eventstore.Command, plainCode string, err error) { + events = append(events, schemauser.NewPhoneChangedEvent(ctx, + agg, + phone.Number, + )) + if phone.Verified { + events = append(events, schemauser.NewPhoneVerifiedEvent(ctx, agg)) + } else { + cryptoCode, err := c.newPhoneCode(ctx, c.eventstore.Filter, alg) //nolint:staticcheck + if err != nil { + return nil, "", err + } + if phone.ReturnCode { + plainCode = cryptoCode.Plain + } + events = append(events, schemauser.NewPhoneCodeAddedEvent(ctx, agg, + cryptoCode.Crypted, + cryptoCode.Expiry, + phone.ReturnCode, + )) + } + return events, plainCode, nil +} + +func (c *Commands) getSchemaUserExists(ctx context.Context, resourceOwner, id string) (*UserV3WriteModel, error) { + writeModel := NewExistsUserV3WriteModel(resourceOwner, id) + if err := c.eventstore.FilterToQueryReducer(ctx, writeModel); err != nil { + return nil, err + } + return writeModel, nil +} diff --git a/internal/command/user_v3_model.go b/internal/command/user_v3_model.go new file mode 100644 index 0000000000..51f783aaed --- /dev/null +++ b/internal/command/user_v3_model.go @@ -0,0 +1,174 @@ +package command + +import ( + "bytes" + "context" + "encoding/json" + + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/repository/user/schemauser" +) + +type UserV3WriteModel struct { + eventstore.WriteModel + + PhoneWM bool + EmailWM bool + DataWM bool + + SchemaID string + SchemaRevision uint64 + + Email string + IsEmailVerified bool + EmailVerifiedFailedCount int + Phone string + IsPhoneVerified bool + PhoneVerifiedFailedCount int + + Data json.RawMessage + + State domain.UserState +} + +func NewExistsUserV3WriteModel(resourceOwner, userID string) *UserV3WriteModel { + return &UserV3WriteModel{ + WriteModel: eventstore.WriteModel{ + AggregateID: userID, + ResourceOwner: resourceOwner, + }, + PhoneWM: false, + EmailWM: false, + DataWM: false, + } +} + +func NewUserV3WriteModel(resourceOwner, userID string) *UserV3WriteModel { + return &UserV3WriteModel{ + WriteModel: eventstore.WriteModel{ + AggregateID: userID, + ResourceOwner: resourceOwner, + }, + PhoneWM: true, + EmailWM: true, + DataWM: true, + } +} + +func (wm *UserV3WriteModel) Reduce() error { + for _, event := range wm.Events { + switch e := event.(type) { + case *schemauser.CreatedEvent: + wm.SchemaID = e.SchemaID + wm.SchemaRevision = 1 + wm.Data = e.Data + + wm.State = domain.UserStateActive + case *schemauser.UpdatedEvent: + if e.SchemaID != nil { + wm.SchemaID = *e.SchemaID + } + if e.SchemaRevision != nil { + wm.SchemaRevision = *e.SchemaRevision + } + if len(e.Data) > 0 { + wm.Data = e.Data + } + case *schemauser.DeletedEvent: + wm.State = domain.UserStateDeleted + case *schemauser.EmailUpdatedEvent: + wm.Email = string(e.EmailAddress) + case *schemauser.EmailCodeAddedEvent: + wm.IsEmailVerified = false + wm.EmailVerifiedFailedCount = 0 + case *schemauser.EmailVerifiedEvent: + wm.IsEmailVerified = true + wm.EmailVerifiedFailedCount = 0 + case *schemauser.EmailVerificationFailedEvent: + wm.EmailVerifiedFailedCount += 1 + case *schemauser.PhoneChangedEvent: + wm.Phone = string(e.PhoneNumber) + case *schemauser.PhoneCodeAddedEvent: + wm.IsPhoneVerified = false + wm.PhoneVerifiedFailedCount = 0 + case *schemauser.PhoneVerifiedEvent: + wm.PhoneVerifiedFailedCount = 0 + wm.IsPhoneVerified = true + case *schemauser.PhoneVerificationFailedEvent: + wm.PhoneVerifiedFailedCount += 1 + } + } + return wm.WriteModel.Reduce() +} + +func (wm *UserV3WriteModel) Query() *eventstore.SearchQueryBuilder { + query := eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). + ResourceOwner(wm.ResourceOwner). + AddQuery(). + AggregateTypes(schemauser.AggregateType). + AggregateIDs(wm.AggregateID). + EventTypes( + schemauser.CreatedType, + schemauser.DeletedType, + ) + if wm.DataWM { + query = query.EventTypes( + schemauser.UpdatedType, + ) + } + if wm.EmailWM { + query = query.EventTypes( + schemauser.EmailUpdatedType, + schemauser.EmailVerifiedType, + schemauser.EmailCodeAddedType, + schemauser.EmailVerificationFailedType, + ) + } + if wm.PhoneWM { + query = query.EventTypes( + schemauser.PhoneUpdatedType, + schemauser.PhoneVerifiedType, + schemauser.PhoneCodeAddedType, + schemauser.PhoneVerificationFailedType, + ) + } + return query.Builder() +} + +func (wm *UserV3WriteModel) NewUpdatedEvent( + ctx context.Context, + agg *eventstore.Aggregate, + schemaID *string, + schemaRevision *uint64, + data json.RawMessage, +) *schemauser.UpdatedEvent { + changes := make([]schemauser.Changes, 0) + if schemaID != nil && wm.SchemaID != *schemaID { + changes = append(changes, schemauser.ChangeSchemaID(wm.SchemaID, *schemaID)) + } + if schemaRevision != nil && wm.SchemaRevision != *schemaRevision { + changes = append(changes, schemauser.ChangeSchemaRevision(wm.SchemaRevision, *schemaRevision)) + } + if !bytes.Equal(wm.Data, data) { + changes = append(changes, schemauser.ChangeData(data)) + } + if len(changes) == 0 { + return nil + } + return schemauser.NewUpdatedEvent(ctx, agg, changes) +} + +func UserV3AggregateFromWriteModel(wm *eventstore.WriteModel) *eventstore.Aggregate { + return &eventstore.Aggregate{ + ID: wm.AggregateID, + Type: schemauser.AggregateType, + ResourceOwner: wm.ResourceOwner, + InstanceID: wm.InstanceID, + Version: schemauser.AggregateVersion, + } +} + +func (wm *UserV3WriteModel) Exists() bool { + return wm.State != domain.UserStateDeleted && wm.State != domain.UserStateUnspecified +} diff --git a/internal/command/user_v3_test.go b/internal/command/user_v3_test.go new file mode 100644 index 0000000000..5bbb9e0c55 --- /dev/null +++ b/internal/command/user_v3_test.go @@ -0,0 +1,1103 @@ +package command + +import ( + "context" + "encoding/json" + "errors" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/crypto" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/id" + "github.com/zitadel/zitadel/internal/id/mock" + "github.com/zitadel/zitadel/internal/repository/user/schema" + "github.com/zitadel/zitadel/internal/repository/user/schemauser" + "github.com/zitadel/zitadel/internal/zerrors" +) + +func TestCommands_CreateSchemaUser(t *testing.T) { + type fields struct { + eventstore func(t *testing.T) *eventstore.Eventstore + idGenerator id.Generator + checkPermission domain.PermissionCheck + newCode encrypedCodeFunc + } + type args struct { + ctx context.Context + user *CreateSchemaUser + } + type res struct { + returnCodeEmail string + returnCodePhone string + details *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + "no resourceOwner, error", + fields{ + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), + }, + args{ + ctx: authz.NewMockContext("instanceID", "", ""), + user: &CreateSchemaUser{}, + }, + res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowInvalidArgument(nil, "COMMAND-urEJKa1tJM", "Errors.ResourceOwnerMissing")) + }, + }, + }, + { + "no schemaID, error", + fields{ + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), + }, + args{ + ctx: authz.NewMockContext("instanceID", "", ""), + user: &CreateSchemaUser{ + ResourceOwner: "org1", + }, + }, + res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowInvalidArgument(nil, "COMMAND-TFo06JgnF2", "Errors.UserSchema.ID.Missing")) + }, + }, + }, + { + "schema not existing, error", + fields{ + eventstore: expectEventstore( + expectFilter(), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args{ + ctx: authz.NewMockContext("instanceID", "", ""), + user: &CreateSchemaUser{ + ResourceOwner: "org1", + SchemaID: "type", + schemaRevision: 1, + }, + }, + res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowPreconditionFailed(nil, "COMMAND-N9QOuN4F7o", "Errors.UserSchema.NotExists")) + }, + }, + }, + { + "no data, error", + fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + schema.NewCreatedEvent( + context.Background(), + &schema.NewAggregate("id1", "instanceID").Aggregate, + "type", + json.RawMessage(`{ + "$schema": "urn:zitadel:schema:v1", + "type": "object", + "properties": { + "name": { + "type": "string" + } + } + }`), + []domain.AuthenticatorType{domain.AuthenticatorTypeUsername}, + ), + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + idGenerator: mock.ExpectID(t, "id1"), + }, + args{ + ctx: authz.NewMockContext("instanceID", "", ""), + user: &CreateSchemaUser{ + ResourceOwner: "org1", + SchemaID: "type", + schemaRevision: 1, + }, + }, + res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowInvalidArgument(nil, "COMMAND-7o3ZGxtXUz", "Errors.User.Invalid")) + }, + }, + }, + { + "user create, no permission", + fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + schema.NewCreatedEvent( + context.Background(), + &schema.NewAggregate("id1", "instanceID").Aggregate, + "type", + json.RawMessage(`{ + "$schema": "urn:zitadel:schema:v1", + "type": "object", + "properties": { + "name": { + "type": "string" + } + } + }`), + []domain.AuthenticatorType{domain.AuthenticatorTypeUsername}, + ), + ), + ), + ), + checkPermission: newMockPermissionCheckNotAllowed(), + idGenerator: mock.ExpectID(t, "id1"), + }, + args{ + ctx: authz.NewMockContext("instanceID", "", ""), + user: &CreateSchemaUser{ + ResourceOwner: "org1", + SchemaID: "type", + schemaRevision: 1, + Data: json.RawMessage(`{ + "name": "user" + }`), + }, + }, + res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowPermissionDenied(nil, "AUTHZ-HKJD33", "Errors.PermissionDenied")) + }, + }, + }, + { + "user created", + fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + schema.NewCreatedEvent( + context.Background(), + &schema.NewAggregate("id1", "instanceID").Aggregate, + "type", + json.RawMessage(`{ + "$schema": "urn:zitadel:schema:v1", + "type": "object", + "properties": { + "name": { + "type": "string" + } + } + }`), + []domain.AuthenticatorType{domain.AuthenticatorTypeUsername}, + ), + ), + ), + expectFilter(), + expectPush( + schemauser.NewCreatedEvent( + context.Background(), + &schemauser.NewAggregate("id1", "org1").Aggregate, + "type", + 1, + json.RawMessage(`{ + "name": "user" + }`), + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + idGenerator: mock.ExpectID(t, "id1"), + }, + args{ + ctx: authz.NewMockContext("instanceID", "", ""), + user: &CreateSchemaUser{ + ResourceOwner: "org1", + SchemaID: "type", + schemaRevision: 1, + Data: json.RawMessage(`{ + "name": "user" + }`), + }, + }, + res{ + details: &domain.ObjectDetails{ + ResourceOwner: "org1", + ID: "id1", + }, + }, + }, + { + "user create, no field permission as admin", + fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + schema.NewCreatedEvent( + context.Background(), + &schema.NewAggregate("id1", "instanceID").Aggregate, + "type", + json.RawMessage(`{ + "$schema": "urn:zitadel:schema:v1", + "type": "object", + "properties": { + "name": { + "urn:zitadel:schema:permission": { + "owner": "r" + }, + "type": "string" + } + } + }`), + []domain.AuthenticatorType{domain.AuthenticatorTypeUsername}, + ), + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + idGenerator: mock.ExpectID(t, "id1"), + }, + args{ + ctx: authz.NewMockContext("instanceID", "", ""), + user: &CreateSchemaUser{ + ResourceOwner: "org1", + SchemaID: "type", + schemaRevision: 1, + Data: json.RawMessage(`{ + "name": "user" + }`), + }, + }, + res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowPreconditionFailed(nil, "COMMAND-SlKXqLSeL6", "Errors.UserSchema.Data.Invalid")) + }, + }, + }, + { + "user create, no field permission as user", + fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + schema.NewCreatedEvent( + context.Background(), + &schema.NewAggregate("id1", "instanceID").Aggregate, + "type", + json.RawMessage(`{ + "$schema": "urn:zitadel:schema:v1", + "type": "object", + "properties": { + "name": { + "urn:zitadel:schema:permission": { + "self": "r" + }, + "type": "string" + } + } + }`), + []domain.AuthenticatorType{domain.AuthenticatorTypeUsername}, + ), + ), + ), + ), + idGenerator: mock.ExpectID(t, "id1"), + }, + args{ + ctx: authz.NewMockContext("instanceID", "org1", "id1"), + user: &CreateSchemaUser{ + ResourceOwner: "org1", + SchemaID: "type", + schemaRevision: 1, + Data: json.RawMessage(`{ + "name": "user" + }`), + }, + }, + res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowPreconditionFailed(nil, "COMMAND-SlKXqLSeL6", "Errors.UserSchema.Data.Invalid")) + }, + }, + }, + { + "user create, invalid data type", + fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + schema.NewCreatedEvent( + context.Background(), + &schema.NewAggregate("id1", "instanceID").Aggregate, + "type", + json.RawMessage(`{ + "$schema": "urn:zitadel:schema:v1", + "type": "object", + "properties": { + "name": { + "type": "string" + } + } + }`), + []domain.AuthenticatorType{domain.AuthenticatorTypeUsername}, + ), + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + idGenerator: mock.ExpectID(t, "id1"), + }, + args{ + ctx: authz.NewMockContext("instanceID", "org1", "user1"), + user: &CreateSchemaUser{ + ResourceOwner: "org1", + SchemaID: "type", + schemaRevision: 1, + Data: json.RawMessage(`{ + "name": 1 + }`), + }, + }, + res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowPreconditionFailed(nil, "COMMAND-SlKXqLSeL6", "Errors.UserSchema.Data.Invalid")) + }, + }, + }, + { + "user created, additional property", + fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + schema.NewCreatedEvent( + context.Background(), + &schema.NewAggregate("id1", "instanceID").Aggregate, + "type", + json.RawMessage(`{ + "$schema": "urn:zitadel:schema:v1", + "type": "object", + "properties": { + "name": { + "type": "string" + } + } + }`), + []domain.AuthenticatorType{domain.AuthenticatorTypeUsername}, + ), + ), + ), + expectFilter(), + expectPush( + schemauser.NewCreatedEvent( + context.Background(), + &schemauser.NewAggregate("id1", "org1").Aggregate, + "type", + 1, + json.RawMessage(`{ + "additional": "property" + }`), + ), + ), + ), + idGenerator: mock.ExpectID(t, "id1"), + checkPermission: newMockPermissionCheckAllowed(), + }, + args{ + ctx: authz.NewMockContext("instanceID", "", ""), + user: &CreateSchemaUser{ + ResourceOwner: "org1", + SchemaID: "type", + schemaRevision: 1, + Data: json.RawMessage(`{ + "additional": "property" + }`), + }, + }, + res{ + details: &domain.ObjectDetails{ + ResourceOwner: "org1", + ID: "id1", + }, + }, + }, + { + "user create, invalid data attribute name", + fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + schema.NewCreatedEvent( + context.Background(), + &schema.NewAggregate("id1", "instanceID").Aggregate, + "type", + json.RawMessage(`{ + "$schema": "urn:zitadel:schema:v1", + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "additionalProperties": false + }`), + []domain.AuthenticatorType{domain.AuthenticatorTypeUsername}, + ), + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + idGenerator: mock.ExpectID(t, "id1"), + }, + args{ + ctx: authz.NewMockContext("instanceID", "org1", "user1"), + user: &CreateSchemaUser{ + ResourceOwner: "org1", + SchemaID: "type", + schemaRevision: 1, + Data: json.RawMessage(`{ + "invalid": "user" + }`), + }, + }, + res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowPreconditionFailed(nil, "COMMAND-SlKXqLSeL6", "Errors.UserSchema.Data.Invalid")) + }, + }, + }, + { + "user created, email return", + fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + schema.NewCreatedEvent( + context.Background(), + &schema.NewAggregate("id1", "instanceID").Aggregate, + "type", + json.RawMessage(`{ + "$schema": "urn:zitadel:schema:v1", + "type": "object", + "properties": { + "name": { + "type": "string" + } + } + }`), + []domain.AuthenticatorType{domain.AuthenticatorTypeUsername}, + ), + ), + ), + expectFilter(), + expectPush( + schemauser.NewCreatedEvent( + context.Background(), + &schemauser.NewAggregate("id1", "org1").Aggregate, + "type", + 1, + json.RawMessage(`{ + "name": "user" + }`), + ), + schemauser.NewEmailUpdatedEvent(context.Background(), + &schemauser.NewAggregate("id1", "org1").Aggregate, + "test@example.com", + ), + schemauser.NewEmailCodeAddedEvent(context.Background(), + &schemauser.NewAggregate("id1", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("emailverify"), + }, + time.Hour*1, + "https://example.com/email/verify?userID={{.UserID}}&code={{.Code}}&orgID={{.OrgID}}", + true, + ), + ), + ), + idGenerator: mock.ExpectID(t, "id1"), + checkPermission: newMockPermissionCheckAllowed(), + newCode: mockEncryptedCode("emailverify", time.Hour), + }, + args{ + ctx: authz.NewMockContext("instanceID", "", ""), + user: &CreateSchemaUser{ + ResourceOwner: "org1", + SchemaID: "type", + schemaRevision: 1, + Data: json.RawMessage(`{ + "name": "user" + }`), + Email: &Email{ + Address: "test@example.com", + ReturnCode: true, + URLTemplate: "https://example.com/email/verify?userID={{.UserID}}&code={{.Code}}&orgID={{.OrgID}}", + }, + }, + }, + res{ + details: &domain.ObjectDetails{ + ResourceOwner: "org1", + ID: "id1", + }, + returnCodeEmail: "emailverify", + }, + }, + { + "user created, email to verify", + fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + schema.NewCreatedEvent( + context.Background(), + &schema.NewAggregate("id1", "instanceID").Aggregate, + "type", + json.RawMessage(`{ + "$schema": "urn:zitadel:schema:v1", + "type": "object", + "properties": { + "name": { + "type": "string" + } + } + }`), + []domain.AuthenticatorType{domain.AuthenticatorTypeUsername}, + ), + ), + ), + expectFilter(), + expectPush( + schemauser.NewCreatedEvent( + context.Background(), + &schemauser.NewAggregate("id1", "org1").Aggregate, + "type", + 1, + json.RawMessage(`{ + "name": "user" + }`), + ), + schemauser.NewEmailUpdatedEvent(context.Background(), + &schemauser.NewAggregate("id1", "org1").Aggregate, + "test@example.com", + ), + schemauser.NewEmailCodeAddedEvent(context.Background(), + &schemauser.NewAggregate("id1", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("emailverify"), + }, + time.Hour*1, + "https://example.com/email/verify?userID={{.UserID}}&code={{.Code}}&orgID={{.OrgID}}", + false, + ), + ), + ), + idGenerator: mock.ExpectID(t, "id1"), + checkPermission: newMockPermissionCheckAllowed(), + newCode: mockEncryptedCode("emailverify", time.Hour), + }, + args{ + ctx: authz.NewMockContext("instanceID", "", ""), + user: &CreateSchemaUser{ + ResourceOwner: "org1", + SchemaID: "type", + schemaRevision: 1, + Data: json.RawMessage(`{ + "name": "user" + }`), + Email: &Email{ + Address: "test@example.com", + URLTemplate: "https://example.com/email/verify?userID={{.UserID}}&code={{.Code}}&orgID={{.OrgID}}", + }, + }, + }, + res{ + details: &domain.ObjectDetails{ + ResourceOwner: "org1", + ID: "id1", + }, + }, + }, + { + "user created, phone return", + fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + schema.NewCreatedEvent( + context.Background(), + &schema.NewAggregate("id1", "instanceID").Aggregate, + "type", + json.RawMessage(`{ + "$schema": "urn:zitadel:schema:v1", + "type": "object", + "properties": { + "name": { + "type": "string" + } + } + }`), + []domain.AuthenticatorType{domain.AuthenticatorTypeUsername}, + ), + ), + ), + expectFilter(), + expectPush( + schemauser.NewCreatedEvent( + context.Background(), + &schemauser.NewAggregate("id1", "org1").Aggregate, + "type", + 1, + json.RawMessage(`{ + "name": "user" + }`), + ), + schemauser.NewPhoneChangedEvent(context.Background(), + &schemauser.NewAggregate("id1", "org1").Aggregate, + "+41791234567", + ), + schemauser.NewPhoneCodeAddedEvent(context.Background(), + &schemauser.NewAggregate("id1", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("phoneverify"), + }, + time.Hour*1, + true, + ), + ), + ), + idGenerator: mock.ExpectID(t, "id1"), + checkPermission: newMockPermissionCheckAllowed(), + newCode: mockEncryptedCode("phoneverify", time.Hour), + }, + args{ + ctx: authz.NewMockContext("instanceID", "", ""), + user: &CreateSchemaUser{ + ResourceOwner: "org1", + SchemaID: "type", + schemaRevision: 1, + Data: json.RawMessage(`{ + "name": "user" + }`), + Phone: &Phone{ + Number: "+41791234567", + ReturnCode: true, + }, + }, + }, + res{ + details: &domain.ObjectDetails{ + ResourceOwner: "org1", + ID: "id1", + }, + returnCodePhone: "phoneverify", + }, + }, + { + "user created, phone to verify", + fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + schema.NewCreatedEvent( + context.Background(), + &schema.NewAggregate("id1", "instanceID").Aggregate, + "type", + json.RawMessage(`{ + "$schema": "urn:zitadel:schema:v1", + "type": "object", + "properties": { + "name": { + "type": "string" + } + } + }`), + []domain.AuthenticatorType{domain.AuthenticatorTypeUsername}, + ), + ), + ), + expectFilter(), + expectPush( + schemauser.NewCreatedEvent( + context.Background(), + &schemauser.NewAggregate("id1", "org1").Aggregate, + "type", + 1, + json.RawMessage(`{ + "name": "user" + }`), + ), + schemauser.NewPhoneChangedEvent(context.Background(), + &schemauser.NewAggregate("id1", "org1").Aggregate, + "+41791234567", + ), + schemauser.NewPhoneCodeAddedEvent(context.Background(), + &schemauser.NewAggregate("id1", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("phoneverify"), + }, + time.Hour*1, + false, + ), + ), + ), + idGenerator: mock.ExpectID(t, "id1"), + checkPermission: newMockPermissionCheckAllowed(), + newCode: mockEncryptedCode("phoneverify", time.Hour), + }, + args{ + ctx: authz.NewMockContext("instanceID", "", ""), + user: &CreateSchemaUser{ + ResourceOwner: "org1", + SchemaID: "type", + schemaRevision: 1, + Data: json.RawMessage(`{ + "name": "user" + }`), + Phone: &Phone{ + Number: "+41791234567", + }, + }, + }, + res{ + details: &domain.ObjectDetails{ + ResourceOwner: "org1", + ID: "id1", + }, + }, + }, + { + "user created, full verified", + fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + schema.NewCreatedEvent( + context.Background(), + &schema.NewAggregate("id1", "instanceID").Aggregate, + "type", + json.RawMessage(`{ + "$schema": "urn:zitadel:schema:v1", + "type": "object", + "properties": { + "name": { + "type": "string" + } + } + }`), + []domain.AuthenticatorType{domain.AuthenticatorTypeUsername}, + ), + ), + ), + expectFilter(), + expectPush( + schemauser.NewCreatedEvent( + context.Background(), + &schemauser.NewAggregate("id1", "org1").Aggregate, + "type", + 1, + json.RawMessage(`{ + "name": "user" + }`), + ), + schemauser.NewEmailUpdatedEvent(context.Background(), + &schemauser.NewAggregate("id1", "org1").Aggregate, + "test@example.com", + ), + schemauser.NewEmailVerifiedEvent(context.Background(), + &schemauser.NewAggregate("id1", "org1").Aggregate, + ), + schemauser.NewPhoneChangedEvent(context.Background(), + &schemauser.NewAggregate("id1", "org1").Aggregate, + "+41791234567", + ), + schemauser.NewPhoneVerifiedEvent(context.Background(), + &schemauser.NewAggregate("id1", "org1").Aggregate, + ), + ), + ), + idGenerator: mock.ExpectID(t, "id1"), + checkPermission: newMockPermissionCheckAllowed(), + }, + args{ + ctx: authz.NewMockContext("instanceID", "", ""), + user: &CreateSchemaUser{ + ResourceOwner: "org1", + SchemaID: "type", + schemaRevision: 1, + Data: json.RawMessage(`{ + "name": "user" + }`), + Email: &Email{Address: "test@example.com", Verified: true}, + Phone: &Phone{Number: "+41791234567", Verified: true}, + }, + }, + res{ + details: &domain.ObjectDetails{ + ResourceOwner: "org1", + ID: "id1", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &Commands{ + eventstore: tt.fields.eventstore(t), + idGenerator: tt.fields.idGenerator, + checkPermission: tt.fields.checkPermission, + newEncryptedCode: tt.fields.newCode, + } + err := c.CreateSchemaUser(tt.args.ctx, tt.args.user, crypto.CreateMockEncryptionAlg(gomock.NewController(t))) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assertObjectDetails(t, tt.res.details, tt.args.user.Details) + } + + if tt.res.returnCodePhone != "" { + assert.Equal(t, tt.res.returnCodePhone, tt.args.user.ReturnCodePhone) + } + if tt.res.returnCodeEmail != "" { + assert.Equal(t, tt.res.returnCodeEmail, tt.args.user.ReturnCodeEmail) + } + }) + } +} + +func TestCommandSide_DeleteSchemaUser(t *testing.T) { + type fields struct { + eventstore func(*testing.T) *eventstore.Eventstore + checkPermission domain.PermissionCheck + } + type ( + args struct { + ctx context.Context + userID string + } + ) + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "userid missing, invalid argument error", + fields: fields{ + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + userID: "", + }, + res: res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowInvalidArgument(nil, "COMMAND-Vs4wJCME7T", "Errors.IDMissing")) + }, + }, + }, + { + name: "user not existing, not found error", + fields: fields{ + eventstore: expectEventstore( + expectFilter(), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + }, + res: res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowNotFound(nil, "COMMAND-syHyCsGmvM", "Errors.User.NotFound")) + }, + }, + }, + { + name: "user removed, not found error", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + schemauser.NewCreatedEvent(context.Background(), + &schemauser.NewAggregate("user1", "org1").Aggregate, + "schema", + 1, + json.RawMessage(`{ + "name": "user" + }`), + ), + ), + eventFromEventPusher( + schemauser.NewDeletedEvent(context.Background(), + &schemauser.NewAggregate("user1", "org1").Aggregate, + ), + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + }, + res: res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowNotFound(nil, "COMMAND-syHyCsGmvM", "Errors.User.NotFound")) + }, + }, + }, + { + name: "remove user, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + schemauser.NewCreatedEvent(context.Background(), + &schemauser.NewAggregate("user1", "org1").Aggregate, + "schema", + 1, + json.RawMessage(`{ + "name": "user" + }`), + ), + ), + ), + expectPush( + schemauser.NewDeletedEvent(context.Background(), + &schemauser.NewAggregate("user1", "org1").Aggregate, + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "remove user, no permission", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + schemauser.NewCreatedEvent(context.Background(), + &schemauser.NewAggregate("user1", "org1").Aggregate, + "schema", + 1, + json.RawMessage(`{ + "name": "user" + }`), + ), + ), + ), + ), + checkPermission: newMockPermissionCheckNotAllowed(), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + }, + res: res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowPermissionDenied(nil, "AUTHZ-HKJD33", "Errors.PermissionDenied")) + }, + }, + }, + { + name: "remove user, self", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + schemauser.NewCreatedEvent(context.Background(), + &schemauser.NewAggregate("user1", "org1").Aggregate, + "schema", + 1, + json.RawMessage(`{ + "name": "user" + }`), + ), + ), + ), + expectPush( + schemauser.NewDeletedEvent(authz.NewMockContext("instanceID", "org1", "user1"), + &schemauser.NewAggregate("user1", "org1").Aggregate, + ), + ), + ), + }, + args: args{ + ctx: authz.NewMockContext("instanceID", "org1", "user1"), + userID: "user1", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore(t), + checkPermission: tt.fields.checkPermission, + } + got, err := r.DeleteSchemaUser(tt.args.ctx, tt.args.userID) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assertObjectDetails(t, tt.res.want, got) + } + }) + } +} diff --git a/internal/domain/custom_login_text.go b/internal/domain/custom_login_text.go index ac53d09b27..66b666dffb 100644 --- a/internal/domain/custom_login_text.go +++ b/internal/domain/custom_login_text.go @@ -265,12 +265,6 @@ const ( LoginKeyRegisterOrgSaveButtonText = LoginKeyRegistrationOrg + "SaveButtonText" LoginKeyRegisterOrgBackButtonText = LoginKeyRegistrationOrg + "BackButtonText" - LoginKeyLinkingUserPrompt = "LinkingUserPrompt." - LoginKeyLinkingUserPromptTitle = LoginKeyLinkingUserPrompt + "Title" - LoginKeyLinkingUserPromptDescription = LoginKeyLinkingUserPrompt + "Description" - LoginKeyLinkingUserPromptLinkButtonText = LoginKeyLinkingUserPrompt + "LinkButtonText" - LoginKeyLinkingUserPromptOtherButtonText = LoginKeyLinkingUserPrompt + "OtherButtonText" - LoginKeyLinkingUserDone = "LinkingUsersDone." LoginKeyLinkingUserDoneTitle = LoginKeyLinkingUserDone + "Title" LoginKeyLinkingUserDoneDescription = LoginKeyLinkingUserDone + "Description" @@ -343,7 +337,6 @@ type CustomLoginText struct { RegistrationUser RegistrationUserScreenText ExternalRegistrationUserOverview ExternalRegistrationUserOverviewScreenText RegistrationOrg RegistrationOrgScreenText - LinkingUserPrompt LinkingUserPromptScreenText LinkingUsersDone LinkingUserDoneScreenText ExternalNotFound ExternalUserNotFoundScreenText LoginSuccess SuccessLoginScreenText @@ -616,13 +609,6 @@ type RegistrationOrgScreenText struct { SaveButtonText string } -type LinkingUserPromptScreenText struct { - Title string - Description string - LinkButtonText string - OtherButtonText string -} - type LinkingUserDoneScreenText struct { Title string Description string diff --git a/internal/domain/schema/permission.go b/internal/domain/schema/permission.go index deb33ab14c..4a2fdee3cb 100644 --- a/internal/domain/schema/permission.go +++ b/internal/domain/schema/permission.go @@ -20,16 +20,16 @@ const ( PermissionProperty = "urn:zitadel:schema:permission" ) -type role int32 +type Role int32 const ( - roleUnspecified role = iota - roleSelf - roleOwner + RoleUnspecified Role = iota + RoleSelf + RoleOwner ) type permissionExtension struct { - role role + role Role } // Compile implements the [jsonschema.ExtCompiler] interface. @@ -57,14 +57,14 @@ func (c permissionExtension) Compile(ctx jsonschema.CompilerContext, m map[strin return } default: - return nil, zerrors.ThrowInvalidArgument(nil, "SCHEMA-GFjio", "invalid permission role") + return nil, zerrors.ThrowInvalidArgument(nil, "SCHEMA-GFjio", "invalid permission Role") } } return permissionExtensionConfig{c.role, perms}, nil } type permissionExtensionConfig struct { - role role + role Role permissions *permissions } @@ -72,17 +72,17 @@ type permissionExtensionConfig struct { // It validates the fields of the json instance according to the permission schema. func (s permissionExtensionConfig) Validate(ctx jsonschema.ValidationContext, v interface{}) error { switch s.role { - case roleSelf: + case RoleSelf: if s.permissions.self == nil || !s.permissions.self.write { return ctx.Error("permission", "missing required permission") } return nil - case roleOwner: + case RoleOwner: if s.permissions.owner == nil || !s.permissions.owner.write { return ctx.Error("permission", "missing required permission") } return nil - case roleUnspecified: + case RoleUnspecified: fallthrough default: return ctx.Error("permission", "missing required permission") diff --git a/internal/domain/schema/permission_test.go b/internal/domain/schema/permission_test.go index b0799384bd..11b665078c 100644 --- a/internal/domain/schema/permission_test.go +++ b/internal/domain/schema/permission_test.go @@ -14,7 +14,7 @@ import ( func TestPermissionExtension(t *testing.T) { type args struct { - role role + role Role schema string instance string } @@ -83,7 +83,7 @@ func TestPermissionExtension(t *testing.T) { }, }, { - "invalid role, compilation err", + "invalid Role, compilation err", args{ schema: `{ "type": "object", @@ -98,13 +98,13 @@ func TestPermissionExtension(t *testing.T) { }`, }, want{ - compilationErr: zerrors.ThrowInvalidArgument(nil, "SCHEMA-GFjio", "invalid permission role"), + compilationErr: zerrors.ThrowInvalidArgument(nil, "SCHEMA-GFjio", "invalid permission Role"), }, }, { "invalid permission self, validation err", args{ - role: roleSelf, + role: RoleSelf, schema: `{ "type": "object", "properties": { @@ -126,7 +126,7 @@ func TestPermissionExtension(t *testing.T) { { "invalid permission owner, validation err", args{ - role: roleOwner, + role: RoleOwner, schema: `{ "type": "object", "properties": { @@ -148,7 +148,7 @@ func TestPermissionExtension(t *testing.T) { { "valid permission self, ok", args{ - role: roleSelf, + role: RoleSelf, schema: `{ "type": "object", "properties": { @@ -170,7 +170,7 @@ func TestPermissionExtension(t *testing.T) { { "valid permission owner, ok", args{ - role: roleOwner, + role: RoleOwner, schema: `{ "type": "object", "properties": { @@ -190,9 +190,9 @@ func TestPermissionExtension(t *testing.T) { }, }, { - "no role, validation err", + "no Role, validation err", args{ - role: roleUnspecified, + role: RoleUnspecified, schema: `{ "type": "object", "properties": { @@ -214,7 +214,7 @@ func TestPermissionExtension(t *testing.T) { { "no permission required, ok", args{ - role: roleSelf, + role: RoleSelf, schema: `{ "type": "object", "properties": { diff --git a/internal/domain/schema/schema.go b/internal/domain/schema/schema.go index a8eee88de6..126d7473c2 100644 --- a/internal/domain/schema/schema.go +++ b/internal/domain/schema/schema.go @@ -19,7 +19,7 @@ const ( MetaSchemaID = "urn:zitadel:schema:v1" ) -func NewSchema(role role, r io.Reader) (*jsonschema.Schema, error) { +func NewSchema(role Role, r io.Reader) (*jsonschema.Schema, error) { c := jsonschema.NewCompiler() if err := c.AddResource(PermissionSchemaID, strings.NewReader(permissionJSON)); err != nil { return nil, err @@ -31,11 +31,11 @@ func NewSchema(role role, r io.Reader) (*jsonschema.Schema, error) { role, }) if err := c.AddResource("schema.json", r); err != nil { - return nil, zerrors.ThrowInvalidArgument(err, "COMMA-Frh42", "Errors.UserSchema.Schema.Invalid") + return nil, zerrors.ThrowInvalidArgument(err, "COMMA-Frh42", "Errors.UserSchema.Invalid") } schema, err := c.Compile("schema.json") if err != nil { - return nil, zerrors.ThrowInvalidArgument(err, "COMMA-W21tg", "Errors.UserSchema.Schema.Invalid") + return nil, zerrors.ThrowInvalidArgument(err, "COMMA-W21tg", "Errors.UserSchema.Invalid") } return schema, nil } diff --git a/internal/execution/execution_test.go b/internal/execution/execution_test.go index c2c6198edc..184823f9b2 100644 --- a/internal/execution/execution_test.go +++ b/internal/execution/execution_test.go @@ -5,7 +5,6 @@ import ( "context" "encoding/json" "errors" - "fmt" "io" "net/http" "net/http/httptest" @@ -378,7 +377,6 @@ func Test_CallTargets(t *testing.T) { } else { assert.NoError(t, err) } - fmt.Println(respBody) assert.Equal(t, tt.res.ret, respBody) }) } diff --git a/internal/feature/feature.go b/internal/feature/feature.go index 34dd5d908a..b4b2c21d4c 100644 --- a/internal/feature/feature.go +++ b/internal/feature/feature.go @@ -15,6 +15,7 @@ const ( KeyActions KeyImprovedPerformance KeyWebKey + KeyDebugOIDCParentError ) //go:generate enumer -type Level -transform snake -trimprefix Level @@ -39,6 +40,7 @@ type Features struct { Actions bool `json:"actions,omitempty"` ImprovedPerformance []ImprovedPerformanceType `json:"improved_performance,omitempty"` WebKey bool `json:"web_key,omitempty"` + DebugOIDCParentError bool `json:"debug_oidc_parent_error,omitempty"` } type ImprovedPerformanceType int32 diff --git a/internal/feature/key_enumer.go b/internal/feature/key_enumer.go index 6452a258c3..c6d12fcb75 100644 --- a/internal/feature/key_enumer.go +++ b/internal/feature/key_enumer.go @@ -7,11 +7,11 @@ import ( "strings" ) -const _KeyName = "unspecifiedlogin_default_orgtrigger_introspection_projectionslegacy_introspectionuser_schematoken_exchangeactionsimproved_performanceweb_key" +const _KeyName = "unspecifiedlogin_default_orgtrigger_introspection_projectionslegacy_introspectionuser_schematoken_exchangeactionsimproved_performanceweb_keydebug_oidc_parent_error" -var _KeyIndex = [...]uint8{0, 11, 28, 61, 81, 92, 106, 113, 133, 140} +var _KeyIndex = [...]uint8{0, 11, 28, 61, 81, 92, 106, 113, 133, 140, 163} -const _KeyLowerName = "unspecifiedlogin_default_orgtrigger_introspection_projectionslegacy_introspectionuser_schematoken_exchangeactionsimproved_performanceweb_key" +const _KeyLowerName = "unspecifiedlogin_default_orgtrigger_introspection_projectionslegacy_introspectionuser_schematoken_exchangeactionsimproved_performanceweb_keydebug_oidc_parent_error" func (i Key) String() string { if i < 0 || i >= Key(len(_KeyIndex)-1) { @@ -33,9 +33,10 @@ func _KeyNoOp() { _ = x[KeyActions-(6)] _ = x[KeyImprovedPerformance-(7)] _ = x[KeyWebKey-(8)] + _ = x[KeyDebugOIDCParentError-(9)] } -var _KeyValues = []Key{KeyUnspecified, KeyLoginDefaultOrg, KeyTriggerIntrospectionProjections, KeyLegacyIntrospection, KeyUserSchema, KeyTokenExchange, KeyActions, KeyImprovedPerformance, KeyWebKey} +var _KeyValues = []Key{KeyUnspecified, KeyLoginDefaultOrg, KeyTriggerIntrospectionProjections, KeyLegacyIntrospection, KeyUserSchema, KeyTokenExchange, KeyActions, KeyImprovedPerformance, KeyWebKey, KeyDebugOIDCParentError} var _KeyNameToValueMap = map[string]Key{ _KeyName[0:11]: KeyUnspecified, @@ -56,6 +57,8 @@ var _KeyNameToValueMap = map[string]Key{ _KeyLowerName[113:133]: KeyImprovedPerformance, _KeyName[133:140]: KeyWebKey, _KeyLowerName[133:140]: KeyWebKey, + _KeyName[140:163]: KeyDebugOIDCParentError, + _KeyLowerName[140:163]: KeyDebugOIDCParentError, } var _KeyNames = []string{ @@ -68,6 +71,7 @@ var _KeyNames = []string{ _KeyName[106:113], _KeyName[113:133], _KeyName[133:140], + _KeyName[140:163], } // KeyString retrieves an enum value from the enum constants string name. diff --git a/internal/integration/client.go b/internal/integration/client.go index 55f80938bf..ed5f0620c4 100644 --- a/internal/integration/client.go +++ b/internal/integration/client.go @@ -34,11 +34,14 @@ import ( idp_pb "github.com/zitadel/zitadel/pkg/grpc/idp/v2" mgmt "github.com/zitadel/zitadel/pkg/grpc/management" "github.com/zitadel/zitadel/pkg/grpc/object/v2" + object_v3alpha "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha" oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2" oidc_pb_v2beta "github.com/zitadel/zitadel/pkg/grpc/oidc/v2beta" "github.com/zitadel/zitadel/pkg/grpc/org/v2" org_v2beta "github.com/zitadel/zitadel/pkg/grpc/org/v2beta" action "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha" + user_v3alpha "github.com/zitadel/zitadel/pkg/grpc/resources/user/v3alpha" + userschema_v3alpha "github.com/zitadel/zitadel/pkg/grpc/resources/userschema/v3alpha" webkey_v3alpha "github.com/zitadel/zitadel/pkg/grpc/resources/webkey/v3alpha" "github.com/zitadel/zitadel/pkg/grpc/session/v2" session_v2beta "github.com/zitadel/zitadel/pkg/grpc/session/v2beta" @@ -46,8 +49,7 @@ import ( settings_v2beta "github.com/zitadel/zitadel/pkg/grpc/settings/v2beta" "github.com/zitadel/zitadel/pkg/grpc/system" user_pb "github.com/zitadel/zitadel/pkg/grpc/user" - schema "github.com/zitadel/zitadel/pkg/grpc/user/schema/v3alpha" - "github.com/zitadel/zitadel/pkg/grpc/user/v2" + user_v2 "github.com/zitadel/zitadel/pkg/grpc/user/v2" user_v2beta "github.com/zitadel/zitadel/pkg/grpc/user/v2beta" ) @@ -57,7 +59,7 @@ type Client struct { Mgmt mgmt.ManagementServiceClient Auth auth.AuthServiceClient UserV2beta user_v2beta.UserServiceClient - UserV2 user.UserServiceClient + UserV2 user_v2.UserServiceClient SessionV2beta session_v2beta.SessionServiceClient SessionV2 session.SessionServiceClient SettingsV2beta settings_v2beta.SettingsServiceClient @@ -70,9 +72,10 @@ type Client struct { ActionV3Alpha action.ZITADELActionsClient FeatureV2beta feature_v2beta.FeatureServiceClient FeatureV2 feature.FeatureServiceClient - UserSchemaV3 schema.UserSchemaServiceClient + UserSchemaV3 userschema_v3alpha.ZITADELUserSchemasClient WebKeyV3Alpha webkey_v3alpha.ZITADELWebKeysClient IDPv2 idp_pb.IdentityProviderServiceClient + UserV3Alpha user_v3alpha.ZITADELUsersClient } func newClient(cc *grpc.ClientConn) Client { @@ -82,7 +85,7 @@ func newClient(cc *grpc.ClientConn) Client { Mgmt: mgmt.NewManagementServiceClient(cc), Auth: auth.NewAuthServiceClient(cc), UserV2beta: user_v2beta.NewUserServiceClient(cc), - UserV2: user.NewUserServiceClient(cc), + UserV2: user_v2.NewUserServiceClient(cc), SessionV2beta: session_v2beta.NewSessionServiceClient(cc), SessionV2: session.NewSessionServiceClient(cc), SettingsV2beta: settings_v2beta.NewSettingsServiceClient(cc), @@ -95,9 +98,10 @@ func newClient(cc *grpc.ClientConn) Client { ActionV3Alpha: action.NewZITADELActionsClient(cc), FeatureV2beta: feature_v2beta.NewFeatureServiceClient(cc), FeatureV2: feature.NewFeatureServiceClient(cc), - UserSchemaV3: schema.NewUserSchemaServiceClient(cc), + UserSchemaV3: userschema_v3alpha.NewZITADELUserSchemasClient(cc), WebKeyV3Alpha: webkey_v3alpha.NewZITADELWebKeysClient(cc), IDPv2: idp_pb.NewIdentityProviderServiceClient(cc), + UserV3Alpha: user_v3alpha.NewZITADELUsersClient(cc), } } @@ -148,29 +152,29 @@ func (t *Tester) UseIsolatedInstance(tt *testing.T, iamOwnerCtx, systemCtx conte return primaryDomain, instanceId, adminUser.GetUserId(), t.updateInstanceAndOrg(newCtx, fmt.Sprintf("%s:%d", primaryDomain, t.Config.ExternalPort)) } -func (s *Tester) CreateHumanUser(ctx context.Context) *user.AddHumanUserResponse { - resp, err := s.Client.UserV2.AddHumanUser(ctx, &user.AddHumanUserRequest{ +func (s *Tester) CreateHumanUser(ctx context.Context) *user_v2.AddHumanUserResponse { + resp, err := s.Client.UserV2.AddHumanUser(ctx, &user_v2.AddHumanUserRequest{ Organization: &object.Organization{ Org: &object.Organization_OrgId{ OrgId: s.Organisation.ID, }, }, - Profile: &user.SetHumanProfile{ + Profile: &user_v2.SetHumanProfile{ GivenName: "Mickey", FamilyName: "Mouse", PreferredLanguage: gu.Ptr("nl"), - Gender: gu.Ptr(user.Gender_GENDER_MALE), + Gender: gu.Ptr(user_v2.Gender_GENDER_MALE), }, - Email: &user.SetHumanEmail{ + Email: &user_v2.SetHumanEmail{ Email: fmt.Sprintf("%d@mouse.com", time.Now().UnixNano()), - Verification: &user.SetHumanEmail_ReturnCode{ - ReturnCode: &user.ReturnEmailVerificationCode{}, + Verification: &user_v2.SetHumanEmail_ReturnCode{ + ReturnCode: &user_v2.ReturnEmailVerificationCode{}, }, }, - Phone: &user.SetHumanPhone{ + Phone: &user_v2.SetHumanPhone{ Phone: "+41791234567", - Verification: &user.SetHumanPhone_ReturnCode{ - ReturnCode: &user.ReturnPhoneVerificationCode{}, + Verification: &user_v2.SetHumanPhone_ReturnCode{ + ReturnCode: &user_v2.ReturnPhoneVerificationCode{}, }, }, }) @@ -178,23 +182,23 @@ func (s *Tester) CreateHumanUser(ctx context.Context) *user.AddHumanUserResponse return resp } -func (s *Tester) CreateHumanUserNoPhone(ctx context.Context) *user.AddHumanUserResponse { - resp, err := s.Client.UserV2.AddHumanUser(ctx, &user.AddHumanUserRequest{ +func (s *Tester) CreateHumanUserNoPhone(ctx context.Context) *user_v2.AddHumanUserResponse { + resp, err := s.Client.UserV2.AddHumanUser(ctx, &user_v2.AddHumanUserRequest{ Organization: &object.Organization{ Org: &object.Organization_OrgId{ OrgId: s.Organisation.ID, }, }, - Profile: &user.SetHumanProfile{ + Profile: &user_v2.SetHumanProfile{ GivenName: "Mickey", FamilyName: "Mouse", PreferredLanguage: gu.Ptr("nl"), - Gender: gu.Ptr(user.Gender_GENDER_MALE), + Gender: gu.Ptr(user_v2.Gender_GENDER_MALE), }, - Email: &user.SetHumanEmail{ + Email: &user_v2.SetHumanEmail{ Email: fmt.Sprintf("%d@mouse.com", time.Now().UnixNano()), - Verification: &user.SetHumanEmail_ReturnCode{ - ReturnCode: &user.ReturnEmailVerificationCode{}, + Verification: &user_v2.SetHumanEmail_ReturnCode{ + ReturnCode: &user_v2.ReturnEmailVerificationCode{}, }, }, }) @@ -202,29 +206,29 @@ func (s *Tester) CreateHumanUserNoPhone(ctx context.Context) *user.AddHumanUserR return resp } -func (s *Tester) CreateHumanUserWithTOTP(ctx context.Context, secret string) *user.AddHumanUserResponse { - resp, err := s.Client.UserV2.AddHumanUser(ctx, &user.AddHumanUserRequest{ +func (s *Tester) CreateHumanUserWithTOTP(ctx context.Context, secret string) *user_v2.AddHumanUserResponse { + resp, err := s.Client.UserV2.AddHumanUser(ctx, &user_v2.AddHumanUserRequest{ Organization: &object.Organization{ Org: &object.Organization_OrgId{ OrgId: s.Organisation.ID, }, }, - Profile: &user.SetHumanProfile{ + Profile: &user_v2.SetHumanProfile{ GivenName: "Mickey", FamilyName: "Mouse", PreferredLanguage: gu.Ptr("nl"), - Gender: gu.Ptr(user.Gender_GENDER_MALE), + Gender: gu.Ptr(user_v2.Gender_GENDER_MALE), }, - Email: &user.SetHumanEmail{ + Email: &user_v2.SetHumanEmail{ Email: fmt.Sprintf("%d@mouse.com", time.Now().UnixNano()), - Verification: &user.SetHumanEmail_ReturnCode{ - ReturnCode: &user.ReturnEmailVerificationCode{}, + Verification: &user_v2.SetHumanEmail_ReturnCode{ + ReturnCode: &user_v2.ReturnEmailVerificationCode{}, }, }, - Phone: &user.SetHumanPhone{ + Phone: &user_v2.SetHumanPhone{ Phone: "+41791234567", - Verification: &user.SetHumanPhone_ReturnCode{ - ReturnCode: &user.ReturnPhoneVerificationCode{}, + Verification: &user_v2.SetHumanPhone_ReturnCode{ + ReturnCode: &user_v2.ReturnPhoneVerificationCode{}, }, }, TotpSecret: gu.Ptr(secret), @@ -239,15 +243,15 @@ func (s *Tester) CreateOrganization(ctx context.Context, name, adminEmail string Admins: []*org.AddOrganizationRequest_Admin{ { UserType: &org.AddOrganizationRequest_Admin_Human{ - Human: &user.AddHumanUserRequest{ - Profile: &user.SetHumanProfile{ + Human: &user_v2.AddHumanUserRequest{ + Profile: &user_v2.SetHumanProfile{ GivenName: "firstname", FamilyName: "lastname", }, - Email: &user.SetHumanEmail{ + Email: &user_v2.SetHumanEmail{ Email: adminEmail, - Verification: &user.SetHumanEmail_ReturnCode{ - ReturnCode: &user.ReturnEmailVerificationCode{}, + Verification: &user_v2.SetHumanEmail_ReturnCode{ + ReturnCode: &user_v2.ReturnEmailVerificationCode{}, }, }, }, @@ -292,29 +296,29 @@ func (s *Tester) CreateOrganizationWithUserID(ctx context.Context, name, userID return resp } -func (s *Tester) CreateHumanUserVerified(ctx context.Context, org, email string) *user.AddHumanUserResponse { - resp, err := s.Client.UserV2.AddHumanUser(ctx, &user.AddHumanUserRequest{ +func (s *Tester) CreateHumanUserVerified(ctx context.Context, org, email string) *user_v2.AddHumanUserResponse { + resp, err := s.Client.UserV2.AddHumanUser(ctx, &user_v2.AddHumanUserRequest{ Organization: &object.Organization{ Org: &object.Organization_OrgId{ OrgId: org, }, }, - Profile: &user.SetHumanProfile{ + Profile: &user_v2.SetHumanProfile{ GivenName: "Mickey", FamilyName: "Mouse", NickName: gu.Ptr("Mickey"), PreferredLanguage: gu.Ptr("nl"), - Gender: gu.Ptr(user.Gender_GENDER_MALE), + Gender: gu.Ptr(user_v2.Gender_GENDER_MALE), }, - Email: &user.SetHumanEmail{ + Email: &user_v2.SetHumanEmail{ Email: email, - Verification: &user.SetHumanEmail_IsVerified{ + Verification: &user_v2.SetHumanEmail_IsVerified{ IsVerified: true, }, }, - Phone: &user.SetHumanPhone{ + Phone: &user_v2.SetHumanPhone{ Phone: "+41791234567", - Verification: &user.SetHumanPhone_IsVerified{ + Verification: &user_v2.SetHumanPhone_IsVerified{ IsVerified: true, }, }, @@ -334,12 +338,12 @@ func (s *Tester) CreateMachineUser(ctx context.Context) *mgmt.AddMachineUserResp return resp } -func (s *Tester) CreateUserIDPlink(ctx context.Context, userID, externalID, idpID, username string) *user.AddIDPLinkResponse { +func (s *Tester) CreateUserIDPlink(ctx context.Context, userID, externalID, idpID, username string) *user_v2.AddIDPLinkResponse { resp, err := s.Client.UserV2.AddIDPLink( ctx, - &user.AddIDPLinkRequest{ + &user_v2.AddIDPLinkRequest{ UserId: userID, - IdpLink: &user.IDPLink{ + IdpLink: &user_v2.IDPLink{ IdpId: idpID, UserId: externalID, UserName: username, @@ -351,13 +355,13 @@ func (s *Tester) CreateUserIDPlink(ctx context.Context, userID, externalID, idpI } func (s *Tester) RegisterUserPasskey(ctx context.Context, userID string) { - reg, err := s.Client.UserV2.CreatePasskeyRegistrationLink(ctx, &user.CreatePasskeyRegistrationLinkRequest{ + reg, err := s.Client.UserV2.CreatePasskeyRegistrationLink(ctx, &user_v2.CreatePasskeyRegistrationLinkRequest{ UserId: userID, - Medium: &user.CreatePasskeyRegistrationLinkRequest_ReturnCode{}, + Medium: &user_v2.CreatePasskeyRegistrationLinkRequest_ReturnCode{}, }) logging.OnError(err).Fatal("create user passkey") - pkr, err := s.Client.UserV2.RegisterPasskey(ctx, &user.RegisterPasskeyRequest{ + pkr, err := s.Client.UserV2.RegisterPasskey(ctx, &user_v2.RegisterPasskeyRequest{ UserId: userID, Code: reg.GetCode(), Domain: s.Config.ExternalDomain, @@ -366,7 +370,7 @@ func (s *Tester) RegisterUserPasskey(ctx context.Context, userID string) { attestationResponse, err := s.WebAuthN.CreateAttestationResponse(pkr.GetPublicKeyCredentialCreationOptions()) logging.OnError(err).Fatal("create user passkey") - _, err = s.Client.UserV2.VerifyPasskeyRegistration(ctx, &user.VerifyPasskeyRegistrationRequest{ + _, err = s.Client.UserV2.VerifyPasskeyRegistration(ctx, &user_v2.VerifyPasskeyRegistrationRequest{ UserId: userID, PasskeyId: pkr.GetPasskeyId(), PublicKeyCredential: attestationResponse, @@ -376,7 +380,7 @@ func (s *Tester) RegisterUserPasskey(ctx context.Context, userID string) { } func (s *Tester) RegisterUserU2F(ctx context.Context, userID string) { - pkr, err := s.Client.UserV2.RegisterU2F(ctx, &user.RegisterU2FRequest{ + pkr, err := s.Client.UserV2.RegisterU2F(ctx, &user_v2.RegisterU2FRequest{ UserId: userID, Domain: s.Config.ExternalDomain, }) @@ -384,7 +388,7 @@ func (s *Tester) RegisterUserU2F(ctx context.Context, userID string) { attestationResponse, err := s.WebAuthN.CreateAttestationResponse(pkr.GetPublicKeyCredentialCreationOptions()) logging.OnError(err).Fatal("create user u2f") - _, err = s.Client.UserV2.VerifyU2FRegistration(ctx, &user.VerifyU2FRegistrationRequest{ + _, err = s.Client.UserV2.VerifyU2FRegistration(ctx, &user_v2.VerifyU2FRegistrationRequest{ UserId: userID, U2FId: pkr.GetU2FId(), PublicKeyCredential: attestationResponse, @@ -394,9 +398,9 @@ func (s *Tester) RegisterUserU2F(ctx context.Context, userID string) { } func (s *Tester) SetUserPassword(ctx context.Context, userID, password string, changeRequired bool) *object.Details { - resp, err := s.Client.UserV2.SetPassword(ctx, &user.SetPasswordRequest{ + resp, err := s.Client.UserV2.SetPassword(ctx, &user_v2.SetPasswordRequest{ UserId: userID, - NewPassword: &user.Password{ + NewPassword: &user_v2.Password{ Password: password, ChangeRequired: changeRequired, }, @@ -757,24 +761,57 @@ func (s *Tester) SetExecution(ctx context.Context, t *testing.T, cond *action.Co return target } -func (s *Tester) CreateUserSchema(ctx context.Context, t *testing.T) *schema.CreateUserSchemaResponse { - return s.CreateUserSchemaWithType(ctx, t, fmt.Sprint(time.Now().UnixNano()+1)) +func (s *Tester) CreateUserSchemaEmpty(ctx context.Context) *userschema_v3alpha.CreateUserSchemaResponse { + return s.CreateUserSchemaEmptyWithType(ctx, fmt.Sprint(time.Now().UnixNano()+1)) } -func (s *Tester) CreateUserSchemaWithType(ctx context.Context, t *testing.T, schemaType string) *schema.CreateUserSchemaResponse { +func (s *Tester) CreateUserSchema(ctx context.Context, schemaData []byte) *userschema_v3alpha.CreateUserSchemaResponse { + userSchema := new(structpb.Struct) + err := userSchema.UnmarshalJSON(schemaData) + logging.OnError(err).Fatal("create userschema unmarshal") + schema, err := s.Client.UserSchemaV3.CreateUserSchema(ctx, &userschema_v3alpha.CreateUserSchemaRequest{ + UserSchema: &userschema_v3alpha.UserSchema{ + Type: fmt.Sprint(time.Now().UnixNano() + 1), + DataType: &userschema_v3alpha.UserSchema_Schema{ + Schema: userSchema, + }, + }, + }) + logging.OnError(err).Fatal("create userschema") + return schema +} + +func (s *Tester) CreateUserSchemaEmptyWithType(ctx context.Context, schemaType string) *userschema_v3alpha.CreateUserSchemaResponse { userSchema := new(structpb.Struct) err := userSchema.UnmarshalJSON([]byte(`{ "$schema": "urn:zitadel:schema:v1", "type": "object", "properties": {} }`)) - require.NoError(t, err) - target, err := s.Client.UserSchemaV3.CreateUserSchema(ctx, &schema.CreateUserSchemaRequest{ - Type: schemaType, - DataType: &schema.CreateUserSchemaRequest_Schema{ - Schema: userSchema, + logging.OnError(err).Fatal("create userschema unmarshal") + schema, err := s.Client.UserSchemaV3.CreateUserSchema(ctx, &userschema_v3alpha.CreateUserSchemaRequest{ + UserSchema: &userschema_v3alpha.UserSchema{ + Type: schemaType, + DataType: &userschema_v3alpha.UserSchema_Schema{ + Schema: userSchema, + }, }, }) - require.NoError(t, err) - return target + logging.OnError(err).Fatal("create userschema") + return schema +} + +func (s *Tester) CreateSchemaUser(ctx context.Context, orgID string, schemaID string, data []byte) *user_v3alpha.CreateUserResponse { + userData := new(structpb.Struct) + err := userData.UnmarshalJSON(data) + logging.OnError(err).Fatal("create user unmarshal") + user, err := s.Client.UserV3Alpha.CreateUser(ctx, &user_v3alpha.CreateUserRequest{ + Organization: &object_v3alpha.Organization{Property: &object_v3alpha.Organization_OrgId{OrgId: orgID}}, + User: &user_v3alpha.CreateUser{ + SchemaId: schemaID, + Data: userData, + }, + }) + logging.OnError(err).Fatal("create user") + return user } diff --git a/internal/integration/config/zitadel.yaml b/internal/integration/config/zitadel.yaml index 291099ea1c..d58529b7a8 100644 --- a/internal/integration/config/zitadel.yaml +++ b/internal/integration/config/zitadel.yaml @@ -53,4 +53,13 @@ SystemAPIUsers: KeyData: "LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUF6aStGRlNKTDdmNXl3NEtUd3pnTQpQMzRlUEd5Y20vTStrVDBNN1Y0Q2d4NVYzRWFESXZUUUtUTGZCYUVCNDV6YjlMdGpJWHpEdzByWFJvUzJoTzZ0CmgrQ1lRQ3ozS0N2aDA5QzBJenhaaUIySVMzSC9hVCs1Qng5RUZZK3ZuQWtaamNjYnlHNVlOUnZtdE9sbnZJZUkKSDdxWjB0RXdrUGZGNUdFWk5QSlB0bXkzVUdWN2lvZmRWUVMxeFJqNzMrYU13NXJ2SDREOElkeWlBQzNWZWtJYgpwdDBWajBTVVgzRHdLdG9nMzM3QnpUaVBrM2FYUkYwc2JGaFFvcWRKUkk4TnFnWmpDd2pxOXlmSTV0eXhZc3duCitKR3pIR2RIdlczaWRPRGxtd0V0NUsycGFzaVJJV0syT0dmcSt3MEVjbHRRSGFidXFFUGdabG1oQ2tSZE5maXgKQndJREFRQUIKLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tCg==" InitProjections: - Enabled: true \ No newline at end of file + Enabled: true + +# Extend key lifetimes so we do not see more legacy keys when +# integration tests are rerun on the same DB with more than 6 hours apart. +# The test counts the amount of keys returned from the JWKS endpoint and fails +# with 2 or more legacy public keys, +SystemDefaults: + KeyConfig: + PrivateKeyLifetime: 7200h + PublicKeyLifetime: 14400h diff --git a/internal/integration/integration.go b/internal/integration/integration.go index 18836be8fc..20af65993b 100644 --- a/internal/integration/integration.go +++ b/internal/integration/integration.go @@ -431,3 +431,19 @@ func (s *Tester) updateInstanceAndOrg(ctx context.Context, domain string) contex logging.OnError(err).Fatal("query organisation") return ctx } + +func await(af func() error) error { + maxTimer := time.NewTimer(15 * time.Minute) + for { + err := af() + if err == nil { + return nil + } + select { + case <-maxTimer.C: + return err + case <-time.After(time.Second / 10): + continue + } + } +} diff --git a/internal/integration/oidc.go b/internal/integration/oidc.go index 3e90cb6856..1d15d25f29 100644 --- a/internal/integration/oidc.go +++ b/internal/integration/oidc.go @@ -3,6 +3,7 @@ package integration import ( "context" "fmt" + "io" "net/http" "net/url" "strings" @@ -27,7 +28,7 @@ func (s *Tester) CreateOIDCClient(ctx context.Context, redirectURI, logoutRedire if len(grantTypes) == 0 { grantTypes = []app.OIDCGrantType{app.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE, app.OIDCGrantType_OIDC_GRANT_TYPE_REFRESH_TOKEN} } - return s.Client.Mgmt.AddOIDCApp(ctx, &management.AddOIDCAppRequest{ + resp, err := s.Client.Mgmt.AddOIDCApp(ctx, &management.AddOIDCAppRequest{ ProjectId: projectID, Name: fmt.Sprintf("app-%d", time.Now().UnixNano()), RedirectUris: []string{redirectURI}, @@ -46,6 +47,16 @@ func (s *Tester) CreateOIDCClient(ctx context.Context, redirectURI, logoutRedire AdditionalOrigins: nil, SkipNativeAppSuccessPage: false, }) + if err != nil { + return nil, err + } + return resp, await(func() error { + _, err := s.Client.Mgmt.GetAppByID(ctx, &management.GetAppByIDRequest{ + ProjectId: projectID, + AppId: resp.GetAppId(), + }) + return err + }) } func (s *Tester) CreateOIDCNativeClient(ctx context.Context, redirectURI, logoutRedirectURI, projectID string, devMode bool) (*management.AddOIDCAppResponse, error) { @@ -95,7 +106,7 @@ func (s *Tester) CreateOIDCImplicitFlowClient(ctx context.Context, redirectURI s if err != nil { return nil, err } - return s.Client.Mgmt.AddOIDCApp(ctx, &management.AddOIDCAppRequest{ + resp, err := s.Client.Mgmt.AddOIDCApp(ctx, &management.AddOIDCAppRequest{ ProjectId: project.GetId(), Name: fmt.Sprintf("app-%d", time.Now().UnixNano()), RedirectUris: []string{redirectURI}, @@ -114,6 +125,16 @@ func (s *Tester) CreateOIDCImplicitFlowClient(ctx context.Context, redirectURI s AdditionalOrigins: nil, SkipNativeAppSuccessPage: false, }) + if err != nil { + return nil, err + } + return resp, await(func() error { + _, err := s.Client.Mgmt.GetAppByID(ctx, &management.GetAppByIDRequest{ + ProjectId: project.GetId(), + AppId: resp.GetAppId(), + }) + return err + }) } func (s *Tester) CreateOIDCTokenExchangeClient(ctx context.Context) (client *management.AddOIDCAppResponse, keyData []byte, err error) { @@ -284,6 +305,10 @@ func CheckRedirect(req *http.Request) (*url.URL, error) { return nil, err } defer resp.Body.Close() + if resp.StatusCode < 300 || resp.StatusCode >= 400 { + body, _ := io.ReadAll(resp.Body) + return nil, fmt.Errorf("check redirect unexpected status: %q; body: %q", resp.Status, body) + } return resp.Location() } diff --git a/internal/query/access_token.go b/internal/query/access_token.go index a777a6afc7..4180a6ad5e 100644 --- a/internal/query/access_token.go +++ b/internal/query/access_token.go @@ -109,14 +109,14 @@ func (q *Queries) ActiveAccessTokenByToken(ctx context.Context, token string) (m split := strings.Split(token, "-") if len(split) != 2 { - return nil, zerrors.ThrowPermissionDenied(nil, "QUERY-LJK2W", "Errors.OIDCSession.Token.Invalid") + return nil, zerrors.ThrowUnauthenticated(nil, "QUERY-LJK2W", "Errors.OIDCSession.Token.Invalid") } model, err = q.accessTokenByOIDCSessionAndTokenID(ctx, split[0], split[1]) if err != nil { return nil, err } if !model.AccessTokenExpiration.After(time.Now()) { - return nil, zerrors.ThrowPermissionDenied(nil, "QUERY-SAF3rf", "Errors.OIDCSession.Token.Expired") + return nil, zerrors.ThrowUnauthenticated(nil, "QUERY-SAF3rf", "Errors.OIDCSession.Token.Expired") } if err = q.checkSessionNotTerminatedAfter(ctx, model.SessionID, model.UserID, model.Position, model.UserAgent.GetFingerprintID()); err != nil { return nil, err @@ -130,10 +130,10 @@ func (q *Queries) accessTokenByOIDCSessionAndTokenID(ctx context.Context, oidcSe model = newOIDCSessionAccessTokenReadModel(oidcSessionID) if err = q.eventstore.FilterToQueryReducer(ctx, model); err != nil { - return nil, zerrors.ThrowPermissionDenied(err, "QUERY-ASfe2", "Errors.OIDCSession.Token.Invalid") + return nil, zerrors.ThrowUnauthenticated(err, "QUERY-ASfe2", "Errors.OIDCSession.Token.Invalid") } if model.AccessTokenID != tokenID { - return nil, zerrors.ThrowPermissionDenied(nil, "QUERY-M2u9w", "Errors.OIDCSession.Token.Invalid") + return nil, zerrors.ThrowUnauthenticated(nil, "QUERY-M2u9w", "Errors.OIDCSession.Token.Invalid") } return model, nil } @@ -152,11 +152,11 @@ func (q *Queries) checkSessionNotTerminatedAfter(ctx context.Context, sessionID, } err = q.eventstore.FilterToQueryReducer(ctx, model) if err != nil { - return zerrors.ThrowPermissionDenied(err, "QUERY-SJ642", "Errors.Internal") + return zerrors.ThrowUnauthenticated(err, "QUERY-SJ642", "Errors.Internal") } if model.terminated { - return zerrors.ThrowPermissionDenied(nil, "QUERY-IJL3H", "Errors.OIDCSession.Token.Invalid") + return zerrors.ThrowUnauthenticated(nil, "QUERY-IJL3H", "Errors.OIDCSession.Token.Invalid") } return nil } diff --git a/internal/query/custom_text.go b/internal/query/custom_text.go index 4d766b5102..e92c910b69 100644 --- a/internal/query/custom_text.go +++ b/internal/query/custom_text.go @@ -409,9 +409,6 @@ func CustomTextsToLoginDomain(instanceID, aggregateID, lang string, texts *Custo if strings.HasPrefix(text.Key, domain.LoginKeyRegistrationOrg) { registrationOrgKeyToDomain(text, result) } - if strings.HasPrefix(text.Key, domain.LoginKeyLinkingUserPrompt) { - linkingUserPromptKeyToDomain(text, result) - } if strings.HasPrefix(text.Key, domain.LoginKeyLinkingUserDone) { linkingUserDoneKeyToDomain(text, result) } @@ -1106,21 +1103,6 @@ func registrationOrgKeyToDomain(text *CustomText, result *domain.CustomLoginText } } -func linkingUserPromptKeyToDomain(text *CustomText, result *domain.CustomLoginText) { - if text.Key == domain.LoginKeyLinkingUserPromptTitle { - result.LinkingUserPrompt.Title = text.Text - } - if text.Key == domain.LoginKeyLinkingUserPromptDescription { - result.LinkingUserPrompt.Description = text.Text - } - if text.Key == domain.LoginKeyLinkingUserPromptLinkButtonText { - result.LinkingUserPrompt.LinkButtonText = text.Text - } - if text.Key == domain.LoginKeyLinkingUserPromptOtherButtonText { - result.LinkingUserPrompt.OtherButtonText = text.Text - } -} - func linkingUserDoneKeyToDomain(text *CustomText, result *domain.CustomLoginText) { if text.Key == domain.LoginKeyLinkingUserDoneTitle { result.LinkingUsersDone.Title = text.Text diff --git a/internal/query/idp_user_link.go b/internal/query/idp_user_link.go index 4f13d9d315..5caf6c6646 100644 --- a/internal/query/idp_user_link.go +++ b/internal/query/idp_user_link.go @@ -3,6 +3,7 @@ package query import ( "context" "database/sql" + "slices" sq "github.com/Masterminds/squirrel" @@ -42,6 +43,15 @@ func (q *IDPUserLinksSearchQuery) toQuery(query sq.SelectBuilder) sq.SelectBuild return query } +func (q *IDPUserLinksSearchQuery) hasUserID() bool { + for _, query := range q.Queries { + if query.Col() == IDPUserLinkUserIDCol { + return true + } + } + return false +} + var ( idpUserLinkTable = table{ name: projection.IDPUserLinkTable, @@ -89,30 +99,33 @@ var ( } ) -func (l *IDPUserLinks) RemoveNoPermission(ctx context.Context, permissionCheck domain.PermissionCheck) { - removableIndexes := make([]int, 0) - for i := range l.Links { - ctxData := authz.GetCtxData(ctx) - if ctxData.UserID != l.Links[i].UserID { - if err := permissionCheck(ctx, domain.PermissionUserRead, l.Links[i].ResourceOwner, l.Links[i].UserID); err != nil { - removableIndexes = append(removableIndexes, i) +func idpLinksCheckPermission(ctx context.Context, links *IDPUserLinks, permissionCheck domain.PermissionCheck) { + links.Links = slices.DeleteFunc(links.Links, + func(link *IDPUserLink) bool { + return userCheckPermission(ctx, link.ResourceOwner, link.UserID, permissionCheck) != nil + }, + ) +} + +func (q *Queries) IDPUserLinks(ctx context.Context, queries *IDPUserLinksSearchQuery, permissionCheck domain.PermissionCheck) (idps *IDPUserLinks, err error) { + links, err := q.idpUserLinks(ctx, queries, false) + if err != nil { + return nil, err + } + if permissionCheck != nil && len(links.Links) > 0 { + // when userID for query is provided, only one check has to be done + if queries.hasUserID() { + if err := userCheckPermission(ctx, links.Links[0].ResourceOwner, links.Links[0].UserID, permissionCheck); err != nil { + return nil, err } + } else { + idpLinksCheckPermission(ctx, links, permissionCheck) } } - removed := 0 - for _, removeIndex := range removableIndexes { - l.Links = removeIDPLink(l.Links, removeIndex-removed) - removed++ - } - // reset count as some users could be removed - l.SearchResponse.Count = uint64(len(l.Links)) + return links, nil } -func removeIDPLink(slice []*IDPUserLink, s int) []*IDPUserLink { - return append(slice[:s], slice[s+1:]...) -} - -func (q *Queries) IDPUserLinks(ctx context.Context, queries *IDPUserLinksSearchQuery, withOwnerRemoved bool) (idps *IDPUserLinks, err error) { +func (q *Queries) idpUserLinks(ctx context.Context, queries *IDPUserLinksSearchQuery, withOwnerRemoved bool) (idps *IDPUserLinks, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() diff --git a/internal/query/idp_user_link_test.go b/internal/query/idp_user_link_test.go index bcbe6c2062..b8ba2d087a 100644 --- a/internal/query/idp_user_link_test.go +++ b/internal/query/idp_user_link_test.go @@ -1,6 +1,7 @@ package query import ( + "context" "database/sql" "database/sql/driver" "errors" @@ -8,9 +9,175 @@ import ( "regexp" "testing" + "github.com/stretchr/testify/require" + + "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/domain" ) +func TestUser_idpLinksCheckPermission(t *testing.T) { + type want struct { + links []*IDPUserLink + } + type args struct { + user string + links *IDPUserLinks + } + tests := []struct { + name string + args args + want want + permissions []string + }{ + { + "permissions for all users", + args{ + "none", + &IDPUserLinks{ + Links: []*IDPUserLink{ + {UserID: "first"}, {UserID: "second"}, {UserID: "third"}, + }, + }, + }, + want{ + links: []*IDPUserLink{ + {UserID: "first"}, {UserID: "second"}, {UserID: "third"}, + }, + }, + []string{"first", "second", "third"}, + }, + { + "permissions for one user, first", + args{ + "none", + &IDPUserLinks{ + Links: []*IDPUserLink{ + {UserID: "first"}, {UserID: "second"}, {UserID: "third"}, + }, + }, + }, + want{ + links: []*IDPUserLink{ + {UserID: "first"}, + }, + }, + []string{"first"}, + }, + { + "permissions for one user, second", + args{ + "none", + &IDPUserLinks{ + Links: []*IDPUserLink{ + {UserID: "first"}, {UserID: "second"}, {UserID: "third"}, + }, + }, + }, + want{ + links: []*IDPUserLink{ + {UserID: "second"}, + }, + }, + []string{"second"}, + }, + { + "permissions for one user, third", + args{ + "none", + &IDPUserLinks{ + Links: []*IDPUserLink{ + {UserID: "first"}, {UserID: "second"}, {UserID: "third"}, + }, + }, + }, + want{ + links: []*IDPUserLink{ + {UserID: "third"}, + }, + }, + []string{"third"}, + }, + { + "permissions for two users, first", + args{ + "none", + &IDPUserLinks{ + Links: []*IDPUserLink{ + {UserID: "first"}, {UserID: "second"}, {UserID: "third"}, + }, + }, + }, + want{ + links: []*IDPUserLink{ + {UserID: "first"}, {UserID: "third"}, + }, + }, + []string{"first", "third"}, + }, + { + "permissions for two users, second", + args{ + "none", + &IDPUserLinks{ + Links: []*IDPUserLink{ + {UserID: "first"}, {UserID: "second"}, {UserID: "third"}, + }, + }, + }, + want{ + links: []*IDPUserLink{ + {UserID: "second"}, {UserID: "third"}, + }, + }, + []string{"second", "third"}, + }, + { + "no permissions", + args{ + "none", + &IDPUserLinks{ + Links: []*IDPUserLink{ + {UserID: "first"}, {UserID: "second"}, {UserID: "third"}, + }, + }, + }, + want{ + links: []*IDPUserLink{}, + }, + []string{}, + }, + { + "no permissions, self", + args{ + "second", + &IDPUserLinks{ + Links: []*IDPUserLink{ + {UserID: "first"}, {UserID: "second"}, {UserID: "third"}, + }, + }, + }, + want{ + links: []*IDPUserLink{{UserID: "second"}}, + }, + []string{}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + checkPermission := func(ctx context.Context, permission, orgID, resourceID string) (err error) { + for _, perm := range tt.permissions { + if resourceID == perm { + return nil + } + } + return errors.New("failed") + } + idpLinksCheckPermission(authz.SetCtxData(context.Background(), authz.CtxData{UserID: tt.args.user}), tt.args.links, checkPermission) + require.Equal(t, tt.want.links, tt.args.links.Links) + }) + } +} + var ( idpUserLinksQuery = regexp.QuoteMeta(`SELECT projections.idp_user_links3.idp_id,` + ` projections.idp_user_links3.user_id,` + diff --git a/internal/query/instance_features.go b/internal/query/instance_features.go index f10039fa66..40bea9d8bf 100644 --- a/internal/query/instance_features.go +++ b/internal/query/instance_features.go @@ -17,6 +17,7 @@ type InstanceFeatures struct { Actions FeatureSource[bool] ImprovedPerformance FeatureSource[[]feature.ImprovedPerformanceType] WebKey FeatureSource[bool] + DebugOIDCParentError FeatureSource[bool] } func (q *Queries) GetInstanceFeatures(ctx context.Context, cascade bool) (_ *InstanceFeatures, err error) { diff --git a/internal/query/instance_features_model.go b/internal/query/instance_features_model.go index a2ab09d263..134aafa67f 100644 --- a/internal/query/instance_features_model.go +++ b/internal/query/instance_features_model.go @@ -68,6 +68,7 @@ func (m *InstanceFeaturesReadModel) Query() *eventstore.SearchQueryBuilder { feature_v2.InstanceActionsEventType, feature_v2.InstanceImprovedPerformanceEventType, feature_v2.InstanceWebKeyEventType, + feature_v2.InstanceDebugOIDCParentErrorEventType, ). Builder().ResourceOwner(m.ResourceOwner) } @@ -118,6 +119,8 @@ func reduceInstanceFeatureSet[T any](features *InstanceFeatures, event *feature_ features.ImprovedPerformance.set(level, event.Value) case feature.KeyWebKey: features.WebKey.set(level, event.Value) + case feature.KeyDebugOIDCParentError: + features.DebugOIDCParentError.set(level, event.Value) } return nil } diff --git a/internal/query/org_test.go b/internal/query/org_test.go index b8c6073dbe..db41f9ffd1 100644 --- a/internal/query/org_test.go +++ b/internal/query/org_test.go @@ -439,11 +439,10 @@ func TestQueries_IsOrgUnique(t *testing.T) { t.Errorf("expectation was met: %v", err) } }) - } } -func TestOrg_RemoveNoPermission(t *testing.T) { +func TestOrg_orgsCheckPermission(t *testing.T) { type want struct { orgs []*Org } diff --git a/internal/query/projection/instance_features.go b/internal/query/projection/instance_features.go index d24fe6d203..c0aaa8c3a7 100644 --- a/internal/query/projection/instance_features.go +++ b/internal/query/projection/instance_features.go @@ -92,6 +92,10 @@ func (*instanceFeatureProjection) Reducers() []handler.AggregateReducer { Event: feature_v2.InstanceWebKeyEventType, Reduce: reduceInstanceSetFeature[bool], }, + { + Event: feature_v2.InstanceDebugOIDCParentErrorEventType, + Reduce: reduceInstanceSetFeature[bool], + }, { Event: instance.InstanceRemovedEventType, Reduce: reduceInstanceRemovedHelper(InstanceDomainInstanceIDCol), diff --git a/internal/query/projection/user_schema.go b/internal/query/projection/user_schema.go index 2961596e30..7140604034 100644 --- a/internal/query/projection/user_schema.go +++ b/internal/query/projection/user_schema.go @@ -12,9 +12,10 @@ import ( ) const ( - UserSchemaTable = "projections.user_schemas" + UserSchemaTable = "projections.user_schemas1" UserSchemaIDCol = "id" + UserSchemaCreationDateCol = "creation_date" UserSchemaChangeDateCol = "change_date" UserSchemaSequenceCol = "sequence" UserSchemaInstanceIDCol = "instance_id" @@ -39,6 +40,7 @@ func (*userSchemaProjection) Init() *old_handler.Check { return handler.NewTableCheck( handler.NewTable([]*handler.InitColumn{ handler.NewColumn(UserSchemaIDCol, handler.ColumnTypeText), + handler.NewColumn(UserSchemaCreationDateCol, handler.ColumnTypeTimestamp), handler.NewColumn(UserSchemaChangeDateCol, handler.ColumnTypeTimestamp), handler.NewColumn(UserSchemaSequenceCol, handler.ColumnTypeInt64), handler.NewColumn(UserSchemaStateCol, handler.ColumnTypeEnum), @@ -102,6 +104,7 @@ func (p *userSchemaProjection) reduceCreated(event eventstore.Event) (*handler.S event, []handler.Column{ handler.NewCol(UserSchemaIDCol, event.Aggregate().ID), + handler.NewCol(UserSchemaCreationDateCol, handler.OnlySetValueOnInsert(UserSchemaTable, e.CreationDate())), handler.NewCol(UserSchemaChangeDateCol, event.CreatedAt()), handler.NewCol(UserSchemaSequenceCol, event.Sequence()), handler.NewCol(UserSchemaInstanceIDCol, event.Aggregate().InstanceID), @@ -130,7 +133,10 @@ func (p *userSchemaProjection) reduceUpdated(event eventstore.Event) (*handler.S if len(e.Schema) > 0 { cols = append(cols, handler.NewCol(UserSchemaSchemaCol, e.Schema)) - cols = append(cols, handler.NewIncrementCol(UserSchemaRevisionCol, 1)) + } + + if e.SchemaRevision != nil { + cols = append(cols, handler.NewCol(UserSchemaRevisionCol, *e.SchemaRevision)) } if len(e.PossibleAuthenticators) > 0 { diff --git a/internal/query/projection/user_schema_test.go b/internal/query/projection/user_schema_test.go index ac7b0704c5..4fecc89958 100644 --- a/internal/query/projection/user_schema_test.go +++ b/internal/query/projection/user_schema_test.go @@ -39,10 +39,11 @@ func TestUserSchemaProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "INSERT INTO projections.user_schemas (id, change_date, sequence, instance_id, state, type, revision, schema, possible_authenticators) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", + expectedStmt: "INSERT INTO projections.user_schemas1 (id, creation_date, change_date, sequence, instance_id, state, type, revision, schema, possible_authenticators) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)", expectedArgs: []interface{}{ "agg-id", anyArg{}, + anyArg{}, uint64(15), "instance-id", domain.UserSchemaStateActive, @@ -61,9 +62,9 @@ func TestUserSchemaProjection_reduces(t *testing.T) { args: args{ event: getEvent( testEvent( - schema.CreatedType, + schema.UpdatedType, schema.AggregateType, - []byte(`{"schemaType": "type", "schema": {"$schema":"urn:zitadel:schema:v1","properties":{"name":{"type":"string","urn:zitadel:schema:permission":{"self":"rw"}}},"type":"object"}, "possibleAuthenticators": [1,2]}`), + []byte(`{"schemaType": "type", "schemaRevision": 2, "schema": {"$schema":"urn:zitadel:schema:v1","properties":{"name":{"type":"string","urn:zitadel:schema:permission":{"self":"rw"}}},"type":"object"}, "possibleAuthenticators": [1,2]}`), ), eventstore.GenericEventMapper[schema.UpdatedEvent]), }, reduce: (&userSchemaProjection{}).reduceUpdated, @@ -73,13 +74,13 @@ func TestUserSchemaProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.user_schemas SET (change_date, sequence, type, schema, revision, possible_authenticators) = ($1, $2, $3, $4, revision + $5, $6) WHERE (id = $7) AND (instance_id = $8)", + expectedStmt: "UPDATE projections.user_schemas1 SET (change_date, sequence, type, schema, revision, possible_authenticators) = ($1, $2, $3, $4, $5, $6) WHERE (id = $7) AND (instance_id = $8)", expectedArgs: []interface{}{ anyArg{}, uint64(15), "type", json.RawMessage(`{"$schema":"urn:zitadel:schema:v1","properties":{"name":{"type":"string","urn:zitadel:schema:permission":{"self":"rw"}}},"type":"object"}`), - 1, + uint64(2), []domain.AuthenticatorType{domain.AuthenticatorTypeUsername, domain.AuthenticatorTypePassword}, "agg-id", "instance-id", @@ -106,7 +107,7 @@ func TestUserSchemaProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.user_schemas SET (change_date, sequence, state) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", + expectedStmt: "UPDATE projections.user_schemas1 SET (change_date, sequence, state) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -136,7 +137,7 @@ func TestUserSchemaProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.user_schemas SET (change_date, sequence, state) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", + expectedStmt: "UPDATE projections.user_schemas1 SET (change_date, sequence, state) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -166,7 +167,7 @@ func TestUserSchemaProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "DELETE FROM projections.user_schemas WHERE (id = $1) AND (instance_id = $2)", + expectedStmt: "DELETE FROM projections.user_schemas1 WHERE (id = $1) AND (instance_id = $2)", expectedArgs: []interface{}{ "agg-id", "instance-id", @@ -193,7 +194,7 @@ func TestUserSchemaProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "DELETE FROM projections.user_schemas WHERE (instance_id = $1)", + expectedStmt: "DELETE FROM projections.user_schemas1 WHERE (instance_id = $1)", expectedArgs: []interface{}{ "agg-id", }, diff --git a/internal/query/user.go b/internal/query/user.go index 497cd89d6d..415e50aae5 100644 --- a/internal/query/user.go +++ b/internal/query/user.go @@ -125,15 +125,9 @@ type NotifyUser struct { } func usersCheckPermission(ctx context.Context, users *Users, permissionCheck domain.PermissionCheck) { - ctxData := authz.GetCtxData(ctx) users.Users = slices.DeleteFunc(users.Users, func(user *User) bool { - if ctxData.UserID != user.ID { - if err := permissionCheck(ctx, domain.PermissionUserRead, user.ResourceOwner, user.ID); err != nil { - return true - } - } - return false + return userCheckPermission(ctx, user.ResourceOwner, user.ID, permissionCheck) != nil }, ) } @@ -347,6 +341,27 @@ var ( //go:embed user_by_id.sql var userByIDQuery string +func userCheckPermission(ctx context.Context, resourceOwner string, userID string, permissionCheck domain.PermissionCheck) error { + ctxData := authz.GetCtxData(ctx) + if ctxData.UserID != userID { + if err := permissionCheck(ctx, domain.PermissionUserRead, resourceOwner, userID); err != nil { + return err + } + } + return nil +} + +func (q *Queries) GetUserByIDWithPermission(ctx context.Context, shouldTriggerBulk bool, userID string, permissionCheck domain.PermissionCheck) (*User, error) { + user, err := q.GetUserByID(ctx, shouldTriggerBulk, userID) + if err != nil { + return nil, err + } + if err := userCheckPermission(ctx, user.ResourceOwner, user.ID, permissionCheck); err != nil { + return nil, err + } + return user, nil +} + func (q *Queries) GetUserByID(ctx context.Context, shouldTriggerBulk bool, userID string) (user *User, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() diff --git a/internal/query/user_auth_method.go b/internal/query/user_auth_method.go index 6a93f069b0..ce919a9128 100644 --- a/internal/query/user_auth_method.go +++ b/internal/query/user_auth_method.go @@ -4,6 +4,7 @@ import ( "context" "database/sql" "errors" + "slices" "time" sq "github.com/Masterminds/squirrel" @@ -98,27 +99,12 @@ type AuthMethods struct { AuthMethods []*AuthMethod } -func (l *AuthMethods) RemoveNoPermission(ctx context.Context, permissionCheck domain.PermissionCheck) { - removableIndexes := make([]int, 0) - for i := range l.AuthMethods { - ctxData := authz.GetCtxData(ctx) - if ctxData.UserID != l.AuthMethods[i].UserID { - if err := permissionCheck(ctx, domain.PermissionUserRead, l.AuthMethods[i].ResourceOwner, l.AuthMethods[i].UserID); err != nil { - removableIndexes = append(removableIndexes, i) - } - } - } - removed := 0 - for _, removeIndex := range removableIndexes { - l.AuthMethods = removeAuthMethod(l.AuthMethods, removeIndex-removed) - removed++ - } - // reset count as some users could be removed - l.SearchResponse.Count = uint64(len(l.AuthMethods)) -} - -func removeAuthMethod(slice []*AuthMethod, s int) []*AuthMethod { - return append(slice[:s], slice[s+1:]...) +func authMethodsCheckPermission(ctx context.Context, methods *AuthMethods, permissionCheck domain.PermissionCheck) { + methods.AuthMethods = slices.DeleteFunc(methods.AuthMethods, + func(method *AuthMethod) bool { + return userCheckPermission(ctx, method.ResourceOwner, method.UserID, permissionCheck) != nil + }, + ) } type AuthMethod struct { @@ -144,7 +130,34 @@ type UserAuthMethodSearchQueries struct { Queries []SearchQuery } -func (q *Queries) SearchUserAuthMethods(ctx context.Context, queries *UserAuthMethodSearchQueries, withOwnerRemoved bool) (userAuthMethods *AuthMethods, err error) { +func (q *UserAuthMethodSearchQueries) hasUserID() bool { + for _, query := range q.Queries { + if query.Col() == UserAuthMethodColumnUserID { + return true + } + } + return false +} + +func (q *Queries) SearchUserAuthMethods(ctx context.Context, queries *UserAuthMethodSearchQueries, permissionCheck domain.PermissionCheck) (userAuthMethods *AuthMethods, err error) { + methods, err := q.searchUserAuthMethods(ctx, queries, false) + if err != nil { + return nil, err + } + if permissionCheck != nil && len(methods.AuthMethods) > 0 { + // when userID for query is provided, only one check has to be done + if queries.hasUserID() { + if err := userCheckPermission(ctx, methods.AuthMethods[0].ResourceOwner, methods.AuthMethods[0].UserID, permissionCheck); err != nil { + return nil, err + } + } else { + authMethodsCheckPermission(ctx, methods, permissionCheck) + } + } + return methods, nil +} + +func (q *Queries) searchUserAuthMethods(ctx context.Context, queries *UserAuthMethodSearchQueries, withOwnerRemoved bool) (userAuthMethods *AuthMethods, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() diff --git a/internal/query/user_auth_method_test.go b/internal/query/user_auth_method_test.go index 95f9a0229c..7221129b0b 100644 --- a/internal/query/user_auth_method_test.go +++ b/internal/query/user_auth_method_test.go @@ -10,11 +10,176 @@ import ( "testing" sq "github.com/Masterminds/squirrel" + "github.com/stretchr/testify/require" + "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/zerrors" ) +func TestUser_authMethodsCheckPermission(t *testing.T) { + type want struct { + methods []*AuthMethod + } + type args struct { + user string + methods *AuthMethods + } + tests := []struct { + name string + args args + want want + permissions []string + }{ + { + "permissions for all users", + args{ + "none", + &AuthMethods{ + AuthMethods: []*AuthMethod{ + {UserID: "first"}, {UserID: "second"}, {UserID: "third"}, + }, + }, + }, + want{ + methods: []*AuthMethod{ + {UserID: "first"}, {UserID: "second"}, {UserID: "third"}, + }, + }, + []string{"first", "second", "third"}, + }, + { + "permissions for one user, first", + args{ + "none", + &AuthMethods{ + AuthMethods: []*AuthMethod{ + {UserID: "first"}, {UserID: "second"}, {UserID: "third"}, + }, + }, + }, + want{ + methods: []*AuthMethod{ + {UserID: "first"}, + }, + }, + []string{"first"}, + }, + { + "permissions for one user, second", + args{ + "none", + &AuthMethods{ + AuthMethods: []*AuthMethod{ + {UserID: "first"}, {UserID: "second"}, {UserID: "third"}, + }, + }, + }, + want{ + methods: []*AuthMethod{ + {UserID: "second"}, + }, + }, + []string{"second"}, + }, + { + "permissions for one user, third", + args{ + "none", + &AuthMethods{ + AuthMethods: []*AuthMethod{ + {UserID: "first"}, {UserID: "second"}, {UserID: "third"}, + }, + }, + }, + want{ + methods: []*AuthMethod{ + {UserID: "third"}, + }, + }, + []string{"third"}, + }, + { + "permissions for two users, first", + args{ + "none", + &AuthMethods{ + AuthMethods: []*AuthMethod{ + {UserID: "first"}, {UserID: "second"}, {UserID: "third"}, + }, + }, + }, + want{ + methods: []*AuthMethod{ + {UserID: "first"}, {UserID: "third"}, + }, + }, + []string{"first", "third"}, + }, + { + "permissions for two users, second", + args{ + "none", + &AuthMethods{ + AuthMethods: []*AuthMethod{ + {UserID: "first"}, {UserID: "second"}, {UserID: "third"}, + }, + }, + }, + want{ + methods: []*AuthMethod{ + {UserID: "second"}, {UserID: "third"}, + }, + }, + []string{"second", "third"}, + }, + { + "no permissions", + args{ + "none", + &AuthMethods{ + AuthMethods: []*AuthMethod{ + {UserID: "first"}, {UserID: "second"}, {UserID: "third"}, + }, + }, + }, + want{ + methods: []*AuthMethod{}, + }, + []string{}, + }, + { + "no permissions, self", + args{ + "second", + &AuthMethods{ + AuthMethods: []*AuthMethod{ + {UserID: "first"}, {UserID: "second"}, {UserID: "third"}, + }, + }, + }, + want{ + methods: []*AuthMethod{{UserID: "second"}}, + }, + []string{}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + checkPermission := func(ctx context.Context, permission, orgID, resourceID string) (err error) { + for _, perm := range tt.permissions { + if resourceID == perm { + return nil + } + } + return errors.New("failed") + } + authMethodsCheckPermission(authz.SetCtxData(context.Background(), authz.CtxData{UserID: tt.args.user}), tt.args.methods, checkPermission) + require.Equal(t, tt.want.methods, tt.args.methods.AuthMethods) + }) + } +} + var ( prepareUserAuthMethodsStmt = `SELECT projections.user_auth_methods4.token_id,` + ` projections.user_auth_methods4.creation_date,` + diff --git a/internal/query/user_schema.go b/internal/query/user_schema.go index ac5dbb1d16..ff5117d264 100644 --- a/internal/query/user_schema.go +++ b/internal/query/user_schema.go @@ -26,7 +26,6 @@ func (e *UserSchemas) SetState(s *State) { } type UserSchema struct { - ID string domain.ObjectDetails State domain.UserSchemaState Type string @@ -49,6 +48,10 @@ var ( name: projection.UserSchemaIDCol, table: userSchemaTable, } + UserSchemaCreationDateCol = Column{ + name: projection.UserSchemaCreationDateCol, + table: userSchemaTable, + } UserSchemaChangeDateCol = Column{ name: projection.UserSchemaChangeDateCol, table: userSchemaTable, @@ -131,6 +134,7 @@ func NewUserSchemaStateSearchQuery(value domain.UserSchemaState) (SearchQuery, e func prepareUserSchemaQuery() (sq.SelectBuilder, func(*sql.Row) (*UserSchema, error)) { return sq.Select( UserSchemaIDCol.identifier(), + UserSchemaCreationDateCol.identifier(), UserSchemaChangeDateCol.identifier(), UserSchemaSequenceCol.identifier(), UserSchemaInstanceIDCol.identifier(), @@ -147,6 +151,7 @@ func prepareUserSchemaQuery() (sq.SelectBuilder, func(*sql.Row) (*UserSchema, er var schema database.ByteArray[byte] err := row.Scan( &u.ID, + &u.CreationDate, &u.EventDate, &u.Sequence, &u.ResourceOwner, @@ -173,6 +178,7 @@ func prepareUserSchemaQuery() (sq.SelectBuilder, func(*sql.Row) (*UserSchema, er func prepareUserSchemasQuery() (sq.SelectBuilder, func(*sql.Rows) (*UserSchemas, error)) { return sq.Select( UserSchemaIDCol.identifier(), + UserSchemaCreationDateCol.identifier(), UserSchemaChangeDateCol.identifier(), UserSchemaSequenceCol.identifier(), UserSchemaInstanceIDCol.identifier(), @@ -195,6 +201,7 @@ func prepareUserSchemasQuery() (sq.SelectBuilder, func(*sql.Rows) (*UserSchemas, u := new(UserSchema) err := rows.Scan( &u.ID, + &u.CreationDate, &u.EventDate, &u.Sequence, &u.ResourceOwner, diff --git a/internal/query/user_schema_test.go b/internal/query/user_schema_test.go index b9cd81b990..19526373a9 100644 --- a/internal/query/user_schema_test.go +++ b/internal/query/user_schema_test.go @@ -15,19 +15,21 @@ import ( ) var ( - prepareUserSchemasStmt = `SELECT projections.user_schemas.id,` + - ` projections.user_schemas.change_date,` + - ` projections.user_schemas.sequence,` + - ` projections.user_schemas.instance_id,` + - ` projections.user_schemas.state,` + - ` projections.user_schemas.type,` + - ` projections.user_schemas.revision,` + - ` projections.user_schemas.schema,` + - ` projections.user_schemas.possible_authenticators,` + + prepareUserSchemasStmt = `SELECT projections.user_schemas1.id,` + + ` projections.user_schemas1.creation_date,` + + ` projections.user_schemas1.change_date,` + + ` projections.user_schemas1.sequence,` + + ` projections.user_schemas1.instance_id,` + + ` projections.user_schemas1.state,` + + ` projections.user_schemas1.type,` + + ` projections.user_schemas1.revision,` + + ` projections.user_schemas1.schema,` + + ` projections.user_schemas1.possible_authenticators,` + ` COUNT(*) OVER ()` + ` FROM projections.user_schemas` prepareUserSchemasCols = []string{ "id", + "creation_date", "change_date", "sequence", "instance_id", @@ -39,18 +41,20 @@ var ( "count", } - prepareUserSchemaStmt = `SELECT projections.user_schemas.id,` + - ` projections.user_schemas.change_date,` + - ` projections.user_schemas.sequence,` + - ` projections.user_schemas.instance_id,` + - ` projections.user_schemas.state,` + - ` projections.user_schemas.type,` + - ` projections.user_schemas.revision,` + - ` projections.user_schemas.schema,` + - ` projections.user_schemas.possible_authenticators` + + prepareUserSchemaStmt = `SELECT projections.user_schemas1.id,` + + ` projections.user_schemas1.creation_date,` + + ` projections.user_schemas1.change_date,` + + ` projections.user_schemas1.sequence,` + + ` projections.user_schemas1.instance_id,` + + ` projections.user_schemas1.state,` + + ` projections.user_schemas1.type,` + + ` projections.user_schemas1.revision,` + + ` projections.user_schemas1.schema,` + + ` projections.user_schemas1.possible_authenticators` + ` FROM projections.user_schemas` prepareUserSchemaCols = []string{ "id", + "creation_date", "change_date", "sequence", "instance_id", @@ -96,6 +100,7 @@ func Test_UserSchemaPrepares(t *testing.T) { { "id", testNow, + testNow, uint64(20211109), "instance-id", domain.UserSchemaStateActive, @@ -113,9 +118,10 @@ func Test_UserSchemaPrepares(t *testing.T) { }, UserSchemas: []*UserSchema{ { - ID: "id", ObjectDetails: domain.ObjectDetails{ + ID: "id", EventDate: testNow, + CreationDate: testNow, Sequence: 20211109, ResourceOwner: "instance-id", }, @@ -139,6 +145,7 @@ func Test_UserSchemaPrepares(t *testing.T) { { "id-1", testNow, + testNow, uint64(20211109), "instance-id", domain.UserSchemaStateActive, @@ -150,6 +157,7 @@ func Test_UserSchemaPrepares(t *testing.T) { { "id-2", testNow, + testNow, uint64(20211110), "instance-id", domain.UserSchemaStateInactive, @@ -167,9 +175,10 @@ func Test_UserSchemaPrepares(t *testing.T) { }, UserSchemas: []*UserSchema{ { - ID: "id-1", ObjectDetails: domain.ObjectDetails{ + ID: "id-1", EventDate: testNow, + CreationDate: testNow, Sequence: 20211109, ResourceOwner: "instance-id", }, @@ -180,9 +189,10 @@ func Test_UserSchemaPrepares(t *testing.T) { PossibleAuthenticators: database.NumberArray[domain.AuthenticatorType]{domain.AuthenticatorTypeUsername, domain.AuthenticatorTypePassword}, }, { - ID: "id-2", ObjectDetails: domain.ObjectDetails{ + ID: "id-2", EventDate: testNow, + CreationDate: testNow, Sequence: 20211110, ResourceOwner: "instance-id", }, @@ -240,6 +250,7 @@ func Test_UserSchemaPrepares(t *testing.T) { []driver.Value{ "id", testNow, + testNow, uint64(20211109), "instance-id", domain.UserSchemaStateActive, @@ -251,9 +262,10 @@ func Test_UserSchemaPrepares(t *testing.T) { ), }, object: &UserSchema{ - ID: "id", ObjectDetails: domain.ObjectDetails{ + ID: "id", EventDate: testNow, + CreationDate: testNow, Sequence: 20211109, ResourceOwner: "instance-id", }, diff --git a/internal/query/user_test.go b/internal/query/user_test.go index 0a6b0c36e7..89556d41e8 100644 --- a/internal/query/user_test.go +++ b/internal/query/user_test.go @@ -9,15 +9,17 @@ import ( "regexp" "testing" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.org/x/text/language" + "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/zerrors" ) -func TestUser_RemoveNoPermission(t *testing.T) { +func TestUser_usersCheckPermission(t *testing.T) { type want struct { users []*User } @@ -140,6 +142,85 @@ func TestUser_RemoveNoPermission(t *testing.T) { } } +func TestUser_userCheckPermission(t *testing.T) { + type args struct { + ctxData string + resourceowner string + user string + } + type perm struct { + resourceowner string + user string + } + tests := []struct { + name string + wantErr bool + args args + permissions []perm + }{ + { + name: "permission, self", + args: args{ + resourceowner: "org", + user: "user", + ctxData: "user", + }, + permissions: []perm{}, + }, + { + name: "permission, user", + args: args{ + resourceowner: "org1", + user: "user1", + ctxData: "user2", + }, + permissions: []perm{{"org1", "user1"}}, + wantErr: false, + }, + { + name: "permission, org", + args: args{ + resourceowner: "org1", + user: "user1", + ctxData: "user2", + }, + permissions: []perm{{"org1", "user3"}}, + }, + { + name: "permission, none", + args: args{ + resourceowner: "org1", + user: "user1", + ctxData: "user2", + }, + permissions: []perm{}, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + checkPermission := func(ctx context.Context, permission, orgID, resourceID string) (err error) { + for _, perm := range tt.permissions { + if resourceID == perm.user { + return nil + } + if orgID == perm.resourceowner { + return nil + } + } + return errors.New("failed") + } + + granted := userCheckPermission(authz.SetCtxData(context.Background(), authz.CtxData{UserID: tt.args.ctxData}), tt.args.resourceowner, tt.args.user, checkPermission) + if tt.wantErr { + assert.Error(t, granted) + } else { + assert.NoError(t, granted) + } + }) + } +} + var ( loginNamesQuery = `SELECT login_names.user_id, ARRAY_AGG(login_names.login_name)::TEXT[] AS loginnames, ARRAY_AGG(LOWER(login_names.login_name))::TEXT[] AS loginnames_lower, login_names.instance_id` + ` FROM projections.login_names3 AS login_names` + diff --git a/internal/repository/feature/feature_v2/eventstore.go b/internal/repository/feature/feature_v2/eventstore.go index 97b4e4ed3a..eebed8a3b8 100644 --- a/internal/repository/feature/feature_v2/eventstore.go +++ b/internal/repository/feature/feature_v2/eventstore.go @@ -24,4 +24,5 @@ func init() { eventstore.RegisterFilterEventMapper(AggregateType, InstanceActionsEventType, eventstore.GenericEventMapper[SetEvent[bool]]) eventstore.RegisterFilterEventMapper(AggregateType, InstanceImprovedPerformanceEventType, eventstore.GenericEventMapper[SetEvent[[]feature.ImprovedPerformanceType]]) eventstore.RegisterFilterEventMapper(AggregateType, InstanceWebKeyEventType, eventstore.GenericEventMapper[SetEvent[bool]]) + eventstore.RegisterFilterEventMapper(AggregateType, InstanceDebugOIDCParentErrorEventType, eventstore.GenericEventMapper[SetEvent[bool]]) } diff --git a/internal/repository/feature/feature_v2/feature.go b/internal/repository/feature/feature_v2/feature.go index 27e1ed40fc..4c056c235f 100644 --- a/internal/repository/feature/feature_v2/feature.go +++ b/internal/repository/feature/feature_v2/feature.go @@ -29,6 +29,7 @@ var ( InstanceActionsEventType = setEventTypeFromFeature(feature.LevelInstance, feature.KeyActions) InstanceImprovedPerformanceEventType = setEventTypeFromFeature(feature.LevelInstance, feature.KeyImprovedPerformance) InstanceWebKeyEventType = setEventTypeFromFeature(feature.LevelInstance, feature.KeyWebKey) + InstanceDebugOIDCParentErrorEventType = setEventTypeFromFeature(feature.LevelInstance, feature.KeyDebugOIDCParentError) ) const ( diff --git a/internal/repository/project/api_config.go b/internal/repository/project/api_config.go index eac410847b..6316bce36f 100644 --- a/internal/repository/project/api_config.go +++ b/internal/repository/project/api_config.go @@ -10,12 +10,10 @@ import ( ) const ( - APIConfigAddedType = applicationEventTypePrefix + "config.api.added" - APIConfigChangedType = applicationEventTypePrefix + "config.api.changed" - APIConfigSecretChangedType = applicationEventTypePrefix + "config.api.secret.changed" - APIClientSecretCheckSucceededType = applicationEventTypePrefix + "api.secret.check.succeeded" - APIClientSecretCheckFailedType = applicationEventTypePrefix + "api.secret.check.failed" - APIConfigSecretHashUpdatedType = applicationEventTypePrefix + "config.api.secret.updated" + APIConfigAddedType = applicationEventTypePrefix + "config.api.added" + APIConfigChangedType = applicationEventTypePrefix + "config.api.changed" + APIConfigSecretChangedType = applicationEventTypePrefix + "config.api.secret.changed" + APIConfigSecretHashUpdatedType = applicationEventTypePrefix + "config.api.secret.updated" ) type APIConfigAddedEvent struct { @@ -202,90 +200,6 @@ func APIConfigSecretChangedEventMapper(event eventstore.Event) (eventstore.Event return e, nil } -type APIConfigSecretCheckSucceededEvent struct { - eventstore.BaseEvent `json:"-"` - - AppID string `json:"appId"` -} - -func (e *APIConfigSecretCheckSucceededEvent) Payload() interface{} { - return e -} - -func (e *APIConfigSecretCheckSucceededEvent) UniqueConstraints() []*eventstore.UniqueConstraint { - return nil -} - -func NewAPIConfigSecretCheckSucceededEvent( - ctx context.Context, - aggregate *eventstore.Aggregate, - appID string, -) *APIConfigSecretCheckSucceededEvent { - return &APIConfigSecretCheckSucceededEvent{ - BaseEvent: *eventstore.NewBaseEventForPush( - ctx, - aggregate, - APIClientSecretCheckSucceededType, - ), - AppID: appID, - } -} - -func APIConfigSecretCheckSucceededEventMapper(event eventstore.Event) (eventstore.Event, error) { - e := &APIConfigSecretCheckSucceededEvent{ - BaseEvent: *eventstore.BaseEventFromRepo(event), - } - - err := event.Unmarshal(e) - if err != nil { - return nil, zerrors.ThrowInternal(err, "API-837gV", "unable to unmarshal api config") - } - - return e, nil -} - -type APIConfigSecretCheckFailedEvent struct { - eventstore.BaseEvent `json:"-"` - - AppID string `json:"appId"` -} - -func (e *APIConfigSecretCheckFailedEvent) Payload() interface{} { - return e -} - -func (e *APIConfigSecretCheckFailedEvent) UniqueConstraints() []*eventstore.UniqueConstraint { - return nil -} - -func NewAPIConfigSecretCheckFailedEvent( - ctx context.Context, - aggregate *eventstore.Aggregate, - appID string, -) *APIConfigSecretCheckFailedEvent { - return &APIConfigSecretCheckFailedEvent{ - BaseEvent: *eventstore.NewBaseEventForPush( - ctx, - aggregate, - APIClientSecretCheckFailedType, - ), - AppID: appID, - } -} - -func APIConfigSecretCheckFailedEventMapper(event eventstore.Event) (eventstore.Event, error) { - e := &APIConfigSecretCheckFailedEvent{ - BaseEvent: *eventstore.BaseEventFromRepo(event), - } - - err := event.Unmarshal(e) - if err != nil { - return nil, zerrors.ThrowInternal(err, "API-987g%", "unable to unmarshal api config") - } - - return e, nil -} - type APIConfigSecretHashUpdatedEvent struct { *eventstore.BaseEvent `json:"-"` diff --git a/internal/repository/project/eventstore.go b/internal/repository/project/eventstore.go index fe8e14a508..5705649739 100644 --- a/internal/repository/project/eventstore.go +++ b/internal/repository/project/eventstore.go @@ -35,8 +35,6 @@ func init() { eventstore.RegisterFilterEventMapper(AggregateType, OIDCConfigAddedType, OIDCConfigAddedEventMapper) eventstore.RegisterFilterEventMapper(AggregateType, OIDCConfigChangedType, OIDCConfigChangedEventMapper) eventstore.RegisterFilterEventMapper(AggregateType, OIDCConfigSecretChangedType, OIDCConfigSecretChangedEventMapper) - eventstore.RegisterFilterEventMapper(AggregateType, OIDCClientSecretCheckSucceededType, OIDCConfigSecretCheckSucceededEventMapper) - eventstore.RegisterFilterEventMapper(AggregateType, OIDCClientSecretCheckFailedType, OIDCConfigSecretCheckFailedEventMapper) eventstore.RegisterFilterEventMapper(AggregateType, OIDCConfigSecretHashUpdatedType, eventstore.GenericEventMapper[OIDCConfigSecretHashUpdatedEvent]) eventstore.RegisterFilterEventMapper(AggregateType, APIConfigAddedType, APIConfigAddedEventMapper) eventstore.RegisterFilterEventMapper(AggregateType, APIConfigChangedType, APIConfigChangedEventMapper) diff --git a/internal/repository/project/oidc_config.go b/internal/repository/project/oidc_config.go index 68f2a59949..5ea20c220a 100644 --- a/internal/repository/project/oidc_config.go +++ b/internal/repository/project/oidc_config.go @@ -11,12 +11,10 @@ import ( ) const ( - OIDCConfigAddedType = applicationEventTypePrefix + "config.oidc.added" - OIDCConfigChangedType = applicationEventTypePrefix + "config.oidc.changed" - OIDCConfigSecretChangedType = applicationEventTypePrefix + "config.oidc.secret.changed" - OIDCClientSecretCheckSucceededType = applicationEventTypePrefix + "oidc.secret.check.succeeded" - OIDCClientSecretCheckFailedType = applicationEventTypePrefix + "oidc.secret.check.failed" - OIDCConfigSecretHashUpdatedType = applicationEventTypePrefix + "config.oidc.secret.updated" + OIDCConfigAddedType = applicationEventTypePrefix + "config.oidc.added" + OIDCConfigChangedType = applicationEventTypePrefix + "config.oidc.changed" + OIDCConfigSecretChangedType = applicationEventTypePrefix + "config.oidc.secret.changed" + OIDCConfigSecretHashUpdatedType = applicationEventTypePrefix + "config.oidc.secret.updated" ) type OIDCConfigAddedEvent struct { @@ -409,90 +407,6 @@ func OIDCConfigSecretChangedEventMapper(event eventstore.Event) (eventstore.Even return e, nil } -type OIDCConfigSecretCheckSucceededEvent struct { - eventstore.BaseEvent `json:"-"` - - AppID string `json:"appId"` -} - -func (e *OIDCConfigSecretCheckSucceededEvent) Payload() interface{} { - return e -} - -func (e *OIDCConfigSecretCheckSucceededEvent) UniqueConstraints() []*eventstore.UniqueConstraint { - return nil -} - -func NewOIDCConfigSecretCheckSucceededEvent( - ctx context.Context, - aggregate *eventstore.Aggregate, - appID string, -) *OIDCConfigSecretCheckSucceededEvent { - return &OIDCConfigSecretCheckSucceededEvent{ - BaseEvent: *eventstore.NewBaseEventForPush( - ctx, - aggregate, - OIDCClientSecretCheckSucceededType, - ), - AppID: appID, - } -} - -func OIDCConfigSecretCheckSucceededEventMapper(event eventstore.Event) (eventstore.Event, error) { - e := &OIDCConfigSecretCheckSucceededEvent{ - BaseEvent: *eventstore.BaseEventFromRepo(event), - } - - err := event.Unmarshal(e) - if err != nil { - return nil, zerrors.ThrowInternal(err, "OIDC-837gV", "unable to unmarshal oidc config") - } - - return e, nil -} - -type OIDCConfigSecretCheckFailedEvent struct { - eventstore.BaseEvent `json:"-"` - - AppID string `json:"appId"` -} - -func (e *OIDCConfigSecretCheckFailedEvent) Payload() interface{} { - return e -} - -func (e *OIDCConfigSecretCheckFailedEvent) UniqueConstraints() []*eventstore.UniqueConstraint { - return nil -} - -func NewOIDCConfigSecretCheckFailedEvent( - ctx context.Context, - aggregate *eventstore.Aggregate, - appID string, -) *OIDCConfigSecretCheckFailedEvent { - return &OIDCConfigSecretCheckFailedEvent{ - BaseEvent: *eventstore.NewBaseEventForPush( - ctx, - aggregate, - OIDCClientSecretCheckFailedType, - ), - AppID: appID, - } -} - -func OIDCConfigSecretCheckFailedEventMapper(event eventstore.Event) (eventstore.Event, error) { - e := &OIDCConfigSecretCheckFailedEvent{ - BaseEvent: *eventstore.BaseEventFromRepo(event), - } - - err := event.Unmarshal(e) - if err != nil { - return nil, zerrors.ThrowInternal(err, "OIDC-987g%", "unable to unmarshal oidc config") - } - - return e, nil -} - type OIDCConfigSecretHashUpdatedEvent struct { *eventstore.BaseEvent `json:"-"` diff --git a/internal/repository/user/schema/schema.go b/internal/repository/user/schema/schema.go index b626ee4c7d..729b1aed4e 100644 --- a/internal/repository/user/schema/schema.go +++ b/internal/repository/user/schema/schema.go @@ -4,6 +4,8 @@ import ( "context" "encoding/json" + "github.com/muhlemmer/gu" + "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" ) @@ -79,7 +81,9 @@ type UpdatedEvent struct { SchemaType *string `json:"schemaType,omitempty"` Schema json.RawMessage `json:"schema,omitempty"` PossibleAuthenticators []domain.AuthenticatorType `json:"possibleAuthenticators,omitempty"` + SchemaRevision *uint64 `json:"schemaRevision,omitempty"` oldSchemaType string + oldRevision uint64 } func (e *UpdatedEvent) SetBaseEvent(event *eventstore.BaseEvent) { @@ -139,6 +143,13 @@ func ChangePossibleAuthenticators(possibleAuthenticators []domain.AuthenticatorT } } +func IncreaseRevision(oldRevision uint64) func(event *UpdatedEvent) { + return func(e *UpdatedEvent) { + e.SchemaRevision = gu.Ptr(oldRevision + 1) + e.oldRevision = oldRevision + } +} + type DeactivatedEvent struct { *eventstore.BaseEvent `json:"-"` } diff --git a/internal/repository/user/schemauser/aggregate.go b/internal/repository/user/schemauser/aggregate.go new file mode 100644 index 0000000000..1c9901c08c --- /dev/null +++ b/internal/repository/user/schemauser/aggregate.go @@ -0,0 +1,25 @@ +package schemauser + +import ( + "github.com/zitadel/zitadel/internal/eventstore" +) + +const ( + AggregateType = "user" + AggregateVersion = "v3" +) + +type Aggregate struct { + eventstore.Aggregate +} + +func NewAggregate(id, resourceOwner string) *Aggregate { + return &Aggregate{ + Aggregate: eventstore.Aggregate{ + Type: AggregateType, + Version: AggregateVersion, + ID: id, + ResourceOwner: resourceOwner, + }, + } +} diff --git a/internal/repository/user/schemauser/email.go b/internal/repository/user/schemauser/email.go new file mode 100644 index 0000000000..07ae1bdf71 --- /dev/null +++ b/internal/repository/user/schemauser/email.go @@ -0,0 +1,202 @@ +package schemauser + +import ( + "context" + "time" + + "github.com/zitadel/zitadel/internal/api/http" + "github.com/zitadel/zitadel/internal/crypto" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/zerrors" +) + +const ( + emailEventPrefix = eventPrefix + "email." + EmailUpdatedType = emailEventPrefix + "updated" + EmailVerifiedType = emailEventPrefix + "verified" + EmailVerificationFailedType = emailEventPrefix + "verification.failed" + EmailCodeAddedType = emailEventPrefix + "code.added" + EmailCodeSentType = emailEventPrefix + "code.sent" +) + +type EmailUpdatedEvent struct { + eventstore.BaseEvent `json:"-"` + + EmailAddress domain.EmailAddress `json:"email,omitempty"` +} + +func (e *EmailUpdatedEvent) Payload() interface{} { + return e +} + +func (e *EmailUpdatedEvent) UniqueConstraints() []*eventstore.UniqueConstraint { + return nil +} + +func NewEmailUpdatedEvent(ctx context.Context, aggregate *eventstore.Aggregate, emailAddress domain.EmailAddress) *EmailUpdatedEvent { + return &EmailUpdatedEvent{ + BaseEvent: *eventstore.NewBaseEventForPush( + ctx, + aggregate, + EmailUpdatedType, + ), + EmailAddress: emailAddress, + } +} + +func EmailUpdatedEventMapper(event eventstore.Event) (eventstore.Event, error) { + emailChangedEvent := &EmailUpdatedEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(event), + } + err := event.Unmarshal(emailChangedEvent) + if err != nil { + return nil, zerrors.ThrowInternal(err, "USER-4M0sd", "unable to unmarshal human password changed") + } + + return emailChangedEvent, nil +} + +type EmailVerifiedEvent struct { + eventstore.BaseEvent `json:"-"` + + IsEmailVerified bool `json:"-"` +} + +func (e *EmailVerifiedEvent) Payload() interface{} { + return nil +} + +func (e *EmailVerifiedEvent) UniqueConstraints() []*eventstore.UniqueConstraint { + return nil +} + +func NewEmailVerifiedEvent(ctx context.Context, aggregate *eventstore.Aggregate) *EmailVerifiedEvent { + return &EmailVerifiedEvent{ + BaseEvent: *eventstore.NewBaseEventForPush( + ctx, + aggregate, + EmailVerifiedType, + ), + } +} + +func HumanVerifiedEventMapper(event eventstore.Event) (eventstore.Event, error) { + emailVerified := &EmailVerifiedEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(event), + IsEmailVerified: true, + } + return emailVerified, nil +} + +type EmailVerificationFailedEvent struct { + eventstore.BaseEvent `json:"-"` +} + +func (e *EmailVerificationFailedEvent) Payload() interface{} { + return nil +} + +func (e *EmailVerificationFailedEvent) UniqueConstraints() []*eventstore.UniqueConstraint { + return nil +} + +func NewHumanEmailVerificationFailedEvent(ctx context.Context, aggregate *eventstore.Aggregate) *EmailVerificationFailedEvent { + return &EmailVerificationFailedEvent{ + BaseEvent: *eventstore.NewBaseEventForPush( + ctx, + aggregate, + EmailVerificationFailedType, + ), + } +} + +func EmailVerificationFailedEventMapper(event eventstore.Event) (eventstore.Event, error) { + return &EmailVerificationFailedEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(event), + }, nil +} + +type EmailCodeAddedEvent struct { + eventstore.BaseEvent `json:"-"` + + Code *crypto.CryptoValue `json:"code,omitempty"` + Expiry time.Duration `json:"expiry,omitempty"` + URLTemplate string `json:"url_template,omitempty"` + CodeReturned bool `json:"code_returned,omitempty"` + TriggeredAtOrigin string `json:"triggerOrigin,omitempty"` +} + +func (e *EmailCodeAddedEvent) Payload() interface{} { + return e +} + +func (e *EmailCodeAddedEvent) UniqueConstraints() []*eventstore.UniqueConstraint { + return nil +} + +func (e *EmailCodeAddedEvent) TriggerOrigin() string { + return e.TriggeredAtOrigin +} + +func NewEmailCodeAddedEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, + code *crypto.CryptoValue, + expiry time.Duration, + urlTemplate string, + codeReturned bool, +) *EmailCodeAddedEvent { + return &EmailCodeAddedEvent{ + BaseEvent: *eventstore.NewBaseEventForPush( + ctx, + aggregate, + EmailCodeAddedType, + ), + Code: code, + Expiry: expiry, + URLTemplate: urlTemplate, + CodeReturned: codeReturned, + TriggeredAtOrigin: http.DomainContext(ctx).Origin(), + } +} + +func EmailCodeAddedEventMapper(event eventstore.Event) (eventstore.Event, error) { + codeAdded := &EmailCodeAddedEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(event), + } + err := event.Unmarshal(codeAdded) + if err != nil { + return nil, zerrors.ThrowInternal(err, "USER-3M0sd", "unable to unmarshal human email code added") + } + + return codeAdded, nil +} + +type EmailCodeSentEvent struct { + eventstore.BaseEvent `json:"-"` +} + +func (e *EmailCodeSentEvent) Payload() interface{} { + return nil +} + +func (e *EmailCodeSentEvent) UniqueConstraints() []*eventstore.UniqueConstraint { + return nil +} + +func NewHumanEmailCodeSentEvent(ctx context.Context, aggregate *eventstore.Aggregate) *EmailCodeSentEvent { + return &EmailCodeSentEvent{ + BaseEvent: *eventstore.NewBaseEventForPush( + ctx, + aggregate, + EmailCodeSentType, + ), + } +} + +func EmailCodeSentEventMapper(event eventstore.Event) (eventstore.Event, error) { + return &EmailCodeSentEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(event), + }, nil +} diff --git a/internal/repository/user/schemauser/eventstore.go b/internal/repository/user/schemauser/eventstore.go new file mode 100644 index 0000000000..b9cf03e5d3 --- /dev/null +++ b/internal/repository/user/schemauser/eventstore.go @@ -0,0 +1,9 @@ +package schemauser + +import "github.com/zitadel/zitadel/internal/eventstore" + +func init() { + eventstore.RegisterFilterEventMapper(AggregateType, CreatedType, eventstore.GenericEventMapper[CreatedEvent]) + eventstore.RegisterFilterEventMapper(AggregateType, UpdatedType, eventstore.GenericEventMapper[UpdatedEvent]) + eventstore.RegisterFilterEventMapper(AggregateType, DeletedType, eventstore.GenericEventMapper[DeletedEvent]) +} diff --git a/internal/repository/user/schemauser/phone.go b/internal/repository/user/schemauser/phone.go new file mode 100644 index 0000000000..5110772c04 --- /dev/null +++ b/internal/repository/user/schemauser/phone.go @@ -0,0 +1,198 @@ +package schemauser + +import ( + "context" + "time" + + "github.com/zitadel/zitadel/internal/api/http" + "github.com/zitadel/zitadel/internal/crypto" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/zerrors" +) + +const ( + phoneEventPrefix = eventPrefix + "phone." + PhoneUpdatedType = phoneEventPrefix + "updated" + PhoneVerifiedType = phoneEventPrefix + "verified" + PhoneVerificationFailedType = phoneEventPrefix + "verification.failed" + PhoneCodeAddedType = phoneEventPrefix + "code.added" + PhoneCodeSentType = phoneEventPrefix + "code.sent" +) + +type PhoneChangedEvent struct { + eventstore.BaseEvent `json:"-"` + + PhoneNumber domain.PhoneNumber `json:"phone,omitempty"` +} + +func (e *PhoneChangedEvent) Payload() interface{} { + return e +} + +func (e *PhoneChangedEvent) UniqueConstraints() []*eventstore.UniqueConstraint { + return nil +} + +func NewPhoneChangedEvent(ctx context.Context, aggregate *eventstore.Aggregate, phone domain.PhoneNumber) *PhoneChangedEvent { + return &PhoneChangedEvent{ + BaseEvent: *eventstore.NewBaseEventForPush( + ctx, + aggregate, + PhoneUpdatedType, + ), + PhoneNumber: phone, + } +} + +func PhoneChangedEventMapper(event eventstore.Event) (eventstore.Event, error) { + phoneChangedEvent := &PhoneChangedEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(event), + } + err := event.Unmarshal(phoneChangedEvent) + if err != nil { + return nil, zerrors.ThrowInternal(err, "USER-5M0pd", "unable to unmarshal phone changed") + } + + return phoneChangedEvent, nil +} + +type PhoneVerifiedEvent struct { + eventstore.BaseEvent `json:"-"` + + IsPhoneVerified bool `json:"-"` +} + +func (e *PhoneVerifiedEvent) Payload() interface{} { + return nil +} + +func (e *PhoneVerifiedEvent) UniqueConstraints() []*eventstore.UniqueConstraint { + return nil +} + +func NewPhoneVerifiedEvent(ctx context.Context, aggregate *eventstore.Aggregate) *PhoneVerifiedEvent { + return &PhoneVerifiedEvent{ + BaseEvent: *eventstore.NewBaseEventForPush( + ctx, + aggregate, + PhoneVerifiedType, + ), + } +} + +func PhoneVerifiedEventMapper(event eventstore.Event) (eventstore.Event, error) { + return &PhoneVerifiedEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(event), + IsPhoneVerified: true, + }, nil +} + +type PhoneVerificationFailedEvent struct { + eventstore.BaseEvent `json:"-"` +} + +func (e *PhoneVerificationFailedEvent) Payload() interface{} { + return nil +} + +func (e *PhoneVerificationFailedEvent) UniqueConstraints() []*eventstore.UniqueConstraint { + return nil +} + +func NewPhoneVerificationFailedEvent(ctx context.Context, aggregate *eventstore.Aggregate) *PhoneVerificationFailedEvent { + return &PhoneVerificationFailedEvent{ + BaseEvent: *eventstore.NewBaseEventForPush( + ctx, + aggregate, + PhoneVerificationFailedType, + ), + } +} + +func PhoneVerificationFailedEventMapper(event eventstore.Event) (eventstore.Event, error) { + return &PhoneVerificationFailedEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(event), + }, nil +} + +type PhoneCodeAddedEvent struct { + eventstore.BaseEvent `json:"-"` + + Code *crypto.CryptoValue `json:"code,omitempty"` + Expiry time.Duration `json:"expiry,omitempty"` + CodeReturned bool `json:"code_returned,omitempty"` + TriggeredAtOrigin string `json:"triggerOrigin,omitempty"` +} + +func (e *PhoneCodeAddedEvent) Payload() interface{} { + return e +} + +func (e *PhoneCodeAddedEvent) UniqueConstraints() []*eventstore.UniqueConstraint { + return nil +} + +func (e *PhoneCodeAddedEvent) TriggerOrigin() string { + return e.TriggeredAtOrigin +} + +func NewPhoneCodeAddedEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, + code *crypto.CryptoValue, + expiry time.Duration, + codeReturned bool, +) *PhoneCodeAddedEvent { + return &PhoneCodeAddedEvent{ + BaseEvent: *eventstore.NewBaseEventForPush( + ctx, + aggregate, + PhoneCodeAddedType, + ), + Code: code, + Expiry: expiry, + CodeReturned: codeReturned, + TriggeredAtOrigin: http.DomainContext(ctx).Origin(), + } +} + +func PhoneCodeAddedEventMapper(event eventstore.Event) (eventstore.Event, error) { + codeAdded := &PhoneCodeAddedEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(event), + } + err := event.Unmarshal(codeAdded) + if err != nil { + return nil, zerrors.ThrowInternal(err, "USER-6Ms9d", "unable to unmarshal phone code added") + } + + return codeAdded, nil +} + +type PhoneCodeSentEvent struct { + eventstore.BaseEvent `json:"-"` +} + +func (e *PhoneCodeSentEvent) Payload() interface{} { + return e +} + +func (e *PhoneCodeSentEvent) UniqueConstraints() []*eventstore.UniqueConstraint { + return nil +} + +func NewPhoneCodeSentEvent(ctx context.Context, aggregate *eventstore.Aggregate) *PhoneCodeSentEvent { + return &PhoneCodeSentEvent{ + BaseEvent: *eventstore.NewBaseEventForPush( + ctx, + aggregate, + PhoneCodeSentType, + ), + } +} + +func PhoneCodeSentEventMapper(event eventstore.Event) (eventstore.Event, error) { + return &PhoneCodeSentEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(event), + }, nil +} diff --git a/internal/repository/user/schemauser/user.go b/internal/repository/user/schemauser/user.go new file mode 100644 index 0000000000..4c88d53087 --- /dev/null +++ b/internal/repository/user/schemauser/user.go @@ -0,0 +1,144 @@ +package schemauser + +import ( + "context" + "encoding/json" + + "github.com/zitadel/zitadel/internal/eventstore" +) + +const ( + eventPrefix = "user." + CreatedType = eventPrefix + "created" + UpdatedType = eventPrefix + "updated" + DeletedType = eventPrefix + "deleted" +) + +type CreatedEvent struct { + *eventstore.BaseEvent `json:"-"` + ID string `json:"id"` + SchemaID string `json:"schemaID"` + SchemaRevision uint64 `json:"schemaRevision"` + Data json.RawMessage `json:"user,omitempty"` +} + +func (e *CreatedEvent) SetBaseEvent(event *eventstore.BaseEvent) { + e.BaseEvent = event +} + +func (e *CreatedEvent) Payload() interface{} { + return e +} + +func (e *CreatedEvent) UniqueConstraints() []*eventstore.UniqueConstraint { + return nil +} + +func NewCreatedEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, + + schemaID string, + schemaRevision uint64, + data json.RawMessage, +) *CreatedEvent { + return &CreatedEvent{ + BaseEvent: eventstore.NewBaseEventForPush( + ctx, + aggregate, + CreatedType, + ), + SchemaID: schemaID, + SchemaRevision: schemaRevision, + Data: data, + } +} + +type UpdatedEvent struct { + *eventstore.BaseEvent `json:"-"` + + SchemaID *string `json:"schemaID,omitempty"` + SchemaRevision *uint64 `json:"schemaRevision,omitempty"` + Data json.RawMessage `json:"schema,omitempty"` + oldSchemaID string + oldRevision uint64 +} + +func (e *UpdatedEvent) SetBaseEvent(event *eventstore.BaseEvent) { + e.BaseEvent = event +} + +func (e *UpdatedEvent) Payload() interface{} { + return e +} + +func (e *UpdatedEvent) UniqueConstraints() []*eventstore.UniqueConstraint { + return nil +} +func NewUpdatedEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, + changes []Changes, +) *UpdatedEvent { + updatedEvent := &UpdatedEvent{ + BaseEvent: eventstore.NewBaseEventForPush( + ctx, + aggregate, + UpdatedType, + ), + } + for _, change := range changes { + change(updatedEvent) + } + return updatedEvent +} + +type Changes func(event *UpdatedEvent) + +func ChangeSchemaID(oldSchemaID, schemaID string) func(event *UpdatedEvent) { + return func(e *UpdatedEvent) { + e.SchemaID = &schemaID + e.oldSchemaID = oldSchemaID + } +} +func ChangeSchemaRevision(oldSchemaRevision, schemaRevision uint64) func(event *UpdatedEvent) { + return func(e *UpdatedEvent) { + e.SchemaRevision = &schemaRevision + e.oldRevision = oldSchemaRevision + } +} + +func ChangeData(data json.RawMessage) func(event *UpdatedEvent) { + return func(e *UpdatedEvent) { + e.Data = data + } +} + +type DeletedEvent struct { + *eventstore.BaseEvent `json:"-"` +} + +func (e *DeletedEvent) SetBaseEvent(event *eventstore.BaseEvent) { + e.BaseEvent = event +} + +func (e *DeletedEvent) Payload() interface{} { + return e +} + +func (e *DeletedEvent) UniqueConstraints() []*eventstore.UniqueConstraint { + return nil +} + +func NewDeletedEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, +) *DeletedEvent { + return &DeletedEvent{ + BaseEvent: eventstore.NewBaseEventForPush( + ctx, + aggregate, + DeletedType, + ), + } +} diff --git a/internal/static/i18n/bg.yaml b/internal/static/i18n/bg.yaml index 7f4a96e265..5eac86ce0f 100644 --- a/internal/static/i18n/bg.yaml +++ b/internal/static/i18n/bg.yaml @@ -594,6 +594,11 @@ Errors: NotActive: Потребителската схема не е активна NotInactive: Потребителската схема не е неактивна NotExists: Потребителската схема не съществува + ID: + Missing: Липсва идентификатор на потребителска схема + Invalid: Потребителската схема е невалидна + Data: + Invalid: Невалидни данни за потребителска схема TokenExchange: FeatureDisabled: Функцията Token Exchange е деактивирана за вашето копие. https://zitadel.com/docs/apis/resources/feature_service_v2/feature-service-set-instance-features Token: @@ -1352,6 +1357,26 @@ EventTypes: deactivated: Потребителската схема е деактивирана reactivated: Потребителската схема е активирана отново deleted: Потребителската схема е изтрита + user: + created: Потребител е създаден + updated: Потребителят е актуализиран + deleted: Потребителят е изтрит + email: + updated: Имейл адресът е променен + verified: Имейл адресът е потвърден + verification: + failed: Проверката на имейл адреса не бе успешна + code: + added: Генериран код за потвърждение на имейл адрес + sent: Кодът за потвърждение на имейл адреса е изпратен + phone: + updated: Телефонният номер е променен + verified: Телефонният номер е потвърден + verification: + failed: Неуспешна проверка на телефонния номер + code: + added: Генериран код за потвърждение на телефонен номер + sent: Кодът за потвърждение на телефонния номер е изпратен web_key: added: Добавен уеб ключ activated: Уеб ключът е активиран diff --git a/internal/static/i18n/cs.yaml b/internal/static/i18n/cs.yaml index ed44f2a490..2c6c0dc266 100644 --- a/internal/static/i18n/cs.yaml +++ b/internal/static/i18n/cs.yaml @@ -575,6 +575,11 @@ Errors: NotActive: Uživatelské schéma není aktivní NotInactive: Uživatelské schéma není neaktivní NotExists: Uživatelské schéma neexistuje + ID: + Missing: Chybí ID schématu uživatele + Invalid: Uživatelské schéma je neplatné + Data: + Invalid: Data neplatná pro uživatelské schéma TokenExchange: FeatureDisabled: Funkce Token Exchange je pro vaši instanci zakázána. https://zitadel.com/docs/apis/resources/feature_service_v2/feature-service-set-instance-features Token: @@ -1318,6 +1323,26 @@ EventTypes: deactivated: Uživatelské schéma deaktivováno reactivated: Uživatelské schéma bylo znovu aktivováno deleted: Uživatelské schéma bylo smazáno + user: + created: Uživatel vytvořen + updated: Uživatel aktualizován + deleted: Uživatel byl smazán + email: + updated: E-mailová adresa změněna + verified: E-mailová adresa ověřena + verification: + failed: Ověření e-mailové adresy se nezdařilo + code: + added: Vygenerován ověřovací kód e-mailové adresy + sent: Ověřovací kód e-mailové adresy odeslán + phone: + updated: Telefonní číslo změněno + verified: Telefonní číslo ověřeno + verification: + failed: Ověření telefonního čísla se nezdařilo + code: + added: Byl vygenerován ověřovací kód telefonního čísla + sent: Ověřovací kód telefonního čísla odeslán web_key: added: Přidán webový klíč activated: Web Key aktivován diff --git a/internal/static/i18n/de.yaml b/internal/static/i18n/de.yaml index 432aae3c5e..68ade0ed42 100644 --- a/internal/static/i18n/de.yaml +++ b/internal/static/i18n/de.yaml @@ -577,6 +577,11 @@ Errors: NotActive: Benutzerschema nicht aktiv NotInactive: Benutzerschema nicht inaktiv NotExists: Benutzerschema existiert nicht + ID: + Missing: BenutzerschemaID fehlt + Invalid: Benutzerschema ist ungültig + Data: + Invalid: Daten für Benutzerschema ungültig TokenExchange: FeatureDisabled: Die Token-Austauschfunktion ist für Ihre Instanz deaktiviert. https://zitadel.com/docs/apis/resources/feature_service_v2/feature-service-set-instance-features Token: @@ -1320,6 +1325,26 @@ EventTypes: deactivated: Benutzerschema deaktiviert reactivated: Benutzerschema reaktiviert deleted: Benutzerschema gelöscht + user: + created: Benutzer erstellt + updated: Benutzer aktualisiert + deleted: Benutzer gelöscht + email: + updated: E-Mail-Adresse geändert + verified: E-Mail-Adresse verifiziert + verification: + failed: E-Mail-Adressenverifizierung fehlgeschlagen + code: + added: E-Mail-Adressenverifizierungscode generiert + sent: E-Mail-Adressenverifizierungscode gesendet + phone: + updated: Telefonnummer geändert + verified: Telefonnummer verifiziert + verification: + failed: Telefonnummernverifizierung fehlgeschlagen + code: + added: Telefonnummernverifizierungscode generiert + sent: Telefonnummernverifizierungscode gesendet web_key: added: Web Key hinzugefügt activated: Web Key aktiviert diff --git a/internal/static/i18n/en.yaml b/internal/static/i18n/en.yaml index bb464ee400..cc920a3efb 100644 --- a/internal/static/i18n/en.yaml +++ b/internal/static/i18n/en.yaml @@ -577,6 +577,11 @@ Errors: NotActive: User Schema not active NotInactive: User Schema not inactive NotExists: User Schema does not exist + ID: + Missing: User Schema ID missing + Invalid: User Schema invalid + Data: + Invalid: Data invalid for User Schema TokenExchange: FeatureDisabled: Token Exchange feature is disabled for your instance. https://zitadel.com/docs/apis/resources/feature_service_v2/feature-service-set-instance-features Token: @@ -1320,6 +1325,28 @@ EventTypes: deactivated: User Schema deactivated reactivated: User Schema reactivated deleted: User Schema deleted + user: + created: User created + updated: User updated + deleted: User deleted + email: + updated: Email address changed + verified: Email address verified + verification: + failed: Email address verification failed + code: + added: Email address verification code generated + sent: Email address verification code sent + phone: + updated: Phone number changed + verified: Phone number verified + verification: + failed: Phone number verification failed + code: + added: Phone number verification code generated + sent: Phone number verification code sent + + web_key: added: Web Key added activated: Web Key activated diff --git a/internal/static/i18n/es.yaml b/internal/static/i18n/es.yaml index e4901cf205..58ecfa261c 100644 --- a/internal/static/i18n/es.yaml +++ b/internal/static/i18n/es.yaml @@ -577,6 +577,11 @@ Errors: NotActive: Esquema de usuario no activo NotInactive: Esquema de usuario no inactivo NotExists: El esquema de usuario no existe + ID: + Missing: Falta el ID del esquema de usuario + Invalid: Esquema de usuario no válido + Data: + Invalid: Datos no válidos para el esquema de usuario TokenExchange: FeatureDisabled: La función de intercambio de tokens está deshabilitada para su instancia. https://zitadel.com/docs/apis/resources/feature_service_v2/feature-service-set-instance-features Token: @@ -1320,6 +1325,26 @@ EventTypes: deactivated: Esquema de usuario desactivado reactivated: Esquema de usuario reactivado deleted: Esquema de usuario eliminado + user: + created: Usuario creado + updated: Usuario actualizado + deleted: Usuario eliminado + email: + updated: Dirección de correo electrónico modificada + verified: Dirección de correo electrónico verificada + verification: + failed: Error en la verificación de la dirección de correo electrónico + code: + added: Código de verificación de la dirección de correo electrónico generado + sent: Código de verificación de la dirección de correo electrónico enviado + phone: + updated: Número de teléfono modificado + verified: Número de teléfono verificado + verification: + failed: Error en la verificación del número de teléfono + code: + added: Código de verificación del número de teléfono generado + sent: Código de verificación del número de teléfono enviado web_key: added: Clave web añadida activated: Clave web activada diff --git a/internal/static/i18n/fr.yaml b/internal/static/i18n/fr.yaml index 6b0dd9b799..8fa31c4667 100644 --- a/internal/static/i18n/fr.yaml +++ b/internal/static/i18n/fr.yaml @@ -577,6 +577,11 @@ Errors: NotActive: Schéma utilisateur non actif NotInactive: Le schéma utilisateur n'est pas inactif NotExists: Le schéma utilisateur n'existe pas + ID: + Missing: ID de schéma utilisateur manquant + Invalid: Schéma utilisateur non valide + Data: + Invalid: Données non valides pour le schéma utilisateur TokenExchange: FeatureDisabled: La fonctionnalité Token Exchange est désactivée pour votre instance. https://zitadel.com/docs/apis/resources/feature_service_v2/feature-service-set-instance-features Token: @@ -1315,6 +1320,26 @@ EventTypes: deactivated: Schéma utilisateur désactivé reactivated: Schéma utilisateur réactivé deleted: Schéma utilisateur supprimé + user: + created: Utilisateur créé + updated: Utilisateur mis à jour + deleted: Utilisateur supprimé + email: + updated: Adresse e-mail modifiée + verified: Adresse e-mail vérifiée + verification: + failed: Échec de la vérification de l'adresse e-mail + code: + added: Code de vérification de l'adresse e-mail généré + sent: Code de vérification de l'adresse e-mail envoyé + phone: + updated: Numéro de téléphone modifié + verified: Numéro de téléphone vérifié + verification: + failed: Échec de la vérification du numéro de téléphone + code: + added: Code de vérification du numéro de téléphone généré + sent: Code de vérification du numéro de téléphone envoyé web_key: added: Clé Web ajoutée activated: Clé Web activée diff --git a/internal/static/i18n/it.yaml b/internal/static/i18n/it.yaml index 8ec4284206..d21c80e60d 100644 --- a/internal/static/i18n/it.yaml +++ b/internal/static/i18n/it.yaml @@ -577,6 +577,11 @@ Errors: NotActive: Schema utente non attivo NotInactive: Schema utente non inattivo NotExists: Lo schema utente non esiste + ID: + Missing: ID schema utente mancante + Invalid: Schema utente non valido + Data: + Invalid: Dati non validi per lo schema utente TokenExchange: FeatureDisabled: La funzionalità di scambio token è disabilitata per la tua istanza. https://zitadel.com/docs/apis/resources/feature_service_v2/feature-service-set-instance-features Token: @@ -1316,6 +1321,26 @@ EventTypes: deactivated: Schema utente disattivato reactivated: Schema utente riattivato deleted: Schema utente eliminato + user: + created: Utente creato + updated: Utente aggiornato + deleted: Utente eliminato + email: + updated: Indirizzo email modificato + verified: Indirizzo email verificato + verification: + failed: Verifica indirizzo email non riuscita + code: + added: Codice di verifica indirizzo email generato + sent: Codice di verifica indirizzo email inviato + phone: + updated: Numero di telefono modificato + verified: Numero di telefono verificato + verification: + failed: Verifica numero di telefono non riuscita + code: + added: Codice di verifica numero di telefono generato + sent: Codice di verifica numero di telefono inviato web_key: added: Web Key aggiunto activated: Web Key attivato diff --git a/internal/static/i18n/ja.yaml b/internal/static/i18n/ja.yaml index 618fc7d35a..ed50c7db8d 100644 --- a/internal/static/i18n/ja.yaml +++ b/internal/static/i18n/ja.yaml @@ -566,6 +566,11 @@ Errors: NotActive: ユーザースキーマがアクティブではありません NotInactive: ユーザースキーマが非アクティブではありません NotExists: ユーザースキーマが存在しません + ID: + Missing: ユーザー スキーマ ID がありません + Invalid: ユーザー スキーマが無効です + Data: + Invalid: ユーザー スキーマのデータが無効です TokenExchange: FeatureDisabled: インスタンスではトークン交換機能が無効になっています。 https://zitadel.com/docs/apis/resources/feature_service_v2/feature-service-set-instance-features Token: @@ -1306,6 +1311,26 @@ EventTypes: deactivated: ユーザースキーマが非アクティブ化されました reactivated: ユーザースキーマが再アクティブ化されました deleted: ユーザースキーマが削除されました + user: + created: ユーザーが作成されました + updated: ユーザーが更新されました + deleted: ユーザーが削除されました + email: + updated: メールアドレスが変更されました + verified: メールアドレスが確認されました + verification: + failed: メールアドレスの確認に失敗しました + code: + added: メールアドレスの確認コードが生成されました + sent: メールアドレスの確認コードが送信されました + phone: + updated: 電話番号が変更されました + verified: 電話番号が確認されました + verification: + failed: 電話番号の確認に失敗しました + code: + added: 電話番号の確認コードが生成されました + sent: 電話番号の確認コードが送信されました web_key: added: Web キーが追加されました activated: Web キーが有効化されました diff --git a/internal/static/i18n/mk.yaml b/internal/static/i18n/mk.yaml index e308bd4855..7239b9ce9a 100644 --- a/internal/static/i18n/mk.yaml +++ b/internal/static/i18n/mk.yaml @@ -576,6 +576,11 @@ Errors: NotActive: Корисничката шема не е активна NotInactive: Корисничката шема не е неактивна NotExists: Корисничката шема не постои + ID: + Missing: Недостасува ID на корисничка шема + Invalid: Корисничката шема е неважечка + Data: + Invalid: Податоците не се валидни за корисничка шема TokenExchange: FeatureDisabled: Функцијата за размена на токени е оневозможена на вашиот пример. https://zitadel.com/docs/apis/resources/feature_service_v2/feature-service-set-instance-features Token: @@ -1318,6 +1323,26 @@ EventTypes: deactivated: Корисничката шема е деактивирана reactivated: Корисничката шема е реактивирана deleted: Корисничката шема е избришана + user: + created: Корисникот е создаден + updated: Корисникот е ажуриран + deleted: Корисникот е избришан + email: + updated: Адресата на е-пошта е променета + verified: Адресата на е-пошта е потврдена + verification: + failed: Потврдата на адресата на е-пошта не успеа + code: + added: Генериран е код за потврда на адресата на е-пошта + sent: Испратен е код за потврда на адресата на е-пошта + phone: + updated: Телефонскиот број е променет + verified: Телефонскиот број е потврден + verification: + failed: Потврдата на телефонскиот број не успеа + code: + added: Генериран е кодот за потврда на телефонскиот број + sent: Кодот за потврда на телефонскиот број е испратен web_key: added: Додаден е веб-клуч activated: Веб-клучот е активиран diff --git a/internal/static/i18n/nl.yaml b/internal/static/i18n/nl.yaml index 6eda75b42a..3991abb255 100644 --- a/internal/static/i18n/nl.yaml +++ b/internal/static/i18n/nl.yaml @@ -577,6 +577,11 @@ Errors: NotActive: Gebruikersschema niet actief NotInactive: Gebruikersschema niet inactief NotExists: Gebruikersschema bestaat niet + ID: + Missing: Недостасува ID на корисничка шема + Invalid: Корисничката шема е неважечка + Data: + Invalid: Податоците не се валидни за корисничка шема TokenExchange: FeatureDisabled: De Token Exchange-functie is uitgeschakeld voor uw instantie. https://zitadel.com/docs/apis/resources/feature_service_v2/feature-service-set-instance-features Token: @@ -1315,6 +1320,26 @@ EventTypes: deactivated: Gebruikersschema gedeactiveerd reactivated: Gebruikersschema opnieuw geactiveerd deleted: Gebruikersschema verwijderd + user: + created: Gebruiker aangemaakt + updated: Gebruiker bijgewerkt + deleted: Gebruiker verwijderd + email: + updated: E-mailadres gewijzigd + verified: E-mailadres geverifieerd + verification: + failed: E-mailadres verificatie mislukt + code: + added: E-mailadres verificatiecode gegenereerd + sent: E-mailadres verificatiecode verzonden + phone: + updated: Telefoonnummer gewijzigd + verified: Telefoonnummer geverifieerd + verification: + failed: Telefoonnummer verificatie mislukt + code: + added: Telefoonnummer verificatiecode gegenereerd + sent: Telefoonnummer verificatiecode verzonden web_key: added: Web Key toegevoegd activated: Web Key geactiveerd diff --git a/internal/static/i18n/pl.yaml b/internal/static/i18n/pl.yaml index 18ae28521f..e9fcac3a3e 100644 --- a/internal/static/i18n/pl.yaml +++ b/internal/static/i18n/pl.yaml @@ -577,6 +577,11 @@ Errors: NotActive: Schemat użytkownika nieaktywny NotInactive: Schemat użytkownika nie jest nieaktywny NotExists: Schemat użytkownika nie istnieje + ID: + Missing: Brak identyfikatora schematu użytkownika + Invalid: Nieprawidłowy schemat użytkownika + Data: + Invalid: Nieprawidłowe dane dla schematu użytkownika TokenExchange: FeatureDisabled: Funkcja wymiany tokenów jest wyłączona dla Twojej instancji. https://zitadel.com/docs/apis/resources/feature_service_v2/feature-service-set-instance-features Token: @@ -1320,6 +1325,26 @@ EventTypes: deactivated: Schemat użytkownika dezaktywowany reactivated: Schemat użytkownika został ponownie aktywowany deleted: Schemat użytkownika został usunięty + user: + created: Użytkownik utworzony + updated: Użytkownik zaktualizowany + deleted: Użytkownik usunięty + email: + updated: Adres e-mail zmieniony + verified: Adres e-mail zweryfikowany + verification: + failed: Weryfikacja adresu e-mail nie powiodła się + code: + added: Wygenerowano kod weryfikacyjny adresu e-mail + sent: Wysłano kod weryfikacyjny adresu e-mail + phone: + updated: Numer telefonu zmieniony + verified: Numer telefonu zweryfikowany + verification: + failed: Weryfikacja numeru telefonu nie powiodła się + code: + added: Wygenerowano kod weryfikacyjny numeru telefonu + sent: Wysłano kod weryfikacyjny numeru telefonu web_key: added: Dodano klucz internetowy activated: Klucz internetowy aktywowano diff --git a/internal/static/i18n/pt.yaml b/internal/static/i18n/pt.yaml index 2d270bb274..fa553641be 100644 --- a/internal/static/i18n/pt.yaml +++ b/internal/static/i18n/pt.yaml @@ -572,6 +572,11 @@ Errors: NotActive: Esquema do usuário não ativo NotInactive: Esquema do usuário não inativo NotExists: O esquema do usuário não existe + ID: + Missing: ID do esquema do utilizador em falta + Invalid: Esquema de utilizador inválido + Data: + Invalid: Dados inválidos para o esquema do utilizador TokenExchange: FeatureDisabled: O recurso Token Exchange está desabilitado para sua instância. https://zitadel.com/docs/apis/resources/feature_service_v2/feature-service-set-instance-features Token: @@ -1312,6 +1317,26 @@ EventTypes: deactivated: Esquema de usuário desativado reactivated: Esquema do usuário reativado deleted: Esquema do usuário excluído + user: + created: Utilizador criado + updated: Utilizador atualizado + deleted: Utilizador excluído + email: + updated: Endereço de e-mail alterado + verified: Endereço de e-mail verificado + verification: + failed: Falha na verificação do endereço de e-mail + code: + added: Código de verificação do endereço de e-mail gerado + sent: Código de verificação do endereço de e-mail enviado + phone: + updated: Número de telefone alterado + verified: Número de telefone verificado + verification: + failed: Falha na verificação do número de telefone + code: + added: Código de verificação do número de telefone gerado + sent: Código de verificação do número de telefone enviado web_key: added: Chave Web adicionada activated: Chave Web ativada diff --git a/internal/static/i18n/ru.yaml b/internal/static/i18n/ru.yaml index e38edb367a..00bfe57955 100644 --- a/internal/static/i18n/ru.yaml +++ b/internal/static/i18n/ru.yaml @@ -566,6 +566,11 @@ Errors: NotActive: Пользовательская схема не активна NotInactive: Пользовательская схема не неактивна NotExists: Пользовательская схема не существует + ID: + Missing: Отсутствует идентификатор схемы пользователя + Invalid: Недействительная схема пользователя + Data: + Invalid: Данные недействительны для схемы пользователя TokenExchange: FeatureDisabled: Функция обмена токенами отключена для вашего экземпляра. https://zitadel.com/docs/apis/resources/feature_service_v2/feature-service-set-instance-features Token: @@ -1306,6 +1311,26 @@ EventTypes: deactivated: Пользовательская схема деактивирована reactivated: Пользовательская схема повторно активирована deleted: Пользовательская схема удалена + user: + created: Пользователь создан + updated: Пользователь обновлен + deleted: Пользователь удален + email: + updated: Адрес электронной почты изменен + verified: Адрес электронной почты проверен + verification: + failed: Проверка адреса электронной почты не удалась + code: + added: Сгенерирован код проверки адреса электронной почты + sent: Отправлен код проверки адреса электронной почты + phone: + updated: Номер телефона изменен + verified: Номер телефона проверен + verification: + failed: Проверка номера телефона не удалась + code: + added: Сгенерирован код проверки номера телефона + sent: Отправлен код проверки номера телефона web_key: added: Добавлен веб-ключ activated: Веб-ключ активирован diff --git a/internal/static/i18n/sv.yaml b/internal/static/i18n/sv.yaml index f6013bd182..da33e42269 100644 --- a/internal/static/i18n/sv.yaml +++ b/internal/static/i18n/sv.yaml @@ -576,6 +576,11 @@ Errors: NotActive: Användarschema inte aktivt NotInactive: Användarschema inte inaktivt NotExists: Användarschema existerar inte + ID: + Missing: Användarschema-ID saknas + Invalid: Ogiltigt användarschema + Data: + Invalid: Data ogiltig för användarschema TokenExchange: FeatureDisabled: Token Exchange-funktionen är inaktiverad för din instans. https://zitadel.com/docs/apis/resources/feature_service_v2/feature-service-set-instance-features Token: @@ -1319,6 +1324,26 @@ EventTypes: deactivated: Användarschema avaktiverat reactivated: Användarschema återaktiverat deleted: Användarschema borttaget + user: + created: Användare skapad + updated: Användaren uppdaterad + deleted: Användare raderad + email: + updated: E-postadress ändrad + verified: E-postadress verifierad + verification: + failed: E-postadressverifiering misslyckades + code: + added: Verifieringskod för e-postadress genererad + sent: E-postadressens verifieringskod har skickats + phone: + updated: Telefonnummer ändrat + verified: Telefonnummer verifierat + verification: + failed: Verifiering av telefonnummer misslyckades + code: + added: Verifieringskod för telefonnummer genererad + sent: Verifieringskoden för telefonnummer har skickats web_key: added: Webbnyckel har lagts till activated: Webbnyckel aktiverad diff --git a/internal/static/i18n/zh.yaml b/internal/static/i18n/zh.yaml index 2f2d6ed2c7..881909a20c 100644 --- a/internal/static/i18n/zh.yaml +++ b/internal/static/i18n/zh.yaml @@ -577,6 +577,11 @@ Errors: NotActive: 用户架构未激活 NotInactive: 用户架构未处于非活动状态 NotExists: 用户架构不存在 + ID: + Missing: 缺少用户架构 ID + Invalid: 用户架构无效 + Data: + Invalid: 用户架构的数据无效 TokenExchange: FeatureDisabled: 您的实例已禁用令牌交换功能。 https://zitadel.com/docs/apis/resources/feature_service_v2/feature-service-set-instance-features Token: @@ -1319,6 +1324,26 @@ EventTypes: deactivated: 用户架构已停用 reactivated: 用户架构已重新激活 deleted: 用户架构已删除 + user: + created: 用户已创建 + updated: 用户已更新 + deleted: 用户已删除 + email: + updated: 电子邮件地址已更改 + verified: 电子邮件地址已验证 + verification: + failed: 电子邮件地址验证失败 + code: + added: 电子邮件地址验证码已生成 + sent: 电子邮件地址验证码已发送 + phone: + updated: 电话号码已更改 + verified: 电话号码已验证 + verification: + failed: 电话号码验证失败 + code: + added: 电话号码验证码已生成 + sent: 电话号码验证码已发送 web_key: added: 已添加 Web Key activated: 已激活 Web Key diff --git a/load-test/Makefile b/load-test/Makefile index 106b272811..4d87760eca 100644 --- a/load-test/Makefile +++ b/load-test/Makefile @@ -6,33 +6,40 @@ ADMIN_PASSWORD ?= .PHONY: human_password_login human_password_login: bundle - k6 run dist/human_password_login.js --vus ${VUS} --duration ${DURATION} + k6 run --summary-trend-stats "min,avg,max,p(50),p(95),p(99)" dist/human_password_login.js --vus ${VUS} --duration ${DURATION} .PHONY: machine_pat_login machine_pat_login: bundle - k6 run dist/machine_pat_login.js --vus ${VUS} --duration ${DURATION} + k6 run --summary-trend-stats "min,avg,max,p(50),p(95),p(99)" dist/machine_pat_login.js --vus ${VUS} --duration ${DURATION} .PHONY: machine_client_credentials_login machine_client_credentials_login: bundle - k6 run dist/machine_client_credentials_login.js --vus ${VUS} --duration ${DURATION} + k6 run --summary-trend-stats "min,avg,max,p(50),p(95),p(99)" dist/machine_client_credentials_login.js --vus ${VUS} --duration ${DURATION} .PHONY: user_info user_info: bundle - k6 run dist/user_info.js --vus ${VUS} --duration ${DURATION} + k6 run --summary-trend-stats "min,avg,max,p(50),p(95),p(99)" dist/user_info.js --vus ${VUS} --duration ${DURATION} .PHONY: manipulate_user manipulate_user: bundle - k6 run dist/manipulate_user.js --vus ${VUS} --duration ${DURATION} + k6 run --summary-trend-stats "min,avg,max,p(50),p(95),p(99)" dist/manipulate_user.js --vus ${VUS} --duration ${DURATION} .PHONY: introspect introspect: ensure_modules bundle go install go.k6.io/xk6/cmd/xk6@latest cd ../../xk6-modules && xk6 build --with xk6-zitadel=. - ./../../xk6-modules/k6 run dist/introspection.js --vus ${VUS} --duration ${DURATION} + ./../../xk6-modules/k6 run --summary-trend-stats "min,avg,max,p(50),p(95),p(99)" dist/introspection.js --vus ${VUS} --duration ${DURATION} .PHONY: add_session add_session: bundle - k6 run dist/session.js --vus ${VUS} --duration ${DURATION} + k6 run --summary-trend-stats "min,avg,max,p(50),p(95),p(99)" dist/session.js --vus ${VUS} --duration ${DURATION} + +.PHONY: machine_jwt_profile_grant +machine_jwt_profile_grant: ensure_modules ensure_key_pair bundle + go install go.k6.io/xk6/cmd/xk6@latest + cd ../../xk6-modules && xk6 build --with xk6-zitadel=. + ./../../xk6-modules/k6 run --summary-trend-stats "min,avg,max,p(50),p(95),p(99)" dist/machine_jwt_profile_grant.js --iterations 1 + # --vus ${VUS} --duration ${DURATION} .PHONY: lint lint: @@ -50,4 +57,16 @@ endif .PHONY: bundle bundle: npm i - npm run bundle \ No newline at end of file + npm run bundle + +.PHONY: ensure_key_pair +ensure_key_pair: +ifeq (,$(wildcard $(PWD)/.keys)) + mkdir .keys +endif +ifeq (,$(wildcard $(PWD)/.keys/key.pem)) + openssl genrsa -out .keys/key.pem 2048 +endif +ifeq (,$(wildcard $(PWD)/.keys/key.pem.pub)) + openssl rsa -in .keys/key.pem -outform PEM -pubout -out .keys/key.pem.pub +endif \ No newline at end of file diff --git a/load-test/README.md b/load-test/README.md index 18705a897d..e046372ee0 100644 --- a/load-test/README.md +++ b/load-test/README.md @@ -49,4 +49,7 @@ Before you run the tests you need an initialized user. The tests don't implement test: calls introspection endpoint using the given JWTs * `make add_session` setup: creates human users - test: creates new sessions with user id check \ No newline at end of file + test: creates new sessions with user id check +* `make machine_jwt_profile_grant` + setup: generates private/public key, creates machine users, adds a key + test: creates a token and calls user info \ No newline at end of file diff --git a/load-test/package-lock.json b/load-test/package-lock.json index 3f519201bd..d6e10ba428 100644 --- a/load-test/package-lock.json +++ b/load-test/package-lock.json @@ -19,7 +19,7 @@ "babel-loader": "9.1.3", "clean-webpack-plugin": "4.0.0", "copy-webpack-plugin": "^12.0.2", - "prettier": "^3.1.1", + "prettier": "^3.3.3", "prettier-plugin-organize-imports": "^3.2.4", "typescript": "5.4.5", "webpack": "5.89.0", @@ -27,7 +27,7 @@ "webpack-glob-entries": "^1.0.1" }, "engines": { - "node": "16 || 18 || 20" + "node": "18 || 20" } }, "node_modules/@ampproject/remapping": { @@ -2389,12 +2389,12 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -2841,9 +2841,9 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "dependencies": { "to-regex-range": "^5.0.1" @@ -2965,9 +2965,9 @@ } }, "node_modules/globby": { - "version": "14.0.1", - "resolved": "https://registry.npmjs.org/globby/-/globby-14.0.1.tgz", - "integrity": "sha512-jOMLD2Z7MAhyG8aJpNOpmziMOP4rPLcc95oQPKXBazW82z+CEgPFBQvEpRUa1KeIMUJo4Wsm+q6uzO/Q/4BksQ==", + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-14.0.2.tgz", + "integrity": "sha512-s3Fq41ZVh7vbbe2PN3nrW7yC7U7MFVc5c98/iTl9c2GawNMKx/J648KQRW6WKkuU8GIbbh2IXfIRQjOZnXcTnw==", "dev": true, "dependencies": { "@sindresorhus/merge-streams": "^2.1.0", @@ -3012,9 +3012,9 @@ } }, "node_modules/ignore": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", - "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, "engines": { "node": ">= 4" @@ -3383,12 +3383,12 @@ } }, "node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", + "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", "dev": true, "dependencies": { - "braces": "^3.0.2", + "braces": "^3.0.3", "picomatch": "^2.3.1" }, "engines": { @@ -3636,9 +3636,9 @@ } }, "node_modules/prettier": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", - "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", + "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", "dev": true, "bin": { "prettier": "bin/prettier.cjs" diff --git a/load-test/package.json b/load-test/package.json index c471a4a329..cf5297d246 100644 --- a/load-test/package.json +++ b/load-test/package.json @@ -4,30 +4,30 @@ "repository": "ssh://git@github.com/zitadel/zitadel.git", "author": "ZITADEL Authors ", "engines": { - "node": "16 || 18 || 20" + "node": "18 || 20" }, "license": "Apache-2.0", "devDependencies": { - "@babel/core": "7.23.7", - "@babel/plugin-proposal-class-properties": "7.13.0", - "@babel/plugin-proposal-object-rest-spread": "7.13.8", - "@babel/preset-env": "7.23.8", - "@babel/preset-typescript": "7.23.3", - "@types/k6": ">=0.50.0", - "@types/webpack": "5.28.5", - "babel-loader": "9.1.3", - "clean-webpack-plugin": "4.0.0", - "copy-webpack-plugin": "^12.0.2", - "typescript": "5.4.5", - "webpack": "5.89.0", - "webpack-cli": "5.1.4", - "webpack-glob-entries": "^1.0.1", - "prettier": "^3.1.1", - "prettier-plugin-organize-imports": "^3.2.4" + "@babel/core": "7.23.7", + "@babel/plugin-proposal-class-properties": "7.13.0", + "@babel/plugin-proposal-object-rest-spread": "7.13.8", + "@babel/preset-env": "7.23.8", + "@babel/preset-typescript": "7.23.3", + "@types/k6": ">=0.50.0", + "@types/webpack": "5.28.5", + "babel-loader": "9.1.3", + "clean-webpack-plugin": "4.0.0", + "copy-webpack-plugin": "^12.0.2", + "prettier": "^3.3.3", + "prettier-plugin-organize-imports": "^3.2.4", + "typescript": "5.4.5", + "webpack": "5.89.0", + "webpack-cli": "5.1.4", + "webpack-glob-entries": "^1.0.1" }, "scripts": { - "bundle": "webpack", - "lint": "prettier --check src", - "lint:fix": "prettier --write src" + "bundle": "webpack", + "lint": "prettier --check src", + "lint:fix": "prettier --write src" } - } \ No newline at end of file +} diff --git a/load-test/src/oidc.ts b/load-test/src/oidc.ts index a9173bfceb..a7ebce7dc3 100644 --- a/load-test/src/oidc.ts +++ b/load-test/src/oidc.ts @@ -1,8 +1,11 @@ import { JSONObject, check, fail } from 'k6'; import encoding from 'k6/encoding'; -import http from 'k6/http'; +import http, { RequestBody } from 'k6/http'; import { Trend } from 'k6/metrics'; import url from './url'; +import { Config } from './config'; +// @ts-ignore Import module +import zitadel from 'k6/x/zitadel'; export class Tokens { idToken?: string; @@ -103,4 +106,66 @@ export function clientCredentials(clientId: string, clientSecret: string): Promi resolve(tokens) }); }); -} \ No newline at end of file +} + +export interface TokenRequest { + payload(): RequestBody; + headers(): { [name: string]: string; }; +} + +const privateKey = open('../.keys/key.pem'); + +export class JWTProfileRequest implements TokenRequest { + keyPayload!: { + userId: string; + expiration: number; + keyId: string; + }; + + constructor(userId: string, keyId: string) { + this.keyPayload = { + userId: userId, + // 1 minute + expiration: 60*1_000_000_000, + keyId: keyId, + }; + } + + payload(): RequestBody{ + const assertion = zitadel.signJWTProfileAssertion( + this.keyPayload.userId, + this.keyPayload.keyId, + { + audience: [Config.host], + expiration: this.keyPayload.expiration, + key: privateKey + }); + return { + 'grant_type': 'urn:ietf:params:oauth:grant-type:jwt-bearer', + scope: 'openid', + assertion: `${assertion}` + }; + }; + public headers(): { [name: string]: string; } { + return { + 'Content-Type': 'application/x-www-form-urlencoded' + }; + }; +} + +const tokenDurationTrend = new Trend('oidc_token_duration', true); +export async function token(request: TokenRequest): Promise { + return http.asyncRequest('POST', configuration().token_endpoint, + request.payload(), + { + headers: request.headers(), + }, + ).then((res) => { + tokenDurationTrend.add(res.timings.duration); + check(res, { + 'token status ok': (r) => r.status === 200, + 'access token returned': (r) => r.json('access_token')! != undefined && r.json('access_token')! != '', + }); + return new Tokens(res.json() as JSONObject); + }); +}; \ No newline at end of file diff --git a/load-test/src/use_cases/machine_jwt_profile_grant.ts b/load-test/src/use_cases/machine_jwt_profile_grant.ts new file mode 100644 index 0000000000..084ac4f684 --- /dev/null +++ b/load-test/src/use_cases/machine_jwt_profile_grant.ts @@ -0,0 +1,57 @@ +import { loginByUsernamePassword } from '../login_ui'; +import { createOrg, removeOrg } from '../org'; +import {createMachine, User, addMachineKey} from '../user'; +import {JWTProfileRequest, token, userinfo} from '../oidc'; +import { Config, MaxVUs } from '../config'; +import encoding from 'k6/encoding'; + +const publicKey = encoding.b64encode(open('../.keys/key.pem.pub')); + +export async function setup() { + const tokens = loginByUsernamePassword(Config.admin as User); + console.info('setup: admin signed in'); + + const org = await createOrg(tokens.accessToken!); + console.info(`setup: org (${org.organizationId}) created`); + + let machines = ( + await Promise.all( + Array.from({ length: MaxVUs() }, (_, i) => { + return createMachine(`zitachine-${i}`, org, tokens.accessToken!); + }), + ) + ).map((machine) => { + return { userId: machine.userId, loginName: machine.loginNames[0] }; + }); + console.info(`setup: ${machines.length} machines created`); + + let keys = ( + await Promise.all( + machines.map((machine) => { + return addMachineKey( + machine.userId, + org, + tokens.accessToken!, + publicKey, + ); + }), + ) + ).map((key, i) => { + return { userId: machines[i].userId, keyId: key.keyId }; + }); + console.info(`setup: ${keys.length} keys added`); + + return { tokens, machines: keys, org }; +} + +export default function (data: any) { + token(new JWTProfileRequest(data.machines[__VU - 1].userId, data.machines[__VU - 1].keyId)) + .then((token) => { + userinfo(token.accessToken!) + }) +} + +export function teardown(data: any) { + removeOrg(data.org, data.tokens.accessToken); + console.info('teardown: org removed'); +} diff --git a/load-test/src/user.ts b/load-test/src/user.ts index 21214b16ab..6402be2034 100644 --- a/load-test/src/user.ts +++ b/load-test/src/user.ts @@ -197,6 +197,38 @@ export function addMachineSecret(userId: string, org: Org, accessToken: string): }); } +export type MachineKey = { + keyId: string; +}; + +const addMachineKeyTrend = new Trend('user_add_machine_key_duration', true); +export function addMachineKey(userId: string, org: Org, accessToken: string, publicKey?: string): Promise { + return new Promise((resolve, reject) => { + let response = http.asyncRequest('POST', url(`/management/v1/users/${userId}/keys`), + JSON.stringify({ + type: 'KEY_TYPE_JSON', + userId: userId, + // base64 encoded public key + publicKey: publicKey + }), + { + headers: { + authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + 'x-zitadel-orgid': org.organizationId, + }, + }); + response.then((res) => { + check(res, { + 'generate machine key status ok': (r) => r.status === 200, + }) || reject(`unable to generate machine Key (user id: ${userId}) status: ${res.status} body: ${res.body}`); + + addMachineKeyTrend.add(res.timings.duration); + resolve(res.json()! as MachineKey); + }); + }); +} + const lockUserTrend = new Trend('lock_user_duration', true); export function lockUser(userId: string, org: Org, accessToken: string): Promise> { return new Promise((resolve, reject) => { diff --git a/load-test/webpack.config.js b/load-test/webpack.config.js index 4d5a4c6c4c..580e891425 100644 --- a/load-test/webpack.config.js +++ b/load-test/webpack.config.js @@ -32,14 +32,6 @@ module.exports = { }, plugins: [ new CleanWebpackPlugin(), - // Copy assets to the destination folder - // see `src/post-file-test.ts` for an test example using an asset - new CopyPlugin({ - patterns: [{ - from: path.resolve(__dirname, 'assets'), - noErrorOnMissing: true - }], - }), ], optimization: { // Don't minimize, as it's not used in the browser diff --git a/proto/zitadel/admin.proto b/proto/zitadel/admin.proto index 9fe6ff6434..e6f098c2a2 100644 --- a/proto/zitadel/admin.proto +++ b/proto/zitadel/admin.proto @@ -7829,7 +7829,8 @@ message SetCustomLoginTextsRequest { zitadel.text.v1.PasswordlessRegistrationScreenText passwordless_registration_text = 33; zitadel.text.v1.PasswordlessRegistrationDoneScreenText passwordless_registration_done_text = 34; zitadel.text.v1.ExternalRegistrationUserOverviewScreenText external_registration_user_overview_text = 35; - zitadel.text.v1.LinkingUserPromptScreenText linking_user_prompt_text = 36; + // Deprecated: the linking user prompt screen no longer exists + zitadel.text.v1.LinkingUserPromptScreenText linking_user_prompt_text = 36 [deprecated = true]; } message SetCustomLoginTextsResponse { diff --git a/proto/zitadel/feature/v2/instance.proto b/proto/zitadel/feature/v2/instance.proto index 24c6df5db6..159c639908 100644 --- a/proto/zitadel/feature/v2/instance.proto +++ b/proto/zitadel/feature/v2/instance.proto @@ -65,6 +65,13 @@ message SetInstanceFeaturesRequest{ 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"; + description: "Return parent errors to OIDC clients for debugging purposes. Parent errors may contain sensitive data or unwanted details about the system status of zitadel. Only enable if really needed."; + } + ]; } message SetInstanceFeaturesResponse { @@ -143,4 +150,11 @@ message GetInstanceFeaturesResponse { 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"; + description: "Return parent errors to OIDC clients for debugging purposes. Parent errors may contain sensitive data or unwanted details about the system status of zitadel. Only enable if really needed."; + } + ]; } diff --git a/proto/zitadel/feature/v2beta/instance.proto b/proto/zitadel/feature/v2beta/instance.proto index 33d00af3eb..2059abf17e 100644 --- a/proto/zitadel/feature/v2beta/instance.proto +++ b/proto/zitadel/feature/v2beta/instance.proto @@ -65,6 +65,13 @@ message SetInstanceFeaturesRequest{ 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"; + description: "Return parent errors to OIDC clients for debugging purposes. Parent errors may contain sensitive data or unwanted details about the system status of zitadel. Only enable if really needed."; + } + ]; } message SetInstanceFeaturesResponse { @@ -143,4 +150,11 @@ message GetInstanceFeaturesResponse { 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"; + description: "Return parent errors to OIDC clients for debugging purposes. Parent errors may contain sensitive data or unwanted details about the system status of zitadel. Only enable if really needed."; + } + ]; } diff --git a/proto/zitadel/management.proto b/proto/zitadel/management.proto index e02e23da6b..7b57065a48 100644 --- a/proto/zitadel/management.proto +++ b/proto/zitadel/management.proto @@ -11299,7 +11299,8 @@ message SetCustomLoginTextsRequest { zitadel.text.v1.PasswordlessRegistrationScreenText passwordless_registration_text = 33; zitadel.text.v1.PasswordlessRegistrationDoneScreenText passwordless_registration_done_text = 34; zitadel.text.v1.ExternalRegistrationUserOverviewScreenText external_registration_user_overview_text = 35; - zitadel.text.v1.LinkingUserPromptScreenText linking_user_prompt_text = 36; + // Deprecated: the linking user prompt screen no longer exists + zitadel.text.v1.LinkingUserPromptScreenText linking_user_prompt_text = 36 [deprecated = true]; } message SetCustomLoginTextsResponse { diff --git a/proto/zitadel/object/v3alpha/object.proto b/proto/zitadel/object/v3alpha/object.proto index 5c067ddc1a..a3a2d2325a 100644 --- a/proto/zitadel/object/v3alpha/object.proto +++ b/proto/zitadel/object/v3alpha/object.proto @@ -27,3 +27,11 @@ message Instance { string domain = 2; } } + +message Organization { + oneof property { + option (validate.required) = true; + string org_id = 1; + string org_domain = 2; + } +} \ No newline at end of file diff --git a/proto/zitadel/resources/action/v3alpha/action_service.proto b/proto/zitadel/resources/action/v3alpha/action_service.proto index 228899310a..1254dddf86 100644 --- a/proto/zitadel/resources/action/v3alpha/action_service.proto +++ b/proto/zitadel/resources/action/v3alpha/action_service.proto @@ -17,7 +17,6 @@ import "zitadel/resources/action/v3alpha/query.proto"; import "zitadel/resources/object/v3alpha/object.proto"; import "zitadel/object/v3alpha/object.proto"; - option go_package = "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha;action"; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { diff --git a/proto/zitadel/user/v3alpha/authenticator.proto b/proto/zitadel/resources/user/v3alpha/authenticator.proto similarity index 84% rename from proto/zitadel/user/v3alpha/authenticator.proto rename to proto/zitadel/resources/user/v3alpha/authenticator.proto index 7527bcdb69..827f67c331 100644 --- a/proto/zitadel/user/v3alpha/authenticator.proto +++ b/proto/zitadel/resources/user/v3alpha/authenticator.proto @@ -1,15 +1,15 @@ syntax = "proto3"; -package zitadel.user.v3alpha; +package zitadel.resources.user.v3alpha; import "google/api/field_behavior.proto"; import "google/protobuf/struct.proto"; import "google/protobuf/timestamp.proto"; import "protoc-gen-openapiv2/options/annotations.proto"; import "validate/validate.proto"; -import "zitadel/object/v2/object.proto"; +import "zitadel/resources/object/v3alpha/object.proto"; -option go_package = "github.com/zitadel/zitadel/pkg/grpc/user/v3alpha"; +option go_package = "github.com/zitadel/zitadel/pkg/grpc/resources/user/v3alpha;user"; message Authenticators { // All of the user's usernames, which will be used for identification during authentication. @@ -109,6 +109,50 @@ message WebAuthN { bool user_verified = 4; } +message StartWebAuthNRegistration { + // Domain on which the user currently is or will be authenticated. + string domain = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1, + max_length: 200, + example: "\"my-domain.zitadel.cloud\""; + } + ]; + // Optionally specify the authenticator type of the passkey device (platform or cross-platform). + // If none is provided, both values are allowed. + WebAuthNAuthenticatorType authenticator_type = 2; + // Optionally provide a one time code generated by ZITADEL. + // This is required to start the passkey registration without user authentication. + optional AuthenticatorRegistrationCode code = 3; +} + +message VerifyWebAuthNRegistration { + // PublicKeyCredential Interface. + // Generated helper methods populate the field from JSON created by a WebAuthN client. + // See also: https://www.w3.org/TR/webauthn/#publickeycredential + google.protobuf.Struct public_key_credential = 1 [ + (validate.rules).message.required = true, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "{\"type\":\"public-key\",\"id\":\"pawVarF4xPxLFmfCnRkwXWeTrKGzabcAi92LEI1WC00\",\"rawId\":\"pawVarF4xPxLFmfCnRkwXWeTrKGzabcAi92LEI1WC00\",\"response\":{\"attestationObject\":\"o2NmbXRmcGFja2VkZ2F0dFN0bXSiY2FsZyZjc2lnWEcwRQIgRKS3VpeE9tfExXRzkoUKnG4rQWPvtSSt4YtDGgTx32oCIQDPey-2YJ4uIg-QCM4jj6aE2U3tgMFM_RP7Efx6xRu3JGhhdXRoRGF0YVikSZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NFAAAAADju76085Yhmlt1CEOHkwLQAIKWsFWqxeMT8SxZnwp0ZMF1nk6yhs2m3AIvdixCNVgtNpQECAyYgASFYIMGUDSP2FAQn2MIfPMy7cyB_Y30VqixVgGULTBtFjfRiIlggjUGfQo3_-CrMmH3S-ZQkFKWKnNBQEAMkFtG-9A4zqW0\",\"clientDataJSON\":\"eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiQlhXdHh0WGxJeFZZa0pHT1dVaUVmM25zby02aXZKdWw2YmNmWHdMVlFIayIsIm9yaWdpbiI6Imh0dHBzOi8vbG9jYWxob3N0OjgwODAifQ\"}}"; + min_length: 55; + max_length: 1048576; //1 MB + } + ]; + // Provide a name for the WebAuthN device. This will help identify it in the future. + string web_auth_n_name = 2 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"fido key\"" + } + ]; +} + message OTPSMS { // unique identifier of the one-time-password (OTP) SMS authenticator. string otp_sms_id = 1 [ @@ -167,7 +211,7 @@ message AuthenticationKey { example: "\"69629023906488334\""; } ]; - zitadel.object.v2.Details details = 2; + zitadel.resources.object.v3alpha.Details details = 2; // the file type of the key AuthNKeyType type = 3 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -246,6 +290,30 @@ message SetPassword { } // Provide if the user needs to change the password on the next use. bool change_required = 3; + // If neither, the current password nor a verification code generated by the PasswordReset is provided, + // the user must be granted permission to set a password. + oneof verification { + // Provide the current password to verify you're allowed to change the password. + string current_password = 4 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"Secr3tP4ssw0rd!\""; + } + ]; + // Or provider the verification code generated during password reset request. + string verification_code = 5 [ + (validate.rules).string = {min_len: 1, max_len: 20}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 20; + example: "\"SKJd342k\""; + } + ]; + } } message SendPasswordResetEmail { diff --git a/proto/zitadel/user/v3alpha/communication.proto b/proto/zitadel/resources/user/v3alpha/communication.proto similarity index 96% rename from proto/zitadel/user/v3alpha/communication.proto rename to proto/zitadel/resources/user/v3alpha/communication.proto index af799f411d..b27379cf1a 100644 --- a/proto/zitadel/user/v3alpha/communication.proto +++ b/proto/zitadel/resources/user/v3alpha/communication.proto @@ -1,8 +1,8 @@ syntax = "proto3"; -package zitadel.user.v3alpha; +package zitadel.resources.user.v3alpha; -option go_package = "github.com/zitadel/zitadel/pkg/grpc/user/v3alpha"; +option go_package = "github.com/zitadel/zitadel/pkg/grpc/resources/user/v3alpha;user"; import "google/api/field_behavior.proto"; import "protoc-gen-openapiv2/options/annotations.proto"; diff --git a/proto/zitadel/user/v3alpha/query.proto b/proto/zitadel/resources/user/v3alpha/query.proto similarity index 69% rename from proto/zitadel/user/v3alpha/query.proto rename to proto/zitadel/resources/user/v3alpha/query.proto index 4e2bf062b1..4fffe517e2 100644 --- a/proto/zitadel/user/v3alpha/query.proto +++ b/proto/zitadel/resources/user/v3alpha/query.proto @@ -1,72 +1,71 @@ syntax = "proto3"; -package zitadel.user.v3alpha; +package zitadel.resources.user.v3alpha; -option go_package = "github.com/zitadel/zitadel/pkg/grpc/user/v3alpha"; +option go_package = "github.com/zitadel/zitadel/pkg/grpc/resources/user/v3alpha;user"; import "google/api/field_behavior.proto"; import "protoc-gen-openapiv2/options/annotations.proto"; import "validate/validate.proto"; -import "zitadel/user/v3alpha/user.proto"; -import "zitadel/object/v2/object.proto"; +import "zitadel/resources/user/v3alpha/user.proto"; +import "zitadel/resources/object/v3alpha/object.proto"; -message SearchQuery { - oneof query { +message SearchFilter { + oneof Filter { option (validate.required) = true; - - // Union the results of each sub query ('OR'). - OrQuery or_query = 1; + // Union the results of each sub filter ('OR'). + OrFilter or_filter = 1; // Limit the result to match all sub queries ('AND'). // Note that if you specify multiple queries, they will be implicitly used as andQueries. - // Use the andQuery in combination with orQuery and notQuery. - AndQuery and_query = 2; + // Use the andFilter in combination with orFilter and notFilter. + AndFilter and_filter = 2; // Exclude / Negate the result of the sub query ('NOT'). - NotQuery not_query = 3; + NotFilter not_filter = 3; // Limit the result to a specific user ID. - UserIDQuery user_id_query = 4; + UserIDFilter user_id_filter = 4; // Limit the result to a specific organization. - OrganizationIDQuery organization_id_query = 5; + OrganizationIDFilter organization_id_filter = 5; // Limit the result to a specific username. - UsernameQuery username_query = 6; + UsernameFilter username_filter = 6; // Limit the result to a specific contact email. - EmailQuery email_query = 7; + EmailFilter email_filter = 7; // Limit the result to a specific contact phone. - PhoneQuery phone_query = 8; + PhoneFilter phone_filter = 8; // Limit the result to a specific state of the user. - StateQuery state_query = 9; + StateFilter state_filter = 9; // Limit the result to a specific schema ID. - SchemaIDQuery schema_ID_query = 10; + SchemaIDFilter schema_id_filter = 10; // Limit the result to a specific schema type. - SchemaTypeQuery schema_type_query = 11; + SchemaTypeFilter schema_type_filter = 11; } } -message OrQuery { - repeated SearchQuery queries = 1 [ +message OrFilter { + repeated SearchFilter queries = 1 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "[{\"userIdQuery\": {\"id\": \"163840776835432705\",\"method\": \"TEXT_QUERY_METHOD_EQUALS\"}},{\"userIdQuery\": {\"id\": \"163840776835943483\",\"method\": \"TEXT_QUERY_METHOD_EQUALS\"}}]" + example: "[{\"userIdFilter\": {\"id\": \"163840776835432705\",\"method\": \"TEXT_FILTER_METHOD_EQUALS\"}},{\"userIdFilter\": {\"id\": \"163840776835943483\",\"method\": \"TEXT_FILTER_METHOD_EQUALS\"}}]" } ]; } -message AndQuery { - repeated SearchQuery queries = 1 [ +message AndFilter { + repeated SearchFilter queries = 1 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "[{\"organizationIdQuery\": {\"id\": \"163840776835432705\",\"method\": \"TEXT_QUERY_METHOD_EQUALS\"}},{\"usernameQuery\": {\"username\": \"gigi\",\"method\": \"TEXT_QUERY_METHOD_EQUALS\"}}]" + example: "[{\"organizationIdFilter\": {\"id\": \"163840776835432705\",\"method\": \"TEXT_FILTER_METHOD_EQUALS\"}},{\"usernameFilter\": {\"username\": \"gigi\",\"method\": \"TEXT_FILTER_METHOD_EQUALS\"}}]" } ]; } -message NotQuery { - SearchQuery query = 1 [ +message NotFilter { + SearchFilter query = 1 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "{\"schemaIDQuery\": {\"id\": \"163840776835432705\"}}" + example: "{\"schemaIDFilter\": {\"id\": \"163840776835432705\"}}" } ]; } -message UserIDQuery { +message UserIDFilter { // Defines the ID of the user to query for. string id = 1 [ (validate.rules).string = {min_len: 1, max_len: 200}, @@ -78,12 +77,12 @@ message UserIDQuery { } ]; // Defines which text comparison method used for the id query. - zitadel.object.v2.TextQueryMethod method = 2 [ + zitadel.resources.object.v3alpha.TextFilterMethod method = 2 [ (validate.rules).enum.defined_only = true ]; } -message OrganizationIDQuery { +message OrganizationIDFilter { // Defines the ID of the organization to query for. string id = 1 [ (validate.rules).string = {min_len: 1, max_len: 200}, @@ -95,12 +94,12 @@ message OrganizationIDQuery { } ]; // Defines which text comparison method used for the id query. - zitadel.object.v2.TextQueryMethod method = 2 [ + zitadel.resources.object.v3alpha.TextFilterMethod method = 2 [ (validate.rules).enum.defined_only = true ]; } -message UsernameQuery { +message UsernameFilter { // Defines the username to query for. string username = 1 [ (validate.rules).string = {min_len: 1, max_len: 200}, @@ -112,14 +111,14 @@ message UsernameQuery { } ]; // Defines which text comparison method used for the username query. - zitadel.object.v2.TextQueryMethod method = 2 [ + zitadel.resources.object.v3alpha.TextFilterMethod method = 2 [ (validate.rules).enum.defined_only = true ]; // Defines that the username must only be unique in the organisation. bool is_organization_specific = 3; } -message EmailQuery { +message EmailFilter { // Defines the email of the user to query for. string address = 1 [ (validate.rules).string = {max_len: 200}, @@ -131,12 +130,12 @@ message EmailQuery { } ]; // Defines which text comparison method used for the email query. - zitadel.object.v2.TextQueryMethod method = 2 [ + zitadel.resources.object.v3alpha.TextFilterMethod method = 2 [ (validate.rules).enum.defined_only = true ]; } -message PhoneQuery { +message PhoneFilter { // Defines the phone of the user to query for. string number = 1 [ (validate.rules).string = {min_len: 1, max_len: 20}, @@ -148,13 +147,13 @@ message PhoneQuery { } ]; // Defines which text comparison method used for the phone query. - zitadel.object.v2.TextQueryMethod method = 2 [ + zitadel.resources.object.v3alpha.TextFilterMethod method = 2 [ (validate.rules).enum.defined_only = true ]; } -message StateQuery { +message StateFilter { // Defines the state to query for. State state = 1 [ (validate.rules).enum.defined_only = true, @@ -164,7 +163,7 @@ message StateQuery { ]; } -message SchemaIDQuery { +message SchemaIDFilter { // Defines the ID of the schema to query for. string id = 1 [ (validate.rules).string = {min_len: 1, max_len: 200}, @@ -177,7 +176,7 @@ message SchemaIDQuery { ]; } -message SchemaTypeQuery { +message SchemaTypeFilter { // Defines which type to query for. string type = 1 [ (validate.rules).string = {min_len: 1, max_len: 200}, @@ -189,7 +188,7 @@ message SchemaTypeQuery { } ]; // Defines which text comparison method used for the type query. - zitadel.object.v2.TextQueryMethod method = 2 [ + zitadel.resources.object.v3alpha.TextFilterMethod method = 2 [ (validate.rules).enum.defined_only = true ]; } diff --git a/proto/zitadel/resources/user/v3alpha/user.proto b/proto/zitadel/resources/user/v3alpha/user.proto new file mode 100644 index 0000000000..1be5f3c3f3 --- /dev/null +++ b/proto/zitadel/resources/user/v3alpha/user.proto @@ -0,0 +1,112 @@ +syntax = "proto3"; + +package zitadel.resources.user.v3alpha; + +import "google/api/field_behavior.proto"; +import "google/protobuf/struct.proto"; +import "google/protobuf/timestamp.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; +import "validate/validate.proto"; +import "zitadel/resources/object/v3alpha/object.proto"; +import "zitadel/resources/user/v3alpha/authenticator.proto"; +import "zitadel/resources/user/v3alpha/communication.proto"; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/resources/user/v3alpha;user"; + +message CreateUser { + // Define the schema the user's data schema by providing it's ID. + string schema_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1, + max_length: 200, + example: "\"69629026806489455\""; + } + ]; + google.protobuf.Struct data = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "{\"name\":\"Gigi\",\"description\":\"the giraffe\"}" + } + ]; + // Set the contact information (email, phone) for the user. + SetContact contact = 3; + // Set the initial authenticators of the user. + SetAuthenticators authenticators = 4; + // Optionally set a unique identifier of the user. If unset, ZITADEL will take care of it. + optional string user_id = 5 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1, + max_length: 200, + example: "\"69629026806489455\""; + } + ]; +} + +message PatchUser { + optional string schema_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1, + max_length: 200, + example: "\"69629026806489455\""; + } + ]; + optional google.protobuf.Struct data = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "{\"name\":\"Gigi\",\"description\":\"the giraffe\"}" + } + ]; + // Set the contact information (email, phone) for the user. + optional SetContact contact = 3; + // TODO: No SetAuthenticators? +} + +message GetUser{ + // Details provide some base information (such as the last change date) of the user. + zitadel.resources.object.v3alpha.Details details = 1; + // The schema the user and it's data is based on. + GetSchema schema = 2; + google.protobuf.Struct data = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "{\"name\":\"Gigi\",\"description\":\"the giraffe\"}" + } + ]; + // Contact information for the user. ZITADEL will use this in case of internal notifications. + Contact contact = 4; + // The user's authenticators. They are used to identify and authenticate the user + // during the authentication process. + Authenticators authenticators = 5; + // State of the user. + State state = 6; +} + +enum State { + USER_STATE_UNSPECIFIED = 0; + USER_STATE_ACTIVE = 1; + USER_STATE_INACTIVE = 2; + USER_STATE_DELETED = 3; + USER_STATE_LOCKED = 4; +} + +message GetSchema { + // The unique identifier of the user schema. + string id = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629026806489455\"" + } + ]; + // The human readable name of the user schema. + string type = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"employees\""; + } + ]; + // The revision the user's data is based on of the revision. + uint32 revision = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "7"; + } + ]; +} diff --git a/proto/zitadel/user/v3alpha/user_service.proto b/proto/zitadel/resources/user/v3alpha/user_service.proto similarity index 74% rename from proto/zitadel/user/v3alpha/user_service.proto rename to proto/zitadel/resources/user/v3alpha/user_service.proto index b193cc52cb..e99f28af05 100644 --- a/proto/zitadel/user/v3alpha/user_service.proto +++ b/proto/zitadel/resources/user/v3alpha/user_service.proto @@ -1,6 +1,6 @@ syntax = "proto3"; -package zitadel.user.v3alpha; +package zitadel.resources.user.v3alpha; import "google/api/annotations.proto"; import "google/api/field_behavior.proto"; @@ -8,20 +8,20 @@ import "google/protobuf/duration.proto"; import "google/protobuf/struct.proto"; import "protoc-gen-openapiv2/options/annotations.proto"; import "validate/validate.proto"; -import "zitadel/object/v2/object.proto"; +import "zitadel/object/v3alpha/object.proto"; +import "zitadel/resources/object/v3alpha/object.proto"; import "zitadel/protoc_gen_zitadel/v2/options.proto"; -import "zitadel/user/v3alpha/authenticator.proto"; -import "zitadel/user/v3alpha/communication.proto"; -import "zitadel/user/v3alpha/query.proto"; -import "zitadel/user/v3alpha/user.proto"; - -option go_package = "github.com/zitadel/zitadel/pkg/grpc/user/v3alpha"; +import "zitadel/resources/user/v3alpha/authenticator.proto"; +import "zitadel/resources/user/v3alpha/communication.proto"; +import "zitadel/resources/user/v3alpha/query.proto"; +import "zitadel/resources/user/v3alpha/user.proto"; +option go_package = "github.com/zitadel/zitadel/pkg/grpc/resources/user/v3alpha;user"; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { info: { title: "User Service"; - version: "3.0-preview"; + version: "3.0-alpha"; description: "This API is intended to manage users with your own data schema in a ZITADEL instance. This project is in preview state. It can AND will continue breaking until the service provides the same functionality as the v1 and v2 user services."; contact:{ name: "ZITADEL" @@ -46,7 +46,7 @@ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { produces: "application/grpc-web+proto"; host: "$CUSTOM-DOMAIN"; - base_path: "/"; + base_path: "/resources/v3alpha/users"; external_docs: { description: "Detailed information about ZITADEL", @@ -106,16 +106,16 @@ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { } }; -service UserService { +service ZITADELUsers { - // List users + // Search users // - // List all matching users. By default, we will return all users of your instance. + // Search all matching users. By default, we will return all users of your instance. // Make sure to include a limit and sorting for pagination. - rpc ListUsers (ListUsersRequest) returns (ListUsersResponse) { + rpc SearchUsers (SearchUsersRequest) returns (SearchUsersResponse) { option (google.api.http) = { - post: "/v3alpha/users/search" - body: "*" + post: "/_search" + body: "filters" }; option (zitadel.protoc_gen_zitadel.v2.options) = { @@ -148,9 +148,9 @@ service UserService { // User by ID // // Returns the user identified by the requested ID. - rpc GetUserByID (GetUserByIDRequest) returns (GetUserByIDResponse) { + rpc GetUser (GetUserRequest) returns (GetUserResponse) { option (google.api.http) = { - get: "/v3alpha/users/{user_id}" + get: "/{user_id}" }; option (zitadel.protoc_gen_zitadel.v2.options) = { @@ -174,13 +174,13 @@ service UserService { // Create a new user with an optional data schema. rpc CreateUser (CreateUserRequest) returns (CreateUserResponse) { option (google.api.http) = { - post: "/v3alpha/users" - body: "*" + post: "/" + body: "user" }; option (zitadel.protoc_gen_zitadel.v2.options) = { auth_option: { - permission: "user.write" + permission: "authenticated" org_field: "organization" } http_response: { @@ -203,13 +203,13 @@ service UserService { }; } - // Update a user + // Patch a user // - // Update an existing user with data based on a user schema. - rpc UpdateUser (UpdateUserRequest) returns (UpdateUserResponse) { + // Patch an existing user with data based on a user schema. + rpc PatchUser (PatchUserRequest) returns (PatchUserResponse) { option (google.api.http) = { - put: "/v3alpha/users/{user_id}" - body: "*" + patch: "/{user_id}" + body: "user" }; option (zitadel.protoc_gen_zitadel.v2.options) = { @@ -222,7 +222,7 @@ service UserService { responses: { key: "200"; value: { - description: "User successfully updated"; + description: "User successfully updated or left unchanged"; }; }; }; @@ -238,7 +238,7 @@ service UserService { // The endpoint returns an error if the user is already in the state 'deactivated'. rpc DeactivateUser (DeactivateUserRequest) returns (DeactivateUserResponse) { option (google.api.http) = { - post: "/v3alpha/users/{user_id}/deactivate" + post: "/{user_id}/_deactivate" }; option (zitadel.protoc_gen_zitadel.v2.options) = { @@ -265,7 +265,7 @@ service UserService { // The endpoint returns an error if the user is not in the state 'deactivated'. rpc ReactivateUser (ReactivateUserRequest) returns (ReactivateUserResponse) { option (google.api.http) = { - post: "/v3alpha/users/{user_id}/reactivate" + post: "/{user_id}/_reactivate" }; option (zitadel.protoc_gen_zitadel.v2.options) = { @@ -294,7 +294,7 @@ service UserService { // The endpoint returns an error if the user is already in the state 'locked'. rpc LockUser (LockUserRequest) returns (LockUserResponse) { option (google.api.http) = { - post: "/v3alpha/users/{user_id}/lock" + post: "/{user_id}/_lock" }; option (zitadel.protoc_gen_zitadel.v2.options) = { @@ -321,7 +321,7 @@ service UserService { // The endpoint returns an error if the user is not in the state 'locked'. rpc UnlockUser (UnlockUserRequest) returns (UnlockUserResponse) { option (google.api.http) = { - post: "/v3alpha/users/{user_id}/unlock" + post: "/{user_id}/_unlock" }; option (zitadel.protoc_gen_zitadel.v2.options) = { @@ -346,7 +346,7 @@ service UserService { // The user will be able to log in anymore. rpc DeleteUser (DeleteUserRequest) returns (DeleteUserResponse) { option (google.api.http) = { - delete: "/v3alpha/users/{user_id}" + delete: "/{user_id}" }; option (zitadel.protoc_gen_zitadel.v2.options) = { @@ -372,8 +372,8 @@ service UserService { // which can be either returned or will be sent to the user by email. rpc SetContactEmail (SetContactEmailRequest) returns (SetContactEmailResponse) { option (google.api.http) = { - put: "/v3alpha/users/{user_id}/email" - body: "*" + put: "/{user_id}/email" + body: "email" }; option (zitadel.protoc_gen_zitadel.v2.options) = { @@ -397,8 +397,8 @@ service UserService { // Verify the contact email with the provided code. rpc VerifyContactEmail (VerifyContactEmailRequest) returns (VerifyContactEmailResponse) { option (google.api.http) = { - post: "/v3alpha/users/{user_id}/email/verify" - body: "*" + post: "/{user_id}/email/_verify" + body: "verification_code" }; option (zitadel.protoc_gen_zitadel.v2.options) = { @@ -422,7 +422,7 @@ service UserService { // Resend the email with the verification code for the contact email address. rpc ResendContactEmailCode (ResendContactEmailCodeRequest) returns (ResendContactEmailCodeResponse) { option (google.api.http) = { - post: "/v3alpha/users/{user_id}/email/resend" + post: "/{user_id}/email/_resend" body: "*" }; @@ -449,8 +449,8 @@ service UserService { // which can be either returned or will be sent to the user by SMS. rpc SetContactPhone (SetContactPhoneRequest) returns (SetContactPhoneResponse) { option (google.api.http) = { - put: "/v3alpha/users/{user_id}/phone" - body: "*" + put: "/{user_id}/phone" + body: "phone" }; option (zitadel.protoc_gen_zitadel.v2.options) = { @@ -474,8 +474,8 @@ service UserService { // Verify the contact phone with the provided code. rpc VerifyContactPhone (VerifyContactPhoneRequest) returns (VerifyContactPhoneResponse) { option (google.api.http) = { - post: "/v3alpha/users/{user_id}/phone/verify" - body: "*" + post: "/{user_id}/phone/_verify" + body: "verification_code" }; option (zitadel.protoc_gen_zitadel.v2.options) = { @@ -499,7 +499,7 @@ service UserService { // Resend the phone with the verification code for the contact phone number. rpc ResendContactPhoneCode (ResendContactPhoneCodeRequest) returns (ResendContactPhoneCodeResponse) { option (google.api.http) = { - post: "/v3alpha/users/{user_id}/phone/resend" + post: "/{user_id}/phone/_resend" body: "*" }; @@ -524,8 +524,8 @@ service UserService { // Add a new unique username to a user. The username will be used to identify the user on authentication. rpc AddUsername (AddUsernameRequest) returns (AddUsernameResponse) { option (google.api.http) = { - post: "/v3alpha/users/{user_id}/username" - body: "*" + post: "/{user_id}/username" + body: "username" }; option (zitadel.protoc_gen_zitadel.v2.options) = { @@ -549,7 +549,7 @@ service UserService { // Remove an existing username of a user, so it cannot be used for authentication anymore. rpc RemoveUsername (RemoveUsernameRequest) returns (RemoveUsernameResponse) { option (google.api.http) = { - delete: "/v3alpha/users/{user_id}/username/{username_id}" + delete: "/{user_id}/username/{username_id}" }; option (zitadel.protoc_gen_zitadel.v2.options) = { @@ -573,8 +573,8 @@ service UserService { // Add, update or reset a user's password with either a verification code or the current password. rpc SetPassword (SetPasswordRequest) returns (SetPasswordResponse) { option (google.api.http) = { - post: "/v2/users/{user_id}/password" - body: "*" + post: "/{user_id}/password" + body: "new_password" }; option (zitadel.protoc_gen_zitadel.v2.options) = { @@ -598,7 +598,7 @@ service UserService { // Request a code to be able to set a new password. rpc RequestPasswordReset (RequestPasswordResetRequest) returns (RequestPasswordResetResponse) { option (google.api.http) = { - post: "/v2/users/{user_id}/password/reset" + post: "/{user_id}/password/_reset" body: "*" }; @@ -625,8 +625,8 @@ service UserService { // which are used to verify the device. rpc StartWebAuthNRegistration (StartWebAuthNRegistrationRequest) returns (StartWebAuthNRegistrationResponse) { option (google.api.http) = { - post: "/v3alpha/users/{user_id}/webauthn" - body: "*" + post: "/{user_id}/webauthn" + body: "registration" }; option (zitadel.protoc_gen_zitadel.v2.options) = { @@ -650,8 +650,8 @@ service UserService { // Verify the WebAuthN registration started by StartWebAuthNRegistration with the public key credential. rpc VerifyWebAuthNRegistration (VerifyWebAuthNRegistrationRequest) returns (VerifyWebAuthNRegistrationResponse) { option (google.api.http) = { - post: "/v3alpha/users/{user_id}/webauthn/{web_auth_n_id}" - body: "*" + post: "/{user_id}/webauthn/{web_auth_n_id}/_verify" + body: "verify" }; option (zitadel.protoc_gen_zitadel.v2.options) = { @@ -675,7 +675,7 @@ service UserService { // The code will allow the user to start a new WebAuthN registration. rpc CreateWebAuthNRegistrationLink (CreateWebAuthNRegistrationLinkRequest) returns (CreateWebAuthNRegistrationLinkResponse) { option (google.api.http) = { - post: "/v3alpha/users/{user_id}/webauthn/registration_link" + post: "/{user_id}/webauthn/registration_link" body: "*" }; @@ -699,7 +699,7 @@ service UserService { // Remove an existing WebAuthN authenticator from a user, so it cannot be used for authentication anymore. rpc RemoveWebAuthNAuthenticator (RemoveWebAuthNAuthenticatorRequest) returns (RemoveWebAuthNAuthenticatorResponse) { option (google.api.http) = { - delete: "/v3alpha/users/{user_id}/webauthn/{web_auth_n_id}" + delete: "/{user_id}/webauthn/{web_auth_n_id}" }; option (zitadel.protoc_gen_zitadel.v2.options) = { @@ -723,7 +723,7 @@ service UserService { // As a response a secret is returned, which is used to initialize a TOTP app or device. rpc StartTOTPRegistration (StartTOTPRegistrationRequest) returns (StartTOTPRegistrationResponse) { option (google.api.http) = { - post: "/v3alpha/users/{user_id}/totp" + post: "/{user_id}/totp" }; option (zitadel.protoc_gen_zitadel.v2.options) = { @@ -746,8 +746,8 @@ service UserService { // Verify the time-based one-time-password (TOTP) registration with the generated code. rpc VerifyTOTPRegistration (VerifyTOTPRegistrationRequest) returns (VerifyTOTPRegistrationResponse) { option (google.api.http) = { - post: "/v3alpha/users/{user_id}/totp/{totp_id}/verify" - body: "*" + post: "/{user_id}/totp/{totp_id}/_verify" + body: "code" }; option (zitadel.protoc_gen_zitadel.v2.options) = { @@ -770,7 +770,7 @@ service UserService { // Remove an existing time-based one-time-password (TOTP) authenticator from a user, so it cannot be used for authentication anymore. rpc RemoveTOTPAuthenticator (RemoveTOTPAuthenticatorRequest) returns (RemoveTOTPAuthenticatorResponse) { option (google.api.http) = { - delete: "/v3alpha/users/{user_id}/totp/{totp_id}" + delete: "/{user_id}/totp/{totp_id}" }; option (zitadel.protoc_gen_zitadel.v2.options) = { @@ -795,8 +795,8 @@ service UserService { // which can be either returned or will be sent to the user by SMS. rpc AddOTPSMSAuthenticator (AddOTPSMSAuthenticatorRequest) returns (AddOTPSMSAuthenticatorResponse) { option (google.api.http) = { - post: "/v3alpha/users/{user_id}/otp_sms" - body: "*" + post: "/{user_id}/otp_sms" + body: "phone" }; option (zitadel.protoc_gen_zitadel.v2.options) = { @@ -819,8 +819,8 @@ service UserService { // Verify the OTP SMS registration with the provided code. rpc VerifyOTPSMSRegistration (VerifyOTPSMSRegistrationRequest) returns (VerifyOTPSMSRegistrationResponse) { option (google.api.http) = { - post: "/v3alpha/users/{user_id}/otp_sms/{otp_sms_id}/verify" - body: "*" + post: "/{user_id}/otp_sms/{otp_sms_id}/_verify" + body: "code" }; option (zitadel.protoc_gen_zitadel.v2.options) = { @@ -844,7 +844,7 @@ service UserService { // Remove an existing one-time-password (OTP) SMS authenticator from a user, so it cannot be used for authentication anymore. rpc RemoveOTPSMSAuthenticator (RemoveOTPSMSAuthenticatorRequest) returns (RemoveOTPSMSAuthenticatorResponse) { option (google.api.http) = { - delete: "/v3alpha/users/{user_id}/otp_sms/{otp_sms_id}" + delete: "/{user_id}/otp_sms/{otp_sms_id}" }; option (zitadel.protoc_gen_zitadel.v2.options) = { @@ -869,8 +869,8 @@ service UserService { // which can be either returned or will be sent to the user by email. rpc AddOTPEmailAuthenticator (AddOTPEmailAuthenticatorRequest) returns (AddOTPEmailAuthenticatorResponse) { option (google.api.http) = { - post: "/v3alpha/users/{user_id}/otp_email" - body: "*" + post: "/{user_id}/otp_email" + body: "email" }; option (zitadel.protoc_gen_zitadel.v2.options) = { @@ -893,8 +893,8 @@ service UserService { // Verify the OTP Email registration with the provided code. rpc VerifyOTPEmailRegistration (VerifyOTPEmailRegistrationRequest) returns (VerifyOTPEmailRegistrationResponse) { option (google.api.http) = { - post: "/v3alpha/users/{user_id}/otp_email/{otp_email_id}/verify" - body: "*" + post: "/{user_id}/otp_email/{otp_email_id}/_verify" + body: "code" }; option (zitadel.protoc_gen_zitadel.v2.options) = { @@ -918,7 +918,7 @@ service UserService { // Remove an existing one-time-password (OTP) Email authenticator from a user, so it cannot be used for authentication anymore. rpc RemoveOTPEmailAuthenticator (RemoveOTPEmailAuthenticatorRequest) returns (RemoveOTPEmailAuthenticatorResponse) { option (google.api.http) = { - delete: "/v3alpha/users/{user_id}/otp_email/{otp_email_id}" + delete: "/{user_id}/otp_email/{otp_email_id}" }; option (zitadel.protoc_gen_zitadel.v2.options) = { @@ -961,13 +961,12 @@ service UserService { }; } - // Retrieve the information of the IDP authentication intent + // Get the information of the IDP authentication intent // - // Retrieve the information returned by the identity provider (IDP) for registration or updating an existing user with new information. - rpc RetrieveIdentityProviderIntent (RetrieveIdentityProviderIntentRequest) returns (RetrieveIdentityProviderIntentResponse) { + // Get the information returned by the identity provider (IDP) for registration or updating an existing user with new information. + rpc GetIdentityProviderIntent (GetIdentityProviderIntentRequest) returns (GetIdentityProviderIntentResponse) { option (google.api.http) = { - post: "/v3alpha/idp_intents/{idp_intent_id}" - body: "*" + get: "/v3alpha/idp_intents/{idp_intent_id}" }; option (zitadel.protoc_gen_zitadel.v2.options) = { @@ -992,8 +991,8 @@ service UserService { // This will allow the user to authenticate with the provided IDP. rpc AddIDPAuthenticator (AddIDPAuthenticatorRequest) returns (AddIDPAuthenticatorResponse) { option (google.api.http) = { - post: "/v3alpha/users/{user_id}/idps" - body: "*" + post: "/{user_id}/idps" + body: "authenticator" }; option (zitadel.protoc_gen_zitadel.v2.options) = { @@ -1017,7 +1016,7 @@ service UserService { // Remove an existing identity provider (IDP) authenticator from a user, so it cannot be used for authentication anymore. rpc RemoveIDPAuthenticator (RemoveIDPAuthenticatorRequest) returns (RemoveIDPAuthenticatorResponse) { option (google.api.http) = { - delete: "/v3alpha/users/{user_id}/idps/{idp_id}" + delete: "/{user_id}/idps/{idp_id}" }; option (zitadel.protoc_gen_zitadel.v2.options) = { @@ -1037,31 +1036,40 @@ service UserService { } -message ListUsersRequest { - // list limitations and ordering. - zitadel.object.v2.ListQuery query = 1; +message SearchUsersRequest { + optional zitadel.object.v3alpha.Instance instance = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"domain from HOST or :authority header\"" + } + ]; + + // Search limitations and ordering. + zitadel.resources.object.v3alpha.SearchQuery query = 2; // the field the result is sorted. - zitadel.user.v3alpha.FieldName sorting_column = 2 [ + FieldName sorting_column = 3 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { example: "\"FIELD_NAME_SCHEMA_TYPE\"" } ]; // Define the criteria to query for. - repeated zitadel.user.v3alpha.SearchQuery queries = 3; + repeated SearchFilter filters = 4; } -message ListUsersResponse { +message SearchUsersResponse { // Details provides information about the returned result including total amount found. - zitadel.object.v2.ListDetails details = 1; - // States by which field the results are sorted. - zitadel.user.v3alpha.FieldName sorting_column = 2; + zitadel.resources.object.v3alpha.ListDetails details = 1; // The result contains the user schemas, which matched the queries. - repeated zitadel.user.v3alpha.User result = 3; + repeated GetUser result = 2; } -message GetUserByIDRequest { +message GetUserRequest { + optional zitadel.object.v3alpha.Instance instance = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"domain from HOST or :authority header\"" + } + ]; // unique identifier of the user. - string user_id = 1 [ + string user_id = 2 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1072,100 +1080,67 @@ message GetUserByIDRequest { ]; } -message GetUserByIDResponse { - zitadel.user.v3alpha.User user = 1; +message GetUserResponse { + GetUser user = 1; } message CreateUserRequest { - // Optionally set a unique identifier of the user. If unset, ZITADEL will take care of it. - optional string user_id = 1 [ - (validate.rules).string = {min_len: 1, max_len: 200}, + optional zitadel.object.v3alpha.Instance instance = 1 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - min_length: 1, - max_length: 200, - example: "\"69629026806489455\""; + default: "\"domain from HOST or :authority header\"" } ]; // Set the organization the user belongs to. - zitadel.object.v2.Organization organization = 2 [ + zitadel.object.v3alpha.Organization organization = 2 [ (validate.rules).message = {required: true}, (google.api.field_behavior) = REQUIRED ]; - // Set the initial authenticators of the user. - SetAuthenticators authenticators = 3; - // Set the contact information (email, phone) for the user. - SetContact contact = 4; - // Define the schema the user's data schema by providing it's ID. - string schema_id = 5 [ - (validate.rules).string = {min_len: 1, max_len: 200}, - (google.api.field_behavior) = REQUIRED, - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - min_length: 1, - max_length: 200, - example: "\"69629026806489455\""; - } - ]; - // Provide data about the user. It will be validated based on the specified schema. - google.protobuf.Struct data = 6 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "{\"name\":\"Gigi\",\"description\":\"the giraffe\"}" - } - ]; + CreateUser user = 3; } message CreateUserResponse { - string user_id = 1; - zitadel.object.v2.Details details = 2; + zitadel.resources.object.v3alpha.Details details = 1; // The email code will be set if a contact email was set with a return_code verification option. - optional string email_code = 3 [ + optional string email_code = 2 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { example: "\"SKJd342k\""; } ]; // The phone code will be set if a contact phone was set with a return_code verification option. - optional string phone_code = 4 [ + optional string phone_code = 3 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { example: "\"IFi39dk2\""; } ]; } -message UpdateUserRequest { +message PatchUserRequest { + optional zitadel.object.v3alpha.Instance instance = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"domain from HOST or :authority header\"" + } + ]; + // Optionally expect the user to be in this organization. + optional zitadel.object.v3alpha.Organization organization = 2; // unique identifier of the user. - string user_id = 1 [ + string user_id = 3 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { example: "\"69629012906488334\""; } ]; - // Add or update the contact information (email, phone) for the user if needed. - optional SetContact contact = 4; - // Change the schema the user's data schema by providing it's ID if needed. - optional string schema_id = 5 [ - (validate.rules).string = {min_len: 1, max_len: 200}, - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - min_length: 1, - max_length: 200, - example: "\"69629026806489455\""; - } - ]; - // Update the user data if needed. It will be validated based on the specified schema. - optional google.protobuf.Struct data = 6 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "{\"name\":\"Gigi\",\"description\":\"the giraffe\"}" - } - ]; + PatchUser user = 4; } -message UpdateUserResponse { - zitadel.object.v2.Details details = 1; +message PatchUserResponse { + zitadel.resources.object.v3alpha.Details details = 1; // The email code will be set if a contact email was set with a return_code verification option. - optional string email_code = 3 [ + optional string email_code = 2 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { example: "\"SKJd342k\""; } ]; // The phone code will be set if a contact phone was set with a return_code verification option. - optional string phone_code = 4 [ + optional string phone_code = 3 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { example: "\"IFi39dk2\""; } @@ -1173,8 +1148,15 @@ message UpdateUserResponse { } message DeactivateUserRequest { + optional zitadel.object.v3alpha.Instance instance = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"domain from HOST or :authority header\"" + } + ]; + // Optionally expect the user to be in this organization. + optional zitadel.object.v3alpha.Organization organization = 2; // unique identifier of the user. - string user_id = 1 [ + string user_id = 3 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1186,13 +1168,20 @@ message DeactivateUserRequest { } message DeactivateUserResponse { - zitadel.object.v2.Details details = 1; + zitadel.resources.object.v3alpha.Details details = 1; } message ReactivateUserRequest { + optional zitadel.object.v3alpha.Instance instance = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"domain from HOST or :authority header\"" + } + ]; + // Optionally expect the user to be in this organization. + optional zitadel.object.v3alpha.Organization organization = 2; // unique identifier of the user. - string user_id = 1 [ + string user_id = 3 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1204,12 +1193,19 @@ message ReactivateUserRequest { } message ReactivateUserResponse { - zitadel.object.v2.Details details = 1; + zitadel.resources.object.v3alpha.Details details = 1; } message LockUserRequest { + optional zitadel.object.v3alpha.Instance instance = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"domain from HOST or :authority header\"" + } + ]; + // Optionally expect the user to be in this organization. + optional zitadel.object.v3alpha.Organization organization = 2; // unique identifier of the user. - string user_id = 1 [ + string user_id = 3 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1221,12 +1217,19 @@ message LockUserRequest { } message LockUserResponse { - zitadel.object.v2.Details details = 1; + zitadel.resources.object.v3alpha.Details details = 1; } message UnlockUserRequest { + optional zitadel.object.v3alpha.Instance instance = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"domain from HOST or :authority header\"" + } + ]; + // Optionally expect the user to be in this organization. + optional zitadel.object.v3alpha.Organization organization = 2; // unique identifier of the user. - string user_id = 1 [ + string user_id = 3 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1238,12 +1241,19 @@ message UnlockUserRequest { } message UnlockUserResponse { - zitadel.object.v2.Details details = 1; + zitadel.resources.object.v3alpha.Details details = 1; } message DeleteUserRequest { + optional zitadel.object.v3alpha.Instance instance = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"domain from HOST or :authority header\"" + } + ]; + // Optionally expect the user to be in this organization. + optional zitadel.object.v3alpha.Organization organization = 2; // unique identifier of the user. - string user_id = 1 [ + string user_id = 3 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1255,12 +1265,19 @@ message DeleteUserRequest { } message DeleteUserResponse { - zitadel.object.v2.Details details = 1; + zitadel.resources.object.v3alpha.Details details = 1; } message SetContactEmailRequest { + optional zitadel.object.v3alpha.Instance instance = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"domain from HOST or :authority header\"" + } + ]; + // Optionally expect the user to be in this organization. + optional zitadel.object.v3alpha.Organization organization = 2; // unique identifier of the user. - string user_id = 1 [ + string user_id = 3 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1270,11 +1287,11 @@ message SetContactEmailRequest { } ]; // Set the user's contact email and it's verification state. - SetEmail email = 2; + SetEmail email = 4; } message SetContactEmailResponse { - zitadel.object.v2.Details details = 1; + zitadel.resources.object.v3alpha.Details details = 1; // The verification code will be set if a contact email was set with a return_code verification option. optional string verification_code = 3 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1284,8 +1301,15 @@ message SetContactEmailResponse { } message VerifyContactEmailRequest { + optional zitadel.object.v3alpha.Instance instance = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"domain from HOST or :authority header\"" + } + ]; + // Optionally expect the user to be in this organization. + optional zitadel.object.v3alpha.Organization organization = 2; // unique identifier of the user. - string user_id = 1 [ + string user_id = 3 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1295,7 +1319,7 @@ message VerifyContactEmailRequest { } ]; // Set the verification code generated during the set contact email request. - string verification_code = 2 [ + string verification_code = 4 [ (validate.rules).string = {min_len: 1, max_len: 20}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1307,12 +1331,19 @@ message VerifyContactEmailRequest { } message VerifyContactEmailResponse { - zitadel.object.v2.Details details = 1; + zitadel.resources.object.v3alpha.Details details = 1; } message ResendContactEmailCodeRequest { + optional zitadel.object.v3alpha.Instance instance = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"domain from HOST or :authority header\"" + } + ]; + // Optionally expect the user to be in this organization. + optional zitadel.object.v3alpha.Organization organization = 2; // unique identifier of the user. - string user_id = 1 [ + string user_id = 3 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1324,21 +1355,28 @@ message ResendContactEmailCodeRequest { // if no verification is specified, an email is sent oneof verification { // Let ZITADEL send the link to the user via email. - SendEmailVerificationCode send_code = 2; + SendEmailVerificationCode send_code = 4; // Get the code back to provide it to the user in your preferred mechanism. - ReturnEmailVerificationCode return_code = 3; + ReturnEmailVerificationCode return_code = 5; } } message ResendContactEmailCodeResponse { - zitadel.object.v2.Details details = 1; + zitadel.resources.object.v3alpha.Details details = 1; // in case the verification was set to return_code, the code will be returned. optional string verification_code = 2; } message SetContactPhoneRequest { + optional zitadel.object.v3alpha.Instance instance = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"domain from HOST or :authority header\"" + } + ]; + // Optionally expect the user to be in this organization. + optional zitadel.object.v3alpha.Organization organization = 2; // unique identifier of the user. - string user_id = 1 [ + string user_id = 3 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1348,11 +1386,11 @@ message SetContactPhoneRequest { } ]; // Set the user's contact phone and it's verification state. - SetPhone phone = 2; + SetPhone phone = 4; } message SetContactPhoneResponse { - zitadel.object.v2.Details details = 1; + zitadel.resources.object.v3alpha.Details details = 1; // The phone verification code will be set if a contact phone was set with a return_code verification option. optional string email_code = 3 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1362,8 +1400,15 @@ message SetContactPhoneResponse { } message VerifyContactPhoneRequest { + optional zitadel.object.v3alpha.Instance instance = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"domain from HOST or :authority header\"" + } + ]; + // Optionally expect the user to be in this organization. + optional zitadel.object.v3alpha.Organization organization = 2; // unique identifier of the user. - string user_id = 1 [ + string user_id = 3 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1373,7 +1418,7 @@ message VerifyContactPhoneRequest { } ]; // Set the verification code generated during the set contact phone request. - string verification_code = 2 [ + string verification_code = 4 [ (validate.rules).string = {min_len: 1, max_len: 20}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1385,12 +1430,19 @@ message VerifyContactPhoneRequest { } message VerifyContactPhoneResponse { - zitadel.object.v2.Details details = 1; + zitadel.resources.object.v3alpha.Details details = 1; } message ResendContactPhoneCodeRequest { + optional zitadel.object.v3alpha.Instance instance = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"domain from HOST or :authority header\"" + } + ]; + // Optionally expect the user to be in this organization. + optional zitadel.object.v3alpha.Organization organization = 2; // unique identifier of the user. - string user_id = 1 [ + string user_id = 3 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1402,21 +1454,28 @@ message ResendContactPhoneCodeRequest { // if no verification is specified, a SMS is sent oneof verification { // Let ZITADEL send the link to the user via SMS. - SendPhoneVerificationCode send_code = 2; + SendPhoneVerificationCode send_code = 4; // Get the code back to provide it to the user in your preferred mechanism. - ReturnPhoneVerificationCode return_code = 3; + ReturnPhoneVerificationCode return_code = 5; } } message ResendContactPhoneCodeResponse { - zitadel.object.v2.Details details = 1; - // in case the verification was set to return_code, the code will be returned. - optional string verification_code = 2; + zitadel.resources.object.v3alpha.Details details = 1; + // in case the verification was set to return_code, the code will be returned. + optional string verification_code = 2; } message AddUsernameRequest { + optional zitadel.object.v3alpha.Instance instance = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"domain from HOST or :authority header\"" + } + ]; + // Optionally expect the user to be in this organization. + optional zitadel.object.v3alpha.Organization organization = 2; // unique identifier of the user. - string user_id = 1 [ + string user_id = 3 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1426,11 +1485,11 @@ message AddUsernameRequest { } ]; // Set the user's new username. - SetUsername username = 2; + SetUsername username = 4; } message AddUsernameResponse { - zitadel.object.v2.Details details = 1; + zitadel.resources.object.v3alpha.Details details = 1; // unique identifier of the username. string username_id = 2 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1440,8 +1499,15 @@ message AddUsernameResponse { } message RemoveUsernameRequest { + optional zitadel.object.v3alpha.Instance instance = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"domain from HOST or :authority header\"" + } + ]; + // Optionally expect the user to be in this organization. + optional zitadel.object.v3alpha.Organization organization = 2; // unique identifier of the user. - string user_id = 1 [ + string user_id = 3 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1451,7 +1517,7 @@ message RemoveUsernameRequest { } ]; // unique identifier of the username. - string username_id = 2 [ + string username_id = 4 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1463,12 +1529,19 @@ message RemoveUsernameRequest { } message RemoveUsernameResponse { - zitadel.object.v2.Details details = 1; + zitadel.resources.object.v3alpha.Details details = 1; } message SetPasswordRequest { + optional zitadel.object.v3alpha.Instance instance = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"domain from HOST or :authority header\"" + } + ]; + // Optionally expect the user to be in this organization. + optional zitadel.object.v3alpha.Organization organization = 2; // unique identifier of the user. - string user_id = 1 [ + string user_id = 3 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1478,40 +1551,23 @@ message SetPasswordRequest { } ]; // Provide the new password (in plain text or as hash). - SetPassword new_password = 2; - // If neither, the current password nor a verification code generated by the PasswordReset is provided, - // the user must be granted permission to set a password. - oneof verification { - // Provide the current password to verify you're allowed to change the password. - string current_password = 3 [ - (validate.rules).string = {min_len: 1, max_len: 200}, - (google.api.field_behavior) = REQUIRED, - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - min_length: 1; - max_length: 200; - example: "\"Secr3tP4ssw0rd!\""; - } - ]; - // Or provider the verification code generated during password reset request. - string verification_code = 4 [ - (validate.rules).string = {min_len: 1, max_len: 20}, - (google.api.field_behavior) = REQUIRED, - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - min_length: 1; - max_length: 20; - example: "\"SKJd342k\""; - } - ]; - } + SetPassword new_password = 4; } message SetPasswordResponse { - zitadel.object.v2.Details details = 1; + zitadel.resources.object.v3alpha.Details details = 1; } message RequestPasswordResetRequest { + optional zitadel.object.v3alpha.Instance instance = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"domain from HOST or :authority header\"" + } + ]; + // Optionally expect the user to be in this organization. + optional zitadel.object.v3alpha.Organization organization = 2; // unique identifier of the user. - string user_id = 1 [ + string user_id = 3 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1523,16 +1579,16 @@ message RequestPasswordResetRequest { // If no medium is specified, an email is sent with the default url. oneof medium { // Let ZITADEL send the link to the user via email. - SendPasswordResetEmail send_email = 2; + SendPasswordResetEmail send_email = 4; // Let ZITADEL send the link to the user via SMS. - SendPasswordResetSMS send_sms = 3; + SendPasswordResetSMS send_sms = 5; // Get the code back to provide it to the user in your preferred mechanism. - ReturnPasswordResetCode return_code = 4; + ReturnPasswordResetCode return_code = 6; } } message RequestPasswordResetResponse { - zitadel.object.v2.Details details = 1; + zitadel.resources.object.v3alpha.Details details = 1; // In case the medium was set to return_code, the code will be returned. optional string verification_code = 2 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1542,8 +1598,15 @@ message RequestPasswordResetResponse { } message StartWebAuthNRegistrationRequest { + optional zitadel.object.v3alpha.Instance instance = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"domain from HOST or :authority header\"" + } + ]; + // Optionally expect the user to be in this organization. + optional zitadel.object.v3alpha.Organization organization = 2; // unique identifier of the user. - string user_id = 1 [ + string user_id = 3 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1552,26 +1615,11 @@ message StartWebAuthNRegistrationRequest { example: "\"69629026806489455\""; } ]; - // Domain on which the user currently is or will be authenticated. - string domain = 4 [ - (validate.rules).string = {min_len: 1, max_len: 200}, - (google.api.field_behavior) = REQUIRED, - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - min_length: 1, - max_length: 200, - example: "\"my-domain.zitadel.cloud\""; - } - ]; - // Optionally specify the authenticator type of the passkey device (platform or cross-platform). - // If none is provided, both values are allowed. - WebAuthNAuthenticatorType authenticator_type = 3; - // Optionally provide a one time code generated by ZITADEL. - // This is required to start the passkey registration without user authentication. - optional AuthenticatorRegistrationCode code = 2; + StartWebAuthNRegistration registration = 4; } message StartWebAuthNRegistrationResponse { - zitadel.object.v2.Details details = 1; + zitadel.resources.object.v3alpha.Details details = 1; // unique identifier of the WebAuthN registration. string web_auth_n_id = 2 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1589,8 +1637,15 @@ message StartWebAuthNRegistrationResponse { } message VerifyWebAuthNRegistrationRequest { + optional zitadel.object.v3alpha.Instance instance = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"domain from HOST or :authority header\"" + } + ]; + // Optionally expect the user to be in this organization. + optional zitadel.object.v3alpha.Organization organization = 2; // unique identifier of the user. - string user_id = 1 [ + string user_id = 3 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1600,7 +1655,7 @@ message VerifyWebAuthNRegistrationRequest { } ]; // unique identifier of the WebAuthN registration, which was returned in the start webauthn registration. - string web_auth_n_id = 2 [ + string web_auth_n_id = 4 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1609,37 +1664,23 @@ message VerifyWebAuthNRegistrationRequest { example: "\"163840776835432705\""; } ]; - // PublicKeyCredential Interface. - // Generated helper methods populate the field from JSON created by a WebAuthN client. - // See also: https://www.w3.org/TR/webauthn/#publickeycredential - google.protobuf.Struct public_key_credential = 3 [ - (validate.rules).message.required = true, - (google.api.field_behavior) = REQUIRED, - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "{\"type\":\"public-key\",\"id\":\"pawVarF4xPxLFmfCnRkwXWeTrKGzabcAi92LEI1WC00\",\"rawId\":\"pawVarF4xPxLFmfCnRkwXWeTrKGzabcAi92LEI1WC00\",\"response\":{\"attestationObject\":\"o2NmbXRmcGFja2VkZ2F0dFN0bXSiY2FsZyZjc2lnWEcwRQIgRKS3VpeE9tfExXRzkoUKnG4rQWPvtSSt4YtDGgTx32oCIQDPey-2YJ4uIg-QCM4jj6aE2U3tgMFM_RP7Efx6xRu3JGhhdXRoRGF0YVikSZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NFAAAAADju76085Yhmlt1CEOHkwLQAIKWsFWqxeMT8SxZnwp0ZMF1nk6yhs2m3AIvdixCNVgtNpQECAyYgASFYIMGUDSP2FAQn2MIfPMy7cyB_Y30VqixVgGULTBtFjfRiIlggjUGfQo3_-CrMmH3S-ZQkFKWKnNBQEAMkFtG-9A4zqW0\",\"clientDataJSON\":\"eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiQlhXdHh0WGxJeFZZa0pHT1dVaUVmM25zby02aXZKdWw2YmNmWHdMVlFIayIsIm9yaWdpbiI6Imh0dHBzOi8vbG9jYWxob3N0OjgwODAifQ\"}}"; - min_length: 55; - max_length: 1048576; //1 MB - } - ]; - // Provide a name for the WebAuthN device. This will help identify it in the future. - string web_auth_n_name = 4 [ - (validate.rules).string = {min_len: 1, max_len: 200}, - (google.api.field_behavior) = REQUIRED, - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - min_length: 1; - max_length: 200; - example: "\"fido key\"" - } - ]; + VerifyWebAuthNRegistration verify = 5; } message VerifyWebAuthNRegistrationResponse { - zitadel.object.v2.Details details = 1; + zitadel.resources.object.v3alpha.Details details = 1; } message CreateWebAuthNRegistrationLinkRequest { + optional zitadel.object.v3alpha.Instance instance = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"domain from HOST or :authority header\"" + } + ]; + // Optionally expect the user to be in this organization. + optional zitadel.object.v3alpha.Organization organization = 2; // unique identifier of the user. - string user_id = 1 [ + string user_id = 3 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1651,21 +1692,28 @@ message CreateWebAuthNRegistrationLinkRequest { // if no medium is specified, an email is sent with the default url. oneof medium { // Let ZITADEL send the link to the user via email. - SendWebAuthNRegistrationLink send_link = 2; + SendWebAuthNRegistrationLink send_link = 4; // Get the code back to provide it to the user in your preferred mechanism. - ReturnWebAuthNRegistrationCode return_code = 3; + ReturnWebAuthNRegistrationCode return_code = 5; } } message CreateWebAuthNRegistrationLinkResponse { - zitadel.object.v2.Details details = 1; + zitadel.resources.object.v3alpha.Details details = 1; // In case the medium was set to return_code, the code will be returned. optional AuthenticatorRegistrationCode code = 2; } message RemoveWebAuthNAuthenticatorRequest { + optional zitadel.object.v3alpha.Instance instance = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"domain from HOST or :authority header\"" + } + ]; + // Optionally expect the user to be in this organization. + optional zitadel.object.v3alpha.Organization organization = 2; // unique identifier of the user. - string user_id = 1 [ + string user_id = 3 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1675,7 +1723,7 @@ message RemoveWebAuthNAuthenticatorRequest { } ]; // unique identifier of the WebAuthN authenticator. - string web_auth_n_id = 2 [ + string web_auth_n_id = 4 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1687,12 +1735,19 @@ message RemoveWebAuthNAuthenticatorRequest { } message RemoveWebAuthNAuthenticatorResponse { - zitadel.object.v2.Details details = 1; + zitadel.resources.object.v3alpha.Details details = 1; } message StartTOTPRegistrationRequest { + optional zitadel.object.v3alpha.Instance instance = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"domain from HOST or :authority header\"" + } + ]; + // Optionally expect the user to be in this organization. + optional zitadel.object.v3alpha.Organization organization = 2; // unique identifier of the user. - string user_id = 1 [ + string user_id = 3 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1704,7 +1759,7 @@ message StartTOTPRegistrationRequest { } message StartTOTPRegistrationResponse { - zitadel.object.v2.Details details = 1; + zitadel.resources.object.v3alpha.Details details = 1; // unique identifier of the TOTP registration. string totp_id = 2 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1726,8 +1781,15 @@ message StartTOTPRegistrationResponse { } message VerifyTOTPRegistrationRequest { + optional zitadel.object.v3alpha.Instance instance = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"domain from HOST or :authority header\"" + } + ]; + // Optionally expect the user to be in this organization. + optional zitadel.object.v3alpha.Organization organization = 2; // unique identifier of the user. - string user_id = 1 [ + string user_id = 3 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1737,7 +1799,7 @@ message VerifyTOTPRegistrationRequest { } ]; // unique identifier of the TOTP registration, which was returned in the start TOTP registration. - string totp_id = 2 [ + string totp_id = 4 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1747,7 +1809,7 @@ message VerifyTOTPRegistrationRequest { } ]; // Code generated by TOTP app or device. - string code = 3 [ + string code = 5 [ (validate.rules).string = {min_len: 6, max_len: 9}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1759,12 +1821,19 @@ message VerifyTOTPRegistrationRequest { } message VerifyTOTPRegistrationResponse { - zitadel.object.v2.Details details = 1; + zitadel.resources.object.v3alpha.Details details = 1; } message RemoveTOTPAuthenticatorRequest { + optional zitadel.object.v3alpha.Instance instance = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"domain from HOST or :authority header\"" + } + ]; + // Optionally expect the user to be in this organization. + optional zitadel.object.v3alpha.Organization organization = 2; // unique identifier of the user. - string user_id = 1 [ + string user_id = 3 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1774,7 +1843,7 @@ message RemoveTOTPAuthenticatorRequest { } ]; // unique identifier of the TOTP authenticator. - string totp_id = 2 [ + string totp_id = 4 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1786,12 +1855,19 @@ message RemoveTOTPAuthenticatorRequest { } message RemoveTOTPAuthenticatorResponse { - zitadel.object.v2.Details details = 1; + zitadel.resources.object.v3alpha.Details details = 1; } message AddOTPSMSAuthenticatorRequest { + optional zitadel.object.v3alpha.Instance instance = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"domain from HOST or :authority header\"" + } + ]; + // Optionally expect the user to be in this organization. + optional zitadel.object.v3alpha.Organization organization = 2; // unique identifier of the user. - string user_id = 1 [ + string user_id = 3 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1801,11 +1877,11 @@ message AddOTPSMSAuthenticatorRequest { } ]; // Set the user's phone for the OTP SMS authenticator and it's verification state. - SetPhone phone = 2; + SetPhone phone = 4; } message AddOTPSMSAuthenticatorResponse { - zitadel.object.v2.Details details = 1; + zitadel.resources.object.v3alpha.Details details = 1; // unique identifier of the OTP SMS registration. string otp_sms_id = 2 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1822,8 +1898,15 @@ message AddOTPSMSAuthenticatorResponse { } message VerifyOTPSMSRegistrationRequest { + optional zitadel.object.v3alpha.Instance instance = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"domain from HOST or :authority header\"" + } + ]; + // Optionally expect the user to be in this organization. + optional zitadel.object.v3alpha.Organization organization = 2; // unique identifier of the user. - string user_id = 1 [ + string user_id = 3 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1833,7 +1916,7 @@ message VerifyOTPSMSRegistrationRequest { } ]; // unique identifier of the OTP SMS registration, which was returned in the add OTP SMS. - string otp_sms_id = 2 [ + string otp_sms_id = 4 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1843,7 +1926,7 @@ message VerifyOTPSMSRegistrationRequest { } ]; // Set the verification code generated during the add OTP SMS request. - string code = 3 [ + string code = 5 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1855,12 +1938,19 @@ message VerifyOTPSMSRegistrationRequest { } message VerifyOTPSMSRegistrationResponse { - zitadel.object.v2.Details details = 1; + zitadel.resources.object.v3alpha.Details details = 1; } message RemoveOTPSMSAuthenticatorRequest { + optional zitadel.object.v3alpha.Instance instance = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"domain from HOST or :authority header\"" + } + ]; + // Optionally expect the user to be in this organization. + optional zitadel.object.v3alpha.Organization organization = 2; // unique identifier of the user. - string user_id = 1 [ + string user_id = 3 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1870,7 +1960,7 @@ message RemoveOTPSMSAuthenticatorRequest { } ]; // unique identifier of the OTP SMS authenticator. - string otp_sms_id = 2 [ + string otp_sms_id = 4 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1882,12 +1972,19 @@ message RemoveOTPSMSAuthenticatorRequest { } message RemoveOTPSMSAuthenticatorResponse { - zitadel.object.v2.Details details = 1; + zitadel.resources.object.v3alpha.Details details = 1; } message AddOTPEmailAuthenticatorRequest { + optional zitadel.object.v3alpha.Instance instance = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"domain from HOST or :authority header\"" + } + ]; + // Optionally expect the user to be in this organization. + optional zitadel.object.v3alpha.Organization organization = 2; // unique identifier of the user. - string user_id = 1 [ + string user_id = 3 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1897,11 +1994,11 @@ message AddOTPEmailAuthenticatorRequest { } ]; // Set the user's email for the OTP Email authenticator and it's verification state. - SetEmail email = 2; + SetEmail email = 4; } message AddOTPEmailAuthenticatorResponse { - zitadel.object.v2.Details details = 1; + zitadel.resources.object.v3alpha.Details details = 1; // unique identifier of the OTP Email registration. string otp_email_id = 2 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1917,8 +2014,15 @@ message AddOTPEmailAuthenticatorResponse { } message VerifyOTPEmailRegistrationRequest { + optional zitadel.object.v3alpha.Instance instance = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"domain from HOST or :authority header\"" + } + ]; + // Optionally expect the user to be in this organization. + optional zitadel.object.v3alpha.Organization organization = 2; // unique identifier of the user. - string user_id = 1 [ + string user_id = 3 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1928,7 +2032,7 @@ message VerifyOTPEmailRegistrationRequest { } ]; // unique identifier of the OTP Email registration, which was returned in the add OTP Email. - string otp_email_id = 2 [ + string otp_email_id = 4 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1938,7 +2042,7 @@ message VerifyOTPEmailRegistrationRequest { } ]; // Set the verification code generated during the add OTP Email request. - string code = 3 [ + string code = 5 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1950,12 +2054,19 @@ message VerifyOTPEmailRegistrationRequest { } message VerifyOTPEmailRegistrationResponse { - zitadel.object.v2.Details details = 1; + zitadel.resources.object.v3alpha.Details details = 1; } message RemoveOTPEmailAuthenticatorRequest { + optional zitadel.object.v3alpha.Instance instance = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"domain from HOST or :authority header\"" + } + ]; + // Optionally expect the user to be in this organization. + optional zitadel.object.v3alpha.Organization organization = 2; // unique identifier of the user. - string user_id = 1 [ + string user_id = 3 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1965,7 +2076,7 @@ message RemoveOTPEmailAuthenticatorRequest { } ]; // unique identifier of the OTP Email authenticator. - string otp_email_id = 2 [ + string otp_email_id = 4 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1977,12 +2088,19 @@ message RemoveOTPEmailAuthenticatorRequest { } message RemoveOTPEmailAuthenticatorResponse { - zitadel.object.v2.Details details = 1; + zitadel.resources.object.v3alpha.Details details = 1; } message StartIdentityProviderIntentRequest { + optional zitadel.object.v3alpha.Instance instance = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"domain from HOST or :authority header\"" + } + ]; + // Optionally expect the user to be in this organization. + optional zitadel.object.v3alpha.Organization organization = 2; // ID of an existing identity provider (IDP). - string idp_id = 1 [ + string idp_id = 3 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -1993,13 +2111,13 @@ message StartIdentityProviderIntentRequest { ]; oneof content { - RedirectURLs urls = 2; - LDAPCredentials ldap = 3; + RedirectURLs urls = 4; + LDAPCredentials ldap = 5; } } message StartIdentityProviderIntentResponse { - zitadel.object.v2.Details details = 1; + zitadel.resources.object.v3alpha.Details details = 1; // the next step to take in the idp intent flow. oneof next_step { // The authentication URL to which the client should redirect. @@ -2016,9 +2134,16 @@ message StartIdentityProviderIntentResponse { } } -message RetrieveIdentityProviderIntentRequest { +message GetIdentityProviderIntentRequest { + optional zitadel.object.v3alpha.Instance instance = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"domain from HOST or :authority header\"" + } + ]; + // Optionally expect the user to be in this organization. + optional zitadel.object.v3alpha.Organization organization = 2; // ID of the identity provider (IDP) intent, previously returned on the success response of the start identity provider intent. - string idp_intent_id = 1 [ + string idp_intent_id = 3 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -2028,7 +2153,7 @@ message RetrieveIdentityProviderIntentRequest { } ]; // Token of the identity provider (IDP) intent, previously returned on the success response of the start identity provider intent. - string idp_intent_token = 2 [ + string idp_intent_token = 4 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -2039,8 +2164,8 @@ message RetrieveIdentityProviderIntentRequest { ]; } -message RetrieveIdentityProviderIntentResponse { - zitadel.object.v2.Details details = 1; +message GetIdentityProviderIntentResponse { + zitadel.resources.object.v3alpha.Details details = 1; // Information returned by the identity provider (IDP) such as the identification of the user // and detailed / profile information. IDPInformation idp_information = 2; @@ -2053,8 +2178,15 @@ message RetrieveIdentityProviderIntentResponse { } message AddIDPAuthenticatorRequest { + optional zitadel.object.v3alpha.Instance instance = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"domain from HOST or :authority header\"" + } + ]; + // Optionally expect the user to be in this organization. + optional zitadel.object.v3alpha.Organization organization = 2; // unique identifier of the user. - string user_id = 1 [ + string user_id = 3 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -2063,16 +2195,23 @@ message AddIDPAuthenticatorRequest { example: "\"69629026806489455\""; } ]; - IDPAuthenticator idp_authenticator = 2; + IDPAuthenticator authenticator = 4; } message AddIDPAuthenticatorResponse { - zitadel.object.v2.Details details = 1; + zitadel.resources.object.v3alpha.Details details = 1; } message RemoveIDPAuthenticatorRequest { + optional zitadel.object.v3alpha.Instance instance = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"domain from HOST or :authority header\"" + } + ]; + // Optionally expect the user to be in this organization. + optional zitadel.object.v3alpha.Organization organization = 2; // unique identifier of the user. - string user_id = 1 [ + string user_id = 3 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -2082,7 +2221,7 @@ message RemoveIDPAuthenticatorRequest { } ]; // unique identifier of the identity provider (IDP) authenticator. - string idp_id = 2 [ + string idp_id = 4 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -2094,6 +2233,6 @@ message RemoveIDPAuthenticatorRequest { } message RemoveIDPAuthenticatorResponse { - zitadel.object.v2.Details details = 1; + zitadel.resources.object.v3alpha.Details details = 1; } diff --git a/proto/zitadel/resources/userschema/v3alpha/user_schema.proto b/proto/zitadel/resources/userschema/v3alpha/user_schema.proto new file mode 100644 index 0000000000..d9c2e30918 --- /dev/null +++ b/proto/zitadel/resources/userschema/v3alpha/user_schema.proto @@ -0,0 +1,211 @@ +syntax = "proto3"; + +package zitadel.resources.userschema.v3alpha; + +import "google/api/field_behavior.proto"; +import "google/protobuf/struct.proto"; +import "validate/validate.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; +import "zitadel/resources/object/v3alpha/object.proto"; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/resources/userschema/v3alpha;userschema"; + +message GetUserSchema { + // Details provide some base information (such as the last change date) of the schema. + zitadel.resources.object.v3alpha.Details details = 1; + UserSchema config = 2; + // Current state of the schema. + State state = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"STATE_ACTIVE\"" + } + ]; + // Revision is a read only version of the schema, each update of the `schema`-field increases the revision. + uint32 revision = 4 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2\"" + } + ]; +} + +message UserSchema { + // Type is a human readable word describing the schema. + string type = 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: "\"employees\""; + } + ]; + oneof data_type { + option (validate.required) = true; + + // JSON schema representation defining the user. + google.protobuf.Struct schema = 2 [ + (validate.rules).message = {required: true}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "{\"$schema\":\"https://example.com/user/employees\",\"type\":\"object\",\"properties\":{\"name\":{\"type\":\"string\",\"required\":true},\"description\":{\"type\":\"string\"}}}" + } + ]; + + // (--In the future we will allow to use an external registry.--) + } + // Defines the possible types of authenticators. + repeated AuthenticatorType possible_authenticators = 3 [ + (validate.rules).repeated = {unique: true, items: {enum: {defined_only: true, not_in: [0]}}}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "[\"AUTHENTICATOR_TYPE_USERNAME\",\"AUTHENTICATOR_TYPE_PASSWORD\",\"AUTHENTICATOR_TYPE_WEBAUTHN\"]"; + } + ]; +} + +message PatchUserSchema { + // Type is a human readable word describing the schema. + optional string type = 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: "\"employees\""; + } + ]; + oneof data_type { + // JSON schema representation defining the user. + google.protobuf.Struct schema = 3 [ + (validate.rules).message = {required: true}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "{\"$schema\":\"https://example.com/user/employees\",\"type\":\"object\",\"properties\":{\"name\":{\"type\":\"string\",\"required\":true},\"description\":{\"type\":\"string\"}}}" + } + ]; + } + // Defines the possible types of authenticators. + // + // Removal of an authenticator does not remove the authenticator on a user. + repeated AuthenticatorType possible_authenticators = 4 [ + (validate.rules).repeated = {unique: true, items: {enum: {defined_only: true, not_in: [0]}}}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "[\"AUTHENTICATOR_TYPE_USERNAME\",\"AUTHENTICATOR_TYPE_PASSWORD\",\"AUTHENTICATOR_TYPE_WEBAUTHN\"]"; + } + ]; +} + +enum FieldName { + FIELD_NAME_UNSPECIFIED = 0; + FIELD_NAME_TYPE = 1; + FIELD_NAME_STATE = 2; + FIELD_NAME_REVISION = 3; + FIELD_NAME_CHANGE_DATE = 4; + FIELD_NAME_CREATION_DATE = 5; +} + +message SearchFilter { + oneof Filter { + option (validate.required) = true; + + // Union the results of each sub filter ('OR'). + OrFilter or_filter = 1; + // Limit the result to match all sub queries ('AND'). + // Note that if you specify multiple queries, they will be implicitly used as andQueries. + // Use the andFilter in combination with orFilter and notFilter. + AndFilter and_filter = 2; + // Exclude / Negate the result of the sub filter ('NOT'). + NotFilter not_filter = 3; + + // Limit the result to a specific schema type. + TypeFilter type_filter = 5; + // Limit the result to a specific state of the schema. + StateFilter state_filter = 6; + // Limit the result to a specific schema ID. + IDFilter id_filter = 7; + } +} + +message OrFilter { + repeated SearchFilter queries = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "[{\"idFilter\": {\"id\": \"163840776835432705\",\"method\": \"TEXT_FILTER_METHOD_EQUALS\"}},{\"idFilter\": {\"id\": \"163840776835943483\",\"method\": \"TEXT_FILTER_METHOD_EQUALS\"}}]" + } + ]; +} +message AndFilter { + repeated SearchFilter queries = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "[{\"typeFilter\": {\"id\": \"employees\",\"method\": \"TEXT_FILTER_METHOD_STARTS_WITH\"}},{\"stateFilter\": {\"state\": \"STATE_ACTIVE\"}}]" + } + ]; +} + +message NotFilter { + SearchFilter filter = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "{\"stateFilter\": {\"state\": \"STATE_ACTIVE\"}}" + } + ]; +} + +message IDFilter { + // Defines the ID of the user schema to filter for. + 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: "\"163840776835432705\""; + } + ]; + // Defines which text comparison method used for the id filter. + zitadel.resources.object.v3alpha.TextFilterMethod method = 2 [ + (validate.rules).enum.defined_only = true + ]; +} + +message TypeFilter { + // Defines which type to filter for. + string type = 1 [ + (validate.rules).string = {max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + max_length: 200, + example: "\"employees\""; + } + ]; + // Defines which text comparison method used for the type filter. + zitadel.resources.object.v3alpha.TextFilterMethod method = 2 [ + (validate.rules).enum.defined_only = true + ]; +} + +message StateFilter { + // Defines the state to filter for. + State state = 1 [ + (validate.rules).enum.defined_only = true, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"STATE_ACTIVE\"" + } + ]; +} + +enum State { + STATE_UNSPECIFIED = 0; + STATE_ACTIVE = 1; + STATE_INACTIVE = 2; +} + +enum AuthenticatorType { + AUTHENTICATOR_TYPE_UNSPECIFIED = 0; + AUTHENTICATOR_TYPE_USERNAME = 1; + AUTHENTICATOR_TYPE_PASSWORD = 2; + AUTHENTICATOR_TYPE_WEBAUTHN = 3; + AUTHENTICATOR_TYPE_TOTP = 4; + AUTHENTICATOR_TYPE_OTP_EMAIL = 5; + AUTHENTICATOR_TYPE_OTP_SMS = 6; + AUTHENTICATOR_TYPE_AUTHENTICATION_KEY = 7; + AUTHENTICATOR_TYPE_IDENTITY_PROVIDER = 8; +} \ No newline at end of file diff --git a/proto/zitadel/user/schema/v3alpha/user_schema_service.proto b/proto/zitadel/resources/userschema/v3alpha/user_schema_service.proto similarity index 66% rename from proto/zitadel/user/schema/v3alpha/user_schema_service.proto rename to proto/zitadel/resources/userschema/v3alpha/user_schema_service.proto index 14e59f1eab..b709ac2905 100644 --- a/proto/zitadel/user/schema/v3alpha/user_schema_service.proto +++ b/proto/zitadel/resources/userschema/v3alpha/user_schema_service.proto @@ -1,6 +1,6 @@ syntax = "proto3"; -package zitadel.user.schema.v3alpha; +package zitadel.resources.userschema.v3alpha; import "google/api/annotations.proto"; import "google/api/field_behavior.proto"; @@ -8,12 +8,12 @@ import "google/protobuf/duration.proto"; import "google/protobuf/struct.proto"; import "protoc-gen-openapiv2/options/annotations.proto"; import "validate/validate.proto"; -import "zitadel/object/v2/object.proto"; +import "zitadel/object/v3alpha/object.proto"; +import "zitadel/resources/object/v3alpha/object.proto"; import "zitadel/protoc_gen_zitadel/v2/options.proto"; -import "zitadel/user/schema/v3alpha/user_schema.proto"; - -option go_package = "github.com/zitadel/zitadel/pkg/grpc/user/schema/v3alpha"; +import "zitadel/resources/userschema/v3alpha/user_schema.proto"; +option go_package = "github.com/zitadel/zitadel/pkg/grpc/resources/userschema/v3alpha;userschema"; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { info: { @@ -43,7 +43,7 @@ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { produces: "application/grpc-web+proto"; host: "$CUSTOM-DOMAIN"; - base_path: "/"; + base_path: "/resources/v3alpha/user_schemas"; external_docs: { description: "Detailed information about ZITADEL", @@ -103,15 +103,15 @@ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { } }; -service UserSchemaService { +service ZITADELUserSchemas { - // List user schemas + // Search user schemas // - // List all matching user schemas. By default, we will return all user schema of your instance. Make sure to include a limit and sorting for pagination. - rpc ListUserSchemas (ListUserSchemasRequest) returns (ListUserSchemasResponse) { + // Search all matching user schemas. By default, we will return all user schema of your instance. Make sure to include a limit and sorting for pagination. + rpc SearchUserSchemas (SearchUserSchemasRequest) returns (SearchUserSchemasResponse) { option (google.api.http) = { - post: "/v3alpha/user_schemas/search" - body: "*" + post: "/_search" + body: "filters" }; option (zitadel.protoc_gen_zitadel.v2.options) = { @@ -124,13 +124,13 @@ service UserSchemaService { responses: { key: "200"; value: { - description: "A list of all user schema matching the query"; + description: "A list of all user schema matching the search"; }; }; responses: { key: "400"; value: { - description: "invalid list query"; + description: "invalid search"; schema: { json_schema: { ref: "#/definitions/rpcStatus"; @@ -144,9 +144,9 @@ service UserSchemaService { // User schema by ID // // Returns the user schema identified by the requested ID. - rpc GetUserSchemaByID (GetUserSchemaByIDRequest) returns (GetUserSchemaByIDResponse) { + rpc GetUserSchema (GetUserSchemaRequest) returns (GetUserSchemaResponse) { option (google.api.http) = { - get: "/v3alpha/user_schemas/{id}" + get: "/{id}" }; option (zitadel.protoc_gen_zitadel.v2.options) = { @@ -170,8 +170,8 @@ service UserSchemaService { // Create the first revision of a new user schema. The schema can then be used on users to store and validate their data. rpc CreateUserSchema (CreateUserSchemaRequest) returns (CreateUserSchemaResponse) { option (google.api.http) = { - post: "/v3alpha/user_schemas" - body: "*" + post: "/" + body: "user_schema" }; option (zitadel.protoc_gen_zitadel.v2.options) = { @@ -198,13 +198,13 @@ service UserSchemaService { }; } - // Update a user schema + // Patch a user schema // - // Update an existing user schema to a new revision. Users based on the current revision will not be affected until they are updated. - rpc UpdateUserSchema (UpdateUserSchemaRequest) returns (UpdateUserSchemaResponse) { + // Patch an existing user schema to a new revision. Users based on the current revision will not be affected until they are updated. + rpc PatchUserSchema (PatchUserSchemaRequest) returns (PatchUserSchemaResponse) { option (google.api.http) = { - put: "/v3alpha/user_schemas/{id}" - body: "*" + patch: "/{id}" + body: "user_schema" }; option (zitadel.protoc_gen_zitadel.v2.options) = { @@ -228,7 +228,7 @@ service UserSchemaService { // Deactivate an existing user schema and change it into a read-only state. Users based on this schema cannot be updated anymore, but are still able to authenticate. rpc DeactivateUserSchema (DeactivateUserSchemaRequest) returns (DeactivateUserSchemaResponse) { option (google.api.http) = { - post: "/v3alpha/user_schemas/{id}/deactivate" + post: "/{id}/_deactivate" }; option (zitadel.protoc_gen_zitadel.v2.options) = { @@ -252,7 +252,7 @@ service UserSchemaService { // Reactivate an previously deactivated user schema and change it into an active state again. rpc ReactivateUserSchema (ReactivateUserSchemaRequest) returns (ReactivateUserSchemaResponse) { option (google.api.http) = { - post: "/v3alpha/user_schemas/{id}/reactivate" + post: "/{id}/_reactivate" }; option (zitadel.protoc_gen_zitadel.v2.options) = { @@ -276,7 +276,7 @@ service UserSchemaService { // Delete an existing user schema. This operation is only allowed if there are no associated users to it. rpc DeleteUserSchema (DeleteUserSchemaRequest) returns (DeleteUserSchemaResponse) { option (google.api.http) = { - delete: "/v3alpha/user_schemas/{id}" + delete: "/{id}" }; option (zitadel.protoc_gen_zitadel.v2.options) = { @@ -294,33 +294,36 @@ service UserSchemaService { }; }; } - } -message ListUserSchemasRequest { - // list limitations and ordering. - zitadel.object.v2.ListQuery query = 1; - // the field the result is sorted. - zitadel.user.schema.v3alpha.FieldName sorting_column = 2 [ +message SearchUserSchemasRequest { + optional zitadel.object.v3alpha.Instance instance = 1 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"FIELD_NAME_TYPE\""; + default: "\"domain from HOST or :authority header\"" } ]; - // Define the criteria to query for. - repeated zitadel.user.schema.v3alpha.SearchQuery queries = 3; + // list limitations and ordering. + optional zitadel.resources.object.v3alpha.SearchQuery query = 2; + // The field the result is sorted by. The default is the creation date. Beware that if you change this, your result pagination might be inconsistent. + optional FieldName sorting_column = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"TARGET_FIELD_NAME_CREATION_DATE\"" + } + ]; + // Define the criteria to filter for. + repeated SearchFilter filters = 4; } -message ListUserSchemasResponse { +message SearchUserSchemasResponse { // Details provides information about the returned result including total amount found. - zitadel.object.v2.ListDetails details = 1; + zitadel.resources.object.v3alpha.ListDetails details = 1; // States by which field the results are sorted. - zitadel.user.schema.v3alpha.FieldName sorting_column = 2; + FieldName sorting_column = 2; // The result contains the user schemas, which matched the queries. - repeated zitadel.user.schema.v3alpha.UserSchema result = 3; + repeated GetUserSchema result = 3; } - -message GetUserSchemaByIDRequest { +message GetUserSchemaRequest { // unique identifier of the schema. string id = 1 [ (validate.rules).string = {min_len: 1, max_len: 200}, @@ -333,120 +336,117 @@ message GetUserSchemaByIDRequest { ]; } -message GetUserSchemaByIDResponse { - zitadel.user.schema.v3alpha.UserSchema schema = 1; +message GetUserSchemaResponse { + GetUserSchema user_schema = 2; } - message CreateUserSchemaRequest { - // Type is a human readable word describing the schema. - string type = 1 [ - (validate.rules).string = {min_len: 1, max_len: 200}, - (google.api.field_behavior) = REQUIRED, + optional zitadel.object.v3alpha.Instance instance = 1 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - min_length: 1; - max_length: 200; - example: "\"employees\""; - } - ]; - oneof data_type { - option (validate.required) = true; - - // JSON schema representation defining the user. - google.protobuf.Struct schema = 2 [ - (validate.rules).message = {required: true}, - (google.api.field_behavior) = REQUIRED, - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "{\"$schema\":\"https://example.com/user/employees\",\"type\":\"object\",\"properties\":{\"name\":{\"type\":\"string\",\"required\":true},\"description\":{\"type\":\"string\"}}}" - } - ]; - - // (--In the future we will allow to use an external registry.--) - } - // Defines the possible types of authenticators. - repeated AuthenticatorType possible_authenticators = 3 [ - (validate.rules).repeated = {unique: true, items: {enum: {defined_only: true, not_in: [0]}}}, - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "[\"AUTHENTICATOR_TYPE_USERNAME\",\"AUTHENTICATOR_TYPE_PASSWORD\",\"AUTHENTICATOR_TYPE_WEBAUTHN\"]"; + default: "\"domain from HOST or :authority header\"" } ]; + UserSchema user_schema = 2; } message CreateUserSchemaResponse { - // ID is the read-only unique identifier of the schema. - string id = 1; // Details provide some base information (such as the last change date) of the schema. - zitadel.object.v2.Details details = 2; + zitadel.resources.object.v3alpha.Details details = 2; } -message UpdateUserSchemaRequest { +message PatchUserSchemaRequest { + optional zitadel.object.v3alpha.Instance instance = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"domain from HOST or :authority header\"" + } + ]; // unique identifier of the schema. - string id = 1; - // Type is a human readable word describing the schema. - optional string type = 2 [ + 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: "\"employees\""; - } - ]; - oneof data_type { - // JSON schema representation defining the user. - google.protobuf.Struct schema = 3 [ - (validate.rules).message = {required: true}, - (google.api.field_behavior) = REQUIRED, - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "{\"$schema\":\"https://example.com/user/employees\",\"type\":\"object\",\"properties\":{\"name\":{\"type\":\"string\",\"required\":true},\"description\":{\"type\":\"string\"}}}" - } - ]; - } - // Defines the possible types of authenticators. - // - // Removal of an authenticator does not remove the authenticator on a user. - repeated AuthenticatorType possible_authenticators = 4 [ - (validate.rules).repeated = {unique: true, items: {enum: {defined_only: true, not_in: [0]}}}, - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "[\"AUTHENTICATOR_TYPE_USERNAME\",\"AUTHENTICATOR_TYPE_PASSWORD\",\"AUTHENTICATOR_TYPE_WEBAUTHN\"]"; + min_length: 1, + max_length: 200, + example: "\"69629026806489455\""; } ]; + + PatchUserSchema user_schema = 3; } -message UpdateUserSchemaResponse { +message PatchUserSchemaResponse { // Details provide some base information (such as the last change date) of the schema. - zitadel.object.v2.Details details = 1; + zitadel.resources.object.v3alpha.Details details = 1; } message DeactivateUserSchemaRequest { + optional zitadel.object.v3alpha.Instance instance = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"domain from HOST or :authority header\"" + } + ]; // unique identifier of the schema. - string id = 1; + string id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1, + max_length: 200, + example: "\"69629026806489455\""; + } + ]; } message DeactivateUserSchemaResponse { // Details provide some base information (such as the last change date) of the schema. - zitadel.object.v2.Details details = 1; + zitadel.resources.object.v3alpha.Details details = 1; } message ReactivateUserSchemaRequest { + optional zitadel.object.v3alpha.Instance instance = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"domain from HOST or :authority header\"" + } + ]; // unique identifier of the schema. - string id = 1; + string id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1, + max_length: 200, + example: "\"69629026806489455\""; + } + ]; } message ReactivateUserSchemaResponse { // Details provide some base information (such as the last change date) of the schema. - zitadel.object.v2.Details details = 1; + zitadel.resources.object.v3alpha.Details details = 1; } message DeleteUserSchemaRequest { + optional zitadel.object.v3alpha.Instance instance = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"domain from HOST or :authority header\"" + } + ]; // unique identifier of the schema. - string id = 1; + string id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1, + max_length: 200, + example: "\"69629026806489455\""; + } + ]; } message DeleteUserSchemaResponse { // Details provide some base information (such as the last change date) of the schema. - zitadel.object.v2.Details details = 1; + zitadel.resources.object.v3alpha.Details details = 1; } diff --git a/proto/zitadel/resources/webkey/v3alpha/webkey_service.proto b/proto/zitadel/resources/webkey/v3alpha/webkey_service.proto index 8cf6c379da..c9783d3b09 100644 --- a/proto/zitadel/resources/webkey/v3alpha/webkey_service.proto +++ b/proto/zitadel/resources/webkey/v3alpha/webkey_service.proto @@ -147,7 +147,7 @@ service ZITADELWebKeys { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { summary: "Activate a signing key for the instance"; - description: "Switch the active signing web key. The previously active key will be deactivated." + description: "Switch the active signing web key. The previously active key will be deactivated. Note that the JWKs OIDC endpoint returns a cacheable response. Therefore it is not advised to activate a key that has been created within the cache duration (default is 5min), as the public key may not have been propagated to caches and clients yet." responses: { key: "200" value: { diff --git a/proto/zitadel/text.proto b/proto/zitadel/text.proto index af464bf97f..b9174fc44c 100644 --- a/proto/zitadel/text.proto +++ b/proto/zitadel/text.proto @@ -92,7 +92,8 @@ message LoginCustomText { PasswordlessRegistrationDoneScreenText passwordless_registration_done_text = 34; ExternalRegistrationUserOverviewScreenText external_registration_user_overview_text = 35; bool is_default = 36; - LinkingUserPromptScreenText linking_user_prompt_text = 37; + // Deprecated: the linking user prompt screen no longer exists + zitadel.text.v1.LinkingUserPromptScreenText linking_user_prompt_text = 37 [deprecated = true]; } message SelectAccountScreenText { diff --git a/proto/zitadel/user/schema/v3alpha/user_schema.proto b/proto/zitadel/user/schema/v3alpha/user_schema.proto deleted file mode 100644 index e7a7a0737a..0000000000 --- a/proto/zitadel/user/schema/v3alpha/user_schema.proto +++ /dev/null @@ -1,170 +0,0 @@ -syntax = "proto3"; - -package zitadel.user.schema.v3alpha; - -import "google/api/field_behavior.proto"; -import "google/protobuf/struct.proto"; -import "validate/validate.proto"; -import "protoc-gen-openapiv2/options/annotations.proto"; -import "zitadel/object/v2/object.proto"; - -option go_package = "github.com/zitadel/zitadel/pkg/grpc/user/schema/v3alpha"; - -message UserSchema { - - // ID is the read-only unique identifier of the schema. - string id = 1 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"69629012906488334\"" - } - ]; - // Details provide some base information (such as the last change date) of the schema. - zitadel.object.v2.Details details = 2; - // Type is a human readable text describing the schema. - string type = 3 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"employees\"" - } - ]; - // Current state of the schema. - State state = 4 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"STATE_ACTIVE\"" - } - ]; - // Revision is a read only version of the schema, each update of the `schema`-field increases the revision. - uint32 revision = 5 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"2\"" - } - ]; - // JSON schema representation defining the user. - google.protobuf.Struct schema = 6 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "{\"$schema\":\"https://example.com/user/employees\",\"type\":\"object\",\"properties\":{\"name\":{\"type\":\"string\",\"required\":true},\"description\":{\"type\":\"string\"}}}" - } - ]; - // Defines the possible types of authenticators. - // This allows creating different user types like human/machine without usage of actions to validate possible authenticators. - // Removal of an authenticator does not remove the authenticator on a user. - repeated AuthenticatorType possible_authenticators = 7 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "[\"AUTHENTICATOR_TYPE_USERNAME\",\"AUTHENTICATOR_TYPE_PASSWORD\",\"AUTHENTICATOR_TYPE_WEBAUTHN\"]"; - } - ]; -} - -enum FieldName { - FIELD_NAME_UNSPECIFIED = 0; - FIELD_NAME_TYPE = 1; - FIELD_NAME_STATE = 2; - FIELD_NAME_REVISION = 3; - FIELD_NAME_CHANGE_DATE = 4; -} - -message SearchQuery { - oneof query { - option (validate.required) = true; - - // Union the results of each sub query ('OR'). - OrQuery or_query = 1; - // Limit the result to match all sub queries ('AND'). - // Note that if you specify multiple queries, they will be implicitly used as andQueries. - // Use the andQuery in combination with orQuery and notQuery. - AndQuery and_query = 2; - // Exclude / Negate the result of the sub query ('NOT'). - NotQuery not_query = 3; - - // Limit the result to a specific schema type. - TypeQuery type_query = 5; - // Limit the result to a specific state of the schema. - StateQuery state_query = 6; - // Limit the result to a specific schema ID. - IDQuery id_query = 7; - } -} - -message OrQuery { - repeated SearchQuery queries = 1 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "[{\"idQuery\": {\"id\": \"163840776835432705\",\"method\": \"TEXT_QUERY_METHOD_EQUALS\"}},{\"idQuery\": {\"id\": \"163840776835943483\",\"method\": \"TEXT_QUERY_METHOD_EQUALS\"}}]" - } - ]; -} -message AndQuery { - repeated SearchQuery queries = 1 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "[{\"typeQuery\": {\"id\": \"employees\",\"method\": \"TEXT_QUERY_METHOD_STARTS_WITH\"}},{\"stateQuery\": {\"state\": \"STATE_ACTIVE\"}}]" - } - ]; -} - -message NotQuery { - SearchQuery query = 1 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "{\"stateQuery\": {\"state\": \"STATE_ACTIVE\"}}" - } - ]; -} - -message IDQuery { - // Defines the ID of the user schema to query for. - 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: "\"163840776835432705\""; - } - ]; - // Defines which text comparison method used for the id query. - zitadel.object.v2.TextQueryMethod method = 2 [ - (validate.rules).enum.defined_only = true - ]; -} - -message TypeQuery { - // Defines which type to query for. - string type = 1 [ - (validate.rules).string = {max_len: 200}, - (google.api.field_behavior) = REQUIRED, - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - max_length: 200, - example: "\"employees\""; - } - ]; - // Defines which text comparison method used for the type query. - zitadel.object.v2.TextQueryMethod method = 2 [ - (validate.rules).enum.defined_only = true - ]; -} - -message StateQuery { - // Defines the state to query for. - State state = 1 [ - (validate.rules).enum.defined_only = true, - (google.api.field_behavior) = REQUIRED, - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"STATE_ACTIVE\"" - } - ]; -} - -enum State { - STATE_UNSPECIFIED = 0; - STATE_ACTIVE = 1; - STATE_INACTIVE = 2; -} - -enum AuthenticatorType { - AUTHENTICATOR_TYPE_UNSPECIFIED = 0; - AUTHENTICATOR_TYPE_USERNAME = 1; - AUTHENTICATOR_TYPE_PASSWORD = 2; - AUTHENTICATOR_TYPE_WEBAUTHN = 3; - AUTHENTICATOR_TYPE_TOTP = 4; - AUTHENTICATOR_TYPE_OTP_EMAIL = 5; - AUTHENTICATOR_TYPE_OTP_SMS = 6; - AUTHENTICATOR_TYPE_AUTHENTICATION_KEY = 7; - AUTHENTICATOR_TYPE_IDENTITY_PROVIDER = 8; -} \ No newline at end of file diff --git a/proto/zitadel/user/v3alpha/user.proto b/proto/zitadel/user/v3alpha/user.proto deleted file mode 100644 index 47b9d7ee98..0000000000 --- a/proto/zitadel/user/v3alpha/user.proto +++ /dev/null @@ -1,66 +0,0 @@ -syntax = "proto3"; - -package zitadel.user.v3alpha; - -import "google/api/field_behavior.proto"; -import "google/protobuf/struct.proto"; -import "google/protobuf/timestamp.proto"; -import "protoc-gen-openapiv2/options/annotations.proto"; -import "validate/validate.proto"; -import "zitadel/object/v2/object.proto"; -import "zitadel/user/v3alpha/authenticator.proto"; -import "zitadel/user/v3alpha/communication.proto"; - -option go_package = "github.com/zitadel/zitadel/pkg/grpc/user/v3alpha"; - -message User { - - // ID is the read-only unique identifier of the user. - string user_id = 1 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"69629012906488334\""; - } - ]; - // Details provide some base information (such as the last change date) of the user. - zitadel.object.v2.Details details = 2; - // The user's authenticators. They are used to identify and authenticate the user - // during the authentication process. - Authenticators authenticators = 3; - // Contact information for the user. ZITADEL will use this in case of internal notifications. - Contact contact = 4; - // State of the user. - State state = 5; - // The schema the user and it's data is based on. - Schema schema = 6; - // The user's data based on the provided schema. - google.protobuf.Struct data = 7; -} - -enum State { - USER_STATE_UNSPECIFIED = 0; - USER_STATE_ACTIVE = 1; - USER_STATE_INACTIVE = 2; - USER_STATE_DELETED = 3; - USER_STATE_LOCKED = 4; -} - -message Schema { - // The unique identifier of the user schema. - string id = 1 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"69629026806489455\"" - } - ]; - // The human readable name of the user schema. - string type = 2 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"employees\""; - } - ]; - // The revision the user's data is based on of the revision. - uint32 revision = 3 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "7"; - } - ]; -} diff --git a/release-channels.yaml b/release-channels.yaml index e58f35267a..4d2b5bf892 100644 --- a/release-channels.yaml +++ b/release-channels.yaml @@ -1 +1 @@ -stable: "v2.53.9" +stable: "v2.54.8"