Skip to content

Commit

Permalink
Merge pull request #1107 from neicnordic/feature/api-keyhash-actions
Browse files Browse the repository at this point in the history
[API] expand key hash actions
  • Loading branch information
jbygdell authored Nov 12, 2024
2 parents 5275a20 + 38a1b7a commit dabdadb
Show file tree
Hide file tree
Showing 8 changed files with 337 additions and 51 deletions.
2 changes: 1 addition & 1 deletion .github/integration/tests/sda/10_upload_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ for q in accession archived backup completed inbox ingest mappings verified; do
curl -s -k -u guest:guest -X DELETE "$URI/api/queues/sda/$q/contents"
done
## truncate database
psql -U postgres -h postgres -d sda -At -c "TRUNCATE TABLE sda.files CASCADE;"
psql -U postgres -h postgres -d sda -At -c "TRUNCATE TABLE sda.files, sda.encryption_keys CASCADE;"

pip -q install s3cmd

Expand Down
41 changes: 0 additions & 41 deletions .github/integration/tests/sda/11_api-getfiles_test.sh

This file was deleted.

76 changes: 76 additions & 0 deletions .github/integration/tests/sda/11_api_test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
#!/bin/sh
set -e

# Test the API files endpoint
token="$(curl http://oidc:8080/tokens | jq -r '.[0]')"
response="$(curl -s -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

# test inserting a c4gh public key hash
payload=$(
jq -c -n \
--arg description "this is the key description" \
--arg pubkey "$( base64 -w0 /shared/c4gh.pub.pem)" \
'$ARGS.named'
)

resp="$(curl -s -k -L -o /dev/null -w "%{http_code}\n" -H "Authorization: Bearer $token" -H "Content-Type: application/json" -X POST -d "$payload" "http://api:8080/c4gh-keys/add")"
if [ "$resp" != "200" ]; then
echo "Error when adding a public key hash, expected 200 got: $resp"
exit 1
fi

# again to verify we get an error
resp="$(curl -s -k -L -o /dev/null -w "%{http_code}\n" -H "Authorization: Bearer $token" -H "Content-Type: application/json" -X POST -d "$payload" "http://api:8080/c4gh-keys/add")"
if [ "$resp" != "409" ]; then
echo "Error when adding a public key hash, expected 409 got: $resp"
exit 1
fi

# add key that will be deprecated
new_payload=$(
jq -c -n \
--arg description "this key will be deprecated" \
--arg pubkey "LS0tLS1CRUdJTiBDUllQVDRHSCBQVUJMSUMgS0VZLS0tLS0KTmdUdEFNLzRIUVR4b0I5bHZlRHVaYW5sRmVpWXVHRzBQTTg1eHNBU2xrZz0KLS0tLS1FTkQgQ1JZUFQ0R0ggUFVCTElDIEtFWS0tLS0tCg==" \
'$ARGS.named'
)

resp="$(curl -s -k -L -o /dev/null -w "%{http_code}\n" -H "Authorization: Bearer $token" -H "Content-Type: application/json" -X POST -d "$new_payload" "http://api:8080/c4gh-keys/add")"
if [ "$resp" != "200" ]; then
echo "Error when adding a public key hash, expected 200 got: $resp"
exit 1
fi

deprecated_hash="3604ed00cff81d04f1a01f65bde0ee65a9e515e898b861b43ccf39c6c0129648"

resp="$(curl -s -k -L -o /dev/null -w "%{http_code}\n" -H "Authorization: Bearer $token" -H "Content-Type: application/json" -X POST "http://api:8080/c4gh-keys/deprecate/$deprecated_hash")"
if [ "$resp" != "200" ]; then
echo "Error when adding a public key hash, expected 200 got: $resp"
exit 1
fi


# list key hashes
resp="$(curl -s -k -L -H "Authorization: Bearer $token" -X GET "http://api:8080/c4gh-keys/list" | jq '. | length')"
if [ "$resp" -ne 2 ]; then
echo "Error when listing key hash, expected 2 entries got: $resp"
exit 1
fi

manual_hash=$(sed -n '2p' /shared/c4gh.pub.pem | base64 -d -w0 | xxd -c64 -ps)
resp="$(curl -s -k -L -H "Authorization: Bearer $token" -X GET "http://api:8080/c4gh-keys/list" | jq -r .[0].hash)"
if [ "$resp" != "$manual_hash" ]; then
echo "Error when listing key hash, expected $manual_hash got: $resp"
exit 1
fi
ts=$(date +"%F %T")
depr="$(curl -s -k -L -H "Authorization: Bearer $token" -X GET "http://api:8080/c4gh-keys/list" | jq -r .[1].deprecated_at)"
if [ "$depr" != "$ts" ]; then
echo "Error when listing key hash, expected $ts got: $depr"
exit 1
fi

