Skip to content

Commit 04ae31f

Browse files
authored
Merge pull request #194 from hookdeck/feat/gcp-dest-auth
feat: Support GCP destination auth
2 parents b9b54cb + 85a5e2f commit 04ae31f

File tree

6 files changed

+177
-12
lines changed

6 files changed

+177
-12
lines changed

REFERENCE.md

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -976,7 +976,21 @@ hookdeck connection create \
976976
--destination-url "https://api.example.com/stripe"
977977
```
978978

979-
**4. Destination with Bearer Token**
979+
**4. Destination with Hookdeck Signature (Default)**
980+
```bash
981+
# Hookdeck automatically signs outgoing webhooks - no configuration needed
982+
hookdeck connection create \
983+
--source-name "stripe-webhooks" \
984+
--source-type STRIPE \
985+
--source-webhook-secret "whsec_stripe_secret" \
986+
--destination-name "api-with-verification" \
987+
--destination-type HTTP \
988+
--destination-url "https://api.example.com/webhook" \
989+
--destination-auth-method hookdeck
990+
```
991+
*Note: Hookdeck Signature authentication is the default. Hookdeck automatically signs all outgoing webhooks with a signature that can be verified using Hookdeck's verification libraries. No webhook secret needs to be configured.*
992+
993+
**5. Destination with Bearer Token**
980994
```bash
981995
hookdeck connection create \
982996
--source-name "github-webhooks" \
@@ -985,9 +999,11 @@ hookdeck connection create \
985999
--destination-name "ci-system" \
9861000
--destination-type HTTP \
9871001
--destination-url "https://ci.example.com/webhook" \
1002+
--destination-auth-method bearer \
9881003
--destination-bearer-token "bearer_token_xyz"
1004+
```
9891005

990-
**5. Source with Custom Response and Allowed HTTP Methods**
1006+
**6. Source with Custom Response and Allowed HTTP Methods**
9911007
```bash
9921008
hookdeck connection create \
9931009
--source-name "api-webhooks" \
@@ -1002,7 +1018,7 @@ hookdeck connection create \
10021018

10031019
#### Rule Configuration Examples
10041020

1005-
**6. Retry Rules**
1021+
**7. Retry Rules**
10061022
```bash
10071023
hookdeck connection create \
10081024
--source-name "payment-webhooks" \
@@ -1015,7 +1031,7 @@ hookdeck connection create \
10151031
--rule-retry-interval 60000
10161032
```
10171033

1018-
**7. Filter Rules**
1034+
**8. Filter Rules**
10191035
```bash
10201036
hookdeck connection create \
10211037
--source-name "events" \
@@ -1026,7 +1042,7 @@ hookdeck connection create \
10261042
--rule-filter-body '{"event_type":"payment.succeeded"}'
10271043
```
10281044

1029-
**8. All Rule Types Combined**
1045+
**9. All Rule Types Combined**
10301046
```bash
10311047
hookdeck connection create \
10321048
--source-name "shopify-webhooks" \
@@ -1042,7 +1058,7 @@ hookdeck connection create \
10421058
--rule-delay 5000
10431059
```
10441060

1045-
**9. Rate Limiting**
1061+
**10. Rate Limiting**
10461062
```bash
10471063
hookdeck connection create \
10481064
--source-name "high-volume-source" \
@@ -1054,6 +1070,19 @@ hookdeck connection create \
10541070
--destination-rate-limit-period minute
10551071
```
10561072

1073+
**11. GCP Service Account Authentication**
1074+
```bash
1075+
hookdeck connection create \
1076+
--source-name "webhooks" \
1077+
--source-type HTTP \
1078+
--destination-name "gcp-cloud-function" \
1079+
--destination-type HTTP \
1080+
--destination-url "https://us-central1-project-id.cloudfunctions.net/function" \
1081+
--destination-auth-method gcp \
1082+
--destination-gcp-service-account-key '{"type":"service_account","project_id":"project-id","private_key_id":"key-id","private_key":"-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n","client_email":"service-account@project-id.iam.gserviceaccount.com"}' \
1083+
--destination-gcp-scope "https://www.googleapis.com/auth/cloud-platform"
1084+
```
1085+
10571086
#### Available Flags
10581087

