feat: support client_credentials for service users (#5134)

Request an access_token for service users with OAuth 2.0 Client Credentials Grant. Added functionality to generate and remove a secret on service users.
This commit is contained in:
Stefan Benz 2023-01-31 20:52:47 +01:00 committed by GitHub
parent 7c7c93117b
commit e2fdd3f077
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
48 changed files with 2113 additions and 311 deletions

View File

@ -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,

View File

@ -0,0 +1,50 @@
<h1 mat-dialog-title>
<span class="title">{{ 'USER.SECRETDIALOG.CLIENTSECRET' | translate }}</span>
</h1>
<p class="desc cnsl-secondary-text">{{ 'USER.SECRETDIALOG.CLIENTSECRET_DESCRIPTION' | translate }}</p>
<div mat-dialog-content>
<div class="flex" *ngIf="data.clientId">
<span class="overflow-auto" data-e2e="client-id"><span class="desc">ClientId:</span> {{ data.clientId }}</span>
<button
color="primary"
[disabled]="copied === data.clientId"
matTooltip="copy to clipboard"
cnslCopyToClipboard
[valueToCopy]="data.clientId"
(copiedValue)="this.copied = $event"
mat-icon-button
data-e2e="client-id-copy"
>
<i *ngIf="copied !== data.clientId" class="las la-clipboard"></i>
<i *ngIf="copied === data.clientId" class="las la-clipboard-check"></i>
</button>
</div>
<div *ngIf="data.clientSecret" class="flex">
<span class="overflow-auto"><span class="desc cnsl-secondary-text">ClientSecret:</span> {{ data.clientSecret }}</span>
<button
color="primary"
[disabled]="copied === data.clientSecret"
matTooltip="copy to clipboard"
cnslCopyToClipboard
[valueToCopy]="data.clientSecret"
(copiedValue)="this.copied = $event"
mat-icon-button
>
<i *ngIf="copied !== data.clientSecret" class="las la-clipboard"></i>
<i *ngIf="copied === data.clientSecret" class="las la-clipboard-check"></i>
</button>
</div>
</div>
<div mat-dialog-actions class="action">
<button
cdkFocusInitial
color="primary"
mat-raised-button
class="ok-button"
(click)="closeDialog()"
data-e2e="close-dialog"
>
{{ 'ACTIONS.CLOSE' | translate }}
</button>
</div>

View File

@ -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;
}
}
}

View File

@ -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<MachineSecretDialogComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [MachineSecretDialogComponent],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(MachineSecretDialogComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -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<MachineSecretDialogComponent>, @Inject(MAT_DIALOG_DATA) public data: any) {}
public closeDialog(): void {
this.dialogRef.close(false);
}
}

View File

@ -10,6 +10,12 @@
[hasActions]="['user.write$', 'user.write:' + user.id] | hasRole | async"
>
<ng-template topActions cnslHasRole [hasRole]="['user.write$', 'user.write:' + user.id]">
<button mat-menu-item color="warn" *ngIf="user?.machine" (click)="generateMachineSecret()">
{{ 'USER.PAGES.GENERATESECRET' | translate }}
</button>
<button mat-menu-item color="warn" *ngIf="user?.machine?.hasSecret" (click)="removeMachineSecret()">
{{ 'USER.PAGES.REMOVESECRET' | translate }}
</button>
<button mat-menu-item color="warn" *ngIf="user?.state === UserState.USER_STATE_LOCKED" (click)="unlockUser()">
{{ 'USER.PAGES.UNLOCK' | translate }}
</button>

View File

@ -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

View File

@ -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<GenerateMachineSecretResponse.AsObject> {
const req = new GenerateMachineSecretRequest();
req.setUserId(userId);
return this.grpcService.mgmt.generateMachineSecret(req, null).then((resp) => resp.toObject());
}
public removeMachineSecret(userId: string): Promise<RemoveMachineSecretResponse.AsObject> {
const req = new RemoveMachineSecretRequest();
req.setUserId(userId);
return this.grpcService.mgmt.removeMachineSecret(req, null).then((resp) => resp.toObject());
}
public getPrivacyPolicy(): Promise<GetPrivacyPolicyResponse.AsObject> {
const req = new GetPrivacyPolicyRequest();
return this.grpcService.mgmt.getPrivacyPolicy(req, null).then((resp) => resp.toObject());

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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 管理角色",

View File

@ -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 |

View File

@ -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)
**Link to spec.** [OThe OAuth 2.0 Authorization Framework Section 1.3.3](https://tools.ietf.org/html/rfc6749#section-1.3.3)

View File

@ -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<br /> |
### 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<br /> string.max_len: 200<br /> |
### RemoveMachineSecretResponse
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| details | zitadel.v1.ObjectDetails | - | |
### RemoveMultiFactorFromLoginPolicyRequest

View File

@ -133,6 +133,7 @@ title: zitadel/user.proto
| ----- | ---- | ----------- | ----------- |
| name | string | - | |
| description | string | - | |
| has_secret | bool | - | |

View File

@ -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

View File

@ -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",

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 MiB

2
go.mod
View File

@ -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

4
go.sum
View File

@ -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=

View File

@ -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 {

View File

@ -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,
}
}

View File

@ -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) }()

View File

@ -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
}

View File

@ -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()
}

View File

@ -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
}
}

View File

@ -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)
}
})
}
}

View File

@ -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
}

View File

@ -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")

View File

@ -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 " +

View File

@ -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 " +

View File

@ -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 " +

View File

@ -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 {

View File

@ -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",
},

View File

@ -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,
}
}

View File

@ -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" +

View File

@ -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,
},
},
},

View File

@ -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)
}

View File

@ -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
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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: 自注册用户

View File

@ -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}];

View File

@ -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 {