Skip to content

Commit 348a1da

Browse files
authored
feat: allow for postgres and http functions on each extensibility point (#1528)
## What kind of change does this PR introduce? After this change, users can opt to use either Postgres or HTTP functions on each extensibility/extension point. From an implementation standpoint, all new extension points must support both HTTP and Postgres functions
1 parent dd4f0e0 commit 348a1da

File tree

9 files changed

+425
-70
lines changed

9 files changed

+425
-70
lines changed

internal/api/hooks.go

Lines changed: 89 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@ import (
77
"errors"
88
"fmt"
99
"io"
10+
"mime"
1011
"net"
1112
"net/http"
13+
"net/url"
1214
"strings"
1315
"time"
1416

@@ -31,7 +33,7 @@ const (
3133
PayloadLimit = 200 * 1024 // 200KB
3234
)
3335

34-
func (a *API) runPostgresHook(ctx context.Context, tx *storage.Connection, name string, input, output any) ([]byte, error) {
36+
func (a *API) runPostgresHook(ctx context.Context, tx *storage.Connection, hookConfig conf.ExtensibilityPointConfiguration, input, output any) ([]byte, error) {
3537
db := a.db.WithContext(ctx)
3638

3739
request, err := json.Marshal(input)
@@ -46,7 +48,7 @@ func (a *API) runPostgresHook(ctx context.Context, tx *storage.Connection, name
4648
return terr
4749
}
4850

49-
if terr := tx.RawQuery(fmt.Sprintf("select %s(?);", name), request).First(&response); terr != nil {
51+
if terr := tx.RawQuery(fmt.Sprintf("select %s(?);", hookConfig.HookName), request).First(&response); terr != nil {
5052
return terr
5153
}
5254

@@ -75,7 +77,8 @@ func (a *API) runPostgresHook(ctx context.Context, tx *storage.Connection, name
7577
return response, nil
7678
}
7779

78-
func (a *API) runHTTPHook(ctx context.Context, r *http.Request, hookConfig conf.ExtensibilityPointConfiguration, input, output any) ([]byte, error) {
80+
func (a *API) runHTTPHook(r *http.Request, hookConfig conf.ExtensibilityPointConfiguration, input, output any) ([]byte, error) {
81+
ctx := r.Context()
7982
client := http.Client{
8083
Timeout: DefaultHTTPHookTimeout,
8184
}
@@ -135,6 +138,15 @@ func (a *API) runHTTPHook(ctx context.Context, r *http.Request, hookConfig conf.
135138
}
136139

137140
defer rsp.Body.Close()
141+
// Header.Get is case insensitive
142+
contentType := rsp.Header.Get("Content-Type")
143+
mediaType, _, err := mime.ParseMediaType(contentType)
144+
if err != nil {
145+
return nil, internalServerError("Invalid Content-Type header")
146+
}
147+
if mediaType != "application/json" {
148+
return nil, internalServerError("Invalid JSON response. Received content-type: " + contentType)
149+
}
138150

139151
switch rsp.StatusCode {
140152
case http.StatusOK, http.StatusNoContent, http.StatusAccepted:
@@ -172,67 +184,80 @@ func (a *API) runHTTPHook(ctx context.Context, r *http.Request, hookConfig conf.
172184
return nil, nil
173185
}
174186

175-
func (a *API) invokeHTTPHook(ctx context.Context, r *http.Request, input, output any) error {
187+
// invokePostgresHook invokes the hook code. conn can be nil, in which case a new
188+
// transaction is opened. If calling invokeHook within a transaction, always
189+
// pass the current transaction, as pool-exhaustion deadlocks are very easy to
190+
// trigger.
191+
func (a *API) invokeHook(conn *storage.Connection, r *http.Request, input, output any, uri string) error {
192+
var err error
193+
var response []byte
194+
u, err := url.Parse(uri)
195+
if err != nil {
196+
return err
197+
}
198+
176199
switch input.(type) {
177200
case *hooks.SendSMSInput:
178201
hookOutput, ok := output.(*hooks.SendSMSOutput)
179202
if !ok {
180203
panic("output should be *hooks.SendSMSOutput")
181204
}
182-
var response []byte
183-
var err error
184-
185-
if response, err = a.runHTTPHook(ctx, r, a.config.Hook.SendSMS, input, output); err != nil {
186-
return internalServerError("Error invoking Send SMS hook.").WithInternalError(err)
205+
if response, err = a.runHook(r, conn, a.config.Hook.SendSMS, input, output, u.Scheme); err != nil {
206+
return err
187207
}
188-
189208
if err := json.Unmarshal(response, hookOutput); err != nil {
190209
return internalServerError("Error unmarshaling Send SMS output.").WithInternalError(err)
191210
}
211+
if hookOutput.IsError() {
212+
httpCode := hookOutput.HookError.HTTPCode
213+
214+
if httpCode == 0 {
215+
httpCode = http.StatusInternalServerError
216+
}
217+
httpError := &HTTPError{
218+
HTTPStatus: httpCode,
219+
Message: hookOutput.HookError.Message,
220+
}
221+
return httpError.WithInternalError(&hookOutput.HookError)
222+
}
223+
return nil
192224
case *hooks.SendEmailInput:
193225
hookOutput, ok := output.(*hooks.SendEmailOutput)
194226
if !ok {
195227
panic("output should be *hooks.SendEmailOutput")
196228
}
197-
198-
var response []byte
199-
var err error
200-
201-
if response, err = a.runHTTPHook(ctx, r, a.config.Hook.SendEmail, input, output); err != nil {
202-
return internalServerError("Error invoking Send Email hook.").WithInternalError(err)
203-
}
204-
if err != nil {
229+
if response, err = a.runHook(r, conn, a.config.Hook.SendEmail, input, output, u.Scheme); err != nil {
205230
return err
206231
}
207-
208232
if err := json.Unmarshal(response, hookOutput); err != nil {
209-
return internalServerError("Error unmarshaling Send Email hook output.").WithInternalError(err)
233+
return internalServerError("Error unmarshaling Send Email output.").WithInternalError(err)
210234
}
235+
if hookOutput.IsError() {
236+
httpCode := hookOutput.HookError.HTTPCode
211237

212-
default:
213-
panic("unknown HTTP hook type")
214-
}
215-
return nil
216-
}
238+
if httpCode == 0 {
239+
httpCode = http.StatusInternalServerError
240+
}
217241

218-
// invokePostgresHook invokes the hook code. tx can be nil, in which case a new
219-
// transaction is opened. If calling invokeHook within a transaction, always
220-
// pass the current transaction, as pool-exhaustion deadlocks are very easy to
221-
// trigger.
222-
func (a *API) invokePostgresHook(ctx context.Context, conn *storage.Connection, input, output any) error {
223-
config := a.config
224-
// Switch based on hook type
225-
switch input.(type) {
242+
httpError := &HTTPError{
243+
HTTPStatus: httpCode,
244+
Message: hookOutput.HookError.Message,
245+
}
246+
247+
return httpError.WithInternalError(&hookOutput.HookError)
248+
}
249+
return nil
226250
case *hooks.MFAVerificationAttemptInput:
227251
hookOutput, ok := output.(*hooks.MFAVerificationAttemptOutput)
228252
if !ok {
229253
panic("output should be *hooks.MFAVerificationAttemptOutput")
230254
}
231-
232-
if _, err := a.runPostgresHook(ctx, conn, config.Hook.MFAVerificationAttempt.HookName, input, output); err != nil {
233-
return internalServerError("Error invoking MFA verification hook.").WithInternalError(err)
255+
if response, err = a.runHook(r, conn, a.config.Hook.MFAVerificationAttempt, input, output, u.Scheme); err != nil {
256+
return err
257+
}
258+
if err := json.Unmarshal(response, hookOutput); err != nil {
259+
return internalServerError("Error unmarshaling MFA Verification Attempt output.").WithInternalError(err)
234260
}
235-
236261
if hookOutput.IsError() {
237262
httpCode := hookOutput.HookError.HTTPCode
238263

@@ -247,18 +272,19 @@ func (a *API) invokePostgresHook(ctx context.Context, conn *storage.Connection,
247272

248273
return httpError.WithInternalError(&hookOutput.HookError)
249274
}
250-
251275
return nil
252276
case *hooks.PasswordVerificationAttemptInput:
253277
hookOutput, ok := output.(*hooks.PasswordVerificationAttemptOutput)
254278
if !ok {
255279
panic("output should be *hooks.PasswordVerificationAttemptOutput")
256280
}
257281

258-
if _, err := a.runPostgresHook(ctx, conn, config.Hook.PasswordVerificationAttempt.HookName, input, output); err != nil {
259-
return internalServerError("Error invoking password verification hook.").WithInternalError(err)
282+
if response, err = a.runHook(r, conn, a.config.Hook.PasswordVerificationAttempt, input, output, u.Scheme); err != nil {
283+
return err
284+
}
285+
if err := json.Unmarshal(response, hookOutput); err != nil {
286+
return internalServerError("Error unmarshaling Password Verification Attempt output.").WithInternalError(err)
260287
}
261-
262288
if hookOutput.IsError() {
263289
httpCode := hookOutput.HookError.HTTPCode
264290

@@ -280,9 +306,11 @@ func (a *API) invokePostgresHook(ctx context.Context, conn *storage.Connection,
280306
if !ok {
281307
panic("output should be *hooks.CustomAccessTokenOutput")
282308
}
283-
284-
if _, err := a.runPostgresHook(ctx, conn, config.Hook.CustomAccessToken.HookName, input, output); err != nil {
285-
return internalServerError("Error invoking access token hook.").WithInternalError(err)
309+
if response, err = a.runHook(r, conn, a.config.Hook.CustomAccessToken, input, output, u.Scheme); err != nil {
310+
return err
311+
}
312+
if err := json.Unmarshal(response, hookOutput); err != nil {
313+
return internalServerError("Error unmarshaling Custom Access Token output.").WithInternalError(err)
286314
}
287315

288316
if hookOutput.IsError() {
@@ -305,7 +333,6 @@ func (a *API) invokePostgresHook(ctx context.Context, conn *storage.Connection,
305333
if httpCode == 0 {
306334
httpCode = http.StatusInternalServerError
307335
}
308-
309336
httpError := &HTTPError{
310337
HTTPStatus: httpCode,
311338
Message: err.Error(),
@@ -314,8 +341,24 @@ func (a *API) invokePostgresHook(ctx context.Context, conn *storage.Connection,
314341
return httpError
315342
}
316343
return nil
344+
}
345+
return nil
346+
}
317347

348+
func (a *API) runHook(r *http.Request, conn *storage.Connection, hookConfig conf.ExtensibilityPointConfiguration, input, output any, scheme string) ([]byte, error) {
349+
ctx := r.Context()
350+
var response []byte
351+
var err error
352+
switch strings.ToLower(scheme) {
353+
case "http", "https":
354+
response, err = a.runHTTPHook(r, hookConfig, input, output)
355+
case "pg-functions":
356+
response, err = a.runPostgresHook(ctx, conn, hookConfig, input, output)
318357
default:
319-
panic("unknown Postgres hook input type")
358+
return nil, fmt.Errorf("unsupported protocol: %v only postgres hooks and HTTPS functions are supported at the moment", scheme)
359+
}
360+
if err != nil {
361+
return nil, internalServerError("Error running hook URI: %v", hookConfig.URI).WithInternalError(err)
320362
}
363+
return response, nil
321364
}

0 commit comments

Comments
 (0)