echo "api test completed successfully"
47 changes: 40 additions & 7 deletions sda/cmd/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,13 +88,15 @@ func setup(config *config.Config) *http.Server {
r.GET("/files", getFiles)
// admin endpoints below here
if len(config.API.Admins) > 0 {
r.POST("/file/ingest", isAdmin(), ingestFile) // start ingestion of a file
r.POST("/file/accession", isAdmin(), setAccession) // assign accession ID to a file
r.POST("/dataset/create", isAdmin(), createDataset) // maps a set of files to a dataset
r.POST("/dataset/release/*dataset", isAdmin(), releaseDataset) // Releases a dataset to be accessible
r.POST("/c4gh-keys/add", isAdmin(), addC4ghHash) // Adds a key hash to the database
r.GET("/users", isAdmin(), listActiveUsers) // Lists all users
r.GET("/users/:username/files", isAdmin(), listUserFiles) // Lists all unmapped files for a user
r.POST("/file/ingest", isAdmin(), ingestFile) // start ingestion of a file
r.POST("/file/accession", isAdmin(), setAccession) // assign accession ID to a file
r.POST("/dataset/create", isAdmin(), createDataset) // maps a set of files to a dataset
r.POST("/dataset/release/*dataset", isAdmin(), releaseDataset) // Releases a dataset to be accessible
r.POST("/c4gh-keys/add", isAdmin(), addC4ghHash) // Adds a key hash to the database
r.POST("/c4gh-keys/deprecate/*keyHash", isAdmin(), deprecateC4ghHash) // Deprecate a given key hash
r.GET("/c4gh-keys/list", isAdmin(), listC4ghHashes) // Lists key hashes in the database
r.GET("/users", isAdmin(), listActiveUsers) // Lists all users
r.GET("/users/:username/files", isAdmin(), listUserFiles) // Lists all unmapped files for a user
}

cfg := &tls.Config{MinVersion: tls.VersionTLS12}
Expand Down Expand Up @@ -551,3 +553,34 @@ func addC4ghHash(c *gin.Context) {

c.Status(http.StatusOK)
}

func listC4ghHashes(c *gin.Context) {
hashes, err := Conf.API.DB.ListKeyHashes()
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, err.Error())

return
}

for n, h := range hashes {
ct, _ := time.Parse(time.RFC3339, h.CreatedAt)
hashes[n].CreatedAt = ct.Format(time.DateTime)

if h.DeprecatedAt != "" {
dt, _ := time.Parse(time.RFC3339, h.DeprecatedAt)
hashes[n].DeprecatedAt = dt.Format(time.DateTime)
}
}
c.Writer.Header().Set("Content-Type", "application/json")
c.JSON(200, hashes)
}

func deprecateC4ghHash(c *gin.Context) {
keyHash := strings.TrimPrefix(c.Param("keyHash"), "/")
err = Conf.API.DB.DeprecateKeyHash(keyHash)
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, err.Error())

return
}
}
106 changes: 105 additions & 1 deletion sda/cmd/api/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,9 @@ func TestMain(m *testing.M) {
if err := pool.Purge(oidc); err != nil {
log.Fatalf("Could not purge resource: %s", err)
}
// cleanup temp files
_ = os.RemoveAll(ECPath)
_ = os.RemoveAll(RSAPath)

os.Exit(code)
}
Expand Down Expand Up @@ -376,7 +379,9 @@ func (suite *TestSuite) SetupSuite() {
assert.NoError(suite.T(), err)

}

