mirror of
https://github.com/andwati/zitadel.git
synced 2026-10-26 01:02:16 +00:00
feat(action v2): add JWT and JWE payload type options (#11196)
# Which Problems Are Solved The payload in actions V2 is currently sent as JSON to the target endpoint. It might get exposed to intermediary infrastructure or logging systems. For these scenarios there needs to be an application-layer encryption, where the provider of the endpoint can define an key to be used for the encryption. # How the Problems Are Solved - Added an additional option to the target to specify the payload type: `JSON` (current and default), `JWT`, `JWE` (api and console) - added endpoints to upload and manage public keys (to be used for encryption) for a target - updated all action v2 executions (interceptors, oidc, saml, ...) to provide the `GetActiveSigningWebKey` from queries - implemented jwt and jwe in the exections incl. refactoring of code and tests - changes to the authn_keys table: - added a `fingerprint` column - dropped not null constraint on expiration - moved the `GetSignerOnce` into its own package to prevent circular dependencies # Additional Changes None # Additional Context closes #11061 --------- Co-authored-by: Marco A. <[email protected]> Co-authored-by: conblem <[email protected]> Co-authored-by: Silvan <[email protected]>
This commit is contained in:
co-authored by
Marco A.
conblem
Silvan
parent
72c8f0192c
commit
791d0587aa
@@ -0,0 +1,27 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"context"
|
||||
_ "embed"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/database"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
)
|
||||
|
||||
var (
|
||||
//go:embed 68.sql
|
||||
targetAddPayloadTypeAndKeyFingerprintColumn string
|
||||
)
|
||||
|
||||
type TargetAddPayloadTypeColumn struct {
|
||||
dbClient *database.DB
|
||||
}
|
||||
|
||||
func (mig *TargetAddPayloadTypeColumn) Execute(ctx context.Context, _ eventstore.Event) error {
|
||||
_, err := mig.dbClient.ExecContext(ctx, targetAddPayloadTypeAndKeyFingerprintColumn)
|
||||
return err
|
||||
}
|
||||
|
||||
func (mig *TargetAddPayloadTypeColumn) String() string {
|
||||
return "68_target2_add_payload_type_authn_key2_fingerprint_expiration"
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
ALTER TABLE IF EXISTS projections.targets2
|
||||
ADD COLUMN IF NOT EXISTS payload_type smallint default 0;
|
||||
|
||||
ALTER TABLE IF EXISTS projections.authn_keys2
|
||||
ADD COLUMN IF NOT EXISTS fingerprint text;
|
||||
|
||||
ALTER TABLE IF EXISTS projections.authn_keys2
|
||||
ALTER COLUMN expiration DROP NOT NULL;
|
||||
@@ -164,6 +164,7 @@ type Steps struct {
|
||||
s65FixUserMetadata5Index *FixUserMetadata5Index
|
||||
s66SessionRecoveryCodeCheckedAt *SessionRecoveryCodeCheckedAt
|
||||
s67SyncMemberRoleFields *SyncMemberRoleFields
|
||||
s68TargetAddPayloadTypeColumn *TargetAddPayloadTypeColumn
|
||||
}
|
||||
|
||||
func MustNewSteps(v *viper.Viper) *Steps {
|
||||
|
||||
@@ -225,6 +225,7 @@ func Setup(ctx context.Context, config *Config, steps *Steps, masterKey string)
|
||||
steps.s65FixUserMetadata5Index = &FixUserMetadata5Index{dbClient: dbClient}
|
||||
steps.s66SessionRecoveryCodeCheckedAt = &SessionRecoveryCodeCheckedAt{dbClient: dbClient}
|
||||
steps.s67SyncMemberRoleFields = &SyncMemberRoleFields{dbClient: dbClient}
|
||||
steps.s68TargetAddPayloadTypeColumn = &TargetAddPayloadTypeColumn{dbClient: dbClient}
|
||||
|
||||
err = projection.Create(ctx, dbClient, eventstoreClient, config.Projections, nil, nil, nil)
|
||||
logging.OnError(err).Fatal("unable to start projections")
|
||||
@@ -342,6 +343,7 @@ func Setup(ctx context.Context, config *Config, steps *Steps, masterKey string)
|
||||
steps.s48Apps7SAMLConfigsLoginVersion,
|
||||
steps.s59SetupWebkeys, // this step needs commands.
|
||||
steps.s66SessionRecoveryCodeCheckedAt,
|
||||
steps.s68TargetAddPayloadTypeColumn,
|
||||
} {
|
||||
setupErr = executeMigration(ctx, eventstoreClient, step, "migration failed")
|
||||
if setupErr != nil {
|
||||
|
||||
@@ -19,7 +19,6 @@ func (mig *TransactionalTables) Execute(ctx context.Context, _ eventstore.Event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return pool.Migrate(ctx)
|
||||
}
|
||||
|
||||
|
||||
@@ -331,6 +331,7 @@ func startZitadel(ctx context.Context, config *Config, masterKey string, server
|
||||
config.Executions,
|
||||
q,
|
||||
keys.Target,
|
||||
queries.GetActiveSigningWebKey,
|
||||
)
|
||||
execution.Start(ctx)
|
||||
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ import { ChangeDetectionStrategy, Component, computed, effect, EventEmitter, Inp
|
||||
import { combineLatestWith, Observable, ReplaySubject } from 'rxjs';
|
||||
import { filter, map, startWith } from 'rxjs/operators';
|
||||
import { MatTableDataSource } from '@angular/material/table';
|
||||
import { Target } from '@zitadel/proto/zitadel/action/v2beta/target_pb';
|
||||
import { Target } from '@zitadel/proto/zitadel/action/v2/target_pb';
|
||||
import { toSignal } from '@angular/core/rxjs-interop';
|
||||
import { CorrectlyTypedExecution } from '../../actions-two-add-action/actions-two-add-action-dialog.component';
|
||||
|
||||
|
||||
+3
-3
@@ -13,10 +13,10 @@ import {
|
||||
} from '../actions-two-add-action/actions-two-add-action-dialog.component';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { MessageInitShape } from '@bufbuild/protobuf';
|
||||
import { SetExecutionRequestSchema } from '@zitadel/proto/zitadel/action/v2beta/action_service_pb';
|
||||
import { Target } from '@zitadel/proto/zitadel/action/v2beta/target_pb';
|
||||
import { SetExecutionRequestSchema } from '@zitadel/proto/zitadel/action/v2/action_service_pb';
|
||||
import { Target } from '@zitadel/proto/zitadel/action/v2/target_pb';
|
||||
import { InfoSectionType } from '../../info-section/info-section.component';
|
||||
import { ExecutionFieldName } from '@zitadel/proto/zitadel/action/v2beta/query_pb';
|
||||
import { ExecutionFieldName } from '@zitadel/proto/zitadel/action/v2/query_pb';
|
||||
|
||||
@Component({
|
||||
selector: 'cnsl-actions-two-actions',
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ import { ToastService } from 'src/app/services/toast.service';
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
import { atLeastOneFieldValidator, requiredValidator } from 'src/app/modules/form-field/validators/validators';
|
||||
import { Message } from '@bufbuild/protobuf';
|
||||
import { Condition } from '@zitadel/proto/zitadel/action/v2beta/execution_pb';
|
||||
import { Condition } from '@zitadel/proto/zitadel/action/v2/execution_pb';
|
||||
import { distinctUntilChanged, startWith, takeUntil } from 'rxjs/operators';
|
||||
import { CreateQueryResult } from '@tanstack/angular-query-experimental';
|
||||
import { toSignal } from '@angular/core/rxjs-interop';
|
||||
|
||||
+2
-2
@@ -10,9 +10,9 @@ import {
|
||||
} from './actions-two-add-action-condition/actions-two-add-action-condition.component';
|
||||
import { ActionsTwoAddActionTargetComponent } from './actions-two-add-action-target/actions-two-add-action-target.component';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Condition, Execution } from '@zitadel/proto/zitadel/action/v2beta/execution_pb';
|
||||
import { Condition, Execution } from '@zitadel/proto/zitadel/action/v2/execution_pb';
|
||||
import { Subject } from 'rxjs';
|
||||
import { SetExecutionRequestSchema } from '@zitadel/proto/zitadel/action/v2beta/action_service_pb';
|
||||
import { SetExecutionRequestSchema } from '@zitadel/proto/zitadel/action/v2/action_service_pb';
|
||||
|
||||
enum Page {
|
||||
Type,
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@ import { ToastService } from 'src/app/services/toast.service';
|
||||
import { MatAutocompleteModule } from '@angular/material/autocomplete';
|
||||
import { InputModule } from 'src/app/modules/input/input.module';
|
||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
import { Target } from '@zitadel/proto/zitadel/action/v2beta/target_pb';
|
||||
import { Target } from '@zitadel/proto/zitadel/action/v2/target_pb';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { ActionConditionPipeModule } from 'src/app/pipes/action-condition-pipe/action-condition-pipe.module';
|
||||
import { MatTableDataSource, MatTableModule } from '@angular/material/table';
|
||||
|
||||
+12
@@ -31,6 +31,18 @@
|
||||
</span>
|
||||
</cnsl-form-field>
|
||||
|
||||
<cnsl-form-field class="full-width">
|
||||
<cnsl-label>{{ 'ACTIONSTWO.TARGET.CREATE.PAYLOAD_TYPE' | translate }}</cnsl-label>
|
||||
<mat-select [formControl]="targetForm.controls.payloadType" name="payloadType">
|
||||
<mat-option *ngFor="let type of payloadTypes" [value]="type">
|
||||
{{ 'ACTIONSTWO.TARGET.CREATE.PAYLOAD_TYPES.' + type | translate }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
<span class="name-hint cnsl-secondary-text types-description">
|
||||
{{ 'ACTIONSTWO.TARGET.CREATE.PAYLOAD_TYPE_DESCRIPTION' | translate }}
|
||||
</span>
|
||||
</cnsl-form-field>
|
||||
|
||||
<cnsl-form-field class="full-width">
|
||||
<cnsl-label>{{ 'ACTIONSTWO.TARGET.CREATE.TIMEOUT' | translate }}</cnsl-label>
|
||||
<input cnslInput type="number" placeholder="10" [formControl]="targetForm.controls.timeout" />
|
||||
|
||||
+12
-6
@@ -10,11 +10,9 @@ import { requiredValidator } from '../../form-field/validators/validators';
|
||||
import { MessageInitShape } from '@bufbuild/protobuf';
|
||||
import { DurationSchema } from '@bufbuild/protobuf/wkt';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { Target } from '@zitadel/proto/zitadel/action/v2beta/target_pb';
|
||||
import {
|
||||
CreateTargetRequestSchema,
|
||||
UpdateTargetRequestSchema,
|
||||
} from '@zitadel/proto/zitadel/action/v2beta/action_service_pb';
|
||||
import { PayloadType, Target } from '@zitadel/proto/zitadel/action/v2/target_pb';
|
||||
import { CreateTargetRequestSchema, UpdateTargetRequestSchema } from '@zitadel/proto/zitadel/action/v2/action_service_pb';
|
||||
import { getEnumKeyFromValue, getEnumKeys } from 'src/app/utils/enum.utils';
|
||||
|
||||
type TargetTypes = ActionTwoAddTargetDialogComponent['targetTypes'][number];
|
||||
|
||||
@@ -36,6 +34,7 @@ type TargetTypes = ActionTwoAddTargetDialogComponent['targetTypes'][number];
|
||||
export class ActionTwoAddTargetDialogComponent {
|
||||
protected readonly targetTypes = ['restCall', 'restWebhook', 'restAsync'] as const;
|
||||
protected readonly targetForm: ReturnType<typeof this.buildTargetForm>;
|
||||
protected readonly payloadTypes = getEnumKeys(PayloadType);
|
||||
|
||||
constructor(
|
||||
private fb: FormBuilder,
|
||||
@@ -56,6 +55,8 @@ export class ActionTwoAddTargetDialogComponent {
|
||||
endpoint: data.target.endpoint,
|
||||
timeout: Number(data.target.timeout?.seconds),
|
||||
type: this.data.target?.targetType?.case ?? 'restWebhook',
|
||||
payloadType:
|
||||
getEnumKeyFromValue(PayloadType, this.data.target?.payloadType ?? PayloadType.UNSPECIFIED) || this.payloadTypes[0],
|
||||
interruptOnError:
|
||||
data.target.targetType.case === 'restWebhook' || data.target.targetType.case === 'restCall'
|
||||
? data.target.targetType.value.interruptOnError
|
||||
@@ -70,6 +71,10 @@ export class ActionTwoAddTargetDialogComponent {
|
||||
nonNullable: true,
|
||||
validators: [requiredValidator],
|
||||
}),
|
||||
payloadType: new FormControl<(typeof this.payloadTypes)[number]>(this.payloadTypes[0], {
|
||||
nonNullable: true,
|
||||
validators: [requiredValidator],
|
||||
}),
|
||||
endpoint: new FormControl<string>('', { nonNullable: true, validators: [requiredValidator] }),
|
||||
timeout: new FormControl<number>(10, { nonNullable: true, validators: [requiredValidator] }),
|
||||
interruptOnError: new FormControl<boolean>(false, { nonNullable: true }),
|
||||
@@ -81,7 +86,7 @@ export class ActionTwoAddTargetDialogComponent {
|
||||
return;
|
||||
}
|
||||
|
||||
const { type, name, endpoint, timeout, interruptOnError } = this.targetForm.getRawValue();
|
||||
const { type, name, endpoint, timeout, interruptOnError, payloadType } = this.targetForm.getRawValue();
|
||||
|
||||
const timeoutDuration: MessageInitShape<typeof DurationSchema> = {
|
||||
seconds: BigInt(timeout),
|
||||
@@ -100,6 +105,7 @@ export class ActionTwoAddTargetDialogComponent {
|
||||
endpoint,
|
||||
timeout: timeoutDuration,
|
||||
targetType,
|
||||
payloadType: PayloadType[payloadType],
|
||||
};
|
||||
|
||||
this.dialogRef.close(
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ import { ChangeDetectionStrategy, Component, effect, EventEmitter, Input, Output
|
||||
import { ReplaySubject } from 'rxjs';
|
||||
import { filter, startWith } from 'rxjs/operators';
|
||||
import { MatTableDataSource } from '@angular/material/table';
|
||||
import { Target } from '@zitadel/proto/zitadel/action/v2beta/target_pb';
|
||||
import { Target } from '@zitadel/proto/zitadel/action/v2/target_pb';
|
||||
import { toSignal } from '@angular/core/rxjs-interop';
|
||||
|
||||
@Component({
|
||||
|
||||
+2
-5
@@ -7,11 +7,8 @@ import { catchError, map, startWith, switchMap } from 'rxjs/operators';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { ActionTwoAddTargetDialogComponent } from '../actions-two-add-target/actions-two-add-target-dialog.component';
|
||||
import { MessageInitShape } from '@bufbuild/protobuf';
|
||||
import { Target } from '@zitadel/proto/zitadel/action/v2beta/target_pb';
|
||||
import {
|
||||
CreateTargetRequestSchema,
|
||||
UpdateTargetRequestSchema,
|
||||
} from '@zitadel/proto/zitadel/action/v2beta/action_service_pb';
|
||||
import { Target } from '@zitadel/proto/zitadel/action/v2/target_pb';
|
||||
import { CreateTargetRequestSchema, UpdateTargetRequestSchema } from '@zitadel/proto/zitadel/action/v2/action_service_pb';
|
||||
import { InfoSectionType } from '../../info-section/info-section.component';
|
||||
|
||||
@Component({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core';
|
||||
import { Condition } from '@zitadel/proto/zitadel/action/v2beta/execution_pb';
|
||||
import { Condition } from '@zitadel/proto/zitadel/action/v2/execution_pb';
|
||||
|
||||
@Pipe({
|
||||
name: 'condition',
|
||||
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
SetExecutionResponse,
|
||||
UpdateTargetRequestSchema,
|
||||
UpdateTargetResponse,
|
||||
} from '@zitadel/proto/zitadel/action/v2beta/action_service_pb';
|
||||
} from '@zitadel/proto/zitadel/action/v2/action_service_pb';
|
||||
import { UserService } from './user.service';
|
||||
import { injectQuery } from '@tanstack/angular-query-experimental';
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ import { createGrpcWebTransport } from '@connectrpc/connect-web';
|
||||
import { createClientFor } from '@zitadel/client';
|
||||
|
||||
import { WebKeyService } from '@zitadel/proto/zitadel/webkey/v2beta/webkey_service_pb';
|
||||
import { ActionService } from '@zitadel/proto/zitadel/action/v2beta/action_service_pb';
|
||||
import { ActionService } from '@zitadel/proto/zitadel/action/v2/action_service_pb';
|
||||
|
||||
const createWebKeyServiceClient = createClientFor(WebKeyService);
|
||||
const createActionServiceClient = createClientFor(ActionService);
|
||||
|
||||
@@ -30,8 +30,8 @@ type NumericEnum = Record<string, string | number> & Record<number, string>;
|
||||
* // Returns: ['SAML_BINDING_UNSPECIFIED', 'SAML_BINDING_POST', ...]
|
||||
* ```
|
||||
*/
|
||||
export function getEnumKeys<T extends NumericEnum>(enumObject: T): string[] {
|
||||
return Object.keys(enumObject).filter((key) => isNaN(Number(key)));
|
||||
export function getEnumKeys<T extends NumericEnum>(enumObject: T): (keyof T)[] {
|
||||
return Object.keys(enumObject).filter((key) => isNaN(Number(key))) as unknown as (keyof T)[];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -45,7 +45,7 @@ export function getEnumKeys<T extends NumericEnum>(enumObject: T): string[] {
|
||||
* // Returns: 'SAML_BINDING_POST'
|
||||
* ```
|
||||
*/
|
||||
export function getEnumKeyFromValue<T extends NumericEnum>(enumObject: T, value: number): string | undefined {
|
||||
export function getEnumKeyFromValue<T extends NumericEnum>(enumObject: T, value: number): keyof T | undefined {
|
||||
return Object.keys(enumObject).find((key) => enumObject[key] === value && isNaN(Number(key)));
|
||||
}
|
||||
|
||||
|
||||
@@ -622,6 +622,14 @@
|
||||
"restAsync": "REST асинхронно"
|
||||
},
|
||||
"TYPES_DESCRIPTION": "Webhook, обаждането обработва кода на състоянието, но отговорът е без значение\nCall, обаждането обработва кода на състоянието и отговора\nAsync, обаждането не обработва нито кода на състоянието, нито отговора, но може да бъде извикано паралелно с други цели",
|
||||
"PAYLOAD_TYPE": "Тип на съдържанието",
|
||||
"PAYLOAD_TYPES": {
|
||||
"UNSPECIFIED": "Не е уточнено",
|
||||
"JSON": "JSON",
|
||||
"JWT": "JSON Web Token (JWT)",
|
||||
"JWE": "Шифрован JSON Web Token (JWE)"
|
||||
},
|
||||
"PAYLOAD_TYPE_DESCRIPTION": "JSON: Стандартно JSON съдържание със заглавна част за подпис и валидация.\nJWT: Съдържанието се изпраща като подписан JWT. Верификацията се извършва чрез подписа на JWT.\nJWE: JWT е допълнително шифрован, за да се предотврати неоторизиран достъп до информацията в съдържанието.",
|
||||
"ENDPOINT": "Крайна точка",
|
||||
"ENDPOINT_DESCRIPTION": "Въведете крайната точка, където се хоства вашият код. Уверете се, че е достъпна за нас!",
|
||||
"TIMEOUT": "Време за изчакване",
|
||||
|
||||
@@ -623,6 +623,14 @@
|
||||
"restAsync": "REST Asynchronní"
|
||||
},
|
||||
"TYPES_DESCRIPTION": "Webhook, volání zpracovává stavový kód, ale odpověď je irelevantní\nCall, volání zpracovává stavový kód a odpověď\nAsync, volání nezpracovává ani stavový kód, ani odpověď, ale může být spuštěno paralelně s jinými cíli",
|
||||
"PAYLOAD_TYPE": "Typ obsahu",
|
||||
"PAYLOAD_TYPES": {
|
||||
"UNSPECIFIED": "Neurčeno",
|
||||
"JSON": "JSON",
|
||||
"JWT": "JSON Web Token (JWT)",
|
||||
"JWE": "Šifrovaný JSON Web Token (JWE)"
|
||||
},
|
||||
"PAYLOAD_TYPE_DESCRIPTION": "JSON: Standardní JSON payload s hlavičkou pro podpis a ověření.\nJWT: Payload je odeslán jako podepsané JWT. Ověření probíhá pomocí podpisu JWT.\nJWE: JWT je navíc zašifrován, aby se předešlo neoprávněnému přístupu k informacím v payloadu.",
|
||||
"ENDPOINT": "Koncový bod",
|
||||
"ENDPOINT_DESCRIPTION": "Zadejte koncový bod, kde je hostován váš kód. Ujistěte se, že je pro nás přístupný!",
|
||||
"TIMEOUT": "Časový limit",
|
||||
|
||||
@@ -623,6 +623,14 @@
|
||||
"restAsync": "REST Asynchron"
|
||||
},
|
||||
"TYPES_DESCRIPTION": "Webhook, der Aufruf verarbeitet den Statuscode, aber die Antwort ist irrelevant\nCall, der Aufruf verarbeitet den Statuscode und die Antwort\nAsync, der Aufruf verarbeitet weder Statuscode noch Antwort, kann aber parallel zu anderen Zielen aufgerufen werden",
|
||||
"PAYLOAD_TYPE": "Payload-Typ",
|
||||
"PAYLOAD_TYPES": {
|
||||
"UNSPECIFIED": "Nicht angegeben",
|
||||
"JSON": "JSON",
|
||||
"JWT": "JSON Web Token (JWT)",
|
||||
"JWE": "Verschlüsseltes JSON Web Token (JWE)"
|
||||
},
|
||||
"PAYLOAD_TYPE_DESCRIPTION": "JSON: Standard-JSON-Payload mit einem Signatur-Header zur Verifikation.\nJWT: Der Payload wird als signiertes JWT gesendet. Die Verifikation erfolgt über die JWT-Signatur.\nJWE: Das JWT wird zusätzlich verschlüsselt, um unautorisierten Zugriff auf die Payloadinformationen zu verhindern.",
|
||||
"ENDPOINT": "Endpunkt",
|
||||
"ENDPOINT_DESCRIPTION": "Geben Sie den Endpunkt ein, an dem Ihr Code gehostet wird. Stellen Sie sicher, dass er für uns zugänglich ist!",
|
||||
"TIMEOUT": "Timeout",
|
||||
|
||||
@@ -624,6 +624,14 @@
|
||||
"restAsync": "REST Async"
|
||||
},
|
||||
"TYPES_DESCRIPTION": "Webhook, the call handles the status code but response is irrelevant\nCall, the call handles the status code and response\nAsync, the call handles neither status code nor response, but can be called in parallel with other Targets",
|
||||
"PAYLOAD_TYPE": "Payload Type",
|
||||
"PAYLOAD_TYPES": {
|
||||
"UNSPECIFIED": "Unspecified",
|
||||
"JSON": "JSON",
|
||||
"JWT": "JSON Web Token (JWT)",
|
||||
"JWE": "Encrypted JSON Web Token (JWE)"
|
||||
},
|
||||
"PAYLOAD_TYPE_DESCRIPTION": "JSON: Standard JSON payload with an signature header for verification.\nJWT: Payload is sent as signed JWT. Verification is self-contained using the JWT signature.\nJWE: The JWT is additionally encrypted to prevent unauthorized access to payload information.",
|
||||
"ENDPOINT": "Endpoint",
|
||||
"ENDPOINT_DESCRIPTION": "Enter the endpoint where your code is hosted. Make sure it is accessible to us!",
|
||||
"TIMEOUT": "Timeout",
|
||||
|
||||
@@ -623,6 +623,14 @@
|
||||
"restAsync": "REST Asíncrono"
|
||||
},
|
||||
"TYPES_DESCRIPTION": "Webhook, la llamada maneja el código de estado pero la respuesta es irrelevante\nCall, la llamada maneja el código de estado y la respuesta\nAsync, la llamada no maneja ni el código de estado ni la respuesta, pero puede ser llamada en paralelo con otros objetivos",
|
||||
"PAYLOAD_TYPE": "Tipo de carga útil",
|
||||
"PAYLOAD_TYPES": {
|
||||
"UNSPECIFIED": "No especificado",
|
||||
"JSON": "JSON",
|
||||
"JWT": "JSON Web Token (JWT)",
|
||||
"JWE": "JSON Web Token cifrado (JWE)"
|
||||
},
|
||||
"PAYLOAD_TYPE_DESCRIPTION": "JSON: Carga útil JSON estándar con un encabezado de firma para verificación.\nJWT: La carga útil se envía como un JWT firmado. La verificación es autónoma mediante la firma del JWT.\nJWE: El JWT se cifra adicionalmente para evitar el acceso no autorizado a la información de la carga útil.",
|
||||
"ENDPOINT": "Punto de conexión",
|
||||
"ENDPOINT_DESCRIPTION": "Introduce el punto de conexión donde se aloja tu código. ¡Asegúrate de que sea accesible para nosotros!",
|
||||
"TIMEOUT": "Tiempo de espera",
|
||||
|
||||
@@ -623,6 +623,14 @@
|
||||
"restAsync": "REST Asynchrone"
|
||||
},
|
||||
"TYPES_DESCRIPTION": "Webhook, l'appel gère le code d'état mais la réponse est sans importance\nCall, l'appel gère le code d'état et la réponse\nAsync, l'appel ne gère ni le code d'état ni la réponse, mais peut être appelé en parallèle avec d'autres cibles",
|
||||
"PAYLOAD_TYPE": "Type de charge utile",
|
||||
"PAYLOAD_TYPES": {
|
||||
"UNSPECIFIED": "Non spécifié",
|
||||
"JSON": "JSON",
|
||||
"JWT": "JSON Web Token (JWT)",
|
||||
"JWE": "JSON Web Token chiffré (JWE)"
|
||||
},
|
||||
"PAYLOAD_TYPE_DESCRIPTION": "JSON: Charge utile JSON standard avec un en‑tête de signature pour la vérification.\nJWT: La charge utile est envoyée sous forme de JWT signé. La vérification se fait via la signature du JWT.\nJWE: Le JWT est en plus chiffré pour empêcher l'accès non autorisé aux informations de la charge utile.",
|
||||
"ENDPOINT": "Point de terminaison",
|
||||
"ENDPOINT_DESCRIPTION": "Entrez le point de terminaison où votre code est hébergé. Assurez-vous qu'il nous est accessible !",
|
||||
"TIMEOUT": "Délai d'attente",
|
||||
|
||||
@@ -623,6 +623,14 @@
|
||||
"restAsync": "REST Aszinkron"
|
||||
},
|
||||
"TYPES_DESCRIPTION": "Webhook, a hívás kezeli az állapotkódot, de a válasz lényegtelen\nCall, a hívás kezeli az állapotkódot és a választ\nAsync, a hívás sem az állapotkódot, sem a választ nem kezeli, de párhuzamosan hívható más célokkal",
|
||||
"PAYLOAD_TYPE": "Tartalom típusa",
|
||||
"PAYLOAD_TYPES": {
|
||||
"UNSPECIFIED": "Nincs megadva",
|
||||
"JSON": "JSON",
|
||||
"JWT": "JSON Web Token (JWT)",
|
||||
"JWE": "Titkosított JSON Web Token (JWE)"
|
||||
},
|
||||
"PAYLOAD_TYPE_DESCRIPTION": "JSON: Szabványos JSON üzenet aláírási fejlécvel az ellenőrzéshez.\nJWT: Az üzenet aláírt JWT-ként kerül elküldésre. Az ellenőrzés a JWT aláírásával történik.\nJWE: A JWT további titkosítást kap, hogy megakadályozza az illetéktelen hozzáférést az üzenet adataihoz.",
|
||||
"ENDPOINT": "Végpont",
|
||||
"ENDPOINT_DESCRIPTION": "Adja meg azt a végpontot, ahol a kódja található. Győződjön meg arról, hogy elérhető számunkra!",
|
||||
"TIMEOUT": "Időtúllépés",
|
||||
|
||||
@@ -623,6 +623,14 @@
|
||||
"restAsync": "REST Asinkron"
|
||||
},
|
||||
"TYPES_DESCRIPTION": "Webhook, panggilan menangani kode status tetapi respons tidak relevan\nCall, panggilan menangani kode status dan respons\nAsync, panggilan tidak menangani kode status maupun respons, tetapi dapat dipanggil secara paralel dengan Target lain",
|
||||
"PAYLOAD_TYPE": "Tipe payload",
|
||||
"PAYLOAD_TYPES": {
|
||||
"UNSPECIFIED": "Tidak ditentukan",
|
||||
"JSON": "JSON",
|
||||
"JWT": "JSON Web Token (JWT)",
|
||||
"JWE": "JSON Web Token terenkripsi (JWE)"
|
||||
},
|
||||
"PAYLOAD_TYPE_DESCRIPTION": "JSON: Payload JSON standar dengan header tanda tangan untuk verifikasi.\nJWT: Payload dikirim sebagai JWT yang ditandatangani. Verifikasi dilakukan melalui tanda tangan JWT.\nJWE: JWT dienkripsi tambahan untuk mencegah akses tidak sah ke informasi payload.",
|
||||
"ENDPOINT": "Titik Akhir",
|
||||
"ENDPOINT_DESCRIPTION": "Masukkan titik akhir tempat kode Anda dihosting. Pastikan dapat diakses oleh kami!",
|
||||
"TIMEOUT": "Batas Waktu",
|
||||
|
||||
@@ -622,6 +622,14 @@
|
||||
"restAsync": "REST Asincrono"
|
||||
},
|
||||
"TYPES_DESCRIPTION": "Webhook, la chiamata gestisce il codice di stato ma la risposta è irrilevante\nCall, la chiamata gestisce il codice di stato e la risposta\nAsync, la chiamata non gestisce né il codice di stato né la risposta, ma può essere eseguita in parallelo con altri obiettivi",
|
||||
"PAYLOAD_TYPE": "Tipo di payload",
|
||||
"PAYLOAD_TYPES": {
|
||||
"UNSPECIFIED": "Non specificato",
|
||||
"JSON": "JSON",
|
||||
"JWT": "JSON Web Token (JWT)",
|
||||
"JWE": "JSON Web Token crittografato (JWE)"
|
||||
},
|
||||
"PAYLOAD_TYPE_DESCRIPTION": "JSON: Payload JSON standard con un'intestazione di firma per la verifica.\nJWT: Il payload viene inviato come JWT firmato. La verifica è autonoma utilizzando la firma del JWT.\nJWE: Il JWT è inoltre crittografato per prevenire l'accesso non autorizzato alle informazioni del payload.",
|
||||
"ENDPOINT": "Endpoint",
|
||||
"ENDPOINT_DESCRIPTION": "Inserisci l'endpoint in cui è ospitato il tuo codice. Assicurati che sia accessibile per noi!",
|
||||
"TIMEOUT": "Timeout",
|
||||
|
||||
@@ -623,6 +623,14 @@
|
||||
"restAsync": "REST 非同期"
|
||||
},
|
||||
"TYPES_DESCRIPTION": "Webhook、呼び出しはステータスコードを処理しますが、応答は無関係です\nCall、呼び出しはステータスコードと応答を処理します\nAsync、呼び出しはステータスコードも応答も処理しませんが、他のターゲットと並行して呼び出すことができます",
|
||||
"PAYLOAD_TYPE": "ペイロードの種類",
|
||||
"PAYLOAD_TYPES": {
|
||||
"UNSPECIFIED": "未指定",
|
||||
"JSON": "JSON",
|
||||
"JWT": "JSON Web Token (JWT)",
|
||||
"JWE": "暗号化された JSON Web Token (JWE)"
|
||||
},
|
||||
"PAYLOAD_TYPE_DESCRIPTION": "JSON: 検証用の署名ヘッダーを持つ標準の JSON ペイロード。\nJWT: ペイロードは署名付き JWT として送信されます。検証は JWT の署名で行われます。\nJWE: JWT はさらに暗号化され、ペイロード情報への不正アクセスを防ぎます。",
|
||||
"ENDPOINT": "エンドポイント",
|
||||
"ENDPOINT_DESCRIPTION": "コードがホストされているエンドポイントを入力します。アクセス可能であることを確認してください。",
|
||||
"TIMEOUT": "タイムアウト",
|
||||
|
||||
@@ -623,6 +623,14 @@
|
||||
"restAsync": "REST 비동기"
|
||||
},
|
||||
"TYPES_DESCRIPTION": "Webhook, 호출은 상태 코드를 처리하지만 응답은 중요하지 않습니다\nCall, 호출은 상태 코드와 응답을 처리합니다\nAsync, 호출은 상태 코드나 응답을 처리하지 않지만 다른 대상과 병렬로 호출할 수 있습니다",
|
||||
"PAYLOAD_TYPE": "페이로드 유형",
|
||||
"PAYLOAD_TYPES": {
|
||||
"UNSPECIFIED": "지정되지 않음",
|
||||
"JSON": "JSON",
|
||||
"JWT": "JSON Web Token (JWT)",
|
||||
"JWE": "암호화된 JSON Web Token (JWE)"
|
||||
},
|
||||
"PAYLOAD_TYPE_DESCRIPTION": "JSON: 검증을 위한 서명 헤더가 포함된 표준 JSON 페이로드.\nJWT: 페이로드는 서명된 JWT로 전송됩니다. 검증은 JWT 서명을 통해 수행됩니다.\nJWE: JWT는 추가로 암호화되어 페이로드 정보에 대한 무단 접근을 방지합니다.",
|
||||
"ENDPOINT": "엔드포인트",
|
||||
"ENDPOINT_DESCRIPTION": "코드가 호스팅되는 엔드포인트를 입력하십시오. 우리에게 액세스할 수 있는지 확인하십시오!",
|
||||
"TIMEOUT": "시간 초과",
|
||||
|
||||
@@ -623,6 +623,14 @@
|
||||
"restAsync": "REST Асинхроно"
|
||||
},
|
||||
"TYPES_DESCRIPTION": "Webhook, повикот го обработува статусниот код но одговорот е ирелевантен\nCall, повикот го обработува статусниот код и одговорот\nAsync, повикот не го обработува ниту статусниот код ниту одговорот, но може да се повика паралелно со други цели",
|
||||
"PAYLOAD_TYPE": "Тип на содржина",
|
||||
"PAYLOAD_TYPES": {
|
||||
"UNSPECIFIED": "Не е наведено",
|
||||
"JSON": "JSON",
|
||||
"JWT": "JSON Web Token (JWT)",
|
||||
"JWE": "Шифриран JSON Web Token (JWE)"
|
||||
},
|
||||
"PAYLOAD_TYPE_DESCRIPTION": "JSON: Стандардна JSON содржина со заглавие за потпис и верификација.\nJWT: Содржината се праќа како потпишан JWT. Верификацијата се врши преку потписот на JWT.\nJWE: JWT-от е дополнително шифриран за да се спречи неовластен пристап до информациите во содржината.",
|
||||
"ENDPOINT": "Крајна точка",
|
||||
"ENDPOINT_DESCRIPTION": "Внесете ја крајната точка каде што е хостиран вашиот код. Осигурете се дека е достапна за нас!",
|
||||
"TIMEOUT": "Време на истекување",
|
||||
|
||||
@@ -623,6 +623,14 @@
|
||||
"restAsync": "REST Asynchroon"
|
||||
},
|
||||
"TYPES_DESCRIPTION": "Webhook, de oproep verwerkt de statuscode maar de reactie is irrelevant\nCall, de oproep verwerkt de statuscode en de reactie\nAsync, de oproep verwerkt noch de statuscode noch de reactie, maar kan parallel aan andere doelen worden aangeroepen",
|
||||
"PAYLOAD_TYPE": "Payload-type",
|
||||
"PAYLOAD_TYPES": {
|
||||
"UNSPECIFIED": "Niet opgegeven",
|
||||
"JSON": "JSON",
|
||||
"JWT": "JSON Web Token (JWT)",
|
||||
"JWE": "Versleuteld JSON Web Token (JWE)"
|
||||
},
|
||||
"PAYLOAD_TYPE_DESCRIPTION": "JSON: Standaard JSON-payload met een handtekening-header voor verificatie.\nJWT: De payload wordt als een ondertekend JWT verzonden. Verificatie gebeurt via de JWT-handtekening.\nJWE: Het JWT wordt daarnaast versleuteld om ongeautoriseerde toegang tot payloadinformatie te voorkomen.",
|
||||
"ENDPOINT": "Eindpunt",
|
||||
"ENDPOINT_DESCRIPTION": "Voer het eindpunt in waar uw code wordt gehost. Zorg ervoor dat het voor ons toegankelijk is!",
|
||||
"TIMEOUT": "Time-out",
|
||||
|
||||
@@ -622,6 +622,14 @@
|
||||
"restAsync": "REST Asynchroniczny"
|
||||
},
|
||||
"TYPES_DESCRIPTION": "Webhook, wywołanie obsługuje kod stanu, ale odpowiedź jest nieistotna\nCall, wywołanie obsługuje kod stanu i odpowiedź\nAsync, wywołanie nie obsługuje ani kodu stanu, ani odpowiedzi, ale może być wywoływane równolegle z innymi celami",
|
||||
"PAYLOAD_TYPE": "Typ ładunku",
|
||||
"PAYLOAD_TYPES": {
|
||||
"UNSPECIFIED": "Nieokreślony",
|
||||
"JSON": "JSON",
|
||||
"JWT": "JSON Web Token (JWT)",
|
||||
"JWE": "Zaszyfrowany JSON Web Token (JWE)"
|
||||
},
|
||||
"PAYLOAD_TYPE_DESCRIPTION": "JSON: Standardowy ładunek JSON z nagłówkiem podpisu do weryfikacji.\nJWT: Ładunek jest wysyłany jako podpisany JWT. Weryfikacja odbywa się za pomocą podpisu JWT.\nJWE: JWT jest dodatkowo szyfrowany, aby zapobiec nieautoryzowanemu dostępowi do informacji w ładunku.",
|
||||
"ENDPOINT": "Punkt końcowy",
|
||||
"ENDPOINT_DESCRIPTION": "Wprowadź punkt końcowy, w którym hostowany jest Twój kod. Upewnij się, że jest dla nas dostępny!",
|
||||
"TIMEOUT": "Limit czasu",
|
||||
|
||||
@@ -623,6 +623,14 @@
|
||||
"restAsync": "REST Assíncrono"
|
||||
},
|
||||
"TYPES_DESCRIPTION": "Webhook, a chamada lida com o código de status, mas a resposta é irrelevante\nCall, a chamada lida com o código de status e a resposta\nAsync, a chamada não lida nem com o código de status nem com a resposta, mas pode ser chamada em paralelo com outros alvos",
|
||||
"PAYLOAD_TYPE": "Tipo de carga útil",
|
||||
"PAYLOAD_TYPES": {
|
||||
"UNSPECIFIED": "Não especificado",
|
||||
"JSON": "JSON",
|
||||
"JWT": "JSON Web Token (JWT)",
|
||||
"JWE": "JSON Web Token cifrado (JWE)"
|
||||
},
|
||||
"PAYLOAD_TYPE_DESCRIPTION": "JSON: Payload JSON padrão com um cabeçalho de assinatura para verificação.\nJWT: O payload é enviado como um JWT assinado. A verificação é feita pela assinatura do JWT.\nJWE: O JWT é adicionalmente cifrado para evitar acesso não autorizado às informações do payload.",
|
||||
"ENDPOINT": "Ponto de Extremidade",
|
||||
"ENDPOINT_DESCRIPTION": "Insira o ponto de extremidade onde seu código está hospedado. Certifique-se de que ele esteja acessível para nós!",
|
||||
"TIMEOUT": "Tempo Limite",
|
||||
|
||||
@@ -623,6 +623,14 @@
|
||||
"restAsync": "REST Asincron"
|
||||
},
|
||||
"TYPES_DESCRIPTION": "Webhook, apelul gestionează codul de stare, dar răspunsul este irelevant\nCall, apelul gestionează codul de stare și răspunsul\nAsync, apelul nu gestionează nici codul de stare, nici răspunsul, dar poate fi apelat în paralel cu alte Ținte",
|
||||
"PAYLOAD_TYPE": "Tipul payload-ului",
|
||||
"PAYLOAD_TYPES": {
|
||||
"UNSPECIFIED": "Nespecificat",
|
||||
"JSON": "JSON",
|
||||
"JWT": "JSON Web Token (JWT)",
|
||||
"JWE": "JSON Web Token criptat (JWE)"
|
||||
},
|
||||
"PAYLOAD_TYPE_DESCRIPTION": "JSON: Payload JSON standard cu un antet de semnătură pentru verificare.\nJWT: Payload-ul este trimis ca un JWT semnat. Verificarea se realizează prin semnătura JWT.\nJWE: JWT-ul este în plus criptat pentru a preveni accesul neautorizat la informațiile din payload.",
|
||||
"ENDPOINT": "Punct Final",
|
||||
"ENDPOINT_DESCRIPTION": "Introduceți punctul final unde este găzduit codul dvs. Asigurați-vă că este accesibil pentru noi!",
|
||||
"TIMEOUT": "Timeout",
|
||||
|
||||
@@ -622,6 +622,14 @@
|
||||
"restAsync": "REST Асинхронный"
|
||||
},
|
||||
"TYPES_DESCRIPTION": "Webhook, вызов обрабатывает код состояния, но ответ не имеет значения\nCall, вызов обрабатывает код состояния и ответ\nAsync, вызов не обрабатывает ни код состояния, ни ответ, но может выполняться параллельно с другими целями",
|
||||
"PAYLOAD_TYPE": "Тип полезной нагрузки",
|
||||
"PAYLOAD_TYPES": {
|
||||
"UNSPECIFIED": "Не указано",
|
||||
"JSON": "JSON",
|
||||
"JWT": "JSON Web Token (JWT)",
|
||||
"JWE": "Зашифрованный JSON Web Token (JWE)"
|
||||
},
|
||||
"PAYLOAD_TYPE_DESCRIPTION": "JSON: Стандартная JSON‑полезная нагрузка с заголовком подписи для проверки.\nJWT: Полезная нагрузка отправляется как подписанный JWT. Проверка выполняется с помощью подписи JWT.\nJWE: JWT дополнительно шифруется, чтобы предотвратить несанкционированный доступ к информации в полезной нагрузке.",
|
||||
"ENDPOINT": "Конечная точка",
|
||||
"ENDPOINT_DESCRIPTION": "Введите конечную точку, где размещен ваш код. Убедитесь, что он доступен для нас!",
|
||||
"TIMEOUT": "Тайм-аут",
|
||||
|
||||
@@ -623,6 +623,14 @@
|
||||
"restAsync": "REST Asynkron"
|
||||
},
|
||||
"TYPES_DESCRIPTION": "Webhook, anropet hanterar statuskoden men svaret är irrelevant\nCall, anropet hanterar statuskoden och svaret\nAsync, anropet hanterar varken statuskod eller svar men kan anropas parallellt med andra mål",
|
||||
"PAYLOAD_TYPE": "Payload-typ",
|
||||
"PAYLOAD_TYPES": {
|
||||
"UNSPECIFIED": "Ej angiven",
|
||||
"JSON": "JSON",
|
||||
"JWT": "JSON Web Token (JWT)",
|
||||
"JWE": "Krypterad JSON Web Token (JWE)"
|
||||
},
|
||||
"PAYLOAD_TYPE_DESCRIPTION": "JSON: Standard JSON-payload med en signatur-header för verifiering.\nJWT: Payload skickas som en signerad JWT. Verifiering sker via JWT-signaturen.\nJWE: JWT är dessutom krypterad för att förhindra obehörig åtkomst till payload-information.",
|
||||
"ENDPOINT": "Slutpunkt",
|
||||
"ENDPOINT_DESCRIPTION": "Ange slutpunkten där din kod finns. Se till att den är tillgänglig för oss!",
|
||||
"TIMEOUT": "Tidsgräns",
|
||||
|
||||
@@ -623,6 +623,14 @@
|
||||
"restAsync": "REST Async"
|
||||
},
|
||||
"TYPES_DESCRIPTION": "Webhook, çağrı durum kodunu işler ancak yanıt önemsizdir\nCall, çağrı durum kodunu ve yanıtını işler\nAsync, çağrı ne durum kodunu ne de yanıtını işler, ancak diğer Hedeflerle paralel olarak çağrılabilir",
|
||||
"PAYLOAD_TYPE": "Yük türü",
|
||||
"PAYLOAD_TYPES": {
|
||||
"UNSPECIFIED": "Belirtilmemiş",
|
||||
"JSON": "JSON",
|
||||
"JWT": "JSON Web Token (JWT)",
|
||||
"JWE": "Şifrelenmiş JSON Web Token (JWE)"
|
||||
},
|
||||
"PAYLOAD_TYPE_DESCRIPTION": "JSON: Doğrulama için imza başlığı olan standart JSON payload.\nJWT: Payload imzalı bir JWT olarak gönderilir. Doğrulama, JWT imzası ile gerçekleştirilir.\nJWE: JWT, payload bilgilerine yetkisiz erişimi önlemek için ayrıca şifrelenir.",
|
||||
"ENDPOINT": "Endpoint",
|
||||
"ENDPOINT_DESCRIPTION": "Kodunuzun barındırıldığı endpoint'i girin. Bize erişilebilir olduğundan emin olun!",
|
||||
"TIMEOUT": "Zaman Aşımı",
|
||||
|
||||
@@ -624,6 +624,14 @@
|
||||
"restAsync": "REST Асинхронний"
|
||||
},
|
||||
"TYPES_DESCRIPTION": "Webhook, виклик обробляє код стану, але відповідь не має значення\nВиклик, виклик обробляє код стану та відповідь\nАсинхронний, виклик не обробляє ні код стану, ні відповідь, але може викликатися паралельно з іншими цілями",
|
||||
"PAYLOAD_TYPE": "Тип корисного навантаження",
|
||||
"PAYLOAD_TYPES": {
|
||||
"UNSPECIFIED": "Не вказано",
|
||||
"JSON": "JSON",
|
||||
"JWT": "JSON Web Token (JWT)",
|
||||
"JWE": "Зашифрований JSON Web Token (JWE)"
|
||||
},
|
||||
"PAYLOAD_TYPE_DESCRIPTION": "JSON: Стандартний JSON payload з заголовком підпису для перевірки.\nJWT: Payload надсилається як підписаний JWT. Перевірка відбувається за допомогою підпису JWT.\nJWE: JWT додатково шифрується, щоб запобігти несанкціонованому доступу до інформації у payload.",
|
||||
"ENDPOINT": "Кінцева точка",
|
||||
"ENDPOINT_DESCRIPTION": "Введіть кінцеву точку, де розміщений ваш код. Переконайтеся, що вона доступна для нас!",
|
||||
"TIMEOUT": "Тайм-аут",
|
||||
|
||||
@@ -623,6 +623,14 @@
|
||||
"restAsync": "REST 异步"
|
||||
},
|
||||
"TYPES_DESCRIPTION": "Webhook,调用处理状态码但响应无关紧要\nCall,调用处理状态码和响应\nAsync,调用既不处理状态码也不处理响应,但可以与其他目标并行调用",
|
||||
"PAYLOAD_TYPE": "有效负载类型",
|
||||
"PAYLOAD_TYPES": {
|
||||
"UNSPECIFIED": "未指定",
|
||||
"JSON": "JSON",
|
||||
"JWT": "JSON Web Token (JWT)",
|
||||
"JWE": "加密的 JSON Web Token (JWE)"
|
||||
},
|
||||
"PAYLOAD_TYPE_DESCRIPTION": "JSON: 用于验证的签名头的标准 JSON 有效负载。\nJWT: 有效负载作为已签名的 JWT 发送。验证通过 JWT 签名完成。\nJWE: JWT 另外被加密,以防止对有效负载信息的未授权访问。",
|
||||
"ENDPOINT": "端点",
|
||||
"ENDPOINT_DESCRIPTION": "输入您的代码托管的端点。确保我们可以访问它!",
|
||||
"TIMEOUT": "超时",
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/zitadel/zitadel-go/v3/pkg/actions"
|
||||
)
|
||||
|
||||
const signingKey = "somekey" // signing key received after creating the target
|
||||
|
||||
// webhook HandleFunc to read the request body and then print out the contents
|
||||
func webhook(w http.ResponseWriter, req *http.Request) {
|
||||
// read the body content
|
||||
sentBody, err := io.ReadAll(req.Body)
|
||||
if err != nil {
|
||||
// if there was an error while reading the body return an error
|
||||
http.Error(w, "error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer req.Body.Close()
|
||||
// validate signature
|
||||
if err := actions.ValidateRequestPayload(sentBody, &req.Header, signingKey); err != nil {
|
||||
// if the signed content is not equal the sent content return an error
|
||||
http.Error(w, "error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
// print out the read content
|
||||
fmt.Println(string(sentBody))
|
||||
}
|
||||
|
||||
func main() {
|
||||
// handle the HTTP call under "/webhook"
|
||||
http.HandleFunc("/webhook", webhook)
|
||||
|
||||
// start an HTTP server with the before defined function to handle the endpoint under "http://localhost:8090"
|
||||
http.ListenAndServe(":8090", nil)
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/go-jose/go-jose/v4"
|
||||
"github.com/zitadel/oidc/v3/pkg/client"
|
||||
"github.com/zitadel/oidc/v3/pkg/client/rp"
|
||||
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||
)
|
||||
|
||||
// webhook HandleFunc to read the request body and then print out the contents
|
||||
func webhook(signatureAlgorithms []jose.SignatureAlgorithm, keySet oidc.KeySet, privateKey any) func(w http.ResponseWriter, req *http.Request) {
|
||||
return func(w http.ResponseWriter, req *http.Request) {
|
||||
// read the body content
|
||||
sentBody, err := io.ReadAll(req.Body)
|
||||
if err != nil {
|
||||
// if there was an error while reading the body return an error
|
||||
http.Error(w, "error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer req.Body.Close()
|
||||
// decrypt the JWE, validate the JWT and extract the payload from it
|
||||
payload, err := validateJWE(req.Context(), string(sentBody), signatureAlgorithms, keySet, privateKey)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
// print out the payload
|
||||
fmt.Println(string(payload))
|
||||
}
|
||||
}
|
||||
|
||||
func validateJWE(ctx context.Context, jweString string, signatureAlgorithms []jose.SignatureAlgorithm, keySet oidc.KeySet, privateKey any) ([]byte, error) {
|
||||
// For parsing the JWE, we need to specify which algorithms we expect.
|
||||
// You can specify either `RSA_OAEP_256` (rsa.PrivateKey) or `ECDH_ES_A256KW` (ecdsa.PrivateKey) or both, depending on which private key(s) you have.
|
||||
// The content encryption algorithm will always be `A256GCM` as used by Zitadel.
|
||||
parsedJWE, err := jose.ParseEncrypted(jweString, []jose.KeyAlgorithm{jose.RSA_OAEP_256, jose.ECDH_ES_A256KW}, []jose.ContentEncryption{jose.A256GCM})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// In this example we only use a single key and loaded it on start, but you can manage and rotate the key used for the encryption through the API.
|
||||
// You might also have a key management system in your application.
|
||||
// Using the KeyID from the JWE header (parsedJWE.Header.KeyID), we could load the correct private key for decryption.
|
||||
|
||||
// Decrypt the JWE using the private key to get the inner JWT
|
||||
decryptedJWT, err := parsedJWE.Decrypt(privateKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Now validate the inner JWT and return the payload
|
||||
return validateJWT(ctx, string(decryptedJWT), signatureAlgorithms, keySet)
|
||||
}
|
||||
|
||||
func validateJWT(ctx context.Context, jwtString string, algorithms []jose.SignatureAlgorithm, keySet oidc.KeySet) ([]byte, error) {
|
||||
// Parse the signed JWT to get the JWS object (which contains the signatures and unverified payload)
|
||||
parsedJWS, err := jose.ParseSigned(jwtString, algorithms)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Verify the signature using the retrieved key and return the payload
|
||||
return keySet.VerifySignature(ctx, parsedJWS)
|
||||
}
|
||||
|
||||
func main() {
|
||||
ctx := context.Background()
|
||||
// set the issuer to the ZITADEL instance URL
|
||||
issuer := "http://localhost:8080"
|
||||
|
||||
// the oidc client library will call the discovery endpoint to get the JWKS URI and supported signing algorithms
|
||||
discover, err := client.Discover(ctx, issuer, http.DefaultClient)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
signatureAlgorithms := make([]jose.SignatureAlgorithm, len(discover.IDTokenSigningAlgValuesSupported))
|
||||
for i, alg := range discover.IDTokenSigningAlgValuesSupported {
|
||||
signatureAlgorithms[i] = jose.SignatureAlgorithm(alg)
|
||||
}
|
||||
keySet := rp.NewRemoteKeySet(http.DefaultClient, discover.JwksURI)
|
||||
|
||||
// this example uses the private key from the PEM file to decrypt the JWE
|
||||
// make sure to load your private key accordingly
|
||||
privateKeyPEM, err := os.ReadFile("./private-key.pem")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
block, _ := pem.Decode(privateKeyPEM)
|
||||
privateKey, err := x509.ParsePKCS8PrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// handle the HTTP call under "/webhook"
|
||||
http.HandleFunc("/webhook", webhook(signatureAlgorithms, keySet, privateKey))
|
||||
|
||||
// start an HTTP server with the before defined function to handle the endpoint under "http://localhost:8090"
|
||||
http.ListenAndServe(":8090", nil)
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-jose/go-jose/v4"
|
||||
"github.com/zitadel/oidc/v3/pkg/client"
|
||||
"github.com/zitadel/oidc/v3/pkg/client/rp"
|
||||
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||
)
|
||||
|
||||
// webhook HandleFunc to read the request body and then print out the contents
|
||||
func webhook(signatureAlgorithms []jose.SignatureAlgorithm, keySet oidc.KeySet) func(w http.ResponseWriter, req *http.Request) {
|
||||
return func(w http.ResponseWriter, req *http.Request) {
|
||||
// read the body content
|
||||
sentBody, err := io.ReadAll(req.Body)
|
||||
if err != nil {
|
||||
// if there was an error while reading the body return an error
|
||||
http.Error(w, "error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer req.Body.Close()
|
||||
// validate the JWT and extract the payload from it
|
||||
payload, err := validateJWT(req.Context(), string(sentBody), signatureAlgorithms, keySet)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
// print out the payload
|
||||
fmt.Println(string(payload))
|
||||
}
|
||||
}
|
||||
|
||||
func validateJWT(ctx context.Context, jwtString string, algorithms []jose.SignatureAlgorithm, keySet oidc.KeySet) ([]byte, error) {
|
||||
// Parse the signed JWT to get the JWS object (which contains the signatures and unverified payload)
|
||||
parsedJWS, err := jose.ParseSigned(jwtString, algorithms)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Verify the signature using the retrieved key and return the payload
|
||||
return keySet.VerifySignature(ctx, parsedJWS)
|
||||
}
|
||||
|
||||
func main() {
|
||||
ctx := context.Background()
|
||||
// set the issuer to the ZITADEL instance URL
|
||||
issuer := "http://localhost:8080"
|
||||
|
||||
// the oidc client library will call the discovery endpoint to get the JWKS URI and supported signing algorithms
|
||||
discover, err := client.Discover(ctx, issuer, http.DefaultClient)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
signatureAlgorithms := make([]jose.SignatureAlgorithm, len(discover.IDTokenSigningAlgValuesSupported))
|
||||
for i, alg := range discover.IDTokenSigningAlgValuesSupported {
|
||||
signatureAlgorithms[i] = jose.SignatureAlgorithm(alg)
|
||||
}
|
||||
keySet := rp.NewRemoteKeySet(http.DefaultClient, discover.JwksURI)
|
||||
|
||||
// handle the HTTP call under "/webhook"
|
||||
http.HandleFunc("/webhook", webhook(signatureAlgorithms, keySet))
|
||||
|
||||
// start an HTTP server with the before defined function to handle the endpoint under "http://localhost:8090"
|
||||
http.ListenAndServe(":8090", nil)
|
||||
}
|
||||
@@ -1,224 +0,0 @@
|
||||
---
|
||||
title: Test Actions Request Signature Check
|
||||
---
|
||||
|
||||
This guide shows you how to leverage the ZITADEL actions feature to react to API requests in your ZITADEL instance.
|
||||
You can use the actions feature to create a target that will be called when a specific API request occurs.
|
||||
This is useful for information provisioning in between systems or for triggering workflows based on API requests in ZITADEL.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before you start, make sure you have everything set up correctly.
|
||||
|
||||
- You need to be at least a ZITADEL [_IAM_OWNER_](/guides/manage/console/managers)
|
||||
- Your ZITADEL instance needs to have the actions feature enabled.
|
||||
|
||||
:::info
|
||||
Note that this guide assumes that ZITADEL is running on the same machine as the target and can be reached via `localhost`.
|
||||
In case you are using a different setup, you need to adjust the target URL accordingly and will need to make sure that the target is reachable from ZITADEL.
|
||||
:::
|
||||
|
||||
:::warning
|
||||
To marshal and unmarshal the request please use a package like [protojson](https://pkg.go.dev/google.golang.org/protobuf/encoding/protojson),
|
||||
as the request is a protocol buffer message, to avoid potential problems with the attribute names.
|
||||
:::
|
||||
|
||||
## Start example target
|
||||
|
||||
To test the actions feature, you need to create a target that will be called when an API endpoint is called.
|
||||
You will need to implement a listener that can receive HTTP requests, check the signature and process the request.
|
||||
For this example, we will use a simple Go HTTP server that will print the received request to standard output.
|
||||
The 'signingKey' is the key received in the next step 'Create target'.
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/zitadel/zitadel-go/v3/pkg/actions"
|
||||
)
|
||||
|
||||
const signingKey = "somekey" // signing key received after creating the target
|
||||
|
||||
// webhook HandleFunc to read the request body and then print out the contents
|
||||
func webhook(w http.ResponseWriter, req *http.Request) {
|
||||
// read the body content
|
||||
sentBody, err := io.ReadAll(req.Body)
|
||||
if err != nil {
|
||||
// if there was an error while reading the body return an error
|
||||
http.Error(w, "error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer req.Body.Close()
|
||||
// validate signature
|
||||
if err := actions.ValidateRequestPayload(sentBody, &req.Header, signingKey); err != nil {
|
||||
// if the signed content is not equal the sent content return an error
|
||||
http.Error(w, "error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
// print out the read content
|
||||
fmt.Println(string(sentBody))
|
||||
}
|
||||
|
||||
func main() {
|
||||
// handle the HTTP call under "/webhook"
|
||||
http.HandleFunc("/webhook", webhook)
|
||||
|
||||
// start an HTTP server with the before defined function to handle the endpoint under "http://localhost:8090"
|
||||
http.ListenAndServe(":8090", nil)
|
||||
}
|
||||
```
|
||||
|
||||
:::info
|
||||
The example above runs only on your local machine (`localhost`).
|
||||
To test it with Zitadel, you must make your listener reachable from the internet.
|
||||
You can do this by using **Webhook.site** (see [Creating a Listener with Webhook.site](./webhook-site-setup)).
|
||||
:::
|
||||
|
||||
## Create target
|
||||
|
||||
As you see in the example above the target is created with HTTP and port '8090' and if we want to use it as webhook, the target can be created as follows:
|
||||
|
||||
See [Create a target](/apis/resources/action_service_v2/action-service-create-target) for more detailed information.
|
||||
|
||||
```shell
|
||||
curl -L -X POST 'https://$CUSTOM-DOMAIN/v2/actions/targets' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H 'Accept: application/json' \
|
||||
-H 'Authorization: Bearer <TOKEN>' \
|
||||
--data-raw '{
|
||||
"name": "local webhook",
|
||||
"restWebhook": {
|
||||
"interruptOnError": true
|
||||
},
|
||||
"endpoint": "http://localhost:8090/webhook",
|
||||
"timeout": "10s"
|
||||
}'
|
||||
```
|
||||
|
||||
Example response after creating the target:
|
||||
```json
|
||||
{
|
||||
"id": "344649040681500814",
|
||||
"creationDate": "2025-10-31T15:00:36.432595dZ",
|
||||
"signingKey": "somekey"
|
||||
}
|
||||
```
|
||||
Save the returned ID to set in the execution. Use the `signingKey` to validate the request signature.
|
||||
|
||||
## Set execution
|
||||
|
||||
To configure ZITADEL to call the target when an API endpoint is called, you need to set an execution and define the request
|
||||
condition.
|
||||
|
||||
See [Set an execution](/apis/resources/action_service_v2/action-service-set-execution) for more detailed information.
|
||||
|
||||
```shell
|
||||
curl -L -X PUT 'https://$CUSTOM-DOMAIN/v2/actions/executions' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H 'Accept: application/json' \
|
||||
-H 'Authorization: Bearer <TOKEN>' \
|
||||
--data-raw '{
|
||||
"condition": {
|
||||
"request": {
|
||||
"method": "/zitadel.user.v2.UserService/CreateUser"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
"<TargetID returned>"
|
||||
]
|
||||
}'
|
||||
```
|
||||
|
||||
## Example call
|
||||
|
||||
Now that you have set up the target and execution, you can test it by creating a user through the Console UI or
|
||||
by calling the ZITADEL API to create a human user.
|
||||
|
||||
```shell
|
||||
curl -L -X POST 'https://$CUSTOM-DOMAIN/v2/users/new' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H 'Accept: application/json' \
|
||||
-H 'Authorization: Bearer <TOKEN>' \
|
||||
--data-raw '{
|
||||
"organizationId": "344648897353810062",
|
||||
"human":
|
||||
{
|
||||
"profile":
|
||||
{
|
||||
"givenName": "Minnie",
|
||||
"familyName": "Mouse",
|
||||
"nickName": "Mini",
|
||||
"displayName": "Minnie Mouse",
|
||||
"preferredLanguage": "en",
|
||||
"gender": "GENDER_FEMALE"
|
||||
},
|
||||
"email":
|
||||
{
|
||||
"email": "mini+test@mouse.com"
|
||||
}
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
Your server should now print out something like the following. Check out
|
||||
the [Sent information Request](./usage#sent-information-request) payload description.
|
||||
|
||||
```json
|
||||
{
|
||||
"fullMethod": "/zitadel.user.v2.UserService/CreateUser",
|
||||
"instanceID": "344648897353744526",
|
||||
"orgID": "344648897353810062",
|
||||
"projectID": "344648897353875598",
|
||||
"userID": "344648897354465422",
|
||||
"request":
|
||||
{
|
||||
"organizationId": "344648897353810062",
|
||||
"human":
|
||||
{
|
||||
"profile":
|
||||
{
|
||||
"givenName": "Minnie",
|
||||
"familyName": "Mouse",
|
||||
"nickName": "Mini",
|
||||
"displayName": "Minnie Mouse",
|
||||
"preferredLanguage": "en",
|
||||
"gender": "GENDER_FEMALE"
|
||||
},
|
||||
"email":
|
||||
{
|
||||
"email": "mini+test@mouse.com"
|
||||
}
|
||||
}
|
||||
},
|
||||
"headers":
|
||||
{
|
||||
"Content-Type":
|
||||
[
|
||||
"application/grpc"
|
||||
],
|
||||
"Host":
|
||||
[
|
||||
"localhost:8080"
|
||||
],
|
||||
"X-Forwarded-For":
|
||||
[
|
||||
"::1"
|
||||
],
|
||||
"X-Forwarded-Host":
|
||||
[
|
||||
"localhost:8080"
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Conclusion
|
||||
|
||||
You have successfully set up a target and execution to react to API requests in your ZITADEL instance.
|
||||
This feature can now be used to provision information in between systems or for triggering workflows based on API requests in ZITADEL.
|
||||
Additionally, you are sure that the request was not tempered with, as the signature was created with the combination of signing key and payload.
|
||||
Find more information about the actions feature in the [API documentation](/concepts/features/actions_v2).
|
||||
@@ -0,0 +1,242 @@
|
||||
---
|
||||
title: Verify Payload Integrity
|
||||
---
|
||||
|
||||
import Tabs from "@theme/Tabs";
|
||||
import TabItem from "@theme/TabItem";
|
||||
import CodeBlock from '@theme/CodeBlock';
|
||||
import JSON from "!!raw-loader!./go/json"
|
||||
import JWT from "!!raw-loader!./go/jwt"
|
||||
import JWE from "!!raw-loader!./go/jwe"
|
||||
|
||||
This guide shows you how to verify the integrity of the received data on your target.
|
||||
There are three options available, which will be demonstrated in the following sections.
|
||||
The examples are based on the request action, but the same principles apply to other action types as well.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before you start, make sure you have everything set up correctly.
|
||||
|
||||
- You need to be at least a ZITADEL [_IAM_OWNER_](/guides/manage/console/managers)
|
||||
|
||||
:::info
|
||||
Note that this guide assumes that ZITADEL is running on the same machine as the target and can be reached via `localhost`.
|
||||
In case you are using a different setup, you need to adjust the target URL accordingly and will need to make sure that the target is reachable from ZITADEL.
|
||||
:::
|
||||
|
||||
## Payload and Validation Types
|
||||
|
||||
ZITADEL supports the following three types of payload in actions:
|
||||
- **JSON**: The payload is sent as JSON in the request body. Additionally, a signature header is sent to validate the payload.
|
||||
This is the simplest form of payload and validation and doesn't require any additional calls to validate the payload. It's also the default type,
|
||||
which will be used if no type is specified.
|
||||
- **JWT**: The payload is sent as a JSON Web Token (JWT) in the request body. The JWT is signed with the signing key of the instance.
|
||||
This allows you to validate the payload by verifying the signature of the JWT using the signing key managed though the webkeys endpoint of the ZITADEL instance,
|
||||
which allows for easier key rotation and management. Additionally, it's the base for the JWE type, in case you need to encrypt the payload for certain use cases.
|
||||
- **JWE**: The payload is sent as encrypted JSON Web Token (JWE) in the request body. The JWT is additionally encrypted with the public key provided by you.
|
||||
This allows you to validate and decrypt the payload by verifying the signature of the JWT using the signing key managed though the webkeys endpoint of the ZITADEL instance,
|
||||
and decrypting the payload using your private key. This is the most secure form of payload and validation, but requires additional setup to provide the public key to ZITADEL.
|
||||
This type is recommended if the payload contains sensitive information that should not be exposed to any intermediaries.
|
||||
|
||||
## Create target
|
||||
|
||||
We'll start the endpoint on port '8090' and if we want to use it as webhook, the target can be created as follows:
|
||||
|
||||
See [Create a target](/apis/resources/action_service_v2/action-service-create-target) for more detailed information.
|
||||
|
||||
Specify the `payloadType` according to the implementation you want to test: `PAYLOAD_TYPE_JSON`, `PAYLOAD_TYPE_JWT`, or `PAYLOAD_TYPE_JWE`.
|
||||
|
||||
```shell
|
||||
curl -L -X POST 'https://$CUSTOM-DOMAIN/v2/actions/targets' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H 'Accept: application/json' \
|
||||
-H 'Authorization: Bearer <TOKEN>' \
|
||||
--data-raw '{
|
||||
"name": "local webhook",
|
||||
"restWebhook": {
|
||||
"interruptOnError": true
|
||||
},
|
||||
"endpoint": "http://localhost:8090/webhook",
|
||||
"timeout": "10s",
|
||||
"payloadType": "PAYLOAD_TYPE_JSON"
|
||||
}'
|
||||
```
|
||||
|
||||
Example response after creating the target:
|
||||
```json
|
||||
{
|
||||
"id": "344649040681500814",
|
||||
"creationDate": "2025-10-31T15:00:36.432595dZ",
|
||||
"signingKey": "somekey"
|
||||
}
|
||||
```
|
||||
Save the returned ID to set in the execution. If you're intending to use the `PAYLOAD_TYPE_JSON`, additionally store the `signingKey` and use it in the example above.
|
||||
|
||||
### JWE Specific Setup
|
||||
|
||||
If you are using the `PAYLOAD_TYPE_JWE`, you need to provide a public key to ZITADEL so that it can encrypt the payload.
|
||||
|
||||
Create a public/private key pair. You can use the following command to generate an RSA key pair:
|
||||
```shell
|
||||
openssl genpkey -algorithm RSA -outform PEM -out private_key.pem -pkeyopt rsa_keygen_bits:2048
|
||||
openssl rsa -pubout -in private_key.pem -out public_key.pem
|
||||
```
|
||||
|
||||
Then upload the public key to ZITADEL using the following command. Replace `<TargetID>` with the ID of the target you created earlier.
|
||||
Use the base64 encoded content of the `public_key.pem` file as the value for `publicKey`.
|
||||
```shell
|
||||
curl -L -X POST 'https://$CUSTOM-DOMAIN/v2/actions/targets/<TargetID>/publickeys' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H 'Accept: application/json' \
|
||||
-H 'Authorization: Bearer <TOKEN>' \
|
||||
--data-raw '{
|
||||
"publicKey": "<base64 encoded contents of public_key.pem>"
|
||||
}'
|
||||
```
|
||||
|
||||
Be sure to also activate the public key for the target using the returned `<KeyID>` from the previous request:
|
||||
```shell
|
||||
curl -L -X PUT 'https://$CUSTOM-DOMAIN/v2/actions/targets/<TargetID>/publickeys/<KeyID>/activate' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H 'Accept: application/json' \
|
||||
-H 'Authorization: Bearer <TOKEN>'
|
||||
```
|
||||
|
||||
## Set execution
|
||||
|
||||
To configure ZITADEL to call the target when an API endpoint is called, you need to set an execution and define the request
|
||||
condition.
|
||||
|
||||
See [Set an execution](/apis/resources/action_service_v2/action-service-set-execution) for more detailed information.
|
||||
|
||||
```shell
|
||||
curl -L -X PUT 'https://$CUSTOM-DOMAIN/v2/actions/executions' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H 'Accept: application/json' \
|
||||
-H 'Authorization: Bearer <TOKEN>' \
|
||||
--data-raw '{
|
||||
"condition": {
|
||||
"request": {
|
||||
"method": "/zitadel.user.v2.UserService/CreateUser"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
"<TargetID returned>"
|
||||
]
|
||||
}'
|
||||
```
|
||||
|
||||
## Start example target
|
||||
|
||||
To test the actions feature, you need to create a target that will be called when an API endpoint is called.
|
||||
You will need to implement a listener that can receive HTTP requests.
|
||||
For this example, we will use a simple Go HTTP server that will print the received request to standard output.
|
||||
As mentioned before, this validation can and should be applied to any target implementation.
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="JSON">
|
||||
<CodeBlock language="go">{JSON}</CodeBlock>
|
||||
</TabItem>
|
||||
<TabItem value="JWT">
|
||||
<CodeBlock language="go">{JWT}</CodeBlock>
|
||||
</TabItem>
|
||||
<TabItem value="JWE">
|
||||
<CodeBlock language="go">{JWE}</CodeBlock>
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
:::info
|
||||
The example above runs only on your local machine (`localhost`).
|
||||
To test it with Zitadel, you must make your listener reachable from the internet.
|
||||
You can do this by using **Webhook.site** (see [Creating a Listener with Webhook.site](./webhook-site-setup)).
|
||||
:::
|
||||
|
||||
## Example call
|
||||
|
||||
Now that you have set up the target and execution, you can test it by creating a user through the Console UI or
|
||||
by calling the ZITADEL API to create a human user.
|
||||
|
||||
```shell
|
||||
curl -L -X POST 'https://$CUSTOM-DOMAIN/v2/users/new' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H 'Accept: application/json' \
|
||||
-H 'Authorization: Bearer <TOKEN>' \
|
||||
--data-raw '{
|
||||
"organizationId": "344648897353810062",
|
||||
"human":
|
||||
{
|
||||
"profile":
|
||||
{
|
||||
"givenName": "Minnie",
|
||||
"familyName": "Mouse",
|
||||
"nickName": "Mini",
|
||||
"displayName": "Minnie Mouse",
|
||||
"preferredLanguage": "en",
|
||||
"gender": "GENDER_FEMALE"
|
||||
},
|
||||
"email":
|
||||
{
|
||||
"email": "[email protected]"
|
||||
}
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
Your server should now print out something like the following. Check out
|
||||
the [Sent information Request](./usage#sent-information-request) payload description.
|
||||
|
||||
```json
|
||||
{
|
||||
"fullMethod": "/zitadel.user.v2.UserService/CreateUser",
|
||||
"instanceID": "344648897353744526",
|
||||
"orgID": "344648897353810062",
|
||||
"projectID": "344648897353875598",
|
||||
"userID": "344648897354465422",
|
||||
"request":
|
||||
{
|
||||
"organizationId": "344648897353810062",
|
||||
"human":
|
||||
{
|
||||
"profile":
|
||||
{
|
||||
"givenName": "Minnie",
|
||||
"familyName": "Mouse",
|
||||
"nickName": "Mini",
|
||||
"displayName": "Minnie Mouse",
|
||||
"preferredLanguage": "en",
|
||||
"gender": "GENDER_FEMALE"
|
||||
},
|
||||
"email":
|
||||
{
|
||||
"email": "[email protected]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"headers":
|
||||
{
|
||||
"Content-Type":
|
||||
[
|
||||
"application/grpc"
|
||||
],
|
||||
"Host":
|
||||
[
|
||||
"localhost:8080"
|
||||
],
|
||||
"X-Forwarded-For":
|
||||
[
|
||||
"::1"
|
||||
],
|
||||
"X-Forwarded-Host":
|
||||
[
|
||||
"localhost:8080"
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Conclusion
|
||||
|
||||
You have successfully set up a target and verified the payload integrity for request actions using your preferred payload type.
|
||||
You can now extend this setup to other action types and integrate it into your workflows as needed.
|
||||
Selecting the appropriate payload type ensures that your data is transmitted securely and can be validated effectively on the receiving end.
|
||||
Find more information about the actions feature in the [API documentation](/concepts/features/actions_v2).
|
||||
+4
-4
@@ -506,10 +506,6 @@ module.exports = {
|
||||
type: "doc",
|
||||
id: "guides/integrate/actions/testing-request-manipulation",
|
||||
},
|
||||
{
|
||||
type: "doc",
|
||||
id: "guides/integrate/actions/testing-request-signature",
|
||||
},
|
||||
{
|
||||
type: "doc",
|
||||
id: "guides/integrate/actions/testing-response",
|
||||
@@ -530,6 +526,10 @@ module.exports = {
|
||||
type: "doc",
|
||||
id: "guides/integrate/actions/testing-event",
|
||||
},
|
||||
{
|
||||
type: "doc",
|
||||
id: "guides/integrate/actions/testing-request-signature",
|
||||
},
|
||||
{
|
||||
type: "doc",
|
||||
id: "guides/integrate/actions/migrate-from-v1",
|
||||
|
||||
+1
-1
@@ -207,7 +207,7 @@ func (a *API) registerConnectServer(service server.ConnectServer) {
|
||||
connect_middleware.AuthorizationInterceptor(a.verifier, a.systemAuthZ, a.authConfig),
|
||||
connect_middleware.TranslationHandler(),
|
||||
connect_middleware.QuotaExhaustedInterceptor(a.accessInterceptor.AccessService(), system_pb.SystemService_ServiceDesc.ServiceName),
|
||||
connect_middleware.ExecutionHandler(a.targetEncryptionAlgorithm),
|
||||
connect_middleware.ExecutionHandler(a.targetEncryptionAlgorithm, a.queries.GetActiveSigningWebKey),
|
||||
connect_middleware.ValidationHandler(),
|
||||
connect_middleware.ServiceHandler(),
|
||||
connect_middleware.ActivityInterceptor(),
|
||||
|
||||
@@ -72,8 +72,9 @@ func TestServer_ExecutionTarget(t *testing.T) {
|
||||
// create target for target changes
|
||||
targetCreatedName := integration.TargetName()
|
||||
targetCreatedURL := "https://nonexistent"
|
||||
targetCreatedPayloadType := action.PayloadType_PAYLOAD_TYPE_JSON
|
||||
|
||||
targetCreated := instance.CreateTarget(ctx, t, targetCreatedName, targetCreatedURL, target_domain.TargetTypeCall, false)
|
||||
targetCreated := instance.CreateTarget(ctx, t, targetCreatedName, targetCreatedURL, target_domain.TargetTypeCall, false, targetCreatedPayloadType)
|
||||
|
||||
// request received by target
|
||||
wantRequest := &middleware.ContextInfoRequest{
|
||||
@@ -89,7 +90,7 @@ func TestServer_ExecutionTarget(t *testing.T) {
|
||||
// replace original request with different targetID
|
||||
urlRequest, closeRequest, calledRequest, _ := integration.TestServerCallProto(wantRequest, 0, http.StatusOK, changedRequest)
|
||||
|
||||
targetRequest := waitForTarget(ctx, t, instance, urlRequest, target_domain.TargetTypeCall, false)
|
||||
targetRequest := waitForTarget(ctx, t, instance, urlRequest, target_domain.TargetTypeCall, false, targetCreatedPayloadType)
|
||||
|
||||
waitForExecutionOnCondition(ctx, t, instance, conditionRequestFullMethod(fullMethod), []string{targetRequest.GetId()})
|
||||
|
||||
@@ -105,9 +106,10 @@ func TestServer_ExecutionTarget(t *testing.T) {
|
||||
InterruptOnError: false,
|
||||
},
|
||||
},
|
||||
Timeout: durationpb.New(5 * time.Second),
|
||||
Endpoint: targetCreatedURL,
|
||||
SigningKey: targetCreated.GetSigningKey(),
|
||||
Timeout: durationpb.New(5 * time.Second),
|
||||
Endpoint: targetCreatedURL,
|
||||
SigningKey: targetCreated.GetSigningKey(),
|
||||
PayloadType: targetCreatedPayloadType,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -122,9 +124,10 @@ func TestServer_ExecutionTarget(t *testing.T) {
|
||||
InterruptOnError: false,
|
||||
},
|
||||
},
|
||||
Timeout: durationpb.New(5 * time.Second),
|
||||
Endpoint: targetCreatedURL,
|
||||
SigningKey: targetCreated.GetSigningKey(),
|
||||
Timeout: durationpb.New(5 * time.Second),
|
||||
Endpoint: targetCreatedURL,
|
||||
SigningKey: targetCreated.GetSigningKey(),
|
||||
PayloadType: targetCreatedPayloadType,
|
||||
},
|
||||
}
|
||||
// content for update
|
||||
@@ -138,9 +141,10 @@ func TestServer_ExecutionTarget(t *testing.T) {
|
||||
InterruptOnError: false,
|
||||
},
|
||||
},
|
||||
Timeout: durationpb.New(5 * time.Second),
|
||||
Endpoint: targetCreatedURL,
|
||||
SigningKey: targetCreated.GetSigningKey(),
|
||||
Timeout: durationpb.New(5 * time.Second),
|
||||
Endpoint: targetCreatedURL,
|
||||
SigningKey: targetCreated.GetSigningKey(),
|
||||
PayloadType: targetCreatedPayloadType,
|
||||
}
|
||||
|
||||
// response received by target
|
||||
@@ -157,7 +161,7 @@ func TestServer_ExecutionTarget(t *testing.T) {
|
||||
// after request with different targetID, return changed response
|
||||
targetResponseURL, closeResponse, calledResponse, _ := integration.TestServerCallProto(wantResponse, 0, http.StatusOK, changedResponse)
|
||||
|
||||
targetResponse := waitForTarget(ctx, t, instance, targetResponseURL, target_domain.TargetTypeCall, false)
|
||||
targetResponse := waitForTarget(ctx, t, instance, targetResponseURL, target_domain.TargetTypeCall, false, targetCreatedPayloadType)
|
||||
waitForExecutionOnCondition(ctx, t, instance, conditionResponseFullMethod(fullMethod), []string{targetResponse.GetId()})
|
||||
return func() {
|
||||
closeRequest()
|
||||
@@ -195,7 +199,7 @@ func TestServer_ExecutionTarget(t *testing.T) {
|
||||
wantRequest := &middleware.ContextInfoRequest{FullMethod: fullMethod, InstanceID: instance.ID(), OrgID: orgID, ProjectID: projectID, UserID: userID, Request: middleware.Message{Message: request}}
|
||||
urlRequest, closeRequest, calledRequest, _ := integration.TestServerCallProto(wantRequest, 0, http.StatusInternalServerError, nil)
|
||||
|
||||
targetRequest := waitForTarget(ctx, t, instance, urlRequest, target_domain.TargetTypeCall, true)
|
||||
targetRequest := waitForTarget(ctx, t, instance, urlRequest, target_domain.TargetTypeCall, true, action.PayloadType_PAYLOAD_TYPE_JSON)
|
||||
waitForExecutionOnCondition(ctx, t, instance, conditionRequestFullMethod(fullMethod), []string{targetRequest.GetId()})
|
||||
// GetTarget with used target
|
||||
request.Id = targetRequest.GetId()
|
||||
@@ -222,8 +226,9 @@ func TestServer_ExecutionTarget(t *testing.T) {
|
||||
// create target for target changes
|
||||
targetCreatedName := integration.TargetName()
|
||||
targetCreatedURL := "https://nonexistent"
|
||||
targetCreatedPayloadType := action.PayloadType_PAYLOAD_TYPE_JSON
|
||||
|
||||
targetCreated := instance.CreateTarget(ctx, t, targetCreatedName, targetCreatedURL, target_domain.TargetTypeCall, false)
|
||||
targetCreated := instance.CreateTarget(ctx, t, targetCreatedName, targetCreatedURL, target_domain.TargetTypeCall, false, targetCreatedPayloadType)
|
||||
|
||||
// GetTarget with used target
|
||||
request.Id = targetCreated.GetId()
|
||||
@@ -240,9 +245,10 @@ func TestServer_ExecutionTarget(t *testing.T) {
|
||||
InterruptOnError: false,
|
||||
},
|
||||
},
|
||||
Timeout: durationpb.New(5 * time.Second),
|
||||
Endpoint: targetCreatedURL,
|
||||
SigningKey: targetCreated.GetSigningKey(),
|
||||
Timeout: durationpb.New(5 * time.Second),
|
||||
Endpoint: targetCreatedURL,
|
||||
SigningKey: targetCreated.GetSigningKey(),
|
||||
PayloadType: targetCreatedPayloadType,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -259,7 +265,7 @@ func TestServer_ExecutionTarget(t *testing.T) {
|
||||
// after request with different targetID, return changed response
|
||||
targetResponseURL, closeResponse, calledResponse, _ := integration.TestServerCallProto(wantResponse, 0, http.StatusInternalServerError, nil)
|
||||
|
||||
targetResponse := waitForTarget(ctx, t, instance, targetResponseURL, target_domain.TargetTypeCall, true)
|
||||
targetResponse := waitForTarget(ctx, t, instance, targetResponseURL, target_domain.TargetTypeCall, true, targetCreatedPayloadType)
|
||||
waitForExecutionOnCondition(ctx, t, instance, conditionResponseFullMethod(fullMethod), []string{targetResponse.GetId()})
|
||||
return func() {
|
||||
closeResponse()
|
||||
@@ -307,7 +313,7 @@ func TestServer_ExecutionTarget_Event(t *testing.T) {
|
||||
urlRequest, closeF, calledF, resetF := integration.TestServerCall(nil, 0, http.StatusOK, nil)
|
||||
defer closeF()
|
||||
|
||||
targetResponse := waitForTarget(isolatedIAMOwnerCTX, t, instance, urlRequest, target_domain.TargetTypeWebhook, true)
|
||||
targetResponse := waitForTarget(isolatedIAMOwnerCTX, t, instance, urlRequest, target_domain.TargetTypeWebhook, true, action.PayloadType_PAYLOAD_TYPE_JSON)
|
||||
waitForExecutionOnCondition(isolatedIAMOwnerCTX, t, instance, conditionEvent(event), []string{targetResponse.GetId()})
|
||||
|
||||
tests := []struct {
|
||||
@@ -365,7 +371,7 @@ func TestServer_ExecutionTarget_Event_LongerThanTargetTimeout(t *testing.T) {
|
||||
urlRequest, closeF, calledF, resetF := integration.TestServerCall(nil, 5*time.Second, http.StatusOK, nil)
|
||||
defer closeF()
|
||||
|
||||
targetResponse := waitForTarget(isolatedIAMOwnerCTX, t, instance, urlRequest, target_domain.TargetTypeWebhook, true)
|
||||
targetResponse := waitForTarget(isolatedIAMOwnerCTX, t, instance, urlRequest, target_domain.TargetTypeWebhook, true, action.PayloadType_PAYLOAD_TYPE_JSON)
|
||||
waitForExecutionOnCondition(isolatedIAMOwnerCTX, t, instance, conditionEvent(event), []string{targetResponse.GetId()})
|
||||
|
||||
tests := []struct {
|
||||
@@ -416,7 +422,7 @@ func TestServer_ExecutionTarget_Event_LongerThanTransactionTimeout(t *testing.T)
|
||||
urlRequest, closeF, calledF, resetF := integration.TestServerCall(nil, 1*time.Second, http.StatusOK, nil)
|
||||
defer closeF()
|
||||
|
||||
targetResponse := waitForTarget(isolatedIAMOwnerCTX, t, instance, urlRequest, target_domain.TargetTypeWebhook, true)
|
||||
targetResponse := waitForTarget(isolatedIAMOwnerCTX, t, instance, urlRequest, target_domain.TargetTypeWebhook, true, action.PayloadType_PAYLOAD_TYPE_JSON)
|
||||
waitForExecutionOnCondition(isolatedIAMOwnerCTX, t, instance, conditionEvent(event), []string{targetResponse.GetId()})
|
||||
|
||||
tests := []struct {
|
||||
@@ -499,8 +505,8 @@ func waitForExecutionOnCondition(ctx context.Context, t *testing.T, instance *in
|
||||
}, retryDuration, tick, "timeout waiting for expected execution result")
|
||||
}
|
||||
|
||||
func waitForTarget(ctx context.Context, t *testing.T, instance *integration.Instance, endpoint string, ty target_domain.TargetType, interrupt bool) *action.CreateTargetResponse {
|
||||
resp := instance.CreateTarget(ctx, t, "", endpoint, ty, interrupt)
|
||||
func waitForTarget(ctx context.Context, t *testing.T, instance *integration.Instance, endpoint string, ty target_domain.TargetType, interrupt bool, payloadType action.PayloadType) *action.CreateTargetResponse {
|
||||
resp := instance.CreateTarget(ctx, t, "", endpoint, ty, interrupt, payloadType)
|
||||
|
||||
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(ctx, time.Minute)
|
||||
require.EventuallyWithT(t, func(ttt *assert.CollectT) {
|
||||
@@ -779,7 +785,7 @@ func expectPreUserinfoExecution(ctx context.Context, t *testing.T, instance *int
|
||||
|
||||
targetURL, closeF, _, _ := integration.TestServerCall(expectedContextInfo, 0, http.StatusOK, response)
|
||||
|
||||
targetResp := waitForTarget(ctx, t, instance, targetURL, target_domain.TargetTypeCall, true)
|
||||
targetResp := waitForTarget(ctx, t, instance, targetURL, target_domain.TargetTypeCall, true, action.PayloadType_PAYLOAD_TYPE_JSON)
|
||||
waitForExecutionOnCondition(ctx, t, instance, conditionFunction("preuserinfo"), []string{targetResp.GetId()})
|
||||
return userResp.GetUserId(), closeF
|
||||
}
|
||||
@@ -1087,7 +1093,7 @@ func expectPreAccessTokenExecution(ctx context.Context, t *testing.T, instance *
|
||||
|
||||
targetURL, closeF, _, _ := integration.TestServerCall(expectedContextInfo, 0, http.StatusOK, response)
|
||||
|
||||
targetResp := waitForTarget(ctx, t, instance, targetURL, target_domain.TargetTypeCall, true)
|
||||
targetResp := waitForTarget(ctx, t, instance, targetURL, target_domain.TargetTypeCall, true, action.PayloadType_PAYLOAD_TYPE_JSON)
|
||||
waitForExecutionOnCondition(ctx, t, instance, conditionFunction("preaccesstoken"), []string{targetResp.GetId()})
|
||||
return userResp.GetUserId(), closeF
|
||||
}
|
||||
@@ -1252,7 +1258,7 @@ func expectPreSAMLResponseExecution(ctx context.Context, t *testing.T, instance
|
||||
|
||||
targetURL, closeF, _, _ := integration.TestServerCall(expectedContextInfo, 0, http.StatusOK, response)
|
||||
|
||||
targetResp := waitForTarget(ctx, t, instance, targetURL, target_domain.TargetTypeCall, true)
|
||||
targetResp := waitForTarget(ctx, t, instance, targetURL, target_domain.TargetTypeCall, true, action.PayloadType_PAYLOAD_TYPE_JSON)
|
||||
waitForExecutionOnCondition(ctx, t, instance, conditionFunction("presamlresponse"), []string{targetResp.GetId()})
|
||||
|
||||
return userResp.GetUserId(), closeF
|
||||
|
||||
@@ -18,7 +18,7 @@ import (
|
||||
func TestServer_SetExecution_Request(t *testing.T) {
|
||||
instance := integration.NewInstance(CTX)
|
||||
isolatedIAMOwnerCTX := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner)
|
||||
targetResp := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://notexisting", target_domain.TargetTypeWebhook, false)
|
||||
targetResp := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://notexisting", target_domain.TargetTypeWebhook, false, action.PayloadType_PAYLOAD_TYPE_JSON)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -175,7 +175,7 @@ func assertSetExecutionResponse(t *testing.T, creationDate, setDate time.Time, e
|
||||
func TestServer_SetExecution_Response(t *testing.T) {
|
||||
instance := integration.NewInstance(CTX)
|
||||
isolatedIAMOwnerCTX := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner)
|
||||
targetResp := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://notexisting", target_domain.TargetTypeWebhook, false)
|
||||
targetResp := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://notexisting", target_domain.TargetTypeWebhook, false, action.PayloadType_PAYLOAD_TYPE_JSON)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -319,7 +319,7 @@ func TestServer_SetExecution_Response(t *testing.T) {
|
||||
func TestServer_SetExecution_Event(t *testing.T) {
|
||||
instance := integration.NewInstance(CTX)
|
||||
isolatedIAMOwnerCTX := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner)
|
||||
targetResp := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://notexisting", target_domain.TargetTypeWebhook, false)
|
||||
targetResp := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://notexisting", target_domain.TargetTypeWebhook, false, action.PayloadType_PAYLOAD_TYPE_JSON)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -482,7 +482,7 @@ func TestServer_SetExecution_Event(t *testing.T) {
|
||||
func TestServer_SetExecution_Function(t *testing.T) {
|
||||
instance := integration.NewInstance(CTX)
|
||||
isolatedIAMOwnerCTX := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner)
|
||||
targetResp := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://notexisting", target_domain.TargetTypeWebhook, false)
|
||||
targetResp := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://notexisting", target_domain.TargetTypeWebhook, false, action.PayloadType_PAYLOAD_TYPE_JSON)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/protobuf/types/known/durationpb"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
target_domain "github.com/zitadel/zitadel/internal/execution/target"
|
||||
"github.com/zitadel/zitadel/internal/integration"
|
||||
@@ -54,7 +55,7 @@ func TestServer_GetTarget(t *testing.T) {
|
||||
ctx: isolatedIAMOwnerCTX,
|
||||
dep: func(ctx context.Context, request *action.GetTargetRequest, response *action.GetTargetResponse) error {
|
||||
name := integration.TargetName()
|
||||
resp := instance.CreateTarget(ctx, t, name, "https://example.com", target_domain.TargetTypeWebhook, false)
|
||||
resp := instance.CreateTarget(ctx, t, name, "https://example.com", target_domain.TargetTypeWebhook, false, action.PayloadType_PAYLOAD_TYPE_JSON)
|
||||
request.Id = resp.GetId()
|
||||
response.Target.Id = resp.GetId()
|
||||
response.Target.Name = name
|
||||
@@ -71,7 +72,8 @@ func TestServer_GetTarget(t *testing.T) {
|
||||
TargetType: &action.Target_RestWebhook{
|
||||
RestWebhook: &action.RESTWebhook{},
|
||||
},
|
||||
Timeout: durationpb.New(5 * time.Second),
|
||||
Timeout: durationpb.New(5 * time.Second),
|
||||
PayloadType: action.PayloadType_PAYLOAD_TYPE_JSON,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -81,7 +83,7 @@ func TestServer_GetTarget(t *testing.T) {
|
||||
ctx: isolatedIAMOwnerCTX,
|
||||
dep: func(ctx context.Context, request *action.GetTargetRequest, response *action.GetTargetResponse) error {
|
||||
name := integration.TargetName()
|
||||
resp := instance.CreateTarget(ctx, t, name, "https://example.com", target_domain.TargetTypeAsync, false)
|
||||
resp := instance.CreateTarget(ctx, t, name, "https://example.com", target_domain.TargetTypeAsync, false, action.PayloadType_PAYLOAD_TYPE_JSON)
|
||||
request.Id = resp.GetId()
|
||||
response.Target.Id = resp.GetId()
|
||||
response.Target.Name = name
|
||||
@@ -98,7 +100,8 @@ func TestServer_GetTarget(t *testing.T) {
|
||||
TargetType: &action.Target_RestAsync{
|
||||
RestAsync: &action.RESTAsync{},
|
||||
},
|
||||
Timeout: durationpb.New(5 * time.Second),
|
||||
Timeout: durationpb.New(5 * time.Second),
|
||||
PayloadType: action.PayloadType_PAYLOAD_TYPE_JSON,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -108,7 +111,7 @@ func TestServer_GetTarget(t *testing.T) {
|
||||
ctx: isolatedIAMOwnerCTX,
|
||||
dep: func(ctx context.Context, request *action.GetTargetRequest, response *action.GetTargetResponse) error {
|
||||
name := integration.TargetName()
|
||||
resp := instance.CreateTarget(ctx, t, name, "https://example.com", target_domain.TargetTypeWebhook, true)
|
||||
resp := instance.CreateTarget(ctx, t, name, "https://example.com", target_domain.TargetTypeWebhook, true, action.PayloadType_PAYLOAD_TYPE_JSON)
|
||||
request.Id = resp.GetId()
|
||||
response.Target.Id = resp.GetId()
|
||||
response.Target.Name = name
|
||||
@@ -127,7 +130,8 @@ func TestServer_GetTarget(t *testing.T) {
|
||||
InterruptOnError: true,
|
||||
},
|
||||
},
|
||||
Timeout: durationpb.New(5 * time.Second),
|
||||
Timeout: durationpb.New(5 * time.Second),
|
||||
PayloadType: action.PayloadType_PAYLOAD_TYPE_JSON,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -137,7 +141,7 @@ func TestServer_GetTarget(t *testing.T) {
|
||||
ctx: isolatedIAMOwnerCTX,
|
||||
dep: func(ctx context.Context, request *action.GetTargetRequest, response *action.GetTargetResponse) error {
|
||||
name := integration.TargetName()
|
||||
resp := instance.CreateTarget(ctx, t, name, "https://example.com", target_domain.TargetTypeCall, false)
|
||||
resp := instance.CreateTarget(ctx, t, name, "https://example.com", target_domain.TargetTypeCall, false, action.PayloadType_PAYLOAD_TYPE_JSON)
|
||||
request.Id = resp.GetId()
|
||||
response.Target.Id = resp.GetId()
|
||||
response.Target.Name = name
|
||||
@@ -156,7 +160,8 @@ func TestServer_GetTarget(t *testing.T) {
|
||||
InterruptOnError: false,
|
||||
},
|
||||
},
|
||||
Timeout: durationpb.New(5 * time.Second),
|
||||
Timeout: durationpb.New(5 * time.Second),
|
||||
PayloadType: action.PayloadType_PAYLOAD_TYPE_JSON,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -166,7 +171,7 @@ func TestServer_GetTarget(t *testing.T) {
|
||||
ctx: isolatedIAMOwnerCTX,
|
||||
dep: func(ctx context.Context, request *action.GetTargetRequest, response *action.GetTargetResponse) error {
|
||||
name := integration.TargetName()
|
||||
resp := instance.CreateTarget(ctx, t, name, "https://example.com", target_domain.TargetTypeCall, true)
|
||||
resp := instance.CreateTarget(ctx, t, name, "https://example.com", target_domain.TargetTypeCall, true, action.PayloadType_PAYLOAD_TYPE_JSON)
|
||||
request.Id = resp.GetId()
|
||||
response.Target.Id = resp.GetId()
|
||||
response.Target.Name = name
|
||||
@@ -185,7 +190,8 @@ func TestServer_GetTarget(t *testing.T) {
|
||||
InterruptOnError: true,
|
||||
},
|
||||
},
|
||||
Timeout: durationpb.New(5 * time.Second),
|
||||
Timeout: durationpb.New(5 * time.Second),
|
||||
PayloadType: action.PayloadType_PAYLOAD_TYPE_JSON,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -261,7 +267,7 @@ func TestServer_ListTargets(t *testing.T) {
|
||||
ctx: isolatedIAMOwnerCTX,
|
||||
dep: func(ctx context.Context, request *action.ListTargetsRequest, response *action.ListTargetsResponse) {
|
||||
name := integration.TargetName()
|
||||
resp := instance.CreateTarget(ctx, t, name, "https://example.com", target_domain.TargetTypeWebhook, false)
|
||||
resp := instance.CreateTarget(ctx, t, name, "https://example.com", target_domain.TargetTypeWebhook, false, action.PayloadType_PAYLOAD_TYPE_JSON)
|
||||
request.Filters[0].Filter = &action.TargetSearchFilter_InTargetIdsFilter{
|
||||
InTargetIdsFilter: &action.InTargetIDsFilter{
|
||||
TargetIds: []string{resp.GetId()},
|
||||
@@ -291,7 +297,8 @@ func TestServer_ListTargets(t *testing.T) {
|
||||
InterruptOnError: false,
|
||||
},
|
||||
},
|
||||
Timeout: durationpb.New(5 * time.Second),
|
||||
Timeout: durationpb.New(5 * time.Second),
|
||||
PayloadType: action.PayloadType_PAYLOAD_TYPE_JSON,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -301,7 +308,7 @@ func TestServer_ListTargets(t *testing.T) {
|
||||
ctx: isolatedIAMOwnerCTX,
|
||||
dep: func(ctx context.Context, request *action.ListTargetsRequest, response *action.ListTargetsResponse) {
|
||||
name := integration.TargetName()
|
||||
resp := instance.CreateTarget(ctx, t, name, "https://example.com", target_domain.TargetTypeWebhook, false)
|
||||
resp := instance.CreateTarget(ctx, t, name, "https://example.com", target_domain.TargetTypeWebhook, false, action.PayloadType_PAYLOAD_TYPE_JSON)
|
||||
request.Filters[0].Filter = &action.TargetSearchFilter_TargetNameFilter{
|
||||
TargetNameFilter: &action.TargetNameFilter{
|
||||
TargetName: name,
|
||||
@@ -331,7 +338,8 @@ func TestServer_ListTargets(t *testing.T) {
|
||||
InterruptOnError: false,
|
||||
},
|
||||
},
|
||||
Timeout: durationpb.New(5 * time.Second),
|
||||
Timeout: durationpb.New(5 * time.Second),
|
||||
PayloadType: action.PayloadType_PAYLOAD_TYPE_JSON,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -344,9 +352,9 @@ func TestServer_ListTargets(t *testing.T) {
|
||||
name1 := integration.TargetName()
|
||||
name2 := integration.TargetName()
|
||||
name3 := integration.TargetName()
|
||||
resp1 := instance.CreateTarget(ctx, t, name1, "https://example.com", target_domain.TargetTypeWebhook, false)
|
||||
resp2 := instance.CreateTarget(ctx, t, name2, "https://example.com", target_domain.TargetTypeCall, true)
|
||||
resp3 := instance.CreateTarget(ctx, t, name3, "https://example.com", target_domain.TargetTypeAsync, false)
|
||||
resp1 := instance.CreateTarget(ctx, t, name1, "https://example.com", target_domain.TargetTypeWebhook, false, action.PayloadType_PAYLOAD_TYPE_JSON)
|
||||
resp2 := instance.CreateTarget(ctx, t, name2, "https://example.com", target_domain.TargetTypeCall, true, action.PayloadType_PAYLOAD_TYPE_JWT)
|
||||
resp3 := instance.CreateTarget(ctx, t, name3, "https://example.com", target_domain.TargetTypeAsync, false, action.PayloadType_PAYLOAD_TYPE_JWE)
|
||||
request.Filters[0].Filter = &action.TargetSearchFilter_InTargetIdsFilter{
|
||||
InTargetIdsFilter: &action.InTargetIDsFilter{
|
||||
TargetIds: []string{resp1.GetId(), resp2.GetId(), resp3.GetId()},
|
||||
@@ -386,7 +394,8 @@ func TestServer_ListTargets(t *testing.T) {
|
||||
TargetType: &action.Target_RestAsync{
|
||||
RestAsync: &action.RESTAsync{},
|
||||
},
|
||||
Timeout: durationpb.New(5 * time.Second),
|
||||
Timeout: durationpb.New(5 * time.Second),
|
||||
PayloadType: action.PayloadType_PAYLOAD_TYPE_JWE,
|
||||
},
|
||||
{
|
||||
Endpoint: "https://example.com",
|
||||
@@ -395,7 +404,8 @@ func TestServer_ListTargets(t *testing.T) {
|
||||
InterruptOnError: true,
|
||||
},
|
||||
},
|
||||
Timeout: durationpb.New(5 * time.Second),
|
||||
Timeout: durationpb.New(5 * time.Second),
|
||||
PayloadType: action.PayloadType_PAYLOAD_TYPE_JWT,
|
||||
},
|
||||
{
|
||||
Endpoint: "https://example.com",
|
||||
@@ -404,7 +414,8 @@ func TestServer_ListTargets(t *testing.T) {
|
||||
InterruptOnError: false,
|
||||
},
|
||||
},
|
||||
Timeout: durationpb.New(5 * time.Second),
|
||||
Timeout: durationpb.New(5 * time.Second),
|
||||
PayloadType: action.PayloadType_PAYLOAD_TYPE_JSON,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -445,7 +456,7 @@ func assertPaginationResponse(t *assert.CollectT, expected *filter.PaginationRes
|
||||
func TestServer_ListExecutions(t *testing.T) {
|
||||
instance := integration.NewInstance(CTX)
|
||||
isolatedIAMOwnerCTX := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner)
|
||||
targetResp := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", target_domain.TargetTypeWebhook, false)
|
||||
targetResp := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", target_domain.TargetTypeWebhook, false, action.PayloadType_PAYLOAD_TYPE_JSON)
|
||||
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
@@ -523,7 +534,7 @@ func TestServer_ListExecutions(t *testing.T) {
|
||||
args: args{
|
||||
ctx: isolatedIAMOwnerCTX,
|
||||
dep: func(ctx context.Context, request *action.ListExecutionsRequest, response *action.ListExecutionsResponse) {
|
||||
target := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", target_domain.TargetTypeWebhook, false)
|
||||
target := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", target_domain.TargetTypeWebhook, false, action.PayloadType_PAYLOAD_TYPE_JSON)
|
||||
// add target as Filter to the request
|
||||
request.Filters[0] = &action.ExecutionSearchFilter{
|
||||
Filter: &action.ExecutionSearchFilter_TargetFilter{
|
||||
@@ -781,3 +792,334 @@ func TestServer_ListExecutions(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestServer_ListPublicKeys(t *testing.T) {
|
||||
instance := integration.NewInstance(CTX)
|
||||
isolatedIAMOwnerCTX := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner)
|
||||
|
||||
targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", target_domain.TargetTypeWebhook, false, action.PayloadType_PAYLOAD_TYPE_JSON).GetId()
|
||||
activeKey, err := instance.Client.ActionV2.AddPublicKey(isolatedIAMOwnerCTX, &action.AddPublicKeyRequest{
|
||||
TargetId: targetID,
|
||||
PublicKey: publicKeyBytes,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
changeDate, err := instance.Client.ActionV2.ActivatePublicKey(isolatedIAMOwnerCTX, &action.ActivatePublicKeyRequest{
|
||||
TargetId: targetID,
|
||||
KeyId: activeKey.GetKeyId(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
expirationDate := timestamppb.New(time.Now().Add(24 * time.Hour))
|
||||
inactiveKey, err := instance.Client.ActionV2.AddPublicKey(isolatedIAMOwnerCTX, &action.AddPublicKeyRequest{
|
||||
TargetId: targetID,
|
||||
PublicKey: rsaPublicKeyBytes,
|
||||
ExpirationDate: expirationDate,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
anotherKey, err := instance.Client.ActionV2.AddPublicKey(isolatedIAMOwnerCTX, &action.AddPublicKeyRequest{
|
||||
TargetId: targetID,
|
||||
PublicKey: rsaPublicKeyBytes,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
req *action.ListPublicKeysRequest
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *action.ListPublicKeysResponse
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "missing permission",
|
||||
args: args{
|
||||
ctx: instance.WithAuthorizationToken(context.Background(), integration.UserTypeOrgOwner),
|
||||
req: &action.ListPublicKeysRequest{},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid target id",
|
||||
args: args{
|
||||
ctx: isolatedIAMOwnerCTX,
|
||||
req: &action.ListPublicKeysRequest{
|
||||
TargetId: "notfound",
|
||||
},
|
||||
},
|
||||
want: &action.ListPublicKeysResponse{
|
||||
Pagination: &filter.PaginationResponse{
|
||||
AppliedLimit: 100,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "list all",
|
||||
args: args{
|
||||
ctx: isolatedIAMOwnerCTX,
|
||||
req: &action.ListPublicKeysRequest{
|
||||
TargetId: targetID,
|
||||
},
|
||||
},
|
||||
want: &action.ListPublicKeysResponse{
|
||||
Pagination: &filter.PaginationResponse{
|
||||
AppliedLimit: 100,
|
||||
TotalResult: 3,
|
||||
},
|
||||
PublicKeys: []*action.PublicKey{
|
||||
{
|
||||
KeyId: anotherKey.GetKeyId(),
|
||||
Active: false,
|
||||
PublicKey: rsaPublicKeyBytes,
|
||||
Fingerprint: rsaFingerprint,
|
||||
ExpirationDate: nil,
|
||||
CreationDate: anotherKey.GetCreationDate(),
|
||||
ChangeDate: anotherKey.GetCreationDate(),
|
||||
},
|
||||
{
|
||||
KeyId: inactiveKey.GetKeyId(),
|
||||
Active: false,
|
||||
PublicKey: rsaPublicKeyBytes,
|
||||
Fingerprint: rsaFingerprint,
|
||||
ExpirationDate: expirationDate,
|
||||
CreationDate: inactiveKey.GetCreationDate(),
|
||||
ChangeDate: inactiveKey.GetCreationDate(),
|
||||
},
|
||||
{
|
||||
KeyId: activeKey.GetKeyId(),
|
||||
Active: true,
|
||||
PublicKey: publicKeyBytes,
|
||||
Fingerprint: fingerprint,
|
||||
ExpirationDate: nil,
|
||||
CreationDate: activeKey.GetCreationDate(),
|
||||
ChangeDate: changeDate.GetChangeDate(),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "list only active",
|
||||
args: args{
|
||||
ctx: isolatedIAMOwnerCTX,
|
||||
req: &action.ListPublicKeysRequest{
|
||||
TargetId: targetID,
|
||||
Filters: []*action.PublicKeySearchFilter{
|
||||
{
|
||||
Filter: &action.PublicKeySearchFilter_ActiveFilter{
|
||||
ActiveFilter: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: &action.ListPublicKeysResponse{
|
||||
Pagination: &filter.PaginationResponse{
|
||||
AppliedLimit: 100,
|
||||
TotalResult: 1,
|
||||
},
|
||||
PublicKeys: []*action.PublicKey{
|
||||
{
|
||||
KeyId: activeKey.GetKeyId(),
|
||||
Active: true,
|
||||
PublicKey: publicKeyBytes,
|
||||
Fingerprint: fingerprint,
|
||||
ExpirationDate: nil,
|
||||
CreationDate: activeKey.GetCreationDate(),
|
||||
ChangeDate: changeDate.GetChangeDate(),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "list only inactive",
|
||||
args: args{
|
||||
ctx: isolatedIAMOwnerCTX,
|
||||
req: &action.ListPublicKeysRequest{
|
||||
TargetId: targetID,
|
||||
Filters: []*action.PublicKeySearchFilter{
|
||||
{
|
||||
Filter: &action.PublicKeySearchFilter_ActiveFilter{
|
||||
ActiveFilter: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: &action.ListPublicKeysResponse{
|
||||
Pagination: &filter.PaginationResponse{
|
||||
AppliedLimit: 100,
|
||||
TotalResult: 2,
|
||||
},
|
||||
PublicKeys: []*action.PublicKey{
|
||||
{
|
||||
KeyId: anotherKey.GetKeyId(),
|
||||
Active: false,
|
||||
PublicKey: rsaPublicKeyBytes,
|
||||
Fingerprint: rsaFingerprint,
|
||||
ExpirationDate: nil,
|
||||
CreationDate: anotherKey.GetCreationDate(),
|
||||
ChangeDate: anotherKey.GetCreationDate(),
|
||||
},
|
||||
{
|
||||
KeyId: inactiveKey.GetKeyId(),
|
||||
Active: false,
|
||||
PublicKey: rsaPublicKeyBytes,
|
||||
Fingerprint: rsaFingerprint,
|
||||
ExpirationDate: expirationDate,
|
||||
CreationDate: inactiveKey.GetCreationDate(),
|
||||
ChangeDate: inactiveKey.GetCreationDate(),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "list with expiration date filter (boefore or equals)",
|
||||
args: args{
|
||||
ctx: isolatedIAMOwnerCTX,
|
||||
req: &action.ListPublicKeysRequest{
|
||||
TargetId: targetID,
|
||||
Filters: []*action.PublicKeySearchFilter{
|
||||
{
|
||||
Filter: &action.PublicKeySearchFilter_ExpirationDateFilter{
|
||||
ExpirationDateFilter: &filter.TimestampFilter{
|
||||
Timestamp: expirationDate,
|
||||
Method: filter.TimestampFilterMethod_TIMESTAMP_FILTER_METHOD_BEFORE_OR_EQUALS,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: &action.ListPublicKeysResponse{
|
||||
Pagination: &filter.PaginationResponse{
|
||||
AppliedLimit: 100,
|
||||
TotalResult: 1,
|
||||
},
|
||||
PublicKeys: []*action.PublicKey{
|
||||
{
|
||||
KeyId: inactiveKey.GetKeyId(),
|
||||
Active: false,
|
||||
PublicKey: rsaPublicKeyBytes,
|
||||
Fingerprint: rsaFingerprint,
|
||||
ExpirationDate: expirationDate,
|
||||
CreationDate: inactiveKey.GetCreationDate(),
|
||||
ChangeDate: inactiveKey.GetCreationDate(),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "list with expiration date filter (after incl. no expiration)",
|
||||
args: args{
|
||||
ctx: isolatedIAMOwnerCTX,
|
||||
req: &action.ListPublicKeysRequest{
|
||||
TargetId: targetID,
|
||||
Filters: []*action.PublicKeySearchFilter{
|
||||
{
|
||||
Filter: &action.PublicKeySearchFilter_ExpirationDateFilter{
|
||||
ExpirationDateFilter: &filter.TimestampFilter{
|
||||
Timestamp: expirationDate,
|
||||
Method: filter.TimestampFilterMethod_TIMESTAMP_FILTER_METHOD_AFTER,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: &action.ListPublicKeysResponse{
|
||||
Pagination: &filter.PaginationResponse{
|
||||
AppliedLimit: 100,
|
||||
TotalResult: 2,
|
||||
},
|
||||
PublicKeys: []*action.PublicKey{
|
||||
{
|
||||
KeyId: anotherKey.GetKeyId(),
|
||||
Active: false,
|
||||
PublicKey: rsaPublicKeyBytes,
|
||||
Fingerprint: rsaFingerprint,
|
||||
ExpirationDate: nil,
|
||||
CreationDate: anotherKey.GetCreationDate(),
|
||||
ChangeDate: anotherKey.GetCreationDate(),
|
||||
},
|
||||
{
|
||||
KeyId: activeKey.GetKeyId(),
|
||||
Active: true,
|
||||
PublicKey: publicKeyBytes,
|
||||
Fingerprint: fingerprint,
|
||||
ExpirationDate: nil,
|
||||
CreationDate: activeKey.GetCreationDate(),
|
||||
ChangeDate: changeDate.GetChangeDate(),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "list by key id",
|
||||
args: args{
|
||||
ctx: isolatedIAMOwnerCTX,
|
||||
req: &action.ListPublicKeysRequest{
|
||||
TargetId: targetID,
|
||||
Filters: []*action.PublicKeySearchFilter{
|
||||
{
|
||||
Filter: &action.PublicKeySearchFilter_KeyIdsFilter{
|
||||
KeyIdsFilter: &filter.InIDsFilter{
|
||||
Ids: []string{activeKey.GetKeyId(), anotherKey.GetKeyId()},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
SortingColumn: action.PublicKeyFieldName_PUBLIC_KEY_FIELD_NAME_CHANGE_DATE,
|
||||
Pagination: &filter.PaginationRequest{
|
||||
Limit: 1,
|
||||
Offset: 1,
|
||||
Asc: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
want: &action.ListPublicKeysResponse{
|
||||
Pagination: &filter.PaginationResponse{
|
||||
TotalResult: 2,
|
||||
AppliedLimit: 1,
|
||||
},
|
||||
PublicKeys: []*action.PublicKey{
|
||||
{
|
||||
KeyId: anotherKey.GetKeyId(),
|
||||
Active: false,
|
||||
PublicKey: rsaPublicKeyBytes,
|
||||
Fingerprint: rsaFingerprint,
|
||||
ExpirationDate: nil,
|
||||
CreationDate: anotherKey.GetCreationDate(),
|
||||
ChangeDate: anotherKey.GetCreationDate(),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(isolatedIAMOwnerCTX, 10*time.Second)
|
||||
require.EventuallyWithT(t, func(ttt *assert.CollectT) {
|
||||
got, err := instance.Client.ActionV2.ListPublicKeys(tt.args.ctx, tt.args.req)
|
||||
if tt.wantErr {
|
||||
assert.Error(ttt, err, "Error: "+err.Error())
|
||||
return
|
||||
}
|
||||
assert.NoError(ttt, err)
|
||||
assertPublicKeys(ttt, tt.want, got)
|
||||
}, retryDuration, tick, "timeout waiting for expected public keys")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func assertPublicKeys(ttt *assert.CollectT, want *action.ListPublicKeysResponse, got *action.ListPublicKeysResponse) {
|
||||
assert.EqualExportedValues(ttt, want.Pagination, got.Pagination)
|
||||
assert.Len(ttt, got.PublicKeys, len(want.PublicKeys))
|
||||
for i := range want.PublicKeys {
|
||||
assert.Equal(ttt, want.PublicKeys[i].GetKeyId(), got.PublicKeys[i].GetKeyId())
|
||||
assert.Equal(ttt, want.PublicKeys[i].GetActive(), got.PublicKeys[i].GetActive())
|
||||
assert.Equal(ttt, want.PublicKeys[i].GetFingerprint(), got.PublicKeys[i].GetFingerprint())
|
||||
assert.Equal(ttt, want.PublicKeys[i].GetPublicKey(), got.PublicKeys[i].GetPublicKey())
|
||||
assertDateWithinRangeClockSkew(ttt, got.PublicKeys[i].GetExpirationDate().AsTime(), want.PublicKeys[i].GetExpirationDate().AsTime(), want.PublicKeys[i].GetExpirationDate().AsTime())
|
||||
assertDateWithinRangeClockSkew(ttt, got.PublicKeys[i].GetCreationDate().AsTime(), want.PublicKeys[i].GetCreationDate().AsTime(), want.PublicKeys[i].GetCreationDate().AsTime())
|
||||
assertDateWithinRangeClockSkew(ttt, got.PublicKeys[i].GetChangeDate().AsTime(), want.PublicKeys[i].GetChangeDate().AsTime(), want.PublicKeys[i].GetChangeDate().AsTime())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,13 +4,25 @@ package action_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/muhlemmer/gu"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/protobuf/types/known/durationpb"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
target_domain "github.com/zitadel/zitadel/internal/execution/target"
|
||||
"github.com/zitadel/zitadel/internal/integration"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/action/v2"
|
||||
@@ -25,7 +37,7 @@ func TestServer_CreateTarget(t *testing.T) {
|
||||
signingKey bool
|
||||
}
|
||||
alreadyExistingTargetName := integration.TargetName()
|
||||
instance.CreateTarget(isolatedIAMOwnerCTX, t, alreadyExistingTargetName, "https://example.com", target_domain.TargetTypeAsync, false)
|
||||
instance.CreateTarget(isolatedIAMOwnerCTX, t, alreadyExistingTargetName, "https://example.com", target_domain.TargetTypeAsync, false, action.PayloadType_PAYLOAD_TYPE_JSON)
|
||||
tests := []struct {
|
||||
name string
|
||||
ctx context.Context
|
||||
@@ -115,7 +127,8 @@ func TestServer_CreateTarget(t *testing.T) {
|
||||
TargetType: &action.CreateTargetRequest_RestAsync{
|
||||
RestAsync: &action.RESTAsync{},
|
||||
},
|
||||
Timeout: durationpb.New(10 * time.Second),
|
||||
Timeout: durationpb.New(10 * time.Second),
|
||||
PayloadType: action.PayloadType_PAYLOAD_TYPE_JSON,
|
||||
},
|
||||
want: want{
|
||||
id: true,
|
||||
@@ -134,7 +147,8 @@ func TestServer_CreateTarget(t *testing.T) {
|
||||
InterruptOnError: false,
|
||||
},
|
||||
},
|
||||
Timeout: durationpb.New(10 * time.Second),
|
||||
Timeout: durationpb.New(10 * time.Second),
|
||||
PayloadType: action.PayloadType_PAYLOAD_TYPE_JSON,
|
||||
},
|
||||
want: want{
|
||||
id: true,
|
||||
@@ -153,7 +167,8 @@ func TestServer_CreateTarget(t *testing.T) {
|
||||
InterruptOnError: true,
|
||||
},
|
||||
},
|
||||
Timeout: durationpb.New(10 * time.Second),
|
||||
Timeout: durationpb.New(10 * time.Second),
|
||||
PayloadType: action.PayloadType_PAYLOAD_TYPE_JSON,
|
||||
},
|
||||
want: want{
|
||||
id: true,
|
||||
@@ -172,7 +187,8 @@ func TestServer_CreateTarget(t *testing.T) {
|
||||
InterruptOnError: false,
|
||||
},
|
||||
},
|
||||
Timeout: durationpb.New(10 * time.Second),
|
||||
Timeout: durationpb.New(10 * time.Second),
|
||||
PayloadType: action.PayloadType_PAYLOAD_TYPE_JSON,
|
||||
},
|
||||
want: want{
|
||||
id: true,
|
||||
@@ -192,7 +208,8 @@ func TestServer_CreateTarget(t *testing.T) {
|
||||
InterruptOnError: true,
|
||||
},
|
||||
},
|
||||
Timeout: durationpb.New(10 * time.Second),
|
||||
Timeout: durationpb.New(10 * time.Second),
|
||||
PayloadType: action.PayloadType_PAYLOAD_TYPE_JSON,
|
||||
},
|
||||
want: want{
|
||||
id: true,
|
||||
@@ -262,7 +279,7 @@ func TestServer_UpdateTarget(t *testing.T) {
|
||||
{
|
||||
name: "missing permission",
|
||||
prepare: func(request *action.UpdateTargetRequest) {
|
||||
targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", target_domain.TargetTypeWebhook, false).GetId()
|
||||
targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", target_domain.TargetTypeWebhook, false, action.PayloadType_PAYLOAD_TYPE_JSON).GetId()
|
||||
request.Id = targetID
|
||||
},
|
||||
args: args{
|
||||
@@ -289,7 +306,7 @@ func TestServer_UpdateTarget(t *testing.T) {
|
||||
{
|
||||
name: "no change, ok",
|
||||
prepare: func(request *action.UpdateTargetRequest) {
|
||||
targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", target_domain.TargetTypeWebhook, false).GetId()
|
||||
targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", target_domain.TargetTypeWebhook, false, action.PayloadType_PAYLOAD_TYPE_JSON).GetId()
|
||||
request.Id = targetID
|
||||
},
|
||||
args: args{
|
||||
@@ -307,7 +324,7 @@ func TestServer_UpdateTarget(t *testing.T) {
|
||||
{
|
||||
name: "change name, ok",
|
||||
prepare: func(request *action.UpdateTargetRequest) {
|
||||
targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", target_domain.TargetTypeWebhook, false).GetId()
|
||||
targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", target_domain.TargetTypeWebhook, false, action.PayloadType_PAYLOAD_TYPE_JSON).GetId()
|
||||
request.Id = targetID
|
||||
},
|
||||
args: args{
|
||||
@@ -325,7 +342,7 @@ func TestServer_UpdateTarget(t *testing.T) {
|
||||
{
|
||||
name: "regenerate signingkey, ok",
|
||||
prepare: func(request *action.UpdateTargetRequest) {
|
||||
targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", target_domain.TargetTypeWebhook, false).GetId()
|
||||
targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", target_domain.TargetTypeWebhook, false, action.PayloadType_PAYLOAD_TYPE_JSON).GetId()
|
||||
request.Id = targetID
|
||||
},
|
||||
args: args{
|
||||
@@ -343,7 +360,7 @@ func TestServer_UpdateTarget(t *testing.T) {
|
||||
{
|
||||
name: "change type, ok",
|
||||
prepare: func(request *action.UpdateTargetRequest) {
|
||||
targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", target_domain.TargetTypeWebhook, false).GetId()
|
||||
targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", target_domain.TargetTypeWebhook, false, action.PayloadType_PAYLOAD_TYPE_JSON).GetId()
|
||||
request.Id = targetID
|
||||
},
|
||||
args: args{
|
||||
@@ -365,7 +382,7 @@ func TestServer_UpdateTarget(t *testing.T) {
|
||||
{
|
||||
name: "change url, ok",
|
||||
prepare: func(request *action.UpdateTargetRequest) {
|
||||
targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", target_domain.TargetTypeWebhook, false).GetId()
|
||||
targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", target_domain.TargetTypeWebhook, false, action.PayloadType_PAYLOAD_TYPE_JSON).GetId()
|
||||
request.Id = targetID
|
||||
},
|
||||
args: args{
|
||||
@@ -383,7 +400,7 @@ func TestServer_UpdateTarget(t *testing.T) {
|
||||
{
|
||||
name: "change timeout, ok",
|
||||
prepare: func(request *action.UpdateTargetRequest) {
|
||||
targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", target_domain.TargetTypeWebhook, false).GetId()
|
||||
targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", target_domain.TargetTypeWebhook, false, action.PayloadType_PAYLOAD_TYPE_JSON).GetId()
|
||||
request.Id = targetID
|
||||
},
|
||||
args: args{
|
||||
@@ -398,10 +415,28 @@ func TestServer_UpdateTarget(t *testing.T) {
|
||||
signingKey: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "change timeout, ok",
|
||||
prepare: func(request *action.UpdateTargetRequest) {
|
||||
targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", target_domain.TargetTypeWebhook, false, action.PayloadType_PAYLOAD_TYPE_JSON).GetId()
|
||||
request.Id = targetID
|
||||
},
|
||||
args: args{
|
||||
ctx: isolatedIAMOwnerCTX,
|
||||
req: &action.UpdateTargetRequest{
|
||||
PayloadType: action.PayloadType_PAYLOAD_TYPE_JWT,
|
||||
},
|
||||
},
|
||||
want: want{
|
||||
change: true,
|
||||
changeDate: true,
|
||||
signingKey: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "change type async, ok",
|
||||
prepare: func(request *action.UpdateTargetRequest) {
|
||||
targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", target_domain.TargetTypeAsync, false).GetId()
|
||||
targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", target_domain.TargetTypeAsync, false, action.PayloadType_PAYLOAD_TYPE_JSON).GetId()
|
||||
request.Id = targetID
|
||||
},
|
||||
args: args{
|
||||
@@ -497,7 +532,7 @@ func TestServer_DeleteTarget(t *testing.T) {
|
||||
ctx: iamOwnerCtx,
|
||||
prepare: func(request *action.DeleteTargetRequest) (time.Time, time.Time) {
|
||||
creationDate := time.Now().UTC()
|
||||
targetID := instance.CreateTarget(iamOwnerCtx, t, "", "https://example.com", target_domain.TargetTypeWebhook, false).GetId()
|
||||
targetID := instance.CreateTarget(iamOwnerCtx, t, "", "https://example.com", target_domain.TargetTypeWebhook, false, action.PayloadType_PAYLOAD_TYPE_JSON).GetId()
|
||||
request.Id = targetID
|
||||
return creationDate, time.Time{}
|
||||
},
|
||||
@@ -509,7 +544,7 @@ func TestServer_DeleteTarget(t *testing.T) {
|
||||
ctx: iamOwnerCtx,
|
||||
prepare: func(request *action.DeleteTargetRequest) (time.Time, time.Time) {
|
||||
creationDate := time.Now().UTC()
|
||||
targetID := instance.CreateTarget(iamOwnerCtx, t, "", "https://example.com", target_domain.TargetTypeWebhook, false).GetId()
|
||||
targetID := instance.CreateTarget(iamOwnerCtx, t, "", "https://example.com", target_domain.TargetTypeWebhook, false, action.PayloadType_PAYLOAD_TYPE_JSON).GetId()
|
||||
request.Id = targetID
|
||||
instance.DeleteTarget(iamOwnerCtx, t, targetID)
|
||||
return creationDate, time.Now().UTC()
|
||||
@@ -546,3 +581,496 @@ func assertDeleteTargetResponse(t *testing.T, creationDate, deletionDate time.Ti
|
||||
assert.Nil(t, actualResp.DeletionDate)
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
publicKey = func() *ecdsa.PublicKey {
|
||||
privateKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
return &privateKey.PublicKey
|
||||
}()
|
||||
publicKeyBytes = func() []byte {
|
||||
pubBytes, _ := x509.MarshalPKIXPublicKey(publicKey)
|
||||
return pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: pubBytes})
|
||||
}()
|
||||
fingerprint = func() string {
|
||||
block, _ := x509.MarshalPKIXPublicKey(publicKey)
|
||||
hash := sha256.Sum256(block)
|
||||
return fmt.Sprintf("SHA256:%s", base64.RawStdEncoding.EncodeToString(hash[:]))
|
||||
}()
|
||||
|
||||
rsaPublicKey = func() *rsa.PublicKey {
|
||||
privateKey, _ := rsa.GenerateKey(rand.Reader, 2048)
|
||||
return &privateKey.PublicKey
|
||||
}()
|
||||
rsaPublicKeyBytes = func() []byte {
|
||||
data, _ := crypto.PublicKeyToBytes(rsaPublicKey)
|
||||
return data
|
||||
}()
|
||||
rsaFingerprint = func() string {
|
||||
block, _ := x509.MarshalPKIXPublicKey(rsaPublicKey)
|
||||
hash := sha256.Sum256(block)
|
||||
return fmt.Sprintf("SHA256:%s", base64.RawStdEncoding.EncodeToString(hash[:]))
|
||||
}()
|
||||
)
|
||||
|
||||
func TestServer_AddPublicKey(t *testing.T) {
|
||||
instance := integration.NewInstance(CTX)
|
||||
isolatedIAMOwnerCTX := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner)
|
||||
type want struct {
|
||||
id bool
|
||||
creationDate bool
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
ctx context.Context
|
||||
req *action.AddPublicKeyRequest
|
||||
want want
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "missing permission",
|
||||
ctx: instance.WithAuthorizationToken(context.Background(), integration.UserTypeOrgOwner),
|
||||
req: &action.AddPublicKeyRequest{
|
||||
TargetId: "targetID",
|
||||
PublicKey: publicKeyBytes,
|
||||
ExpirationDate: timestamppb.New(time.Now().UTC().Add(time.Hour)),
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "empty target id",
|
||||
ctx: isolatedIAMOwnerCTX,
|
||||
req: &action.AddPublicKeyRequest{
|
||||
TargetId: "",
|
||||
PublicKey: publicKeyBytes,
|
||||
ExpirationDate: timestamppb.New(time.Now().UTC().Add(time.Hour)),
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "empty public key",
|
||||
ctx: isolatedIAMOwnerCTX,
|
||||
req: &action.AddPublicKeyRequest{
|
||||
TargetId: "targetID",
|
||||
PublicKey: nil,
|
||||
ExpirationDate: timestamppb.New(time.Now().UTC().Add(time.Hour)),
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid public key",
|
||||
ctx: isolatedIAMOwnerCTX,
|
||||
req: &action.AddPublicKeyRequest{
|
||||
TargetId: "targetID",
|
||||
PublicKey: []byte("invalid"),
|
||||
ExpirationDate: timestamppb.New(time.Now().UTC().Add(time.Hour)),
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "expiration date in the past",
|
||||
ctx: isolatedIAMOwnerCTX,
|
||||
req: &action.AddPublicKeyRequest{
|
||||
TargetId: "targetID",
|
||||
PublicKey: publicKeyBytes,
|
||||
ExpirationDate: timestamppb.New(time.Now().UTC().Add(-1 * time.Hour)),
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "target not existing",
|
||||
ctx: isolatedIAMOwnerCTX,
|
||||
req: &action.AddPublicKeyRequest{
|
||||
TargetId: "notexisting",
|
||||
PublicKey: publicKeyBytes,
|
||||
ExpirationDate: timestamppb.New(time.Now().UTC().Add(time.Hour)),
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "add public key, ok",
|
||||
ctx: isolatedIAMOwnerCTX,
|
||||
req: func() *action.AddPublicKeyRequest {
|
||||
targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", target_domain.TargetTypeWebhook, false, action.PayloadType_PAYLOAD_TYPE_JSON).GetId()
|
||||
return &action.AddPublicKeyRequest{
|
||||
TargetId: targetID,
|
||||
PublicKey: publicKeyBytes,
|
||||
ExpirationDate: timestamppb.New(time.Now().UTC().Add(time.Hour)),
|
||||
}
|
||||
}(),
|
||||
want: want{
|
||||
id: true,
|
||||
creationDate: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
start := time.Now()
|
||||
got, err := instance.Client.ActionV2.AddPublicKey(tt.ctx, tt.req)
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, got.GetKeyId())
|
||||
assertDateWithinRangeClockSkew(t, got.GetCreationDate().AsTime(), start, time.Now())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestServer_ActivatePublicKey(t *testing.T) {
|
||||
instance := integration.NewInstance(CTX)
|
||||
isolatedIAMOwnerCTX := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner)
|
||||
tests := []struct {
|
||||
name string
|
||||
ctx context.Context
|
||||
req *action.ActivatePublicKeyRequest
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "missing permission",
|
||||
ctx: instance.WithAuthorizationToken(context.Background(), integration.UserTypeOrgOwner),
|
||||
req: &action.ActivatePublicKeyRequest{
|
||||
TargetId: "targetID",
|
||||
KeyId: "keyID",
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "empty target id",
|
||||
ctx: isolatedIAMOwnerCTX,
|
||||
req: &action.ActivatePublicKeyRequest{
|
||||
TargetId: "",
|
||||
KeyId: "keyID",
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "empty key id",
|
||||
ctx: isolatedIAMOwnerCTX,
|
||||
req: &action.ActivatePublicKeyRequest{
|
||||
TargetId: "targetID",
|
||||
KeyId: "",
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "target not existing",
|
||||
ctx: isolatedIAMOwnerCTX,
|
||||
req: &action.ActivatePublicKeyRequest{
|
||||
TargetId: "notexisting",
|
||||
KeyId: "keyID",
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "key not existing",
|
||||
ctx: isolatedIAMOwnerCTX,
|
||||
req: func() *action.ActivatePublicKeyRequest {
|
||||
targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", target_domain.TargetTypeWebhook, false, action.PayloadType_PAYLOAD_TYPE_JSON).GetId()
|
||||
return &action.ActivatePublicKeyRequest{
|
||||
TargetId: targetID,
|
||||
KeyId: "notexisting",
|
||||
}
|
||||
}(),
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "already expired key",
|
||||
ctx: isolatedIAMOwnerCTX,
|
||||
req: func() *action.ActivatePublicKeyRequest {
|
||||
targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", target_domain.TargetTypeWebhook, false, action.PayloadType_PAYLOAD_TYPE_JSON).GetId()
|
||||
resp, err := instance.Client.ActionV2.AddPublicKey(isolatedIAMOwnerCTX, &action.AddPublicKeyRequest{
|
||||
TargetId: targetID,
|
||||
PublicKey: publicKeyBytes,
|
||||
ExpirationDate: timestamppb.New(time.Now().Add(1 * time.Second)),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
time.Sleep(2 * time.Second)
|
||||
return &action.ActivatePublicKeyRequest{
|
||||
TargetId: targetID,
|
||||
KeyId: resp.GetKeyId(),
|
||||
}
|
||||
}(),
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "already activated key",
|
||||
ctx: isolatedIAMOwnerCTX,
|
||||
req: func() *action.ActivatePublicKeyRequest {
|
||||
targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", target_domain.TargetTypeWebhook, false, action.PayloadType_PAYLOAD_TYPE_JSON).GetId()
|
||||
resp, err := instance.Client.ActionV2.AddPublicKey(isolatedIAMOwnerCTX, &action.AddPublicKeyRequest{
|
||||
TargetId: targetID,
|
||||
PublicKey: publicKeyBytes,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
_, err = instance.Client.ActionV2.ActivatePublicKey(isolatedIAMOwnerCTX, &action.ActivatePublicKeyRequest{
|
||||
TargetId: targetID,
|
||||
KeyId: resp.GetKeyId(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
return &action.ActivatePublicKeyRequest{
|
||||
TargetId: targetID,
|
||||
KeyId: resp.GetKeyId(),
|
||||
}
|
||||
}(),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "activate key, ok",
|
||||
ctx: isolatedIAMOwnerCTX,
|
||||
req: func() *action.ActivatePublicKeyRequest {
|
||||
targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", target_domain.TargetTypeWebhook, false, action.PayloadType_PAYLOAD_TYPE_JSON).GetId()
|
||||
resp, err := instance.Client.ActionV2.AddPublicKey(isolatedIAMOwnerCTX, &action.AddPublicKeyRequest{
|
||||
TargetId: targetID,
|
||||
PublicKey: publicKeyBytes,
|
||||
ExpirationDate: timestamppb.New(time.Now().UTC().Add(time.Hour)),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
return &action.ActivatePublicKeyRequest{
|
||||
TargetId: targetID,
|
||||
KeyId: resp.GetKeyId(),
|
||||
}
|
||||
}(),
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
start := time.Now()
|
||||
got, err := instance.Client.ActionV2.ActivatePublicKey(tt.ctx, tt.req)
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
assertDateWithinRangeClockSkew(t, got.GetChangeDate().AsTime(), start, time.Now())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestServer_DeactivatePublicKey(t *testing.T) {
|
||||
instance := integration.NewInstance(CTX)
|
||||
isolatedIAMOwnerCTX := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner)
|
||||
tests := []struct {
|
||||
name string
|
||||
ctx context.Context
|
||||
req *action.DeactivatePublicKeyRequest
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "missing permission",
|
||||
ctx: instance.WithAuthorizationToken(context.Background(), integration.UserTypeOrgOwner),
|
||||
req: &action.DeactivatePublicKeyRequest{
|
||||
TargetId: "targetID",
|
||||
KeyId: "keyID",
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "empty target id",
|
||||
ctx: isolatedIAMOwnerCTX,
|
||||
req: &action.DeactivatePublicKeyRequest{
|
||||
TargetId: "",
|
||||
KeyId: "keyID",
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "empty key id",
|
||||
ctx: isolatedIAMOwnerCTX,
|
||||
req: &action.DeactivatePublicKeyRequest{
|
||||
TargetId: "targetID",
|
||||
KeyId: "",
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "target not existing",
|
||||
ctx: isolatedIAMOwnerCTX,
|
||||
req: &action.DeactivatePublicKeyRequest{
|
||||
TargetId: "notexisting",
|
||||
KeyId: "keyID",
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "key not existing",
|
||||
ctx: isolatedIAMOwnerCTX,
|
||||
req: func() *action.DeactivatePublicKeyRequest {
|
||||
targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", target_domain.TargetTypeWebhook, false, action.PayloadType_PAYLOAD_TYPE_JSON).GetId()
|
||||
return &action.DeactivatePublicKeyRequest{
|
||||
TargetId: targetID,
|
||||
KeyId: "notexisting",
|
||||
}
|
||||
}(),
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "already deactivated key",
|
||||
ctx: isolatedIAMOwnerCTX,
|
||||
req: func() *action.DeactivatePublicKeyRequest {
|
||||
targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", target_domain.TargetTypeWebhook, false, action.PayloadType_PAYLOAD_TYPE_JSON).GetId()
|
||||
resp, err := instance.Client.ActionV2.AddPublicKey(isolatedIAMOwnerCTX, &action.AddPublicKeyRequest{
|
||||
TargetId: targetID,
|
||||
PublicKey: publicKeyBytes,
|
||||
ExpirationDate: timestamppb.New(time.Now().UTC().Add(time.Hour)),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
return &action.DeactivatePublicKeyRequest{
|
||||
TargetId: targetID,
|
||||
KeyId: resp.GetKeyId(),
|
||||
}
|
||||
}(),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "deactivate key, ok",
|
||||
ctx: isolatedIAMOwnerCTX,
|
||||
req: func() *action.DeactivatePublicKeyRequest {
|
||||
targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", target_domain.TargetTypeWebhook, false, action.PayloadType_PAYLOAD_TYPE_JSON).GetId()
|
||||
resp, err := instance.Client.ActionV2.AddPublicKey(isolatedIAMOwnerCTX, &action.AddPublicKeyRequest{
|
||||
TargetId: targetID,
|
||||
PublicKey: publicKeyBytes,
|
||||
ExpirationDate: timestamppb.New(time.Now().UTC().Add(time.Hour)),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
_, err = instance.Client.ActionV2.ActivatePublicKey(isolatedIAMOwnerCTX, &action.ActivatePublicKeyRequest{
|
||||
TargetId: targetID,
|
||||
KeyId: resp.GetKeyId(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
return &action.DeactivatePublicKeyRequest{
|
||||
TargetId: targetID,
|
||||
KeyId: resp.GetKeyId(),
|
||||
}
|
||||
}(),
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
start := time.Now()
|
||||
got, err := instance.Client.ActionV2.DeactivatePublicKey(tt.ctx, tt.req)
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
assertDateWithinRangeClockSkew(t, got.GetChangeDate().AsTime(), start, time.Now())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestServer_RemovePublicKey(t *testing.T) {
|
||||
instance := integration.NewInstance(CTX)
|
||||
isolatedIAMOwnerCTX := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner)
|
||||
tests := []struct {
|
||||
name string
|
||||
ctx context.Context
|
||||
req *action.RemovePublicKeyRequest
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "missing permission",
|
||||
ctx: instance.WithAuthorizationToken(context.Background(), integration.UserTypeOrgOwner),
|
||||
req: &action.RemovePublicKeyRequest{
|
||||
TargetId: "targetID",
|
||||
KeyId: "keyID",
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "empty target id",
|
||||
ctx: isolatedIAMOwnerCTX,
|
||||
req: &action.RemovePublicKeyRequest{
|
||||
TargetId: "",
|
||||
KeyId: "keyID",
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "empty key id",
|
||||
ctx: isolatedIAMOwnerCTX,
|
||||
req: &action.RemovePublicKeyRequest{
|
||||
TargetId: "targetID",
|
||||
KeyId: "",
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "target not existing",
|
||||
ctx: isolatedIAMOwnerCTX,
|
||||
req: &action.RemovePublicKeyRequest{
|
||||
TargetId: "notexisting",
|
||||
KeyId: "keyID",
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "key not existing",
|
||||
ctx: isolatedIAMOwnerCTX,
|
||||
req: func() *action.RemovePublicKeyRequest {
|
||||
targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", target_domain.TargetTypeWebhook, false, action.PayloadType_PAYLOAD_TYPE_JSON).GetId()
|
||||
return &action.RemovePublicKeyRequest{
|
||||
TargetId: targetID,
|
||||
KeyId: "notexisting",
|
||||
}
|
||||
}(),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "remove activated key",
|
||||
ctx: isolatedIAMOwnerCTX,
|
||||
req: func() *action.RemovePublicKeyRequest {
|
||||
targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", target_domain.TargetTypeWebhook, false, action.PayloadType_PAYLOAD_TYPE_JSON).GetId()
|
||||
resp, err := instance.Client.ActionV2.AddPublicKey(isolatedIAMOwnerCTX, &action.AddPublicKeyRequest{
|
||||
TargetId: targetID,
|
||||
PublicKey: publicKeyBytes,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
_, err = instance.Client.ActionV2.ActivatePublicKey(isolatedIAMOwnerCTX, &action.ActivatePublicKeyRequest{
|
||||
TargetId: targetID,
|
||||
KeyId: resp.GetKeyId(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
return &action.RemovePublicKeyRequest{
|
||||
TargetId: targetID,
|
||||
KeyId: resp.GetKeyId(),
|
||||
}
|
||||
}(),
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "remove key, ok",
|
||||
ctx: isolatedIAMOwnerCTX,
|
||||
req: func() *action.RemovePublicKeyRequest {
|
||||
targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", target_domain.TargetTypeWebhook, false, action.PayloadType_PAYLOAD_TYPE_JSON).GetId()
|
||||
resp, err := instance.Client.ActionV2.AddPublicKey(isolatedIAMOwnerCTX, &action.AddPublicKeyRequest{
|
||||
TargetId: targetID,
|
||||
PublicKey: publicKeyBytes,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
return &action.RemovePublicKeyRequest{
|
||||
TargetId: targetID,
|
||||
KeyId: resp.GetKeyId(),
|
||||
}
|
||||
}(),
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, err := instance.Client.ActionV2.RemovePublicKey(tt.ctx, tt.req)
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func assertDateWithinRangeClockSkew(t assert.TestingT, actual time.Time, start, end time.Time) {
|
||||
assert.WithinRange(t, actual, start.Add(-time.Second), end.Add(time.Second))
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/action/v2"
|
||||
filter_pb "github.com/zitadel/zitadel/pkg/grpc/filter/v2"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -58,6 +59,21 @@ func (s *Server) ListTargets(ctx context.Context, req *connect.Request[action.Li
|
||||
}), nil
|
||||
}
|
||||
|
||||
func (s *Server) ListPublicKeys(ctx context.Context, req *connect.Request[action.ListPublicKeysRequest]) (*connect.Response[action.ListPublicKeysResponse], error) {
|
||||
queries, err := s.listPublicKeysRequestToQuery(req.Msg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res, err := s.query.SearchAuthNKeys(ctx, queries, query.JoinFilterTarget, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return connect.NewResponse(&action.ListPublicKeysResponse{
|
||||
Pagination: filter.QueryToPaginationPb(queries.SearchRequest, res.SearchResponse),
|
||||
PublicKeys: publicKeysToPb(res.AuthNKeys),
|
||||
}), nil
|
||||
}
|
||||
|
||||
func (s *Server) ListExecutions(ctx context.Context, req *connect.Request[action.ListExecutionsRequest]) (*connect.Response[action.ListExecutionsResponse], error) {
|
||||
queries, err := s.ListExecutionsRequestToModel(req.Msg)
|
||||
if err != nil {
|
||||
@@ -83,11 +99,12 @@ func targetsToPb(targets []*query.Target) []*action.Target {
|
||||
|
||||
func targetToPb(t *query.Target) *action.Target {
|
||||
target := &action.Target{
|
||||
Id: t.ID,
|
||||
Name: t.Name,
|
||||
Timeout: durationpb.New(t.Timeout),
|
||||
Endpoint: t.Endpoint,
|
||||
SigningKey: t.SigningKey,
|
||||
Id: t.ID,
|
||||
Name: t.Name,
|
||||
Timeout: durationpb.New(t.Timeout),
|
||||
Endpoint: t.Endpoint,
|
||||
SigningKey: t.SigningKey,
|
||||
PayloadType: payloadTypeToPb(t.PayloadType),
|
||||
}
|
||||
switch t.TargetType {
|
||||
case target_domain.TargetTypeWebhook:
|
||||
@@ -109,6 +126,21 @@ func targetToPb(t *query.Target) *action.Target {
|
||||
return target
|
||||
}
|
||||
|
||||
func payloadTypeToPb(payloadType target_domain.PayloadType) action.PayloadType {
|
||||
switch payloadType {
|
||||
case target_domain.PayloadTypeUnspecified:
|
||||
return action.PayloadType_PAYLOAD_TYPE_UNSPECIFIED
|
||||
case target_domain.PayloadTypeJSON:
|
||||
return action.PayloadType_PAYLOAD_TYPE_JSON
|
||||
case target_domain.PayloadTypeJWT:
|
||||
return action.PayloadType_PAYLOAD_TYPE_JWT
|
||||
case target_domain.PayloadTypeJWE:
|
||||
return action.PayloadType_PAYLOAD_TYPE_JWE
|
||||
default:
|
||||
return action.PayloadType_PAYLOAD_TYPE_UNSPECIFIED
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) ListTargetsRequestToModel(req *action.ListTargetsRequest) (*query.TargetSearchQueries, error) {
|
||||
offset, limit, asc, err := filter.PaginationPbToQuery(s.systemDefaults, req.Pagination)
|
||||
if err != nil {
|
||||
@@ -403,3 +435,98 @@ func includeEventToCondition(id string) *action.Condition {
|
||||
func includeFunctionToCondition(id string) *action.Condition {
|
||||
return &action.Condition{ConditionType: &action.Condition_Function{Function: &action.FunctionExecution{Name: strings.TrimPrefix(id, "/")}}}
|
||||
}
|
||||
|
||||
func (s *Server) listPublicKeysRequestToQuery(req *action.ListPublicKeysRequest) (*query.AuthNKeySearchQueries, error) {
|
||||
offset, limit, asc, err := filter.PaginationPbToQuery(s.systemDefaults, req.Pagination)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
queries, err := publicKeyFiltersToQuery(req.Filters, req.GetTargetId())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &query.AuthNKeySearchQueries{
|
||||
SearchRequest: query.SearchRequest{
|
||||
Offset: offset,
|
||||
Limit: limit,
|
||||
Asc: asc,
|
||||
SortingColumn: publicKeyFieldNameToSortingColumn(req.GetSortingColumn()),
|
||||
},
|
||||
Queries: queries,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func publicKeyFiltersToQuery(filters []*action.PublicKeySearchFilter, targetID string) (_ []query.SearchQuery, err error) {
|
||||
f := make([]query.SearchQuery, len(filters)+1)
|
||||
for i, filter := range filters {
|
||||
f[i], err = publicKeyFilterToQuery(filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
f[len(filters)], err = query.NewAuthNKeyIdentifyerQuery(targetID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
|
||||
func publicKeyFilterToQuery(filter *action.PublicKeySearchFilter) (query.SearchQuery, error) {
|
||||
switch q := filter.Filter.(type) {
|
||||
case *action.PublicKeySearchFilter_KeyIdsFilter:
|
||||
return publicKeyKeyIDsFilterToQuery(q.KeyIdsFilter)
|
||||
case *action.PublicKeySearchFilter_ActiveFilter:
|
||||
return publicKeyActiveFilterToQuery(q.ActiveFilter)
|
||||
case *action.PublicKeySearchFilter_ExpirationDateFilter:
|
||||
return publicKeyExpirationFilterToQuery(q.ExpirationDateFilter)
|
||||
default:
|
||||
return nil, zerrors.ThrowInvalidArgument(nil, "GRPC-vR9nC", "List.Query.Invalid")
|
||||
}
|
||||
}
|
||||
|
||||
func publicKeyKeyIDsFilterToQuery(idsFilter *filter_pb.InIDsFilter) (query.SearchQuery, error) {
|
||||
return query.NewAuthNKeyInIDsSearchQuery(idsFilter.GetIds())
|
||||
}
|
||||
|
||||
func publicKeyActiveFilterToQuery(activeFilter bool) (query.SearchQuery, error) {
|
||||
return query.NewAuthNKeyEnabledSearchQuery(activeFilter)
|
||||
}
|
||||
|
||||
func publicKeyExpirationFilterToQuery(f *filter_pb.TimestampFilter) (query.SearchQuery, error) {
|
||||
return query.NewAuthNKeyExpirationDateQuery(f.GetTimestamp().AsTime(), filter.TimestampMethodPbToQuery(f.Method))
|
||||
}
|
||||
|
||||
func publicKeyFieldNameToSortingColumn(column action.PublicKeyFieldName) query.Column {
|
||||
switch column {
|
||||
case action.PublicKeyFieldName_PUBLIC_KEY_FIELD_NAME_UNSPECIFIED:
|
||||
return query.AuthNKeyColumnCreationDate
|
||||
case action.PublicKeyFieldName_PUBLIC_KEY_FIELD_NAME_CREATION_DATE:
|
||||
return query.AuthNKeyColumnCreationDate
|
||||
case action.PublicKeyFieldName_PUBLIC_KEY_FIELD_NAME_CHANGE_DATE:
|
||||
return query.AuthNKeyColumnChangeDate
|
||||
case action.PublicKeyFieldName_PUBLIC_KEY_FIELD_NAME_EXPIRATION_DATE:
|
||||
return query.AuthNKeyColumnExpiration
|
||||
default:
|
||||
return query.AuthNKeyColumnCreationDate
|
||||
}
|
||||
}
|
||||
|
||||
func publicKeysToPb(keys []*query.AuthNKey) []*action.PublicKey {
|
||||
pks := make([]*action.PublicKey, len(keys))
|
||||
for i, key := range keys {
|
||||
var expiration *timestamppb.Timestamp
|
||||
if !key.Expiration.IsZero() {
|
||||
expiration = timestamppb.New(key.Expiration)
|
||||
}
|
||||
pks[i] = &action.PublicKey{
|
||||
KeyId: key.ID,
|
||||
Active: key.Enabled,
|
||||
PublicKey: key.PublicKey,
|
||||
Fingerprint: key.Fingerprint,
|
||||
ExpirationDate: expiration,
|
||||
CreationDate: timestamppb.New(key.CreationDate),
|
||||
ChangeDate: timestamppb.New(key.ChangeDate),
|
||||
}
|
||||
}
|
||||
return pks
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ package action
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"connectrpc.com/connect"
|
||||
"github.com/muhlemmer/gu"
|
||||
@@ -64,6 +66,60 @@ func (s *Server) DeleteTarget(ctx context.Context, req *connect.Request[action.D
|
||||
}), nil
|
||||
}
|
||||
|
||||
func (s *Server) AddPublicKey(ctx context.Context, req *connect.Request[action.AddPublicKeyRequest]) (*connect.Response[action.AddPublicKeyResponse], error) {
|
||||
instanceID := authz.GetInstance(ctx).InstanceID()
|
||||
var expirationDate time.Time
|
||||
if req.Msg.GetExpirationDate() != nil {
|
||||
expirationDate = req.Msg.GetExpirationDate().AsTime()
|
||||
}
|
||||
key := &command.TargetPublicKey{
|
||||
TargetID: strings.TrimSpace(req.Msg.GetTargetId()),
|
||||
PublicKey: req.Msg.GetPublicKey(),
|
||||
Expiration: expirationDate,
|
||||
}
|
||||
creationDate, err := s.command.AddTargetPublicKey(ctx, key, instanceID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return connect.NewResponse(&action.AddPublicKeyResponse{
|
||||
KeyId: key.KeyID,
|
||||
CreationDate: timestamppb.New(creationDate),
|
||||
}), nil
|
||||
}
|
||||
|
||||
func (s *Server) ActivatePublicKey(ctx context.Context, req *connect.Request[action.ActivatePublicKeyRequest]) (*connect.Response[action.ActivatePublicKeyResponse], error) {
|
||||
instanceID := authz.GetInstance(ctx).InstanceID()
|
||||
changeDate, err := s.command.ActivateTargetPublicKey(ctx, strings.TrimSpace(req.Msg.GetTargetId()), strings.TrimSpace(req.Msg.GetKeyId()), instanceID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return connect.NewResponse(&action.ActivatePublicKeyResponse{
|
||||
ChangeDate: timestamppb.New(changeDate),
|
||||
}), nil
|
||||
}
|
||||
|
||||
func (s *Server) DeactivatePublicKey(ctx context.Context, req *connect.Request[action.DeactivatePublicKeyRequest]) (*connect.Response[action.DeactivatePublicKeyResponse], error) {
|
||||
instanceID := authz.GetInstance(ctx).InstanceID()
|
||||
changeDate, err := s.command.DeactivateTargetPublicKey(ctx, strings.TrimSpace(req.Msg.GetTargetId()), strings.TrimSpace(req.Msg.GetKeyId()), instanceID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return connect.NewResponse(&action.DeactivatePublicKeyResponse{
|
||||
ChangeDate: timestamppb.New(changeDate),
|
||||
}), nil
|
||||
}
|
||||
|
||||
func (s *Server) RemovePublicKey(ctx context.Context, req *connect.Request[action.RemovePublicKeyRequest]) (*connect.Response[action.RemovePublicKeyResponse], error) {
|
||||
instanceID := authz.GetInstance(ctx).InstanceID()
|
||||
deletionDate, err := s.command.RemoveTargetPublicKey(ctx, strings.TrimSpace(req.Msg.GetTargetId()), strings.TrimSpace(req.Msg.GetKeyId()), instanceID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return connect.NewResponse(&action.RemovePublicKeyResponse{
|
||||
DeletionDate: timestamppb.New(deletionDate),
|
||||
}), nil
|
||||
}
|
||||
|
||||
func createTargetToCommand(req *action.CreateTargetRequest) *command.AddTarget {
|
||||
var (
|
||||
targetType target_domain.TargetType
|
||||
@@ -85,6 +141,22 @@ func createTargetToCommand(req *action.CreateTargetRequest) *command.AddTarget {
|
||||
Endpoint: req.GetEndpoint(),
|
||||
Timeout: req.GetTimeout().AsDuration(),
|
||||
InterruptOnError: interruptOnError,
|
||||
PayloadType: payloadTypeToDomain(req.GetPayloadType()),
|
||||
}
|
||||
}
|
||||
|
||||
func payloadTypeToDomain(payloadType action.PayloadType) target_domain.PayloadType {
|
||||
switch payloadType {
|
||||
case action.PayloadType_PAYLOAD_TYPE_UNSPECIFIED:
|
||||
return target_domain.PayloadTypeUnspecified
|
||||
case action.PayloadType_PAYLOAD_TYPE_JSON:
|
||||
return target_domain.PayloadTypeJSON
|
||||
case action.PayloadType_PAYLOAD_TYPE_JWT:
|
||||
return target_domain.PayloadTypeJWT
|
||||
case action.PayloadType_PAYLOAD_TYPE_JWE:
|
||||
return target_domain.PayloadTypeJWE
|
||||
default:
|
||||
return target_domain.PayloadTypeUnspecified
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,6 +174,7 @@ func updateTargetToCommand(req *action.UpdateTargetRequest) *command.ChangeTarge
|
||||
Name: req.Name,
|
||||
Endpoint: req.Endpoint,
|
||||
ExpirationSigningKey: expirationSigningKey,
|
||||
PayloadType: payloadTypeToDomain(req.GetPayloadType()),
|
||||
}
|
||||
if req.TargetType != nil {
|
||||
switch t := req.GetTargetType().(type) {
|
||||
|
||||
@@ -30,6 +30,7 @@ func Test_createTargetToCommand(t *testing.T) {
|
||||
Endpoint: "",
|
||||
Timeout: 0,
|
||||
InterruptOnError: false,
|
||||
PayloadType: target_domain.PayloadTypeUnspecified,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -40,7 +41,8 @@ func Test_createTargetToCommand(t *testing.T) {
|
||||
TargetType: &action.CreateTargetRequest_RestWebhook{
|
||||
RestWebhook: &action.RESTWebhook{},
|
||||
},
|
||||
Timeout: durationpb.New(10 * time.Second),
|
||||
Timeout: durationpb.New(10 * time.Second),
|
||||
PayloadType: action.PayloadType_PAYLOAD_TYPE_JSON,
|
||||
}},
|
||||
want: &command.AddTarget{
|
||||
Name: "target 1",
|
||||
@@ -48,6 +50,7 @@ func Test_createTargetToCommand(t *testing.T) {
|
||||
Endpoint: "https://example.com/hooks/1",
|
||||
Timeout: 10 * time.Second,
|
||||
InterruptOnError: false,
|
||||
PayloadType: target_domain.PayloadTypeJSON,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -58,7 +61,8 @@ func Test_createTargetToCommand(t *testing.T) {
|
||||
TargetType: &action.CreateTargetRequest_RestAsync{
|
||||
RestAsync: &action.RESTAsync{},
|
||||
},
|
||||
Timeout: durationpb.New(10 * time.Second),
|
||||
Timeout: durationpb.New(10 * time.Second),
|
||||
PayloadType: action.PayloadType_PAYLOAD_TYPE_JWT,
|
||||
}},
|
||||
want: &command.AddTarget{
|
||||
Name: "target 1",
|
||||
@@ -66,6 +70,7 @@ func Test_createTargetToCommand(t *testing.T) {
|
||||
Endpoint: "https://example.com/hooks/1",
|
||||
Timeout: 10 * time.Second,
|
||||
InterruptOnError: false,
|
||||
PayloadType: target_domain.PayloadTypeJWT,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -78,7 +83,8 @@ func Test_createTargetToCommand(t *testing.T) {
|
||||
InterruptOnError: true,
|
||||
},
|
||||
},
|
||||
Timeout: durationpb.New(10 * time.Second),
|
||||
Timeout: durationpb.New(10 * time.Second),
|
||||
PayloadType: action.PayloadType_PAYLOAD_TYPE_JWE,
|
||||
}},
|
||||
want: &command.AddTarget{
|
||||
Name: "target 1",
|
||||
@@ -86,6 +92,7 @@ func Test_createTargetToCommand(t *testing.T) {
|
||||
Endpoint: "https://example.com/hooks/1",
|
||||
Timeout: 10 * time.Second,
|
||||
InterruptOnError: true,
|
||||
PayloadType: target_domain.PayloadTypeJWE,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ func TestServer_ExecutionTarget(t *testing.T) {
|
||||
targetCreatedName := integration.TargetName()
|
||||
targetCreatedURL := "https://nonexistent"
|
||||
|
||||
targetCreated := instance.CreateTarget(ctx, t, targetCreatedName, targetCreatedURL, target_domain.TargetTypeCall, false)
|
||||
targetCreated := instance.CreateTargetWithoutPayloadType(ctx, t, targetCreatedName, targetCreatedURL, target_domain.TargetTypeCall, false)
|
||||
|
||||
// request received by target
|
||||
wantRequest := &middleware.ContextInfoRequest{
|
||||
@@ -223,7 +223,7 @@ func TestServer_ExecutionTarget(t *testing.T) {
|
||||
targetCreatedName := integration.TargetName()
|
||||
targetCreatedURL := "https://nonexistent"
|
||||
|
||||
targetCreated := instance.CreateTarget(ctx, t, targetCreatedName, targetCreatedURL, target_domain.TargetTypeCall, false)
|
||||
targetCreated := instance.CreateTargetWithoutPayloadType(ctx, t, targetCreatedName, targetCreatedURL, target_domain.TargetTypeCall, false)
|
||||
|
||||
// GetTarget with used target
|
||||
request.Id = targetCreated.GetId()
|
||||
|
||||
@@ -18,7 +18,7 @@ import (
|
||||
func TestServer_SetExecution_Request(t *testing.T) {
|
||||
instance := integration.NewInstance(CTX)
|
||||
isolatedIAMOwnerCTX := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner)
|
||||
targetResp := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://notexisting", target_domain.TargetTypeWebhook, false)
|
||||
targetResp := instance.CreateTargetWithoutPayloadType(isolatedIAMOwnerCTX, t, "", "https://notexisting", target_domain.TargetTypeWebhook, false)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -175,7 +175,7 @@ func assertSetExecutionResponse(t *testing.T, creationDate, setDate time.Time, e
|
||||
func TestServer_SetExecution_Response(t *testing.T) {
|
||||
instance := integration.NewInstance(CTX)
|
||||
isolatedIAMOwnerCTX := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner)
|
||||
targetResp := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://notexisting", target_domain.TargetTypeWebhook, false)
|
||||
targetResp := instance.CreateTargetWithoutPayloadType(isolatedIAMOwnerCTX, t, "", "https://notexisting", target_domain.TargetTypeWebhook, false)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -319,7 +319,7 @@ func TestServer_SetExecution_Response(t *testing.T) {
|
||||
func TestServer_SetExecution_Event(t *testing.T) {
|
||||
instance := integration.NewInstance(CTX)
|
||||
isolatedIAMOwnerCTX := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner)
|
||||
targetResp := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://notexisting", target_domain.TargetTypeWebhook, false)
|
||||
targetResp := instance.CreateTargetWithoutPayloadType(isolatedIAMOwnerCTX, t, "", "https://notexisting", target_domain.TargetTypeWebhook, false)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -482,7 +482,7 @@ func TestServer_SetExecution_Event(t *testing.T) {
|
||||
func TestServer_SetExecution_Function(t *testing.T) {
|
||||
instance := integration.NewInstance(CTX)
|
||||
isolatedIAMOwnerCTX := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner)
|
||||
targetResp := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://notexisting", target_domain.TargetTypeWebhook, false)
|
||||
targetResp := instance.CreateTargetWithoutPayloadType(isolatedIAMOwnerCTX, t, "", "https://notexisting", target_domain.TargetTypeWebhook, false)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
@@ -54,7 +54,7 @@ func TestServer_GetTarget(t *testing.T) {
|
||||
ctx: isolatedIAMOwnerCTX,
|
||||
dep: func(ctx context.Context, request *action.GetTargetRequest, response *action.GetTargetResponse) error {
|
||||
name := integration.TargetName()
|
||||
resp := instance.CreateTarget(ctx, t, name, "https://example.com", target_domain.TargetTypeWebhook, false)
|
||||
resp := instance.CreateTargetWithoutPayloadType(ctx, t, name, "https://example.com", target_domain.TargetTypeWebhook, false)
|
||||
request.Id = resp.GetId()
|
||||
response.Target.Id = resp.GetId()
|
||||
response.Target.Name = name
|
||||
@@ -81,7 +81,7 @@ func TestServer_GetTarget(t *testing.T) {
|
||||
ctx: isolatedIAMOwnerCTX,
|
||||
dep: func(ctx context.Context, request *action.GetTargetRequest, response *action.GetTargetResponse) error {
|
||||
name := integration.TargetName()
|
||||
resp := instance.CreateTarget(ctx, t, name, "https://example.com", target_domain.TargetTypeAsync, false)
|
||||
resp := instance.CreateTargetWithoutPayloadType(ctx, t, name, "https://example.com", target_domain.TargetTypeAsync, false)
|
||||
request.Id = resp.GetId()
|
||||
response.Target.Id = resp.GetId()
|
||||
response.Target.Name = name
|
||||
@@ -108,7 +108,7 @@ func TestServer_GetTarget(t *testing.T) {
|
||||
ctx: isolatedIAMOwnerCTX,
|
||||
dep: func(ctx context.Context, request *action.GetTargetRequest, response *action.GetTargetResponse) error {
|
||||
name := integration.TargetName()
|
||||
resp := instance.CreateTarget(ctx, t, name, "https://example.com", target_domain.TargetTypeWebhook, true)
|
||||
resp := instance.CreateTargetWithoutPayloadType(ctx, t, name, "https://example.com", target_domain.TargetTypeWebhook, true)
|
||||
request.Id = resp.GetId()
|
||||
response.Target.Id = resp.GetId()
|
||||
response.Target.Name = name
|
||||
@@ -137,7 +137,7 @@ func TestServer_GetTarget(t *testing.T) {
|
||||
ctx: isolatedIAMOwnerCTX,
|
||||
dep: func(ctx context.Context, request *action.GetTargetRequest, response *action.GetTargetResponse) error {
|
||||
name := integration.TargetName()
|
||||
resp := instance.CreateTarget(ctx, t, name, "https://example.com", target_domain.TargetTypeCall, false)
|
||||
resp := instance.CreateTargetWithoutPayloadType(ctx, t, name, "https://example.com", target_domain.TargetTypeCall, false)
|
||||
request.Id = resp.GetId()
|
||||
response.Target.Id = resp.GetId()
|
||||
response.Target.Name = name
|
||||
@@ -166,7 +166,7 @@ func TestServer_GetTarget(t *testing.T) {
|
||||
ctx: isolatedIAMOwnerCTX,
|
||||
dep: func(ctx context.Context, request *action.GetTargetRequest, response *action.GetTargetResponse) error {
|
||||
name := integration.TargetName()
|
||||
resp := instance.CreateTarget(ctx, t, name, "https://example.com", target_domain.TargetTypeCall, true)
|
||||
resp := instance.CreateTargetWithoutPayloadType(ctx, t, name, "https://example.com", target_domain.TargetTypeCall, true)
|
||||
request.Id = resp.GetId()
|
||||
response.Target.Id = resp.GetId()
|
||||
response.Target.Name = name
|
||||
@@ -261,7 +261,7 @@ func TestServer_ListTargets(t *testing.T) {
|
||||
ctx: isolatedIAMOwnerCTX,
|
||||
dep: func(ctx context.Context, request *action.ListTargetsRequest, response *action.ListTargetsResponse) {
|
||||
name := integration.TargetName()
|
||||
resp := instance.CreateTarget(ctx, t, name, "https://example.com", target_domain.TargetTypeWebhook, false)
|
||||
resp := instance.CreateTargetWithoutPayloadType(ctx, t, name, "https://example.com", target_domain.TargetTypeWebhook, false)
|
||||
request.Filters[0].Filter = &action.TargetSearchFilter_InTargetIdsFilter{
|
||||
InTargetIdsFilter: &action.InTargetIDsFilter{
|
||||
TargetIds: []string{resp.GetId()},
|
||||
@@ -301,7 +301,7 @@ func TestServer_ListTargets(t *testing.T) {
|
||||
ctx: isolatedIAMOwnerCTX,
|
||||
dep: func(ctx context.Context, request *action.ListTargetsRequest, response *action.ListTargetsResponse) {
|
||||
name := integration.TargetName()
|
||||
resp := instance.CreateTarget(ctx, t, name, "https://example.com", target_domain.TargetTypeWebhook, false)
|
||||
resp := instance.CreateTargetWithoutPayloadType(ctx, t, name, "https://example.com", target_domain.TargetTypeWebhook, false)
|
||||
request.Filters[0].Filter = &action.TargetSearchFilter_TargetNameFilter{
|
||||
TargetNameFilter: &action.TargetNameFilter{
|
||||
TargetName: name,
|
||||
@@ -344,9 +344,9 @@ func TestServer_ListTargets(t *testing.T) {
|
||||
name1 := integration.TargetName()
|
||||
name2 := integration.TargetName()
|
||||
name3 := integration.TargetName()
|
||||
resp1 := instance.CreateTarget(ctx, t, name1, "https://example.com", target_domain.TargetTypeWebhook, false)
|
||||
resp2 := instance.CreateTarget(ctx, t, name2, "https://example.com", target_domain.TargetTypeCall, true)
|
||||
resp3 := instance.CreateTarget(ctx, t, name3, "https://example.com", target_domain.TargetTypeAsync, false)
|
||||
resp1 := instance.CreateTargetWithoutPayloadType(ctx, t, name1, "https://example.com", target_domain.TargetTypeWebhook, false)
|
||||
resp2 := instance.CreateTargetWithoutPayloadType(ctx, t, name2, "https://example.com", target_domain.TargetTypeCall, true)
|
||||
resp3 := instance.CreateTargetWithoutPayloadType(ctx, t, name3, "https://example.com", target_domain.TargetTypeAsync, false)
|
||||
request.Filters[0].Filter = &action.TargetSearchFilter_InTargetIdsFilter{
|
||||
InTargetIdsFilter: &action.InTargetIDsFilter{
|
||||
TargetIds: []string{resp1.GetId(), resp2.GetId(), resp3.GetId()},
|
||||
@@ -445,7 +445,7 @@ func assertPaginationResponse(t *assert.CollectT, expected *filter.PaginationRes
|
||||
func TestServer_ListExecutions(t *testing.T) {
|
||||
instance := integration.NewInstance(CTX)
|
||||
isolatedIAMOwnerCTX := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner)
|
||||
targetResp := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", target_domain.TargetTypeWebhook, false)
|
||||
targetResp := instance.CreateTargetWithoutPayloadType(isolatedIAMOwnerCTX, t, "", "https://example.com", target_domain.TargetTypeWebhook, false)
|
||||
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
@@ -523,7 +523,7 @@ func TestServer_ListExecutions(t *testing.T) {
|
||||
args: args{
|
||||
ctx: isolatedIAMOwnerCTX,
|
||||
dep: func(ctx context.Context, request *action.ListExecutionsRequest, response *action.ListExecutionsResponse) {
|
||||
target := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", target_domain.TargetTypeWebhook, false)
|
||||
target := instance.CreateTargetWithoutPayloadType(isolatedIAMOwnerCTX, t, "", "https://example.com", target_domain.TargetTypeWebhook, false)
|
||||
// add target as Filter to the request
|
||||
request.Filters[0] = &action.ExecutionSearchFilter{
|
||||
Filter: &action.ExecutionSearchFilter_TargetFilter{
|
||||
|
||||
@@ -25,7 +25,7 @@ func TestServer_CreateTarget(t *testing.T) {
|
||||
signingKey bool
|
||||
}
|
||||
alreadyExistingTargetName := integration.TargetName()
|
||||
instance.CreateTarget(isolatedIAMOwnerCTX, t, alreadyExistingTargetName, "https://example.com", target_domain.TargetTypeAsync, false)
|
||||
instance.CreateTargetWithoutPayloadType(isolatedIAMOwnerCTX, t, alreadyExistingTargetName, "https://example.com", target_domain.TargetTypeAsync, false)
|
||||
tests := []struct {
|
||||
name string
|
||||
ctx context.Context
|
||||
@@ -262,7 +262,7 @@ func TestServer_UpdateTarget(t *testing.T) {
|
||||
{
|
||||
name: "missing permission",
|
||||
prepare: func(request *action.UpdateTargetRequest) {
|
||||
targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", target_domain.TargetTypeWebhook, false).GetId()
|
||||
targetID := instance.CreateTargetWithoutPayloadType(isolatedIAMOwnerCTX, t, "", "https://example.com", target_domain.TargetTypeWebhook, false).GetId()
|
||||
request.Id = targetID
|
||||
},
|
||||
args: args{
|
||||
@@ -289,7 +289,7 @@ func TestServer_UpdateTarget(t *testing.T) {
|
||||
{
|
||||
name: "no change, ok",
|
||||
prepare: func(request *action.UpdateTargetRequest) {
|
||||
targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", target_domain.TargetTypeWebhook, false).GetId()
|
||||
targetID := instance.CreateTargetWithoutPayloadType(isolatedIAMOwnerCTX, t, "", "https://example.com", target_domain.TargetTypeWebhook, false).GetId()
|
||||
request.Id = targetID
|
||||
},
|
||||
args: args{
|
||||
@@ -307,7 +307,7 @@ func TestServer_UpdateTarget(t *testing.T) {
|
||||
{
|
||||
name: "change name, ok",
|
||||
prepare: func(request *action.UpdateTargetRequest) {
|
||||
targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", target_domain.TargetTypeWebhook, false).GetId()
|
||||
targetID := instance.CreateTargetWithoutPayloadType(isolatedIAMOwnerCTX, t, "", "https://example.com", target_domain.TargetTypeWebhook, false).GetId()
|
||||
request.Id = targetID
|
||||
},
|
||||
args: args{
|
||||
@@ -325,7 +325,7 @@ func TestServer_UpdateTarget(t *testing.T) {
|
||||
{
|
||||
name: "regenerate signingkey, ok",
|
||||
prepare: func(request *action.UpdateTargetRequest) {
|
||||
targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", target_domain.TargetTypeWebhook, false).GetId()
|
||||
targetID := instance.CreateTargetWithoutPayloadType(isolatedIAMOwnerCTX, t, "", "https://example.com", target_domain.TargetTypeWebhook, false).GetId()
|
||||
request.Id = targetID
|
||||
},
|
||||
args: args{
|
||||
@@ -343,7 +343,7 @@ func TestServer_UpdateTarget(t *testing.T) {
|
||||
{
|
||||
name: "change type, ok",
|
||||
prepare: func(request *action.UpdateTargetRequest) {
|
||||
targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", target_domain.TargetTypeWebhook, false).GetId()
|
||||
targetID := instance.CreateTargetWithoutPayloadType(isolatedIAMOwnerCTX, t, "", "https://example.com", target_domain.TargetTypeWebhook, false).GetId()
|
||||
request.Id = targetID
|
||||
},
|
||||
args: args{
|
||||
@@ -365,7 +365,7 @@ func TestServer_UpdateTarget(t *testing.T) {
|
||||
{
|
||||
name: "change url, ok",
|
||||
prepare: func(request *action.UpdateTargetRequest) {
|
||||
targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", target_domain.TargetTypeWebhook, false).GetId()
|
||||
targetID := instance.CreateTargetWithoutPayloadType(isolatedIAMOwnerCTX, t, "", "https://example.com", target_domain.TargetTypeWebhook, false).GetId()
|
||||
request.Id = targetID
|
||||
},
|
||||
args: args{
|
||||
@@ -383,7 +383,7 @@ func TestServer_UpdateTarget(t *testing.T) {
|
||||
{
|
||||
name: "change timeout, ok",
|
||||
prepare: func(request *action.UpdateTargetRequest) {
|
||||
targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", target_domain.TargetTypeWebhook, false).GetId()
|
||||
targetID := instance.CreateTargetWithoutPayloadType(isolatedIAMOwnerCTX, t, "", "https://example.com", target_domain.TargetTypeWebhook, false).GetId()
|
||||
request.Id = targetID
|
||||
},
|
||||
args: args{
|
||||
@@ -401,7 +401,7 @@ func TestServer_UpdateTarget(t *testing.T) {
|
||||
{
|
||||
name: "change type async, ok",
|
||||
prepare: func(request *action.UpdateTargetRequest) {
|
||||
targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", target_domain.TargetTypeAsync, false).GetId()
|
||||
targetID := instance.CreateTargetWithoutPayloadType(isolatedIAMOwnerCTX, t, "", "https://example.com", target_domain.TargetTypeAsync, false).GetId()
|
||||
request.Id = targetID
|
||||
},
|
||||
args: args{
|
||||
@@ -497,7 +497,7 @@ func TestServer_DeleteTarget(t *testing.T) {
|
||||
ctx: iamOwnerCtx,
|
||||
prepare: func(request *action.DeleteTargetRequest) (time.Time, time.Time) {
|
||||
creationDate := time.Now().UTC()
|
||||
targetID := instance.CreateTarget(iamOwnerCtx, t, "", "https://example.com", target_domain.TargetTypeWebhook, false).GetId()
|
||||
targetID := instance.CreateTargetWithoutPayloadType(iamOwnerCtx, t, "", "https://example.com", target_domain.TargetTypeWebhook, false).GetId()
|
||||
request.Id = targetID
|
||||
return creationDate, time.Time{}
|
||||
},
|
||||
@@ -509,7 +509,7 @@ func TestServer_DeleteTarget(t *testing.T) {
|
||||
ctx: iamOwnerCtx,
|
||||
prepare: func(request *action.DeleteTargetRequest) (time.Time, time.Time) {
|
||||
creationDate := time.Now().UTC()
|
||||
targetID := instance.CreateTarget(iamOwnerCtx, t, "", "https://example.com", target_domain.TargetTypeWebhook, false).GetId()
|
||||
targetID := instance.CreateTargetWithoutPayloadType(iamOwnerCtx, t, "", "https://example.com", target_domain.TargetTypeWebhook, false).GetId()
|
||||
request.Id = targetID
|
||||
instance.DeleteTarget(iamOwnerCtx, t, targetID)
|
||||
return creationDate, time.Now().UTC()
|
||||
|
||||
@@ -26,12 +26,12 @@ var headersToForward = map[string]bool{
|
||||
strings.ToLower(http_utils.Origin): true,
|
||||
}
|
||||
|
||||
func ExecutionHandler(alg crypto.EncryptionAlgorithm) connect.UnaryInterceptorFunc {
|
||||
func ExecutionHandler(alg crypto.EncryptionAlgorithm, activeSigningKey execution.GetActiveSigningWebKey) connect.UnaryInterceptorFunc {
|
||||
return func(handler connect.UnaryFunc) connect.UnaryFunc {
|
||||
return func(ctx context.Context, req connect.AnyRequest) (_ connect.AnyResponse, err error) {
|
||||
|
||||
requestTargets := execution.QueryExecutionTargetsForRequest(ctx, req.Spec().Procedure)
|
||||
handledReq, err := executeTargetsForRequest(ctx, requestTargets, req.Spec().Procedure, req, alg)
|
||||
handledReq, err := executeTargetsForRequest(ctx, requestTargets, req.Spec().Procedure, req, alg, activeSigningKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -42,12 +42,12 @@ func ExecutionHandler(alg crypto.EncryptionAlgorithm) connect.UnaryInterceptorFu
|
||||
}
|
||||
|
||||
responseTargets := execution.QueryExecutionTargetsForResponse(ctx, req.Spec().Procedure)
|
||||
return executeTargetsForResponse(ctx, responseTargets, req.Spec().Procedure, handledReq, response, alg)
|
||||
return executeTargetsForResponse(ctx, responseTargets, req.Spec().Procedure, handledReq, response, alg, activeSigningKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func executeTargetsForRequest(ctx context.Context, targets []target_domain.Target, fullMethod string, req connect.AnyRequest, alg crypto.EncryptionAlgorithm) (_ connect.AnyRequest, err error) {
|
||||
func executeTargetsForRequest(ctx context.Context, targets []target_domain.Target, fullMethod string, req connect.AnyRequest, alg crypto.EncryptionAlgorithm, activeSigningKey execution.GetActiveSigningWebKey) (_ connect.AnyRequest, err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
|
||||
@@ -67,14 +67,14 @@ func executeTargetsForRequest(ctx context.Context, targets []target_domain.Targe
|
||||
Headers: SetRequestHeaders(req.Header()),
|
||||
}
|
||||
|
||||
_, err = execution.CallTargets(ctx, targets, info, alg)
|
||||
_, err = execution.CallTargets(ctx, targets, info, alg, activeSigningKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func executeTargetsForResponse(ctx context.Context, targets []target_domain.Target, fullMethod string, req connect.AnyRequest, resp connect.AnyResponse, alg crypto.EncryptionAlgorithm) (_ connect.AnyResponse, err error) {
|
||||
func executeTargetsForResponse(ctx context.Context, targets []target_domain.Target, fullMethod string, req connect.AnyRequest, resp connect.AnyResponse, alg crypto.EncryptionAlgorithm, activeSigningKey execution.GetActiveSigningWebKey) (_ connect.AnyResponse, err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
|
||||
@@ -95,7 +95,7 @@ func executeTargetsForResponse(ctx context.Context, targets []target_domain.Targ
|
||||
Headers: SetRequestHeaders(req.Header()),
|
||||
}
|
||||
|
||||
_, err = execution.CallTargets(ctx, targets, info, alg)
|
||||
_, err = execution.CallTargets(ctx, targets, info, alg, activeSigningKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -1,25 +1,43 @@
|
||||
package connect_middleware
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"connectrpc.com/connect"
|
||||
"github.com/go-jose/go-jose/v4"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/protobuf/encoding/protojson"
|
||||
"google.golang.org/protobuf/proto"
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/execution"
|
||||
target_domain "github.com/zitadel/zitadel/internal/execution/target"
|
||||
)
|
||||
|
||||
var (
|
||||
privateKey = func() *rsa.PrivateKey {
|
||||
privateKey, _ := rsa.GenerateKey(rand.Reader, 2048)
|
||||
return privateKey
|
||||
}()
|
||||
encryptionKey = func() []byte {
|
||||
data, _ := crypto.PublicKeyToBytes(&privateKey.PublicKey)
|
||||
return data
|
||||
}()
|
||||
encryptionKeyID = "encryption-key-id"
|
||||
signingAlgorithm = jose.RS256
|
||||
)
|
||||
|
||||
func newMockContentRequest(content string) *connect.Request[structpb.Struct] {
|
||||
return connect.NewRequest(&structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
@@ -57,18 +75,20 @@ func newMockContextInfoResponse(fullMethod, request, response string) *ContextIn
|
||||
|
||||
func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
|
||||
type target struct {
|
||||
reqBody execution.ContextInfo
|
||||
sleep time.Duration
|
||||
statusCode int
|
||||
respBody connect.AnyResponse
|
||||
reqBody execution.ContextInfo
|
||||
sleep time.Duration
|
||||
statusCode int
|
||||
respBody connect.AnyResponse
|
||||
requestVerification func(*testing.T) func([]byte, []byte) bool
|
||||
}
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
|
||||
executionTargets []target_domain.Target
|
||||
targets []target
|
||||
fullMethod string
|
||||
req connect.AnyRequest
|
||||
executionTargets []target_domain.Target
|
||||
targets []target
|
||||
fullMethod string
|
||||
req connect.AnyRequest
|
||||
getActiveSigningWebKey execution.GetActiveSigningWebKey
|
||||
}
|
||||
type res struct {
|
||||
want interface{}
|
||||
@@ -139,10 +159,11 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
|
||||
},
|
||||
targets: []target{
|
||||
{
|
||||
reqBody: newMockContextInfoRequest("/service/method", "content"),
|
||||
respBody: newMockContentResponse("content1"),
|
||||
sleep: 0,
|
||||
statusCode: http.StatusBadRequest,
|
||||
reqBody: newMockContextInfoRequest("/service/method", "content"),
|
||||
respBody: newMockContentResponse("content1"),
|
||||
sleep: 0,
|
||||
statusCode: http.StatusBadRequest,
|
||||
requestVerification: validateJSONPayload,
|
||||
},
|
||||
},
|
||||
req: newMockContentRequest("content"),
|
||||
@@ -168,10 +189,11 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
|
||||
|
||||
targets: []target{
|
||||
{
|
||||
reqBody: newMockContextInfoRequest("/service/method", "content"),
|
||||
respBody: newMockContentResponse("content1"),
|
||||
sleep: 0,
|
||||
statusCode: http.StatusBadRequest,
|
||||
reqBody: newMockContextInfoRequest("/service/method", "content"),
|
||||
respBody: newMockContentResponse("content1"),
|
||||
sleep: 0,
|
||||
statusCode: http.StatusBadRequest,
|
||||
requestVerification: validateJSONPayload,
|
||||
},
|
||||
},
|
||||
req: newMockContentRequest("content"),
|
||||
@@ -196,10 +218,11 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
|
||||
},
|
||||
targets: []target{
|
||||
{
|
||||
reqBody: newMockContextInfoRequest("/service/method", "content"),
|
||||
respBody: newMockContentResponse("content1"),
|
||||
sleep: 5 * time.Second,
|
||||
statusCode: http.StatusOK,
|
||||
reqBody: newMockContextInfoRequest("/service/method", "content"),
|
||||
respBody: newMockContentResponse("content1"),
|
||||
sleep: 5 * time.Second,
|
||||
statusCode: http.StatusOK,
|
||||
requestVerification: validateJSONPayload,
|
||||
},
|
||||
},
|
||||
req: newMockContentRequest("content"),
|
||||
@@ -223,7 +246,10 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
|
||||
},
|
||||
},
|
||||
targets: []target{
|
||||
{reqBody: newMockContextInfoRequest("/service/method", "wrong")},
|
||||
{
|
||||
reqBody: newMockContextInfoRequest("/service/method", "wrong"),
|
||||
requestVerification: validateJSONPayload,
|
||||
},
|
||||
},
|
||||
req: newMockContentRequest("content"),
|
||||
},
|
||||
@@ -247,10 +273,11 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
|
||||
},
|
||||
targets: []target{
|
||||
{
|
||||
reqBody: newMockContextInfoRequest("/service/method", "content"),
|
||||
respBody: newMockContentResponse("content1"),
|
||||
sleep: 0,
|
||||
statusCode: http.StatusOK,
|
||||
reqBody: newMockContextInfoRequest("/service/method", "content"),
|
||||
respBody: newMockContentResponse("content1"),
|
||||
sleep: 0,
|
||||
statusCode: http.StatusOK,
|
||||
requestVerification: validateJSONPayload,
|
||||
},
|
||||
},
|
||||
req: newMockContentRequest("content"),
|
||||
@@ -274,10 +301,11 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
|
||||
},
|
||||
targets: []target{
|
||||
{
|
||||
reqBody: newMockContextInfoRequest("/service/method", "content"),
|
||||
respBody: newMockContentResponse("content1"),
|
||||
sleep: 5 * time.Second,
|
||||
statusCode: http.StatusOK,
|
||||
reqBody: newMockContextInfoRequest("/service/method", "content"),
|
||||
respBody: newMockContentResponse("content1"),
|
||||
sleep: 5 * time.Second,
|
||||
statusCode: http.StatusOK,
|
||||
requestVerification: validateJSONPayload,
|
||||
},
|
||||
},
|
||||
req: newMockContentRequest("content"),
|
||||
@@ -301,10 +329,11 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
|
||||
},
|
||||
targets: []target{
|
||||
{
|
||||
reqBody: newMockContextInfoRequest("/service/method", "content"),
|
||||
respBody: newMockContentResponse("content1"),
|
||||
sleep: 0,
|
||||
statusCode: http.StatusOK,
|
||||
reqBody: newMockContextInfoRequest("/service/method", "content"),
|
||||
respBody: newMockContentResponse("content1"),
|
||||
sleep: 0,
|
||||
statusCode: http.StatusOK,
|
||||
requestVerification: validateJSONPayload,
|
||||
},
|
||||
},
|
||||
req: newMockContentRequest("content"),
|
||||
@@ -329,9 +358,10 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
|
||||
},
|
||||
targets: []target{
|
||||
{
|
||||
reqBody: newMockContextInfoRequest("/service/method", "content"),
|
||||
sleep: 0,
|
||||
statusCode: http.StatusInternalServerError,
|
||||
reqBody: newMockContextInfoRequest("/service/method", "content"),
|
||||
sleep: 0,
|
||||
statusCode: http.StatusInternalServerError,
|
||||
requestVerification: validateJSONPayload,
|
||||
},
|
||||
},
|
||||
req: newMockContentRequest("content"),
|
||||
@@ -356,10 +386,11 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
|
||||
},
|
||||
targets: []target{
|
||||
{
|
||||
reqBody: newMockContextInfoRequest("/service/method", "content"),
|
||||
respBody: newMockContentResponse("content1"),
|
||||
sleep: 5 * time.Second,
|
||||
statusCode: http.StatusOK,
|
||||
reqBody: newMockContextInfoRequest("/service/method", "content"),
|
||||
respBody: newMockContentResponse("content1"),
|
||||
sleep: 5 * time.Second,
|
||||
statusCode: http.StatusOK,
|
||||
requestVerification: validateJSONPayload,
|
||||
},
|
||||
},
|
||||
req: newMockContentRequest("content"),
|
||||
@@ -384,10 +415,11 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
|
||||
},
|
||||
targets: []target{
|
||||
{
|
||||
reqBody: newMockContextInfoRequest("/service/method", "content"),
|
||||
respBody: newMockContentResponse("content1"),
|
||||
sleep: 0,
|
||||
statusCode: http.StatusOK,
|
||||
reqBody: newMockContextInfoRequest("/service/method", "content"),
|
||||
respBody: newMockContentResponse("content1"),
|
||||
sleep: 0,
|
||||
statusCode: http.StatusOK,
|
||||
requestVerification: validateJSONPayload,
|
||||
},
|
||||
},
|
||||
req: newMockContentRequest("content"),
|
||||
@@ -427,22 +459,25 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
|
||||
|
||||
targets: []target{
|
||||
{
|
||||
reqBody: newMockContextInfoRequest("/service/method", "content"),
|
||||
respBody: newMockContentResponse("content1"),
|
||||
sleep: 0,
|
||||
statusCode: http.StatusOK,
|
||||
reqBody: newMockContextInfoRequest("/service/method", "content"),
|
||||
respBody: newMockContentResponse("content1"),
|
||||
sleep: 0,
|
||||
statusCode: http.StatusOK,
|
||||
requestVerification: validateJSONPayload,
|
||||
},
|
||||
{
|
||||
reqBody: newMockContextInfoRequest("/service/method", "content1"),
|
||||
respBody: newMockContentResponse("content2"),
|
||||
sleep: 0,
|
||||
statusCode: http.StatusBadRequest,
|
||||
reqBody: newMockContextInfoRequest("/service/method", "content1"),
|
||||
respBody: newMockContentResponse("content2"),
|
||||
sleep: 0,
|
||||
statusCode: http.StatusBadRequest,
|
||||
requestVerification: validateJSONPayload,
|
||||
},
|
||||
{
|
||||
reqBody: newMockContextInfoRequest("/service/method", "content2"),
|
||||
respBody: newMockContentResponse("content3"),
|
||||
sleep: 0,
|
||||
statusCode: http.StatusOK,
|
||||
reqBody: newMockContextInfoRequest("/service/method", "content2"),
|
||||
respBody: newMockContentResponse("content3"),
|
||||
sleep: 0,
|
||||
statusCode: http.StatusOK,
|
||||
requestVerification: validateJSONPayload,
|
||||
},
|
||||
},
|
||||
req: newMockContentRequest("content"),
|
||||
@@ -481,22 +516,25 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
|
||||
},
|
||||
targets: []target{
|
||||
{
|
||||
reqBody: newMockContextInfoRequest("/service/method", "content"),
|
||||
respBody: newMockContentResponse("content1"),
|
||||
sleep: 0,
|
||||
statusCode: http.StatusOK,
|
||||
reqBody: newMockContextInfoRequest("/service/method", "content"),
|
||||
respBody: newMockContentResponse("content1"),
|
||||
sleep: 0,
|
||||
statusCode: http.StatusOK,
|
||||
requestVerification: validateJSONPayload,
|
||||
},
|
||||
{
|
||||
reqBody: newMockContextInfoRequest("/service/method", "content1"),
|
||||
respBody: newMockContentResponse("content2"),
|
||||
sleep: 5 * time.Second,
|
||||
statusCode: http.StatusBadRequest,
|
||||
reqBody: newMockContextInfoRequest("/service/method", "content1"),
|
||||
respBody: newMockContentResponse("content2"),
|
||||
sleep: 5 * time.Second,
|
||||
statusCode: http.StatusBadRequest,
|
||||
requestVerification: validateJSONPayload,
|
||||
},
|
||||
{
|
||||
reqBody: newMockContextInfoRequest("/service/method", "content2"),
|
||||
respBody: newMockContentResponse("content3"),
|
||||
sleep: 5 * time.Second,
|
||||
statusCode: http.StatusOK,
|
||||
reqBody: newMockContextInfoRequest("/service/method", "content2"),
|
||||
respBody: newMockContentResponse("content3"),
|
||||
sleep: 5 * time.Second,
|
||||
statusCode: http.StatusOK,
|
||||
requestVerification: validateJSONPayload,
|
||||
},
|
||||
},
|
||||
req: newMockContentRequest("content"),
|
||||
@@ -505,6 +543,70 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
|
||||
wantErr: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
"payload JWT, ok",
|
||||
args{
|
||||
ctx: context.Background(),
|
||||
fullMethod: "/service/method",
|
||||
executionTargets: []target_domain.Target{
|
||||
{
|
||||
ExecutionID: "request./zitadel.session.v2.SessionService/SetSession",
|
||||
TargetID: "target",
|
||||
TargetType: target_domain.TargetTypeWebhook,
|
||||
Timeout: time.Minute,
|
||||
InterruptOnError: true,
|
||||
PayloadType: target_domain.PayloadTypeJWT,
|
||||
},
|
||||
},
|
||||
targets: []target{
|
||||
{
|
||||
reqBody: newMockContextInfoRequest("/service/method", "content"),
|
||||
respBody: newMockContentResponse("content1"),
|
||||
sleep: 0,
|
||||
statusCode: http.StatusOK,
|
||||
requestVerification: validateJWTPayload,
|
||||
},
|
||||
},
|
||||
req: newMockContentRequest("content"),
|
||||
getActiveSigningWebKey: mockGetActiveSigningWebKey(),
|
||||
},
|
||||
res{
|
||||
want: newMockContentRequest("content"),
|
||||
},
|
||||
},
|
||||
{
|
||||
"payload JWE, ok",
|
||||
args{
|
||||
ctx: context.Background(),
|
||||
fullMethod: "/service/method",
|
||||
executionTargets: []target_domain.Target{
|
||||
{
|
||||
ExecutionID: "request./zitadel.session.v2.SessionService/SetSession",
|
||||
TargetID: "target",
|
||||
TargetType: target_domain.TargetTypeWebhook,
|
||||
Timeout: time.Minute,
|
||||
InterruptOnError: true,
|
||||
PayloadType: target_domain.PayloadTypeJWE,
|
||||
EncryptionKey: encryptionKey,
|
||||
EncryptionKeyID: encryptionKeyID,
|
||||
},
|
||||
},
|
||||
targets: []target{
|
||||
{
|
||||
reqBody: newMockContextInfoRequest("/service/method", "content"),
|
||||
respBody: newMockContentResponse("content1"),
|
||||
sleep: 0,
|
||||
statusCode: http.StatusOK,
|
||||
requestVerification: validateJWEPayload,
|
||||
},
|
||||
},
|
||||
req: newMockContentRequest("content"),
|
||||
getActiveSigningWebKey: mockGetActiveSigningWebKey(),
|
||||
},
|
||||
res{
|
||||
want: newMockContentRequest("content"),
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
@@ -515,6 +617,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
|
||||
target.sleep,
|
||||
target.statusCode,
|
||||
target.respBody,
|
||||
target.requestVerification(t),
|
||||
)
|
||||
|
||||
tt.args.executionTargets[i].Endpoint = url
|
||||
@@ -527,6 +630,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
|
||||
tt.args.fullMethod,
|
||||
tt.args.req,
|
||||
nil,
|
||||
tt.args.getActiveSigningWebKey,
|
||||
)
|
||||
|
||||
if tt.res.wantErr {
|
||||
@@ -548,6 +652,7 @@ func testServerCall(
|
||||
sleep time.Duration,
|
||||
statusCode int,
|
||||
respBody connect.AnyResponse,
|
||||
requestVerification func(expected, sent []byte) bool,
|
||||
) (string, func()) {
|
||||
handler := func(w http.ResponseWriter, r *http.Request) {
|
||||
data, err := json.Marshal(reqBody)
|
||||
@@ -562,7 +667,7 @@ func testServerCall(
|
||||
return
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(data, sentBody) {
|
||||
if !requestVerification(data, sentBody) {
|
||||
http.Error(w, "error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
@@ -591,6 +696,45 @@ func testServerCall(
|
||||
return server.URL, server.Close
|
||||
}
|
||||
|
||||
func mockGetActiveSigningWebKey() func(ctx context.Context) (*jose.JSONWebKey, error) {
|
||||
return func(ctx context.Context) (*jose.JSONWebKey, error) {
|
||||
return &jose.JSONWebKey{
|
||||
Key: privateKey,
|
||||
Algorithm: string(signingAlgorithm),
|
||||
Use: "sig",
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func validateJSONPayload(t *testing.T) func(expected, sent []byte) bool {
|
||||
return bytes.Equal
|
||||
}
|
||||
|
||||
func validateJWTPayload(t *testing.T) func(expected, sent []byte) bool {
|
||||
return func(expected, sent []byte) bool {
|
||||
jws, err := jose.ParseSigned(string(sent), []jose.SignatureAlgorithm{jose.RS256})
|
||||
require.NoError(t, err)
|
||||
payload, err := jws.Verify(privateKey.Public())
|
||||
require.NoError(t, err)
|
||||
return bytes.Equal(expected, payload)
|
||||
}
|
||||
}
|
||||
|
||||
func validateJWEPayload(t *testing.T) func(expected, sent []byte) bool {
|
||||
return func(expected, sent []byte) bool {
|
||||
parsedJWE, err := jose.ParseEncrypted(string(sent), []jose.KeyAlgorithm{jose.RSA_OAEP_256, jose.ECDH_ES_A256KW}, []jose.ContentEncryption{jose.A256GCM})
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
require.Equal(t, encryptionKeyID, parsedJWE.Header.KeyID)
|
||||
require.Equal(t, "JWT", parsedJWE.Header.ExtraHeaders[jose.HeaderContentType].(string))
|
||||
|
||||
decryptedJWS, err := parsedJWE.Decrypt(privateKey)
|
||||
require.NoError(t, err)
|
||||
return validateJWTPayload(t)(expected, decryptedJWS)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_executeTargetsForGRPCFullMethod_response(t *testing.T) {
|
||||
type target struct {
|
||||
reqBody execution.ContextInfo
|
||||
@@ -710,6 +854,7 @@ func Test_executeTargetsForGRPCFullMethod_response(t *testing.T) {
|
||||
target.sleep,
|
||||
target.statusCode,
|
||||
target.respBody,
|
||||
validateJSONPayload(t),
|
||||
)
|
||||
|
||||
tt.args.executionTargets[i].Endpoint = url
|
||||
@@ -723,6 +868,7 @@ func Test_executeTargetsForGRPCFullMethod_response(t *testing.T) {
|
||||
tt.args.req,
|
||||
tt.args.resp,
|
||||
nil,
|
||||
mockGetActiveSigningWebKey(),
|
||||
)
|
||||
|
||||
if tt.res.wantErr {
|
||||
|
||||
@@ -18,11 +18,11 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/telemetry/tracing"
|
||||
)
|
||||
|
||||
func ExecutionHandler(alg crypto.EncryptionAlgorithm) grpc.UnaryServerInterceptor {
|
||||
func ExecutionHandler(alg crypto.EncryptionAlgorithm, activeSigningKey execution.GetActiveSigningWebKey) grpc.UnaryServerInterceptor {
|
||||
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
|
||||
requestTargets := execution.QueryExecutionTargetsForRequest(ctx, info.FullMethod)
|
||||
// call targets otherwise return req
|
||||
handledReq, err := executeTargetsForRequest(ctx, requestTargets, info.FullMethod, req, alg)
|
||||
handledReq, err := executeTargetsForRequest(ctx, requestTargets, info.FullMethod, req, alg, activeSigningKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -33,11 +33,11 @@ func ExecutionHandler(alg crypto.EncryptionAlgorithm) grpc.UnaryServerIntercepto
|
||||
}
|
||||
|
||||
responseTargets := execution.QueryExecutionTargetsForResponse(ctx, info.FullMethod)
|
||||
return executeTargetsForResponse(ctx, responseTargets, info.FullMethod, handledReq, response, alg)
|
||||
return executeTargetsForResponse(ctx, responseTargets, info.FullMethod, handledReq, response, alg, activeSigningKey)
|
||||
}
|
||||
}
|
||||
|
||||
func executeTargetsForRequest(ctx context.Context, targets []target_domain.Target, fullMethod string, req interface{}, alg crypto.EncryptionAlgorithm) (_ interface{}, err error) {
|
||||
func executeTargetsForRequest(ctx context.Context, targets []target_domain.Target, fullMethod string, req interface{}, alg crypto.EncryptionAlgorithm, activeSigningKey execution.GetActiveSigningWebKey) (_ interface{}, err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
|
||||
@@ -58,10 +58,10 @@ func executeTargetsForRequest(ctx context.Context, targets []target_domain.Targe
|
||||
Headers: connect_middleware.SetRequestHeaders(md),
|
||||
}
|
||||
|
||||
return execution.CallTargets(ctx, targets, info, alg)
|
||||
return execution.CallTargets(ctx, targets, info, alg, activeSigningKey)
|
||||
}
|
||||
|
||||
func executeTargetsForResponse(ctx context.Context, targets []target_domain.Target, fullMethod string, req, resp interface{}, alg crypto.EncryptionAlgorithm) (_ interface{}, err error) {
|
||||
func executeTargetsForResponse(ctx context.Context, targets []target_domain.Target, fullMethod string, req, resp interface{}, alg crypto.EncryptionAlgorithm, activeSigningKey execution.GetActiveSigningWebKey) (_ interface{}, err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
|
||||
@@ -83,7 +83,7 @@ func executeTargetsForResponse(ctx context.Context, targets []target_domain.Targ
|
||||
Headers: connect_middleware.SetRequestHeaders(md),
|
||||
}
|
||||
|
||||
return execution.CallTargets(ctx, targets, info, alg)
|
||||
return execution.CallTargets(ctx, targets, info, alg, activeSigningKey)
|
||||
}
|
||||
|
||||
var _ execution.ContextInfo = &ContextInfoRequest{}
|
||||
|
||||
@@ -1,24 +1,42 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-jose/go-jose/v4"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/protobuf/encoding/protojson"
|
||||
"google.golang.org/protobuf/proto"
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/execution"
|
||||
target_domain "github.com/zitadel/zitadel/internal/execution/target"
|
||||
)
|
||||
|
||||
var (
|
||||
privateKey = func() *rsa.PrivateKey {
|
||||
privateKey, _ := rsa.GenerateKey(rand.Reader, 2048)
|
||||
return privateKey
|
||||
}()
|
||||
encryptionKey = func() []byte {
|
||||
data, _ := crypto.PublicKeyToBytes(&privateKey.PublicKey)
|
||||
return data
|
||||
}()
|
||||
encryptionKeyID = "encryption-key-id"
|
||||
signingAlgorithm = jose.RS256
|
||||
)
|
||||
|
||||
func newMockContentRequest(content string) proto.Message {
|
||||
return &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
@@ -46,18 +64,20 @@ func newMockContextInfoResponse(fullMethod, request, response string) *ContextIn
|
||||
|
||||
func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
|
||||
type target struct {
|
||||
reqBody execution.ContextInfo
|
||||
sleep time.Duration
|
||||
statusCode int
|
||||
respBody interface{}
|
||||
reqBody execution.ContextInfo
|
||||
sleep time.Duration
|
||||
statusCode int
|
||||
respBody interface{}
|
||||
requestVerification func(*testing.T) func([]byte, []byte) bool
|
||||
}
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
|
||||
executionTargets []target_domain.Target
|
||||
targets []target
|
||||
fullMethod string
|
||||
req interface{}
|
||||
executionTargets []target_domain.Target
|
||||
targets []target
|
||||
fullMethod string
|
||||
req interface{}
|
||||
getActiveSigningWebKey execution.GetActiveSigningWebKey
|
||||
}
|
||||
type res struct {
|
||||
want interface{}
|
||||
@@ -128,10 +148,11 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
|
||||
},
|
||||
targets: []target{
|
||||
{
|
||||
reqBody: newMockContextInfoRequest("/service/method", "content"),
|
||||
respBody: newMockContentRequest("content1"),
|
||||
sleep: 0,
|
||||
statusCode: http.StatusBadRequest,
|
||||
reqBody: newMockContextInfoRequest("/service/method", "content"),
|
||||
respBody: newMockContentRequest("content1"),
|
||||
sleep: 0,
|
||||
statusCode: http.StatusBadRequest,
|
||||
requestVerification: validateJSONPayload,
|
||||
},
|
||||
},
|
||||
req: newMockContentRequest("content"),
|
||||
@@ -157,10 +178,11 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
|
||||
|
||||
targets: []target{
|
||||
{
|
||||
reqBody: newMockContextInfoRequest("/service/method", "content"),
|
||||
respBody: newMockContentRequest("content1"),
|
||||
sleep: 0,
|
||||
statusCode: http.StatusBadRequest,
|
||||
reqBody: newMockContextInfoRequest("/service/method", "content"),
|
||||
respBody: newMockContentRequest("content1"),
|
||||
sleep: 0,
|
||||
statusCode: http.StatusBadRequest,
|
||||
requestVerification: validateJSONPayload,
|
||||
},
|
||||
},
|
||||
req: newMockContentRequest("content"),
|
||||
@@ -185,10 +207,11 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
|
||||
},
|
||||
targets: []target{
|
||||
{
|
||||
reqBody: newMockContextInfoRequest("/service/method", "content"),
|
||||
respBody: newMockContentRequest("content1"),
|
||||
sleep: 5 * time.Second,
|
||||
statusCode: http.StatusOK,
|
||||
reqBody: newMockContextInfoRequest("/service/method", "content"),
|
||||
respBody: newMockContentRequest("content1"),
|
||||
sleep: 5 * time.Second,
|
||||
statusCode: http.StatusOK,
|
||||
requestVerification: validateJSONPayload,
|
||||
},
|
||||
},
|
||||
req: newMockContentRequest("content"),
|
||||
@@ -212,7 +235,10 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
|
||||
},
|
||||
},
|
||||
targets: []target{
|
||||
{reqBody: newMockContextInfoRequest("/service/method", "wrong")},
|
||||
{
|
||||
reqBody: newMockContextInfoRequest("/service/method", "wrong"),
|
||||
requestVerification: validateJSONPayload,
|
||||
},
|
||||
},
|
||||
req: newMockContentRequest("content"),
|
||||
},
|
||||
@@ -236,10 +262,11 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
|
||||
},
|
||||
targets: []target{
|
||||
{
|
||||
reqBody: newMockContextInfoRequest("/service/method", "content"),
|
||||
respBody: newMockContentRequest("content1"),
|
||||
sleep: 0,
|
||||
statusCode: http.StatusOK,
|
||||
reqBody: newMockContextInfoRequest("/service/method", "content"),
|
||||
respBody: newMockContentRequest("content1"),
|
||||
sleep: 0,
|
||||
statusCode: http.StatusOK,
|
||||
requestVerification: validateJSONPayload,
|
||||
},
|
||||
},
|
||||
req: newMockContentRequest("content"),
|
||||
@@ -263,10 +290,11 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
|
||||
},
|
||||
targets: []target{
|
||||
{
|
||||
reqBody: newMockContextInfoRequest("/service/method", "content"),
|
||||
respBody: newMockContentRequest("content1"),
|
||||
sleep: 5 * time.Second,
|
||||
statusCode: http.StatusOK,
|
||||
reqBody: newMockContextInfoRequest("/service/method", "content"),
|
||||
respBody: newMockContentRequest("content1"),
|
||||
sleep: 5 * time.Second,
|
||||
statusCode: http.StatusOK,
|
||||
requestVerification: validateJSONPayload,
|
||||
},
|
||||
},
|
||||
req: newMockContentRequest("content"),
|
||||
@@ -290,10 +318,11 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
|
||||
},
|
||||
targets: []target{
|
||||
{
|
||||
reqBody: newMockContextInfoRequest("/service/method", "content"),
|
||||
respBody: newMockContentRequest("content1"),
|
||||
sleep: 0,
|
||||
statusCode: http.StatusOK,
|
||||
reqBody: newMockContextInfoRequest("/service/method", "content"),
|
||||
respBody: newMockContentRequest("content1"),
|
||||
sleep: 0,
|
||||
statusCode: http.StatusOK,
|
||||
requestVerification: validateJSONPayload,
|
||||
},
|
||||
},
|
||||
req: newMockContentRequest("content"),
|
||||
@@ -318,9 +347,10 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
|
||||
},
|
||||
targets: []target{
|
||||
{
|
||||
reqBody: newMockContextInfoRequest("/service/method", "content"),
|
||||
sleep: 0,
|
||||
statusCode: http.StatusInternalServerError,
|
||||
reqBody: newMockContextInfoRequest("/service/method", "content"),
|
||||
sleep: 0,
|
||||
statusCode: http.StatusInternalServerError,
|
||||
requestVerification: validateJSONPayload,
|
||||
},
|
||||
},
|
||||
req: newMockContentRequest("content"),
|
||||
@@ -345,10 +375,11 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
|
||||
},
|
||||
targets: []target{
|
||||
{
|
||||
reqBody: newMockContextInfoRequest("/service/method", "content"),
|
||||
respBody: newMockContentRequest("content1"),
|
||||
sleep: 5 * time.Second,
|
||||
statusCode: http.StatusOK,
|
||||
reqBody: newMockContextInfoRequest("/service/method", "content"),
|
||||
respBody: newMockContentRequest("content1"),
|
||||
sleep: 5 * time.Second,
|
||||
statusCode: http.StatusOK,
|
||||
requestVerification: validateJSONPayload,
|
||||
},
|
||||
},
|
||||
req: newMockContentRequest("content"),
|
||||
@@ -373,10 +404,11 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
|
||||
},
|
||||
targets: []target{
|
||||
{
|
||||
reqBody: newMockContextInfoRequest("/service/method", "content"),
|
||||
respBody: newMockContentRequest("content1"),
|
||||
sleep: 0,
|
||||
statusCode: http.StatusOK,
|
||||
reqBody: newMockContextInfoRequest("/service/method", "content"),
|
||||
respBody: newMockContentRequest("content1"),
|
||||
sleep: 0,
|
||||
statusCode: http.StatusOK,
|
||||
requestVerification: validateJSONPayload,
|
||||
},
|
||||
},
|
||||
req: newMockContentRequest("content"),
|
||||
@@ -416,22 +448,25 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
|
||||
|
||||
targets: []target{
|
||||
{
|
||||
reqBody: newMockContextInfoRequest("/service/method", "content"),
|
||||
respBody: newMockContentRequest("content1"),
|
||||
sleep: 0,
|
||||
statusCode: http.StatusOK,
|
||||
reqBody: newMockContextInfoRequest("/service/method", "content"),
|
||||
respBody: newMockContentRequest("content1"),
|
||||
sleep: 0,
|
||||
statusCode: http.StatusOK,
|
||||
requestVerification: validateJSONPayload,
|
||||
},
|
||||
{
|
||||
reqBody: newMockContextInfoRequest("/service/method", "content1"),
|
||||
respBody: newMockContentRequest("content2"),
|
||||
sleep: 0,
|
||||
statusCode: http.StatusBadRequest,
|
||||
reqBody: newMockContextInfoRequest("/service/method", "content1"),
|
||||
respBody: newMockContentRequest("content2"),
|
||||
sleep: 0,
|
||||
statusCode: http.StatusBadRequest,
|
||||
requestVerification: validateJSONPayload,
|
||||
},
|
||||
{
|
||||
reqBody: newMockContextInfoRequest("/service/method", "content2"),
|
||||
respBody: newMockContentRequest("content3"),
|
||||
sleep: 0,
|
||||
statusCode: http.StatusOK,
|
||||
reqBody: newMockContextInfoRequest("/service/method", "content2"),
|
||||
respBody: newMockContentRequest("content3"),
|
||||
sleep: 0,
|
||||
statusCode: http.StatusOK,
|
||||
requestVerification: validateJSONPayload,
|
||||
},
|
||||
},
|
||||
req: newMockContentRequest("content"),
|
||||
@@ -470,22 +505,25 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
|
||||
},
|
||||
targets: []target{
|
||||
{
|
||||
reqBody: newMockContextInfoRequest("/service/method", "content"),
|
||||
respBody: newMockContentRequest("content1"),
|
||||
sleep: 0,
|
||||
statusCode: http.StatusOK,
|
||||
reqBody: newMockContextInfoRequest("/service/method", "content"),
|
||||
respBody: newMockContentRequest("content1"),
|
||||
sleep: 0,
|
||||
statusCode: http.StatusOK,
|
||||
requestVerification: validateJSONPayload,
|
||||
},
|
||||
{
|
||||
reqBody: newMockContextInfoRequest("/service/method", "content1"),
|
||||
respBody: newMockContentRequest("content2"),
|
||||
sleep: 5 * time.Second,
|
||||
statusCode: http.StatusBadRequest,
|
||||
reqBody: newMockContextInfoRequest("/service/method", "content1"),
|
||||
respBody: newMockContentRequest("content2"),
|
||||
sleep: 5 * time.Second,
|
||||
statusCode: http.StatusBadRequest,
|
||||
requestVerification: validateJSONPayload,
|
||||
},
|
||||
{
|
||||
reqBody: newMockContextInfoRequest("/service/method", "content2"),
|
||||
respBody: newMockContentRequest("content3"),
|
||||
sleep: 5 * time.Second,
|
||||
statusCode: http.StatusOK,
|
||||
reqBody: newMockContextInfoRequest("/service/method", "content2"),
|
||||
respBody: newMockContentRequest("content3"),
|
||||
sleep: 5 * time.Second,
|
||||
statusCode: http.StatusOK,
|
||||
requestVerification: validateJSONPayload,
|
||||
},
|
||||
},
|
||||
req: newMockContentRequest("content"),
|
||||
@@ -494,6 +532,70 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
|
||||
wantErr: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
"payload JWT, ok",
|
||||
args{
|
||||
ctx: context.Background(),
|
||||
fullMethod: "/service/method",
|
||||
executionTargets: []target_domain.Target{
|
||||
{
|
||||
ExecutionID: "request./zitadel.session.v2.SessionService/SetSession",
|
||||
TargetID: "target",
|
||||
TargetType: target_domain.TargetTypeWebhook,
|
||||
Timeout: time.Minute,
|
||||
InterruptOnError: true,
|
||||
PayloadType: target_domain.PayloadTypeJWT,
|
||||
},
|
||||
},
|
||||
targets: []target{
|
||||
{
|
||||
reqBody: newMockContextInfoRequest("/service/method", "content"),
|
||||
respBody: newMockContentRequest("content1"),
|
||||
sleep: 0,
|
||||
statusCode: http.StatusOK,
|
||||
requestVerification: validateJWTPayload,
|
||||
},
|
||||
},
|
||||
req: newMockContentRequest("content"),
|
||||
getActiveSigningWebKey: mockGetActiveSigningWebKey(),
|
||||
},
|
||||
res{
|
||||
want: newMockContentRequest("content"),
|
||||
},
|
||||
},
|
||||
{
|
||||
"payload JWE, ok",
|
||||
args{
|
||||
ctx: context.Background(),
|
||||
fullMethod: "/service/method",
|
||||
executionTargets: []target_domain.Target{
|
||||
{
|
||||
ExecutionID: "request./zitadel.session.v2.SessionService/SetSession",
|
||||
TargetID: "target",
|
||||
TargetType: target_domain.TargetTypeWebhook,
|
||||
Timeout: time.Minute,
|
||||
InterruptOnError: true,
|
||||
PayloadType: target_domain.PayloadTypeJWE,
|
||||
EncryptionKey: encryptionKey,
|
||||
EncryptionKeyID: encryptionKeyID,
|
||||
},
|
||||
},
|
||||
targets: []target{
|
||||
{
|
||||
reqBody: newMockContextInfoRequest("/service/method", "content"),
|
||||
respBody: newMockContentRequest("content1"),
|
||||
sleep: 0,
|
||||
statusCode: http.StatusOK,
|
||||
requestVerification: validateJWEPayload,
|
||||
},
|
||||
},
|
||||
req: newMockContentRequest("content"),
|
||||
getActiveSigningWebKey: mockGetActiveSigningWebKey(),
|
||||
},
|
||||
res{
|
||||
want: newMockContentRequest("content"),
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
@@ -504,6 +606,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
|
||||
target.sleep,
|
||||
target.statusCode,
|
||||
target.respBody,
|
||||
target.requestVerification(t),
|
||||
)
|
||||
|
||||
tt.args.executionTargets[i].Endpoint = url
|
||||
@@ -516,6 +619,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
|
||||
tt.args.fullMethod,
|
||||
tt.args.req,
|
||||
nil,
|
||||
tt.args.getActiveSigningWebKey,
|
||||
)
|
||||
|
||||
if tt.res.wantErr {
|
||||
@@ -537,6 +641,7 @@ func testServerCall(
|
||||
sleep time.Duration,
|
||||
statusCode int,
|
||||
respBody interface{},
|
||||
requestVerification func(expected, sent []byte) bool,
|
||||
) (string, func()) {
|
||||
handler := func(w http.ResponseWriter, r *http.Request) {
|
||||
data, err := json.Marshal(reqBody)
|
||||
@@ -551,7 +656,7 @@ func testServerCall(
|
||||
return
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(data, sentBody) {
|
||||
if !requestVerification(data, sentBody) {
|
||||
http.Error(w, "error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
@@ -580,6 +685,45 @@ func testServerCall(
|
||||
return server.URL, server.Close
|
||||
}
|
||||
|
||||
func mockGetActiveSigningWebKey() func(ctx context.Context) (*jose.JSONWebKey, error) {
|
||||
return func(ctx context.Context) (*jose.JSONWebKey, error) {
|
||||
return &jose.JSONWebKey{
|
||||
Key: privateKey,
|
||||
Algorithm: string(signingAlgorithm),
|
||||
Use: "sig",
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func validateJSONPayload(t *testing.T) func(expected, sent []byte) bool {
|
||||
return bytes.Equal
|
||||
}
|
||||
|
||||
func validateJWTPayload(t *testing.T) func(expected, sent []byte) bool {
|
||||
return func(expected, sent []byte) bool {
|
||||
jws, err := jose.ParseSigned(string(sent), []jose.SignatureAlgorithm{jose.RS256})
|
||||
require.NoError(t, err)
|
||||
payload, err := jws.Verify(privateKey.Public())
|
||||
require.NoError(t, err)
|
||||
return bytes.Equal(expected, payload)
|
||||
}
|
||||
}
|
||||
|
||||
func validateJWEPayload(t *testing.T) func(expected, sent []byte) bool {
|
||||
return func(expected, sent []byte) bool {
|
||||
parsedJWE, err := jose.ParseEncrypted(string(sent), []jose.KeyAlgorithm{jose.RSA_OAEP_256, jose.ECDH_ES_A256KW}, []jose.ContentEncryption{jose.A256GCM})
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
require.Equal(t, encryptionKeyID, parsedJWE.Header.KeyID)
|
||||
require.Equal(t, "JWT", parsedJWE.Header.ExtraHeaders[jose.HeaderContentType].(string))
|
||||
|
||||
decryptedJWS, err := parsedJWE.Decrypt(privateKey)
|
||||
require.NoError(t, err)
|
||||
return validateJWTPayload(t)(expected, decryptedJWS)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_executeTargetsForGRPCFullMethod_response(t *testing.T) {
|
||||
type target struct {
|
||||
reqBody execution.ContextInfo
|
||||
@@ -699,6 +843,7 @@ func Test_executeTargetsForGRPCFullMethod_response(t *testing.T) {
|
||||
target.sleep,
|
||||
target.statusCode,
|
||||
target.respBody,
|
||||
validateJSONPayload(t),
|
||||
)
|
||||
|
||||
tt.args.executionTargets[i].Endpoint = url
|
||||
@@ -712,6 +857,7 @@ func Test_executeTargetsForGRPCFullMethod_response(t *testing.T) {
|
||||
tt.args.req,
|
||||
tt.args.resp,
|
||||
nil,
|
||||
mockGetActiveSigningWebKey(),
|
||||
)
|
||||
|
||||
if tt.res.wantErr {
|
||||
|
||||
@@ -79,7 +79,7 @@ func CreateServer(
|
||||
middleware.AuthorizationInterceptor(verifier, systemAuthz, authConfig),
|
||||
middleware.TranslationHandler(),
|
||||
middleware.QuotaExhaustedInterceptor(accessSvc, system_pb.SystemService_ServiceDesc.ServiceName),
|
||||
middleware.ExecutionHandler(targetEncAlg),
|
||||
middleware.ExecutionHandler(targetEncAlg, queries.GetActiveSigningWebKey),
|
||||
middleware.ValidationHandler(),
|
||||
middleware.ServiceHandler(),
|
||||
middleware.ActivityInterceptor(),
|
||||
|
||||
@@ -98,7 +98,7 @@ func authnKeyCreatedFilterToQuery(f *filter_pb.TimestampFilter) (query.SearchQue
|
||||
}
|
||||
|
||||
func authnKeyExpirationFilterToQuery(f *filter_pb.TimestampFilter) (query.SearchQuery, error) {
|
||||
return query.NewAuthNKeyExpirationDateDateQuery(f.Timestamp.AsTime(), filter.TimestampMethodPbToQuery(f.Method))
|
||||
return query.NewAuthNKeyExpirationDateQuery(f.Timestamp.AsTime(), filter.TimestampMethodPbToQuery(f.Method))
|
||||
}
|
||||
|
||||
// authnKeyFieldNameToSortingColumn defaults to the creation date because this ensures deterministic pagination
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
http_utils "github.com/zitadel/zitadel/internal/api/http"
|
||||
"github.com/zitadel/zitadel/internal/api/http/middleware"
|
||||
"github.com/zitadel/zitadel/internal/api/oidc/sign"
|
||||
"github.com/zitadel/zitadel/internal/api/ui/login"
|
||||
"github.com/zitadel/zitadel/internal/auth/repository/eventsourcing/handler"
|
||||
"github.com/zitadel/zitadel/internal/command"
|
||||
@@ -300,7 +301,7 @@ func (o *OPStorage) TerminateSessionFromRequest(ctx context.Context, endSessionR
|
||||
} else {
|
||||
logoutURI = logoutURI.JoinPath(LogoutPath)
|
||||
}
|
||||
signer, _, err := GetSignerOnce(o.query.GetActiveSigningWebKey)(ctx)
|
||||
signer, _, err := sign.GetSignerOnce(o.query.GetActiveSigningWebKey)(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package sign
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/go-jose/go-jose/v4"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/telemetry/tracing"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
// SignerFunc is a getter function that allows add-hoc retrieval of the instance's signer.
|
||||
type SignerFunc func(ctx context.Context) (jose.Signer, jose.SignatureAlgorithm, error)
|
||||
|
||||
// GetSignerOnce returns a function which retrieves the instance's signer from the database once.
|
||||
// Repeated calls of the returned function return the same results.
|
||||
func GetSignerOnce(
|
||||
getActiveSigningWebKey func(ctx context.Context) (*jose.JSONWebKey, error),
|
||||
) SignerFunc {
|
||||
var (
|
||||
once sync.Once
|
||||
signer jose.Signer
|
||||
signAlg jose.SignatureAlgorithm
|
||||
err error
|
||||
)
|
||||
return func(ctx context.Context) (jose.Signer, jose.SignatureAlgorithm, error) {
|
||||
once.Do(func() {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
|
||||
var webKey *jose.JSONWebKey
|
||||
webKey, err = getActiveSigningWebKey(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
signer, signAlg, err = signerFromWebKey(webKey)
|
||||
})
|
||||
return signer, signAlg, err
|
||||
}
|
||||
}
|
||||
|
||||
func signerFromWebKey(signingKey *jose.JSONWebKey) (jose.Signer, jose.SignatureAlgorithm, error) {
|
||||
signAlg := jose.SignatureAlgorithm(signingKey.Algorithm)
|
||||
signer, err := jose.NewSigner(
|
||||
jose.SigningKey{
|
||||
Algorithm: signAlg,
|
||||
Key: signingKey,
|
||||
},
|
||||
(&jose.SignerOptions{}).WithType("JWT"),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, "", zerrors.ThrowInternal(err, "OIDC-oaF0s", "Errors.Internal")
|
||||
}
|
||||
return signer, signAlg, nil
|
||||
}
|
||||
@@ -4,18 +4,16 @@ import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"slices"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/go-jose/go-jose/v4"
|
||||
"github.com/zitadel/oidc/v3/pkg/crypto"
|
||||
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||
"github.com/zitadel/oidc/v3/pkg/op"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/oidc/sign"
|
||||
"github.com/zitadel/zitadel/internal/command"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/telemetry/tracing"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
/*
|
||||
@@ -59,53 +57,8 @@ func (s *Server) accessTokenResponseFromSession(ctx context.Context, client op.C
|
||||
return resp, err
|
||||
}
|
||||
|
||||
// SignerFunc is a getter function that allows add-hoc retrieval of the instance's signer.
|
||||
type SignerFunc func(ctx context.Context) (jose.Signer, jose.SignatureAlgorithm, error)
|
||||
|
||||
func (s *Server) getSignerOnce() SignerFunc {
|
||||
return GetSignerOnce(s.query.GetActiveSigningWebKey)
|
||||
}
|
||||
|
||||
// GetSignerOnce returns a function which retrieves the instance's signer from the database once.
|
||||
// Repeated calls of the returned function return the same results.
|
||||
func GetSignerOnce(
|
||||
getActiveSigningWebKey func(ctx context.Context) (*jose.JSONWebKey, error),
|
||||
) SignerFunc {
|
||||
var (
|
||||
once sync.Once
|
||||
signer jose.Signer
|
||||
signAlg jose.SignatureAlgorithm
|
||||
err error
|
||||
)
|
||||
return func(ctx context.Context) (jose.Signer, jose.SignatureAlgorithm, error) {
|
||||
once.Do(func() {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
|
||||
var webKey *jose.JSONWebKey
|
||||
webKey, err = getActiveSigningWebKey(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
signer, signAlg, err = signerFromWebKey(webKey)
|
||||
})
|
||||
return signer, signAlg, err
|
||||
}
|
||||
}
|
||||
|
||||
func signerFromWebKey(signingKey *jose.JSONWebKey) (jose.Signer, jose.SignatureAlgorithm, error) {
|
||||
signAlg := jose.SignatureAlgorithm(signingKey.Algorithm)
|
||||
signer, err := jose.NewSigner(
|
||||
jose.SigningKey{
|
||||
Algorithm: signAlg,
|
||||
Key: signingKey,
|
||||
},
|
||||
(&jose.SignerOptions{}).WithType("JWT"),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, "", zerrors.ThrowInternal(err, "OIDC-oaF0s", "Errors.Internal")
|
||||
}
|
||||
return signer, signAlg, nil
|
||||
func (s *Server) getSignerOnce() sign.SignerFunc {
|
||||
return sign.GetSignerOnce(s.query.GetActiveSigningWebKey)
|
||||
}
|
||||
|
||||
// userInfoFunc is a getter function that allows add-hoc retrieval of a user.
|
||||
@@ -120,7 +73,7 @@ func (s *Server) getUserInfo(userID, projectID, clientID string, projectRoleAsse
|
||||
}
|
||||
}
|
||||
|
||||
func (*Server) createIDToken(ctx context.Context, client op.Client, getUserInfo userInfoFunc, roleAssertion bool, getSigningKey SignerFunc, sessionID, accessToken string, audience []string, authMethods []domain.UserAuthMethodType, authTime time.Time, nonce string, actor *domain.TokenActor) (idToken string, exp uint64, err error) {
|
||||
func (*Server) createIDToken(ctx context.Context, client op.Client, getUserInfo userInfoFunc, roleAssertion bool, getSigningKey sign.SignerFunc, sessionID, accessToken string, audience []string, authMethods []domain.UserAuthMethodType, authTime time.Time, nonce string, actor *domain.TokenActor) (idToken string, exp uint64, err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
|
||||
@@ -164,7 +117,7 @@ func timeToOIDCExpiresIn(exp time.Time) uint64 {
|
||||
return uint64(time.Until(exp) / time.Second)
|
||||
}
|
||||
|
||||
func (s *Server) createJWT(ctx context.Context, client op.Client, session *command.OIDCSession, getUserInfo userInfoFunc, assertRoles bool, getSigner SignerFunc) (_ string, err error) {
|
||||
func (s *Server) createJWT(ctx context.Context, client op.Client, session *command.OIDCSession, getUserInfo userInfoFunc, assertRoles bool, getSigner sign.SignerFunc) (_ string, err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"golang.org/x/text/language"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/api/oidc/sign"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/telemetry/tracing"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
@@ -317,7 +318,7 @@ func (s *Server) createExchangeJWT(
|
||||
client *Client,
|
||||
getUserInfo userInfoFunc,
|
||||
roleAssertion bool,
|
||||
getSigner SignerFunc,
|
||||
getSigner sign.SignerFunc,
|
||||
userID,
|
||||
resourceOwner string,
|
||||
audience,
|
||||
|
||||
@@ -461,7 +461,7 @@ func (s *Server) userinfoFlows(ctx context.Context, qu *query.OIDCUserInfo, user
|
||||
UserGrants: qu.UserGrants,
|
||||
}
|
||||
|
||||
resp, err := execution.CallTargets(ctx, executionTargets, info, s.targetEncryptionAlgorithm)
|
||||
resp, err := execution.CallTargets(ctx, executionTargets, info, s.targetEncryptionAlgorithm, s.query.GetActiveSigningWebKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -401,7 +401,7 @@ func (p *Storage) getCustomAttributes(ctx context.Context, user *query.User, use
|
||||
UserGrants: userGrants.UserGrants,
|
||||
}
|
||||
|
||||
resp, err := execution.CallTargets(ctx, executionTargets, info, p.targetEncAlg)
|
||||
resp, err := execution.CallTargets(ctx, executionTargets, info, p.targetEncAlg, p.query.GetActiveSigningWebKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -181,6 +181,7 @@ func TestCommands_SetExecutionRequest(t *testing.T) {
|
||||
KeyID: "id",
|
||||
Crypted: []byte("12345678"),
|
||||
},
|
||||
target_domain.PayloadTypeJSON,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -236,6 +237,7 @@ func TestCommands_SetExecutionRequest(t *testing.T) {
|
||||
KeyID: "id",
|
||||
Crypted: []byte("12345678"),
|
||||
},
|
||||
target_domain.PayloadTypeJSON,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -291,6 +293,7 @@ func TestCommands_SetExecutionRequest(t *testing.T) {
|
||||
KeyID: "id",
|
||||
Crypted: []byte("12345678"),
|
||||
},
|
||||
target_domain.PayloadTypeJSON,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -863,6 +866,7 @@ func TestCommands_SetExecutionResponse(t *testing.T) {
|
||||
KeyID: "id",
|
||||
Crypted: []byte("12345678"),
|
||||
},
|
||||
target_domain.PayloadTypeJSON,
|
||||
),
|
||||
),
|
||||
expectPushFailed(
|
||||
@@ -963,6 +967,7 @@ func TestCommands_SetExecutionResponse(t *testing.T) {
|
||||
KeyID: "id",
|
||||
Crypted: []byte("12345678"),
|
||||
},
|
||||
target_domain.PayloadTypeJSON,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -22,6 +22,7 @@ type AddTarget struct {
|
||||
Endpoint string
|
||||
Timeout time.Duration
|
||||
InterruptOnError bool
|
||||
PayloadType target_domain.PayloadType
|
||||
|
||||
SigningKey string
|
||||
}
|
||||
@@ -77,6 +78,7 @@ func (c *Commands) AddTarget(ctx context.Context, add *AddTarget, resourceOwner
|
||||
add.Timeout,
|
||||
add.InterruptOnError,
|
||||
code.Crypted,
|
||||
add.PayloadType,
|
||||
))
|
||||
if err != nil {
|
||||
return time.Time{}, err
|
||||
@@ -95,6 +97,7 @@ type ChangeTarget struct {
|
||||
Endpoint *string
|
||||
Timeout *time.Duration
|
||||
InterruptOnError *bool
|
||||
PayloadType target_domain.PayloadType
|
||||
|
||||
ExpirationSigningKey bool
|
||||
SigningKey *string
|
||||
@@ -153,6 +156,7 @@ func (c *Commands) ChangeTarget(ctx context.Context, change *ChangeTarget, resou
|
||||
change.Timeout,
|
||||
change.InterruptOnError,
|
||||
changedSigningKey,
|
||||
change.PayloadType,
|
||||
)
|
||||
if changedEvent == nil {
|
||||
return existing.WriteModel.ChangeDate, nil
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
"crypto/rsa"
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/repository/target"
|
||||
"github.com/zitadel/zitadel/internal/telemetry/tracing"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
const (
|
||||
ErrTargetIDMissing = "ErrTargetIDMissing"
|
||||
ErrPublicKeyFormat = "ErrPublicKeyFormat"
|
||||
ErrExpirationDateBeforeNow = "ErrExpirationDateBeforeNow"
|
||||
ErrPublicKeyDeleteActiveKey = "ErrPublicKeyDeleteActiveKey"
|
||||
ErrTargetNotFound = "ErrTargetNotFound"
|
||||
ErrKeyNotFound = "ErrKeyNotFound"
|
||||
ErrPublicKeyExpired = "ErrPublicKeyExpired"
|
||||
)
|
||||
|
||||
type TargetPublicKey struct {
|
||||
TargetID string
|
||||
PublicKey []byte
|
||||
Expiration time.Time
|
||||
|
||||
// KeyID is generated and returned after adding the public key
|
||||
KeyID string
|
||||
}
|
||||
|
||||
func (c *Commands) AddTargetPublicKey(ctx context.Context, key *TargetPublicKey, resourceOwner string) (_ time.Time, err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
|
||||
if key.TargetID == "" {
|
||||
return time.Time{}, zerrors.ThrowInvalidArgument(nil, ErrTargetIDMissing, "Errors.IDMissing")
|
||||
}
|
||||
if !key.Expiration.IsZero() && key.Expiration.Before(time.Now()) {
|
||||
return time.Time{}, zerrors.ThrowInvalidArgument(nil, ErrExpirationDateBeforeNow, "Errors.Target.InvalidExpirationDate")
|
||||
}
|
||||
fingerprint, err := checkPublicKeyAndComputeFingerprint(key.PublicKey)
|
||||
if err != nil {
|
||||
return time.Time{}, err
|
||||
}
|
||||
key.KeyID, err = c.idGenerator.Next()
|
||||
if err != nil {
|
||||
return time.Time{}, err
|
||||
}
|
||||
writeModel, err := c.getTargetKeyWriteModelByID(ctx, key.TargetID, key.KeyID, resourceOwner)
|
||||
if err != nil {
|
||||
return time.Time{}, err
|
||||
}
|
||||
if !writeModel.TargetExists {
|
||||
return time.Time{}, zerrors.ThrowPreconditionFailed(nil, ErrTargetNotFound, "Errors.Target.NotFound")
|
||||
}
|
||||
err = c.pushAppendAndReduce(ctx, writeModel, target.NewKeyAddedEvent(
|
||||
ctx,
|
||||
TargetAggregateFromWriteModel(&writeModel.WriteModel),
|
||||
key.KeyID,
|
||||
key.PublicKey,
|
||||
fingerprint,
|
||||
key.Expiration,
|
||||
))
|
||||
if err != nil {
|
||||
return time.Time{}, err
|
||||
}
|
||||
return writeModel.ChangeDate, nil
|
||||
}
|
||||
|
||||
func (c *Commands) ActivateTargetPublicKey(ctx context.Context, targetID, keyID, resourceOwner string) (_ time.Time, err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
|
||||
if targetID == "" || keyID == "" {
|
||||
return time.Time{}, zerrors.ThrowInvalidArgument(nil, ErrTargetIDMissing, "Errors.IDMissing")
|
||||
}
|
||||
writeModel, err := c.getTargetKeyWriteModelByID(ctx, targetID, keyID, resourceOwner)
|
||||
if err != nil {
|
||||
return time.Time{}, err
|
||||
}
|
||||
if !writeModel.TargetExists || !writeModel.KeyExists {
|
||||
return time.Time{}, zerrors.ThrowPreconditionFailed(nil, ErrKeyNotFound, "Errors.Target.NotFound")
|
||||
}
|
||||
if !writeModel.ExpirationDate.IsZero() && writeModel.ExpirationDate.Before(time.Now()) {
|
||||
return time.Time{}, zerrors.ThrowPreconditionFailed(nil, ErrPublicKeyExpired, "Errors.Target.PublicKeyExpired")
|
||||
}
|
||||
if writeModel.Active {
|
||||
return writeModel.ChangeDate, nil
|
||||
}
|
||||
err = c.pushAppendAndReduce(ctx, writeModel, target.NewKeyActivatedEvent(
|
||||
ctx,
|
||||
TargetAggregateFromWriteModel(&writeModel.WriteModel),
|
||||
keyID,
|
||||
))
|
||||
if err != nil {
|
||||
return time.Time{}, err
|
||||
}
|
||||
return writeModel.ChangeDate, nil
|
||||
}
|
||||
|
||||
func (c *Commands) DeactivateTargetPublicKey(ctx context.Context, targetID, keyID, resourceOwner string) (_ time.Time, err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
|
||||
if targetID == "" || keyID == "" {
|
||||
return time.Time{}, zerrors.ThrowInvalidArgument(nil, ErrTargetIDMissing, "Errors.IDMissing")
|
||||
}
|
||||
writeModel, err := c.getTargetKeyWriteModelByID(ctx, targetID, keyID, resourceOwner)
|
||||
if err != nil {
|
||||
return time.Time{}, err
|
||||
}
|
||||
if !writeModel.TargetExists || !writeModel.KeyExists {
|
||||
return time.Time{}, zerrors.ThrowPreconditionFailed(nil, ErrKeyNotFound, "Errors.Target.NotFound")
|
||||
}
|
||||
if !writeModel.Active {
|
||||
return writeModel.ChangeDate, nil
|
||||
}
|
||||
err = c.pushAppendAndReduce(ctx, writeModel, target.NewKeyDeactivatedEvent(
|
||||
ctx,
|
||||
TargetAggregateFromWriteModel(&writeModel.WriteModel),
|
||||
keyID,
|
||||
))
|
||||
if err != nil {
|
||||
return time.Time{}, err
|
||||
}
|
||||
return writeModel.ChangeDate, nil
|
||||
}
|
||||
|
||||
func (c *Commands) RemoveTargetPublicKey(ctx context.Context, targetID, keyID, resourceOwner string) (_ time.Time, err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
|
||||
if targetID == "" || keyID == "" {
|
||||
return time.Time{}, zerrors.ThrowInvalidArgument(nil, ErrTargetIDMissing, "Errors.IDMissing")
|
||||
}
|
||||
writeModel, err := c.getTargetKeyWriteModelByID(ctx, targetID, keyID, resourceOwner)
|
||||
if err != nil {
|
||||
return time.Time{}, err
|
||||
}
|
||||
if !writeModel.TargetExists || !writeModel.KeyExists {
|
||||
return writeModel.ChangeDate, nil
|
||||
}
|
||||
if writeModel.Active {
|
||||
return time.Time{}, zerrors.ThrowPreconditionFailed(nil, ErrPublicKeyDeleteActiveKey, "Errors.Target.PublicKeyActive")
|
||||
}
|
||||
err = c.pushAppendAndReduce(ctx, writeModel, target.NewKeyRemovedEvent(
|
||||
ctx,
|
||||
TargetAggregateFromWriteModel(&writeModel.WriteModel),
|
||||
keyID,
|
||||
))
|
||||
if err != nil {
|
||||
return time.Time{}, err
|
||||
}
|
||||
return writeModel.ChangeDate, nil
|
||||
}
|
||||
|
||||
func checkPublicKeyAndComputeFingerprint(key []byte) (string, error) {
|
||||
block, _ := pem.Decode(key)
|
||||
if block == nil {
|
||||
return "", zerrors.ThrowInvalidArgument(nil, ErrPublicKeyFormat, "Errors.Target.InvalidPublicKey")
|
||||
}
|
||||
pub, err := x509.ParsePKIXPublicKey(block.Bytes)
|
||||
if err != nil {
|
||||
return "", zerrors.ThrowInvalidArgument(err, ErrPublicKeyFormat, "Errors.Target.InvalidPublicKey")
|
||||
}
|
||||
switch pub.(type) {
|
||||
case *rsa.PublicKey,
|
||||
*ecdsa.PublicKey:
|
||||
fingerprint := sha256.Sum256(block.Bytes)
|
||||
return fmt.Sprintf("SHA256:%s", base64.RawStdEncoding.EncodeToString(fingerprint[:])), nil
|
||||
default:
|
||||
return "", zerrors.ThrowInvalidArgument(nil, ErrPublicKeyFormat, "Errors.Target.InvalidPublicKey")
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Commands) getTargetKeyWriteModelByID(ctx context.Context, targetID, keyID string, resourceOwner string) (*TargetKeyWriteModel, error) {
|
||||
wm := NewTargetKeyWriteModel(targetID, keyID, resourceOwner)
|
||||
err := c.eventstore.FilterToQueryReducer(ctx, wm)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return wm, nil
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/repository/target"
|
||||
)
|
||||
|
||||
type TargetKeyWriteModel struct {
|
||||
eventstore.WriteModel
|
||||
KeyID string
|
||||
|
||||
KeyExists bool
|
||||
TargetExists bool
|
||||
Active bool
|
||||
ExpirationDate time.Time
|
||||
}
|
||||
|
||||
func NewTargetKeyWriteModel(targetID, keyID string, resourceOwner string) *TargetKeyWriteModel {
|
||||
return &TargetKeyWriteModel{
|
||||
WriteModel: eventstore.WriteModel{
|
||||
AggregateID: targetID,
|
||||
ResourceOwner: resourceOwner,
|
||||
},
|
||||
KeyID: keyID,
|
||||
}
|
||||
}
|
||||
|
||||
func (wm *TargetKeyWriteModel) AppendEvents(events ...eventstore.Event) {
|
||||
for _, event := range events {
|
||||
switch e := event.(type) {
|
||||
case *target.KeyAddedEvent:
|
||||
if wm.KeyID != e.KeyID {
|
||||
continue
|
||||
}
|
||||
wm.WriteModel.AppendEvents(e)
|
||||
case *target.KeyActivatedEvent:
|
||||
// don't check for KeyID, as only one key can be active at a time any activation event affects all keys
|
||||
wm.WriteModel.AppendEvents(e)
|
||||
case *target.KeyDeactivatedEvent:
|
||||
if wm.KeyID != e.KeyID {
|
||||
continue
|
||||
}
|
||||
wm.WriteModel.AppendEvents(e)
|
||||
case *target.KeyRemovedEvent:
|
||||
if wm.KeyID != e.KeyID {
|
||||
continue
|
||||
}
|
||||
wm.WriteModel.AppendEvents(e)
|
||||
case *target.AddedEvent:
|
||||
wm.WriteModel.AppendEvents(e)
|
||||
case *target.RemovedEvent:
|
||||
wm.WriteModel.AppendEvents(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (wm *TargetKeyWriteModel) Reduce() error {
|
||||
for _, event := range wm.Events {
|
||||
switch e := event.(type) {
|
||||
case *target.KeyAddedEvent:
|
||||
wm.KeyID = e.KeyID
|
||||
wm.KeyExists = true
|
||||
wm.ExpirationDate = e.ExpirationDate
|
||||
case *target.KeyActivatedEvent:
|
||||
wm.Active = wm.KeyID == e.KeyID
|
||||
case *target.KeyDeactivatedEvent:
|
||||
wm.Active = false
|
||||
case *target.KeyRemovedEvent:
|
||||
wm.KeyExists = false
|
||||
case *target.AddedEvent:
|
||||
wm.TargetExists = true
|
||||
case *target.RemovedEvent:
|
||||
wm.TargetExists = false
|
||||
}
|
||||
}
|
||||
return wm.WriteModel.Reduce()
|
||||
}
|
||||
|
||||
func (wm *TargetKeyWriteModel) Query() *eventstore.SearchQueryBuilder {
|
||||
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
|
||||
ResourceOwner(wm.ResourceOwner).
|
||||
AddQuery().
|
||||
AggregateTypes(target.AggregateType).
|
||||
AggregateIDs(wm.AggregateID).
|
||||
EventTypes(target.KeyAddedEventType,
|
||||
target.KeyActivatedEventType,
|
||||
target.KeyDeactivatedEventType,
|
||||
target.KeyRemovedEventType,
|
||||
target.AddedEventType,
|
||||
target.RemovedEventType).
|
||||
Builder()
|
||||
}
|
||||
@@ -0,0 +1,655 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/id"
|
||||
"github.com/zitadel/zitadel/internal/id/mock"
|
||||
"github.com/zitadel/zitadel/internal/repository/target"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
var (
|
||||
rsaPublicKey = func() *rsa.PublicKey {
|
||||
privateKey, _ := rsa.GenerateKey(rand.Reader, 2048)
|
||||
return &privateKey.PublicKey
|
||||
}()
|
||||
rsaPublicKeyBytes = func() []byte {
|
||||
data, _ := crypto.PublicKeyToBytes(rsaPublicKey)
|
||||
return data
|
||||
}()
|
||||
rsaFingerprint = func() string {
|
||||
block, _ := x509.MarshalPKIXPublicKey(rsaPublicKey)
|
||||
hash := sha256.Sum256(block)
|
||||
return fmt.Sprintf("SHA256:%s", base64.RawStdEncoding.EncodeToString(hash[:]))
|
||||
}()
|
||||
)
|
||||
|
||||
func TestCommands_AddTargetPublicKey(t *testing.T) {
|
||||
type fields struct {
|
||||
eventstore func(t *testing.T) *eventstore.Eventstore
|
||||
idGenerator id.Generator
|
||||
}
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
key *TargetPublicKey
|
||||
resourceOwner string
|
||||
}
|
||||
type res struct {
|
||||
err error
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
res res
|
||||
}{
|
||||
{
|
||||
name: "missing target id",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
key: &TargetPublicKey{},
|
||||
},
|
||||
res: res{
|
||||
err: zerrors.ThrowInvalidArgument(nil, ErrTargetIDMissing, "Errors.IDMissing"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "expiration date before now",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
key: &TargetPublicKey{
|
||||
TargetID: "target-1",
|
||||
Expiration: testNow.Add(-time.Hour),
|
||||
},
|
||||
},
|
||||
res: res{
|
||||
err: zerrors.ThrowInvalidArgument(nil, ErrExpirationDateBeforeNow, "Errors.Target.InvalidExpirationDate"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid public key",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
key: &TargetPublicKey{
|
||||
TargetID: "target-1",
|
||||
PublicKey: []byte("invalid-key"),
|
||||
},
|
||||
},
|
||||
res: res{
|
||||
err: zerrors.ThrowInvalidArgument(nil, ErrPublicKeyFormat, "Errors.Target.InvalidPublicKey"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid public key format (PKCS1)",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
key: &TargetPublicKey{
|
||||
TargetID: "target-1",
|
||||
PublicKey: pem.EncodeToMemory(&pem.Block{Type: "RSA PUBLIC KEY", Bytes: x509.MarshalPKCS1PublicKey(rsaPublicKey)}),
|
||||
},
|
||||
},
|
||||
res: res{
|
||||
err: zerrors.ThrowInvalidArgument(nil, ErrPublicKeyFormat, "Errors.Target.InvalidPublicKey"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid public key type (ed25519)",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
key: &TargetPublicKey{
|
||||
TargetID: "target-1",
|
||||
PublicKey: func() []byte {
|
||||
publicKey, _, _ := ed25519.GenerateKey(rand.Reader)
|
||||
ed25519PublicKeyBytes, _ := x509.MarshalPKIXPublicKey(publicKey)
|
||||
return pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: ed25519PublicKeyBytes})
|
||||
}(),
|
||||
},
|
||||
},
|
||||
res: res{
|
||||
err: zerrors.ThrowInvalidArgument(nil, ErrPublicKeyFormat, "Errors.Target.InvalidPublicKey"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "target not found",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(),
|
||||
),
|
||||
idGenerator: mock.ExpectID(t, "key-1"),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
key: &TargetPublicKey{
|
||||
TargetID: "target-1",
|
||||
PublicKey: rsaPublicKeyBytes,
|
||||
},
|
||||
},
|
||||
res: res{
|
||||
err: zerrors.ThrowPreconditionFailed(nil, ErrTargetNotFound, "Errors.Target.NotFound"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "success",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
targetAddEvent("target-1", "instance1"),
|
||||
),
|
||||
),
|
||||
expectPush(
|
||||
eventFromEventPusher(
|
||||
target.NewKeyAddedEvent(context.Background(),
|
||||
target.NewAggregate("target-1", "instance1"),
|
||||
"key-1",
|
||||
rsaPublicKeyBytes,
|
||||
rsaFingerprint,
|
||||
time.Time{},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
idGenerator: mock.ExpectID(t, "key-1"),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
key: &TargetPublicKey{
|
||||
TargetID: "target-1",
|
||||
PublicKey: rsaPublicKeyBytes,
|
||||
},
|
||||
},
|
||||
res: res{},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &Commands{
|
||||
eventstore: tt.fields.eventstore(t),
|
||||
idGenerator: tt.fields.idGenerator,
|
||||
}
|
||||
_, err := c.AddTargetPublicKey(tt.args.ctx, tt.args.key, tt.args.resourceOwner)
|
||||
assert.ErrorIs(t, err, tt.res.err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommands_ActivateTargetPublicKey(t *testing.T) {
|
||||
type fields struct {
|
||||
eventstore func(t *testing.T) *eventstore.Eventstore
|
||||
}
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
targetID string
|
||||
keyID string
|
||||
resourceOwner string
|
||||
}
|
||||
type res struct {
|
||||
err error
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
res res
|
||||
}{
|
||||
{
|
||||
name: "missing target id",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
},
|
||||
res: res{
|
||||
err: zerrors.ThrowInvalidArgument(nil, ErrTargetIDMissing, "Errors.IDMissing"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "missing key id",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
targetID: "target-1",
|
||||
},
|
||||
res: res{
|
||||
err: zerrors.ThrowInvalidArgument(nil, ErrTargetIDMissing, "Errors.IDMissing"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "target not found",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(),
|
||||
),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
targetID: "target-1",
|
||||
keyID: "key-1",
|
||||
},
|
||||
res: res{
|
||||
err: zerrors.ThrowPreconditionFailed(nil, ErrKeyNotFound, "Errors.Target.NotFound"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "already expired",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
targetAddEvent("target-1", "instance1"),
|
||||
),
|
||||
eventFromEventPusher(
|
||||
target.NewKeyAddedEvent(context.Background(),
|
||||
target.NewAggregate("target-1", "instance1"),
|
||||
"key-1",
|
||||
rsaPublicKeyBytes,
|
||||
rsaFingerprint,
|
||||
time.Now().Add(-time.Hour),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
targetID: "target-1",
|
||||
keyID: "key-1",
|
||||
},
|
||||
res: res{
|
||||
err: zerrors.ThrowPreconditionFailed(nil, ErrPublicKeyExpired, "Errors.Target.PublicKeyExpired"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "already active",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
targetAddEvent("target-1", "instance1"),
|
||||
),
|
||||
eventFromEventPusher(
|
||||
target.NewKeyAddedEvent(context.Background(),
|
||||
target.NewAggregate("target-1", "instance1"),
|
||||
"key-1",
|
||||
rsaPublicKeyBytes,
|
||||
rsaFingerprint,
|
||||
time.Time{},
|
||||
),
|
||||
),
|
||||
eventFromEventPusher(
|
||||
target.NewKeyActivatedEvent(context.Background(),
|
||||
target.NewAggregate("target-1", "instance1"),
|
||||
"key-1",
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
targetID: "target-1",
|
||||
keyID: "key-1",
|
||||
},
|
||||
res: res{},
|
||||
},
|
||||
{
|
||||
name: "success",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
targetAddEvent("target-1", "instance1"),
|
||||
),
|
||||
eventFromEventPusher(
|
||||
target.NewKeyAddedEvent(context.Background(),
|
||||
target.NewAggregate("target-1", "instance1"),
|
||||
"key-1",
|
||||
rsaPublicKeyBytes,
|
||||
rsaFingerprint,
|
||||
time.Time{},
|
||||
),
|
||||
),
|
||||
),
|
||||
expectPush(
|
||||
eventFromEventPusher(
|
||||
target.NewKeyActivatedEvent(context.Background(),
|
||||
target.NewAggregate("target-1", "instance1"),
|
||||
"key-1",
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
targetID: "target-1",
|
||||
keyID: "key-1",
|
||||
},
|
||||
res: res{},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &Commands{
|
||||
eventstore: tt.fields.eventstore(t),
|
||||
}
|
||||
_, err := c.ActivateTargetPublicKey(tt.args.ctx, tt.args.targetID, tt.args.keyID, tt.args.resourceOwner)
|
||||
assert.ErrorIs(t, err, tt.res.err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommands_DeactivateTargetPublicKey(t *testing.T) {
|
||||
type fields struct {
|
||||
eventstore func(t *testing.T) *eventstore.Eventstore
|
||||
}
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
targetID string
|
||||
keyID string
|
||||
resourceOwner string
|
||||
}
|
||||
type res struct {
|
||||
err error
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
res res
|
||||
}{
|
||||
{
|
||||
name: "missing target id",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
},
|
||||
res: res{
|
||||
err: zerrors.ThrowInvalidArgument(nil, ErrTargetIDMissing, "Errors.IDMissing"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "missing key id",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
targetID: "target-1",
|
||||
},
|
||||
res: res{
|
||||
err: zerrors.ThrowInvalidArgument(nil, ErrTargetIDMissing, "Errors.IDMissing"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "target not found",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(),
|
||||
),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
targetID: "target-1",
|
||||
keyID: "key-1",
|
||||
},
|
||||
res: res{
|
||||
err: zerrors.ThrowPreconditionFailed(nil, ErrKeyNotFound, "Errors.Target.NotFound"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "already inactive",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
targetAddEvent("target-1", "instance1"),
|
||||
),
|
||||
eventFromEventPusher(
|
||||
target.NewKeyAddedEvent(context.Background(),
|
||||
target.NewAggregate("target-1", "instance1"),
|
||||
"key-1",
|
||||
rsaPublicKeyBytes,
|
||||
rsaFingerprint,
|
||||
time.Time{},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
targetID: "target-1",
|
||||
keyID: "key-1",
|
||||
},
|
||||
res: res{},
|
||||
},
|
||||
{
|
||||
name: "success",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
targetAddEvent("target-1", "instance1"),
|
||||
),
|
||||
eventFromEventPusher(
|
||||
target.NewKeyAddedEvent(context.Background(),
|
||||
target.NewAggregate("target-1", "instance1"),
|
||||
"key-1",
|
||||
rsaPublicKeyBytes,
|
||||
rsaFingerprint,
|
||||
time.Time{},
|
||||
),
|
||||
),
|
||||
eventFromEventPusher(
|
||||
target.NewKeyActivatedEvent(context.Background(),
|
||||
target.NewAggregate("target-1", "instance1"),
|
||||
"key-1",
|
||||
),
|
||||
),
|
||||
),
|
||||
expectPush(
|
||||
eventFromEventPusher(
|
||||
target.NewKeyDeactivatedEvent(context.Background(),
|
||||
target.NewAggregate("target-1", "instance1"),
|
||||
"key-1",
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
targetID: "target-1",
|
||||
keyID: "key-1",
|
||||
},
|
||||
res: res{},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &Commands{
|
||||
eventstore: tt.fields.eventstore(t),
|
||||
}
|
||||
_, err := c.DeactivateTargetPublicKey(tt.args.ctx, tt.args.targetID, tt.args.keyID, tt.args.resourceOwner)
|
||||
assert.ErrorIs(t, err, tt.res.err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommands_RemoveTargetPublicKey(t *testing.T) {
|
||||
type fields struct {
|
||||
eventstore func(t *testing.T) *eventstore.Eventstore
|
||||
}
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
targetID string
|
||||
keyID string
|
||||
resourceOwner string
|
||||
}
|
||||
type res struct {
|
||||
err error
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
res res
|
||||
}{
|
||||
{
|
||||
name: "missing target id",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
},
|
||||
res: res{
|
||||
err: zerrors.ThrowInvalidArgument(nil, ErrTargetIDMissing, "Errors.IDMissing"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "missing key id",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
targetID: "target-1",
|
||||
},
|
||||
res: res{
|
||||
err: zerrors.ThrowInvalidArgument(nil, ErrTargetIDMissing, "Errors.IDMissing"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "target not found",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(),
|
||||
),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
targetID: "target-1",
|
||||
keyID: "key-1",
|
||||
},
|
||||
res: res{
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "key active",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
targetAddEvent("target-1", "instance1"),
|
||||
),
|
||||
eventFromEventPusher(
|
||||
target.NewKeyAddedEvent(context.Background(),
|
||||
target.NewAggregate("target-1", "instance1"),
|
||||
"key-1",
|
||||
rsaPublicKeyBytes,
|
||||
rsaFingerprint,
|
||||
time.Time{},
|
||||
),
|
||||
),
|
||||
eventFromEventPusher(
|
||||
target.NewKeyActivatedEvent(context.Background(),
|
||||
target.NewAggregate("target-1", "instance1"),
|
||||
"key-1",
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
targetID: "target-1",
|
||||
keyID: "key-1",
|
||||
},
|
||||
res: res{
|
||||
err: zerrors.ThrowPreconditionFailed(nil, ErrPublicKeyDeleteActiveKey, "Errors.Target.PublicKeyActive"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "success",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
targetAddEvent("target-1", "instance1"),
|
||||
),
|
||||
eventFromEventPusher(
|
||||
target.NewKeyAddedEvent(context.Background(),
|
||||
target.NewAggregate("target-1", "instance1"),
|
||||
"key-1",
|
||||
rsaPublicKeyBytes,
|
||||
rsaFingerprint,
|
||||
time.Time{},
|
||||
),
|
||||
),
|
||||
),
|
||||
expectPush(
|
||||
eventFromEventPusher(
|
||||
target.NewKeyRemovedEvent(context.Background(),
|
||||
target.NewAggregate("target-1", "instance1"),
|
||||
"key-1",
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
targetID: "target-1",
|
||||
keyID: "key-1",
|
||||
},
|
||||
res: res{},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &Commands{
|
||||
eventstore: tt.fields.eventstore(t),
|
||||
}
|
||||
_, err := c.RemoveTargetPublicKey(tt.args.ctx, tt.args.targetID, tt.args.keyID, tt.args.resourceOwner)
|
||||
assert.ErrorIs(t, err, tt.res.err)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@ type TargetWriteModel struct {
|
||||
Timeout time.Duration
|
||||
InterruptOnError bool
|
||||
SigningKey *crypto.CryptoValue
|
||||
PayloadType target_domain.PayloadType
|
||||
|
||||
State domain.TargetState
|
||||
}
|
||||
@@ -45,6 +46,7 @@ func (wm *TargetWriteModel) Reduce() error {
|
||||
wm.Timeout = e.Timeout
|
||||
wm.State = domain.TargetActive
|
||||
wm.SigningKey = e.SigningKey
|
||||
wm.PayloadType = e.PayloadType
|
||||
case *target.ChangedEvent:
|
||||
if e.Name != nil {
|
||||
wm.Name = *e.Name
|
||||
@@ -64,6 +66,9 @@ func (wm *TargetWriteModel) Reduce() error {
|
||||
if e.SigningKey != nil {
|
||||
wm.SigningKey = e.SigningKey
|
||||
}
|
||||
if e.PayloadType != target_domain.PayloadTypeUnspecified {
|
||||
wm.PayloadType = e.PayloadType
|
||||
}
|
||||
case *target.RemovedEvent:
|
||||
wm.State = domain.TargetRemoved
|
||||
}
|
||||
@@ -92,6 +97,7 @@ func (wm *TargetWriteModel) NewChangedEvent(
|
||||
timeout *time.Duration,
|
||||
interruptOnError *bool,
|
||||
signingKey *crypto.CryptoValue,
|
||||
payloadType target_domain.PayloadType,
|
||||
) *target.ChangedEvent {
|
||||
changes := make([]target.Changes, 0)
|
||||
if name != nil && wm.Name != *name {
|
||||
@@ -113,6 +119,9 @@ func (wm *TargetWriteModel) NewChangedEvent(
|
||||
if signingKey != nil {
|
||||
changes = append(changes, target.ChangeSigningKey(signingKey))
|
||||
}
|
||||
if payloadType != target_domain.PayloadTypeUnspecified && wm.PayloadType != payloadType {
|
||||
changes = append(changes, target.ChangePayloadType(payloadType))
|
||||
}
|
||||
if len(changes) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ func targetAddEvent(aggID, resourceOwner string) *target.AddedEvent {
|
||||
KeyID: "id",
|
||||
Crypted: []byte("12345678"),
|
||||
},
|
||||
target_domain.PayloadTypeJSON,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -140,6 +140,7 @@ func TestCommands_AddTarget(t *testing.T) {
|
||||
KeyID: "id",
|
||||
Crypted: []byte("12345678"),
|
||||
},
|
||||
target_domain.PayloadTypeJSON,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -150,10 +151,11 @@ func TestCommands_AddTarget(t *testing.T) {
|
||||
args{
|
||||
ctx: context.Background(),
|
||||
add: &AddTarget{
|
||||
Name: "name",
|
||||
Endpoint: "https://example.com",
|
||||
Timeout: time.Second,
|
||||
TargetType: target_domain.TargetTypeWebhook,
|
||||
Name: "name",
|
||||
Endpoint: "https://example.com",
|
||||
Timeout: time.Second,
|
||||
TargetType: target_domain.TargetTypeWebhook,
|
||||
PayloadType: target_domain.PayloadTypeJSON,
|
||||
},
|
||||
resourceOwner: "instance",
|
||||
},
|
||||
@@ -203,10 +205,11 @@ func TestCommands_AddTarget(t *testing.T) {
|
||||
args{
|
||||
ctx: context.Background(),
|
||||
add: &AddTarget{
|
||||
Name: "name",
|
||||
TargetType: target_domain.TargetTypeWebhook,
|
||||
Timeout: time.Second,
|
||||
Endpoint: "https://example.com",
|
||||
Name: "name",
|
||||
TargetType: target_domain.TargetTypeWebhook,
|
||||
Timeout: time.Second,
|
||||
Endpoint: "https://example.com",
|
||||
PayloadType: target_domain.PayloadTypeJSON,
|
||||
},
|
||||
resourceOwner: "instance",
|
||||
},
|
||||
@@ -239,6 +242,7 @@ func TestCommands_AddTarget(t *testing.T) {
|
||||
Endpoint: "https://example.com",
|
||||
Timeout: time.Second,
|
||||
InterruptOnError: true,
|
||||
PayloadType: target_domain.PayloadTypeJSON,
|
||||
},
|
||||
resourceOwner: "instance",
|
||||
},
|
||||
@@ -514,6 +518,7 @@ func TestCommands_ChangeTarget(t *testing.T) {
|
||||
KeyID: "id",
|
||||
Crypted: []byte("12345678"),
|
||||
}),
|
||||
target.ChangePayloadType(target_domain.PayloadTypeJWE),
|
||||
},
|
||||
),
|
||||
),
|
||||
@@ -533,6 +538,7 @@ func TestCommands_ChangeTarget(t *testing.T) {
|
||||
Timeout: gu.Ptr(10 * time.Second),
|
||||
InterruptOnError: gu.Ptr(true),
|
||||
ExpirationSigningKey: true,
|
||||
PayloadType: target_domain.PayloadTypeJWE,
|
||||
},
|
||||
resourceOwner: "instance",
|
||||
},
|
||||
|
||||
@@ -3,15 +3,22 @@ package execution
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"io"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/go-jose/go-jose/v4"
|
||||
"github.com/zitadel/logging"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
zhttp "github.com/zitadel/zitadel/internal/api/http"
|
||||
"github.com/zitadel/zitadel/internal/api/oidc/sign"
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
target_domain "github.com/zitadel/zitadel/internal/execution/target"
|
||||
@@ -27,20 +34,29 @@ type ContextInfo interface {
|
||||
SetHTTPResponseBody([]byte) error
|
||||
}
|
||||
|
||||
type GetActiveSigningWebKey func(ctx context.Context) (*jose.JSONWebKey, error)
|
||||
|
||||
// CallTargets call a list of targets in order with handling of error and responses
|
||||
func CallTargets(
|
||||
ctx context.Context,
|
||||
targets []target_domain.Target,
|
||||
info ContextInfo,
|
||||
alg crypto.EncryptionAlgorithm,
|
||||
activeSigningKey GetActiveSigningWebKey,
|
||||
) (_ interface{}, err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
|
||||
// We make sure the signer and its key are only fetched once per CallTargets call.
|
||||
signerOnce := sign.GetSignerOnce(activeSigningKey)
|
||||
// Create a map to cache encrypters by key ID to avoid recreating them for each target.
|
||||
encrypters := &sync.Map{}
|
||||
|
||||
for _, target := range targets {
|
||||
// call the type of target
|
||||
resp, err := CallTarget(ctx, target, info, alg)
|
||||
resp, err := CallTarget(ctx, target, info, alg, signerOnce, encrypters)
|
||||
// handle error if interrupt is set
|
||||
logging.WithFields("instanceID", authz.GetInstance(ctx).InstanceID(), "target", target.GetTargetID()).OnError(err).Error("error calling target")
|
||||
if err != nil && target.IsInterruptOnError() {
|
||||
return nil, err
|
||||
}
|
||||
@@ -64,6 +80,8 @@ func CallTarget(
|
||||
target target_domain.Target,
|
||||
info ContextInfoRequest,
|
||||
alg crypto.EncryptionAlgorithm,
|
||||
signerOnce sign.SignerFunc,
|
||||
encrypters *sync.Map,
|
||||
) (res []byte, err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
@@ -72,26 +90,137 @@ func CallTarget(
|
||||
if err != nil {
|
||||
return nil, zerrors.ThrowInternal(err, "EXEC-thiiCh5b", "Errors.Internal")
|
||||
}
|
||||
body, err := payload(ctx, info.GetHTTPRequestBody(), target, signerOnce, encrypters)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch target.GetTargetType() {
|
||||
// get request, ignore response and return request and error for handling in list of targets
|
||||
case target_domain.TargetTypeWebhook:
|
||||
return nil, webhook(ctx, target.GetEndpoint(), target.GetTimeout(), info.GetHTTPRequestBody(), signingKey)
|
||||
return nil, webhook(ctx, target.GetEndpoint(), target.GetTimeout(), body, signingKey)
|
||||
// get request, return response and error
|
||||
case target_domain.TargetTypeCall:
|
||||
return Call(ctx, target.GetEndpoint(), target.GetTimeout(), info.GetHTTPRequestBody(), signingKey)
|
||||
return Call(ctx, target.GetEndpoint(), target.GetTimeout(), body, signingKey)
|
||||
case target_domain.TargetTypeAsync:
|
||||
go func(ctx context.Context, target target_domain.Target, info []byte) {
|
||||
if _, err := Call(ctx, target.GetEndpoint(), target.GetTimeout(), info, signingKey); err != nil {
|
||||
logging.WithFields("target", target.GetTargetID()).OnError(err).Info(err)
|
||||
}
|
||||
}(context.WithoutCancel(ctx), target, info.GetHTTPRequestBody())
|
||||
}(context.WithoutCancel(ctx), target, body)
|
||||
return nil, nil
|
||||
default:
|
||||
return nil, zerrors.ThrowInternal(nil, "EXEC-auqnansr2m", "Errors.Execution.Unknown")
|
||||
}
|
||||
}
|
||||
|
||||
func payload(ctx context.Context, payload []byte, target target_domain.Target, signerOnce sign.SignerFunc, encrypters *sync.Map) ([]byte, error) {
|
||||
switch target.GetPayloadType() {
|
||||
case target_domain.PayloadTypeUnspecified,
|
||||
target_domain.PayloadTypeJSON:
|
||||
return payload, nil
|
||||
case target_domain.PayloadTypeJWT:
|
||||
return payloadJWT(ctx, payload, signerOnce)
|
||||
case target_domain.PayloadTypeJWE:
|
||||
return payloadJWE(ctx, payload, target, signerOnce, encrypters)
|
||||
default:
|
||||
return payload, nil
|
||||
}
|
||||
}
|
||||
|
||||
func payloadJWT(ctx context.Context, payload []byte, signerOnce sign.SignerFunc) ([]byte, error) {
|
||||
signer, _, err := signerOnce(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sig, err := signer.Sign(payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data, err := sig.CompactSerialize()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return []byte(data), nil
|
||||
}
|
||||
|
||||
func loadEncrypter(target target_domain.Target, encrypters *sync.Map) (jose.Encrypter, error) {
|
||||
if encrypter, ok := encrypters.Load(target.GetEncryptionKeyID()); ok {
|
||||
return encrypter.(jose.Encrypter), nil
|
||||
}
|
||||
encryptionKey := target.GetEncryptionKey()
|
||||
if len(encryptionKey) == 0 {
|
||||
return nil, zerrors.ThrowInternal(nil, "EXEC-2n8fhs7g", "Errors.Execution.MissingEncryptionKey")
|
||||
}
|
||||
publicKey, algorithm, err := publicKeyFromBytes(encryptionKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
encrypter, err := jose.NewEncrypter(
|
||||
jose.A256GCM,
|
||||
jose.Recipient{
|
||||
Algorithm: algorithm,
|
||||
Key: publicKey,
|
||||
KeyID: target.GetEncryptionKeyID(),
|
||||
},
|
||||
(&jose.EncrypterOptions{}).
|
||||
WithType("JWT").
|
||||
WithContentType("JWT"),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
encrypters.Store(target.GetEncryptionKeyID(), encrypter)
|
||||
return encrypter, nil
|
||||
}
|
||||
|
||||
func payloadJWE(
|
||||
ctx context.Context,
|
||||
payload []byte,
|
||||
target target_domain.Target,
|
||||
signerOnce sign.SignerFunc,
|
||||
encrypters *sync.Map,
|
||||
) ([]byte, error) {
|
||||
payload, err := payloadJWT(ctx, payload, signerOnce)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
encrypter, err := loadEncrypter(target, encrypters)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
enc, err := encrypter.Encrypt(payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
crypted, err := enc.CompactSerialize()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return []byte(crypted), nil
|
||||
}
|
||||
|
||||
func publicKeyFromBytes(data []byte) (any, jose.KeyAlgorithm, error) {
|
||||
block, _ := pem.Decode(data)
|
||||
if block == nil {
|
||||
return nil, "", zerrors.ThrowInternal(nil, "EXEC-3n8fhs7g", "Errors.Execution.InvalidPublicKey")
|
||||
}
|
||||
publicKey, err := x509.ParsePKIXPublicKey(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
switch pk := publicKey.(type) {
|
||||
case *rsa.PublicKey:
|
||||
return pk, jose.RSA_OAEP_256, nil
|
||||
case *ecdsa.PublicKey:
|
||||
return pk, jose.ECDH_ES_A256KW, nil
|
||||
default:
|
||||
return nil, "", zerrors.ThrowInternal(nil, "EXEC-NKJe2", "Errors.Execution.InvalidPublicKey")
|
||||
}
|
||||
}
|
||||
|
||||
// webhook call a webhook, ignore the response but return the errror
|
||||
func webhook(ctx context.Context, url string, timeout time.Duration, body []byte, signingKey string) error {
|
||||
_, err := Call(ctx, url, timeout, body, signingKey)
|
||||
|
||||
@@ -3,20 +3,26 @@ package execution_test
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-jose/go-jose/v4"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/mock/gomock"
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/grpc/server/middleware"
|
||||
"github.com/zitadel/zitadel/internal/api/oidc/sign"
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/execution"
|
||||
target_domain "github.com/zitadel/zitadel/internal/execution/target"
|
||||
@@ -111,7 +117,7 @@ func Test_Call(t *testing.T) {
|
||||
respBody, err := testServer(t,
|
||||
&callTestServer{
|
||||
method: tt.args.method,
|
||||
expectBody: tt.args.body,
|
||||
expectBody: validateJSONPayload(tt.args.body),
|
||||
timeout: tt.args.sleep,
|
||||
statusCode: tt.args.statusCode,
|
||||
respondBody: tt.args.respBody,
|
||||
@@ -135,6 +141,7 @@ func Test_CallTarget(t *testing.T) {
|
||||
info *middleware.ContextInfoRequest
|
||||
server *callTestServer
|
||||
target target_domain.Target
|
||||
signer func(ctx context.Context) (jose.Signer, jose.SignatureAlgorithm, error)
|
||||
}
|
||||
type res struct {
|
||||
body []byte
|
||||
@@ -152,7 +159,7 @@ func Test_CallTarget(t *testing.T) {
|
||||
info: requestContextInfo1,
|
||||
server: &callTestServer{
|
||||
method: http.MethodPost,
|
||||
expectBody: []byte("{\"request\":{\"content\":\"request1\"}}"),
|
||||
expectBody: validateJSONPayload([]byte("{\"request\":{\"content\":\"request1\"}}")),
|
||||
respondBody: []byte("{\"content\":\"request2\"}"),
|
||||
timeout: time.Second,
|
||||
statusCode: http.StatusInternalServerError,
|
||||
@@ -173,7 +180,7 @@ func Test_CallTarget(t *testing.T) {
|
||||
server: &callTestServer{
|
||||
timeout: time.Second,
|
||||
method: http.MethodPost,
|
||||
expectBody: []byte("{\"request\":{\"content\":\"request1\"}}"),
|
||||
expectBody: validateJSONPayload([]byte("{\"request\":{\"content\":\"request1\"}}")),
|
||||
respondBody: []byte("{\"content\":\"request2\"}"),
|
||||
statusCode: http.StatusInternalServerError,
|
||||
},
|
||||
@@ -194,7 +201,7 @@ func Test_CallTarget(t *testing.T) {
|
||||
server: &callTestServer{
|
||||
timeout: time.Second,
|
||||
method: http.MethodPost,
|
||||
expectBody: []byte("{\"request\":{\"content\":\"request1\"}}"),
|
||||
expectBody: validateJSONPayload([]byte("{\"request\":{\"content\":\"request1\"}}")),
|
||||
respondBody: []byte("{\"content\":\"request2\"}"),
|
||||
statusCode: http.StatusOK,
|
||||
},
|
||||
@@ -215,7 +222,7 @@ func Test_CallTarget(t *testing.T) {
|
||||
server: &callTestServer{
|
||||
timeout: time.Second,
|
||||
method: http.MethodPost,
|
||||
expectBody: []byte("{\"request\":{\"content\":\"request1\"}}"),
|
||||
expectBody: validateJSONPayload([]byte("{\"request\":{\"content\":\"request1\"}}")),
|
||||
respondBody: []byte("{\"content\":\"request2\"}"),
|
||||
statusCode: http.StatusOK,
|
||||
signingKey: "signingkey",
|
||||
@@ -242,7 +249,7 @@ func Test_CallTarget(t *testing.T) {
|
||||
server: &callTestServer{
|
||||
timeout: time.Second,
|
||||
method: http.MethodPost,
|
||||
expectBody: []byte("{\"request\":{\"content\":\"request1\"}}"),
|
||||
expectBody: validateJSONPayload([]byte("{\"request\":{\"content\":\"request1\"}}")),
|
||||
respondBody: []byte("{\"content\":\"request2\"}"),
|
||||
statusCode: http.StatusInternalServerError,
|
||||
},
|
||||
@@ -263,7 +270,7 @@ func Test_CallTarget(t *testing.T) {
|
||||
server: &callTestServer{
|
||||
timeout: time.Second,
|
||||
method: http.MethodPost,
|
||||
expectBody: []byte("{\"request\":{\"content\":\"request1\"}}"),
|
||||
expectBody: validateJSONPayload([]byte("{\"request\":{\"content\":\"request1\"}}")),
|
||||
respondBody: []byte("{\"content\":\"request2\"}"),
|
||||
statusCode: http.StatusOK,
|
||||
},
|
||||
@@ -284,7 +291,7 @@ func Test_CallTarget(t *testing.T) {
|
||||
server: &callTestServer{
|
||||
timeout: time.Second,
|
||||
method: http.MethodPost,
|
||||
expectBody: []byte("{\"request\":{\"content\":\"request1\"}}"),
|
||||
expectBody: validateJSONPayload([]byte("{\"request\":{\"content\":\"request1\"}}")),
|
||||
respondBody: []byte("{\"content\":\"request2\"}"),
|
||||
statusCode: http.StatusOK,
|
||||
signingKey: "signingkey",
|
||||
@@ -303,10 +310,103 @@ func Test_CallTarget(t *testing.T) {
|
||||
body: []byte("{\"content\":\"request2\"}"),
|
||||
},
|
||||
},
|
||||
{
|
||||
"webhook, JWT, ok",
|
||||
args{
|
||||
ctx: context.Background(),
|
||||
info: requestContextInfo1,
|
||||
server: &callTestServer{
|
||||
timeout: time.Second,
|
||||
method: http.MethodPost,
|
||||
expectBody: validateJWTPayload([]byte("{\"request\":{\"content\":\"request1\"}}")),
|
||||
respondBody: []byte("{\"content\":\"request2\"}"),
|
||||
statusCode: http.StatusOK,
|
||||
signingKey: "signingkey",
|
||||
},
|
||||
target: target_domain.Target{
|
||||
TargetType: target_domain.TargetTypeWebhook,
|
||||
Timeout: time.Minute,
|
||||
SigningKey: &crypto.CryptoValue{
|
||||
Algorithm: "enc",
|
||||
KeyID: "id",
|
||||
Crypted: []byte("signingkey"),
|
||||
},
|
||||
PayloadType: target_domain.PayloadTypeJWT,
|
||||
},
|
||||
signer: mockSigner,
|
||||
},
|
||||
res{
|
||||
body: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
"webhook, JWE, ok",
|
||||
args{
|
||||
ctx: context.Background(),
|
||||
info: requestContextInfo1,
|
||||
server: &callTestServer{
|
||||
timeout: time.Second,
|
||||
method: http.MethodPost,
|
||||
expectBody: validateJWEPayload([]byte("{\"request\":{\"content\":\"request1\"}}")),
|
||||
respondBody: []byte("{\"content\":\"request2\"}"),
|
||||
statusCode: http.StatusOK,
|
||||
signingKey: "signingkey",
|
||||
},
|
||||
target: target_domain.Target{
|
||||
TargetType: target_domain.TargetTypeWebhook,
|
||||
Timeout: time.Minute,
|
||||
SigningKey: &crypto.CryptoValue{
|
||||
Algorithm: "enc",
|
||||
KeyID: "id",
|
||||
Crypted: []byte("signingkey"),
|
||||
},
|
||||
PayloadType: target_domain.PayloadTypeJWE,
|
||||
EncryptionKey: encryptionKey,
|
||||
EncryptionKeyID: encryptionKeyID,
|
||||
},
|
||||
signer: mockSigner,
|
||||
},
|
||||
res{
|
||||
body: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
"webhook, JWE no encryption key, error",
|
||||
args{
|
||||
ctx: context.Background(),
|
||||
info: requestContextInfo1,
|
||||
server: &callTestServer{
|
||||
timeout: time.Second,
|
||||
method: http.MethodPost,
|
||||
expectBody: validateJWEPayload([]byte("{\"request\":{\"content\":\"request1\"}}")),
|
||||
respondBody: []byte("{\"content\":\"request2\"}"),
|
||||
statusCode: http.StatusOK,
|
||||
signingKey: "signingkey",
|
||||
},
|
||||
target: target_domain.Target{
|
||||
TargetType: target_domain.TargetTypeWebhook,
|
||||
Timeout: time.Minute,
|
||||
SigningKey: &crypto.CryptoValue{
|
||||
Algorithm: "enc",
|
||||
KeyID: "id",
|
||||
Crypted: []byte("signingkey"),
|
||||
},
|
||||
PayloadType: target_domain.PayloadTypeJWE,
|
||||
},
|
||||
signer: mockSigner,
|
||||
},
|
||||
res{
|
||||
wantErr: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
respBody, err := testServer(t, tt.args.server, testCallTarget(tt.args.ctx, tt.args.info, tt.args.target, crypto.CreateMockEncryptionAlg(gomock.NewController(t))))
|
||||
respBody, err := testServer(
|
||||
t,
|
||||
tt.args.server,
|
||||
testCallTarget(tt.args.ctx, tt.args.info, tt.args.target, crypto.CreateMockEncryptionAlg(gomock.NewController(t)), tt.args.signer, &sync.Map{}),
|
||||
)
|
||||
if tt.res.wantErr {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
@@ -319,10 +419,11 @@ func Test_CallTarget(t *testing.T) {
|
||||
|
||||
func Test_CallTargets(t *testing.T) {
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
info *middleware.ContextInfoRequest
|
||||
servers []*callTestServer
|
||||
targets []target_domain.Target
|
||||
ctx context.Context
|
||||
info *middleware.ContextInfoRequest
|
||||
servers []*callTestServer
|
||||
targets []target_domain.Target
|
||||
getActiveSigningWebKey func(*int32) execution.GetActiveSigningWebKey
|
||||
}
|
||||
type res struct {
|
||||
ret interface{}
|
||||
@@ -341,13 +442,13 @@ func Test_CallTargets(t *testing.T) {
|
||||
servers: []*callTestServer{{
|
||||
timeout: time.Second,
|
||||
method: http.MethodPost,
|
||||
expectBody: requestContextInfoBody1,
|
||||
expectBody: validateJSONPayload(requestContextInfoBody1),
|
||||
respondBody: requestContextInfoBody2,
|
||||
statusCode: http.StatusInternalServerError,
|
||||
}, {
|
||||
timeout: time.Second,
|
||||
method: http.MethodPost,
|
||||
expectBody: requestContextInfoBody1,
|
||||
expectBody: validateJSONPayload(requestContextInfoBody1),
|
||||
respondBody: requestContextInfoBody2,
|
||||
statusCode: http.StatusInternalServerError,
|
||||
}},
|
||||
@@ -368,13 +469,13 @@ func Test_CallTargets(t *testing.T) {
|
||||
servers: []*callTestServer{{
|
||||
timeout: time.Second,
|
||||
method: http.MethodPost,
|
||||
expectBody: requestContextInfoBody1,
|
||||
expectBody: validateJSONPayload(requestContextInfoBody1),
|
||||
respondBody: requestContextInfoBody2,
|
||||
statusCode: http.StatusInternalServerError,
|
||||
}, {
|
||||
timeout: time.Second,
|
||||
method: http.MethodPost,
|
||||
expectBody: requestContextInfoBody1,
|
||||
expectBody: validateJSONPayload(requestContextInfoBody1),
|
||||
respondBody: requestContextInfoBody2,
|
||||
statusCode: http.StatusInternalServerError,
|
||||
}},
|
||||
@@ -395,13 +496,13 @@ func Test_CallTargets(t *testing.T) {
|
||||
servers: []*callTestServer{{
|
||||
timeout: time.Second,
|
||||
method: http.MethodPost,
|
||||
expectBody: requestContextInfoBody1,
|
||||
expectBody: validateJSONPayload(requestContextInfoBody1),
|
||||
respondBody: requestContextInfoBody2,
|
||||
statusCode: http.StatusOK,
|
||||
}, {
|
||||
timeout: time.Second,
|
||||
method: http.MethodPost,
|
||||
expectBody: requestContextInfoBody1,
|
||||
expectBody: validateJSONPayload(requestContextInfoBody1),
|
||||
respondBody: []byte("just a string, not json"),
|
||||
statusCode: http.StatusOK,
|
||||
}},
|
||||
@@ -422,13 +523,13 @@ func Test_CallTargets(t *testing.T) {
|
||||
servers: []*callTestServer{{
|
||||
timeout: time.Second,
|
||||
method: http.MethodPost,
|
||||
expectBody: requestContextInfoBody1,
|
||||
expectBody: validateJSONPayload(requestContextInfoBody1),
|
||||
respondBody: requestContextInfoBody2,
|
||||
statusCode: http.StatusOK,
|
||||
}, {
|
||||
timeout: time.Second,
|
||||
method: http.MethodPost,
|
||||
expectBody: requestContextInfoBody1,
|
||||
expectBody: validateJSONPayload(requestContextInfoBody1),
|
||||
respondBody: []byte("just a string, not json"),
|
||||
statusCode: http.StatusOK,
|
||||
}},
|
||||
@@ -440,12 +541,53 @@ func Test_CallTargets(t *testing.T) {
|
||||
ret: requestContextInfo1.GetContent(),
|
||||
},
|
||||
},
|
||||
{
|
||||
"multiple JWT/JWE targets, ok",
|
||||
args{
|
||||
ctx: context.Background(),
|
||||
info: requestContextInfo1,
|
||||
servers: []*callTestServer{{
|
||||
timeout: time.Second,
|
||||
method: http.MethodPost,
|
||||
expectBody: validateJWTPayload(requestContextInfoBody1),
|
||||
respondBody: requestContextInfoBody2,
|
||||
statusCode: http.StatusOK,
|
||||
}, {
|
||||
timeout: time.Second,
|
||||
method: http.MethodPost,
|
||||
expectBody: validateJSONPayload(requestContextInfoBody1),
|
||||
respondBody: requestContextInfoBody2,
|
||||
statusCode: http.StatusOK,
|
||||
}},
|
||||
targets: []target_domain.Target{
|
||||
{
|
||||
TargetType: target_domain.TargetTypeWebhook,
|
||||
PayloadType: target_domain.PayloadTypeJWT,
|
||||
Timeout: time.Minute,
|
||||
},
|
||||
{
|
||||
TargetType: target_domain.TargetTypeWebhook,
|
||||
PayloadType: target_domain.PayloadTypeJWE,
|
||||
Timeout: time.Minute,
|
||||
},
|
||||
},
|
||||
getActiveSigningWebKey: testActiveSingingWebKey,
|
||||
},
|
||||
res{
|
||||
ret: requestContextInfo1.GetContent(),
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var getWebKeyCalls int32
|
||||
var getActiveSigningWebKey execution.GetActiveSigningWebKey
|
||||
if tt.args.getActiveSigningWebKey != nil {
|
||||
getActiveSigningWebKey = tt.args.getActiveSigningWebKey(&getWebKeyCalls)
|
||||
}
|
||||
respBody, err := testServers(t,
|
||||
tt.args.servers,
|
||||
testCallTargets(tt.args.ctx, tt.args.info, tt.args.targets, crypto.CreateMockEncryptionAlg(gomock.NewController(t))),
|
||||
testCallTargets(tt.args.ctx, tt.args.info, tt.args.targets, crypto.CreateMockEncryptionAlg(gomock.NewController(t)), getActiveSigningWebKey),
|
||||
)
|
||||
if tt.res.wantErr {
|
||||
assert.Error(t, err)
|
||||
@@ -453,17 +595,27 @@ func Test_CallTargets(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
assert.Equal(t, tt.res.ret, respBody)
|
||||
var expectedCalls int32
|
||||
if tt.args.getActiveSigningWebKey != nil {
|
||||
expectedCalls = 1
|
||||
}
|
||||
assert.Equal(t, expectedCalls, atomic.LoadInt32(&getWebKeyCalls))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type callTestServer struct {
|
||||
method string
|
||||
expectBody []byte
|
||||
expectBody func(*testing.T, []byte) bool
|
||||
timeout time.Duration
|
||||
statusCode int
|
||||
respondBody []byte
|
||||
signingKey string
|
||||
called bool
|
||||
}
|
||||
|
||||
func (s *callTestServer) Called() bool {
|
||||
return s.called
|
||||
}
|
||||
|
||||
func testServers(
|
||||
@@ -473,8 +625,8 @@ func testServers(
|
||||
) (interface{}, error) {
|
||||
urls := make([]string, len(c))
|
||||
for i := range c {
|
||||
url, close := listen(t, c[i])
|
||||
defer close()
|
||||
url, closeF, _ := listen(t, c[i])
|
||||
defer closeF()
|
||||
urls[i] = url
|
||||
}
|
||||
return call(urls)
|
||||
@@ -485,16 +637,17 @@ func testServer(
|
||||
c *callTestServer,
|
||||
call func(string) ([]byte, error),
|
||||
) ([]byte, error) {
|
||||
url, close := listen(t, c)
|
||||
defer close()
|
||||
url, closeF, _ := listen(t, c)
|
||||
defer closeF()
|
||||
return call(url)
|
||||
}
|
||||
|
||||
func listen(
|
||||
t *testing.T,
|
||||
c *callTestServer,
|
||||
) (url string, close func()) {
|
||||
) (url string, close func(), called func() bool) {
|
||||
handler := func(w http.ResponseWriter, r *http.Request) {
|
||||
c.called = true
|
||||
checkRequest(t, r, c.method, c.expectBody, c.signingKey)
|
||||
|
||||
if c.statusCode != http.StatusOK {
|
||||
@@ -511,13 +664,15 @@ func listen(
|
||||
}
|
||||
}
|
||||
server := httptest.NewServer(http.HandlerFunc(handler))
|
||||
return server.URL, server.Close
|
||||
return server.URL, server.Close, c.Called
|
||||
}
|
||||
|
||||
func checkRequest(t *testing.T, sent *http.Request, method string, expectedBody []byte, signingKey string) {
|
||||
func checkRequest(t *testing.T, sent *http.Request, method string, checkExpectedBody func(*testing.T, []byte) bool, signingKey string) {
|
||||
sentBody, err := io.ReadAll(sent.Body)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedBody, sentBody)
|
||||
if !checkExpectedBody(t, sentBody) {
|
||||
return
|
||||
}
|
||||
require.Equal(t, method, sent.Method)
|
||||
if signingKey != "" {
|
||||
require.NoError(t, actions.ValidatePayload(sentBody, sent.Header.Get(actions.SigningHeader), signingKey))
|
||||
@@ -534,17 +689,20 @@ func testCallTarget(ctx context.Context,
|
||||
info *middleware.ContextInfoRequest,
|
||||
target target_domain.Target,
|
||||
alg crypto.EncryptionAlgorithm,
|
||||
signerOnce sign.SignerFunc,
|
||||
encrypters *sync.Map,
|
||||
) func(string) ([]byte, error) {
|
||||
return func(url string) (r []byte, err error) {
|
||||
target.Endpoint = url
|
||||
return execution.CallTarget(ctx, target, info, alg)
|
||||
return execution.CallTarget(ctx, target, info, alg, signerOnce, encrypters)
|
||||
}
|
||||
}
|
||||
|
||||
func testCallTargets(ctx context.Context,
|
||||
info *middleware.ContextInfoRequest,
|
||||
info execution.ContextInfo,
|
||||
target []target_domain.Target,
|
||||
alg crypto.EncryptionAlgorithm,
|
||||
activeSigningKey execution.GetActiveSigningWebKey,
|
||||
) func([]string) (interface{}, error) {
|
||||
return func(urls []string) (interface{}, error) {
|
||||
targets := make([]target_domain.Target, len(target))
|
||||
@@ -552,7 +710,7 @@ func testCallTargets(ctx context.Context,
|
||||
t.Endpoint = urls[i]
|
||||
targets[i] = t
|
||||
}
|
||||
return execution.CallTargets(ctx, targets, info, alg)
|
||||
return execution.CallTargets(ctx, targets, info, alg, activeSigningKey)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -717,3 +875,67 @@ func Test_handleResponse(t *testing.T) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
var (
|
||||
privateKey = func() *rsa.PrivateKey {
|
||||
privateKey, _ := rsa.GenerateKey(rand.Reader, 2048)
|
||||
return privateKey
|
||||
}()
|
||||
encryptionKey = func() []byte {
|
||||
data, _ := crypto.PublicKeyToBytes(&privateKey.PublicKey)
|
||||
return data
|
||||
}()
|
||||
encryptionKeyID = "encryption-key-id"
|
||||
signingAlgorithm = jose.RS256
|
||||
)
|
||||
|
||||
func mockSigner(ctx context.Context) (jose.Signer, jose.SignatureAlgorithm, error) {
|
||||
return sign.GetSignerOnce(mockGetActiveSigningWebKey)(ctx)
|
||||
}
|
||||
|
||||
func mockGetActiveSigningWebKey(ctx context.Context) (*jose.JSONWebKey, error) {
|
||||
return &jose.JSONWebKey{
|
||||
Key: privateKey,
|
||||
Algorithm: string(signingAlgorithm),
|
||||
Use: "sig",
|
||||
}, nil
|
||||
}
|
||||
|
||||
func testActiveSingingWebKey(getWebKeyCalls *int32) execution.GetActiveSigningWebKey {
|
||||
return func(ctx context.Context) (*jose.JSONWebKey, error) {
|
||||
atomic.AddInt32(getWebKeyCalls, 1)
|
||||
return mockGetActiveSigningWebKey(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func validateJSONPayload(expected []byte) func(*testing.T, []byte) bool {
|
||||
return func(t *testing.T, actual []byte) bool {
|
||||
require.Equal(t, expected, actual)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func validateJWTPayload(expected []byte) func(*testing.T, []byte) bool {
|
||||
return func(t *testing.T, actual []byte) bool {
|
||||
jws, err := jose.ParseSigned(string(actual), []jose.SignatureAlgorithm{jose.RS256})
|
||||
require.NoError(t, err)
|
||||
payload, err := jws.Verify(privateKey.Public())
|
||||
require.NoError(t, err)
|
||||
return bytes.Equal(expected, payload)
|
||||
}
|
||||
}
|
||||
|
||||
func validateJWEPayload(expected []byte) func(*testing.T, []byte) bool {
|
||||
return func(t *testing.T, actual []byte) bool {
|
||||
parsedJWE, err := jose.ParseEncrypted(string(actual), []jose.KeyAlgorithm{jose.RSA_OAEP_256, jose.ECDH_ES_A256KW}, []jose.ContentEncryption{jose.A256GCM})
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
require.Equal(t, encryptionKeyID, parsedJWE.Header.KeyID)
|
||||
require.Equal(t, "JWT", parsedJWE.Header.ExtraHeaders[jose.HeaderContentType].(string))
|
||||
|
||||
decryptedJWS, err := parsedJWE.Decrypt(privateKey)
|
||||
require.NoError(t, err)
|
||||
return validateJWTPayload(expected)(t, decryptedJWS)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package execution
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/eventstore/handler/v2"
|
||||
@@ -16,9 +17,10 @@ func Register(
|
||||
workerConfig WorkerConfig,
|
||||
queue *queue.Queue,
|
||||
targetEncAlg crypto.EncryptionAlgorithm,
|
||||
activeSigningKey GetActiveSigningWebKey,
|
||||
) {
|
||||
queue.ShouldStart()
|
||||
queue.AddWorkers(NewWorker(workerConfig, targetEncAlg))
|
||||
queue.AddWorkers(NewWorker(workerConfig, targetEncAlg, activeSigningKey, time.Now))
|
||||
}
|
||||
|
||||
func Start(ctx context.Context) {
|
||||
|
||||
@@ -14,6 +14,15 @@ const (
|
||||
TargetTypeAsync
|
||||
)
|
||||
|
||||
type PayloadType uint
|
||||
|
||||
const (
|
||||
PayloadTypeUnspecified PayloadType = iota
|
||||
PayloadTypeJSON
|
||||
PayloadTypeJWT
|
||||
PayloadTypeJWE
|
||||
)
|
||||
|
||||
type Target struct {
|
||||
ExecutionID string `json:"execution_id,omitempty"`
|
||||
TargetID string `json:"target_id,omitempty"`
|
||||
@@ -22,6 +31,9 @@ type Target struct {
|
||||
Timeout time.Duration `json:"timeout,omitempty"`
|
||||
InterruptOnError bool `json:"interrupt_on_error,omitempty"`
|
||||
SigningKey *crypto.CryptoValue `json:"signing_key,omitempty"`
|
||||
PayloadType PayloadType `json:"payload_type,omitempty"`
|
||||
EncryptionKey []byte `json:"encryption_key,omitempty"`
|
||||
EncryptionKeyID string `json:"encryption_key_id,omitempty"`
|
||||
}
|
||||
|
||||
func (e *Target) GetExecutionID() string {
|
||||
@@ -48,3 +60,13 @@ func (e *Target) GetSigningKey(alg crypto.EncryptionAlgorithm) (string, error) {
|
||||
}
|
||||
return crypto.DecryptString(e.SigningKey, alg)
|
||||
}
|
||||
func (e *Target) GetPayloadType() PayloadType {
|
||||
return e.PayloadType
|
||||
}
|
||||
func (e *Target) GetEncryptionKey() []byte {
|
||||
return e.EncryptionKey
|
||||
}
|
||||
|
||||
func (e *Target) GetEncryptionKeyID() string {
|
||||
return e.EncryptionKeyID
|
||||
}
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
package execution
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"reflect"
|
||||
"time"
|
||||
)
|
||||
|
||||
type testServer struct {
|
||||
server *httptest.Server
|
||||
called bool
|
||||
}
|
||||
|
||||
func (s *testServer) URL() string {
|
||||
return s.server.URL
|
||||
}
|
||||
|
||||
func (s *testServer) Close() {
|
||||
s.server.Close()
|
||||
}
|
||||
|
||||
func (s *testServer) Called() bool {
|
||||
return s.called
|
||||
}
|
||||
|
||||
func testServerCall(
|
||||
reqBody interface{},
|
||||
sleep time.Duration,
|
||||
statusCode int,
|
||||
respBody interface{},
|
||||
) (string, func(), func() bool) {
|
||||
server := &testServer{
|
||||
called: false,
|
||||
}
|
||||
|
||||
handler := func(w http.ResponseWriter, r *http.Request) {
|
||||
server.called = true
|
||||
if reqBody != nil {
|
||||
data, err := json.Marshal(reqBody)
|
||||
if err != nil {
|
||||
http.Error(w, "error, marshall: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
sentBody, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
http.Error(w, "error, read body: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(data, sentBody) {
|
||||
http.Error(w, "error, equal:\n"+string(data)+"\nsent:\n"+string(sentBody), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
if statusCode != http.StatusOK {
|
||||
http.Error(w, "error, statusCode", statusCode)
|
||||
return
|
||||
}
|
||||
|
||||
time.Sleep(sleep)
|
||||
|
||||
if respBody != nil {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
resp, err := json.Marshal(respBody)
|
||||
if err != nil {
|
||||
http.Error(w, "error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if _, err := w.Write(resp); err != nil {
|
||||
http.Error(w, "error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if _, err := io.WriteString(w, "finished successfully"); err != nil {
|
||||
http.Error(w, "error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
server.server = httptest.NewServer(http.HandlerFunc(handler))
|
||||
return server.URL(), server.Close, server.Called
|
||||
}
|
||||
@@ -18,9 +18,10 @@ type Worker struct {
|
||||
river.WorkerDefaults[*exec_repo.Request]
|
||||
|
||||
config WorkerConfig
|
||||
now nowFunc
|
||||
now NowFunc
|
||||
|
||||
targetEncAlg crypto.EncryptionAlgorithm
|
||||
targetEncAlg crypto.EncryptionAlgorithm
|
||||
activeSigningKey GetActiveSigningWebKey
|
||||
}
|
||||
|
||||
// Timeout implements the Timeout-function of [river.Worker].
|
||||
@@ -45,7 +46,7 @@ func (w *Worker) Work(ctx context.Context, job *river.Job[*exec_repo.Request]) e
|
||||
return river.JobCancel(fmt.Errorf("unable to unmarshal targets because %w", err))
|
||||
}
|
||||
|
||||
_, err = CallTargets(ctx, targets, exec_repo.ContextInfoFromRequest(job.Args), w.targetEncAlg)
|
||||
_, err = CallTargets(ctx, targets, exec_repo.ContextInfoFromRequest(job.Args), w.targetEncAlg, w.activeSigningKey)
|
||||
if err != nil {
|
||||
// If there is an error returned from the targets, it means that the execution was interrupted
|
||||
return river.JobCancel(fmt.Errorf("interruption during call of targets because %w", err))
|
||||
@@ -53,8 +54,8 @@ func (w *Worker) Work(ctx context.Context, job *river.Job[*exec_repo.Request]) e
|
||||
return nil
|
||||
}
|
||||
|
||||
// nowFunc makes [time.Now] mockable
|
||||
type nowFunc func() time.Time
|
||||
// NowFunc makes [time.Now] mockable
|
||||
type NowFunc func() time.Time
|
||||
|
||||
type WorkerConfig struct {
|
||||
Workers uint8
|
||||
@@ -65,11 +66,14 @@ type WorkerConfig struct {
|
||||
func NewWorker(
|
||||
config WorkerConfig,
|
||||
targetEncAlg crypto.EncryptionAlgorithm,
|
||||
activeSigningKey GetActiveSigningWebKey,
|
||||
now NowFunc,
|
||||
) *Worker {
|
||||
return &Worker{
|
||||
config: config,
|
||||
now: time.Now,
|
||||
targetEncAlg: targetEncAlg,
|
||||
config: config,
|
||||
now: now,
|
||||
targetEncAlg: targetEncAlg,
|
||||
activeSigningKey: activeSigningKey,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package execution
|
||||
package execution_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/execution"
|
||||
target_domain "github.com/zitadel/zitadel/internal/execution/target"
|
||||
"github.com/zitadel/zitadel/internal/repository/action"
|
||||
exec_repo "github.com/zitadel/zitadel/internal/repository/execution"
|
||||
@@ -23,26 +24,44 @@ import (
|
||||
)
|
||||
|
||||
type fieldsWorker struct {
|
||||
now nowFunc
|
||||
now execution.NowFunc
|
||||
}
|
||||
type argsWorker struct {
|
||||
job *river.Job[*exec_repo.Request]
|
||||
}
|
||||
type wantWorker struct {
|
||||
targets []target_domain.Target
|
||||
targets []target
|
||||
sendStatusCode int
|
||||
err assert.ErrorAssertionFunc
|
||||
}
|
||||
|
||||
func newExecutionWorker(f fieldsWorker) *Worker {
|
||||
return &Worker{
|
||||
config: WorkerConfig{
|
||||
type target target_domain.Target
|
||||
|
||||
func (t *target) validate(expectedBody []byte) func(*testing.T, []byte) bool {
|
||||
switch t.PayloadType {
|
||||
case target_domain.PayloadTypeUnspecified,
|
||||
target_domain.PayloadTypeJSON:
|
||||
return validateJSONPayload(expectedBody)
|
||||
case target_domain.PayloadTypeJWT:
|
||||
return validateJWTPayload(expectedBody)
|
||||
case target_domain.PayloadTypeJWE:
|
||||
return validateJWEPayload(expectedBody)
|
||||
default:
|
||||
return validateJSONPayload(expectedBody)
|
||||
}
|
||||
}
|
||||
|
||||
func newExecutionWorker(f fieldsWorker) *execution.Worker {
|
||||
return execution.NewWorker(
|
||||
execution.WorkerConfig{
|
||||
Workers: 1,
|
||||
TransactionDuration: 5 * time.Second,
|
||||
MaxTtl: 5 * time.Minute,
|
||||
},
|
||||
now: f.now,
|
||||
}
|
||||
nil,
|
||||
mockGetActiveSigningWebKey,
|
||||
f.now,
|
||||
)
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -85,7 +104,7 @@ func Test_handleEventExecution(t *testing.T) {
|
||||
},
|
||||
},
|
||||
wantWorker{
|
||||
targets: mockTargets(1),
|
||||
targets: mockTargets(target_domain.PayloadTypeJSON),
|
||||
sendStatusCode: http.StatusOK,
|
||||
err: func(tt assert.TestingT, err error, i ...interface{}) bool {
|
||||
return errors.Is(err, new(river.JobCancelError))
|
||||
@@ -153,7 +172,7 @@ func Test_handleEventExecution(t *testing.T) {
|
||||
},
|
||||
},
|
||||
wantWorker{
|
||||
targets: mockTargets(1),
|
||||
targets: mockTargets(target_domain.PayloadTypeJSON),
|
||||
sendStatusCode: http.StatusOK,
|
||||
err: nil,
|
||||
}
|
||||
@@ -187,7 +206,7 @@ func Test_handleEventExecution(t *testing.T) {
|
||||
},
|
||||
},
|
||||
wantWorker{
|
||||
targets: mockTargets(1),
|
||||
targets: mockTargets(target_domain.PayloadTypeJSON),
|
||||
sendStatusCode: http.StatusBadRequest,
|
||||
err: func(tt assert.TestingT, err error, i ...interface{}) bool {
|
||||
return errors.Is(err, zerrors.ThrowPreconditionFailed(nil, "EXEC-dra6yamk98", "Errors.Execution.Failed"))
|
||||
@@ -223,7 +242,7 @@ func Test_handleEventExecution(t *testing.T) {
|
||||
},
|
||||
},
|
||||
wantWorker{
|
||||
targets: mockTargets(3),
|
||||
targets: mockTargets(target_domain.PayloadTypeJSON, target_domain.PayloadTypeJWT, target_domain.PayloadTypeJWE),
|
||||
sendStatusCode: http.StatusOK,
|
||||
err: nil,
|
||||
}
|
||||
@@ -234,15 +253,19 @@ func Test_handleEventExecution(t *testing.T) {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
f, a, w := tt.test()
|
||||
|
||||
body, err := json.Marshal(exec_repo.ContextInfoFromRequest(a.job.Args))
|
||||
require.NoError(t, err)
|
||||
|
||||
closeFuncs := make([]func(), len(w.targets))
|
||||
calledFuncs := make([]func() bool, len(w.targets))
|
||||
for i := range w.targets {
|
||||
url, closeF, calledF := testServerCall(
|
||||
exec_repo.ContextInfoFromRequest(a.job.Args),
|
||||
time.Second,
|
||||
w.sendStatusCode,
|
||||
nil,
|
||||
)
|
||||
url, closeF, calledF := listen(t, &callTestServer{
|
||||
method: http.MethodPost,
|
||||
expectBody: w.targets[i].validate(body),
|
||||
timeout: time.Second,
|
||||
statusCode: w.sendStatusCode,
|
||||
respondBody: nil,
|
||||
})
|
||||
w.targets[i].Endpoint = url
|
||||
closeFuncs[i] = closeF
|
||||
calledFuncs[i] = calledF
|
||||
@@ -273,24 +296,24 @@ func Test_handleEventExecution(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func mockTarget() target_domain.Target {
|
||||
return target_domain.Target{
|
||||
func mockTarget(payloadType target_domain.PayloadType) target {
|
||||
return target{
|
||||
ExecutionID: "executionID",
|
||||
TargetID: "targetID",
|
||||
TargetType: target_domain.TargetTypeWebhook,
|
||||
Endpoint: "endpoint",
|
||||
Timeout: time.Minute,
|
||||
InterruptOnError: true,
|
||||
PayloadType: payloadType,
|
||||
EncryptionKey: encryptionKey,
|
||||
EncryptionKeyID: encryptionKeyID,
|
||||
}
|
||||
}
|
||||
|
||||
func mockTargets(count int) []target_domain.Target {
|
||||
var targets []target_domain.Target
|
||||
if count > 0 {
|
||||
targets = make([]target_domain.Target, count)
|
||||
for i := range targets {
|
||||
targets[i] = mockTarget()
|
||||
}
|
||||
func mockTargets(payloadTypes ...target_domain.PayloadType) []target {
|
||||
targets := make([]target, len(payloadTypes))
|
||||
for i, payloadType := range payloadTypes {
|
||||
targets[i] = mockTarget(payloadType)
|
||||
}
|
||||
return targets
|
||||
}
|
||||
|
||||
@@ -1176,14 +1176,19 @@ func (i *Instance) DeleteProjectGrantMembership(t *testing.T, ctx context.Contex
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func (i *Instance) CreateTarget(ctx context.Context, t *testing.T, name, endpoint string, ty target_domain.TargetType, interrupt bool) *action.CreateTargetResponse {
|
||||
func (i *Instance) CreateTargetWithoutPayloadType(ctx context.Context, t *testing.T, name, endpoint string, ty target_domain.TargetType, interrupt bool) *action.CreateTargetResponse {
|
||||
return i.CreateTarget(ctx, t, name, endpoint, ty, interrupt, action.PayloadType_PAYLOAD_TYPE_UNSPECIFIED)
|
||||
}
|
||||
|
||||
func (i *Instance) CreateTarget(ctx context.Context, t *testing.T, name, endpoint string, ty target_domain.TargetType, interrupt bool, payloadType action.PayloadType) *action.CreateTargetResponse {
|
||||
if name == "" {
|
||||
name = TargetName()
|
||||
}
|
||||
req := &action.CreateTargetRequest{
|
||||
Name: name,
|
||||
Endpoint: endpoint,
|
||||
Timeout: durationpb.New(5 * time.Second),
|
||||
Name: name,
|
||||
Endpoint: endpoint,
|
||||
Timeout: durationpb.New(5 * time.Second),
|
||||
PayloadType: payloadType,
|
||||
}
|
||||
switch ty {
|
||||
case target_domain.TargetTypeWebhook:
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
http_utils "github.com/zitadel/zitadel/internal/api/http"
|
||||
zoidc "github.com/zitadel/zitadel/internal/api/oidc"
|
||||
"github.com/zitadel/zitadel/internal/api/oidc/sign"
|
||||
"github.com/zitadel/zitadel/internal/command"
|
||||
zcrypto "github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
@@ -147,7 +147,7 @@ func (u *backChannelLogoutNotifier) terminateSession(ctx context.Context, id str
|
||||
return err
|
||||
}
|
||||
|
||||
getSigner := zoidc.GetSignerOnce(u.queries.GetActiveSigningWebKey)
|
||||
getSigner := sign.GetSignerOnce(u.queries.GetActiveSigningWebKey)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(len(sessions.sessions))
|
||||
@@ -170,7 +170,7 @@ func (u *backChannelLogoutNotifier) terminateSession(ctx context.Context, id str
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
func (u *backChannelLogoutNotifier) sendLogoutToken(ctx context.Context, oidcSession *backChannelLogoutOIDCSessions, e eventstore.Event, getSigner zoidc.SignerFunc) error {
|
||||
func (u *backChannelLogoutNotifier) sendLogoutToken(ctx context.Context, oidcSession *backChannelLogoutOIDCSessions, e eventstore.Event, getSigner sign.SignerFunc) error {
|
||||
token, err := u.logoutToken(ctx, oidcSession, getSigner)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -182,7 +182,7 @@ func (u *backChannelLogoutNotifier) sendLogoutToken(ctx context.Context, oidcSes
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *backChannelLogoutNotifier) logoutToken(ctx context.Context, oidcSession *backChannelLogoutOIDCSessions, getSigner zoidc.SignerFunc) (string, error) {
|
||||
func (u *backChannelLogoutNotifier) logoutToken(ctx context.Context, oidcSession *backChannelLogoutOIDCSessions, getSigner sign.SignerFunc) (string, error) {
|
||||
jwtID, err := u.idGenerator.Next()
|
||||
if err != nil {
|
||||
return "", err
|
||||
|
||||
@@ -84,6 +84,10 @@ var (
|
||||
name: projection.AuthNKeyEnabledCol,
|
||||
table: authNKeyTable,
|
||||
}
|
||||
AuthNKeyColumnFingerprint = Column{
|
||||
name: projection.AuthNKeyFingerprintCol,
|
||||
table: authNKeyTable,
|
||||
}
|
||||
)
|
||||
|
||||
type AuthNKeys struct {
|
||||
@@ -100,8 +104,11 @@ type AuthNKey struct {
|
||||
Sequence uint64
|
||||
ApplicationID string
|
||||
|
||||
Expiration time.Time
|
||||
Type domain.AuthNKeyType
|
||||
Expiration time.Time
|
||||
Type domain.AuthNKeyType
|
||||
Enabled bool
|
||||
PublicKey []byte
|
||||
Fingerprint string
|
||||
}
|
||||
|
||||
type AuthNKeysData struct {
|
||||
@@ -141,6 +148,7 @@ const (
|
||||
JoinFilterUnspecified JoinFilter = iota
|
||||
JoinFilterApp
|
||||
JoinFilterUserMachine
|
||||
JoinFilterTarget
|
||||
)
|
||||
|
||||
// SearchAuthNKeys returns machine or app keys, depending on the join filter.
|
||||
@@ -173,11 +181,17 @@ func (q *Queries) searchAuthNKeys(ctx context.Context, queries *AuthNKeySearchQu
|
||||
case JoinFilterUserMachine:
|
||||
query = query.Join(join(MachineUserIDCol, AuthNKeyColumnIdentifier))
|
||||
query = userPermissionCheckV2WithCustomColumns(ctx, query, permissionCheckV2, queries.Queries, AuthNKeyColumnResourceOwner, AuthNKeyColumnIdentifier)
|
||||
case JoinFilterTarget:
|
||||
query = query.Join(join(TargetColumnID, AuthNKeyColumnObjectID))
|
||||
}
|
||||
eq := sq.Eq{
|
||||
AuthNKeyColumnEnabled.identifier(): true,
|
||||
AuthNKeyColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(),
|
||||
}
|
||||
// Until now all disabled keys were just filtered out, but target keys can be explicitly disabled and
|
||||
// the state needs to be returned rather than filtering them out.
|
||||
if joinFilter != JoinFilterTarget {
|
||||
eq[AuthNKeyColumnEnabled.identifier()] = true
|
||||
}
|
||||
|
||||
stmt, args, err := query.Where(eq).ToSql()
|
||||
if err != nil {
|
||||
@@ -283,6 +297,14 @@ func NewAuthNKeyIDQuery(id string) (SearchQuery, error) {
|
||||
return NewTextQuery(AuthNKeyColumnID, id, TextEquals)
|
||||
}
|
||||
|
||||
func NewAuthNKeyInIDsSearchQuery(ids []string) (SearchQuery, error) {
|
||||
return NewInTextQuery(AuthNKeyColumnID, ids)
|
||||
}
|
||||
|
||||
func NewAuthNKeyEnabledSearchQuery(enabled bool) (SearchQuery, error) {
|
||||
return NewBoolQuery(AuthNKeyColumnEnabled, enabled)
|
||||
}
|
||||
|
||||
func NewAuthNKeyIdentifyerQuery(id string) (SearchQuery, error) {
|
||||
return NewTextQuery(AuthNKeyColumnIdentifier, id, TextEquals)
|
||||
}
|
||||
@@ -291,8 +313,19 @@ func NewAuthNKeyCreationDateQuery(ts time.Time, compare TimestampComparison) (Se
|
||||
return NewTimestampQuery(AuthNKeyColumnCreationDate, ts, compare)
|
||||
}
|
||||
|
||||
func NewAuthNKeyExpirationDateDateQuery(ts time.Time, compare TimestampComparison) (SearchQuery, error) {
|
||||
return NewTimestampQuery(AuthNKeyColumnExpiration, ts, compare)
|
||||
func NewAuthNKeyExpirationDateQuery(ts time.Time, compare TimestampComparison) (SearchQuery, error) {
|
||||
query, err := NewTimestampQuery(AuthNKeyColumnExpiration, ts, compare)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if compare != TimestampGreater && compare != TimestampGreaterOrEquals {
|
||||
return query, nil
|
||||
}
|
||||
noExpiration, err := NewIsNullQuery(AuthNKeyColumnExpiration)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewOrQuery(query, noExpiration)
|
||||
}
|
||||
|
||||
//go:embed authn_key_user.sql
|
||||
@@ -344,6 +377,9 @@ func prepareAuthNKeysQuery() (sq.SelectBuilder, func(rows *sql.Rows) (*AuthNKeys
|
||||
AuthNKeyColumnExpiration.identifier(),
|
||||
AuthNKeyColumnType.identifier(),
|
||||
AuthNKeyColumnObjectID.identifier(),
|
||||
AuthNKeyColumnEnabled.identifier(),
|
||||
AuthNKeyColumnPublicKey.identifier(),
|
||||
AuthNKeyColumnFingerprint.identifier(),
|
||||
countColumn.identifier(),
|
||||
).From(authNKeyTable.identifier()).
|
||||
PlaceholderFormat(sq.Dollar)
|
||||
@@ -352,6 +388,8 @@ func prepareAuthNKeysQuery() (sq.SelectBuilder, func(rows *sql.Rows) (*AuthNKeys
|
||||
authNKeys := make([]*AuthNKey, 0)
|
||||
var count uint64
|
||||
for rows.Next() {
|
||||
var expiration sql.NullTime
|
||||
var fingerprint sql.NullString
|
||||
authNKey := new(AuthNKey)
|
||||
err := rows.Scan(
|
||||
&authNKey.ID,
|
||||
@@ -360,14 +398,23 @@ func prepareAuthNKeysQuery() (sq.SelectBuilder, func(rows *sql.Rows) (*AuthNKeys
|
||||
&authNKey.ChangeDate,
|
||||
&authNKey.ResourceOwner,
|
||||
&authNKey.Sequence,
|
||||
&authNKey.Expiration,
|
||||
&expiration,
|
||||
&authNKey.Type,
|
||||
&authNKey.ApplicationID,
|
||||
&authNKey.Enabled,
|
||||
&authNKey.PublicKey,
|
||||
&fingerprint,
|
||||
&count,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if expiration.Valid {
|
||||
authNKey.Expiration = expiration.Time
|
||||
}
|
||||
if fingerprint.Valid {
|
||||
authNKey.Fingerprint = fingerprint.String
|
||||
}
|
||||
authNKeys = append(authNKeys, authNKey)
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -27,6 +28,9 @@ var (
|
||||
` projections.authn_keys2.expiration,` +
|
||||
` projections.authn_keys2.type,` +
|
||||
` projections.authn_keys2.object_id,` +
|
||||
` projections.authn_keys2.enabled,` +
|
||||
` projections.authn_keys2.public_key,` +
|
||||
` projections.authn_keys2.fingerprint,` +
|
||||
` COUNT(*) OVER ()` +
|
||||
` FROM projections.authn_keys2`
|
||||
prepareAuthNKeysCols = []string{
|
||||
@@ -39,6 +43,9 @@ var (
|
||||
"expiration",
|
||||
"type",
|
||||
"object_id",
|
||||
"enabled",
|
||||
"public_key",
|
||||
"fingerprint",
|
||||
"count",
|
||||
}
|
||||
|
||||
@@ -132,6 +139,9 @@ func Test_AuthNKeyPrepares(t *testing.T) {
|
||||
testNow,
|
||||
1,
|
||||
"app1",
|
||||
true,
|
||||
[]byte("public"),
|
||||
"fingerprint",
|
||||
},
|
||||
},
|
||||
),
|
||||
@@ -151,6 +161,9 @@ func Test_AuthNKeyPrepares(t *testing.T) {
|
||||
Expiration: testNow,
|
||||
Type: domain.AuthNKeyTypeJSON,
|
||||
ApplicationID: "app1",
|
||||
Enabled: true,
|
||||
PublicKey: []byte("public"),
|
||||
Fingerprint: "fingerprint",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -173,6 +186,9 @@ func Test_AuthNKeyPrepares(t *testing.T) {
|
||||
testNow,
|
||||
1,
|
||||
"app1",
|
||||
true,
|
||||
[]byte("public1"),
|
||||
"fingerprint1",
|
||||
},
|
||||
{
|
||||
"id-2",
|
||||
@@ -181,9 +197,12 @@ func Test_AuthNKeyPrepares(t *testing.T) {
|
||||
testNow,
|
||||
"ro",
|
||||
uint64(20211109),
|
||||
testNow,
|
||||
nil,
|
||||
1,
|
||||
"app1",
|
||||
true,
|
||||
[]byte("public2"),
|
||||
nil,
|
||||
},
|
||||
},
|
||||
),
|
||||
@@ -203,6 +222,9 @@ func Test_AuthNKeyPrepares(t *testing.T) {
|
||||
Expiration: testNow,
|
||||
Type: domain.AuthNKeyTypeJSON,
|
||||
ApplicationID: "app1",
|
||||
Enabled: true,
|
||||
PublicKey: []byte("public1"),
|
||||
Fingerprint: "fingerprint1",
|
||||
},
|
||||
{
|
||||
ID: "id-2",
|
||||
@@ -211,9 +233,12 @@ func Test_AuthNKeyPrepares(t *testing.T) {
|
||||
ChangeDate: testNow,
|
||||
ResourceOwner: "ro",
|
||||
Sequence: 20211109,
|
||||
Expiration: testNow,
|
||||
Expiration: time.Time{},
|
||||
Type: domain.AuthNKeyTypeJSON,
|
||||
ApplicationID: "app1",
|
||||
Enabled: true,
|
||||
PublicKey: []byte("public2"),
|
||||
Fingerprint: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -35,7 +35,10 @@ with domain as (
|
||||
'endpoint', t.endpoint,
|
||||
'timeout', t.timeout,
|
||||
'interrupt_on_error', t.interrupt_on_error,
|
||||
'signing_key', t.signing_key
|
||||
'signing_key', t.signing_key,
|
||||
'payload_type', t.payload_type,
|
||||
'encryption_key', encode(k.public_key, 'base64'),
|
||||
'encryption_key_id', k.id
|
||||
) as execution_targets
|
||||
from domain d
|
||||
join projections.executions1 e
|
||||
@@ -46,6 +49,11 @@ with domain as (
|
||||
join projections.targets2 t
|
||||
on et.instance_id = t.instance_id
|
||||
and et.target_id = t.id
|
||||
left join projections.authn_keys2 k
|
||||
on k.instance_id = et.instance_id
|
||||
and k.object_id = t.id
|
||||
and k.enabled = true
|
||||
and (k.expiration IS NULL or k.expiration > now())
|
||||
order by et.position asc
|
||||
) as x
|
||||
group by instance_id
|
||||
|
||||
@@ -26,7 +26,10 @@ with features as (
|
||||
'endpoint', t.endpoint,
|
||||
'timeout', t.timeout,
|
||||
'interrupt_on_error', t.interrupt_on_error,
|
||||
'signing_key', t.signing_key
|
||||
'signing_key', t.signing_key,
|
||||
'payload_type', t.payload_type,
|
||||
'encryption_key', encode(k.public_key, 'base64'),
|
||||
'encryption_key_id', k.id
|
||||
) as execution_targets
|
||||
from projections.executions1 e
|
||||
join projections.executions1_targets et
|
||||
@@ -35,6 +38,11 @@ with features as (
|
||||
join projections.targets2 t
|
||||
on et.instance_id = t.instance_id
|
||||
and et.target_id = t.id
|
||||
left join projections.authn_keys2 k
|
||||
on k.instance_id = et.instance_id
|
||||
and k.object_id = t.id
|
||||
and k.enabled = true
|
||||
and (k.expiration IS NULL or k.expiration > now())
|
||||
where e.instance_id = $1
|
||||
order by et.position asc
|
||||
) as x
|
||||
|
||||
@@ -2,6 +2,7 @@ package projection
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
@@ -11,6 +12,7 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/repository/instance"
|
||||
"github.com/zitadel/zitadel/internal/repository/org"
|
||||
"github.com/zitadel/zitadel/internal/repository/project"
|
||||
"github.com/zitadel/zitadel/internal/repository/target"
|
||||
"github.com/zitadel/zitadel/internal/repository/user"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
@@ -30,6 +32,7 @@ const (
|
||||
AuthNKeyPublicKeyCol = "public_key"
|
||||
AuthNKeyTypeCol = "type"
|
||||
AuthNKeyEnabledCol = "enabled"
|
||||
AuthNKeyFingerprintCol = "fingerprint"
|
||||
)
|
||||
|
||||
type authNKeyProjection struct{}
|
||||
@@ -53,11 +56,12 @@ func (*authNKeyProjection) Init() *old_handler.Check {
|
||||
handler.NewColumn(AuthNKeyAggregateIDCol, handler.ColumnTypeText),
|
||||
handler.NewColumn(AuthNKeySequenceCol, handler.ColumnTypeInt64),
|
||||
handler.NewColumn(AuthNKeyObjectIDCol, handler.ColumnTypeText),
|
||||
handler.NewColumn(AuthNKeyExpirationCol, handler.ColumnTypeTimestamp),
|
||||
handler.NewColumn(AuthNKeyExpirationCol, handler.ColumnTypeTimestamp, handler.Nullable()),
|
||||
handler.NewColumn(AuthNKeyIdentifierCol, handler.ColumnTypeText),
|
||||
handler.NewColumn(AuthNKeyPublicKeyCol, handler.ColumnTypeBytes),
|
||||
handler.NewColumn(AuthNKeyEnabledCol, handler.ColumnTypeBool, handler.Default(true)),
|
||||
handler.NewColumn(AuthNKeyTypeCol, handler.ColumnTypeEnum, handler.Default(0)),
|
||||
handler.NewColumn(AuthNKeyFingerprintCol, handler.ColumnTypeText, handler.Nullable()),
|
||||
},
|
||||
handler.NewPrimaryKey(AuthNKeyInstanceIDCol, AuthNKeyIDCol),
|
||||
handler.WithIndex(handler.NewIndex("enabled", []string{AuthNKeyEnabledCol})),
|
||||
@@ -117,6 +121,31 @@ func (p *authNKeyProjection) Reducers() []handler.AggregateReducer {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Aggregate: target.AggregateType,
|
||||
EventReducers: []handler.EventReducer{
|
||||
{
|
||||
Event: target.KeyAddedEventType,
|
||||
Reduce: p.reduceAuthNKeyAdded,
|
||||
},
|
||||
{
|
||||
Event: target.KeyActivatedEventType,
|
||||
Reduce: p.reduceTargetKeyActivated,
|
||||
},
|
||||
{
|
||||
Event: target.KeyDeactivatedEventType,
|
||||
Reduce: p.reduceTargetKeyDeactivated,
|
||||
},
|
||||
{
|
||||
Event: target.KeyRemovedEventType,
|
||||
Reduce: p.reduceAuthNKeyRemoved,
|
||||
},
|
||||
{
|
||||
Event: target.RemovedEventType,
|
||||
Reduce: p.reduceAuthNKeyRemoved,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Aggregate: org.AggregateType,
|
||||
EventReducers: []handler.EventReducer{
|
||||
@@ -141,30 +170,43 @@ func (p *authNKeyProjection) Reducers() []handler.AggregateReducer {
|
||||
func (p *authNKeyProjection) reduceAuthNKeyAdded(event eventstore.Event) (*handler.Statement, error) {
|
||||
var authNKeyEvent struct {
|
||||
eventstore.BaseEvent
|
||||
keyID string
|
||||
objectID string
|
||||
expiration time.Time
|
||||
identifier string
|
||||
publicKey []byte
|
||||
keyType domain.AuthNKeyType
|
||||
keyID string
|
||||
objectID string
|
||||
expiration sql.NullTime
|
||||
identifier string
|
||||
publicKey []byte
|
||||
keyType domain.AuthNKeyType
|
||||
fingerprint sql.NullString
|
||||
enabled bool
|
||||
}
|
||||
switch e := event.(type) {
|
||||
case *project.ApplicationKeyAddedEvent:
|
||||
authNKeyEvent.BaseEvent = e.BaseEvent
|
||||
authNKeyEvent.keyID = e.KeyID
|
||||
authNKeyEvent.objectID = e.AppID
|
||||
authNKeyEvent.expiration = e.ExpirationDate
|
||||
authNKeyEvent.expiration = sql.NullTime{Time: e.ExpirationDate, Valid: true}
|
||||
authNKeyEvent.identifier = e.ClientID
|
||||
authNKeyEvent.publicKey = e.PublicKey
|
||||
authNKeyEvent.keyType = e.KeyType
|
||||
authNKeyEvent.enabled = true
|
||||
case *user.MachineKeyAddedEvent:
|
||||
authNKeyEvent.BaseEvent = e.BaseEvent
|
||||
authNKeyEvent.keyID = e.KeyID
|
||||
authNKeyEvent.objectID = e.Aggregate().ID
|
||||
authNKeyEvent.expiration = e.ExpirationDate
|
||||
authNKeyEvent.expiration = sql.NullTime{Time: e.ExpirationDate, Valid: true}
|
||||
authNKeyEvent.identifier = e.Aggregate().ID
|
||||
authNKeyEvent.publicKey = e.PublicKey
|
||||
authNKeyEvent.keyType = e.KeyType
|
||||
authNKeyEvent.enabled = true
|
||||
case *target.KeyAddedEvent:
|
||||
authNKeyEvent.BaseEvent = e.BaseEvent
|
||||
authNKeyEvent.keyID = e.KeyID
|
||||
authNKeyEvent.objectID = e.Aggregate().ID
|
||||
authNKeyEvent.expiration = sql.NullTime{Time: e.ExpirationDate, Valid: !e.ExpirationDate.IsZero()}
|
||||
authNKeyEvent.identifier = e.Aggregate().ID
|
||||
authNKeyEvent.publicKey = e.PublicKey
|
||||
authNKeyEvent.fingerprint = sql.NullString{String: e.Fingerprint, Valid: true}
|
||||
authNKeyEvent.enabled = false
|
||||
default:
|
||||
return nil, zerrors.ThrowInvalidArgumentf(nil, "PROJE-Dgb32", "reduce.wrong.event.type %v", []eventstore.EventType{project.ApplicationKeyAddedEventType, user.MachineKeyAddedEventType})
|
||||
}
|
||||
@@ -183,6 +225,8 @@ func (p *authNKeyProjection) reduceAuthNKeyAdded(event eventstore.Event) (*handl
|
||||
handler.NewCol(AuthNKeyIdentifierCol, authNKeyEvent.identifier),
|
||||
handler.NewCol(AuthNKeyPublicKeyCol, authNKeyEvent.publicKey),
|
||||
handler.NewCol(AuthNKeyTypeCol, authNKeyEvent.keyType),
|
||||
handler.NewCol(AuthNKeyFingerprintCol, authNKeyEvent.fingerprint),
|
||||
handler.NewCol(AuthNKeyEnabledCol, authNKeyEvent.enabled),
|
||||
},
|
||||
), nil
|
||||
}
|
||||
@@ -239,6 +283,10 @@ func (p *authNKeyProjection) reduceAuthNKeyRemoved(event eventstore.Event) (*han
|
||||
condition = handler.NewCond(AuthNKeyIDCol, e.KeyID)
|
||||
case *user.UserRemovedEvent:
|
||||
condition = handler.NewCond(AuthNKeyAggregateIDCol, e.Aggregate().ID)
|
||||
case *target.KeyRemovedEvent:
|
||||
condition = handler.NewCond(AuthNKeyIDCol, e.KeyID)
|
||||
case *target.RemovedEvent:
|
||||
condition = handler.NewCond(AuthNKeyAggregateIDCol, e.Aggregate().ID)
|
||||
default:
|
||||
return nil, zerrors.ThrowInvalidArgumentf(nil, "PROJE-BGge42", "reduce.wrong.event.type %v", []eventstore.EventType{project.ApplicationKeyRemovedEventType, project.ApplicationRemovedType, project.ProjectRemovedType, user.MachineKeyRemovedEventType, user.UserRemovedType})
|
||||
}
|
||||
@@ -265,3 +313,55 @@ func (p *authNKeyProjection) reduceOwnerRemoved(event eventstore.Event) (*handle
|
||||
},
|
||||
), nil
|
||||
}
|
||||
|
||||
func (p *authNKeyProjection) reduceTargetKeyActivated(event eventstore.Event) (*handler.Statement, error) {
|
||||
e, err := assertEvent[*target.KeyActivatedEvent](event)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return handler.NewMultiStatement(
|
||||
e,
|
||||
handler.AddUpdateStatement(
|
||||
[]handler.Column{
|
||||
handler.NewCol(AuthNKeyEnabledCol, false),
|
||||
handler.NewCol(AuthNKeyChangeDateCol, e.CreationDate()),
|
||||
handler.NewCol(AuthNKeySequenceCol, e.Sequence()),
|
||||
},
|
||||
[]handler.Condition{
|
||||
handler.NewCond(AuthNKeyInstanceIDCol, e.Aggregate().InstanceID),
|
||||
handler.NewCond(AuthNKeyAggregateIDCol, e.Aggregate().ID),
|
||||
handler.NewCond(AuthNKeyEnabledCol, true),
|
||||
},
|
||||
),
|
||||
handler.AddUpdateStatement(
|
||||
[]handler.Column{
|
||||
handler.NewCol(AuthNKeyEnabledCol, true),
|
||||
handler.NewCol(AuthNKeyChangeDateCol, e.CreationDate()),
|
||||
handler.NewCol(AuthNKeySequenceCol, e.Sequence()),
|
||||
},
|
||||
[]handler.Condition{
|
||||
handler.NewCond(AuthNKeyIDCol, e.KeyID),
|
||||
handler.NewCond(AuthNKeyInstanceIDCol, e.Aggregate().InstanceID),
|
||||
},
|
||||
),
|
||||
), nil
|
||||
}
|
||||
|
||||
func (p *authNKeyProjection) reduceTargetKeyDeactivated(event eventstore.Event) (*handler.Statement, error) {
|
||||
e, err := assertEvent[*target.KeyDeactivatedEvent](event)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return handler.NewUpdateStatement(
|
||||
e,
|
||||
[]handler.Column{
|
||||
handler.NewCol(AuthNKeyEnabledCol, false),
|
||||
handler.NewCol(AuthNKeyChangeDateCol, e.CreationDate()),
|
||||
handler.NewCol(AuthNKeySequenceCol, e.Sequence()),
|
||||
},
|
||||
[]handler.Condition{
|
||||
handler.NewCond(AuthNKeyIDCol, e.KeyID),
|
||||
handler.NewCond(AuthNKeyInstanceIDCol, e.Aggregate().InstanceID),
|
||||
},
|
||||
), nil
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package projection
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"testing"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
@@ -9,6 +10,7 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/repository/instance"
|
||||
"github.com/zitadel/zitadel/internal/repository/org"
|
||||
"github.com/zitadel/zitadel/internal/repository/project"
|
||||
"github.com/zitadel/zitadel/internal/repository/target"
|
||||
"github.com/zitadel/zitadel/internal/repository/user"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
@@ -40,7 +42,7 @@ func TestAuthNKeyProjection_reduces(t *testing.T) {
|
||||
executer: &testExecuter{
|
||||
executions: []execution{
|
||||
{
|
||||
expectedStmt: "INSERT INTO projections.authn_keys2 (id, creation_date, change_date, resource_owner, instance_id, aggregate_id, sequence, object_id, expiration, identifier, public_key, type) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)",
|
||||
expectedStmt: "INSERT INTO projections.authn_keys2 (id, creation_date, change_date, resource_owner, instance_id, aggregate_id, sequence, object_id, expiration, identifier, public_key, type, fingerprint, enabled) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)",
|
||||
expectedArgs: []interface{}{
|
||||
"keyId",
|
||||
anyArg{},
|
||||
@@ -54,6 +56,8 @@ func TestAuthNKeyProjection_reduces(t *testing.T) {
|
||||
"clientId",
|
||||
[]byte("publicKey"),
|
||||
domain.AuthNKeyTypeJSON,
|
||||
sql.NullString{}, // fingerprint is not set for apps
|
||||
true, // apps keys are enabled by default
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -77,7 +81,7 @@ func TestAuthNKeyProjection_reduces(t *testing.T) {
|
||||
executer: &testExecuter{
|
||||
executions: []execution{
|
||||
{
|
||||
expectedStmt: "INSERT INTO projections.authn_keys2 (id, creation_date, change_date, resource_owner, instance_id, aggregate_id, sequence, object_id, expiration, identifier, public_key, type) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)",
|
||||
expectedStmt: "INSERT INTO projections.authn_keys2 (id, creation_date, change_date, resource_owner, instance_id, aggregate_id, sequence, object_id, expiration, identifier, public_key, type, fingerprint, enabled) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)",
|
||||
expectedArgs: []interface{}{
|
||||
"keyId",
|
||||
anyArg{},
|
||||
@@ -91,6 +95,47 @@ func TestAuthNKeyProjection_reduces(t *testing.T) {
|
||||
"agg-id",
|
||||
[]byte("publicKey"),
|
||||
domain.AuthNKeyTypeJSON,
|
||||
sql.NullString{}, // fingerprint is not set for users
|
||||
true, // user keys are enabled by default
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "reduceAuthNKeyAdded target",
|
||||
args: args{
|
||||
event: getEvent(
|
||||
testEvent(
|
||||
target.KeyAddedEventType,
|
||||
target.AggregateType,
|
||||
[]byte(`{"keyId": "keyId", "expirationDate": "2021-11-30T15:00:00Z", "publicKey": "cHVibGljS2V5", "fingerprint": "fingerprint"}`),
|
||||
), eventstore.GenericEventMapper[target.KeyAddedEvent]),
|
||||
},
|
||||
reduce: (&authNKeyProjection{}).reduceAuthNKeyAdded,
|
||||
want: wantReduce{
|
||||
aggregateType: eventstore.AggregateType("target"),
|
||||
sequence: 15,
|
||||
executer: &testExecuter{
|
||||
executions: []execution{
|
||||
{
|
||||
expectedStmt: "INSERT INTO projections.authn_keys2 (id, creation_date, change_date, resource_owner, instance_id, aggregate_id, sequence, object_id, expiration, identifier, public_key, type, fingerprint, enabled) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)",
|
||||
expectedArgs: []interface{}{
|
||||
"keyId",
|
||||
anyArg{},
|
||||
anyArg{},
|
||||
"ro-id",
|
||||
"instance-id",
|
||||
"agg-id",
|
||||
uint64(15),
|
||||
"agg-id",
|
||||
anyArg{},
|
||||
"agg-id",
|
||||
[]byte("publicKey"),
|
||||
domain.AuthNKeyTypeNONE,
|
||||
sql.NullString{String: "fingerprint", Valid: true},
|
||||
false, // target keys are disabled by default
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -470,6 +515,60 @@ func TestAuthNKeyProjection_reduces(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "reduceAuthNKeyRemoved target key removed",
|
||||
args: args{
|
||||
event: getEvent(
|
||||
testEvent(
|
||||
target.KeyRemovedEventType,
|
||||
target.AggregateType,
|
||||
[]byte(`{"keyId": "keyId"}`),
|
||||
), eventstore.GenericEventMapper[target.KeyRemovedEvent]),
|
||||
},
|
||||
reduce: (&authNKeyProjection{}).reduceAuthNKeyRemoved,
|
||||
want: wantReduce{
|
||||
aggregateType: eventstore.AggregateType("target"),
|
||||
sequence: 15,
|
||||
executer: &testExecuter{
|
||||
executions: []execution{
|
||||
{
|
||||
expectedStmt: "DELETE FROM projections.authn_keys2 WHERE (id = $1) AND (instance_id = $2)",
|
||||
expectedArgs: []interface{}{
|
||||
"keyId",
|
||||
"instance-id",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "reduceAuthNKeyRemoved target removed",
|
||||
args: args{
|
||||
event: getEvent(
|
||||
testEvent(
|
||||
target.RemovedEventType,
|
||||
target.AggregateType,
|
||||
[]byte(`{"keyId": "keyId"}`),
|
||||
), eventstore.GenericEventMapper[target.RemovedEvent]),
|
||||
},
|
||||
reduce: (&authNKeyProjection{}).reduceAuthNKeyRemoved,
|
||||
want: wantReduce{
|
||||
aggregateType: eventstore.AggregateType("target"),
|
||||
sequence: 15,
|
||||
executer: &testExecuter{
|
||||
executions: []execution{
|
||||
{
|
||||
expectedStmt: "DELETE FROM projections.authn_keys2 WHERE (aggregate_id = $1) AND (instance_id = $2)",
|
||||
expectedArgs: []interface{}{
|
||||
"agg-id",
|
||||
"instance-id",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "reduceOwnerRemoved",
|
||||
args: args{
|
||||
@@ -497,6 +596,77 @@ func TestAuthNKeyProjection_reduces(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "reduceTargetKeyActivated",
|
||||
args: args{
|
||||
event: getEvent(
|
||||
testEvent(
|
||||
target.KeyActivatedEventType,
|
||||
target.AggregateType,
|
||||
[]byte(`{"keyId": "keyId"}`),
|
||||
), eventstore.GenericEventMapper[target.KeyActivatedEvent]),
|
||||
},
|
||||
reduce: (&authNKeyProjection{}).reduceTargetKeyActivated,
|
||||
want: wantReduce{
|
||||
aggregateType: eventstore.AggregateType("target"),
|
||||
sequence: 15,
|
||||
executer: &testExecuter{
|
||||
executions: []execution{
|
||||
{
|
||||
expectedStmt: "UPDATE projections.authn_keys2 SET (enabled, change_date, sequence) = ($1, $2, $3) WHERE (instance_id = $4) AND (aggregate_id = $5) AND (enabled = $6)",
|
||||
expectedArgs: []interface{}{
|
||||
false,
|
||||
anyArg{},
|
||||
uint64(15),
|
||||
"instance-id",
|
||||
"agg-id",
|
||||
true,
|
||||
},
|
||||
},
|
||||
{
|
||||
expectedStmt: "UPDATE projections.authn_keys2 SET (enabled, change_date, sequence) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)",
|
||||
expectedArgs: []interface{}{
|
||||
true,
|
||||
anyArg{},
|
||||
uint64(15),
|
||||
"keyId",
|
||||
"instance-id",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "reduceTargetKeyDeactivated",
|
||||
args: args{
|
||||
event: getEvent(
|
||||
testEvent(
|
||||
target.KeyDeactivatedEventType,
|
||||
target.AggregateType,
|
||||
[]byte(`{"keyId": "keyId"}`),
|
||||
), eventstore.GenericEventMapper[target.KeyDeactivatedEvent]),
|
||||
},
|
||||
reduce: (&authNKeyProjection{}).reduceTargetKeyDeactivated,
|
||||
want: wantReduce{
|
||||
aggregateType: eventstore.AggregateType("target"),
|
||||
sequence: 15,
|
||||
executer: &testExecuter{
|
||||
executions: []execution{
|
||||
{
|
||||
expectedStmt: "UPDATE projections.authn_keys2 SET (enabled, change_date, sequence) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)",
|
||||
expectedArgs: []interface{}{
|
||||
false,
|
||||
anyArg{},
|
||||
uint64(15),
|
||||
"keyId",
|
||||
"instance-id",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
old_handler "github.com/zitadel/zitadel/internal/eventstore/handler"
|
||||
"github.com/zitadel/zitadel/internal/eventstore/handler/v2"
|
||||
target_domain "github.com/zitadel/zitadel/internal/execution/target"
|
||||
"github.com/zitadel/zitadel/internal/repository/instance"
|
||||
"github.com/zitadel/zitadel/internal/repository/target"
|
||||
)
|
||||
@@ -24,6 +25,7 @@ const (
|
||||
TargetTimeoutCol = "timeout"
|
||||
TargetInterruptOnErrorCol = "interrupt_on_error"
|
||||
TargetSigningKey = "signing_key"
|
||||
TargetPayloadType = "payload_type"
|
||||
)
|
||||
|
||||
type targetProjection struct{}
|
||||
@@ -51,6 +53,7 @@ func (*targetProjection) Init() *old_handler.Check {
|
||||
handler.NewColumn(TargetTimeoutCol, handler.ColumnTypeInt64),
|
||||
handler.NewColumn(TargetInterruptOnErrorCol, handler.ColumnTypeBool),
|
||||
handler.NewColumn(TargetSigningKey, handler.ColumnTypeJSONB, handler.Nullable()),
|
||||
handler.NewColumn(TargetPayloadType, handler.ColumnTypeEnum, handler.Default(target_domain.PayloadTypeUnspecified)),
|
||||
},
|
||||
handler.NewPrimaryKey(TargetInstanceIDCol, TargetIDCol),
|
||||
),
|
||||
@@ -108,6 +111,7 @@ func (p *targetProjection) reduceTargetAdded(event eventstore.Event) (*handler.S
|
||||
handler.NewCol(TargetTimeoutCol, e.Timeout),
|
||||
handler.NewCol(TargetInterruptOnErrorCol, e.InterruptOnError),
|
||||
handler.NewCol(TargetSigningKey, e.SigningKey),
|
||||
handler.NewCol(TargetPayloadType, e.PayloadType),
|
||||
},
|
||||
), nil
|
||||
}
|
||||
@@ -140,6 +144,9 @@ func (p *targetProjection) reduceTargetChanged(event eventstore.Event) (*handler
|
||||
if e.SigningKey != nil {
|
||||
values = append(values, handler.NewCol(TargetSigningKey, e.SigningKey))
|
||||
}
|
||||
if e.PayloadType != target_domain.PayloadTypeUnspecified {
|
||||
values = append(values, handler.NewCol(TargetPayloadType, e.PayloadType))
|
||||
}
|
||||
return handler.NewUpdateStatement(
|
||||
e,
|
||||
values,
|
||||
|
||||
@@ -29,7 +29,7 @@ func TestTargetProjection_reduces(t *testing.T) {
|
||||
testEvent(
|
||||
target.AddedEventType,
|
||||
target.AggregateType,
|
||||
[]byte(`{"name": "name", "targetType":0, "endpoint":"https://example.com", "timeout": 3000000000, "async": true, "interruptOnError": true, "signingKey": { "cryptoType": 0, "algorithm": "RSA-265", "keyId": "key-id" }}`),
|
||||
[]byte(`{"name": "name", "targetType":0, "endpoint":"https://example.com", "timeout": 3000000000, "async": true, "interruptOnError": true, "signingKey": { "cryptoType": 0, "algorithm": "RSA-265", "keyId": "key-id" }, "payloadType": 1}`),
|
||||
),
|
||||
eventstore.GenericEventMapper[target.AddedEvent],
|
||||
),
|
||||
@@ -41,7 +41,7 @@ func TestTargetProjection_reduces(t *testing.T) {
|
||||
executer: &testExecuter{
|
||||
executions: []execution{
|
||||
{
|
||||
expectedStmt: "INSERT INTO projections.targets2 (instance_id, resource_owner, id, creation_date, change_date, sequence, name, endpoint, target_type, timeout, interrupt_on_error, signing_key) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)",
|
||||
expectedStmt: "INSERT INTO projections.targets2 (instance_id, resource_owner, id, creation_date, change_date, sequence, name, endpoint, target_type, timeout, interrupt_on_error, signing_key, payload_type) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)",
|
||||
expectedArgs: []interface{}{
|
||||
"instance-id",
|
||||
"ro-id",
|
||||
@@ -55,6 +55,7 @@ func TestTargetProjection_reduces(t *testing.T) {
|
||||
3 * time.Second,
|
||||
true,
|
||||
anyArg{},
|
||||
target_domain.PayloadTypeJSON,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -68,7 +69,7 @@ func TestTargetProjection_reduces(t *testing.T) {
|
||||
testEvent(
|
||||
target.ChangedEventType,
|
||||
target.AggregateType,
|
||||
[]byte(`{"name": "name2", "targetType":0, "endpoint":"https://example.com", "timeout": 3000000000, "async": true, "interruptOnError": true, "signingKey": { "cryptoType": 0, "algorithm": "RSA-265", "keyId": "key-id" }}`),
|
||||
[]byte(`{"name": "name2", "targetType":0, "endpoint":"https://example.com", "timeout": 3000000000, "async": true, "interruptOnError": true, "signingKey": { "cryptoType": 0, "algorithm": "RSA-265", "keyId": "key-id" }, "payloadType": 1}`),
|
||||
),
|
||||
eventstore.GenericEventMapper[target.ChangedEvent],
|
||||
),
|
||||
@@ -80,7 +81,7 @@ func TestTargetProjection_reduces(t *testing.T) {
|
||||
executer: &testExecuter{
|
||||
executions: []execution{
|
||||
{
|
||||
expectedStmt: "UPDATE projections.targets2 SET (change_date, sequence, resource_owner, name, target_type, endpoint, timeout, interrupt_on_error, signing_key) = ($1, $2, $3, $4, $5, $6, $7, $8, $9) WHERE (instance_id = $10) AND (id = $11)",
|
||||
expectedStmt: "UPDATE projections.targets2 SET (change_date, sequence, resource_owner, name, target_type, endpoint, timeout, interrupt_on_error, signing_key, payload_type) = ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) WHERE (instance_id = $11) AND (id = $12)",
|
||||
expectedArgs: []interface{}{
|
||||
anyArg{},
|
||||
uint64(15),
|
||||
@@ -91,6 +92,7 @@ func TestTargetProjection_reduces(t *testing.T) {
|
||||
3 * time.Second,
|
||||
true,
|
||||
anyArg{},
|
||||
target_domain.PayloadTypeJSON,
|
||||
"instance-id",
|
||||
"agg-id",
|
||||
},
|
||||
|
||||
@@ -65,6 +65,10 @@ var (
|
||||
name: projection.TargetSigningKey,
|
||||
table: targetTable,
|
||||
}
|
||||
TargetColumnPayloadType = Column{
|
||||
name: projection.TargetPayloadType,
|
||||
table: targetTable,
|
||||
}
|
||||
)
|
||||
|
||||
type Targets struct {
|
||||
@@ -86,6 +90,7 @@ type Target struct {
|
||||
InterruptOnError bool
|
||||
signingKey *crypto.CryptoValue
|
||||
SigningKey string
|
||||
PayloadType target_domain.PayloadType
|
||||
}
|
||||
|
||||
func (t *Target) decryptSigningKey(alg crypto.EncryptionAlgorithm) error {
|
||||
@@ -166,6 +171,7 @@ func prepareTargetsQuery() (sq.SelectBuilder, func(rows *sql.Rows) (*Targets, er
|
||||
TargetColumnURL.identifier(),
|
||||
TargetColumnInterruptOnError.identifier(),
|
||||
TargetColumnSigningKey.identifier(),
|
||||
TargetColumnPayloadType.identifier(),
|
||||
countColumn.identifier(),
|
||||
).From(targetTable.identifier()).
|
||||
PlaceholderFormat(sq.Dollar),
|
||||
@@ -185,6 +191,7 @@ func prepareTargetsQuery() (sq.SelectBuilder, func(rows *sql.Rows) (*Targets, er
|
||||
&target.Endpoint,
|
||||
&target.InterruptOnError,
|
||||
&target.signingKey,
|
||||
&target.PayloadType,
|
||||
&count,
|
||||
)
|
||||
if err != nil {
|
||||
@@ -218,6 +225,7 @@ func prepareTargetQuery() (sq.SelectBuilder, func(row *sql.Row) (*Target, error)
|
||||
TargetColumnURL.identifier(),
|
||||
TargetColumnInterruptOnError.identifier(),
|
||||
TargetColumnSigningKey.identifier(),
|
||||
TargetColumnPayloadType.identifier(),
|
||||
).From(targetTable.identifier()).
|
||||
PlaceholderFormat(sq.Dollar),
|
||||
func(row *sql.Row) (*Target, error) {
|
||||
@@ -233,6 +241,7 @@ func prepareTargetQuery() (sq.SelectBuilder, func(row *sql.Row) (*Target, error)
|
||||
&target.Endpoint,
|
||||
&target.InterruptOnError,
|
||||
&target.signingKey,
|
||||
&target.PayloadType,
|
||||
)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
|
||||
@@ -26,6 +26,7 @@ var (
|
||||
` projections.targets2.endpoint,` +
|
||||
` projections.targets2.interrupt_on_error,` +
|
||||
` projections.targets2.signing_key,` +
|
||||
` projections.targets2.payload_type,` +
|
||||
` COUNT(*) OVER ()` +
|
||||
` FROM projections.targets2`
|
||||
prepareTargetsCols = []string{
|
||||
@@ -39,6 +40,7 @@ var (
|
||||
"endpoint",
|
||||
"interrupt_on_error",
|
||||
"signing_key",
|
||||
"payload_type",
|
||||
"count",
|
||||
}
|
||||
|
||||
@@ -51,7 +53,8 @@ var (
|
||||
` projections.targets2.timeout,` +
|
||||
` projections.targets2.endpoint,` +
|
||||
` projections.targets2.interrupt_on_error,` +
|
||||
` projections.targets2.signing_key` +
|
||||
` projections.targets2.signing_key,` +
|
||||
` projections.targets2.payload_type` +
|
||||
` FROM projections.targets2`
|
||||
prepareTargetCols = []string{
|
||||
"id",
|
||||
@@ -64,6 +67,7 @@ var (
|
||||
"endpoint",
|
||||
"interrupt_on_error",
|
||||
"signing_key",
|
||||
"payload_type",
|
||||
}
|
||||
)
|
||||
|
||||
@@ -114,6 +118,7 @@ func Test_TargetPrepares(t *testing.T) {
|
||||
KeyID: "encKey",
|
||||
Crypted: []byte("crypted"),
|
||||
},
|
||||
target_domain.PayloadTypeJSON,
|
||||
},
|
||||
},
|
||||
),
|
||||
@@ -141,6 +146,7 @@ func Test_TargetPrepares(t *testing.T) {
|
||||
KeyID: "encKey",
|
||||
Crypted: []byte("crypted"),
|
||||
},
|
||||
PayloadType: target_domain.PayloadTypeJSON,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -169,6 +175,7 @@ func Test_TargetPrepares(t *testing.T) {
|
||||
KeyID: "encKey",
|
||||
Crypted: []byte("crypted"),
|
||||
},
|
||||
target_domain.PayloadTypeJSON,
|
||||
},
|
||||
{
|
||||
"id-2",
|
||||
@@ -186,6 +193,7 @@ func Test_TargetPrepares(t *testing.T) {
|
||||
KeyID: "encKey",
|
||||
Crypted: []byte("crypted"),
|
||||
},
|
||||
target_domain.PayloadTypeJWT,
|
||||
},
|
||||
{
|
||||
"id-3",
|
||||
@@ -203,6 +211,7 @@ func Test_TargetPrepares(t *testing.T) {
|
||||
KeyID: "encKey",
|
||||
Crypted: []byte("crypted"),
|
||||
},
|
||||
target_domain.PayloadTypeJWE,
|
||||
},
|
||||
},
|
||||
),
|
||||
@@ -230,6 +239,7 @@ func Test_TargetPrepares(t *testing.T) {
|
||||
KeyID: "encKey",
|
||||
Crypted: []byte("crypted"),
|
||||
},
|
||||
PayloadType: target_domain.PayloadTypeJSON,
|
||||
},
|
||||
{
|
||||
ObjectDetails: domain.ObjectDetails{
|
||||
@@ -249,6 +259,7 @@ func Test_TargetPrepares(t *testing.T) {
|
||||
KeyID: "encKey",
|
||||
Crypted: []byte("crypted"),
|
||||
},
|
||||
PayloadType: target_domain.PayloadTypeJWT,
|
||||
},
|
||||
{
|
||||
ObjectDetails: domain.ObjectDetails{
|
||||
@@ -268,6 +279,7 @@ func Test_TargetPrepares(t *testing.T) {
|
||||
KeyID: "encKey",
|
||||
Crypted: []byte("crypted"),
|
||||
},
|
||||
PayloadType: target_domain.PayloadTypeJWE,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -330,6 +342,7 @@ func Test_TargetPrepares(t *testing.T) {
|
||||
KeyID: "encKey",
|
||||
Crypted: []byte("crypted"),
|
||||
},
|
||||
target_domain.PayloadTypeJSON,
|
||||
},
|
||||
),
|
||||
},
|
||||
@@ -351,6 +364,7 @@ func Test_TargetPrepares(t *testing.T) {
|
||||
KeyID: "encKey",
|
||||
Crypted: []byte("crypted"),
|
||||
},
|
||||
PayloadType: target_domain.PayloadTypeJSON,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -6,4 +6,8 @@ func init() {
|
||||
eventstore.RegisterFilterEventMapper(AggregateType, AddedEventType, eventstore.GenericEventMapper[AddedEvent])
|
||||
eventstore.RegisterFilterEventMapper(AggregateType, ChangedEventType, eventstore.GenericEventMapper[ChangedEvent])
|
||||
eventstore.RegisterFilterEventMapper(AggregateType, RemovedEventType, eventstore.GenericEventMapper[RemovedEvent])
|
||||
eventstore.RegisterFilterEventMapper(AggregateType, KeyAddedEventType, eventstore.GenericEventMapper[KeyAddedEvent])
|
||||
eventstore.RegisterFilterEventMapper(AggregateType, KeyActivatedEventType, eventstore.GenericEventMapper[KeyActivatedEvent])
|
||||
eventstore.RegisterFilterEventMapper(AggregateType, KeyDeactivatedEventType, eventstore.GenericEventMapper[KeyDeactivatedEvent])
|
||||
eventstore.RegisterFilterEventMapper(AggregateType, KeyRemovedEventType, eventstore.GenericEventMapper[KeyRemovedEvent])
|
||||
}
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
package target
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
)
|
||||
|
||||
const (
|
||||
keyEventTypePrefix eventstore.EventType = "target.key."
|
||||
KeyAddedEventType = keyEventTypePrefix + "added"
|
||||
KeyActivatedEventType = keyEventTypePrefix + "activated"
|
||||
KeyDeactivatedEventType = keyEventTypePrefix + "deactivated"
|
||||
KeyRemovedEventType = keyEventTypePrefix + "removed"
|
||||
)
|
||||
|
||||
type KeyAddedEvent struct {
|
||||
eventstore.BaseEvent `json:"-"`
|
||||
|
||||
KeyID string `json:"keyId,omitempty"`
|
||||
PublicKey []byte `json:"publicKey,omitempty"`
|
||||
Fingerprint string `json:"fingerprint,omitempty"`
|
||||
ExpirationDate time.Time `json:"expirationDate,omitzero"`
|
||||
}
|
||||
|
||||
func (e *KeyAddedEvent) SetBaseEvent(b *eventstore.BaseEvent) {
|
||||
e.BaseEvent = *b
|
||||
}
|
||||
|
||||
func (e *KeyAddedEvent) Payload() any {
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *KeyAddedEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewKeyAddedEvent(
|
||||
ctx context.Context,
|
||||
aggregate *eventstore.Aggregate,
|
||||
keyID string,
|
||||
publicKey []byte,
|
||||
fingerprint string,
|
||||
expiration time.Time,
|
||||
) *KeyAddedEvent {
|
||||
return &KeyAddedEvent{
|
||||
BaseEvent: *eventstore.NewBaseEventForPush(
|
||||
ctx, aggregate, KeyAddedEventType,
|
||||
),
|
||||
KeyID: keyID,
|
||||
PublicKey: publicKey,
|
||||
Fingerprint: fingerprint,
|
||||
ExpirationDate: expiration,
|
||||
}
|
||||
}
|
||||
|
||||
type KeyActivatedEvent struct {
|
||||
eventstore.BaseEvent `json:"-"`
|
||||
|
||||
KeyID string `json:"keyId,omitempty"`
|
||||
}
|
||||
|
||||
func (e *KeyActivatedEvent) SetBaseEvent(b *eventstore.BaseEvent) {
|
||||
e.BaseEvent = *b
|
||||
}
|
||||
|
||||
func (e *KeyActivatedEvent) Payload() any {
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *KeyActivatedEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewKeyActivatedEvent(ctx context.Context, aggregate *eventstore.Aggregate, id string) *KeyActivatedEvent {
|
||||
return &KeyActivatedEvent{*eventstore.NewBaseEventForPush(ctx, aggregate, KeyActivatedEventType), id}
|
||||
}
|
||||
|
||||
type KeyDeactivatedEvent struct {
|
||||
eventstore.BaseEvent `json:"-"`
|
||||
|
||||
KeyID string `json:"keyId,omitempty"`
|
||||
}
|
||||
|
||||
func (e *KeyDeactivatedEvent) SetBaseEvent(b *eventstore.BaseEvent) {
|
||||
e.BaseEvent = *b
|
||||
}
|
||||
|
||||
func (e *KeyDeactivatedEvent) Payload() any {
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *KeyDeactivatedEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewKeyDeactivatedEvent(ctx context.Context, aggregate *eventstore.Aggregate, id string) *KeyDeactivatedEvent {
|
||||
return &KeyDeactivatedEvent{*eventstore.NewBaseEventForPush(ctx, aggregate, KeyDeactivatedEventType), id}
|
||||
}
|
||||
|
||||
type KeyRemovedEvent struct {
|
||||
eventstore.BaseEvent `json:"-"`
|
||||
|
||||
KeyID string `json:"keyId,omitempty"`
|
||||
}
|
||||
|
||||
func (e *KeyRemovedEvent) SetBaseEvent(b *eventstore.BaseEvent) {
|
||||
e.BaseEvent = *b
|
||||
}
|
||||
|
||||
func (e *KeyRemovedEvent) Payload() any {
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *KeyRemovedEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewKeyRemovedEvent(ctx context.Context, aggregate *eventstore.Aggregate, id string) *KeyRemovedEvent {
|
||||
return &KeyRemovedEvent{*eventstore.NewBaseEventForPush(ctx, aggregate, KeyRemovedEventType), id}
|
||||
}
|
||||
@@ -19,12 +19,13 @@ const (
|
||||
type AddedEvent struct {
|
||||
eventstore.BaseEvent `json:"-"`
|
||||
|
||||
Name string `json:"name"`
|
||||
TargetType target_domain.TargetType `json:"targetType"`
|
||||
Endpoint string `json:"endpoint"`
|
||||
Timeout time.Duration `json:"timeout"`
|
||||
InterruptOnError bool `json:"interruptOnError"`
|
||||
SigningKey *crypto.CryptoValue `json:"signingKey"`
|
||||
Name string `json:"name"`
|
||||
TargetType target_domain.TargetType `json:"targetType"`
|
||||
Endpoint string `json:"endpoint"`
|
||||
Timeout time.Duration `json:"timeout"`
|
||||
InterruptOnError bool `json:"interruptOnError"`
|
||||
SigningKey *crypto.CryptoValue `json:"signingKey"`
|
||||
PayloadType target_domain.PayloadType `json:"payloadType"`
|
||||
}
|
||||
|
||||
func (e *AddedEvent) SetBaseEvent(b *eventstore.BaseEvent) {
|
||||
@@ -48,12 +49,20 @@ func NewAddedEvent(
|
||||
timeout time.Duration,
|
||||
interruptOnError bool,
|
||||
signingKey *crypto.CryptoValue,
|
||||
payloadType target_domain.PayloadType,
|
||||
) *AddedEvent {
|
||||
return &AddedEvent{
|
||||
*eventstore.NewBaseEventForPush(
|
||||
ctx, aggregate, AddedEventType,
|
||||
),
|
||||
name, targetType, endpoint, timeout, interruptOnError, signingKey}
|
||||
name,
|
||||
targetType,
|
||||
endpoint,
|
||||
timeout,
|
||||
interruptOnError,
|
||||
signingKey,
|
||||
payloadType,
|
||||
}
|
||||
}
|
||||
|
||||
type ChangedEvent struct {
|
||||
@@ -65,6 +74,7 @@ type ChangedEvent struct {
|
||||
Timeout *time.Duration `json:"timeout,omitempty"`
|
||||
InterruptOnError *bool `json:"interruptOnError,omitempty"`
|
||||
SigningKey *crypto.CryptoValue `json:"signingKey,omitempty"`
|
||||
PayloadType target_domain.PayloadType `json:"payloadType,omitempty"`
|
||||
|
||||
oldName string
|
||||
}
|
||||
@@ -144,6 +154,12 @@ func ChangeSigningKey(signingKey *crypto.CryptoValue) func(event *ChangedEvent)
|
||||
}
|
||||
}
|
||||
|
||||
func ChangePayloadType(payloadType target_domain.PayloadType) func(event *ChangedEvent) {
|
||||
return func(e *ChangedEvent) {
|
||||
e.PayloadType = payloadType
|
||||
}
|
||||
}
|
||||
|
||||
type RemovedEvent struct {
|
||||
eventstore.BaseEvent `json:"-"`
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user