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

[API] expand key hash actions #1107

Merged
merged 4 commits into from
Nov 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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 @@ -82,13 +82,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 @@ -501,3 +503,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 {
MalinAhlberg marked this conversation as resolved.
Show resolved Hide resolved
jbygdell marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -1374,3 +1379,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
Loading