func (suite *TestSuite) TearDownSuite() {
assert.NoError(suite.T(), os.RemoveAll(suite.Path))
}
func (suite *TestSuite) SetupTest() {
Conf.Database = database.DBConf{
Host: "localhost",
Expand Down Expand Up @@ -1517,3 +1522,102 @@ func (suite *TestSuite) TestAddC4ghHash_notBase64() {
assert.Equal(suite.T(), http.StatusBadRequest, resp.StatusCode)
defer resp.Body.Close()
}

func (suite *TestSuite) TestListC4ghHashes() {
assert.NoError(suite.T(), Conf.API.DB.AddKeyHash("cbd8f5cc8d936ce437a52cd7991453839581fc69ee26e0daefde6a5d2660fc23", "this is a test key"), "failed to register key in database")

expectedResponse := database.C4ghKeyHash{
Hash: "cbd8f5cc8d936ce437a52cd7991453839581fc69ee26e0daefde6a5d2660fc23",
Description: "this is a test key",
CreatedAt: time.Now().UTC().Format(time.DateTime),
DeprecatedAt: "",
}

gin.SetMode(gin.ReleaseMode)
assert.NoError(suite.T(), setupJwtAuth())
Conf.API.Admins = []string{"dummy"}

r := gin.Default()
r.GET("/c4gh-keys/list", isAdmin(), listC4ghHashes)
ts := httptest.NewServer(r)
defer ts.Close()

client := &http.Client{}
assert.NoError(suite.T(), setupJwtAuth())

req, err := http.NewRequest("GET", ts.URL+"/c4gh-keys/list", nil)
assert.NoError(suite.T(), err)
req.Header.Add("Authorization", "Bearer "+suite.Token)

resp, err := client.Do(req)
assert.NoError(suite.T(), err)
assert.Equal(suite.T(), http.StatusOK, resp.StatusCode)
defer resp.Body.Close()

hashes := []database.C4ghKeyHash{}
err = json.NewDecoder(resp.Body).Decode(&hashes)
assert.NoError(suite.T(), err, "failed to list users from DB")
for n, h := range hashes {
if h.Hash == "cbd8f5cc8d936ce437a52cd7991453839581fc69ee26e0daefde6a5d2660fc23" {
assert.Equal(suite.T(), expectedResponse, hashes[n])

break
}
}
}

func (suite *TestSuite) TestDeprecateC4ghHash() {
assert.NoError(suite.T(), Conf.API.DB.AddKeyHash("abc8f5cc8d936ce437a52cd9991453839581fc69ee26e0daefde6a5d2660fc23", "this is a deprecation test key"), "failed to register key in database")

gin.SetMode(gin.ReleaseMode)
assert.NoError(suite.T(), setupJwtAuth())
Conf.API.Admins = []string{"dummy"}

r := gin.Default()
r.POST("/c4gh-keys/deprecate/*keyHash", isAdmin(), deprecateC4ghHash)
ts := httptest.NewServer(r)
defer ts.Close()

client := &http.Client{}
assert.NoError(suite.T(), setupJwtAuth())

req, err := http.NewRequest("POST", ts.URL+"/c4gh-keys/deprecate/abc8f5cc8d936ce437a52cd9991453839581fc69ee26e0daefde6a5d2660fc23", http.NoBody)
assert.NoError(suite.T(), err)
req.Header.Add("Authorization", "Bearer "+suite.Token)

resp, err := client.Do(req)
assert.NoError(suite.T(), err)
assert.Equal(suite.T(), http.StatusOK, resp.StatusCode)
defer resp.Body.Close()

// a second time gives an error since the key is alreadu deprecated
resp2, err := client.Do(req)
assert.NoError(suite.T(), err)
assert.Equal(suite.T(), http.StatusBadRequest, resp2.StatusCode)
defer resp2.Body.Close()
}

func (suite *TestSuite) TestDeprecateC4ghHash_wrongHash() {
assert.NoError(suite.T(), Conf.API.DB.AddKeyHash("abc8f5cc8d936ce437a52cd7991453839581fc69ee26e0daefde6a5d2660fc99", "this is a deprecation test key"), "failed to register key in database")

gin.SetMode(gin.ReleaseMode)
assert.NoError(suite.T(), setupJwtAuth())
Conf.API.Admins = []string{"dummy"}

r := gin.Default()
r.POST("/c4gh-keys/deprecate/*keyHash", isAdmin(), deprecateC4ghHash)
ts := httptest.NewServer(r)
defer ts.Close()

client := &http.Client{}
assert.NoError(suite.T(), setupJwtAuth())

req, err := http.NewRequest("POST", ts.URL+"/c4gh-keys/deprecate/xyz8f5cc8d936ce437a52cd7991453839581fc69ee26e0daefde6a5d2660fc23", http.NoBody)
assert.NoError(suite.T(), err)
req.Header.Add("Authorization", "Bearer "+suite.Token)

resp, err := client.Do(req)
assert.NoError(suite.T(), err)
assert.Equal(suite.T(), http.StatusBadRequest, resp.StatusCode)
defer resp.Body.Close()
}
57 changes: 57 additions & 0 deletions sda/internal/database/db_functions.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
package database

import (
"database/sql"
"encoding/hex"
"errors"
"math"
Expand Down Expand Up @@ -782,3 +783,59 @@ func (dbs *SDAdb) addKeyHash(keyHash, keyDescription string) error {

return nil
}

type C4ghKeyHash struct {
Hash string `json:"hash"`
Description string `json:"description"`
CreatedAt string `json:"created_at"`
DeprecatedAt string `json:"deprecated_at"`
}

// ListKeyHashes lists the hashes from the encryption_keys table
func (dbs *SDAdb) ListKeyHashes() ([]C4ghKeyHash, error) {
dbs.checkAndReconnectIfNeeded()
db := dbs.DB

const query = "SELECT key_hash, description, created_at, deprecated_at FROM sda.encryption_keys ORDER BY created_at ASC;"

hashList := []C4ghKeyHash{}
rows, err := db.Query(query)
if err != nil {
return nil, err
}
if rows.Err() != nil {
return nil, rows.Err()
}
defer rows.Close()

for rows.Next() {
h := &C4ghKeyHash{}
depr := sql.NullString{}
err := rows.Scan(&h.Hash, &h.Description, &h.CreatedAt, &depr)
if err != nil {
return nil, err
}
h.DeprecatedAt = depr.String

hashList = append(hashList, *h)
}

return hashList, nil
}

func (dbs *SDAdb) DeprecateKeyHash(keyHash string) error {
dbs.checkAndReconnectIfNeeded()
db := dbs.DB

const query = "UPDATE sda.encryption_keys set deprecated_at = NOW() WHERE key_hash = $1 AND deprecated_at IS NULL;"
result, err := db.Exec(query, keyHash)
if err != nil {
return err
}

if rowsAffected, _ := result.RowsAffected(); rowsAffected == 0 {
return errors.New("key hash not found or already deprecated")
}

return nil
}
Loading

0 comments on commit dabdadb

Please sign in to comment.