-
Notifications
You must be signed in to change notification settings - Fork 18
feat: API key authentication middleware #6
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Changes from all commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
dbb1fa7
refactor: make config & redis dependency more explicit
alexluong b22a54d
refactor: move config type declaration to individual packages
alexluong 714f177
chore: remove debug log
alexluong 7908bf0
test: Destination model
alexluong 272d1a1
test: Use miniredis for testing
alexluong c6bf701
test: Destination handlers
alexluong 640cab1
chore: Remove debug log
alexluong 6023a59
chore: Support API_PORT env
alexluong 889b53f
feat: Authentication middleware
alexluong ed38e7c
test: Authentication middleware
alexluong 950dcb8
chore: Remove debug log
alexluong 9aa9347
chore: Rename middleware name to specify API key mechanism
alexluong 9012587
Merge branch 'main' of github.com:hookdeck/EventKit into auth
alexluong File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,214 @@ | ||
package destination_test | ||
|
||
import ( | ||
"context" | ||
"encoding/json" | ||
"net/http" | ||
"net/http/httptest" | ||
"strings" | ||
"testing" | ||
|
||
"github.com/gin-gonic/gin" | ||
"github.com/google/uuid" | ||
"github.com/hookdeck/EventKit/internal/destination" | ||
"github.com/hookdeck/EventKit/internal/util/testutil" | ||
"github.com/stretchr/testify/assert" | ||
) | ||
|
||
func setupRouter(destinationHandlers *destination.DestinationHandlers) *gin.Engine { | ||
gin.SetMode(gin.TestMode) | ||
r := gin.Default() | ||
r.GET("/destinations", destinationHandlers.List) | ||
r.POST("/destinations", destinationHandlers.Create) | ||
r.GET("/destinations/:destinationID", destinationHandlers.Retrieve) | ||
r.PATCH("/destinations/:destinationID", destinationHandlers.Update) | ||
r.DELETE("/destinations/:destinationID", destinationHandlers.Delete) | ||
return r | ||
} | ||
|
||
func TestDestinationListHandler(t *testing.T) { | ||
t.Parallel() | ||
|
||
redisClient := testutil.CreateTestRedisClient(t) | ||
handlers := destination.NewHandlers(redisClient) | ||
router := setupRouter(handlers) | ||
|
||
t.Run("should return 501", func(t *testing.T) { | ||
t.Parallel() | ||
w := httptest.NewRecorder() | ||
req, _ := http.NewRequest("GET", "/destinations", nil) | ||
router.ServeHTTP(w, req) | ||
assert.Equal(t, http.StatusNotImplemented, w.Code) | ||
}) | ||
} | ||
|
||
func TestDestinationCreateHandler(t *testing.T) { | ||
t.Parallel() | ||
|
||
redisClient := testutil.CreateTestRedisClient(t) | ||
handlers := destination.NewHandlers(redisClient) | ||
router := setupRouter(handlers) | ||
|
||
t.Run("should create", func(t *testing.T) { | ||
t.Parallel() | ||
|
||
w := httptest.NewRecorder() | ||
|
||
exampleDestination := destination.CreateDestinationRequest{ | ||
Name: "Test Destination", | ||
} | ||
destinationJSON, _ := json.Marshal(exampleDestination) | ||
req, _ := http.NewRequest("POST", "/destinations", strings.NewReader(string(destinationJSON))) | ||
router.ServeHTTP(w, req) | ||
|
||
var destinationResponse map[string]any | ||
json.Unmarshal(w.Body.Bytes(), &destinationResponse) | ||
|
||
assert.Equal(t, http.StatusCreated, w.Code) | ||
assert.Equal(t, exampleDestination.Name, destinationResponse["name"]) | ||
assert.NotEqual(t, "", destinationResponse["id"]) | ||
}) | ||
} | ||
|
||
func TestDestinationRetrieveHandler(t *testing.T) { | ||
t.Parallel() | ||
|
||
redisClient := testutil.CreateTestRedisClient(t) | ||
handlers := destination.NewHandlers(redisClient) | ||
router := setupRouter(handlers) | ||
|
||
t.Run("should return 404 when there's no destination", func(t *testing.T) { | ||
t.Parallel() | ||
|
||
w := httptest.NewRecorder() | ||
req, _ := http.NewRequest("GET", "/destinations/invalid_id", nil) | ||
router.ServeHTTP(w, req) | ||
|
||
assert.Equal(t, http.StatusNotFound, w.Code) | ||
}) | ||
|
||
t.Run("should retrieve when there's a destination", func(t *testing.T) { | ||
t.Parallel() | ||
|
||
// Setup test destination | ||
exampleDestination := destination.Destination{ | ||
ID: uuid.New().String(), | ||
Name: "Test Destination", | ||
} | ||
redisClient.Set(context.Background(), "destination:"+exampleDestination.ID, exampleDestination.Name, 0) | ||
|
||
// Test HTTP request | ||
w := httptest.NewRecorder() | ||
req, _ := http.NewRequest("GET", "/destinations/"+exampleDestination.ID, nil) | ||
router.ServeHTTP(w, req) | ||
|
||
var destinationResponse map[string]any | ||
json.Unmarshal(w.Body.Bytes(), &destinationResponse) | ||
|
||
assert.Equal(t, http.StatusOK, w.Code) | ||
assert.Equal(t, exampleDestination.ID, destinationResponse["id"]) | ||
assert.Equal(t, exampleDestination.Name, destinationResponse["name"]) | ||
|
||
// Clean up | ||
redisClient.Del(context.Background(), "destination:"+exampleDestination.ID) | ||
}) | ||
} | ||
|
||
func TestDestinationUpdateHandler(t *testing.T) { | ||
t.Parallel() | ||
|
||
redisClient := testutil.CreateTestRedisClient(t) | ||
handlers := destination.NewHandlers(redisClient) | ||
router := setupRouter(handlers) | ||
|
||
initialDestination := destination.Destination{ | ||
Name: "Test Destination", | ||
} | ||
|
||
updateDestinationRequest := destination.UpdateDestinationRequest{ | ||
Name: "Updated Destination", | ||
} | ||
updateDestinationJSON, _ := json.Marshal(updateDestinationRequest) | ||
|
||
t.Run("should validate", func(t *testing.T) { | ||
t.Parallel() | ||
|
||
w := httptest.NewRecorder() | ||
req, _ := http.NewRequest("PATCH", "/destinations/invalid_id", nil) | ||
router.ServeHTTP(w, req) | ||
|
||
assert.Equal(t, http.StatusBadRequest, w.Code) | ||
}) | ||
|
||
t.Run("should return 404 when there's no destination", func(t *testing.T) { | ||
t.Parallel() | ||
|
||
w := httptest.NewRecorder() | ||
req, _ := http.NewRequest("PATCH", "/destinations/invalid_id", strings.NewReader(string(updateDestinationJSON))) | ||
router.ServeHTTP(w, req) | ||
|
||
assert.Equal(t, http.StatusNotFound, w.Code) | ||
}) | ||
|
||
t.Run("should update destination", func(t *testing.T) { | ||
t.Parallel() | ||
|
||
// Setup initial destination | ||
newDestination := initialDestination | ||
newDestination.ID = uuid.New().String() | ||
redisClient.Set(context.Background(), "destination:"+newDestination.ID, newDestination.Name, 0) | ||
|
||
// Test HTTP request | ||
w := httptest.NewRecorder() | ||
req, _ := http.NewRequest("PATCH", "/destinations/"+newDestination.ID, strings.NewReader(string(updateDestinationJSON))) | ||
router.ServeHTTP(w, req) | ||
|
||
var destinationResponse map[string]any | ||
json.Unmarshal(w.Body.Bytes(), &destinationResponse) | ||
|
||
assert.Equal(t, http.StatusAccepted, w.Code) | ||
assert.Equal(t, newDestination.ID, destinationResponse["id"]) | ||
assert.Equal(t, updateDestinationRequest.Name, destinationResponse["name"]) | ||
|
||
// Clean up | ||
redisClient.Del(context.Background(), "destination:"+newDestination.ID) | ||
}) | ||
} | ||
|
||
func TestDestinationDeleteHandler(t *testing.T) { | ||
redisClient := testutil.CreateTestRedisClient(t) | ||
handlers := destination.NewHandlers(redisClient) | ||
router := setupRouter(handlers) | ||
|
||
t.Run("should return 404 when there's no destination", func(t *testing.T) { | ||
t.Parallel() | ||
|
||
w := httptest.NewRecorder() | ||
req, _ := http.NewRequest("DELETE", "/destinations/invalid_id", nil) | ||
router.ServeHTTP(w, req) | ||
|
||
assert.Equal(t, http.StatusNotFound, w.Code) | ||
}) | ||
|
||
t.Run("should delete destination", func(t *testing.T) { | ||
t.Parallel() | ||
|
||
// Setup initial destination | ||
newDestination := destination.Destination{ | ||
ID: uuid.New().String(), | ||
Name: "Test Destination", | ||
} | ||
redisClient.Set(context.Background(), "destination:"+newDestination.ID, newDestination.Name, 0) | ||
|
||
w := httptest.NewRecorder() | ||
req, _ := http.NewRequest("DELETE", "/destinations/"+newDestination.ID, nil) | ||
router.ServeHTTP(w, req) | ||
|
||
var destinationResponse map[string]any | ||
json.Unmarshal(w.Body.Bytes(), &destinationResponse) | ||
|
||
assert.Equal(t, http.StatusOK, w.Code) | ||
assert.Equal(t, newDestination.ID, destinationResponse["id"]) | ||
assert.Equal(t, newDestination.Name, destinationResponse["name"]) | ||
}) | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
package destination_test | ||
|
||
import ( | ||
"context" | ||
"testing" | ||
|
||
"github.com/google/uuid" | ||
"github.com/hookdeck/EventKit/internal/destination" | ||
"github.com/hookdeck/EventKit/internal/util/testutil" | ||
"github.com/stretchr/testify/assert" | ||
) | ||
|
||
func TestDestinationModel(t *testing.T) { | ||
t.Parallel() | ||
|
||
redisClient := testutil.CreateTestRedisClient(t) | ||
model := destination.NewDestinationModel(redisClient) | ||
|
||
input := destination.Destination{ | ||
ID: uuid.New().String(), | ||
Name: "Test Destination", | ||
} | ||
|
||
t.Run("gets empty", func(t *testing.T) { | ||
actual, err := model.Get(context.Background(), input.ID) | ||
assert.Nil(t, actual, "model.Get() should return nil when there's no value") | ||
assert.Nil(t, err, "model.Get() should not return an error when there's no value") | ||
}) | ||
|
||
t.Run("sets", func(t *testing.T) { | ||
err := model.Set(context.Background(), input) | ||
assert.Nil(t, err, "model.Set() should not return an error") | ||
|
||
value, err := redisClient.Get(context.Background(), "destination:"+input.ID).Result() | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
assert.Equal(t, input.Name, value, "model.Set() should set destination name %s", input.Name) | ||
}) | ||
|
||
t.Run("gets", func(t *testing.T) { | ||
actual, err := model.Get(context.Background(), input.ID) | ||
assert.Nil(t, err, "model.Get() should not return an error") | ||
assert.Equal(t, input, *actual, "model.Get() should return %s", input) | ||
}) | ||
|
||
t.Run("overrides", func(t *testing.T) { | ||
input.Name = "Test Destination 2" | ||
|
||
err := model.Set(context.Background(), input) | ||
assert.Nil(t, err, "model.Set() should not return an error", input) | ||
|
||
actual, err := model.Get(context.Background(), input.ID) | ||
assert.Nil(t, err, "model.Get() should not return an error") | ||
assert.Equal(t, input, *actual, "model.Get() should return %s", input) | ||
}) | ||
|
||
t.Run("clears", func(t *testing.T) { | ||
deleted, err := model.Clear(context.Background(), input.ID) | ||
assert.Nil(t, err, "model.Clear() should not return an error") | ||
assert.Equal(t, *deleted, input, "model.Clear() should return deleted value", input) | ||
|
||
actual, err := model.Get(context.Background(), input.ID) | ||
assert.Nil(t, actual, "model.Clear() should properly remove value") | ||
assert.Nil(t, err, "model.Clear() should properly remove value") | ||
}) | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
package api | ||
|
||
import ( | ||
"errors" | ||
"net/http" | ||
"strings" | ||
|
||
"github.com/gin-gonic/gin" | ||
) | ||
|
||
func apiKeyAuthMiddleware(apiKey string) gin.HandlerFunc { | ||
if apiKey == "" { | ||
return func(c *gin.Context) { | ||
c.Next() | ||
} | ||
} | ||
|
||
return func(c *gin.Context) { | ||
authorizationToken, err := extractBearerToken(c.GetHeader("Authorization")) | ||
if err != nil { | ||
// TODO: Consider sending a more detailed error message. | ||
// Currently we don't have clear specs on how to send back error message. | ||
c.AbortWithStatus(http.StatusUnauthorized) | ||
return | ||
} | ||
if authorizationToken != apiKey { | ||
// TODO: Consider sending a more detailed error message. | ||
// Currently we don't have clear specs on how to send back error message. | ||
c.AbortWithStatus(http.StatusUnauthorized) | ||
return | ||
} | ||
c.Next() | ||
} | ||
} | ||
|
||
func extractBearerToken(header string) (string, error) { | ||
if !strings.HasPrefix(header, "Bearer ") { | ||
return "", errors.New("invalid bearer token") | ||
} | ||
return strings.TrimPrefix(header, "Bearer "), nil | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I want to point this out as the spec hasn't mentioned errors yet, afaik. Do you want to circle back on this later?
Happy to sort this out now if you can share what you have in mind or provide the OpenAPI schema spec or something like that.