Skip to content
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

Add files api #360

Merged
merged 34 commits into from
Nov 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
041e051
move api from sda-pipeline
MalinAhlberg Oct 16, 2023
2f1393a
adapt code to new repo
MalinAhlberg Oct 16, 2023
82fb61c
move in api db functions from sda-pipeline
MalinAhlberg Oct 16, 2023
9c13a40
refactor jwt verification
MalinAhlberg Oct 17, 2023
2fe97ad
refactor: use common auth
MalinAhlberg Oct 26, 2023
f45d7a7
fix: db query for get files
MalinAhlberg Oct 31, 2023
6872913
fix: avoid panic if validation token is not setup properly
MalinAhlberg Oct 31, 2023
95ece28
clean up and simplify api tests
MalinAhlberg Oct 20, 2023
c2b4906
refactor: break out auth header parsning and add test
MalinAhlberg Oct 31, 2023
2cc4ab1
refactor: merge api test files
MalinAhlberg Nov 8, 2023
e84581a
refactor: move mock authentication to helper
MalinAhlberg Nov 8, 2023
d06e0fe
fix api tests
MalinAhlberg Nov 9, 2023
4bcf9a6
update dependencies
MalinAhlberg Nov 9, 2023
3e0fef1
add api integration tests
MalinAhlberg Nov 9, 2023
ff410e0
update dependencies
MalinAhlberg Nov 9, 2023
358aa1c
fix linter complaints
MalinAhlberg Nov 9, 2023
08f2846
break out api test
MalinAhlberg Nov 13, 2023
b20b3e6
tests: update env var to integration tests
MalinAhlberg Nov 13, 2023
466645e
Update sda/cmd/api/api.go
MalinAhlberg Nov 14, 2023
70a334a
refactor: prettify, clean up etc
MalinAhlberg Nov 14, 2023
6636457
fix: update test conf for mq
MalinAhlberg Nov 14, 2023
983c328
refactor: clean up go mod file
MalinAhlberg Nov 14, 2023
7e18b71
remove newline
MalinAhlberg Nov 14, 2023
010c7e8
Add api readme
MalinAhlberg Nov 14, 2023
2b29bda
Update sda/cmd/api/api.md
MalinAhlberg Nov 23, 2023
fb710c3
fix: list all relevant files in files api
MalinAhlberg Nov 23, 2023
c371b79
Update sda/cmd/api/api.md
MalinAhlberg Nov 24, 2023
3670a72
Update sda/cmd/api/api.go
MalinAhlberg Nov 27, 2023
9ca18d6
Update sda/cmd/api/api.go
MalinAhlberg Nov 27, 2023
8d10f96
purge all containers if oidc fails
MalinAhlberg Nov 27, 2023
153ab99
refactor: make all tests part of testsuite
MalinAhlberg Nov 27, 2023
f384cdd
feat: add file size to files api
MalinAhlberg Nov 27, 2023
ece5d66
add test for db getuserfiles
MalinAhlberg Nov 27, 2023
61b4cb1
Revert "feat: add file size to files api"
MalinAhlberg Nov 30, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 31 additions & 2 deletions .github/integration/sda-s3-integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,33 @@ services:
- ./sda/oidc.py:/oidc.py
- shared:/shared

api:
command: [ sda-api ]
container_name: api
depends_on:
credentials:
condition: service_completed_successfully
postgres:
condition: service_healthy
oidc:
condition: service_healthy
rabbitmq:
condition: service_healthy
environment:
- BROKER_PASSWORD=ingest
- BROKER_USER=ingest
- BROKER_ROUTINGKEY=ingest
- BROKER_ROUTINGERROR=error
- DB_PASSWORD=download
- DB_USER=download
image: ghcr.io/neicnordic/sensitive-data-archive:PR${PR_NUMBER}
ports:
- "8090:8080"
restart: always
volumes:
- ./sda/config.yaml:/config.yaml
- shared:/shared

integration_test:
container_name: tester
command:
Expand All @@ -298,7 +325,9 @@ services:
sync-api:
condition: service_started
verify:
condition: service_started
condition: service_started
api:
condition: service_started
environment:
- PGPASSWORD=rootpasswd
- STORAGETYPE=s3
Expand All @@ -313,4 +342,4 @@ volumes:
minio_data:
postgres_data:
rabbitmq_data:
shared:
shared:
11 changes: 11 additions & 0 deletions .github/integration/tests/sda/11_api-getfiles_test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#!/bin/sh
set -e

# Test the API files endpoint
token="$(curl http://oidc:8080/tokens | jq -r '.[0]')"
curl -k -L "http://api:8080/files" -H "Authorization: Bearer $token"
response="$(curl -k -L "http://api:8080/files" -H "Authorization: Bearer $token" | jq -r 'sort_by(.inboxPath)|.[-1].fileStatus')"
if [ "$response" != "uploaded" ]; then
echo "API returned incorrect value, expected ready got: $response"
exit 1
fi
183 changes: 183 additions & 0 deletions sda/cmd/api/api.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
package main

import (
"context"
"crypto/tls"
"fmt"
"net/http"
"os"
"os/signal"
"syscall"
"time"

"github.com/gin-gonic/gin"
"github.com/lestrrat-go/jwx/v2/jwk"
"github.com/neicnordic/sensitive-data-archive/internal/broker"
"github.com/neicnordic/sensitive-data-archive/internal/config"
"github.com/neicnordic/sensitive-data-archive/internal/database"
"github.com/neicnordic/sensitive-data-archive/internal/userauth"
log "github.com/sirupsen/logrus"
)

