diff --git a/console/src/app/pages/users/user-detail/user-detail.module.ts b/console/src/app/pages/users/user-detail/user-detail.module.ts index ee25e75a38..749fb035c2 100644 --- a/console/src/app/pages/users/user-detail/user-detail.module.ts +++ b/console/src/app/pages/users/user-detail/user-detail.module.ts @@ -55,6 +55,7 @@ import { PasswordComponent } from './password/password.component'; import { PasswordlessComponent } from './user-detail/passwordless/passwordless.component'; import { UserDetailComponent } from './user-detail/user-detail.component'; import { UserMfaComponent } from './user-detail/user-mfa/user-mfa.component'; +import { MachineSecretDialogComponent } from './user-detail/machine-secret-dialog/machine-secret-dialog.component'; import { MetadataModule } from 'src/app/modules/metadata/metadata.module'; import { QRCodeModule } from 'angularx-qrcode'; @@ -75,6 +76,7 @@ import { QRCodeModule } from 'angularx-qrcode'; DialogU2FComponent, DialogPasswordlessComponent, AuthFactorDialogComponent, + MachineSecretDialogComponent, ], imports: [ ChangesModule, diff --git a/console/src/app/pages/users/user-detail/user-detail/machine-secret-dialog/machine-secret-dialog.component.html b/console/src/app/pages/users/user-detail/user-detail/machine-secret-dialog/machine-secret-dialog.component.html new file mode 100644 index 0000000000..26cfc44582 --- /dev/null +++ b/console/src/app/pages/users/user-detail/user-detail/machine-secret-dialog/machine-secret-dialog.component.html @@ -0,0 +1,50 @@ +

+ {{ 'USER.SECRETDIALOG.CLIENTSECRET' | translate }} +

+

{{ 'USER.SECRETDIALOG.CLIENTSECRET_DESCRIPTION' | translate }}

+
+
+ ClientId: {{ data.clientId }} + +
+ +
+ ClientSecret: {{ data.clientSecret }} + +
+
+
+ +
diff --git a/console/src/app/pages/users/user-detail/user-detail/machine-secret-dialog/machine-secret-dialog.component.scss b/console/src/app/pages/users/user-detail/user-detail/machine-secret-dialog/machine-secret-dialog.component.scss new file mode 100644 index 0000000000..92a915704b --- /dev/null +++ b/console/src/app/pages/users/user-detail/user-detail/machine-secret-dialog/machine-secret-dialog.component.scss @@ -0,0 +1,37 @@ +.title { + font-size: 1.2rem; +} + +.desc { + font-size: 0.9rem; +} + +.full-width { + width: 100%; +} + +.action { + display: flex; + justify-content: flex-end; + + .ok-button { + margin-left: 0.5rem; + } +} + +.flex { + display: flex; + align-items: center; + border: 1px solid #ffffff20; + border-radius: 0.5rem; + padding-left: 0.5rem; + justify-content: space-between; + + .overflow-auto { + overflow: auto; + + .desc { + font-size: 14px; + } + } +} diff --git a/console/src/app/pages/users/user-detail/user-detail/machine-secret-dialog/machine-secret-dialog.component.spec.ts b/console/src/app/pages/users/user-detail/user-detail/machine-secret-dialog/machine-secret-dialog.component.spec.ts new file mode 100644 index 0000000000..a32950bce2 --- /dev/null +++ b/console/src/app/pages/users/user-detail/user-detail/machine-secret-dialog/machine-secret-dialog.component.spec.ts @@ -0,0 +1,24 @@ +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; + +import { MachineSecretDialogComponent } from './machine-secret-dialog.component'; + +describe('MachineSecretDialogComponent', () => { + let component: MachineSecretDialogComponent; + let fixture: ComponentFixture; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + declarations: [MachineSecretDialogComponent], + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(MachineSecretDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/console/src/app/pages/users/user-detail/user-detail/machine-secret-dialog/machine-secret-dialog.component.ts b/console/src/app/pages/users/user-detail/user-detail/machine-secret-dialog/machine-secret-dialog.component.ts new file mode 100644 index 0000000000..dfe1187c65 --- /dev/null +++ b/console/src/app/pages/users/user-detail/user-detail/machine-secret-dialog/machine-secret-dialog.component.ts @@ -0,0 +1,19 @@ +import { Component, Inject } from '@angular/core'; +import { + MAT_LEGACY_DIALOG_DATA as MAT_DIALOG_DATA, + MatLegacyDialogRef as MatDialogRef, +} from '@angular/material/legacy-dialog'; + +@Component({ + selector: 'cnsl-machine-secret-dialog', + templateUrl: './machine-secret-dialog.component.html', + styleUrls: ['./machine-secret-dialog.component.scss'], +}) +export class MachineSecretDialogComponent { + public copied: string = ''; + constructor(public dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) public data: any) {} + + public closeDialog(): void { + this.dialogRef.close(false); + } +} diff --git a/console/src/app/pages/users/user-detail/user-detail/user-detail.component.html b/console/src/app/pages/users/user-detail/user-detail/user-detail.component.html index b36261768e..3752bb00f3 100644 --- a/console/src/app/pages/users/user-detail/user-detail/user-detail.component.html +++ b/console/src/app/pages/users/user-detail/user-detail/user-detail.component.html @@ -10,6 +10,12 @@ [hasActions]="['user.write$', 'user.write:' + user.id] | hasRole | async" > + + diff --git a/console/src/app/pages/users/user-detail/user-detail/user-detail.component.ts b/console/src/app/pages/users/user-detail/user-detail/user-detail.component.ts index d402a2c02d..0fb8372d8c 100644 --- a/console/src/app/pages/users/user-detail/user-detail/user-detail.component.ts +++ b/console/src/app/pages/users/user-detail/user-detail/user-detail.component.ts @@ -21,6 +21,7 @@ import { Buffer } from 'buffer'; import { EditDialogComponent, EditDialogType } from '../auth-user-detail/edit-dialog/edit-dialog.component'; import { ResendEmailDialogComponent } from '../auth-user-detail/resend-email-dialog/resend-email-dialog.component'; import { LoginPolicy } from 'src/app/proto/generated/zitadel/policy_pb'; +import { MachineSecretDialogComponent } from './machine-secret-dialog/machine-secret-dialog.component'; const GENERAL: SidenavSetting = { id: 'general', i18nKey: 'USER.SETTINGS.GENERAL' }; const GRANTS: SidenavSetting = { id: 'grants', i18nKey: 'USER.SETTINGS.USERGRANTS' }; @@ -189,6 +190,38 @@ export class UserDetailComponent implements OnInit { }); } + public generateMachineSecret(): void { + this.mgmtUserService + .generateMachineSecret(this.user.id) + .then((resp) => { + this.toast.showInfo('USER.TOAST.SECRETGENERATED', true); + console.log(resp.clientSecret); + this.dialog.open(MachineSecretDialogComponent, { + data: { + clientId: resp.clientId, + clientSecret: resp.clientSecret, + }, + width: '400px', + }); + this.refreshUser(); + }) + .catch((error) => { + this.toast.showError(error); + }); + } + + public removeMachineSecret(): void { + this.mgmtUserService + .removeMachineSecret(this.user.id) + .then((resp) => { + this.toast.showInfo('USER.TOAST.SECRETREMOVED', true); + this.refreshUser(); + }) + .catch((error) => { + this.toast.showError(error); + }); + } + public changeState(newState: UserState): void { if (newState === UserState.USER_STATE_ACTIVE) { this.mgmtUserService diff --git a/console/src/app/services/mgmt.service.ts b/console/src/app/services/mgmt.service.ts index ee3840caca..ec1c792b61 100644 --- a/console/src/app/services/mgmt.service.ts +++ b/console/src/app/services/mgmt.service.ts @@ -98,6 +98,8 @@ import { DeactivateUserResponse, DeleteActionRequest, DeleteActionResponse, + GenerateMachineSecretRequest, + GenerateMachineSecretResponse, GenerateOrgDomainValidationRequest, GenerateOrgDomainValidationResponse, GetActionRequest, @@ -310,6 +312,8 @@ import { RemoveIDPFromLoginPolicyResponse, RemoveMachineKeyRequest, RemoveMachineKeyResponse, + RemoveMachineSecretRequest, + RemoveMachineSecretResponse, RemoveMultiFactorFromLoginPolicyRequest, RemoveMultiFactorFromLoginPolicyResponse, RemoveOrgDomainRequest, @@ -717,6 +721,18 @@ export class ManagementService { return this.grpcService.mgmt.unlockUser(req, null).then((resp) => resp.toObject()); } + public generateMachineSecret(userId: string): Promise { + const req = new GenerateMachineSecretRequest(); + req.setUserId(userId); + return this.grpcService.mgmt.generateMachineSecret(req, null).then((resp) => resp.toObject()); + } + + public removeMachineSecret(userId: string): Promise { + const req = new RemoveMachineSecretRequest(); + req.setUserId(userId); + return this.grpcService.mgmt.removeMachineSecret(req, null).then((resp) => resp.toObject()); + } + public getPrivacyPolicy(): Promise { const req = new GetPrivacyPolicyRequest(); return this.grpcService.mgmt.getPrivacyPolicy(req, null).then((resp) => resp.toObject()); diff --git a/console/src/assets/i18n/de.json b/console/src/assets/i18n/de.json index 8e914388da..0af5183ba6 100644 --- a/console/src/assets/i18n/de.json +++ b/console/src/assets/i18n/de.json @@ -240,6 +240,8 @@ "STATE": "Status", "DELETE": "Benutzer löschen", "UNLOCK": "Benutzer entsperren", + "GENERATESECRET": "Client Secret generieren", + "REMOVESECRET": "Client Secret löschen", "LOCKEDDESCRIPTION": "Dieser Benutzer wurde aufgrund der Überschreitung der maximalen Anmeldeversuche gesperrt und muss zur erneuten Verwendung entsperrt werden.", "DELETEACCOUNT": "Account löschen", "DELETEACCOUNT_DESC": "Wenn du diese Aktion ausführst, wirst du abgemeldet und danach keinen Zugriff mehr auf dein Konto haben. Diese Aktion kann nicht rückgängig gemacht werden.", @@ -265,6 +267,10 @@ "DESCRIPTION": "Klicken Sie den untenstehenden Button um ein Verifizierung-E-Mail an die aktuelle Adresse zu versenden oder ändern Sie die Emailadresse in dem Feld.", "NEWEMAIL": "Neue Email" }, + "SECRETDIALOG": { + "CLIENTSECRET": "Client Secret", + "CLIENTSECRET_DESCRIPTION": "Verwahre das Client Secret an einem sicheren Ort, da es nicht mehr angezeigt werden kann, sobald der Dialog geschlossen wird." + }, "TABLE": { "DEACTIVATE": "Deaktivieren", "ACTIVATE": "Aktivieren", @@ -589,7 +595,9 @@ "MACHINEADDED": "Service User erstellt!", "DELETED": "Benutzer erfolgreich gelöscht!", "UNLOCKED": "Benutzer erfolgreich freigeschaltet!", - "PASSWORDLESSREGISTRATIONSENT": "Link via email versendet." + "PASSWORDLESSREGISTRATIONSENT": "Link via email versendet.", + "SECRETGENERATED": "Secret erfolgreich generiert!", + "SECRETREMOVED": "Secret erfolgreich gelöscht!" }, "MEMBERSHIPS": { "TITLE": "ZITADEL Manager-Rollen", diff --git a/console/src/assets/i18n/en.json b/console/src/assets/i18n/en.json index 5cc92ce7cd..33e80560d5 100644 --- a/console/src/assets/i18n/en.json +++ b/console/src/assets/i18n/en.json @@ -240,6 +240,8 @@ "STATE": "Status", "DELETE": "Delete User", "UNLOCK": "Unlock User", + "GENERATESECRET": "Generate Client Secret", + "REMOVESECRET": "Remove Client Secret", "LOCKEDDESCRIPTION": "This user has been locked out due to exceeding the maximum login attempts and must be unlocked to be used again.", "DELETEACCOUNT": "Delete Account", "DELETEACCOUNT_DESC": "If you perform this action, you will be logged out and will no longer have access to your account. This action is not reversible, so please continue with caution.", @@ -265,6 +267,10 @@ "DESCRIPTION": "Click the button below to send a notification to the current email address or change the email address in the field.", "NEWEMAIL": "New email address" }, + "SECRETDIALOG": { + "CLIENTSECRET": "Client Secret", + "CLIENTSECRET_DESCRIPTION": "Keep your client secret at a safe place as it will disappear once the dialog is closed." + }, "TABLE": { "DEACTIVATE": "Deactivate", "ACTIVATE": "Activate", @@ -589,7 +595,9 @@ "MACHINEADDED": "Service User created!", "DELETED": "User deleted successfully!", "UNLOCKED": "User unlocked successfully!", - "PASSWORDLESSREGISTRATIONSENT": "Registration Link sent successfully." + "PASSWORDLESSREGISTRATIONSENT": "Registration Link sent successfully.", + "SECRETGENERATED": "Secret generated successfully!", + "SECRETREMOVED": "Secret removed successfully!" }, "MEMBERSHIPS": { "TITLE": "ZITADEL Manager Roles", diff --git a/console/src/assets/i18n/fr.json b/console/src/assets/i18n/fr.json index dfddd3fa2f..249f4f3001 100644 --- a/console/src/assets/i18n/fr.json +++ b/console/src/assets/i18n/fr.json @@ -240,6 +240,8 @@ "STATE": "Statut", "DELETE": "Supprimer l'utilisateur", "UNLOCK": "Déverrouiller l'utilisateur", + "GENERATESECRET": "Générer Client Secret", + "REMOVESECRET": "Supprimer Client Secret", "LOCKEDDESCRIPTION": "Cet utilisateur a été verrouillé pour avoir dépassé le nombre maximum de tentatives de connexion et doit être déverrouillé pour être à nouveau utilisé.", "DELETEACCOUNT": "Supprimer le compte", "DELETEACCOUNT_DESC": "Si vous effectuez cette action, vous serez déconnecté et n'aurez plus accès à votre compte. Cette action n'est pas réversible, veuillez donc continuer avec prudence.", @@ -265,6 +267,10 @@ "DESCRIPTION": "Cliquez sur le bouton ci-dessous pour envoyer une notification à l'adresse e-mail actuelle ou modifier l'adresse e-mail dans le champ.", "NEWEMAIL": "Nouvelle adresse e-mail" }, + "SECRETDIALOG": { + "CLIENTSECRET": "Client Secret", + "CLIENTSECRET_DESCRIPTION": "Conservez votre secret client dans un endroit sûr car il disparaîtra une fois la boîte de dialogue fermée." + }, "TABLE": { "DEACTIVATE": "Désactiver", "ACTIVATE": "Activer", @@ -589,7 +595,9 @@ "MACHINEADDED": "Utilisateur de service créé !", "DELETED": "Utilisateur supprimé avec succès !", "UNLOCKED": "Utilisateur déverrouillé avec succès !", - "PASSWORDLESSREGISTRATIONSENT": "Lien d'enregistrement envoyé avec succès." + "PASSWORDLESSREGISTRATIONSENT": "Lien d'enregistrement envoyé avec succès.", + "SECRETGENERATED": "Secret généré avec succès !", + "SECRETREMOVED": "Secret supprimé avec succès !" }, "MEMBERSHIPS": { "TITLE": "Rôles du gestionnaire ZITADEL", diff --git a/console/src/assets/i18n/it.json b/console/src/assets/i18n/it.json index a2e1e506c9..4f599798bd 100644 --- a/console/src/assets/i18n/it.json +++ b/console/src/assets/i18n/it.json @@ -240,6 +240,8 @@ "STATE": "Stato", "DELETE": "Elimina utente", "UNLOCK": "Sblocca utente", + "GENERATESECRET": "Genera Client Secret", + "REMOVESECRET": "Elimina Client Secret", "LOCKEDDESCRIPTION": "Questo utente \u00e8 stato bloccato a causa del superamento dei tentativi massimi di accesso e deve essere sbloccato per essere utilizzato di nuovo.", "DELETEACCOUNT": "Elimina account personale", "DELETEACCOUNT_DESC": "Se esegui questa azione, sarai disconnesso e non avrai più accesso al tuo account. Questa azione non può essere invertita.", @@ -265,6 +267,10 @@ "DESCRIPTION": "Clicca il pulsante qui sotto per inviare una notifica all'indirizzo email corrente o cambiare l'indirizzo email nel campo.", "NEWEMAIL": "Nuovo indirizzo e-mail" }, + "SECRETDIALOG": { + "CLIENTSECRET": "Client Secret", + "CLIENTSECRET_DESCRIPTION": "Salvate il Client Secret in un luogo sicuro, perch\u00e9 non sarà più disponibile dopo aver chiuso la finestra di dialogo" + }, "TABLE": { "DEACTIVATE": "Disattiva", "ACTIVATE": "Attiva", @@ -589,7 +595,9 @@ "MACHINEADDED": "Utente di servizio creato!", "DELETED": "Utente cancellato con successo!", "UNLOCKED": "Utente sbloccato con successo!", - "PASSWORDLESSREGISTRATIONSENT": "Link per la registrazione inviato con successo." + "PASSWORDLESSREGISTRATIONSENT": "Link per la registrazione inviato con successo.", + "SECRETGENERATED": "Secret generato con successo!", + "SECRETREMOVED": "Secret rimosso con successo!" }, "MEMBERSHIPS": { "TITLE": "Memberships di ZITADEL", diff --git a/console/src/assets/i18n/zh.json b/console/src/assets/i18n/zh.json index c5cc33f56f..93ed1bd6d6 100644 --- a/console/src/assets/i18n/zh.json +++ b/console/src/assets/i18n/zh.json @@ -240,6 +240,8 @@ "STATE": "状态", "DELETE": "删除用户", "UNLOCK": "解锁用户", + "GENERATESECRET": "生成客户密匙", + "REMOVESECRET": "删除客户密匙", "LOCKEDDESCRIPTION": "此用户因超过最大登录尝试次数而被锁定,必须解锁才能再次使用。", "DELETEACCOUNT": "删除账户", "DELETEACCOUNT_DESC": "如果您执行此操作,您将被注销并且无法再访问您的帐户。此操作不可逆,因此请谨慎操作。", @@ -265,6 +267,10 @@ "DESCRIPTION": "单击下面的按钮可向当前电子邮件地址发送通知或更改电子邮件地址。", "NEWEMAIL": "新的电子邮件地址" }, + "SECRETDIALOG": { + "CLIENTSECRET": "客户端秘钥", + "CLIENTSECRET_DESCRIPTION": "将您的客户保密在一个安全的地方,因为一旦对话框关闭,便无法再次查看。" + }, "TABLE": { "DEACTIVATE": "停用", "ACTIVATE": "启用", @@ -589,7 +595,9 @@ "MACHINEADDED": "服务用户已创建成功!", "DELETED": "用户删除成功!", "UNLOCKED": "用户解锁成功!", - "PASSWORDLESSREGISTRATIONSENT": "注册链接发送成功。" + "PASSWORDLESSREGISTRATIONSENT": "注册链接发送成功。", + "SECRETGENERATED": "秘密成功生成!", + "SECRETREMOVED": "秘密被成功删除!" }, "MEMBERSHIPS": { "TITLE": "CITADEL 管理角色", diff --git a/docs/docs/apis/openidoauth/endpoints.mdx b/docs/docs/apis/openidoauth/endpoints.mdx index 94a0168ac6..aa921d2562 100644 --- a/docs/docs/apis/openidoauth/endpoints.mdx +++ b/docs/docs/apis/openidoauth/endpoints.mdx @@ -325,6 +325,53 @@ Send a `client_assertion` as JWT for us to validate the signature against the re | refresh_token | An new opaque refresh_token. | | token_type | Type of the `access_token`. Value is always `Bearer` | +### Client Credentials Grant + +#### Required request Parameters + +| Parameter | Description | +| ---------- | ----------------------------------------------------------------------------------------------------------------------- | +| grant_type | Must be `client_credentials` | +| scope | [Scopes](scopes) you would like to request from ZITADEL. Scopes are space delimited, e.g. `openid profile` | + +Additionally, you need to authenticate your client by either sending `client_id` and `client_secret` as Basic Auth Header. +Check [Client Secret Basic Auth Method](authn-methods#client-secret-basic) on how to build it correctly. + +```BASH +curl --request POST \ + --url {your_domain}/oauth/v2/token \ + --header 'Content-Type: application/x-www-form-urlencoded' \ + --header 'Authorization: Basic ${BASIC_AUTH}' \ + --data grant_type=client_credentials \ + --data scope=openid profile +``` + +Or you can also send your `client_id` and `client_secret` as parameters in the body: + +| Parameter | Description | +| ------------- | -------------------------------- | +| client_id | client_id of the application | +| client_secret | client_secret of the application | + +```BASH +curl --request POST \ + --url {your_domain}/oauth/v2/token \ + --header 'Content-Type: application/x-www-form-urlencoded' \ + --data grant_type=client_credentials \ + --data client_id=${CLIENT_ID} \ + --data client_secret=${CLIENT_SECRET} \ + --data scope=openid profile +``` + +#### Successful Client Credentials response {#token-client-credentials-response} + +| Property | Description | +| ------------ | ------------------------------------------------------------------------------------- | +| access_token | An `access_token` as JWT or opaque token | +| expires_in | Number of second until the expiration of the `access_token` | +| scope | Scopes of the `access_token`. These might differ from the provided `scope` parameter. | +| token_type | Type of the `access_token`. Value is always `Bearer` | + ### Error response | error_type | Possible reason | diff --git a/docs/docs/apis/openidoauth/grant-types.md b/docs/docs/apis/openidoauth/grant-types.md index dd9d693b54..560aa46aa9 100644 --- a/docs/docs/apis/openidoauth/grant-types.md +++ b/docs/docs/apis/openidoauth/grant-types.md @@ -8,14 +8,14 @@ For a list of supported or unsupported `Grant Types` please have a look at the t |:------------------------------------------------------|:--------------------| | Authorization Code | yes | | Authorization Code with PKCE | yes | -| Client Credentials | no | +| Client Credentials | yes | | Device Authorization | under consideration | | Implicit | yes | | JSON Web Token (JWT) Profile | yes | | Refresh Token | yes | | Resource Owner Password Credentials | no | -| Security Assertion Markup Language (SAML) 2.0 Profile | no | -| Token Exchange | no | +| Security Assertion Markup Language (SAML) 2.0 Profile | no | +| Token Exchange | no | ## Authorization Code @@ -131,4 +131,4 @@ Find out how to use it on the [token endpoint](endpoints#token_endpoint) or the > Due to growing security concerns we do not support this grant type. With OAuth 2.1 it looks like this grant will be removed. -**Link to spec.** [OThe OAuth 2.0 Authorization Framework Section 1.3.3](https://tools.ietf.org/html/rfc6749#section-1.3.3) \ No newline at end of file +**Link to spec.** [OThe OAuth 2.0 Authorization Framework Section 1.3.3](https://tools.ietf.org/html/rfc6749#section-1.3.3) diff --git a/docs/docs/apis/proto/management.md b/docs/docs/apis/proto/management.md index ae987b1f2f..e237cbfb43 100644 --- a/docs/docs/apis/proto/management.md +++ b/docs/docs/apis/proto/management.md @@ -581,6 +581,30 @@ Changes a machine user PUT: /users/{user_id}/machine +### GenerateMachineSecret + +> **rpc** GenerateMachineSecret([GenerateMachineSecretRequest](#generatemachinesecretrequest)) +[GenerateMachineSecretResponse](#generatemachinesecretresponse) + +Generates and sets a new machine secret + + + + PUT: /users/{user_id}/secret + + +### RemoveMachineSecret + +> **rpc** RemoveMachineSecret([RemoveMachineSecretRequest](#removemachinesecretrequest)) +[RemoveMachineSecretResponse](#removemachinesecretresponse) + +Removes the machine secret + + + + DELETE: /users/{user_id}/secret + + ### GetMachineKeyByIDs > **rpc** GetMachineKeyByIDs([GetMachineKeyByIDsRequest](#getmachinekeybyidsrequest)) @@ -4425,6 +4449,30 @@ This is an empty request +### GenerateMachineSecretRequest + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| user_id | string | - | string.min_len: 1
| + + + + +### GenerateMachineSecretResponse + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| client_id | string | - | | +| client_secret | string | - | | +| details | zitadel.v1.ObjectDetails | - | | + + + + ### GenerateOrgDomainValidationRequest @@ -7175,6 +7223,28 @@ This is an empty request +### RemoveMachineSecretRequest + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| user_id | string | - | string.min_len: 1
string.max_len: 200
| + + + + +### RemoveMachineSecretResponse + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| details | zitadel.v1.ObjectDetails | - | | + + + + ### RemoveMultiFactorFromLoginPolicyRequest diff --git a/docs/docs/apis/proto/user.md b/docs/docs/apis/proto/user.md index b3fd8e9bd3..b4e15c0841 100644 --- a/docs/docs/apis/proto/user.md +++ b/docs/docs/apis/proto/user.md @@ -133,6 +133,7 @@ title: zitadel/user.proto | ----- | ---- | ----------- | ----------- | | name | string | - | | | description | string | - | | +| has_secret | bool | - | | diff --git a/docs/docs/guides/integrate/client-credentials.md b/docs/docs/guides/integrate/client-credentials.md new file mode 100644 index 0000000000..5f09fb16a5 --- /dev/null +++ b/docs/docs/guides/integrate/client-credentials.md @@ -0,0 +1,88 @@ +--- +title: Client Credentials with Service Users +--- + +This is a guide on how to use Client Credentials with service users in ZITADEL. You can read more about users [here](/concepts/structure/users.md). + +In ZITADEL, the Client Credentials grant can be used for this non-interactive authentication as alternative to the [JWT profile authentication](serviceusers). + +## Create a Service User with a Secret + +1. Navigate to Service Users +2. Click on **New** +3. Enter a username and a display name +4. Click on **Create** +5. Open **Actions** in the top right corner and click on **Generate Client Secret** +6. Copy the **ClientID** and **ClientSecret** from the dialog + +:::note +Be sure to copy in particular the ClientSecret. You won't be able to retrieve it again. +If you lose it, you will have to generate a new one. +::: + +![Create new service user](/img/console_serviceusers_secret.gif) + +## Grant role for ZITADEL + +To be able to access the ZITADEL APIs your service user needs permissions to ZITADEL. + +1. Go to the detail page of your organization +2. Click in the top right corner the "+" button +3. Search for your service user +4. Give the user the role you need, for the example we choose Org Owner (More about [ZITADEL Permissions](../manage/console/managers)) + +![Add org owner to service user](/img/guides/console-service-user-org-owner.gif) + +## Authenticating a service user + +In this step we will authenticate a service user and receive an access_token to use against the ZITADEL API. + +You will need to craft a POST request to ZITADEL's token endpoint: + +```bash +curl --request POST \ + --url https://{your_domain}.zitadel.cloud/oauth/v2/token \ + --header 'Content-Type: application/x-www-form-urlencoded' \ + --header 'Authorization: Basic ${BASIC_AUTH}' \ + --data grant_type=client_credentials \ + --data scope='openid profile email urn:zitadel:iam:org:project:id:zitadel:aud' +``` + +* `grant_type` should be set to `client_credentials` +* `scope` should contain any [Scopes](../../apis/openidoauth/scopes) you want to include, but must include `openid`. For this example, please include `profile`, `email` + and `urn:zitadel:iam:org:project:id:zitadel:aud`. The latter provides access to the ZITADEL API. + +You should receive a successful response with `access_token`, `token_type` and time to expiry in seconds as `expires_in`. + +```bash +HTTP/1.1 200 OK +Content-Type: application/json + +{ + "access_token": "MtjHodGy4zxKylDOhg6kW90WeEQs2q...", + "token_type": "Bearer", + "expires_in": 43199 +} +``` + +## Call ZITADEL API with Token + +Because the received Token includes the `urn:zitadel:iam:org:project:id:zitadel:aud` scope, we can send it in your requests to the ZITADEL API as Authorization Header. +In this example we read the organization of the service user. + +```bash +curl --request GET \ + --url {your-domain}/management/v1/orgs/me \ + --header 'Authorization: Bearer ${TOKEN}' +``` + +## Summary + +* With service users you can secure machine-to-machine communication +* Client Credentials provide an alternative way to JWT Profile for service user authentication +* After successful authorization you can use an access token like for human users + +Where to go from here: + +* Management API +* Securing backend API diff --git a/docs/sidebars.js b/docs/sidebars.js index 189c86b3ea..f769c2546d 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -109,6 +109,7 @@ module.exports = { items: [ "guides/integrate/serviceusers", "guides/integrate/access-zitadel-apis", + "guides/integrate/client-credentials", "guides/integrate/pat", "guides/integrate/access-zitadel-system-api", "guides/integrate/export-and-import", diff --git a/docs/static/img/console_serviceusers_secret.gif b/docs/static/img/console_serviceusers_secret.gif new file mode 100644 index 0000000000..b44c5554a3 Binary files /dev/null and b/docs/static/img/console_serviceusers_secret.gif differ diff --git a/go.mod b/go.mod index 79a60c7087..ef22f60470 100644 --- a/go.mod +++ b/go.mod @@ -55,7 +55,7 @@ require ( github.com/superseriousbusiness/exifremove v0.0.0-20210330092427-6acd27eac203 github.com/ttacon/libphonenumber v1.2.1 github.com/zitadel/logging v0.3.4 - github.com/zitadel/oidc/v2 v2.0.0-dynamic-issuer.6 + github.com/zitadel/oidc/v2 v2.0.0-dynamic-issuer.7 github.com/zitadel/saml v0.0.9 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.27.0 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.27.0 diff --git a/go.sum b/go.sum index 2d1a01e5d1..9a3b391ade 100644 --- a/go.sum +++ b/go.sum @@ -906,8 +906,8 @@ github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5t github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= github.com/zitadel/logging v0.3.4 h1:9hZsTjMMTE3X2LUi0xcF9Q9EdLo+FAezeu52ireBbHM= github.com/zitadel/logging v0.3.4/go.mod h1:aPpLQhE+v6ocNK0TWrBrd363hZ95KcI17Q1ixAQwZF0= -github.com/zitadel/oidc/v2 v2.0.0-dynamic-issuer.6 h1:DGTEizuL1npVfmw+i6lFWxrEdKNUjEFpqGEAZH7amfo= -github.com/zitadel/oidc/v2 v2.0.0-dynamic-issuer.6/go.mod h1:2jHMP6o/WK0EmcNJkz+FSpjeqcCuQG9YqqqzKZkfgIE= +github.com/zitadel/oidc/v2 v2.0.0-dynamic-issuer.7 h1:CGs4gdoSrZZyZM5pGeXCf8FH12r4r8hpJL/wUR3PxRA= +github.com/zitadel/oidc/v2 v2.0.0-dynamic-issuer.7/go.mod h1:2jHMP6o/WK0EmcNJkz+FSpjeqcCuQG9YqqqzKZkfgIE= github.com/zitadel/saml v0.0.9 h1:q7FRu52Wm2S5rsSGuzR2nYhEClvexga8bwnGrBL7Bbw= github.com/zitadel/saml v0.0.9/go.mod h1:DIy/ln32rNYv/bIBA8uOB6Y2JmxjZldDYBeMNn7YyeQ= go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= diff --git a/internal/api/grpc/management/user.go b/internal/api/grpc/management/user.go index 46d12fc848..e2a0ad3c5a 100644 --- a/internal/api/grpc/management/user.go +++ b/internal/api/grpc/management/user.go @@ -756,6 +756,34 @@ func (s *Server) RemoveMachineKey(ctx context.Context, req *mgmt_pb.RemoveMachin }, nil } +func (s *Server) GenerateMachineSecret(ctx context.Context, req *mgmt_pb.GenerateMachineSecretRequest) (*mgmt_pb.GenerateMachineSecretResponse, error) { + // use SecretGeneratorTypeAppSecret as the secrets will be used in the client_credentials grant like a client secret + secretGenerator, err := s.query.InitHashGenerator(ctx, domain.SecretGeneratorTypeAppSecret, s.passwordHashAlg) + if err != nil { + return nil, err + } + set := new(command.GenerateMachineSecret) + details, err := s.command.GenerateMachineSecret(ctx, req.UserId, authz.GetCtxData(ctx).OrgID, secretGenerator, set) + if err != nil { + return nil, err + } + return &mgmt_pb.GenerateMachineSecretResponse{ + ClientId: set.ClientID, + ClientSecret: set.ClientSecret, + Details: obj_grpc.DomainToAddDetailsPb(details), + }, nil +} + +func (s *Server) RemoveMachineSecret(ctx context.Context, req *mgmt_pb.RemoveMachineSecretRequest) (*mgmt_pb.RemoveMachineSecretResponse, error) { + objectDetails, err := s.command.RemoveMachineSecret(ctx, req.UserId, authz.GetCtxData(ctx).OrgID) + if err != nil { + return nil, err + } + return &mgmt_pb.RemoveMachineSecretResponse{ + Details: obj_grpc.DomainToChangeDetailsPb(objectDetails), + }, nil +} + func (s *Server) GetPersonalAccessTokenByIDs(ctx context.Context, req *mgmt_pb.GetPersonalAccessTokenByIDsRequest) (*mgmt_pb.GetPersonalAccessTokenByIDsResponse, error) { resourceOwner, err := query.NewPersonalAccessTokenResourceOwnerSearchQuery(authz.GetCtxData(ctx).OrgID) if err != nil { diff --git a/internal/api/grpc/user/converter.go b/internal/api/grpc/user/converter.go index 1b268498da..608e8c8c93 100644 --- a/internal/api/grpc/user/converter.go +++ b/internal/api/grpc/user/converter.go @@ -72,6 +72,7 @@ func MachineToPb(view *query.Machine) *user_pb.Machine { return &user_pb.Machine{ Name: view.Name, Description: view.Description, + HasSecret: view.HasSecret, } } diff --git a/internal/api/oidc/client.go b/internal/api/oidc/client.go index cc28b87010..9d7499d993 100644 --- a/internal/api/oidc/client.go +++ b/internal/api/oidc/client.go @@ -94,29 +94,7 @@ func (o *OPStorage) ValidateJWTProfileScopes(ctx context.Context, subject string if err != nil { return nil, err } - for i := len(scopes) - 1; i >= 0; i-- { - scope := scopes[i] - if strings.HasPrefix(scope, domain.OrgDomainPrimaryScope) { - var orgID string - org, err := o.query.OrgByPrimaryDomain(ctx, strings.TrimPrefix(scope, domain.OrgDomainPrimaryScope)) - if err == nil { - orgID = org.ID - } - if orgID != user.ResourceOwner { - scopes[i] = scopes[len(scopes)-1] - scopes[len(scopes)-1] = "" - scopes = scopes[:len(scopes)-1] - } - } - if strings.HasPrefix(scope, domain.OrgIDScope) { - if strings.TrimPrefix(scope, domain.OrgIDScope) != user.ResourceOwner { - scopes[i] = scopes[len(scopes)-1] - scopes[len(scopes)-1] = "" - scopes = scopes[:len(scopes)-1] - } - } - } - return scopes, nil + return o.checkOrgScopes(ctx, user, scopes) } func (o *OPStorage) AuthorizeClientIDSecret(ctx context.Context, id string, secret string) (err error) { @@ -209,6 +187,68 @@ func (o *OPStorage) SetIntrospectionFromToken(ctx context.Context, introspection return errors.ThrowPermissionDenied(nil, "OIDC-sdg3G", "token is not valid for this client") } +func (o *OPStorage) ClientCredentialsTokenRequest(ctx context.Context, clientID string, scope []string) (op.TokenRequest, error) { + loginname, err := query.NewUserLoginNamesSearchQuery(clientID) + if err != nil { + return nil, err + } + user, err := o.query.GetUser(ctx, false, false, loginname) + if err != nil { + return nil, err + } + scope, err = o.checkOrgScopes(ctx, user, scope) + if err != nil { + return nil, err + } + return &clientCredentialsRequest{ + sub: user.ID, + scopes: scope, + }, nil +} + +func (o *OPStorage) ClientCredentials(ctx context.Context, clientID, clientSecret string) (op.Client, error) { + loginname, err := query.NewUserLoginNamesSearchQuery(clientID) + if err != nil { + return nil, err + } + user, err := o.query.GetUser(ctx, false, false, loginname) + if err != nil { + return nil, err + } + if _, err := o.command.VerifyMachineSecret(ctx, user.ID, user.ResourceOwner, clientSecret); err != nil { + return nil, err + } + return &clientCredentialsClient{ + id: clientID, + }, nil +} + +func (o *OPStorage) checkOrgScopes(ctx context.Context, user *query.User, scopes []string) ([]string, error) { + for i := len(scopes) - 1; i >= 0; i-- { + scope := scopes[i] + if strings.HasPrefix(scope, domain.OrgDomainPrimaryScope) { + var orgID string + org, err := o.query.OrgByPrimaryDomain(ctx, strings.TrimPrefix(scope, domain.OrgDomainPrimaryScope)) + if err == nil { + orgID = org.ID + } + if orgID != user.ResourceOwner { + scopes[i] = scopes[len(scopes)-1] + scopes[len(scopes)-1] = "" + scopes = scopes[:len(scopes)-1] + } + } + if strings.HasPrefix(scope, domain.OrgIDScope) { + if strings.TrimPrefix(scope, domain.OrgIDScope) != user.ResourceOwner { + scopes[i] = scopes[len(scopes)-1] + scopes[len(scopes)-1] = "" + scopes = scopes[:len(scopes)-1] + } + } + } + return scopes, nil +} + func (o *OPStorage) setUserinfo(ctx context.Context, userInfo oidc.UserInfoSetter, userID, applicationID string, scopes []string) (err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() diff --git a/internal/api/oidc/client_credentials.go b/internal/api/oidc/client_credentials.go new file mode 100644 index 0000000000..f01407f94e --- /dev/null +++ b/internal/api/oidc/client_credentials.go @@ -0,0 +1,120 @@ +package oidc + +import ( + "time" + + "github.com/zitadel/oidc/v2/pkg/oidc" + "github.com/zitadel/oidc/v2/pkg/op" +) + +type clientCredentialsRequest struct { + sub string + scopes []string +} + +func (c *clientCredentialsRequest) GetSubject() string { + return c.sub +} + +// GetAudience returns the audience for token to be created because of the client credentials request +// return nil as the audience is set during the token creation in command.addUserToken +func (c *clientCredentialsRequest) GetAudience() []string { + return nil +} + +func (c *clientCredentialsRequest) GetScopes() []string { + return c.scopes +} + +type clientCredentialsClient struct { + id string +} + +// AccessTokenType returns the AccessTokenType for the token to be created because of the client credentials request +// machine users currently only have opaque tokens ([op.AccessTokenTypeBearer]) +func (c *clientCredentialsClient) AccessTokenType() op.AccessTokenType { + return op.AccessTokenTypeBearer +} + +// GetID returns the client_id (username of the machine user) for the token to be created because of the client credentials request +func (c *clientCredentialsClient) GetID() string { + return c.id +} + +// RedirectURIs returns nil as there are no redirect uris +func (c *clientCredentialsClient) RedirectURIs() []string { + return nil +} + +// PostLogoutRedirectURIs returns nil as there are no logout redirect uris +func (c *clientCredentialsClient) PostLogoutRedirectURIs() []string { + return nil +} + +// ApplicationType returns [op.ApplicationTypeWeb] as the machine users is a confidential client +func (c *clientCredentialsClient) ApplicationType() op.ApplicationType { + return op.ApplicationTypeWeb +} + +// AuthMethod returns the allowed auth method type for machine user. +// It returns Basic Auth +func (c *clientCredentialsClient) AuthMethod() oidc.AuthMethod { + return oidc.AuthMethodBasic +} + +// ResponseTypes returns nil as the types are only required for an authorization request +func (c *clientCredentialsClient) ResponseTypes() []oidc.ResponseType { + return nil +} + +// GrantTypes returns the grant types supported by the machine users, which is currently only client credentials ([oidc.GrantTypeClientCredentials]) +func (c *clientCredentialsClient) GrantTypes() []oidc.GrantType { + return []oidc.GrantType{ + oidc.GrantTypeClientCredentials, + } +} + +// LoginURL returns an empty string as there is no login UI involved +func (c *clientCredentialsClient) LoginURL(_ string) string { + return "" +} + +// IDTokenLifetime returns 0 as there is no id_token issued +func (c *clientCredentialsClient) IDTokenLifetime() time.Duration { + return 0 +} + +// DevMode returns false as there is no dev mode +func (c *clientCredentialsClient) DevMode() bool { + return false +} + +// RestrictAdditionalIdTokenScopes returns nil as no id_token is issued +func (c *clientCredentialsClient) RestrictAdditionalIdTokenScopes() func(scopes []string) []string { + return nil +} + +// RestrictAdditionalAccessTokenScopes returns the scope allowed for the token to be created because of the client credentials request +// currently it allows all scopes to be used in the access token +func (c *clientCredentialsClient) RestrictAdditionalAccessTokenScopes() func(scopes []string) []string { + return func(scopes []string) []string { + return scopes + } +} + +// IsScopeAllowed returns null false as the check is executed during the auth request validation +func (c *clientCredentialsClient) IsScopeAllowed(scope string) bool { + return false +} + +// IDTokenUserinfoClaimsAssertion returns null false as no id_token is issued +func (c *clientCredentialsClient) IDTokenUserinfoClaimsAssertion() bool { + return false +} + +// ClockSkew enable handling clock skew of the token validation. The duration (0-5s) will be added to exp claim and subtracted from iats, +// auth_time and nbf of the token to be created because of the client credentials request. +// It returns 0 as clock skew is not implemented on machine users. +func (c *clientCredentialsClient) ClockSkew() time.Duration { + return 0 +} diff --git a/internal/command/user_machine_model.go b/internal/command/user_machine_model.go index 0ab2f880ec..822b76b968 100644 --- a/internal/command/user_machine_model.go +++ b/internal/command/user_machine_model.go @@ -3,6 +3,7 @@ package command import ( "context" + "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/domain" @@ -17,6 +18,8 @@ type MachineWriteModel struct { Name string Description string UserState domain.UserState + + ClientSecret *crypto.CryptoValue } func NewMachineWriteModel(userID, resourceOwner string) *MachineWriteModel { @@ -63,6 +66,10 @@ func (wm *MachineWriteModel) Reduce() error { } case *user.UserRemovedEvent: wm.UserState = domain.UserStateDeleted + case *user.MachineSecretSetEvent: + wm.ClientSecret = e.ClientSecret + case *user.MachineSecretRemovedEvent: + wm.ClientSecret = nil } } return wm.WriteModel.Reduce() @@ -81,7 +88,9 @@ func (wm *MachineWriteModel) Query() *eventstore.SearchQueryBuilder { user.UserUnlockedType, user.UserDeactivatedType, user.UserReactivatedType, - user.UserRemovedType). + user.UserRemovedType, + user.MachineSecretSetType, + user.MachineSecretRemovedType). Builder() } diff --git a/internal/command/user_machine_secret.go b/internal/command/user_machine_secret.go new file mode 100644 index 0000000000..577e18d3b1 --- /dev/null +++ b/internal/command/user_machine_secret.go @@ -0,0 +1,171 @@ +package command + +import ( + "context" + + "github.com/zitadel/logging" + + "github.com/zitadel/zitadel/internal/command/preparation" + "github.com/zitadel/zitadel/internal/crypto" + "github.com/zitadel/zitadel/internal/domain" + caos_errs "github.com/zitadel/zitadel/internal/errors" + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/repository/user" +) + +type GenerateMachineSecret struct { + ClientID string + ClientSecret string +} + +func (c *Commands) GenerateMachineSecret(ctx context.Context, userID string, resourceOwner string, generator crypto.Generator, set *GenerateMachineSecret) (*domain.ObjectDetails, error) { + agg := user.NewAggregate(userID, resourceOwner) + cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, prepareGenerateMachineSecret(agg, generator, set)) + if err != nil { + return nil, err + } + + events, err := c.eventstore.Push(ctx, cmds...) + if err != nil { + return nil, err + } + + return &domain.ObjectDetails{ + Sequence: events[len(events)-1].Sequence(), + EventDate: events[len(events)-1].CreationDate(), + ResourceOwner: events[len(events)-1].Aggregate().ResourceOwner, + }, nil +} + +func prepareGenerateMachineSecret(a *user.Aggregate, generator crypto.Generator, set *GenerateMachineSecret) preparation.Validation { + return func() (_ preparation.CreateCommands, err error) { + if a.ResourceOwner == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-x0992n", "Errors.ResourceOwnerMissing") + } + if a.ID == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-bzoqjs", "Errors.User.UserIDMissing") + } + return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { + writeModel, err := getMachineWriteModel(ctx, a.ID, a.ResourceOwner, filter) + if err != nil { + return nil, err + } + if !isUserStateExists(writeModel.UserState) { + return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-x8910n", "Errors.User.NotExisting") + } + set.ClientID = writeModel.UserName + + clientSecret, secretString, err := domain.NewMachineClientSecret(generator) + if err != nil { + return nil, err + } + set.ClientSecret = secretString + + return []eventstore.Command{ + user.NewMachineSecretSetEvent(ctx, &a.Aggregate, clientSecret), + }, nil + }, nil + } +} + +func (c *Commands) RemoveMachineSecret(ctx context.Context, userID string, resourceOwner string) (*domain.ObjectDetails, error) { + agg := user.NewAggregate(userID, resourceOwner) + cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, prepareRemoveMachineSecret(agg)) + if err != nil { + return nil, err + } + + events, err := c.eventstore.Push(ctx, cmds...) + if err != nil { + return nil, err + } + + return &domain.ObjectDetails{ + Sequence: events[len(events)-1].Sequence(), + EventDate: events[len(events)-1].CreationDate(), + ResourceOwner: events[len(events)-1].Aggregate().ResourceOwner, + }, nil +} + +func prepareRemoveMachineSecret(a *user.Aggregate) preparation.Validation { + return func() (_ preparation.CreateCommands, err error) { + if a.ResourceOwner == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-0qp2hus", "Errors.ResourceOwnerMissing") + } + if a.ID == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-bzosjs", "Errors.User.UserIDMissing") + } + return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { + writeModel, err := getMachineWriteModel(ctx, a.ID, a.ResourceOwner, filter) + if err != nil { + return nil, err + } + if !isUserStateExists(writeModel.UserState) { + return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-x7s802", "Errors.User.NotExisting") + } + if writeModel.ClientSecret == nil { + return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-coi82n", "Errors.User.Machine.Secret.NotExisting") + } + return []eventstore.Command{ + user.NewMachineSecretRemovedEvent(ctx, &a.Aggregate), + }, nil + }, nil + } +} + +func (c *Commands) VerifyMachineSecret(ctx context.Context, userID string, resourceOwner string, secret string) (*domain.ObjectDetails, error) { + agg := user.NewAggregate(userID, resourceOwner) + cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, prepareVerifyMachineSecret(agg, secret, c.userPasswordAlg)) + if err != nil { + return nil, err + } + + events, err := c.eventstore.Push(ctx, cmds...) + for _, cmd := range cmds { + if cmd.Type() == user.MachineSecretCheckFailedType { + logging.OnError(err).Error("could not push event MachineSecretCheckFailed") + return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-3kjh", "Errors.User.Machine.Secret.Invalid") + } + } + if err != nil { + return nil, err + } + + return &domain.ObjectDetails{ + Sequence: events[len(events)-1].Sequence(), + EventDate: events[len(events)-1].CreationDate(), + ResourceOwner: events[len(events)-1].Aggregate().ResourceOwner, + }, nil +} + +func prepareVerifyMachineSecret(a *user.Aggregate, secret string, algorithm crypto.HashAlgorithm) preparation.Validation { + return func() (_ preparation.CreateCommands, err error) { + if a.ResourceOwner == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-0qp2hus", "Errors.ResourceOwnerMissing") + } + if a.ID == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-bzosjs", "Errors.User.UserIDMissing") + } + return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { + writeModel, err := getMachineWriteModel(ctx, a.ID, a.ResourceOwner, filter) + if err != nil { + return nil, err + } + if !isUserStateExists(writeModel.UserState) { + return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-569sh2o", "Errors.User.NotExisting") + } + if writeModel.ClientSecret == nil { + return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-x8910n", "Errors.User.Machine.Secret.NotExisting") + } + err = crypto.CompareHash(writeModel.ClientSecret, []byte(secret), algorithm) + if err == nil { + return []eventstore.Command{ + user.NewMachineSecretCheckSucceededEvent(ctx, &a.Aggregate), + }, nil + } + return []eventstore.Command{ + user.NewMachineSecretCheckFailedEvent(ctx, &a.Aggregate), + }, nil + }, nil + } +} diff --git a/internal/command/user_machine_secret_test.go b/internal/command/user_machine_secret_test.go new file mode 100644 index 0000000000..d0f93afd72 --- /dev/null +++ b/internal/command/user_machine_secret_test.go @@ -0,0 +1,543 @@ +package command + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/zitadel/zitadel/internal/crypto" + "github.com/zitadel/zitadel/internal/domain" + caos_errs "github.com/zitadel/zitadel/internal/errors" + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/eventstore/repository" + "github.com/zitadel/zitadel/internal/repository/user" +) + +func TestCommandSide_GenerateMachineSecret(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + userID string + resourceOwner string + generator crypto.Generator + set *GenerateMachineSecret + } + type res struct { + want *domain.ObjectDetails + secret *GenerateMachineSecret + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "user invalid, invalid argument error userID", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + userID: "", + resourceOwner: "org1", + generator: GetMockSecretGenerator(t), + set: nil, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "user invalid, invalid argument error resourceowner", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + resourceOwner: "", + generator: GetMockSecretGenerator(t), + set: nil, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "user not existing, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + resourceOwner: "org1", + generator: GetMockSecretGenerator(t), + set: nil, + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "add machine secret, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewMachineAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "user1", + "username", + "user", + false, + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + user.NewMachineSecretSetEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("a"), + }, + ), + ), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + resourceOwner: "org1", + generator: GetMockSecretGenerator(t), + set: &GenerateMachineSecret{}, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + secret: &GenerateMachineSecret{ + ClientID: "user1", + ClientSecret: "a", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.GenerateMachineSecret(tt.args.ctx, tt.args.userID, tt.args.resourceOwner, tt.args.generator, tt.args.set) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assert.Equal(t, tt.res.want, got) + assert.Equal(t, tt.args.set.ClientID, tt.res.secret.ClientID) + assert.Equal(t, tt.args.set.ClientSecret, tt.res.secret.ClientSecret) + } + }) + } +} + +func TestCommandSide_RemoveMachineSecret(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + userID string + resourceOwner string + } + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "user invalid, invalid argument error userID", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + userID: "", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "user invalid, invalid argument error resourceowner", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + resourceOwner: "", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "user not existing, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "user existing without secret, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewMachineAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "user1", + "username", + "user", + false, + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "remove machine secret, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewMachineAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "user1", + "username", + "user", + false, + ), + ), + eventFromEventPusher( + user.NewMachineSecretSetEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("a"), + }, + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + user.NewMachineSecretRemovedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + ), + ), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + resourceOwner: "org1", + }, + 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, + } + got, err := r.RemoveMachineSecret(tt.args.ctx, tt.args.userID, tt.args.resourceOwner) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assert.Equal(t, tt.res.want, got) + } + }) + } +} + +func TestCommandSide_VerifyMachineSecret(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + userID string + resourceOwner string + secret string + } + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "user invalid, invalid argument error userID", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + userID: "", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "user invalid, invalid argument error resourceowner", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + resourceOwner: "", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "user not existing, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "user existing without secret, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewMachineAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "user1", + "username", + "user", + false, + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "verify machine secret, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewMachineAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "user1", + "username", + "user", + false, + ), + ), + eventFromEventPusher( + user.NewMachineSecretSetEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "bcrypt", + KeyID: "id", + Crypted: []byte("$2a$14$HxC7TAXMeowdqHdSBUfsjOUc0IGajYeApxdYl9lAYC0duZmSkgFia"), + }, + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + user.NewMachineSecretCheckSucceededEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + ), + ), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + resourceOwner: "org1", + secret: "test", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "verify machine secret, failed", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewMachineAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "user1", + "username", + "user", + false, + ), + ), + eventFromEventPusher( + user.NewMachineSecretSetEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "bcrypt", + KeyID: "id", + Crypted: []byte("$2a$14$HxC7TAXMeowdqHdSBUfsjOUc0IGajYeApxdYl9lAYC0duZmSkgFia"), + }, + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + user.NewMachineSecretCheckFailedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + ), + ), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + resourceOwner: "org1", + secret: "wrong", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + userPasswordAlg: crypto.NewBCrypt(14), + } + got, err := r.VerifyMachineSecret(tt.args.ctx, tt.args.userID, tt.args.resourceOwner, tt.args.secret) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assert.Equal(t, tt.res.want, got) + } + }) + } +} diff --git a/internal/domain/machine_secret.go b/internal/domain/machine_secret.go new file mode 100644 index 0000000000..d8b0633b48 --- /dev/null +++ b/internal/domain/machine_secret.go @@ -0,0 +1,14 @@ +package domain + +import ( + "github.com/zitadel/zitadel/internal/crypto" + "github.com/zitadel/zitadel/internal/errors" +) + +func NewMachineClientSecret(generator crypto.Generator) (*crypto.CryptoValue, string, error) { + cryptoValue, stringSecret, err := crypto.NewCode(generator) + if err != nil { + return nil, "", errors.ThrowInternal(err, "MODEL-57cjsiw", "Errors.User.Machine.Secret.CouldNotGenerate") + } + return cryptoValue, stringSecret, nil +} diff --git a/internal/query/iam_member_test.go b/internal/query/iam_member_test.go index 6a6251dcc9..6e8bbeba07 100644 --- a/internal/query/iam_member_test.go +++ b/internal/query/iam_member_test.go @@ -20,18 +20,18 @@ var ( ", members.user_id" + ", members.roles" + ", projections.login_names2.login_name" + - ", projections.users6_humans.email" + - ", projections.users6_humans.first_name" + - ", projections.users6_humans.last_name" + - ", projections.users6_humans.display_name" + - ", projections.users6_machines.name" + - ", projections.users6_humans.avatar_key" + + ", projections.users7_humans.email" + + ", projections.users7_humans.first_name" + + ", projections.users7_humans.last_name" + + ", projections.users7_humans.display_name" + + ", projections.users7_machines.name" + + ", projections.users7_humans.avatar_key" + ", COUNT(*) OVER () " + "FROM projections.instance_members3 AS members " + - "LEFT JOIN projections.users6_humans " + - "ON members.user_id = projections.users6_humans.user_id AND members.instance_id = projections.users6_humans.instance_id " + - "LEFT JOIN projections.users6_machines " + - "ON members.user_id = projections.users6_machines.user_id AND members.instance_id = projections.users6_machines.instance_id " + + "LEFT JOIN projections.users7_humans " + + "ON members.user_id = projections.users7_humans.user_id AND members.instance_id = projections.users7_humans.instance_id " + + "LEFT JOIN projections.users7_machines " + + "ON members.user_id = projections.users7_machines.user_id AND members.instance_id = projections.users7_machines.instance_id " + "LEFT JOIN projections.login_names2 " + "ON members.user_id = projections.login_names2.user_id AND members.instance_id = projections.login_names2.instance_id " + "WHERE projections.login_names2.is_primary = $1") diff --git a/internal/query/org_member_test.go b/internal/query/org_member_test.go index 583b8a15ad..62a9da645c 100644 --- a/internal/query/org_member_test.go +++ b/internal/query/org_member_test.go @@ -20,20 +20,20 @@ var ( ", members.user_id" + ", members.roles" + ", projections.login_names2.login_name" + - ", projections.users6_humans.email" + - ", projections.users6_humans.first_name" + - ", projections.users6_humans.last_name" + - ", projections.users6_humans.display_name" + - ", projections.users6_machines.name" + - ", projections.users6_humans.avatar_key" + + ", projections.users7_humans.email" + + ", projections.users7_humans.first_name" + + ", projections.users7_humans.last_name" + + ", projections.users7_humans.display_name" + + ", projections.users7_machines.name" + + ", projections.users7_humans.avatar_key" + ", COUNT(*) OVER () " + "FROM projections.org_members3 AS members " + - "LEFT JOIN projections.users6_humans " + - "ON members.user_id = projections.users6_humans.user_id " + - "AND members.instance_id = projections.users6_humans.instance_id " + - "LEFT JOIN projections.users6_machines " + - "ON members.user_id = projections.users6_machines.user_id " + - "AND members.instance_id = projections.users6_machines.instance_id " + + "LEFT JOIN projections.users7_humans " + + "ON members.user_id = projections.users7_humans.user_id " + + "AND members.instance_id = projections.users7_humans.instance_id " + + "LEFT JOIN projections.users7_machines " + + "ON members.user_id = projections.users7_machines.user_id " + + "AND members.instance_id = projections.users7_machines.instance_id " + "LEFT JOIN projections.login_names2 " + "ON members.user_id = projections.login_names2.user_id " + "AND members.instance_id = projections.login_names2.instance_id " + diff --git a/internal/query/project_grant_member_test.go b/internal/query/project_grant_member_test.go index c9ebdcce39..3b8abc0847 100644 --- a/internal/query/project_grant_member_test.go +++ b/internal/query/project_grant_member_test.go @@ -20,20 +20,20 @@ var ( ", members.user_id" + ", members.roles" + ", projections.login_names2.login_name" + - ", projections.users6_humans.email" + - ", projections.users6_humans.first_name" + - ", projections.users6_humans.last_name" + - ", projections.users6_humans.display_name" + - ", projections.users6_machines.name" + - ", projections.users6_humans.avatar_key" + + ", projections.users7_humans.email" + + ", projections.users7_humans.first_name" + + ", projections.users7_humans.last_name" + + ", projections.users7_humans.display_name" + + ", projections.users7_machines.name" + + ", projections.users7_humans.avatar_key" + ", COUNT(*) OVER () " + "FROM projections.project_grant_members3 AS members " + - "LEFT JOIN projections.users6_humans " + - "ON members.user_id = projections.users6_humans.user_id " + - "AND members.instance_id = projections.users6_humans.instance_id " + - "LEFT JOIN projections.users6_machines " + - "ON members.user_id = projections.users6_machines.user_id " + - "AND members.instance_id = projections.users6_machines.instance_id " + + "LEFT JOIN projections.users7_humans " + + "ON members.user_id = projections.users7_humans.user_id " + + "AND members.instance_id = projections.users7_humans.instance_id " + + "LEFT JOIN projections.users7_machines " + + "ON members.user_id = projections.users7_machines.user_id " + + "AND members.instance_id = projections.users7_machines.instance_id " + "LEFT JOIN projections.login_names2 " + "ON members.user_id = projections.login_names2.user_id " + "AND members.instance_id = projections.login_names2.instance_id " + diff --git a/internal/query/project_member_test.go b/internal/query/project_member_test.go index ea41dc5f9e..e5bd80889b 100644 --- a/internal/query/project_member_test.go +++ b/internal/query/project_member_test.go @@ -20,20 +20,20 @@ var ( ", members.user_id" + ", members.roles" + ", projections.login_names2.login_name" + - ", projections.users6_humans.email" + - ", projections.users6_humans.first_name" + - ", projections.users6_humans.last_name" + - ", projections.users6_humans.display_name" + - ", projections.users6_machines.name" + - ", projections.users6_humans.avatar_key" + + ", projections.users7_humans.email" + + ", projections.users7_humans.first_name" + + ", projections.users7_humans.last_name" + + ", projections.users7_humans.display_name" + + ", projections.users7_machines.name" + + ", projections.users7_humans.avatar_key" + ", COUNT(*) OVER () " + "FROM projections.project_members3 AS members " + - "LEFT JOIN projections.users6_humans " + - "ON members.user_id = projections.users6_humans.user_id " + - "AND members.instance_id = projections.users6_humans.instance_id " + - "LEFT JOIN projections.users6_machines " + - "ON members.user_id = projections.users6_machines.user_id " + - "AND members.instance_id = projections.users6_machines.instance_id " + + "LEFT JOIN projections.users7_humans " + + "ON members.user_id = projections.users7_humans.user_id " + + "AND members.instance_id = projections.users7_humans.instance_id " + + "LEFT JOIN projections.users7_machines " + + "ON members.user_id = projections.users7_machines.user_id " + + "AND members.instance_id = projections.users7_machines.instance_id " + "LEFT JOIN projections.login_names2 " + "ON members.user_id = projections.login_names2.user_id " + "AND members.instance_id = projections.login_names2.instance_id " + diff --git a/internal/query/projection/user.go b/internal/query/projection/user.go index c6c2f4de1e..5104f9326a 100644 --- a/internal/query/projection/user.go +++ b/internal/query/projection/user.go @@ -19,7 +19,7 @@ type userProjection struct { } const ( - UserTable = "projections.users6" + UserTable = "projections.users7" UserHumanTable = UserTable + "_" + UserHumanSuffix UserMachineTable = UserTable + "_" + UserMachineSuffix UserNotifyTable = UserTable + "_" + UserNotifySuffix @@ -62,6 +62,7 @@ const ( MachineUserInstanceIDCol = "instance_id" MachineNameCol = "name" MachineDescriptionCol = "description" + MachineHasSecretCol = "has_secret" // notify UserNotifySuffix = "notifications" @@ -120,6 +121,7 @@ func newUserProjection(ctx context.Context, config crdb.StatementHandlerConfig) crdb.NewColumn(MachineUserInstanceIDCol, crdb.ColumnTypeText), crdb.NewColumn(MachineNameCol, crdb.ColumnTypeText), crdb.NewColumn(MachineDescriptionCol, crdb.ColumnTypeText, crdb.Nullable()), + crdb.NewColumn(MachineHasSecretCol, crdb.ColumnTypeBool, crdb.Default(false)), }, crdb.NewPrimaryKey(MachineUserInstanceIDCol, MachineUserIDCol), UserMachineSuffix, @@ -276,6 +278,14 @@ func (p *userProjection) reducers() []handler.AggregateReducer { Event: user.HumanPasswordChangedType, Reduce: p.reduceHumanPasswordChanged, }, + { + Event: user.MachineSecretSetType, + Reduce: p.reduceMachineSecretSet, + }, + { + Event: user.MachineSecretRemovedType, + Reduce: p.reduceMachineSecretRemoved, + }, }, }, { @@ -907,6 +917,67 @@ func (p *userProjection) reduceHumanPasswordChanged(event eventstore.Event) (*ha ), nil } +func (p *userProjection) reduceMachineSecretSet(event eventstore.Event) (*handler.Statement, error) { + e, ok := event.(*user.MachineSecretSetEvent) + if !ok { + return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-x0p1n1i", "reduce.wrong.event.type %s", user.MachineSecretSetType) + } + return crdb.NewMultiStatement( + e, + crdb.AddUpdateStatement( + []handler.Column{ + handler.NewCol(UserChangeDateCol, e.CreationDate()), + handler.NewCol(UserSequenceCol, e.Sequence()), + }, + []handler.Condition{ + handler.NewCond(UserIDCol, e.Aggregate().ID), + handler.NewCond(UserInstanceIDCol, e.Aggregate().InstanceID), + }, + ), + crdb.AddUpdateStatement( + []handler.Column{ + handler.NewCol(MachineHasSecretCol, true), + }, + []handler.Condition{ + handler.NewCond(MachineUserIDCol, e.Aggregate().ID), + handler.NewCond(MachineUserInstanceIDCol, e.Aggregate().InstanceID), + }, + crdb.WithTableSuffix(UserMachineSuffix), + ), + ), nil +} + +func (p *userProjection) reduceMachineSecretRemoved(event eventstore.Event) (*handler.Statement, error) { + e, ok := event.(*user.MachineSecretRemovedEvent) + if !ok { + return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-x0p6n1i", "reduce.wrong.event.type %s", user.MachineSecretRemovedType) + } + + return crdb.NewMultiStatement( + e, + crdb.AddUpdateStatement( + []handler.Column{ + handler.NewCol(UserChangeDateCol, e.CreationDate()), + handler.NewCol(UserSequenceCol, e.Sequence()), + }, + []handler.Condition{ + handler.NewCond(UserIDCol, e.Aggregate().ID), + handler.NewCond(UserInstanceIDCol, e.Aggregate().InstanceID), + }, + ), + crdb.AddUpdateStatement( + []handler.Column{ + handler.NewCol(MachineHasSecretCol, false), + }, + []handler.Condition{ + handler.NewCond(MachineUserIDCol, e.Aggregate().ID), + handler.NewCond(MachineUserInstanceIDCol, e.Aggregate().InstanceID), + }, + crdb.WithTableSuffix(UserMachineSuffix), + ), + ), nil +} + func (p *userProjection) reduceMachineAdded(event eventstore.Event) (*handler.Statement, error) { e, ok := event.(*user.MachineAddedEvent) if !ok { diff --git a/internal/query/projection/user_test.go b/internal/query/projection/user_test.go index bc435b23a4..3acf6ea835 100644 --- a/internal/query/projection/user_test.go +++ b/internal/query/projection/user_test.go @@ -51,7 +51,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "INSERT INTO projections.users6 (id, creation_date, change_date, resource_owner, instance_id, state, sequence, username, type) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", + expectedStmt: "INSERT INTO projections.users7 (id, creation_date, change_date, resource_owner, instance_id, state, sequence, username, type) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", expectedArgs: []interface{}{ "agg-id", anyArg{}, @@ -65,7 +65,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "INSERT INTO projections.users6_humans (user_id, instance_id, first_name, last_name, nick_name, display_name, preferred_language, gender, email, phone) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)", + expectedStmt: "INSERT INTO projections.users7_humans (user_id, instance_id, first_name, last_name, nick_name, display_name, preferred_language, gender, email, phone) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)", expectedArgs: []interface{}{ "agg-id", "instance-id", @@ -80,7 +80,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "INSERT INTO projections.users6_notifications (user_id, instance_id, last_email, last_phone, password_set) VALUES ($1, $2, $3, $4, $5)", + expectedStmt: "INSERT INTO projections.users7_notifications (user_id, instance_id, last_email, last_phone, password_set) VALUES ($1, $2, $3, $4, $5)", expectedArgs: []interface{}{ "agg-id", "instance-id", @@ -120,7 +120,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "INSERT INTO projections.users6 (id, creation_date, change_date, resource_owner, instance_id, state, sequence, username, type) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", + expectedStmt: "INSERT INTO projections.users7 (id, creation_date, change_date, resource_owner, instance_id, state, sequence, username, type) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", expectedArgs: []interface{}{ "agg-id", anyArg{}, @@ -134,7 +134,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "INSERT INTO projections.users6_humans (user_id, instance_id, first_name, last_name, nick_name, display_name, preferred_language, gender, email, phone) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)", + expectedStmt: "INSERT INTO projections.users7_humans (user_id, instance_id, first_name, last_name, nick_name, display_name, preferred_language, gender, email, phone) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)", expectedArgs: []interface{}{ "agg-id", "instance-id", @@ -149,7 +149,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "INSERT INTO projections.users6_notifications (user_id, instance_id, last_email, last_phone, password_set) VALUES ($1, $2, $3, $4, $5)", + expectedStmt: "INSERT INTO projections.users7_notifications (user_id, instance_id, last_email, last_phone, password_set) VALUES ($1, $2, $3, $4, $5)", expectedArgs: []interface{}{ "agg-id", "instance-id", @@ -184,7 +184,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "INSERT INTO projections.users6 (id, creation_date, change_date, resource_owner, instance_id, state, sequence, username, type) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", + expectedStmt: "INSERT INTO projections.users7 (id, creation_date, change_date, resource_owner, instance_id, state, sequence, username, type) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", expectedArgs: []interface{}{ "agg-id", anyArg{}, @@ -198,7 +198,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "INSERT INTO projections.users6_humans (user_id, instance_id, first_name, last_name, nick_name, display_name, preferred_language, gender, email, phone) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)", + expectedStmt: "INSERT INTO projections.users7_humans (user_id, instance_id, first_name, last_name, nick_name, display_name, preferred_language, gender, email, phone) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)", expectedArgs: []interface{}{ "agg-id", "instance-id", @@ -213,7 +213,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "INSERT INTO projections.users6_notifications (user_id, instance_id, last_email, last_phone, password_set) VALUES ($1, $2, $3, $4, $5)", + expectedStmt: "INSERT INTO projections.users7_notifications (user_id, instance_id, last_email, last_phone, password_set) VALUES ($1, $2, $3, $4, $5)", expectedArgs: []interface{}{ "agg-id", "instance-id", @@ -253,7 +253,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "INSERT INTO projections.users6 (id, creation_date, change_date, resource_owner, instance_id, state, sequence, username, type) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", + expectedStmt: "INSERT INTO projections.users7 (id, creation_date, change_date, resource_owner, instance_id, state, sequence, username, type) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", expectedArgs: []interface{}{ "agg-id", anyArg{}, @@ -267,7 +267,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "INSERT INTO projections.users6_humans (user_id, instance_id, first_name, last_name, nick_name, display_name, preferred_language, gender, email, phone) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)", + expectedStmt: "INSERT INTO projections.users7_humans (user_id, instance_id, first_name, last_name, nick_name, display_name, preferred_language, gender, email, phone) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)", expectedArgs: []interface{}{ "agg-id", "instance-id", @@ -282,7 +282,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "INSERT INTO projections.users6_notifications (user_id, instance_id, last_email, last_phone, password_set) VALUES ($1, $2, $3, $4, $5)", + expectedStmt: "INSERT INTO projections.users7_notifications (user_id, instance_id, last_email, last_phone, password_set) VALUES ($1, $2, $3, $4, $5)", expectedArgs: []interface{}{ "agg-id", "instance-id", @@ -322,7 +322,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "INSERT INTO projections.users6 (id, creation_date, change_date, resource_owner, instance_id, state, sequence, username, type) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", + expectedStmt: "INSERT INTO projections.users7 (id, creation_date, change_date, resource_owner, instance_id, state, sequence, username, type) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", expectedArgs: []interface{}{ "agg-id", anyArg{}, @@ -336,7 +336,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "INSERT INTO projections.users6_humans (user_id, instance_id, first_name, last_name, nick_name, display_name, preferred_language, gender, email, phone) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)", + expectedStmt: "INSERT INTO projections.users7_humans (user_id, instance_id, first_name, last_name, nick_name, display_name, preferred_language, gender, email, phone) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)", expectedArgs: []interface{}{ "agg-id", "instance-id", @@ -351,7 +351,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "INSERT INTO projections.users6_notifications (user_id, instance_id, last_email, last_phone, password_set) VALUES ($1, $2, $3, $4, $5)", + expectedStmt: "INSERT INTO projections.users7_notifications (user_id, instance_id, last_email, last_phone, password_set) VALUES ($1, $2, $3, $4, $5)", expectedArgs: []interface{}{ "agg-id", "instance-id", @@ -386,7 +386,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "INSERT INTO projections.users6 (id, creation_date, change_date, resource_owner, instance_id, state, sequence, username, type) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", + expectedStmt: "INSERT INTO projections.users7 (id, creation_date, change_date, resource_owner, instance_id, state, sequence, username, type) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", expectedArgs: []interface{}{ "agg-id", anyArg{}, @@ -400,7 +400,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "INSERT INTO projections.users6_humans (user_id, instance_id, first_name, last_name, nick_name, display_name, preferred_language, gender, email, phone) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)", + expectedStmt: "INSERT INTO projections.users7_humans (user_id, instance_id, first_name, last_name, nick_name, display_name, preferred_language, gender, email, phone) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)", expectedArgs: []interface{}{ "agg-id", "instance-id", @@ -415,7 +415,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "INSERT INTO projections.users6_notifications (user_id, instance_id, last_email, last_phone, password_set) VALUES ($1, $2, $3, $4, $5)", + expectedStmt: "INSERT INTO projections.users7_notifications (user_id, instance_id, last_email, last_phone, password_set) VALUES ($1, $2, $3, $4, $5)", expectedArgs: []interface{}{ "agg-id", "instance-id", @@ -445,7 +445,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users6 SET state = $1 WHERE (id = $2) AND (instance_id = $3)", + expectedStmt: "UPDATE projections.users7 SET state = $1 WHERE (id = $2) AND (instance_id = $3)", expectedArgs: []interface{}{ domain.UserStateInitial, "agg-id", @@ -473,7 +473,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users6 SET state = $1 WHERE (id = $2) AND (instance_id = $3)", + expectedStmt: "UPDATE projections.users7 SET state = $1 WHERE (id = $2) AND (instance_id = $3)", expectedArgs: []interface{}{ domain.UserStateInitial, "agg-id", @@ -501,7 +501,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users6 SET state = $1 WHERE (id = $2) AND (instance_id = $3)", + expectedStmt: "UPDATE projections.users7 SET state = $1 WHERE (id = $2) AND (instance_id = $3)", expectedArgs: []interface{}{ domain.UserStateActive, "agg-id", @@ -529,7 +529,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users6 SET state = $1 WHERE (id = $2) AND (instance_id = $3)", + expectedStmt: "UPDATE projections.users7 SET state = $1 WHERE (id = $2) AND (instance_id = $3)", expectedArgs: []interface{}{ domain.UserStateActive, "agg-id", @@ -557,7 +557,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users6 SET (change_date, state, sequence) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", + expectedStmt: "UPDATE projections.users7 SET (change_date, state, sequence) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", expectedArgs: []interface{}{ anyArg{}, domain.UserStateLocked, @@ -587,7 +587,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users6 SET (change_date, state, sequence) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", + expectedStmt: "UPDATE projections.users7 SET (change_date, state, sequence) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", expectedArgs: []interface{}{ anyArg{}, domain.UserStateActive, @@ -617,7 +617,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users6 SET (change_date, state, sequence) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", + expectedStmt: "UPDATE projections.users7 SET (change_date, state, sequence) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", expectedArgs: []interface{}{ anyArg{}, domain.UserStateInactive, @@ -647,7 +647,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users6 SET (change_date, state, sequence) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", + expectedStmt: "UPDATE projections.users7 SET (change_date, state, sequence) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", expectedArgs: []interface{}{ anyArg{}, domain.UserStateActive, @@ -677,7 +677,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "DELETE FROM projections.users6 WHERE (id = $1) AND (instance_id = $2)", + expectedStmt: "DELETE FROM projections.users7 WHERE (id = $1) AND (instance_id = $2)", expectedArgs: []interface{}{ "agg-id", "instance-id", @@ -706,7 +706,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users6 SET (change_date, username, sequence) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", + expectedStmt: "UPDATE projections.users7 SET (change_date, username, sequence) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", expectedArgs: []interface{}{ anyArg{}, "username", @@ -738,7 +738,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users6 SET (change_date, username, sequence) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", + expectedStmt: "UPDATE projections.users7 SET (change_date, username, sequence) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", expectedArgs: []interface{}{ anyArg{}, "id@temporary.domain", @@ -775,7 +775,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users6 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users7 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -784,7 +784,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users6_humans SET (first_name, last_name, nick_name, display_name, preferred_language, gender) = ($1, $2, $3, $4, $5, $6) WHERE (user_id = $7) AND (instance_id = $8)", + expectedStmt: "UPDATE projections.users7_humans SET (first_name, last_name, nick_name, display_name, preferred_language, gender) = ($1, $2, $3, $4, $5, $6) WHERE (user_id = $7) AND (instance_id = $8)", expectedArgs: []interface{}{ "first-name", "last-name", @@ -824,7 +824,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users6 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users7 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -833,7 +833,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users6_humans SET (first_name, last_name, nick_name, display_name, preferred_language, gender) = ($1, $2, $3, $4, $5, $6) WHERE (user_id = $7) AND (instance_id = $8)", + expectedStmt: "UPDATE projections.users7_humans SET (first_name, last_name, nick_name, display_name, preferred_language, gender) = ($1, $2, $3, $4, $5, $6) WHERE (user_id = $7) AND (instance_id = $8)", expectedArgs: []interface{}{ "first-name", "last-name", @@ -868,7 +868,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users6 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users7 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -877,7 +877,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users6_humans SET (phone, is_phone_verified) = ($1, $2) WHERE (user_id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users7_humans SET (phone, is_phone_verified) = ($1, $2) WHERE (user_id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ "+41 00 000 00 00", false, @@ -886,7 +886,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users6_notifications SET last_phone = $1 WHERE (user_id = $2) AND (instance_id = $3)", + expectedStmt: "UPDATE projections.users7_notifications SET last_phone = $1 WHERE (user_id = $2) AND (instance_id = $3)", expectedArgs: []interface{}{ &sql.NullString{String: "+41 00 000 00 00", Valid: true}, "agg-id", @@ -916,7 +916,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users6 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users7 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -925,7 +925,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users6_humans SET (phone, is_phone_verified) = ($1, $2) WHERE (user_id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users7_humans SET (phone, is_phone_verified) = ($1, $2) WHERE (user_id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ "+41 00 000 00 00", false, @@ -934,7 +934,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users6_notifications SET last_phone = $1 WHERE (user_id = $2) AND (instance_id = $3)", + expectedStmt: "UPDATE projections.users7_notifications SET last_phone = $1 WHERE (user_id = $2) AND (instance_id = $3)", expectedArgs: []interface{}{ &sql.NullString{String: "+41 00 000 00 00", Valid: true}, "agg-id", @@ -962,7 +962,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users6 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users7 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -971,7 +971,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users6_humans SET (phone, is_phone_verified) = ($1, $2) WHERE (user_id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users7_humans SET (phone, is_phone_verified) = ($1, $2) WHERE (user_id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ nil, nil, @@ -980,7 +980,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users6_notifications SET (last_phone, verified_phone) = ($1, $2) WHERE (user_id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users7_notifications SET (last_phone, verified_phone) = ($1, $2) WHERE (user_id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ nil, nil, @@ -1009,7 +1009,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users6 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users7 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -1018,7 +1018,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users6_humans SET (phone, is_phone_verified) = ($1, $2) WHERE (user_id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users7_humans SET (phone, is_phone_verified) = ($1, $2) WHERE (user_id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ nil, nil, @@ -1027,7 +1027,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users6_notifications SET (last_phone, verified_phone) = ($1, $2) WHERE (user_id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users7_notifications SET (last_phone, verified_phone) = ($1, $2) WHERE (user_id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ nil, nil, @@ -1056,7 +1056,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users6 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users7 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -1065,7 +1065,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users6_humans SET is_phone_verified = $1 WHERE (user_id = $2) AND (instance_id = $3)", + expectedStmt: "UPDATE projections.users7_humans SET is_phone_verified = $1 WHERE (user_id = $2) AND (instance_id = $3)", expectedArgs: []interface{}{ true, "agg-id", @@ -1073,7 +1073,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users6_notifications SET verified_phone = last_phone WHERE (user_id = $1) AND (instance_id = $2)", + expectedStmt: "UPDATE projections.users7_notifications SET verified_phone = last_phone WHERE (user_id = $1) AND (instance_id = $2)", expectedArgs: []interface{}{ "agg-id", "instance-id", @@ -1100,7 +1100,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users6 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users7 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -1109,7 +1109,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users6_humans SET is_phone_verified = $1 WHERE (user_id = $2) AND (instance_id = $3)", + expectedStmt: "UPDATE projections.users7_humans SET is_phone_verified = $1 WHERE (user_id = $2) AND (instance_id = $3)", expectedArgs: []interface{}{ true, "agg-id", @@ -1117,7 +1117,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users6_notifications SET verified_phone = last_phone WHERE (user_id = $1) AND (instance_id = $2)", + expectedStmt: "UPDATE projections.users7_notifications SET verified_phone = last_phone WHERE (user_id = $1) AND (instance_id = $2)", expectedArgs: []interface{}{ "agg-id", "instance-id", @@ -1146,7 +1146,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users6 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users7 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -1155,7 +1155,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users6_humans SET (email, is_email_verified) = ($1, $2) WHERE (user_id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users7_humans SET (email, is_email_verified) = ($1, $2) WHERE (user_id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ "email@zitadel.com", false, @@ -1164,7 +1164,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users6_notifications SET last_email = $1 WHERE (user_id = $2) AND (instance_id = $3)", + expectedStmt: "UPDATE projections.users7_notifications SET last_email = $1 WHERE (user_id = $2) AND (instance_id = $3)", expectedArgs: []interface{}{ &sql.NullString{String: "email@zitadel.com", Valid: true}, "agg-id", @@ -1194,7 +1194,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users6 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users7 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -1203,7 +1203,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users6_humans SET (email, is_email_verified) = ($1, $2) WHERE (user_id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users7_humans SET (email, is_email_verified) = ($1, $2) WHERE (user_id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ "email@zitadel.com", false, @@ -1212,7 +1212,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users6_notifications SET last_email = $1 WHERE (user_id = $2) AND (instance_id = $3)", + expectedStmt: "UPDATE projections.users7_notifications SET last_email = $1 WHERE (user_id = $2) AND (instance_id = $3)", expectedArgs: []interface{}{ &sql.NullString{String: "email@zitadel.com", Valid: true}, "agg-id", @@ -1240,7 +1240,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users6 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users7 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -1249,7 +1249,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users6_humans SET is_email_verified = $1 WHERE (user_id = $2) AND (instance_id = $3)", + expectedStmt: "UPDATE projections.users7_humans SET is_email_verified = $1 WHERE (user_id = $2) AND (instance_id = $3)", expectedArgs: []interface{}{ true, "agg-id", @@ -1257,7 +1257,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users6_notifications SET verified_email = last_email WHERE (user_id = $1) AND (instance_id = $2)", + expectedStmt: "UPDATE projections.users7_notifications SET verified_email = last_email WHERE (user_id = $1) AND (instance_id = $2)", expectedArgs: []interface{}{ "agg-id", "instance-id", @@ -1284,7 +1284,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users6 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users7 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -1293,7 +1293,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users6_humans SET is_email_verified = $1 WHERE (user_id = $2) AND (instance_id = $3)", + expectedStmt: "UPDATE projections.users7_humans SET is_email_verified = $1 WHERE (user_id = $2) AND (instance_id = $3)", expectedArgs: []interface{}{ true, "agg-id", @@ -1301,7 +1301,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users6_notifications SET verified_email = last_email WHERE (user_id = $1) AND (instance_id = $2)", + expectedStmt: "UPDATE projections.users7_notifications SET verified_email = last_email WHERE (user_id = $1) AND (instance_id = $2)", expectedArgs: []interface{}{ "agg-id", "instance-id", @@ -1330,7 +1330,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users6 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users7 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -1339,7 +1339,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users6_humans SET avatar_key = $1 WHERE (user_id = $2) AND (instance_id = $3)", + expectedStmt: "UPDATE projections.users7_humans SET avatar_key = $1 WHERE (user_id = $2) AND (instance_id = $3)", expectedArgs: []interface{}{ "users/agg-id/avatar", "agg-id", @@ -1367,7 +1367,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users6 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users7 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -1376,7 +1376,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users6_humans SET avatar_key = $1 WHERE (user_id = $2) AND (instance_id = $3)", + expectedStmt: "UPDATE projections.users7_humans SET avatar_key = $1 WHERE (user_id = $2) AND (instance_id = $3)", expectedArgs: []interface{}{ nil, "agg-id", @@ -1407,7 +1407,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "INSERT INTO projections.users6 (id, creation_date, change_date, resource_owner, instance_id, state, sequence, username, type) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", + expectedStmt: "INSERT INTO projections.users7 (id, creation_date, change_date, resource_owner, instance_id, state, sequence, username, type) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", expectedArgs: []interface{}{ "agg-id", anyArg{}, @@ -1421,7 +1421,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "INSERT INTO projections.users6_machines (user_id, instance_id, name, description) VALUES ($1, $2, $3, $4)", + expectedStmt: "INSERT INTO projections.users7_machines (user_id, instance_id, name, description) VALUES ($1, $2, $3, $4)", expectedArgs: []interface{}{ "agg-id", "instance-id", @@ -1454,7 +1454,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "INSERT INTO projections.users6 (id, creation_date, change_date, resource_owner, instance_id, state, sequence, username, type) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", + expectedStmt: "INSERT INTO projections.users7 (id, creation_date, change_date, resource_owner, instance_id, state, sequence, username, type) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", expectedArgs: []interface{}{ "agg-id", anyArg{}, @@ -1468,7 +1468,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "INSERT INTO projections.users6_machines (user_id, instance_id, name, description) VALUES ($1, $2, $3, $4)", + expectedStmt: "INSERT INTO projections.users7_machines (user_id, instance_id, name, description) VALUES ($1, $2, $3, $4)", expectedArgs: []interface{}{ "agg-id", "instance-id", @@ -1500,7 +1500,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users6 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users7 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -1509,7 +1509,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users6_machines SET (name, description) = ($1, $2) WHERE (user_id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users7_machines SET (name, description) = ($1, $2) WHERE (user_id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ "machine-name", "description", @@ -1540,7 +1540,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users6 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users7 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -1549,7 +1549,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users6_machines SET name = $1 WHERE (user_id = $2) AND (instance_id = $3)", + expectedStmt: "UPDATE projections.users7_machines SET name = $1 WHERE (user_id = $2) AND (instance_id = $3)", expectedArgs: []interface{}{ "machine-name", "agg-id", @@ -1579,7 +1579,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users6 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users7 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -1588,7 +1588,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users6_machines SET description = $1 WHERE (user_id = $2) AND (instance_id = $3)", + expectedStmt: "UPDATE projections.users7_machines SET description = $1 WHERE (user_id = $2) AND (instance_id = $3)", expectedArgs: []interface{}{ "description", "agg-id", @@ -1618,6 +1618,82 @@ func TestUserProjection_reduces(t *testing.T) { }, }, }, + { + name: "reduceMachineSecretSet", + args: args{ + event: getEvent(testEvent( + repository.EventType(user.MachineSecretSetType), + user.AggregateType, + []byte(`{ + "client_secret": {} + }`), + ), user.MachineSecretSetEventMapper), + }, + reduce: (&userProjection{}).reduceMachineSecretSet, + want: wantReduce{ + aggregateType: user.AggregateType, + sequence: 15, + previousSequence: 10, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "UPDATE projections.users7 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedArgs: []interface{}{ + anyArg{}, + uint64(15), + "agg-id", + "instance-id", + }, + }, + { + expectedStmt: "UPDATE projections.users7_machines SET has_secret = $1 WHERE (user_id = $2) AND (instance_id = $3)", + expectedArgs: []interface{}{ + true, + "agg-id", + "instance-id", + }, + }, + }, + }, + }, + }, + { + name: "reduceMachineSecretSet", + args: args{ + event: getEvent(testEvent( + repository.EventType(user.MachineSecretRemovedType), + user.AggregateType, + []byte(`{}`), + ), user.MachineSecretRemovedEventMapper), + }, + reduce: (&userProjection{}).reduceMachineSecretRemoved, + want: wantReduce{ + aggregateType: user.AggregateType, + sequence: 15, + previousSequence: 10, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "UPDATE projections.users7 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedArgs: []interface{}{ + anyArg{}, + uint64(15), + "agg-id", + "instance-id", + }, + }, + { + expectedStmt: "UPDATE projections.users7_machines SET has_secret = $1 WHERE (user_id = $2) AND (instance_id = $3)", + expectedArgs: []interface{}{ + false, + "agg-id", + "instance-id", + }, + }, + }, + }, + }, + }, { name: "org reduceOwnerRemoved", reduce: (&userProjection{}).reduceOwnerRemoved, @@ -1635,7 +1711,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users6 SET (change_date, sequence, owner_removed) = ($1, $2, $3) WHERE (instance_id = $4) AND (resource_owner = $5)", + expectedStmt: "UPDATE projections.users7 SET (change_date, sequence, owner_removed) = ($1, $2, $3) WHERE (instance_id = $4) AND (resource_owner = $5)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -1665,7 +1741,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "DELETE FROM projections.users6 WHERE (instance_id = $1)", + expectedStmt: "DELETE FROM projections.users7 WHERE (instance_id = $1)", expectedArgs: []interface{}{ "agg-id", }, diff --git a/internal/query/user.go b/internal/query/user.go index e5bd6d7a88..aa8ac36bef 100644 --- a/internal/query/user.go +++ b/internal/query/user.go @@ -90,6 +90,7 @@ type Phone struct { type Machine struct { Name string Description string + HasSecret bool } type NotifyUser struct { @@ -277,6 +278,10 @@ var ( name: projection.MachineDescriptionCol, table: machineTable, } + MachineHasSecretCol = Column{ + name: projection.MachineHasSecretCol, + table: machineTable, + } ) var ( @@ -747,6 +752,7 @@ func prepareUserQuery() (sq.SelectBuilder, func(*sql.Row) (*User, error)) { MachineUserIDCol.identifier(), MachineNameCol.identifier(), MachineDescriptionCol.identifier(), + MachineHasSecretCol.identifier(), countColumn.identifier(), ). From(userTable.identifier()). @@ -782,6 +788,7 @@ func prepareUserQuery() (sq.SelectBuilder, func(*sql.Row) (*User, error)) { machineID := sql.NullString{} name := sql.NullString{} description := sql.NullString{} + hasSecret := sql.NullBool{} err := row.Scan( &u.ID, @@ -809,6 +816,7 @@ func prepareUserQuery() (sq.SelectBuilder, func(*sql.Row) (*User, error)) { &machineID, &name, &description, + &hasSecret, &count, ) @@ -839,6 +847,7 @@ func prepareUserQuery() (sq.SelectBuilder, func(*sql.Row) (*User, error)) { u.Machine = &Machine{ Name: name.String, Description: description.String, + HasSecret: hasSecret.Bool, } } return u, nil @@ -1209,6 +1218,7 @@ func prepareUsersQuery() (sq.SelectBuilder, func(*sql.Rows) (*Users, error)) { MachineUserIDCol.identifier(), MachineNameCol.identifier(), MachineDescriptionCol.identifier(), + MachineHasSecretCol.identifier(), countColumn.identifier()). From(userTable.identifier()). LeftJoin(join(HumanUserIDCol, UserIDCol)). @@ -1246,6 +1256,7 @@ func prepareUsersQuery() (sq.SelectBuilder, func(*sql.Rows) (*Users, error)) { machineID := sql.NullString{} name := sql.NullString{} description := sql.NullString{} + hasSecret := sql.NullBool{} err := rows.Scan( &u.ID, @@ -1273,6 +1284,7 @@ func prepareUsersQuery() (sq.SelectBuilder, func(*sql.Rows) (*Users, error)) { &machineID, &name, &description, + &hasSecret, &count, ) if err != nil { @@ -1302,6 +1314,7 @@ func prepareUsersQuery() (sq.SelectBuilder, func(*sql.Rows) (*Users, error)) { u.Machine = &Machine{ Name: name.String, Description: description.String, + HasSecret: hasSecret.Bool, } } diff --git a/internal/query/user_grant_test.go b/internal/query/user_grant_test.go index ef3df7be01..a8fc3cda64 100644 --- a/internal/query/user_grant_test.go +++ b/internal/query/user_grant_test.go @@ -23,14 +23,14 @@ var ( ", projections.user_grants3.roles" + ", projections.user_grants3.state" + ", projections.user_grants3.user_id" + - ", projections.users6.username" + - ", projections.users6.type" + - ", projections.users6.resource_owner" + - ", projections.users6_humans.first_name" + - ", projections.users6_humans.last_name" + - ", projections.users6_humans.email" + - ", projections.users6_humans.display_name" + - ", projections.users6_humans.avatar_key" + + ", projections.users7.username" + + ", projections.users7.type" + + ", projections.users7.resource_owner" + + ", projections.users7_humans.first_name" + + ", projections.users7_humans.last_name" + + ", projections.users7_humans.email" + + ", projections.users7_humans.display_name" + + ", projections.users7_humans.avatar_key" + ", projections.login_names2.login_name" + ", projections.user_grants3.resource_owner" + ", projections.orgs.name" + @@ -38,8 +38,8 @@ var ( ", projections.user_grants3.project_id" + ", projections.projects3.name" + " FROM projections.user_grants3" + - " LEFT JOIN projections.users6 ON projections.user_grants3.user_id = projections.users6.id AND projections.user_grants3.instance_id = projections.users6.instance_id" + - " LEFT JOIN projections.users6_humans ON projections.user_grants3.user_id = projections.users6_humans.user_id AND projections.user_grants3.instance_id = projections.users6_humans.instance_id" + + " LEFT JOIN projections.users7 ON projections.user_grants3.user_id = projections.users7.id AND projections.user_grants3.instance_id = projections.users7.instance_id" + + " LEFT JOIN projections.users7_humans ON projections.user_grants3.user_id = projections.users7_humans.user_id AND projections.user_grants3.instance_id = projections.users7_humans.instance_id" + " LEFT JOIN projections.orgs ON projections.user_grants3.resource_owner = projections.orgs.id AND projections.user_grants3.instance_id = projections.orgs.instance_id" + " LEFT JOIN projections.projects3 ON projections.user_grants3.project_id = projections.projects3.id AND projections.user_grants3.instance_id = projections.projects3.instance_id" + " LEFT JOIN projections.login_names2 ON projections.user_grants3.user_id = projections.login_names2.user_id AND projections.user_grants3.instance_id = projections.login_names2.instance_id" + @@ -77,14 +77,14 @@ var ( ", projections.user_grants3.roles" + ", projections.user_grants3.state" + ", projections.user_grants3.user_id" + - ", projections.users6.username" + - ", projections.users6.type" + - ", projections.users6.resource_owner" + - ", projections.users6_humans.first_name" + - ", projections.users6_humans.last_name" + - ", projections.users6_humans.email" + - ", projections.users6_humans.display_name" + - ", projections.users6_humans.avatar_key" + + ", projections.users7.username" + + ", projections.users7.type" + + ", projections.users7.resource_owner" + + ", projections.users7_humans.first_name" + + ", projections.users7_humans.last_name" + + ", projections.users7_humans.email" + + ", projections.users7_humans.display_name" + + ", projections.users7_humans.avatar_key" + ", projections.login_names2.login_name" + ", projections.user_grants3.resource_owner" + ", projections.orgs.name" + @@ -93,8 +93,8 @@ var ( ", projections.projects3.name" + ", COUNT(*) OVER ()" + " FROM projections.user_grants3" + - " LEFT JOIN projections.users6 ON projections.user_grants3.user_id = projections.users6.id AND projections.user_grants3.instance_id = projections.users6.instance_id" + - " LEFT JOIN projections.users6_humans ON projections.user_grants3.user_id = projections.users6_humans.user_id AND projections.user_grants3.instance_id = projections.users6_humans.instance_id" + + " LEFT JOIN projections.users7 ON projections.user_grants3.user_id = projections.users7.id AND projections.user_grants3.instance_id = projections.users7.instance_id" + + " LEFT JOIN projections.users7_humans ON projections.user_grants3.user_id = projections.users7_humans.user_id AND projections.user_grants3.instance_id = projections.users7_humans.instance_id" + " LEFT JOIN projections.orgs ON projections.user_grants3.resource_owner = projections.orgs.id AND projections.user_grants3.instance_id = projections.orgs.instance_id" + " LEFT JOIN projections.projects3 ON projections.user_grants3.project_id = projections.projects3.id AND projections.user_grants3.instance_id = projections.projects3.instance_id" + " LEFT JOIN projections.login_names2 ON projections.user_grants3.user_id = projections.login_names2.user_id AND projections.user_grants3.instance_id = projections.login_names2.instance_id" + diff --git a/internal/query/user_test.go b/internal/query/user_test.go index 7e0685618c..af376b340e 100644 --- a/internal/query/user_test.go +++ b/internal/query/user_test.go @@ -23,41 +23,42 @@ var ( preferredLoginNameQuery = `SELECT preferred_login_name.user_id, preferred_login_name.login_name, preferred_login_name.instance_id, preferred_login_name.user_owner_removed, preferred_login_name.policy_owner_removed, preferred_login_name.domain_owner_removed` + ` FROM projections.login_names2 AS preferred_login_name` + ` WHERE preferred_login_name.is_primary = $1` - userQuery = `SELECT projections.users6.id,` + - ` projections.users6.creation_date,` + - ` projections.users6.change_date,` + - ` projections.users6.resource_owner,` + - ` projections.users6.sequence,` + - ` projections.users6.state,` + - ` projections.users6.type,` + - ` projections.users6.username,` + + userQuery = `SELECT projections.users7.id,` + + ` projections.users7.creation_date,` + + ` projections.users7.change_date,` + + ` projections.users7.resource_owner,` + + ` projections.users7.sequence,` + + ` projections.users7.state,` + + ` projections.users7.type,` + + ` projections.users7.username,` + ` login_names.loginnames,` + ` preferred_login_name.login_name,` + - ` projections.users6_humans.user_id,` + - ` projections.users6_humans.first_name,` + - ` projections.users6_humans.last_name,` + - ` projections.users6_humans.nick_name,` + - ` projections.users6_humans.display_name,` + - ` projections.users6_humans.preferred_language,` + - ` projections.users6_humans.gender,` + - ` projections.users6_humans.avatar_key,` + - ` projections.users6_humans.email,` + - ` projections.users6_humans.is_email_verified,` + - ` projections.users6_humans.phone,` + - ` projections.users6_humans.is_phone_verified,` + - ` projections.users6_machines.user_id,` + - ` projections.users6_machines.name,` + - ` projections.users6_machines.description,` + + ` projections.users7_humans.user_id,` + + ` projections.users7_humans.first_name,` + + ` projections.users7_humans.last_name,` + + ` projections.users7_humans.nick_name,` + + ` projections.users7_humans.display_name,` + + ` projections.users7_humans.preferred_language,` + + ` projections.users7_humans.gender,` + + ` projections.users7_humans.avatar_key,` + + ` projections.users7_humans.email,` + + ` projections.users7_humans.is_email_verified,` + + ` projections.users7_humans.phone,` + + ` projections.users7_humans.is_phone_verified,` + + ` projections.users7_machines.user_id,` + + ` projections.users7_machines.name,` + + ` projections.users7_machines.description,` + + ` projections.users7_machines.has_secret,` + ` COUNT(*) OVER ()` + - ` FROM projections.users6` + - ` LEFT JOIN projections.users6_humans ON projections.users6.id = projections.users6_humans.user_id AND projections.users6.instance_id = projections.users6_humans.instance_id` + - ` LEFT JOIN projections.users6_machines ON projections.users6.id = projections.users6_machines.user_id AND projections.users6.instance_id = projections.users6_machines.instance_id` + + ` FROM projections.users7` + + ` LEFT JOIN projections.users7_humans ON projections.users7.id = projections.users7_humans.user_id AND projections.users7.instance_id = projections.users7_humans.instance_id` + + ` LEFT JOIN projections.users7_machines ON projections.users7.id = projections.users7_machines.user_id AND projections.users7.instance_id = projections.users7_machines.instance_id` + ` LEFT JOIN` + ` (` + loginNamesQuery + `) AS login_names` + - ` ON login_names.user_id = projections.users6.id AND login_names.instance_id = projections.users6.instance_id` + + ` ON login_names.user_id = projections.users7.id AND login_names.instance_id = projections.users7.instance_id` + ` LEFT JOIN` + ` (` + preferredLoginNameQuery + `) AS preferred_login_name` + - ` ON preferred_login_name.user_id = projections.users6.id AND preferred_login_name.instance_id = projections.users6.instance_id` + ` ON preferred_login_name.user_id = projections.users7.id AND preferred_login_name.instance_id = projections.users7.instance_id` userCols = []string{ "id", "creation_date", @@ -86,23 +87,24 @@ var ( "user_id", "name", "description", + "has_secret", "count", } - profileQuery = `SELECT projections.users6.id,` + - ` projections.users6.creation_date,` + - ` projections.users6.change_date,` + - ` projections.users6.resource_owner,` + - ` projections.users6.sequence,` + - ` projections.users6_humans.user_id,` + - ` projections.users6_humans.first_name,` + - ` projections.users6_humans.last_name,` + - ` projections.users6_humans.nick_name,` + - ` projections.users6_humans.display_name,` + - ` projections.users6_humans.preferred_language,` + - ` projections.users6_humans.gender,` + - ` projections.users6_humans.avatar_key` + - ` FROM projections.users6` + - ` LEFT JOIN projections.users6_humans ON projections.users6.id = projections.users6_humans.user_id AND projections.users6.instance_id = projections.users6_humans.instance_id` + profileQuery = `SELECT projections.users7.id,` + + ` projections.users7.creation_date,` + + ` projections.users7.change_date,` + + ` projections.users7.resource_owner,` + + ` projections.users7.sequence,` + + ` projections.users7_humans.user_id,` + + ` projections.users7_humans.first_name,` + + ` projections.users7_humans.last_name,` + + ` projections.users7_humans.nick_name,` + + ` projections.users7_humans.display_name,` + + ` projections.users7_humans.preferred_language,` + + ` projections.users7_humans.gender,` + + ` projections.users7_humans.avatar_key` + + ` FROM projections.users7` + + ` LEFT JOIN projections.users7_humans ON projections.users7.id = projections.users7_humans.user_id AND projections.users7.instance_id = projections.users7_humans.instance_id` profileCols = []string{ "id", "creation_date", @@ -118,16 +120,16 @@ var ( "gender", "avatar_key", } - emailQuery = `SELECT projections.users6.id,` + - ` projections.users6.creation_date,` + - ` projections.users6.change_date,` + - ` projections.users6.resource_owner,` + - ` projections.users6.sequence,` + - ` projections.users6_humans.user_id,` + - ` projections.users6_humans.email,` + - ` projections.users6_humans.is_email_verified` + - ` FROM projections.users6` + - ` LEFT JOIN projections.users6_humans ON projections.users6.id = projections.users6_humans.user_id AND projections.users6.instance_id = projections.users6_humans.instance_id` + emailQuery = `SELECT projections.users7.id,` + + ` projections.users7.creation_date,` + + ` projections.users7.change_date,` + + ` projections.users7.resource_owner,` + + ` projections.users7.sequence,` + + ` projections.users7_humans.user_id,` + + ` projections.users7_humans.email,` + + ` projections.users7_humans.is_email_verified` + + ` FROM projections.users7` + + ` LEFT JOIN projections.users7_humans ON projections.users7.id = projections.users7_humans.user_id AND projections.users7.instance_id = projections.users7_humans.instance_id` emailCols = []string{ "id", "creation_date", @@ -138,16 +140,16 @@ var ( "email", "is_email_verified", } - phoneQuery = `SELECT projections.users6.id,` + - ` projections.users6.creation_date,` + - ` projections.users6.change_date,` + - ` projections.users6.resource_owner,` + - ` projections.users6.sequence,` + - ` projections.users6_humans.user_id,` + - ` projections.users6_humans.phone,` + - ` projections.users6_humans.is_phone_verified` + - ` FROM projections.users6` + - ` LEFT JOIN projections.users6_humans ON projections.users6.id = projections.users6_humans.user_id AND projections.users6.instance_id = projections.users6_humans.instance_id` + phoneQuery = `SELECT projections.users7.id,` + + ` projections.users7.creation_date,` + + ` projections.users7.change_date,` + + ` projections.users7.resource_owner,` + + ` projections.users7.sequence,` + + ` projections.users7_humans.user_id,` + + ` projections.users7_humans.phone,` + + ` projections.users7_humans.is_phone_verified` + + ` FROM projections.users7` + + ` LEFT JOIN projections.users7_humans ON projections.users7.id = projections.users7_humans.user_id AND projections.users7.instance_id = projections.users7_humans.instance_id` phoneCols = []string{ "id", "creation_date", @@ -158,14 +160,14 @@ var ( "phone", "is_phone_verified", } - userUniqueQuery = `SELECT projections.users6.id,` + - ` projections.users6.state,` + - ` projections.users6.username,` + - ` projections.users6_humans.user_id,` + - ` projections.users6_humans.email,` + - ` projections.users6_humans.is_email_verified` + - ` FROM projections.users6` + - ` LEFT JOIN projections.users6_humans ON projections.users6.id = projections.users6_humans.user_id AND projections.users6.instance_id = projections.users6_humans.instance_id` + userUniqueQuery = `SELECT projections.users7.id,` + + ` projections.users7.state,` + + ` projections.users7.username,` + + ` projections.users7_humans.user_id,` + + ` projections.users7_humans.email,` + + ` projections.users7_humans.is_email_verified` + + ` FROM projections.users7` + + ` LEFT JOIN projections.users7_humans ON projections.users7.id = projections.users7_humans.user_id AND projections.users7.instance_id = projections.users7_humans.instance_id` userUniqueCols = []string{ "id", "state", @@ -174,40 +176,40 @@ var ( "email", "is_email_verified", } - notifyUserQuery = `SELECT projections.users6.id,` + - ` projections.users6.creation_date,` + - ` projections.users6.change_date,` + - ` projections.users6.resource_owner,` + - ` projections.users6.sequence,` + - ` projections.users6.state,` + - ` projections.users6.type,` + - ` projections.users6.username,` + + notifyUserQuery = `SELECT projections.users7.id,` + + ` projections.users7.creation_date,` + + ` projections.users7.change_date,` + + ` projections.users7.resource_owner,` + + ` projections.users7.sequence,` + + ` projections.users7.state,` + + ` projections.users7.type,` + + ` projections.users7.username,` + ` login_names.loginnames,` + ` preferred_login_name.login_name,` + - ` projections.users6_humans.user_id,` + - ` projections.users6_humans.first_name,` + - ` projections.users6_humans.last_name,` + - ` projections.users6_humans.nick_name,` + - ` projections.users6_humans.display_name,` + - ` projections.users6_humans.preferred_language,` + - ` projections.users6_humans.gender,` + - ` projections.users6_humans.avatar_key,` + - ` projections.users6_notifications.user_id,` + - ` projections.users6_notifications.last_email,` + - ` projections.users6_notifications.verified_email,` + - ` projections.users6_notifications.last_phone,` + - ` projections.users6_notifications.verified_phone,` + - ` projections.users6_notifications.password_set,` + + ` projections.users7_humans.user_id,` + + ` projections.users7_humans.first_name,` + + ` projections.users7_humans.last_name,` + + ` projections.users7_humans.nick_name,` + + ` projections.users7_humans.display_name,` + + ` projections.users7_humans.preferred_language,` + + ` projections.users7_humans.gender,` + + ` projections.users7_humans.avatar_key,` + + ` projections.users7_notifications.user_id,` + + ` projections.users7_notifications.last_email,` + + ` projections.users7_notifications.verified_email,` + + ` projections.users7_notifications.last_phone,` + + ` projections.users7_notifications.verified_phone,` + + ` projections.users7_notifications.password_set,` + ` COUNT(*) OVER ()` + - ` FROM projections.users6` + - ` LEFT JOIN projections.users6_humans ON projections.users6.id = projections.users6_humans.user_id AND projections.users6.instance_id = projections.users6_humans.instance_id` + - ` LEFT JOIN projections.users6_notifications ON projections.users6.id = projections.users6_notifications.user_id AND projections.users6.instance_id = projections.users6_notifications.instance_id` + + ` FROM projections.users7` + + ` LEFT JOIN projections.users7_humans ON projections.users7.id = projections.users7_humans.user_id AND projections.users7.instance_id = projections.users7_humans.instance_id` + + ` LEFT JOIN projections.users7_notifications ON projections.users7.id = projections.users7_notifications.user_id AND projections.users7.instance_id = projections.users7_notifications.instance_id` + ` LEFT JOIN` + ` (` + loginNamesQuery + `) AS login_names` + - ` ON login_names.user_id = projections.users6.id AND login_names.instance_id = projections.users6.instance_id` + + ` ON login_names.user_id = projections.users7.id AND login_names.instance_id = projections.users7.instance_id` + ` LEFT JOIN` + ` (` + preferredLoginNameQuery + `) AS preferred_login_name` + - ` ON preferred_login_name.user_id = projections.users6.id AND preferred_login_name.instance_id = projections.users6.instance_id` + ` ON preferred_login_name.user_id = projections.users7.id AND preferred_login_name.instance_id = projections.users7.instance_id` notifyUserCols = []string{ "id", "creation_date", @@ -237,41 +239,42 @@ var ( "password_set", "count", } - usersQuery = `SELECT projections.users6.id,` + - ` projections.users6.creation_date,` + - ` projections.users6.change_date,` + - ` projections.users6.resource_owner,` + - ` projections.users6.sequence,` + - ` projections.users6.state,` + - ` projections.users6.type,` + - ` projections.users6.username,` + + usersQuery = `SELECT projections.users7.id,` + + ` projections.users7.creation_date,` + + ` projections.users7.change_date,` + + ` projections.users7.resource_owner,` + + ` projections.users7.sequence,` + + ` projections.users7.state,` + + ` projections.users7.type,` + + ` projections.users7.username,` + ` login_names.loginnames,` + ` preferred_login_name.login_name,` + - ` projections.users6_humans.user_id,` + - ` projections.users6_humans.first_name,` + - ` projections.users6_humans.last_name,` + - ` projections.users6_humans.nick_name,` + - ` projections.users6_humans.display_name,` + - ` projections.users6_humans.preferred_language,` + - ` projections.users6_humans.gender,` + - ` projections.users6_humans.avatar_key,` + - ` projections.users6_humans.email,` + - ` projections.users6_humans.is_email_verified,` + - ` projections.users6_humans.phone,` + - ` projections.users6_humans.is_phone_verified,` + - ` projections.users6_machines.user_id,` + - ` projections.users6_machines.name,` + - ` projections.users6_machines.description,` + + ` projections.users7_humans.user_id,` + + ` projections.users7_humans.first_name,` + + ` projections.users7_humans.last_name,` + + ` projections.users7_humans.nick_name,` + + ` projections.users7_humans.display_name,` + + ` projections.users7_humans.preferred_language,` + + ` projections.users7_humans.gender,` + + ` projections.users7_humans.avatar_key,` + + ` projections.users7_humans.email,` + + ` projections.users7_humans.is_email_verified,` + + ` projections.users7_humans.phone,` + + ` projections.users7_humans.is_phone_verified,` + + ` projections.users7_machines.user_id,` + + ` projections.users7_machines.name,` + + ` projections.users7_machines.description,` + + ` projections.users7_machines.has_secret,` + ` COUNT(*) OVER ()` + - ` FROM projections.users6` + - ` LEFT JOIN projections.users6_humans ON projections.users6.id = projections.users6_humans.user_id AND projections.users6.instance_id = projections.users6_humans.instance_id` + - ` LEFT JOIN projections.users6_machines ON projections.users6.id = projections.users6_machines.user_id AND projections.users6.instance_id = projections.users6_machines.instance_id` + + ` FROM projections.users7` + + ` LEFT JOIN projections.users7_humans ON projections.users7.id = projections.users7_humans.user_id AND projections.users7.instance_id = projections.users7_humans.instance_id` + + ` LEFT JOIN projections.users7_machines ON projections.users7.id = projections.users7_machines.user_id AND projections.users7.instance_id = projections.users7_machines.instance_id` + ` LEFT JOIN` + ` (` + loginNamesQuery + `) AS login_names` + - ` ON login_names.user_id = projections.users6.id AND login_names.instance_id = projections.users6.instance_id` + + ` ON login_names.user_id = projections.users7.id AND login_names.instance_id = projections.users7.instance_id` + ` LEFT JOIN` + ` (` + preferredLoginNameQuery + `) AS preferred_login_name` + - ` ON preferred_login_name.user_id = projections.users6.id AND preferred_login_name.instance_id = projections.users6.instance_id` + ` ON preferred_login_name.user_id = projections.users7.id AND preferred_login_name.instance_id = projections.users7.instance_id` usersCols = []string{ "id", "creation_date", @@ -300,6 +303,7 @@ var ( "user_id", "name", "description", + "has_secret", "count", } ) @@ -372,6 +376,7 @@ func Test_UserPrepares(t *testing.T) { nil, nil, nil, + nil, 1, }, ), @@ -439,6 +444,7 @@ func Test_UserPrepares(t *testing.T) { "id", "name", "description", + true, 1, }, ), @@ -457,6 +463,7 @@ func Test_UserPrepares(t *testing.T) { Machine: &Machine{ Name: "name", Description: "description", + HasSecret: true, }, }, }, @@ -1036,6 +1043,7 @@ func Test_UserPrepares(t *testing.T) { nil, nil, nil, + nil, }, }, ), @@ -1111,6 +1119,7 @@ func Test_UserPrepares(t *testing.T) { nil, nil, nil, + nil, }, { "id", @@ -1140,6 +1149,7 @@ func Test_UserPrepares(t *testing.T) { "id", "name", "description", + true, }, }, ), @@ -1188,6 +1198,7 @@ func Test_UserPrepares(t *testing.T) { Machine: &Machine{ Name: "name", Description: "description", + HasSecret: true, }, }, }, diff --git a/internal/repository/user/eventstore.go b/internal/repository/user/eventstore.go index d925a50654..d216c910c6 100644 --- a/internal/repository/user/eventstore.go +++ b/internal/repository/user/eventstore.go @@ -114,5 +114,9 @@ func RegisterEventMappers(es *eventstore.Eventstore) { RegisterFilterEventMapper(AggregateType, MachineKeyAddedEventType, MachineKeyAddedEventMapper). RegisterFilterEventMapper(AggregateType, MachineKeyRemovedEventType, MachineKeyRemovedEventMapper). RegisterFilterEventMapper(AggregateType, PersonalAccessTokenAddedType, PersonalAccessTokenAddedEventMapper). - RegisterFilterEventMapper(AggregateType, PersonalAccessTokenRemovedType, PersonalAccessTokenRemovedEventMapper) + RegisterFilterEventMapper(AggregateType, PersonalAccessTokenRemovedType, PersonalAccessTokenRemovedEventMapper). + RegisterFilterEventMapper(AggregateType, MachineSecretSetType, MachineSecretSetEventMapper). + RegisterFilterEventMapper(AggregateType, MachineSecretRemovedType, MachineSecretRemovedEventMapper). + RegisterFilterEventMapper(AggregateType, MachineSecretCheckSucceededType, MachineSecretCheckSucceededEventMapper). + RegisterFilterEventMapper(AggregateType, MachineSecretCheckFailedType, MachineSecretCheckFailedEventMapper) } diff --git a/internal/repository/user/machine_secret.go b/internal/repository/user/machine_secret.go new file mode 100644 index 0000000000..e21595b3c8 --- /dev/null +++ b/internal/repository/user/machine_secret.go @@ -0,0 +1,171 @@ +package user + +import ( + "context" + "encoding/json" + + "github.com/zitadel/zitadel/internal/crypto" + "github.com/zitadel/zitadel/internal/errors" + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/eventstore/repository" +) + +const ( + machineSecretPrefix = machineEventPrefix + "secret." + MachineSecretSetType = machineSecretPrefix + "set" + MachineSecretRemovedType = machineSecretPrefix + "removed" + MachineSecretCheckSucceededType = machineSecretPrefix + "check.succeeded" + MachineSecretCheckFailedType = machineSecretPrefix + "check.failed" +) + +type MachineSecretSetEvent struct { + eventstore.BaseEvent `json:"-"` + + ClientSecret *crypto.CryptoValue `json:"clientSecret,omitempty"` +} + +func (e *MachineSecretSetEvent) Data() interface{} { + return e +} + +func (e *MachineSecretSetEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint { + return nil +} + +func NewMachineSecretSetEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, + clientSecret *crypto.CryptoValue, +) *MachineSecretSetEvent { + return &MachineSecretSetEvent{ + BaseEvent: *eventstore.NewBaseEventForPush( + ctx, + aggregate, + MachineSecretSetType, + ), + ClientSecret: clientSecret, + } +} + +func MachineSecretSetEventMapper(event *repository.Event) (eventstore.Event, error) { + credentialsSet := &MachineSecretSetEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(event), + } + err := json.Unmarshal(event.Data, credentialsSet) + if err != nil { + return nil, errors.ThrowInternal(err, "USER-lopbqu", "unable to unmarshal machine secret set") + } + + return credentialsSet, nil +} + +type MachineSecretRemovedEvent struct { + eventstore.BaseEvent `json:"-"` +} + +func (e *MachineSecretRemovedEvent) Data() interface{} { + return e +} + +func (e *MachineSecretRemovedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint { + return nil +} + +func NewMachineSecretRemovedEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, +) *MachineSecretRemovedEvent { + return &MachineSecretRemovedEvent{ + BaseEvent: *eventstore.NewBaseEventForPush( + ctx, + aggregate, + MachineSecretRemovedType, + ), + } +} + +func MachineSecretRemovedEventMapper(event *repository.Event) (eventstore.Event, error) { + credentialsRemoved := &MachineSecretRemovedEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(event), + } + err := json.Unmarshal(event.Data, credentialsRemoved) + if err != nil { + return nil, errors.ThrowInternal(err, "USER-quox9j2", "unable to unmarshal machine secret removed") + } + + return credentialsRemoved, nil +} + +type MachineSecretCheckSucceededEvent struct { + eventstore.BaseEvent `json:"-"` +} + +func (e *MachineSecretCheckSucceededEvent) Data() interface{} { + return e +} + +func (e *MachineSecretCheckSucceededEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint { + return nil +} + +func NewMachineSecretCheckSucceededEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, +) *MachineSecretCheckSucceededEvent { + return &MachineSecretCheckSucceededEvent{ + BaseEvent: *eventstore.NewBaseEventForPush( + ctx, + aggregate, + MachineSecretCheckSucceededType, + ), + } +} + +func MachineSecretCheckSucceededEventMapper(event *repository.Event) (eventstore.Event, error) { + check := &MachineSecretCheckSucceededEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(event), + } + err := json.Unmarshal(event.Data, check) + if err != nil { + return nil, errors.ThrowInternal(err, "USER-x002n1p", "unable to unmarshal machine secret check succeeded") + } + + return check, nil +} + +type MachineSecretCheckFailedEvent struct { + eventstore.BaseEvent `json:"-"` +} + +func (e *MachineSecretCheckFailedEvent) Data() interface{} { + return e +} + +func (e *MachineSecretCheckFailedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint { + return nil +} + +func NewMachineSecretCheckFailedEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, +) *MachineSecretCheckFailedEvent { + return &MachineSecretCheckFailedEvent{ + BaseEvent: *eventstore.NewBaseEventForPush( + ctx, + aggregate, + MachineSecretCheckFailedType, + ), + } +} + +func MachineSecretCheckFailedEventMapper(event *repository.Event) (eventstore.Event, error) { + check := &MachineSecretCheckFailedEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(event), + } + err := json.Unmarshal(event.Data, check) + if err != nil { + return nil, errors.ThrowInternal(err, "USER-x7901b1l", "unable to unmarshal machine secret check failed") + } + + return check, nil +} diff --git a/internal/static/i18n/de.yaml b/internal/static/i18n/de.yaml index c3d36778e9..944c341809 100644 --- a/internal/static/i18n/de.yaml +++ b/internal/static/i18n/de.yaml @@ -87,6 +87,10 @@ Errors: Key: NotFound: Maschinen Schlüssel nicht gefunden AlreadyExisting: Machine Schlüssel exisiert bereits + Secret: + NotExisting: Secret existiert nicht + Invalid: Secret ist ungültig + CouldNotGenerate: Secret konnte nicht generiert werden PAT: NotFound: Persönliches Access Token nicht gefunden NotHuman: Der Benutzer muss eine Person sein @@ -475,8 +479,14 @@ EventTypes: added: Technischer Benutzer hinzugefügt changed: Technischer Benutzer geändert key: - added: Key added - removed: Key removed + added: Key hinzugefügt + removed: Key entfernt + secret: + set: Secret gesetzt + removed: Secret entfernt + check: + succeeded: Secret Überprüfung erfolgreich + failed: Secret Überprüfung fehlgeschlagen human: added: Benutzer hinzugefügt selfregistered: Benutzer hat sich selbst registriert diff --git a/internal/static/i18n/en.yaml b/internal/static/i18n/en.yaml index 5999049c68..44eaaefb96 100644 --- a/internal/static/i18n/en.yaml +++ b/internal/static/i18n/en.yaml @@ -87,6 +87,10 @@ Errors: Key: NotFound: Machine key not found AlreadyExisting: Machine key already existing + Secret: + NotExisting: Secret doesn't exist + Invalid: Secret is invalid + CouldNotGenerate: Secret could not be generated PAT: NotFound: Personal Access Token not found NotHuman: The User must be personal @@ -477,6 +481,12 @@ EventTypes: key: added: Key added removed: Key removed + secret: + set: Secret set + removed: Secret removed + check: + succeeded: Secret check succeeded + failed: Secret check failed human: added: Person added selfregistered: Person registered himself diff --git a/internal/static/i18n/fr.yaml b/internal/static/i18n/fr.yaml index 2b93209a0d..8cf12e5404 100644 --- a/internal/static/i18n/fr.yaml +++ b/internal/static/i18n/fr.yaml @@ -87,6 +87,10 @@ Errors: Key: NotFound: Clé de la machine non trouvée AlreadyExisting: Clé de la machine déjà existante + Secret: + NotExisting: Secret n'existe pas + Invalid: Secret n'est pas valide + CouldNotGenerate: Secret n'a pas pu être généré PAT: NotFound: Token d'accès personnel non trouvé NotHuman: L'utilisateur doit être personnel @@ -475,6 +479,12 @@ EventTypes: key: added: Clé ajoutée removed: Clé supprimée + secret: + set: Secret défini + removed: Secret supprimée + check: + succeeded: La vérification de Secret réussie + failed: La vérification de Secret a échoué human: added: Personne ajoutée selfregistered: La personne s'est enregistrée elle-même diff --git a/internal/static/i18n/it.yaml b/internal/static/i18n/it.yaml index ba5f65278a..e8645c8ad5 100644 --- a/internal/static/i18n/it.yaml +++ b/internal/static/i18n/it.yaml @@ -87,6 +87,10 @@ Errors: Key: NotFound: Chiave macchina non trovato AlreadyExisting: Chiave macchina già esistente + Secret: + NotExisting: Secret non esiste + Invalid: Secret non è valido + CouldNotGenerate: Non è stato possibile generare il Secret PAT: NotFound: Personal Access Token non trovato NotHuman: L'utente deve essere personale @@ -475,6 +479,12 @@ EventTypes: key: added: Chiave aggiunta removed: Chiave rimossa + secret: + set: Secret set + removed: Secret rimosso + check: + succeeded: Controllo della Secret riuscito + failed: Controllo della Secret fallito human: added: Persona aggiunta selfregistered: Persona registrata diff --git a/internal/static/i18n/zh.yaml b/internal/static/i18n/zh.yaml index 3130be3639..84fd09a2a4 100644 --- a/internal/static/i18n/zh.yaml +++ b/internal/static/i18n/zh.yaml @@ -87,6 +87,10 @@ Errors: Key: NotFound: 未找到机器密钥 AlreadyExisting: 已有的机器钥匙 + Secret: + NotExisting: 秘密并不存在 + Invalid: 秘密是无效的 + CouldNotGenerate: 无法生成秘密 PAT: NotFound: 未找到个人访问令牌 NotHuman: 用户必须是个人 @@ -465,6 +469,12 @@ EventTypes: key: added: 添加服务用户 Key removed: 删除服务用户 Key + secret: + set: 秘密套装 + removed: 秘密删除 + check: + succeeded: 成功的秘密控制 + failed: 秘密控制失败 human: added: 添加用户 selfregistered: 自注册用户 diff --git a/proto/zitadel/management.proto b/proto/zitadel/management.proto index ea4b3c8386..a13994b88b 100644 --- a/proto/zitadel/management.proto +++ b/proto/zitadel/management.proto @@ -625,6 +625,29 @@ service ManagementService { }; } + // Generates and sets a new machine secret + rpc GenerateMachineSecret(GenerateMachineSecretRequest) returns (GenerateMachineSecretResponse) { + option (google.api.http) = { + put: "/users/{user_id}/secret" + body: "*" + }; + + option (zitadel.v1.auth_option) = { + permission: "user.write" + }; + } + + // Removes the machine secret + rpc RemoveMachineSecret(RemoveMachineSecretRequest) returns (RemoveMachineSecretResponse) { + option (google.api.http) = { + delete: "/users/{user_id}/secret" + }; + + option (zitadel.v1.auth_option) = { + permission: "user.write" + }; + } + // Returns a machine key of a (machine) user rpc GetMachineKeyByIDs(GetMachineKeyByIDsRequest) returns (GetMachineKeyByIDsResponse) { option (google.api.http) = { @@ -3616,6 +3639,24 @@ message UpdateMachineResponse { zitadel.v1.ObjectDetails details = 1; } +message GenerateMachineSecretRequest { + string user_id = 1 [(validate.rules).string.min_len = 1]; +} + +message GenerateMachineSecretResponse { + string client_id = 1; + string client_secret = 2; + zitadel.v1.ObjectDetails details = 3; +} + +message RemoveMachineSecretRequest { + string user_id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; +} + +message RemoveMachineSecretResponse { + zitadel.v1.ObjectDetails details = 1; +} + message GetMachineKeyByIDsRequest { string user_id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; string key_id = 2 [(validate.rules).string = {min_len: 1, max_len: 200}]; diff --git a/proto/zitadel/user.proto b/proto/zitadel/user.proto index 258bf44f1e..67ebe45c0b 100644 --- a/proto/zitadel/user.proto +++ b/proto/zitadel/user.proto @@ -78,6 +78,11 @@ message Machine { example: "\"The one and only IAM\""; } ]; + bool has_secret = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"true\""; + } + ]; } message Profile {