10591088
**Connection Configuration:**
@@ -1084,7 +1113,7 @@ hookdeck connection create \
10841113
- `--destination-cli-path <path>` - CLI path (default: `/`)
10851114
- `--destination-path-forwarding-disabled <true|false>` - Disable path forwarding for HTTP destinations (default: false)
10861115
- `--destination-http-method <method>` - HTTP method for HTTP destinations: `GET`, `POST`, `PUT`, `PATCH`, `DELETE`
1087-
- `--destination-auth-method <method>` - Authentication method: `hookdeck`, `bearer`, `basic`, `api_key`, `custom_signature`, `oauth2_client_credentials`, `oauth2_authorization_code`, `aws`
1116+
- `--destination-auth-method <method>` - Authentication method: `hookdeck`, `bearer`, `basic`, `api_key`, `custom_signature`, `oauth2_client_credentials`, `oauth2_authorization_code`, `aws`, `gcp`
10881117
- `--destination-rate-limit <number>` - Rate limit (requests per period)
10891118
- `--destination-rate-limit-period <period>` - Period: `second`, `minute`, `hour`, `day`, `month`, `year`
10901119

@@ -1136,6 +1165,11 @@ hookdeck connection create \
11361165
- `--destination-aws-region <region>` - AWS region
11371166
- `--destination-aws-service <service>` - AWS service name
11381167

1168+
*GCP Service Account:*
1169+
- `--destination-auth-method gcp`
1170+
- `--destination-gcp-service-account-key <json>` - GCP service account key JSON
1171+
- `--destination-gcp-scope <scope>` - GCP scope (optional)
1172+
11391173
**Rules - Retry:**
11401174
- `--rule-retry-strategy <strategy>` - Strategy: `linear`, `exponential`
11411175
- `--rule-retry-count <number>` - Number of retry attempts (1-20)

