Skip to content
This repository has been archived by the owner on Jun 20, 2024. It is now read-only.

Commit

Permalink
Merge pull request #295 from neicnordic/feature/selectable-middleware
Browse files Browse the repository at this point in the history
Feature/selectable middleware
  • Loading branch information
teemukataja authored Aug 29, 2023
2 parents ccaa180 + 154852b commit 7babab0
Show file tree
Hide file tree
Showing 14 changed files with 189 additions and 109 deletions.
19 changes: 13 additions & 6 deletions api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,20 @@ import (
"time"

"github.com/gin-gonic/gin"
"github.com/neicnordic/sda-download/api/middleware"
"github.com/neicnordic/sda-download/api/s3"
"github.com/neicnordic/sda-download/api/sda"
"github.com/neicnordic/sda-download/internal/config"
log "github.com/sirupsen/logrus"
)

// SelectedMiddleware is used to control authentication and authorization
// behaviour with config app.middleware
// available middlewares:
// "default" for TokenMiddleware
var SelectedMiddleware = func() gin.HandlerFunc {
return nil
}

// healthResponse
func healthResponse(c *gin.Context) {
// ok response to health
Expand All @@ -29,11 +36,11 @@ func Setup() *http.Server {

router.HandleMethodNotAllowed = true

router.GET("/metadata/datasets", middleware.TokenMiddleware(), sda.Datasets)
router.GET("/metadata/datasets/*dataset", middleware.TokenMiddleware(), sda.Files)
router.GET("/files/:fileid", middleware.TokenMiddleware(), sda.Download)
router.GET("/s3/*path", middleware.TokenMiddleware(), s3.Download)
router.HEAD("/s3/*path", middleware.TokenMiddleware(), s3.Download)
router.GET("/metadata/datasets", SelectedMiddleware(), sda.Datasets)
router.GET("/metadata/datasets/*dataset", SelectedMiddleware(), sda.Files)
router.GET("/files/:fileid", SelectedMiddleware(), sda.Download)
router.GET("/s3/*path", SelectedMiddleware(), s3.Download)
router.HEAD("/s3/*path", SelectedMiddleware(), s3.Download)
router.GET("/health", healthResponse)

// Configure TLS settings
Expand Down
5 changes: 5 additions & 0 deletions api/middleware/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Middlewares
- The default middleware is `TokenMiddleware`, which expects an access token, that can be sent to AAI in return for GA4GH visas.
- One may create custom middlewares in this `middleware` package, and register them to the `availableMiddlewares` in [config.go](../../internal/config/config.go), and adding a case for them in [main.go](../../cmd/main.go).
- A middleware for runtime can then be selected with the `app.middleware` config.
- For custom middlewares, it is important, that they use the `storeDatasets` function available in [middleware.go](middleware.go) to set the permissions for accessing data.
28 changes: 17 additions & 11 deletions api/middleware/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,11 @@ func TokenMiddleware() gin.HandlerFunc {
if err != nil {
log.Debugf("no session cookie received")
}
var datasets []string
var datasetCache session.DatasetCache
var exists bool
if sessionCookie != "" {
log.Debug("session cookie received")
datasets, exists = session.Get(sessionCookie)
datasetCache, exists = session.Get(sessionCookie)
}

if !exists {
Expand Down Expand Up @@ -57,11 +57,14 @@ func TokenMiddleware() gin.HandlerFunc {
// 200 OK with [] empty dataset list, when listing datasets (use case for sda-filesystem download tool)
// 404 dataset not found, when listing files from a dataset
// 401 unauthorised, when downloading a file
datasets = auth.GetPermissions(*visas)
datasets := auth.GetPermissions(*visas)
datasetCache = session.DatasetCache{
Datasets: datasets,
}

// Start a new session and store datasets under the session key
key := session.NewSessionKey()
session.Set(key, datasets)
session.Set(key, datasetCache)
c.SetCookie(config.Config.Session.Name, // name
key, // value
int(config.Config.Session.Expiration)/1e9, // max age
Expand All @@ -74,7 +77,7 @@ func TokenMiddleware() gin.HandlerFunc {
}

// Store dataset list to request context, for use in the endpoint handlers
c = storeDatasets(c, datasets)
c = storeDatasets(c, datasetCache)

// Forward request to the next endpoint handler
c.Next()
Expand All @@ -83,7 +86,7 @@ func TokenMiddleware() gin.HandlerFunc {
}

// storeDatasets stores the dataset list to the request context
func storeDatasets(c *gin.Context, datasets []string) *gin.Context {
func storeDatasets(c *gin.Context, datasets session.DatasetCache) *gin.Context {
log.Debugf("storing %v datasets to request context", datasets)

c.Set(datasetsKey, datasets)
Expand All @@ -92,10 +95,13 @@ func storeDatasets(c *gin.Context, datasets []string) *gin.Context {
}

// GetDatasets extracts the dataset list from the request context
var GetDatasets = func(c *gin.Context) []string {
datasets := c.GetStringSlice(datasetsKey)

log.Debugf("returning %v from request context", datasets)
var GetDatasets = func(c *gin.Context) session.DatasetCache {
var datasetCache session.DatasetCache
cached, exists := c.Get(datasetsKey)
if exists {
datasetCache = cached.(session.DatasetCache)
}
log.Debugf("returning %v from request context", cached)

return datasets
return datasetCache
}
25 changes: 16 additions & 9 deletions api/middleware/middleware_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -182,8 +182,8 @@ func TestTokenMiddleware_Success_NoCache(t *testing.T) {
// Now that we are modifying the request context, we need to place the context test inside the handler
expectedDatasets := []string{"dataset1", "dataset2"}
testEndpointWithContextData := func(c *gin.Context) {
datasets := c.GetStringSlice(datasetsKey)
if !reflect.DeepEqual(datasets, expectedDatasets) {
datasets, _ := c.Get(datasetsKey)
if !reflect.DeepEqual(datasets.(session.DatasetCache).Datasets, expectedDatasets) {
t.Errorf("TestTokenMiddleware_Success_NoCache failed, got %s expected %s", datasets, expectedDatasets)
}
}
Expand Down Expand Up @@ -224,10 +224,13 @@ func TestTokenMiddleware_Success_FromCache(t *testing.T) {
originalGetCache := session.Get

// Substitute mock functions
session.Get = func(key string) ([]string, bool) {
session.Get = func(key string) (session.DatasetCache, bool) {
log.Warningf("session.Get %v", key)
cached := session.DatasetCache{
Datasets: []string{"dataset1", "dataset2"},
}

return []string{"dataset1", "dataset2"}, true
return cached, true
}

config.Config.Session.Name = "sda_session_key"
Expand All @@ -245,8 +248,8 @@ func TestTokenMiddleware_Success_FromCache(t *testing.T) {
// Now that we are modifying the request context, we need to place the context test inside the handler
expectedDatasets := []string{"dataset1", "dataset2"}
testEndpointWithContextData := func(c *gin.Context) {
datasets := c.GetStringSlice(datasetsKey)
if !reflect.DeepEqual(datasets, expectedDatasets) {
datasets, _ := c.Get(datasetsKey)
if !reflect.DeepEqual(datasets.(session.DatasetCache).Datasets, expectedDatasets) {
t.Errorf("TestTokenMiddleware_Success_FromCache failed, got %s expected %s", datasets, expectedDatasets)
}
}
Expand Down Expand Up @@ -281,11 +284,13 @@ func TestStoreDatasets(t *testing.T) {
c, _ := gin.CreateTestContext(w)

// Store data to request context
datasets := []string{"dataset1", "dataset2"}
datasets := session.DatasetCache{
Datasets: []string{"dataset1", "dataset2"},
}
modifiedContext := storeDatasets(c, datasets)

// Verify that context has new data
storedDatasets := modifiedContext.Value(datasetsKey).([]string)
storedDatasets := modifiedContext.Value(datasetsKey).(session.DatasetCache)
if !reflect.DeepEqual(datasets, storedDatasets) {
t.Errorf("TestStoreDatasets failed, got %s, expected %s", storedDatasets, datasets)
}
Expand All @@ -299,7 +304,9 @@ func TestGetDatasets(t *testing.T) {
c, _ := gin.CreateTestContext(w)

// Store data to request context
datasets := []string{"dataset1", "dataset2"}
datasets := session.DatasetCache{
Datasets: []string{"dataset1", "dataset2"},
}
modifiedContext := storeDatasets(c, datasets)

// Verify that context has new data
Expand Down
9 changes: 6 additions & 3 deletions api/s3/s3.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,8 @@ func ListBuckets(c *gin.Context) {
}

buckets := []Bucket{}
for _, dataset := range middleware.GetDatasets(c) {
datasetCache := middleware.GetDatasets(c)
for _, dataset := range datasetCache.Datasets {
datasetInfo, err := database.GetDatasetInfo(dataset)
if err != nil {
log.Errorf("Failed to get dataset information: %v", err)
Expand Down Expand Up @@ -122,7 +123,8 @@ func ListObjects(c *gin.Context) {
dataset := c.Param("dataset")

allowed := false
for _, known := range middleware.GetDatasets(c) {
datasetCache := middleware.GetDatasets(c)
for _, known := range datasetCache.Datasets {
if dataset == known {
allowed = true

Expand Down Expand Up @@ -242,7 +244,8 @@ func parseParams(c *gin.Context) *gin.Context {
path = string(protocolPattern.ReplaceAll([]byte(path), []byte("$1/$2")))
}

for _, dataset := range middleware.GetDatasets(c) {
datasetCache := middleware.GetDatasets(c)
for _, dataset := range datasetCache.Datasets {
// check that the path starts with the dataset name, but also that the
// path is only the dataset, or that the following character is a slash.
// This prevents wrong matches in cases like when one dataset name is a
Expand Down
14 changes: 7 additions & 7 deletions api/sda/sda.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,10 @@ func Datasets(c *gin.Context) {

// Retrieve dataset list from request context
// generated by the authentication middleware
datasets := middleware.GetDatasets(c)
datasetCache := middleware.GetDatasets(c)

// Return response
c.JSON(http.StatusOK, datasets)
c.JSON(http.StatusOK, datasetCache.Datasets)
}

// find looks for a dataset name in a list of datasets
Expand All @@ -60,11 +60,11 @@ var getFiles = func(datasetID string, ctx *gin.Context) ([]*database.FileInfo, i

// Retrieve dataset list from request context
// generated by the authentication middleware
datasets := middleware.GetDatasets(ctx)
datasetCache := middleware.GetDatasets(ctx)

log.Debugf("request to process files for dataset %s", sanitizeString(datasetID))

if find(datasetID, datasets) {
if find(datasetID, datasetCache.Datasets) {
// Get file metadata
files, err := database.GetFiles(datasetID)
if err != nil {
Expand Down Expand Up @@ -133,12 +133,12 @@ func Download(c *gin.Context) {
}

// Get datasets from request context, parsed previously by token middleware
datasets := middleware.GetDatasets(c)
datasetCache := middleware.GetDatasets(c)

// Verify user has permission to datafile
permission := false
for d := range datasets {
if datasets[d] == dataset {
for d := range datasetCache.Datasets {
if datasetCache.Datasets[d] == dataset {
permission = true

break
Expand Down
61 changes: 40 additions & 21 deletions api/sda/sda_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/neicnordic/sda-download/api/middleware"
"github.com/neicnordic/sda-download/internal/config"
"github.com/neicnordic/sda-download/internal/database"
"github.com/neicnordic/sda-download/internal/session"
"github.com/neicnordic/sda-download/internal/storage"
)

Expand All @@ -24,14 +25,16 @@ func TestDatasets(t *testing.T) {
originalGetDatasets := middleware.GetDatasets

// Substitute mock functions
middleware.GetDatasets = func(c *gin.Context) []string {
return []string{"dataset1", "dataset2"}
middleware.GetDatasets = func(c *gin.Context) session.DatasetCache {
return session.DatasetCache{
Datasets: []string{"dataset1", "dataset2"},
}
}

// Mock request and response holders
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Set("datasets", []string{"dataset1", "dataset2"})
c.Set("datasets", session.DatasetCache{Datasets: []string{"dataset1", "dataset2"}})

// Test the outcomes of the handler
Datasets(c)
Expand Down Expand Up @@ -97,8 +100,10 @@ func TestGetFiles_Fail_Database(t *testing.T) {
originalGetFilesDB := database.GetFiles

// Substitute mock functions
middleware.GetDatasets = func(ctx *gin.Context) []string {
return []string{"dataset1", "dataset2"}
middleware.GetDatasets = func(ctx *gin.Context) session.DatasetCache {
return session.DatasetCache{
Datasets: []string{"dataset1", "dataset2"},
}
}
database.GetFiles = func(datasetID string) ([]*database.FileInfo, error) {
return nil, errors.New("something went wrong")
Expand Down Expand Up @@ -135,8 +140,10 @@ func TestGetFiles_Fail_NotFound(t *testing.T) {
originalGetDatasets := middleware.GetDatasets

// Substitute mock functions
middleware.GetDatasets = func(ctx *gin.Context) []string {
return []string{"dataset1", "dataset2"}
middleware.GetDatasets = func(ctx *gin.Context) session.DatasetCache {
return session.DatasetCache{
Datasets: []string{"dataset1", "dataset2"},
}
}

// Run test target
Expand Down Expand Up @@ -169,8 +176,10 @@ func TestGetFiles_Success(t *testing.T) {
originalGetFilesDB := database.GetFiles

// Substitute mock functions
middleware.GetDatasets = func(ctx *gin.Context) []string {
return []string{"dataset1", "dataset2"}
middleware.GetDatasets = func(ctx *gin.Context) session.DatasetCache {
return session.DatasetCache{
Datasets: []string{"dataset1", "dataset2"},
}
}
database.GetFiles = func(datasetID string) ([]*database.FileInfo, error) {
fileInfo := database.FileInfo{
Expand Down Expand Up @@ -447,8 +456,8 @@ func TestDownload_Fail_NoPermissions(t *testing.T) {
// nolint:goconst
return "dataset1", nil
}
middleware.GetDatasets = func(ctx *gin.Context) []string {
return []string{}
middleware.GetDatasets = func(ctx *gin.Context) session.DatasetCache {
return session.DatasetCache{}
}

// Mock request and response holders
Expand Down Expand Up @@ -490,8 +499,10 @@ func TestDownload_Fail_GetFile(t *testing.T) {
database.CheckFilePermission = func(fileID string) (string, error) {
return "dataset1", nil
}
middleware.GetDatasets = func(ctx *gin.Context) []string {
return []string{"dataset1"}
middleware.GetDatasets = func(ctx *gin.Context) session.DatasetCache {
return session.DatasetCache{
Datasets: []string{"dataset1"},
}
}
database.GetFile = func(fileID string) (*database.FileDownload, error) {
return nil, errors.New("database error")
Expand Down Expand Up @@ -538,8 +549,10 @@ func TestDownload_Fail_OpenFile(t *testing.T) {
database.CheckFilePermission = func(fileID string) (string, error) {
return "dataset1", nil
}
middleware.GetDatasets = func(ctx *gin.Context) []string {
return []string{"dataset1"}
middleware.GetDatasets = func(ctx *gin.Context) session.DatasetCache {
return session.DatasetCache{
Datasets: []string{"dataset1"},
}
}
database.GetFile = func(fileID string) (*database.FileDownload, error) {
fileDetails := &database.FileDownload{
Expand Down Expand Up @@ -594,8 +607,10 @@ func TestDownload_Fail_ParseCoordinates(t *testing.T) {
database.CheckFilePermission = func(fileID string) (string, error) {
return "dataset1", nil
}
middleware.GetDatasets = func(ctx *gin.Context) []string {
return []string{"dataset1"}
middleware.GetDatasets = func(ctx *gin.Context) session.DatasetCache {
return session.DatasetCache{
Datasets: []string{"dataset1"},
}
}
database.GetFile = func(fileID string) (*database.FileDownload, error) {
fileDetails := &database.FileDownload{
Expand Down Expand Up @@ -655,8 +670,10 @@ func TestDownload_Fail_StreamFile(t *testing.T) {
database.CheckFilePermission = func(fileID string) (string, error) {
return "dataset1", nil
}
middleware.GetDatasets = func(ctx *gin.Context) []string {
return []string{"dataset1"}
middleware.GetDatasets = func(ctx *gin.Context) session.DatasetCache {
return session.DatasetCache{
Datasets: []string{"dataset1"},
}
}
database.GetFile = func(fileID string) (*database.FileDownload, error) {
fileDetails := &database.FileDownload{
Expand Down Expand Up @@ -723,8 +740,10 @@ func TestDownload_Success(t *testing.T) {
database.CheckFilePermission = func(fileID string) (string, error) {
return "dataset1", nil
}
middleware.GetDatasets = func(ctx *gin.Context) []string {
return []string{"dataset1"}
middleware.GetDatasets = func(ctx *gin.Context) session.DatasetCache {
return session.DatasetCache{
Datasets: []string{"dataset1"},
}
}
database.GetFile = func(fileID string) (*database.FileDownload, error) {
fileDetails := &database.FileDownload{
Expand Down
Loading

0 comments on commit 7babab0

Please sign in to comment.