var Conf *config.Config
var err error
var auth *userauth.ValidateFromToken

func main() {
Conf, err = config.NewConfig("api")
if err != nil {
log.Fatal(err)
}
Conf.API.MQ, err = broker.NewMQ(Conf.Broker)
if err != nil {
log.Fatal(err)
}
pontus marked this conversation as resolved.
Show resolved Hide resolved
Conf.API.DB, err = database.NewSDAdb(Conf.Database)
if err != nil {
log.Fatal(err)
}

if err := setupJwtAuth(); err != nil {
log.Fatalf("error when setting up JWT auth, reason %s", err.Error())

}
sigc := make(chan os.Signal, 5)
signal.Notify(sigc, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
go func() {
<-sigc
shutdown()
os.Exit(0)
}()

srv := setup(Conf)
if Conf.API.ServerCert != "" && Conf.API.ServerKey != "" {
log.Infof("Starting web server at https://%s:%d", Conf.API.Host, Conf.API.Port)
if err := srv.ListenAndServeTLS(Conf.API.ServerCert, Conf.API.ServerKey); err != nil {
shutdown()
log.Fatalln(err)
}
} else {
log.Infof("Starting web server at http://%s:%d", Conf.API.Host, Conf.API.Port)
if err := srv.ListenAndServe(); err != nil {
shutdown()
log.Fatalln(err)
}
}

}

func setup(config *config.Config) *http.Server {
r := gin.Default()
r.GET("/ready", readinessResponse)
r.GET("/files", getFiles)

cfg := &tls.Config{
MinVersion: tls.VersionTLS12,
CurvePreferences: []tls.CurveID{tls.CurveP521, tls.CurveP384, tls.CurveP256},
PreferServerCipherSuites: true,
CipherSuites: []uint16{
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
},
}

srv := &http.Server{
Addr: config.API.Host + ":" + fmt.Sprint(config.API.Port),
Handler: r,
TLSConfig: cfg,
TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler)),
ReadHeaderTimeout: 20 * time.Second,
ReadTimeout: 5 * time.Minute,
WriteTimeout: 20 * time.Second,
}

return srv
}

func setupJwtAuth() error {
auth = userauth.NewValidateFromToken(jwk.NewSet())
if Conf.Server.Jwtpubkeyurl != "" {
if err := auth.FetchJwtPubKeyURL(Conf.Server.Jwtpubkeyurl); err != nil {
return err
}
}
if Conf.Server.Jwtpubkeypath != "" {
if err := auth.ReadJwtPubKeyPath(Conf.Server.Jwtpubkeypath); err != nil {
return err
}
}

return nil
}

func shutdown() {
defer Conf.API.MQ.Channel.Close()
defer Conf.API.MQ.Connection.Close()
defer Conf.API.DB.Close()
}

func readinessResponse(c *gin.Context) {
statusCode := http.StatusOK

if Conf.API.MQ.Connection.IsClosed() {
statusCode = http.StatusServiceUnavailable
newConn, err := broker.NewMQ(Conf.Broker)
if err != nil {
log.Errorf("failed to reconnect to MQ, reason: %v", err)
} else {
Conf.API.MQ = newConn
}
}

if Conf.API.MQ.Channel.IsClosed() {
statusCode = http.StatusServiceUnavailable
Conf.API.MQ.Connection.Close()
newConn, err := broker.NewMQ(Conf.Broker)
if err != nil {
log.Errorf("failed to reconnect to MQ, reason: %v", err)
} else {
Conf.API.MQ = newConn
}
}

if DBRes := checkDB(Conf.API.DB, 5*time.Millisecond); DBRes != nil {
log.Debugf("DB connection error :%v", DBRes)
Conf.API.DB.Reconnect()
statusCode = http.StatusServiceUnavailable
}

c.JSON(statusCode, "")
}

func checkDB(database *database.SDAdb, timeout time.Duration) error {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
if database.DB == nil {
return fmt.Errorf("database is nil")
}

return database.DB.PingContext(ctx)
}

// getFiles returns the files from the database for a specific user
func getFiles(c *gin.Context) {
c.Writer.Header().Set("Content-Type", "application/json")
// Get user ID to extract all files
token, err := auth.Authenticate(c.Request)
if err != nil {
// something went wrong with user token
c.JSON(401, err.Error())

return
}

files, err := Conf.API.DB.GetUserFiles(token.Subject())
if err != nil {
// something went wrong with querying or parsing rows
c.JSON(502, err.Error())

return
}

// Return response
c.JSON(200, files)
}
19 changes: 19 additions & 0 deletions sda/cmd/api/api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# API
The API service provides data submitters with functionality to control
their submissions. Users are authenticated with a JWT.

## Service Description

Endpoints:
- `/files`

1. Parses and validates the JWT token against the public keys, either locally provisioned or from OIDC JWK endpoints.
2. The `sub` field from the token is extracted and used as the user's identifier
3. All files belonging to this user are extracted from the database, together with their latest status and creation date

Example:
```bash
$ curl 'https://server/files' -H "Authorization: Bearer $token"
[{"inboxPath":"requester_demo.org/data/file1.c4gh","fileStatus":"uploaded","createAt":"2023-11-13T10:12:43.144242Z"}]
```
If the `token` is invalid, 401 is returned.
Loading
Loading