pkg/cmd/connection_auth_test.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,55 @@ func TestBuildAuthConfig(t *testing.T) {
295295
wantErr: true,
296296
errContains: "--destination-aws-region is required",
297297
},
298+
{
299+
name: "gcp service account auth - valid",
300+
setup: func(cc *connectionCreateCmd) {
301+
cc.DestinationAuthMethod = "gcp"
302+
cc.DestinationGCPServiceAccountKey = `{"type":"service_account","project_id":"test"}`
303+
cc.DestinationGCPScope = "https://www.googleapis.com/auth/cloud-platform"
304+
},
305+
wantType: "GCP_SERVICE_ACCOUNT",
306+
wantErr: false,
307+
validate: func(t *testing.T, config map[string]interface{}) {
308+
if config["type"] != "GCP_SERVICE_ACCOUNT" {
309+
t.Errorf("expected type GCP_SERVICE_ACCOUNT, got %v", config["type"])
310+
}
311+
if config["service_account_key"] == "" {
312+
t.Error("expected service_account_key to be set")
313+
}
314+
if config["scope"] != "https://www.googleapis.com/auth/cloud-platform" {
315+
t.Errorf("expected scope, got %v", config["scope"])
316+
}
317+
},
318+
},
319+
{
320+
name: "gcp service account auth - valid without scope",
321+
setup: func(cc *connectionCreateCmd) {
322+
cc.DestinationAuthMethod = "gcp"
323+
cc.DestinationGCPServiceAccountKey = `{"type":"service_account","project_id":"test"}`
324+
},
325+
wantType: "GCP_SERVICE_ACCOUNT",
326+
wantErr: false,
327+
validate: func(t *testing.T, config map[string]interface{}) {
328+
if config["type"] != "GCP_SERVICE_ACCOUNT" {
329+
t.Errorf("expected type GCP_SERVICE_ACCOUNT, got %v", config["type"])
330+
}
331+
if config["service_account_key"] == "" {
332+
t.Error("expected service_account_key to be set")
333+
}
334+
if _, hasScope := config["scope"]; hasScope {
335+
t.Error("expected scope to not be set when not provided")
336+
}
337+
},
338+
},
339+
{
340+
name: "gcp service account auth - missing key",
341+
setup: func(cc *connectionCreateCmd) {
342+
cc.DestinationAuthMethod = "gcp"
343+
},
344+
wantErr: true,
345+
errContains: "--destination-gcp-service-account-key is required",
346+
},
298347
{
299348
name: "unsupported auth method",
300349
setup: func(cc *connectionCreateCmd) {

pkg/cmd/connection_create.go

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,10 @@ type connectionCreateCmd struct {
8484
DestinationAWSRegion string
8585
DestinationAWSService string
8686

87+
// GCP Service Account flags
88+
DestinationGCPServiceAccountKey string
89+
DestinationGCPScope string
90+
8791
// Destination rate limiting flags
8892
DestinationRateLimit int
8993
DestinationRateLimitPeriod string
@@ -207,7 +211,7 @@ func newConnectionCreateCmd() *connectionCreateCmd {
207211
cc.cmd.Flags().StringVar(&cc.destinationHTTPMethod, "destination-http-method", "", "HTTP method for HTTP destinations (GET, POST, PUT, PATCH, DELETE)")
208212

209213
// Destination authentication flags
210-
cc.cmd.Flags().StringVar(&cc.DestinationAuthMethod, "destination-auth-method", "", "Authentication method for HTTP destinations (hookdeck, bearer, basic, api_key, custom_signature, oauth2_client_credentials, oauth2_authorization_code, aws)")
214+
cc.cmd.Flags().StringVar(&cc.DestinationAuthMethod, "destination-auth-method", "", "Authentication method for HTTP destinations (hookdeck, bearer, basic, api_key, custom_signature, oauth2_client_credentials, oauth2_authorization_code, aws, gcp)")
211215

212216
// Bearer Token
213217
cc.cmd.Flags().StringVar(&cc.DestinationBearerToken, "destination-bearer-token", "", "Bearer token for destination authentication")
@@ -241,6 +245,10 @@ func newConnectionCreateCmd() *connectionCreateCmd {
241245
cc.cmd.Flags().StringVar(&cc.DestinationAWSRegion, "destination-aws-region", "", "AWS region")
242246
cc.cmd.Flags().StringVar(&cc.DestinationAWSService, "destination-aws-service", "", "AWS service name")
243247

248+
// GCP Service Account
249+
cc.cmd.Flags().StringVar(&cc.DestinationGCPServiceAccountKey, "destination-gcp-service-account-key", "", "GCP service account key JSON for destination authentication")
250+
cc.cmd.Flags().StringVar(&cc.DestinationGCPScope, "destination-gcp-scope", "", "GCP scope for service account authentication")
251+
244252
// Destination rate limiting flags
245253
cc.cmd.Flags().IntVar(&cc.DestinationRateLimit, "destination-rate-limit", 0, "Rate limit for destination (requests per period)")
246254
cc.cmd.Flags().StringVar(&cc.DestinationRateLimitPeriod, "destination-rate-limit-period", "", "Rate limit period (second, minute, hour, concurrent)")
@@ -790,8 +798,20 @@ func (cc *connectionCreateCmd) buildAuthConfig() (map[string]interface{}, error)
790798
authConfig["region"] = cc.DestinationAWSRegion
791799
authConfig["service"] = cc.DestinationAWSService
792800

801+
case "gcp":
802+
// GCP_SERVICE_ACCOUNT
803+
if cc.DestinationGCPServiceAccountKey == "" {
804+
return nil, fmt.Errorf("--destination-gcp-service-account-key is required for gcp auth method")
805+
}
806+
authConfig["type"] = "GCP_SERVICE_ACCOUNT"
807+
authConfig["service_account_key"] = cc.DestinationGCPServiceAccountKey
808+
809+
if cc.DestinationGCPScope != "" {
810+
authConfig["scope"] = cc.DestinationGCPScope
811+
}
812+
793813
default:
794-
return nil, fmt.Errorf("unsupported destination authentication method: %s (supported: hookdeck, bearer, basic, api_key, custom_signature, oauth2_client_credentials, oauth2_authorization_code, aws)", cc.DestinationAuthMethod)
814+
return nil, fmt.Errorf("unsupported destination authentication method: %s (supported: hookdeck, bearer, basic, api_key, custom_signature, oauth2_client_credentials, oauth2_authorization_code, aws, gcp)", cc.DestinationAuthMethod)
795815
}
796816

797817
return authConfig, nil

pkg/cmd/connection_upsert.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ func newConnectionUpsertCmd() *connectionUpsertCmd {
116116
cu.cmd.Flags().StringVar(&cu.destinationHTTPMethod, "destination-http-method", "", "HTTP method for HTTP destinations (GET, POST, PUT, PATCH, DELETE)")
117117

118118
// Destination authentication flags
119-
cu.cmd.Flags().StringVar(&cu.DestinationAuthMethod, "destination-auth-method", "", "Authentication method for HTTP destinations (hookdeck, bearer, basic, api_key, custom_signature, oauth2_client_credentials, oauth2_authorization_code, aws)")
119+
cu.cmd.Flags().StringVar(&cu.DestinationAuthMethod, "destination-auth-method", "", "Authentication method for HTTP destinations (hookdeck, bearer, basic, api_key, custom_signature, oauth2_client_credentials, oauth2_authorization_code, aws, gcp)")
120120

121121
// Bearer Token
122122
cu.cmd.Flags().StringVar(&cu.DestinationBearerToken, "destination-bearer-token", "", "Bearer token for destination authentication")
@@ -150,6 +150,10 @@ func newConnectionUpsertCmd() *connectionUpsertCmd {
150150
cu.cmd.Flags().StringVar(&cu.DestinationAWSRegion, "destination-aws-region", "", "AWS region")
151151
cu.cmd.Flags().StringVar(&cu.DestinationAWSService, "destination-aws-service", "", "AWS service name")
152152

153+
// GCP Service Account
154+
cu.cmd.Flags().StringVar(&cu.DestinationGCPServiceAccountKey, "destination-gcp-service-account-key", "", "GCP service account key JSON for destination authentication")
155+
cu.cmd.Flags().StringVar(&cu.DestinationGCPScope, "destination-gcp-scope", "", "GCP scope for service account authentication")
156+
153157
// Destination rate limiting flags
154158
cu.cmd.Flags().IntVar(&cu.DestinationRateLimit, "destination-rate-limit", 0, "Rate limit for destination (requests per period)")
155159
cu.cmd.Flags().StringVar(&cu.DestinationRateLimitPeriod, "destination-rate-limit-period", "", "Rate limit period (second, minute, hour, concurrent)")

test/acceptance/connection_oauth_aws_test.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,4 +184,62 @@ func TestConnectionOAuth2AWSAuthentication(t *testing.T) {
184184

185185
t.Logf("Successfully tested HTTP destination with AWS Signature: %s", connID)
186186
})
187+
188+
t.Run("HTTP_Destination_GCP_ServiceAccount", func(t *testing.T) {
189+
if testing.Short() {
190+
t.Skip("Skipping acceptance test in short mode")
191+
}
192+
193+
cli := NewCLIRunner(t)
194+
timestamp := generateTimestamp()
195+
196+
connName := "test-gcp-sa-conn-" + timestamp
197+
sourceName := "test-gcp-sa-source-" + timestamp
198+
destName := "test-gcp-sa-dest-" + timestamp
199+
destURL := "https://api.hookdeck.com/dev/null"
200+
201+
// Create connection with HTTP destination (GCP Service Account)
202+
// Using a minimal but valid JSON structure for service account key
203+
serviceAccountKey := `{"type":"service_account","project_id":"test-project","private_key_id":"test-key-id","private_key":"-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC\n-----END PRIVATE KEY-----\n","client_email":"test@test-project.iam.gserviceaccount.com","client_id":"123456789","auth_uri":"https://accounts.google.com/o/oauth2/auth","token_uri":"https://oauth2.googleapis.com/token"}`
204+
205+
stdout, stderr, err := cli.Run("connection", "create",
206+
"--name", connName,
207+
"--source-type", "WEBHOOK",
208+
"--source-name", sourceName,
209+
"--destination-type", "HTTP",
210+
"--destination-name", destName,
211+
"--destination-url", destURL,
212+
"--destination-auth-method", "gcp",
213+
"--destination-gcp-service-account-key", serviceAccountKey,
214+
"--destination-gcp-scope", "https://www.googleapis.com/auth/cloud-platform",
215+
"--output", "json")
216+
require.NoError(t, err, "Failed to create connection: stderr=%s", stderr)
217+
218+
var createResp map[string]interface{}
219+
err = json.Unmarshal([]byte(stdout), &createResp)
220+
require.NoError(t, err, "Failed to parse creation response: %s", stdout)
221+
222+
connID, ok := createResp["id"].(string)
223+
require.True(t, ok && connID != "", "Expected connection ID in creation response")
224+
225+
// Verify destination auth configuration
226+
dest, ok := createResp["destination"].(map[string]interface{})
227+
require.True(t, ok, "Expected destination object in creation response")
228+
229+
destConfig, ok := dest["config"].(map[string]interface{})
230+
require.True(t, ok, "Expected destination config object")
231+
232+
if authMethod, ok := destConfig["auth_method"].(map[string]interface{}); ok {
233+
assert.Equal(t, "GCP_SERVICE_ACCOUNT", authMethod["type"], "Auth type should be GCP_SERVICE_ACCOUNT")
234+
assert.Equal(t, "https://www.googleapis.com/auth/cloud-platform", authMethod["scope"], "GCP scope should match")
235+
// Service account key should not be returned for security reasons
236+
}
237+
238+
// Cleanup
239+
t.Cleanup(func() {
240+
deleteConnection(t, cli, connID)
241+
})
242+
243+
t.Logf("Successfully tested HTTP destination with GCP Service Account: %s", connID)
244+
})
187245
}

test/acceptance/connection_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1323,7 +1323,7 @@ func TestConnectionWithDeduplicateRule(t *testing.T) {
13231323
"--destination-name", destName,
13241324
"--destination-type", "CLI",
13251325
"--destination-cli-path", "/webhooks",
1326-
"--rule-deduplicate-window", "86400",
1326+
"--rule-deduplicate-window", "60000",
13271327
"--rule-deduplicate-include-fields", "body.id,body.timestamp",
13281328
)
13291329
require.NoError(t, err, "Should create connection with deduplicate rule")
@@ -1344,7 +1344,7 @@ func TestConnectionWithDeduplicateRule(t *testing.T) {
13441344

13451345
rule := getConn.Rules[0]
13461346
assert.Equal(t, "deduplicate", rule["type"], "Rule type should be deduplicate")
1347-
assert.Equal(t, float64(86400), rule["window"], "Deduplicate window should be 86400 milliseconds")
1347+
assert.Equal(t, float64(60000), rule["window"], "Deduplicate window should be 60000 milliseconds (60 seconds)")
13481348

13491349
// Verify include_fields is correctly set and matches our input
13501350
if includeFields, ok := rule["include_fields"].([]interface{}); ok {

0 commit comments

Comments
 (0)