From d02258a8ef2ce626d2c9a82d2223e22fbfeed5be Mon Sep 17 00:00:00 2001 From: Alex Aperis Date: Wed, 4 Dec 2024 19:25:24 +0100 Subject: [PATCH 01/22] docker complains about mized uppe/lower case --- sda-download/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sda-download/Dockerfile b/sda-download/Dockerfile index 462b97edb..b9ceec036 100644 --- a/sda-download/Dockerfile +++ b/sda-download/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.23-alpine as builder +FROM golang:1.23-alpine AS builder ENV GOPATH=$PWD ENV CGO_ENABLED=0 From a2d465a8776f1eff9754845073ff8f0c800ea310 Mon Sep 17 00:00:00 2001 From: Alex Aperis Date: Wed, 4 Dec 2024 21:23:41 +0100 Subject: [PATCH 02/22] cleanup test scripts - use neic crypt4gh - do not use pip - silent curls --- .../integration/setup/common/10_services.sh | 1 + .../integration/setup/common/1_keys.sh | 1 + .../integration/setup/common/20_tools.sh | 6 +++- .../setup/common/23_create_db_entries.sh | 1 + .../setup/s3/100_s3_storage_setup.sh | 3 +- .../setup/s3notls/99_s3_storage_setup.sh | 3 +- .../integration/tests/common/30_check_db.sh | 1 + .../tests/common/50_check_endpoint.sh | 19 ++++++------- .../tests/common/60_check_s3_endpoint.sh | 1 - .../tests/common/70_check_download.sh | 8 ++++-- .../tests/common/80_check_reencrypt.sh | 28 ++++++++----------- .../tests/s3notls/52_check_endpoint.sh | 20 ++++++------- 12 files changed, 48 insertions(+), 44 deletions(-) diff --git a/sda-download/.github/integration/setup/common/10_services.sh b/sda-download/.github/integration/setup/common/10_services.sh index 1680ff917..52af643e8 100644 --- a/sda-download/.github/integration/setup/common/10_services.sh +++ b/sda-download/.github/integration/setup/common/10_services.sh @@ -1,4 +1,5 @@ #!/bin/bash +set -e # Build containers docker build -t neicnordic/sda-download:latest . || exit 1 diff --git a/sda-download/.github/integration/setup/common/1_keys.sh b/sda-download/.github/integration/setup/common/1_keys.sh index 2e8f08653..3a3bcdb08 100644 --- a/sda-download/.github/integration/setup/common/1_keys.sh +++ b/sda-download/.github/integration/setup/common/1_keys.sh @@ -1,4 +1,5 @@ #!/bin/bash +set -e cd dev_utils || exit 1 diff --git a/sda-download/.github/integration/setup/common/20_tools.sh b/sda-download/.github/integration/setup/common/20_tools.sh index f134a6081..a4d4e5042 100644 --- a/sda-download/.github/integration/setup/common/20_tools.sh +++ b/sda-download/.github/integration/setup/common/20_tools.sh @@ -1,4 +1,8 @@ #!/bin/bash +set -e + +C4GH_VERSION="$(curl --retry 100 -sL https://api.github.com/repos/neicnordic/crypt4gh/releases/latest | jq -r '.name')" +curl --retry 100 -sL https://github.com/neicnordic/crypt4gh/releases/download/"${C4GH_VERSION}"/crypt4gh_linux_x86_64.tar.gz | sudo tar -xz -C /usr/bin/ && + sudo chmod +x /usr/bin/crypt4gh -pip3 install crypt4gh sudo apt install -y jq s3cmd diff --git a/sda-download/.github/integration/setup/common/23_create_db_entries.sh b/sda-download/.github/integration/setup/common/23_create_db_entries.sh index 95172e88c..14085487d 100755 --- a/sda-download/.github/integration/setup/common/23_create_db_entries.sh +++ b/sda-download/.github/integration/setup/common/23_create_db_entries.sh @@ -1,4 +1,5 @@ #!/bin/bash +set -e cd dev_utils || exit 1 diff --git a/sda-download/.github/integration/setup/s3/100_s3_storage_setup.sh b/sda-download/.github/integration/setup/s3/100_s3_storage_setup.sh index 5a58fc2f9..ac2d6b7a7 100644 --- a/sda-download/.github/integration/setup/s3/100_s3_storage_setup.sh +++ b/sda-download/.github/integration/setup/s3/100_s3_storage_setup.sh @@ -1,6 +1,5 @@ #!/bin/bash - -pip3 install s3cmd +set -e cd dev_utils || exit 1 diff --git a/sda-download/.github/integration/setup/s3notls/99_s3_storage_setup.sh b/sda-download/.github/integration/setup/s3notls/99_s3_storage_setup.sh index 8d34eb95d..c24e36199 100644 --- a/sda-download/.github/integration/setup/s3notls/99_s3_storage_setup.sh +++ b/sda-download/.github/integration/setup/s3notls/99_s3_storage_setup.sh @@ -1,6 +1,7 @@ #!/bin/bash +set -e -pip3 install s3cmd +sudo apt install -y s3cmd cd dev_utils || exit 1 diff --git a/sda-download/.github/integration/tests/common/30_check_db.sh b/sda-download/.github/integration/tests/common/30_check_db.sh index 30423f939..4a2509093 100644 --- a/sda-download/.github/integration/tests/common/30_check_db.sh +++ b/sda-download/.github/integration/tests/common/30_check_db.sh @@ -1,4 +1,5 @@ #!/bin/bash +set -e cd dev_utils || exit 1 diff --git a/sda-download/.github/integration/tests/common/50_check_endpoint.sh b/sda-download/.github/integration/tests/common/50_check_endpoint.sh index da754245d..8fa2c20c0 100755 --- a/sda-download/.github/integration/tests/common/50_check_endpoint.sh +++ b/sda-download/.github/integration/tests/common/50_check_endpoint.sh @@ -56,11 +56,11 @@ echo "got correct response when POST method used" # ------------------ # Test good token -token=$(curl --cacert certs/ca.pem "https://localhost:8000/tokens" | jq -r '.[0]') +token=$(curl -s --cacert certs/ca.pem "https://localhost:8000/tokens" | jq -r '.[0]') ## Test datasets endpoint -check_dataset=$(curl --cacert certs/ca.pem -H "Authorization: Bearer $token" https://localhost:8443/metadata/datasets | jq -r '.[0]') +check_dataset=$(curl -s --cacert certs/ca.pem -H "Authorization: Bearer $token" https://localhost:8443/metadata/datasets | jq -r '.[0]') if [ "$check_dataset" != "https://doi.example/ty009.sfrrss/600.45asasga" ]; then echo "dataset https://doi.example/ty009.sfrrss/600.45asasga not found" @@ -72,7 +72,7 @@ echo "expected dataset found" ## Test datasets/files endpoint -check_files=$(curl --cacert certs/ca.pem -H "Authorization: Bearer $token" "https://localhost:8443/metadata/datasets/https://doi.example/ty009.sfrrss/600.45asasga/files" | jq -r '.[0].fileId') +check_files=$(curl -s --cacert certs/ca.pem -H "Authorization: Bearer $token" "https://localhost:8443/metadata/datasets/https://doi.example/ty009.sfrrss/600.45asasga/files" | jq -r '.[0].fileId') if [ "$check_files" != "urn:neic:001-002" ]; then echo "file with id urn:neic:001-002 not found" @@ -88,10 +88,9 @@ echo "expected file found" C4GH_PASSPHRASE=$(grep -F passphrase config.yaml | sed -e 's/.* //' -e 's/"//g') export C4GH_PASSPHRASE -crypt4gh decrypt --sk c4gh.sec.pem < dummy_data.c4gh > old-file.txt - -curl --cacert certs/ca.pem -H "Authorization: Bearer $token" "https://localhost:8443/files/urn:neic:001-002" --output test-download.txt +crypt4gh decrypt -s c4gh.sec.pem -f dummy_data.c4gh && mv dummy_data old-file.txt +curl -s --cacert certs/ca.pem -H "Authorization: Bearer $token" "https://localhost:8443/files/urn:neic:001-002" --output test-download.txt cmp --silent old-file.txt test-download.txt status=$? @@ -102,7 +101,7 @@ else exit 1 fi -curl --cacert certs/ca.pem -H "Authorization: Bearer $token" "https://localhost:8443/files/urn:neic:001-002?startCoordinate=0&endCoordinate=2" --output test-part.txt +curl -s --cacert certs/ca.pem -H "Authorization: Bearer $token" "https://localhost:8443/files/urn:neic:001-002?startCoordinate=0&endCoordinate=2" --output test-part.txt dd if=old-file.txt ibs=1 skip=0 count=2 > old-part.txt @@ -115,7 +114,7 @@ else exit 1 fi -curl --cacert certs/ca.pem -H "Authorization: Bearer $token" "https://localhost:8443/files/urn:neic:001-002?startCoordinate=7&endCoordinate=14" --output test-part2.txt +curl -s --cacert certs/ca.pem -H "Authorization: Bearer $token" "https://localhost:8443/files/urn:neic:001-002?startCoordinate=7&endCoordinate=14" --output test-part2.txt dd if=old-file.txt ibs=1 skip=7 count=7 > old-part2.txt @@ -128,7 +127,7 @@ else exit 1 fi -curl --cacert certs/ca.pem -H "Authorization: Bearer $token" "https://localhost:8443/files/urn:neic:001-002?startCoordinate=70000&endCoordinate=140000" --output test-part3.txt +curl -s --cacert certs/ca.pem -H "Authorization: Bearer $token" "https://localhost:8443/files/urn:neic:001-002?startCoordinate=70000&endCoordinate=140000" --output test-part3.txt dd if=old-file.txt ibs=1 skip=70000 count=70000 > old-part3.txt @@ -162,7 +161,7 @@ echo "got correct response when token has no permissions" # Test token with untrusted sources # for this test we attach a list of trusted sources -token=$(curl --cacert certs/ca.pem "https://localhost:8000/tokens" | jq -r '.[2]') +token=$(curl -s --cacert certs/ca.pem "https://localhost:8000/tokens" | jq -r '.[2]') ## Test datasets endpoint diff --git a/sda-download/.github/integration/tests/common/60_check_s3_endpoint.sh b/sda-download/.github/integration/tests/common/60_check_s3_endpoint.sh index 2d3a69d7b..cc86c44ee 100644 --- a/sda-download/.github/integration/tests/common/60_check_s3_endpoint.sh +++ b/sda-download/.github/integration/tests/common/60_check_s3_endpoint.sh @@ -1,6 +1,5 @@ #!/bin/bash - TLS="True" [[ "$STORAGETYPE" == "s3notls" ]] && TLS="False" diff --git a/sda-download/.github/integration/tests/common/70_check_download.sh b/sda-download/.github/integration/tests/common/70_check_download.sh index 795325a47..87d7d8865 100644 --- a/sda-download/.github/integration/tests/common/70_check_download.sh +++ b/sda-download/.github/integration/tests/common/70_check_download.sh @@ -7,7 +7,7 @@ fi cd dev_utils || exit 1 # get a token, set up variables -token=$(curl --cacert certs/ca.pem "https://localhost:8000/tokens" | jq -r '.[0]') +token=$(curl -s --cacert certs/ca.pem "https://localhost:8000/tokens" | jq -r '.[0]') dataset="https://doi.example/ty009.sfrrss/600.45asasga" file="dummy_data" expected_size=1048605 @@ -15,7 +15,7 @@ C4GH_PASSPHRASE=$(grep -F passphrase config.yaml | sed -e 's/.* //' -e 's/"//g') export C4GH_PASSPHRASE # download decrypted full file, check file size -curl --cacert certs/ca.pem -H "Authorization: Bearer $token" "https://localhost:8443/s3/$dataset/$file" --output full1.bam +curl -s --cacert certs/ca.pem -H "Authorization: Bearer $token" "https://localhost:8443/s3/$dataset/$file" --output full1.bam file_size=$(stat -c %s full1.bam) # Get the size of the file if [ "$file_size" -ne "$expected_size" ]; then @@ -24,9 +24,11 @@ if [ "$file_size" -ne "$expected_size" ]; then fi # test that start, end=0 returns the whole file -curl --cacert certs/ca.pem -H "Authorization: Bearer $token" "https://localhost:8443/s3/$dataset/$file?startCoordinate=0&endCoordinate=0" --output full2.bam +curl -s --cacert certs/ca.pem -H "Authorization: Bearer $token" "https://localhost:8443/s3/$dataset/$file?startCoordinate=0&endCoordinate=0" --output full2.bam if ! cmp --silent full1.bam full2.bam; then echo "Full decrypted files, with and without coordinates, are different" exit 1 fi + +echo "OK" \ No newline at end of file diff --git a/sda-download/.github/integration/tests/common/80_check_reencrypt.sh b/sda-download/.github/integration/tests/common/80_check_reencrypt.sh index d57102067..5e60579e0 100644 --- a/sda-download/.github/integration/tests/common/80_check_reencrypt.sh +++ b/sda-download/.github/integration/tests/common/80_check_reencrypt.sh @@ -7,7 +7,7 @@ fi cd dev_utils || exit 1 # Get a token, set up variables -token=$(curl --cacert certs/ca.pem "https://localhost:8000/tokens" | jq -r '.[0]') +token=$(curl -s --cacert certs/ca.pem "https://localhost:8000/tokens" | jq -r '.[0]') if [ -z "$token" ]; then echo "Failed to obtain token" @@ -19,15 +19,9 @@ file="dummy_data" expected_size=1048605 # Download unencrypted full file, check file size -curl --cacert certs/ca.pem -H "Authorization: Bearer $token" "https://localhost:8443/s3/$dataset/$file" --output full1.bam - -if [ ! -f "full1.bam" ]; then - echo "Failed to download full1.bam" - exit 1 -fi +curl -s --cacert certs/ca.pem -H "Authorization: Bearer $token" "https://localhost:8443/s3/$dataset/$file" --output full1.bam file_size=$(stat -c %s full1.bam) # Get the size of the file - if [ "$file_size" -ne "$expected_size" ]; then echo "Incorrect file size for downloaded file" exit 1 @@ -36,11 +30,7 @@ fi # Test reencrypt the file header with the client public key clientkey=$(base64 -w0 client.pub.pem) reencryptedFile=reencrypted.bam.c4gh -curl --cacert certs/ca.pem -H "Authorization: Bearer $token" -H "Client-Public-Key: $clientkey" "https://localhost:8443/s3-encrypted/$dataset/$file" --output $reencryptedFile -if [ ! -f "$reencryptedFile" ]; then - echo "Failed to download re-encrypted file" - exit 1 -fi +curl -s --cacert certs/ca.pem -H "Authorization: Bearer $token" -H "Client-Public-Key: $clientkey" "https://localhost:8443/s3-encrypted/$dataset/$file" --output $reencryptedFile expected_encrypted_size=1049205 file_size=$(stat -c %s $reencryptedFile) @@ -51,10 +41,13 @@ fi # Decrypt the reencrypted file and compare it with the original unencrypted file export C4GH_PASSPHRASE="strongpass" # passphrase for the client crypt4gh key -if ! crypt4gh decrypt --sk client.sec.pem < $reencryptedFile > full2.bam; then +crypt4gh decrypt -s client.sec.pem -f $reencryptedFile +if [ ! -f "${reencryptedFile%.c4gh}" ] ; then echo "Failed to decrypt re-encrypted file with the client's private key" exit 1 fi +mv "${reencryptedFile%.c4gh}" full2.bam + if ! cmp --silent full1.bam full2.bam; then echo "Decrypted version of $reencryptedFile and the original unencrypted file, are different" @@ -63,7 +56,7 @@ fi # download reencrypted partial file, check file size partReencryptedFile=part1.bam.c4gh -curl --cacert certs/ca.pem -H "Authorization: Bearer $token" -H "Client-Public-Key: $clientkey" "https://localhost:8443/s3-encrypted/$dataset/$file?startCoordinate=0&endCoordinate=1000" --output $partReencryptedFile +curl -s --cacert certs/ca.pem -H "Authorization: Bearer $token" -H "Client-Public-Key: $clientkey" "https://localhost:8443/s3-encrypted/$dataset/$file?startCoordinate=0&endCoordinate=1000" --output $partReencryptedFile file_size=$(stat -c %s $partReencryptedFile) # Get the size of the file part_expected_size=65688 @@ -72,7 +65,8 @@ if [ "$file_size" -ne "$part_expected_size" ]; then exit 1 fi -if ! crypt4gh decrypt --sk client.sec.pem < $partReencryptedFile > part1.bam; then +crypt4gh decrypt -s client.sec.pem -f $partReencryptedFile +if [ ! -f "${partReencryptedFile%.c4gh}" ] ; then echo "Re-encrypted partial file could not be decrypted" exit 1 fi @@ -106,3 +100,5 @@ resp=$(curl --cacert certs/ca.pem -H "Authorization: Bearer $token" -H "Client-P if [ "$resp" -ne 500 ]; then echo "Incorrect response with missing public key, expected 500 got $resp" fi + +echo "OK" \ No newline at end of file diff --git a/sda-download/.github/integration/tests/s3notls/52_check_endpoint.sh b/sda-download/.github/integration/tests/s3notls/52_check_endpoint.sh index 67bdff98e..b4281921c 100644 --- a/sda-download/.github/integration/tests/s3notls/52_check_endpoint.sh +++ b/sda-download/.github/integration/tests/s3notls/52_check_endpoint.sh @@ -41,11 +41,11 @@ echo "got correct response when POST method used" # ------------------ # Test good token -token=$(curl "http://localhost:8000/tokens" | jq -r '.[0]') +token=$(curl -s "http://localhost:8000/tokens" | jq -r '.[0]') ## Test datasets endpoint -check_dataset=$(curl -H "Authorization: Bearer $token" http://localhost:8080/metadata/datasets | jq -r '.[0]') +check_dataset=$(curl -s -H "Authorization: Bearer $token" http://localhost:8080/metadata/datasets | jq -r '.[0]') if [ "$check_dataset" != "https://doi.example/ty009.sfrrss/600.45asasga" ]; then echo "dataset https://doi.example/ty009.sfrrss/600.45asasga not found" @@ -57,7 +57,7 @@ echo "expected dataset found" ## Test datasets/files endpoint -check_files=$(curl -H "Authorization: Bearer $token" "http://localhost:8080/metadata/datasets/https://doi.example/ty009.sfrrss/600.45asasga/files" | jq -r '.[0].fileId') +check_files=$(curl -s -H "Authorization: Bearer $token" "http://localhost:8080/metadata/datasets/https://doi.example/ty009.sfrrss/600.45asasga/files" | jq -r '.[0].fileId') if [ "$check_files" != "urn:neic:001-002" ]; then echo "file with id urn:neic:001-002 not found" @@ -73,9 +73,9 @@ echo "expected file found" C4GH_PASSPHRASE=$(grep -F passphrase config.yaml | sed -e 's/.* //' -e 's/"//g') export C4GH_PASSPHRASE -crypt4gh decrypt --sk c4gh.sec.pem < dummy_data.c4gh > old-file.txt +crypt4gh decrypt -s c4gh.sec.pem -f dummy_data.c4gh && mv dummy_data old-file.txt -curl -H "Authorization: Bearer $token" "http://localhost:8080/files/urn:neic:001-002" --output test-download.txt +curl -s -H "Authorization: Bearer $token" "http://localhost:8080/files/urn:neic:001-002" --output test-download.txt cmp --silent old-file.txt test-download.txt @@ -86,7 +86,7 @@ else echo "Files are different" fi -curl -H "Authorization: Bearer $token" "http://localhost:8080/files/urn:neic:001-002?startCoordinate=0&endCoordinate=2" --output test-part.txt +curl -s -H "Authorization: Bearer $token" "http://localhost:8080/files/urn:neic:001-002?startCoordinate=0&endCoordinate=2" --output test-part.txt dd if=old-file.txt ibs=1 skip=0 count=2 > old-part.txt @@ -99,7 +99,7 @@ else exit 1 fi -curl -H "Authorization: Bearer $token" "http://localhost:8080/files/urn:neic:001-002?startCoordinate=7&endCoordinate=14" --output test-part2.txt +curl -s -H "Authorization: Bearer $token" "http://localhost:8080/files/urn:neic:001-002?startCoordinate=7&endCoordinate=14" --output test-part2.txt dd if=old-file.txt ibs=1 skip=7 count=7 > old-part2.txt @@ -115,7 +115,7 @@ fi # ------------------ # Test get visas failed -token=$(curl "http://localhost:8000/tokens" | jq -r '.[1]') +token=$(curl -s "http://localhost:8000/tokens" | jq -r '.[1]') ## Test datasets endpoint @@ -133,11 +133,11 @@ echo "got correct response when token has no permissions" # Test token with untrusted sources # for this test we don't attach a list of trusted sources -token=$(curl "http://localhost:8000/tokens" | jq -r '.[2]') +token=$(curl -s "http://localhost:8000/tokens" | jq -r '.[2]') ## Test datasets endpoint -check_dataset=$(curl -H "Authorization: Bearer $token" http://localhost:8080/metadata/datasets | jq -r '.[0]') +check_dataset=$(curl -s -H "Authorization: Bearer $token" http://localhost:8080/metadata/datasets | jq -r '.[0]') if [ "$check_dataset" != "https://doi.example/ty009.sfrrss/600.45asasga" ]; then echo "dataset https://doi.example/ty009.sfrrss/600.45asasga not found" From 49dea060edf4673fb90e9e46703cc49e65a51162 Mon Sep 17 00:00:00 2001 From: Alex Aperis Date: Sat, 7 Dec 2024 09:16:22 +0100 Subject: [PATCH 03/22] load internal private c4gh key from file - internal c4gh key is optional - base64 pubkey is created from loaded file - modify tests --- sda-download/internal/config/config.go | 38 +++++++++++++-------- sda-download/internal/config/config_test.go | 8 +++-- 2 files changed, 29 insertions(+), 17 deletions(-) diff --git a/sda-download/internal/config/config.go b/sda-download/internal/config/config.go index c91b10d30..fedfb3d76 100644 --- a/sda-download/internal/config/config.go +++ b/sda-download/internal/config/config.go @@ -380,9 +380,17 @@ func (c *Map) appConfig() error { } var err error - c.App.Crypt4GHPrivateKey, c.App.Crypt4GHPublicKeyB64, err = GenerateC4GHKey() - if err != nil { - return err + if viper.IsSet("app.c4ghPrivateKeyPath") { + + if !viper.IsSet("app.c4ghPassphrase") { + return errors.New("app.c4ghPassphrase is not set") + } + + c.App.Crypt4GHPrivateKey, c.App.Crypt4GHPublicKeyB64, err = GetC4GHKeys() + if err != nil { + return err + } + log.Infoln("Internal c4gh key-pair loaded") } if !slices.Contains(availableMiddlewares, c.App.Middleware) { @@ -481,25 +489,27 @@ func constructWhitelist(obj []TrustedISS) *jwk.MapWhitelist { return wl } -// GeneerateC4GHKey generates a keypair and returns the private key as a byte -// array and the public key as a base64 encoded string -func GenerateC4GHKey() ([32]byte, string, error) { - log.Info("creating temporary crypt4gh key") - - public, private, err := keys.GenerateKeyPair() +// GetC4GHKey reads and decrypts and returns the c4gh key +func GetC4GHKeys() ([32]byte, string, error) { + keyPath := viper.GetString("app.c4ghPrivateKeyPath") + passphrase := viper.GetString("app.c4ghPassphrase") + // Make sure the key path and passphrase is valid + keyFile, err := os.Open(keyPath) if err != nil { - log.Errorf("Error when generating keys: %v", err) - return [32]byte{}, "", err } + private, err := keys.ReadPrivateKey(keyFile, []byte(passphrase)) + if err != nil { + return [32]byte{}, "", fmt.Errorf("error when reading private key: %v", err) + } + keyFile.Close() + public := keys.DerivePublicKey(private) pem := bytes.Buffer{} err = keys.WriteCrypt4GHX25519PublicKey(&pem, public) if err != nil { - log.Errorf("Error when converting public key to PEM format: %v", err) - - return [32]byte{}, "", err + return [32]byte{}, "", fmt.Errorf("error when converting public key to PEM format: %v", err) } b64 := base64.StdEncoding.EncodeToString(pem.Bytes()) diff --git a/sda-download/internal/config/config_test.go b/sda-download/internal/config/config_test.go index 4168d54bc..cfeb7bd03 100644 --- a/sda-download/internal/config/config_test.go +++ b/sda-download/internal/config/config_test.go @@ -86,6 +86,8 @@ func (suite *TestSuite) TestAppConfig() { viper.Set("app.serverkey", "test") viper.Set("log.logLevel", "debug") viper.Set("db.sslmode", "disable") + viper.Set("app.c4ghPrivateKeyPath", "../../dev_utils/c4gh.sec.pem") + viper.Set("app.c4ghPassphrase", "oaagCP1YgAZeEyl2eJAkHv9lkcWXWFgm") c = &Map{} err = c.appConfig() @@ -94,11 +96,11 @@ func (suite *TestSuite) TestAppConfig() { assert.Equal(suite.T(), 1234, c.App.Port) assert.Equal(suite.T(), "test", c.App.ServerCert) assert.Equal(suite.T(), "test", c.App.ServerKey) - assert.NotNil(suite.T(), c.App.Crypt4GHPrivateKey) - assert.NotNil(suite.T(), c.App.Crypt4GHPublicKeyB64) + assert.NotEmpty(suite.T(), c.App.Crypt4GHPrivateKey) + assert.NotEmpty(suite.T(), c.App.Crypt4GHPublicKeyB64) assert.Equal(suite.T(), false, c.App.ServeUnencryptedData) - // Check the key that was generated + // Check the private key that was loaded by checking the derived public key publicKey, err := base64.StdEncoding.DecodeString(c.App.Crypt4GHPublicKeyB64) assert.Nilf(suite.T(), err, "Incorrect public c4gh key generated (error in base64 encoding)") _, err = keys.ReadPublicKey(bytes.NewReader(publicKey)) From 693423ad42f01625c1cf10de3b4e5e5ad538857d Mon Sep 17 00:00:00 2001 From: Alex Aperis Date: Sat, 7 Dec 2024 18:54:20 +0100 Subject: [PATCH 04/22] serve unencrypted files if c4gh key is provided otherwise always serve encrypted files through /s3 - remove /s3-encrypted endpoint - log warning if serving unencrypted --- sda-download/api/api.go | 2 -- sda-download/api/s3/s3.go | 3 ++- sda-download/api/sda/sda.go | 2 +- sda-download/cmd/main.go | 6 +++++- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/sda-download/api/api.go b/sda-download/api/api.go index a770f5d0a..4cbcb6771 100644 --- a/sda-download/api/api.go +++ b/sda-download/api/api.go @@ -45,8 +45,6 @@ func Setup() *http.Server { router.GET("/files/:fileid", SelectedMiddleware(), sda.Download) router.GET("/s3/*path", SelectedMiddleware(), s3.Download) router.HEAD("/s3/*path", SelectedMiddleware(), s3.Download) - router.HEAD("/s3-encrypted/*path", SelectedMiddleware(), s3.Download) - router.GET("/s3-encrypted/*path", SelectedMiddleware(), s3.Download) router.GET("/health", healthResponse) router.HEAD("/", healthResponse) diff --git a/sda-download/api/s3/s3.go b/sda-download/api/s3/s3.go index 00042cb88..5221068b7 100644 --- a/sda-download/api/s3/s3.go +++ b/sda-download/api/s3/s3.go @@ -10,6 +10,7 @@ import ( "github.com/gin-gonic/gin" "github.com/neicnordic/sda-download/api/middleware" "github.com/neicnordic/sda-download/api/sda" + "github.com/neicnordic/sda-download/internal/config" "github.com/neicnordic/sda-download/internal/database" log "github.com/sirupsen/logrus" ) @@ -329,7 +330,7 @@ func Download(c *gin.Context) { ListBuckets(c) case c.Param("filename") != "": - if strings.HasPrefix(c.Request.URL.Path, "/s3-encrypted") { + if config.Config.App.Crypt4GHPublicKeyB64 == "" { GetEcnryptedObject(c) } else { GetObject(c) diff --git a/sda-download/api/sda/sda.go b/sda-download/api/sda/sda.go index 0ab1e15be..85dbcbdcd 100644 --- a/sda-download/api/sda/sda.go +++ b/sda-download/api/sda/sda.go @@ -207,7 +207,7 @@ func Files(c *gin.Context) { // Download serves file contents as bytes func Download(c *gin.Context) { - if c.Param("type") != "encrypted" && !config.Config.App.ServeUnencryptedData { + if c.Param("type") != "encrypted" && config.Config.App.Crypt4GHPublicKeyB64 == "" { c.String(http.StatusBadRequest, "downloading unencrypted data is not supported") return diff --git a/sda-download/cmd/main.go b/sda-download/cmd/main.go index 46c21aa24..9d3c73a70 100644 --- a/sda-download/cmd/main.go +++ b/sda-download/cmd/main.go @@ -49,7 +49,6 @@ func init() { // Initialise OIDC configuration details, err := auth.GetOIDCDetails(conf.OIDC.ConfigurationURL) - log.Info("retrieving OIDC configuration") if err != nil { log.Panicf("oidc init failed, reason: %v", err) } @@ -76,6 +75,11 @@ func main() { // Start the server log.Info("(5/5) Starting web server") + + if config.Config.App.Crypt4GHPublicKeyB64 != "" { + log.Warningln("Serving unencrypted data") + } + if config.Config.App.ServerCert != "" && config.Config.App.ServerKey != "" { log.Infof("Web server is ready to receive connections at https://%s:%d", config.Config.App.Host, config.Config.App.Port) log.Fatal(srv.ListenAndServeTLS(config.Config.App.ServerCert, config.Config.App.ServerKey)) From 442f45b1885e7dbae7c6d97152a32d5eee8c62ca Mon Sep 17 00:00:00 2001 From: Alex Aperis Date: Sun, 8 Dec 2024 20:32:12 +0100 Subject: [PATCH 05/22] add more unitests --- sda-download/api/sda/sda_test.go | 93 +++++++++++++++------ sda-download/internal/config/config_test.go | 11 +++ 2 files changed, 79 insertions(+), 25 deletions(-) diff --git a/sda-download/api/sda/sda_test.go b/sda-download/api/sda/sda_test.go index f30ca483a..6e80078cb 100644 --- a/sda-download/api/sda/sda_test.go +++ b/sda-download/api/sda/sda_test.go @@ -17,6 +17,7 @@ import ( "testing" "github.com/gin-gonic/gin" + "github.com/spf13/viper" "github.com/stretchr/testify/assert" "golang.org/x/crypto/chacha20poly1305" "google.golang.org/protobuf/proto" @@ -338,9 +339,8 @@ func TestFiles_Success(t *testing.T) { func TestDownload_Fail_UnencryptedDownloadNotAllowed(t *testing.T) { // Save original to-be-mocked config - originalServeUnencryptedData := config.Config.App.ServeUnencryptedData - - config.Config.App.ServeUnencryptedData = false + originalServeUnencryptedDataTrigger := config.Config.App.Crypt4GHPublicKeyB64 + config.Config.App.Crypt4GHPublicKeyB64 = "" // Mock request and response holders w := httptest.NewRecorder() @@ -368,7 +368,7 @@ func TestDownload_Fail_UnencryptedDownloadNotAllowed(t *testing.T) { // encrypted w = httptest.NewRecorder() c, _ = gin.CreateTestContext(w) - c.Request = &http.Request{Method: "GET", URL: &url.URL{Path: "/s3/somepath", RawQuery: "filename=somepath"}} + c.Request = &http.Request{Method: "GET", URL: &url.URL{Path: "/mocks3/somepath", RawQuery: "filename=somepath"}} Download(c) response = w.Result() @@ -388,20 +388,26 @@ func TestDownload_Fail_UnencryptedDownloadNotAllowed(t *testing.T) { } // Return mock config to originals - config.Config.App.ServeUnencryptedData = originalServeUnencryptedData + config.Config.App.Crypt4GHPublicKeyB64 = originalServeUnencryptedDataTrigger } func TestDownload_Fail_FileNotFound(t *testing.T) { - // Save original to-be-mocked functions + // Save original to-be-mocked functions and config originalCheckFilePermission := database.CheckFilePermission - originalServeUnencryptedData := config.Config.App.ServeUnencryptedData + originalServeUnencryptedDataTrigger := config.Config.App.Crypt4GHPublicKeyB64 + originalC4ghPrivateKeyFilepath := config.Config.App.Crypt4GHPrivateKey // Substitute mock functions database.CheckFilePermission = func(_ string) (string, error) { return "", errors.New("file not found") } - config.Config.App.ServeUnencryptedData = true + + viper.Set("app.c4ghPrivateKeyPath", "../../dev_utils/c4gh.sec.pem") + viper.Set("app.c4ghPassphrase", "oaagCP1YgAZeEyl2eJAkHv9lkcWXWFgm") + var err error + config.Config.App.Crypt4GHPrivateKey, config.Config.App.Crypt4GHPublicKeyB64, err = config.GetC4GHKeys() + assert.NoError(t, err, "Could not load c4gh keys") // Mock request and response holders w := httptest.NewRecorder() @@ -427,7 +433,10 @@ func TestDownload_Fail_FileNotFound(t *testing.T) { // Return mock functions to originals database.CheckFilePermission = originalCheckFilePermission - config.Config.App.ServeUnencryptedData = originalServeUnencryptedData + config.Config.App.Crypt4GHPublicKeyB64 = originalServeUnencryptedDataTrigger + config.Config.App.Crypt4GHPrivateKey = originalC4ghPrivateKeyFilepath + viper.Set("app.c4ghPrivateKeyPath", "") + viper.Set("app.c4ghPassphrase", "") } @@ -436,7 +445,8 @@ func TestDownload_Fail_NoPermissions(t *testing.T) { // Save original to-be-mocked functions originalCheckFilePermission := database.CheckFilePermission originalGetCacheFromContext := middleware.GetCacheFromContext - originalServeUnencryptedData := config.Config.App.ServeUnencryptedData + originalServeUnencryptedDataTrigger := config.Config.App.Crypt4GHPublicKeyB64 + originalC4ghPrivateKeyFilepath := config.Config.App.Crypt4GHPrivateKey // Substitute mock functions database.CheckFilePermission = func(_ string) (string, error) { @@ -446,7 +456,12 @@ func TestDownload_Fail_NoPermissions(t *testing.T) { middleware.GetCacheFromContext = func(_ *gin.Context) session.Cache { return session.Cache{} } - config.Config.App.ServeUnencryptedData = true + + viper.Set("app.c4ghPrivateKeyPath", "../../dev_utils/c4gh.sec.pem") + viper.Set("app.c4ghPassphrase", "oaagCP1YgAZeEyl2eJAkHv9lkcWXWFgm") + var err error + config.Config.App.Crypt4GHPrivateKey, config.Config.App.Crypt4GHPublicKeyB64, err = config.GetC4GHKeys() + assert.NoError(t, err, "Could not load c4gh keys") // Mock request and response holders w := httptest.NewRecorder() @@ -473,7 +488,10 @@ func TestDownload_Fail_NoPermissions(t *testing.T) { // Return mock functions to originals database.CheckFilePermission = originalCheckFilePermission middleware.GetCacheFromContext = originalGetCacheFromContext - config.Config.App.ServeUnencryptedData = originalServeUnencryptedData + config.Config.App.Crypt4GHPublicKeyB64 = originalServeUnencryptedDataTrigger + config.Config.App.Crypt4GHPrivateKey = originalC4ghPrivateKeyFilepath + viper.Set("app.c4ghPrivateKeyPath", "") + viper.Set("app.c4ghPassphrase", "") } @@ -483,7 +501,8 @@ func TestDownload_Fail_GetFile(t *testing.T) { originalCheckFilePermission := database.CheckFilePermission originalGetCacheFromContext := middleware.GetCacheFromContext originalGetFile := database.GetFile - originalServeUnencryptedData := config.Config.App.ServeUnencryptedData + originalServeUnencryptedDataTrigger := config.Config.App.Crypt4GHPublicKeyB64 + originalC4ghPrivateKeyFilepath := config.Config.App.Crypt4GHPrivateKey // Substitute mock functions database.CheckFilePermission = func(_ string) (string, error) { @@ -497,7 +516,12 @@ func TestDownload_Fail_GetFile(t *testing.T) { database.GetFile = func(_ string) (*database.FileDownload, error) { return nil, errors.New("database error") } - config.Config.App.ServeUnencryptedData = true + + viper.Set("app.c4ghPrivateKeyPath", "../../dev_utils/c4gh.sec.pem") + viper.Set("app.c4ghPassphrase", "oaagCP1YgAZeEyl2eJAkHv9lkcWXWFgm") + var err error + config.Config.App.Crypt4GHPrivateKey, config.Config.App.Crypt4GHPublicKeyB64, err = config.GetC4GHKeys() + assert.NoError(t, err, "Could not load c4gh keys") // Mock request and response holders w := httptest.NewRecorder() @@ -525,7 +549,10 @@ func TestDownload_Fail_GetFile(t *testing.T) { database.CheckFilePermission = originalCheckFilePermission middleware.GetCacheFromContext = originalGetCacheFromContext database.GetFile = originalGetFile - config.Config.App.ServeUnencryptedData = originalServeUnencryptedData + config.Config.App.Crypt4GHPublicKeyB64 = originalServeUnencryptedDataTrigger + config.Config.App.Crypt4GHPrivateKey = originalC4ghPrivateKeyFilepath + viper.Set("app.c4ghPrivateKeyPath", "") + viper.Set("app.c4ghPassphrase", "") } @@ -535,7 +562,8 @@ func TestDownload_Fail_OpenFile(t *testing.T) { originalCheckFilePermission := database.CheckFilePermission originalGetCacheFromContext := middleware.GetCacheFromContext originalGetFile := database.GetFile - originalServeUnencryptedData := config.Config.App.ServeUnencryptedData + originalServeUnencryptedDataTrigger := config.Config.App.Crypt4GHPublicKeyB64 + originalC4ghPrivateKeyFilepath := config.Config.App.Crypt4GHPrivateKey Backend, _ = storage.NewBackend(config.Config.Archive) // Substitute mock functions @@ -556,7 +584,12 @@ func TestDownload_Fail_OpenFile(t *testing.T) { return fileDetails, nil } - config.Config.App.ServeUnencryptedData = true + + viper.Set("app.c4ghPrivateKeyPath", "../../dev_utils/c4gh.sec.pem") + viper.Set("app.c4ghPassphrase", "oaagCP1YgAZeEyl2eJAkHv9lkcWXWFgm") + var err error + config.Config.App.Crypt4GHPrivateKey, config.Config.App.Crypt4GHPublicKeyB64, err = config.GetC4GHKeys() + assert.NoError(t, err, "Could not load c4gh keys") // Mock request and response holders and initialize headers w := httptest.NewRecorder() @@ -589,7 +622,10 @@ func TestDownload_Fail_OpenFile(t *testing.T) { database.CheckFilePermission = originalCheckFilePermission middleware.GetCacheFromContext = originalGetCacheFromContext database.GetFile = originalGetFile - config.Config.App.ServeUnencryptedData = originalServeUnencryptedData + config.Config.App.Crypt4GHPublicKeyB64 = originalServeUnencryptedDataTrigger + config.Config.App.Crypt4GHPrivateKey = originalC4ghPrivateKeyFilepath + viper.Set("app.c4ghPrivateKeyPath", "") + viper.Set("app.c4ghPassphrase", "") } func Test_CalucalateCoords(t *testing.T) { @@ -693,7 +729,8 @@ func TestDownload_Whole_Range_Encrypted(t *testing.T) { originalCheckFilePermission := database.CheckFilePermission originalGetCacheFromContext := middleware.GetCacheFromContext originalGetFile := database.GetFile - originalServeUnencryptedData := config.Config.App.ServeUnencryptedData + originalServeUnencryptedDataTrigger := config.Config.App.Crypt4GHPublicKeyB64 + originalC4ghPrivateKeyFilepath := config.Config.App.Crypt4GHPrivateKey archive := config.Config.Archive archive.Posix.Location = "." Backend, _ = storage.NewBackend(archive) @@ -737,10 +774,13 @@ func TestDownload_Whole_Range_Encrypted(t *testing.T) { config.Config.Reencrypt.ClientKey = keyfile.Name() config.Config.Reencrypt.Timeout = 10 - config.Config.App.ServeUnencryptedData = true + viper.Set("app.c4ghPrivateKeyPath", "../../dev_utils/c4gh.sec.pem") + viper.Set("app.c4ghPassphrase", "oaagCP1YgAZeEyl2eJAkHv9lkcWXWFgm") + config.Config.App.Crypt4GHPrivateKey, config.Config.App.Crypt4GHPublicKeyB64, err = config.GetC4GHKeys() + assert.NoError(t, err, "Could not load c4gh keys") // Set up crypt4gh keys - config.Config.App.Crypt4GHPrivateKey, config.Config.App.Crypt4GHPublicKeyB64, err = config.GenerateC4GHKey() + config.Config.App.Crypt4GHPrivateKey, config.Config.App.Crypt4GHPublicKeyB64, err = config.GetC4GHKeys() assert.NoError(t, err, "Could not generate temporary keys") // Make a file to hold the archive file @@ -833,7 +873,7 @@ func TestDownload_Whole_Range_Encrypted(t *testing.T) { // encrypted w = httptest.NewRecorder() c, _ = gin.CreateTestContext(w) - c.Request = &http.Request{Method: "GET", URL: &url.URL{Path: "/s3-encrypted/somepath", RawQuery: "filename=somepath"}} + c.Request = &http.Request{Method: "GET", URL: &url.URL{Path: "/mocks3/somepath", RawQuery: "filename=somepath"}} c.Request.Header = http.Header{"Client-Public-Key": []string{config.Config.App.Crypt4GHPublicKeyB64}, "Range": []string{"bytes=0-10"}} @@ -852,7 +892,7 @@ func TestDownload_Whole_Range_Encrypted(t *testing.T) { config.Config.App.ServeUnencryptedData = false w = httptest.NewRecorder() c, _ = gin.CreateTestContext(w) - c.Request = &http.Request{Method: "GET", URL: &url.URL{Path: "/s3-encrypted/somepath", RawQuery: "filename=somepath"}} + c.Request = &http.Request{Method: "GET", URL: &url.URL{Path: "/mocks3/somepath", RawQuery: "filename=somepath"}} c.Request.Header = http.Header{"Client-Public-Key": []string{config.Config.App.Crypt4GHPublicKeyB64}, "Range": []string{"bytes=0-10"}} @@ -871,7 +911,7 @@ func TestDownload_Whole_Range_Encrypted(t *testing.T) { // Test encrypted download without passing the key, should fail w = httptest.NewRecorder() c, _ = gin.CreateTestContext(w) - c.Request = &http.Request{Method: "GET", URL: &url.URL{Path: "/s3-encrypted/somepath", RawQuery: "filename=somepath"}} + c.Request = &http.Request{Method: "GET", URL: &url.URL{Path: "/mocks3/somepath", RawQuery: "filename=somepath"}} c.Params = make(gin.Params, 1) c.Params[0] = gin.Param{Key: "type", Value: "encrypted"} @@ -887,5 +927,8 @@ func TestDownload_Whole_Range_Encrypted(t *testing.T) { database.CheckFilePermission = originalCheckFilePermission middleware.GetCacheFromContext = originalGetCacheFromContext database.GetFile = originalGetFile - config.Config.App.ServeUnencryptedData = originalServeUnencryptedData + config.Config.App.Crypt4GHPublicKeyB64 = originalServeUnencryptedDataTrigger + config.Config.App.Crypt4GHPrivateKey = originalC4ghPrivateKeyFilepath + viper.Set("app.c4ghPrivateKeyPath", "") + viper.Set("app.c4ghPassphrase", "") } diff --git a/sda-download/internal/config/config_test.go b/sda-download/internal/config/config_test.go index cfeb7bd03..45969f341 100644 --- a/sda-download/internal/config/config_test.go +++ b/sda-download/internal/config/config_test.go @@ -105,6 +105,17 @@ func (suite *TestSuite) TestAppConfig() { assert.Nilf(suite.T(), err, "Incorrect public c4gh key generated (error in base64 encoding)") _, err = keys.ReadPublicKey(bytes.NewReader(publicKey)) assert.Nilf(suite.T(), err, "Incorrect public c4gh key generated (bad key)") + + // Check false c4gh key + viper.Set("app.c4ghPrivateKeyPath", "some/nonexistent.key") + err = c.appConfig() + assert.ErrorContains(suite.T(), err, "no such file or directory") + + // Check false c4gh key + viper.Set("app.c4ghPrivateKeyPath", "../../dev_utils/c4gh.sec.pem") + viper.Set("app.c4ghPassphrase", "blablabla") + err = c.appConfig() + assert.ErrorContains(suite.T(), err, "chacha20poly1305: message authentication failed") } func (suite *TestSuite) TestArchiveConfig() { From 6bd7bb7dfc2e23d9a90215c45d88184978f3173a Mon Sep 17 00:00:00 2001 From: Alex Aperis Date: Sun, 8 Dec 2024 20:42:48 +0100 Subject: [PATCH 06/22] remove serveUnencryptedData code --- sda-download/api/sda/sda_test.go | 2 -- sda-download/internal/config/config.go | 4 ---- sda-download/internal/config/config_test.go | 2 -- 3 files changed, 8 deletions(-) diff --git a/sda-download/api/sda/sda_test.go b/sda-download/api/sda/sda_test.go index 6e80078cb..89bfc0d2c 100644 --- a/sda-download/api/sda/sda_test.go +++ b/sda-download/api/sda/sda_test.go @@ -889,7 +889,6 @@ func TestDownload_Whole_Range_Encrypted(t *testing.T) { assert.Equal(t, []byte("crypt4gh"), body[:8], "Unexpected body from download") // Test encrypted download, should work even when AllowedUnencryptedDownload is false - config.Config.App.ServeUnencryptedData = false w = httptest.NewRecorder() c, _ = gin.CreateTestContext(w) c.Request = &http.Request{Method: "GET", URL: &url.URL{Path: "/mocks3/somepath", RawQuery: "filename=somepath"}} @@ -906,7 +905,6 @@ func TestDownload_Whole_Range_Encrypted(t *testing.T) { assert.Equal(t, 200, response.StatusCode, "Unexpected status code from download") assert.Equal(t, []byte("crypt4gh"), body[:8], "Unexpected body from download") - config.Config.App.ServeUnencryptedData = true // Test encrypted download without passing the key, should fail w = httptest.NewRecorder() diff --git a/sda-download/internal/config/config.go b/sda-download/internal/config/config.go index fedfb3d76..160f263a7 100644 --- a/sda-download/internal/config/config.go +++ b/sda-download/internal/config/config.go @@ -63,8 +63,6 @@ type AppConfig struct { // Selected middleware for authentication and authorizaton // Optional. Default value is "default" for TokenMiddleware Middleware string - - ServeUnencryptedData bool } type SessionConfig struct { @@ -241,7 +239,6 @@ func NewConfig() (*Map, error) { func (c *Map) applyDefaults() { viper.SetDefault("app.host", "0.0.0.0") viper.SetDefault("app.port", 8080) - viper.SetDefault("app.serveUnencryptedData", false) viper.SetDefault("app.middleware", "default") viper.SetDefault("session.expiration", -1) viper.SetDefault("session.secure", true) @@ -371,7 +368,6 @@ func (c *Map) appConfig() error { c.App.ServerCert = viper.GetString("app.servercert") c.App.ServerKey = viper.GetString("app.serverkey") c.App.Middleware = viper.GetString("app.middleware") - c.App.ServeUnencryptedData = viper.GetBool("app.serveUnencryptedData") if c.App.Port != 443 && c.App.Port != 8080 { c.App.Port = viper.GetInt("app.port") diff --git a/sda-download/internal/config/config_test.go b/sda-download/internal/config/config_test.go index 45969f341..82a95e4a0 100644 --- a/sda-download/internal/config/config_test.go +++ b/sda-download/internal/config/config_test.go @@ -81,7 +81,6 @@ func (suite *TestSuite) TestAppConfig() { viper.Set("app.host", "test") viper.Set("app.port", 1234) - viper.Set("app.serveUnencryptedData", false) viper.Set("app.servercert", "test") viper.Set("app.serverkey", "test") viper.Set("log.logLevel", "debug") @@ -98,7 +97,6 @@ func (suite *TestSuite) TestAppConfig() { assert.Equal(suite.T(), "test", c.App.ServerKey) assert.NotEmpty(suite.T(), c.App.Crypt4GHPrivateKey) assert.NotEmpty(suite.T(), c.App.Crypt4GHPublicKeyB64) - assert.Equal(suite.T(), false, c.App.ServeUnencryptedData) // Check the private key that was loaded by checking the derived public key publicKey, err := base64.StdEncoding.DecodeString(c.App.Crypt4GHPublicKeyB64) From 9d8f0999737a0448d8d71066483eaafc98236747 Mon Sep 17 00:00:00 2001 From: Alex Aperis Date: Mon, 9 Dec 2024 15:11:48 +0100 Subject: [PATCH 07/22] update compose and config - remove serveUnencryptedData configuration - add unencrypted download instance and config --- sda-download/dev_utils/compose-no-tls.yml | 28 +++++++++++++++++++++ sda-download/dev_utils/compose.yml | 30 +++++++++++++++++++++++ sda-download/dev_utils/config-notls.yaml | 3 --- sda-download/dev_utils/config.yaml | 1 - 4 files changed, 58 insertions(+), 4 deletions(-) diff --git a/sda-download/dev_utils/compose-no-tls.yml b/sda-download/dev_utils/compose-no-tls.yml index d94095a86..ac0434f2d 100644 --- a/sda-download/dev_utils/compose-no-tls.yml +++ b/sda-download/dev_utils/compose-no-tls.yml @@ -74,6 +74,34 @@ services: - "8080:8080" restart: always + download-unencrypted: + command: sda-download + container_name: download-unencrypted + depends_on: + db: + condition: service_healthy + s3: + condition: service_healthy + mockauth: + condition: service_started + environment: + - ARCHIVE_URL=http://s3 + - ARCHIVE_TYPE=s3 + - DB_HOST=db + - APP_C4GHPRIVATEKEYPATH=/dev_utils/c4gh.sec.pem + - APP_C4GHPASSPHRASE=oaagCP1YgAZeEyl2eJAkHv9lkcWXWFgm + image: neicnordic/sda-download:latest + build: + context: .. + volumes: + - ./config-notls.yaml:/config.yaml + - ./:/dev_utils/ + - ./archive_data/4293c9a7-dc50-46db-b79a-27ddc0dad1c6:/tmp/4293c9a7-dc50-46db-b79a-27ddc0dad1c6 + mem_limit: 256m + ports: + - "9080:8080" + restart: always + mockauth: command: - /bin/sh diff --git a/sda-download/dev_utils/compose.yml b/sda-download/dev_utils/compose.yml index a3685c878..f9f7ad954 100644 --- a/sda-download/dev_utils/compose.yml +++ b/sda-download/dev_utils/compose.yml @@ -105,6 +105,36 @@ services: - "8443:8443" restart: always + download-unencrypted: + command: sda-download + container_name: download-unencrypted + depends_on: + certfixer: + condition: service_completed_successfully + db: + condition: service_healthy + s3: + condition: service_healthy + mockauth: + condition: service_started + download: + condition: service_started + env_file: ./env.download + environment: + - APP_C4GHPRIVATEKEYPATH=/dev_utils/c4gh.sec.pem + - APP_C4GHPASSPHRASE=oaagCP1YgAZeEyl2eJAkHv9lkcWXWFgm + image: neicnordic/sda-download:latest + volumes: + - ./config.yaml:/config.yaml + - ./:/dev_utils/ + - ./iss.json:/iss.json + - certs:/dev_utils/certs + - ./archive_data/4293c9a7-dc50-46db-b79a-27ddc0dad1c6:/tmp/4293c9a7-dc50-46db-b79a-27ddc0dad1c6 + mem_limit: 256m + ports: + - "9443:8443" + restart: always + reencrypt: image: ghcr.io/neicnordic/sensitive-data-archive:latest build: ../../sda diff --git a/sda-download/dev_utils/config-notls.yaml b/sda-download/dev_utils/config-notls.yaml index fc6b5f126..13b90a638 100644 --- a/sda-download/dev_utils/config-notls.yaml +++ b/sda-download/dev_utils/config-notls.yaml @@ -1,6 +1,3 @@ -app: - serveUnencryptedData: true - log: level: "debug" format: "json" diff --git a/sda-download/dev_utils/config.yaml b/sda-download/dev_utils/config.yaml index 1739c0d8c..b60f62214 100644 --- a/sda-download/dev_utils/config.yaml +++ b/sda-download/dev_utils/config.yaml @@ -4,7 +4,6 @@ app: serverkey: "./dev_utils/certs/download-key.pem" port: "8443" middleware: "default" - serveUnencryptedData: true log: level: "debug" From 8b0a210d49759c0a81dcc27d07b4439d16f00071 Mon Sep 17 00:00:00 2001 From: Alex Aperis Date: Mon, 9 Dec 2024 15:13:52 +0100 Subject: [PATCH 08/22] update integration tests for testing (un)encrypted download cases --- .../setup/s3notls/99_s3_storage_setup.sh | 2 -- .../tests/common/50_check_endpoint.sh | 24 +++++++++++++++---- .../tests/common/60_check_s3_endpoint.sh | 8 ++++--- .../tests/common/70_check_download.sh | 7 ++++-- .../tests/common/80_check_reencrypt.sh | 18 +++++++------- 5 files changed, 38 insertions(+), 21 deletions(-) diff --git a/sda-download/.github/integration/setup/s3notls/99_s3_storage_setup.sh b/sda-download/.github/integration/setup/s3notls/99_s3_storage_setup.sh index c24e36199..575cf9618 100644 --- a/sda-download/.github/integration/setup/s3notls/99_s3_storage_setup.sh +++ b/sda-download/.github/integration/setup/s3notls/99_s3_storage_setup.sh @@ -1,8 +1,6 @@ #!/bin/bash set -e -sudo apt install -y s3cmd - cd dev_utils || exit 1 # Make buckets if they don't exist already diff --git a/sda-download/.github/integration/tests/common/50_check_endpoint.sh b/sda-download/.github/integration/tests/common/50_check_endpoint.sh index 8fa2c20c0..ad05f093d 100755 --- a/sda-download/.github/integration/tests/common/50_check_endpoint.sh +++ b/sda-download/.github/integration/tests/common/50_check_endpoint.sh @@ -90,7 +90,7 @@ export C4GH_PASSPHRASE crypt4gh decrypt -s c4gh.sec.pem -f dummy_data.c4gh && mv dummy_data old-file.txt -curl -s --cacert certs/ca.pem -H "Authorization: Bearer $token" "https://localhost:8443/files/urn:neic:001-002" --output test-download.txt +curl -s --cacert certs/ca.pem -H "Authorization: Bearer $token" "https://localhost:9443/files/urn:neic:001-002" --output test-download.txt cmp --silent old-file.txt test-download.txt status=$? @@ -101,7 +101,7 @@ else exit 1 fi -curl -s --cacert certs/ca.pem -H "Authorization: Bearer $token" "https://localhost:8443/files/urn:neic:001-002?startCoordinate=0&endCoordinate=2" --output test-part.txt +curl -s --cacert certs/ca.pem -H "Authorization: Bearer $token" "https://localhost:9443/files/urn:neic:001-002?startCoordinate=0&endCoordinate=2" --output test-part.txt dd if=old-file.txt ibs=1 skip=0 count=2 > old-part.txt @@ -114,7 +114,7 @@ else exit 1 fi -curl -s --cacert certs/ca.pem -H "Authorization: Bearer $token" "https://localhost:8443/files/urn:neic:001-002?startCoordinate=7&endCoordinate=14" --output test-part2.txt +curl -s --cacert certs/ca.pem -H "Authorization: Bearer $token" "https://localhost:9443/files/urn:neic:001-002?startCoordinate=7&endCoordinate=14" --output test-part2.txt dd if=old-file.txt ibs=1 skip=7 count=7 > old-part2.txt @@ -127,7 +127,7 @@ else exit 1 fi -curl -s --cacert certs/ca.pem -H "Authorization: Bearer $token" "https://localhost:8443/files/urn:neic:001-002?startCoordinate=70000&endCoordinate=140000" --output test-part3.txt +curl -s --cacert certs/ca.pem -H "Authorization: Bearer $token" "https://localhost:9443/files/urn:neic:001-002?startCoordinate=70000&endCoordinate=140000" --output test-part3.txt dd if=old-file.txt ibs=1 skip=70000 count=70000 > old-part3.txt @@ -140,10 +140,19 @@ else exit 1 fi +# test that downloads of decrypted files from a download instance that +# serves only encrypted files (here running at port 8443) should fail +curl -s --cacert certs/ca.pem -H "Authorization: Bearer $token" "https://localhost:8443/files/urn:neic:001-002" --output test-download-fail.txt + +if ! grep -q "downloading unencrypted data is not supported" test-download-fail.txt; then + echo "got unexpected response when trying to download unencrypted data from encrypted endpoint" +exit 1 +fi + # ------------------ # Test get visas failed -token=$(curl --cacert certs/ca.pem "https://localhost:8000/tokens" | jq -r '.[1]') +token=$(curl -s --cacert certs/ca.pem "https://localhost:8000/tokens" | jq -r '.[1]') ## Test datasets endpoint @@ -174,3 +183,8 @@ if [ "$check_empty_token" != "200" ]; then fi echo "got correct response when token permissions from untrusted sources" + +# cleanup +rm old-file.txt old-part*.txt test-download*.txt test-part*.txt + +echo "OK" \ No newline at end of file diff --git a/sda-download/.github/integration/tests/common/60_check_s3_endpoint.sh b/sda-download/.github/integration/tests/common/60_check_s3_endpoint.sh index cc86c44ee..4477f3813 100644 --- a/sda-download/.github/integration/tests/common/60_check_s3_endpoint.sh +++ b/sda-download/.github/integration/tests/common/60_check_s3_endpoint.sh @@ -75,9 +75,9 @@ auth_token() { # # Create s3cmd configs # - -port="8443" -[[ "$TLS" == "False" ]] && port="8080" +# ports 9443/9080 refer to the download instance serving unencrypted files +port="9443" +[[ "$TLS" == "False" ]] && port="9080" cat << EOF >s3cmd.valid [default] @@ -173,6 +173,8 @@ export should_return="70" info 'Testing valid file download with an invalid token' run_test s3cmd -c s3cmd.invalid get --force "s3://$dataset/dummy_data" +# cleanup +rm s3cmd.valid s3cmd.invalid echo info " ----- End of S3 Tests ------------------------------------------------- " diff --git a/sda-download/.github/integration/tests/common/70_check_download.sh b/sda-download/.github/integration/tests/common/70_check_download.sh index 87d7d8865..b0a6800e5 100644 --- a/sda-download/.github/integration/tests/common/70_check_download.sh +++ b/sda-download/.github/integration/tests/common/70_check_download.sh @@ -15,7 +15,7 @@ C4GH_PASSPHRASE=$(grep -F passphrase config.yaml | sed -e 's/.* //' -e 's/"//g') export C4GH_PASSPHRASE # download decrypted full file, check file size -curl -s --cacert certs/ca.pem -H "Authorization: Bearer $token" "https://localhost:8443/s3/$dataset/$file" --output full1.bam +curl -s --cacert certs/ca.pem -H "Authorization: Bearer $token" "https://localhost:9443/s3/$dataset/$file" --output full1.bam file_size=$(stat -c %s full1.bam) # Get the size of the file if [ "$file_size" -ne "$expected_size" ]; then @@ -24,11 +24,14 @@ if [ "$file_size" -ne "$expected_size" ]; then fi # test that start, end=0 returns the whole file -curl -s --cacert certs/ca.pem -H "Authorization: Bearer $token" "https://localhost:8443/s3/$dataset/$file?startCoordinate=0&endCoordinate=0" --output full2.bam +curl -s --cacert certs/ca.pem -H "Authorization: Bearer $token" "https://localhost:9443/s3/$dataset/$file?startCoordinate=0&endCoordinate=0" --output full2.bam if ! cmp --silent full1.bam full2.bam; then echo "Full decrypted files, with and without coordinates, are different" exit 1 fi +# cleanup +rm full*.bam + echo "OK" \ No newline at end of file diff --git a/sda-download/.github/integration/tests/common/80_check_reencrypt.sh b/sda-download/.github/integration/tests/common/80_check_reencrypt.sh index 5e60579e0..3075132cb 100644 --- a/sda-download/.github/integration/tests/common/80_check_reencrypt.sh +++ b/sda-download/.github/integration/tests/common/80_check_reencrypt.sh @@ -18,8 +18,8 @@ dataset="https://doi.example/ty009.sfrrss/600.45asasga" file="dummy_data" expected_size=1048605 -# Download unencrypted full file, check file size -curl -s --cacert certs/ca.pem -H "Authorization: Bearer $token" "https://localhost:8443/s3/$dataset/$file" --output full1.bam +# Download unencrypted full file (from download service at port 9443), check file size +curl -s --cacert certs/ca.pem -H "Authorization: Bearer $token" "https://localhost:9443/s3/$dataset/$file" --output full1.bam file_size=$(stat -c %s full1.bam) # Get the size of the file if [ "$file_size" -ne "$expected_size" ]; then @@ -30,7 +30,7 @@ fi # Test reencrypt the file header with the client public key clientkey=$(base64 -w0 client.pub.pem) reencryptedFile=reencrypted.bam.c4gh -curl -s --cacert certs/ca.pem -H "Authorization: Bearer $token" -H "Client-Public-Key: $clientkey" "https://localhost:8443/s3-encrypted/$dataset/$file" --output $reencryptedFile +curl -s --cacert certs/ca.pem -H "Authorization: Bearer $token" -H "Client-Public-Key: $clientkey" "https://localhost:8443/s3/$dataset/$file" --output $reencryptedFile expected_encrypted_size=1049205 file_size=$(stat -c %s $reencryptedFile) @@ -56,7 +56,7 @@ fi # download reencrypted partial file, check file size partReencryptedFile=part1.bam.c4gh -curl -s --cacert certs/ca.pem -H "Authorization: Bearer $token" -H "Client-Public-Key: $clientkey" "https://localhost:8443/s3-encrypted/$dataset/$file?startCoordinate=0&endCoordinate=1000" --output $partReencryptedFile +curl -s --cacert certs/ca.pem -H "Authorization: Bearer $token" -H "Client-Public-Key: $clientkey" "https://localhost:8443/s3/$dataset/$file?startCoordinate=0&endCoordinate=1000" --output $partReencryptedFile file_size=$(stat -c %s $partReencryptedFile) # Get the size of the file part_expected_size=65688 @@ -83,11 +83,8 @@ if ! grep -q "^THIS FILE IS JUST DUMMY DATA" part1.bam; then exit 1 fi -# Clean up -rm full1.bam full2.bam part1.bam $reencryptedFile - # try to download encrypted full file without sending a public key -resp=$(curl --cacert certs/ca.pem -H "Authorization: Bearer $token" "https://localhost:8443/s3-encrypted/$dataset/$file" -s -o /dev/null -w "%{http_code}") +resp=$(curl --cacert certs/ca.pem -H "Authorization: Bearer $token" "https://localhost:8443/s3/$dataset/$file" -s -o /dev/null -w "%{http_code}") if [ "$resp" -ne 400 ]; then echo "Incorrect response with missing public key, expected 400 got $resp" @@ -95,10 +92,13 @@ if [ "$resp" -ne 400 ]; then fi # try to download encrypted full file with a bad public key -resp=$(curl --cacert certs/ca.pem -H "Authorization: Bearer $token" -H "Client-Public-Key: YmFkIGtleQ==" "https://localhost:8443/s3-encrypted/$dataset/$file" -s -o /dev/null -w "%{http_code}") +resp=$(curl --cacert certs/ca.pem -H "Authorization: Bearer $token" -H "Client-Public-Key: YmFkIGtleQ==" "https://localhost:8443/s3/$dataset/$file" -s -o /dev/null -w "%{http_code}") if [ "$resp" -ne 500 ]; then echo "Incorrect response with missing public key, expected 500 got $resp" fi +# Clean up +rm full1.bam full2.bam part1.bam* $reencryptedFile + echo "OK" \ No newline at end of file From 6ca29b7addfa1b37d85fe7617426301ac2de01cd Mon Sep 17 00:00:00 2001 From: Alex Aperis Date: Mon, 9 Dec 2024 18:48:02 +0100 Subject: [PATCH 09/22] generate c4gh key for tests --- sda-download/api/sda/sda_test.go | 72 +++++++++++++++++---- sda-download/internal/config/config_test.go | 19 ++++-- 2 files changed, 73 insertions(+), 18 deletions(-) diff --git a/sda-download/api/sda/sda_test.go b/sda-download/api/sda/sda_test.go index 89bfc0d2c..a2c6415ee 100644 --- a/sda-download/api/sda/sda_test.go +++ b/sda-download/api/sda/sda_test.go @@ -6,6 +6,7 @@ import ( "encoding/base64" "encoding/binary" "errors" + "fmt" "io" "math" "net/http" @@ -393,6 +394,19 @@ func TestDownload_Fail_UnencryptedDownloadNotAllowed(t *testing.T) { func TestDownload_Fail_FileNotFound(t *testing.T) { + // // Generate a crypth4gh private key file + // _, privateKey, err := keys.GenerateKeyPair() + // assert.NoError(t, err) + // tempDir := t.TempDir() + // defer os.RemoveAll(tempDir) + // privateKeyFile, err := os.Create(fmt.Sprintf("%s/c4fg.key", tempDir)) + // assert.NoError(t, err) + // err = keys.WriteCrypt4GHX25519PrivateKey(privateKeyFile, privateKey, []byte("password")) + // assert.NoError(t, err) + + privateKeyFilePath, err := GenerateTestC4ghKey(t) + assert.NoError(t, err) + // Save original to-be-mocked functions and config originalCheckFilePermission := database.CheckFilePermission originalServeUnencryptedDataTrigger := config.Config.App.Crypt4GHPublicKeyB64 @@ -403,9 +417,8 @@ func TestDownload_Fail_FileNotFound(t *testing.T) { return "", errors.New("file not found") } - viper.Set("app.c4ghPrivateKeyPath", "../../dev_utils/c4gh.sec.pem") - viper.Set("app.c4ghPassphrase", "oaagCP1YgAZeEyl2eJAkHv9lkcWXWFgm") - var err error + viper.Set("app.c4ghPrivateKeyPath", privateKeyFilePath) + viper.Set("app.c4ghPassphrase", "password") config.Config.App.Crypt4GHPrivateKey, config.Config.App.Crypt4GHPublicKeyB64, err = config.GetC4GHKeys() assert.NoError(t, err, "Could not load c4gh keys") @@ -442,6 +455,9 @@ func TestDownload_Fail_FileNotFound(t *testing.T) { func TestDownload_Fail_NoPermissions(t *testing.T) { + privateKeyFilePath, err := GenerateTestC4ghKey(t) + assert.NoError(t, err) + // Save original to-be-mocked functions originalCheckFilePermission := database.CheckFilePermission originalGetCacheFromContext := middleware.GetCacheFromContext @@ -457,9 +473,8 @@ func TestDownload_Fail_NoPermissions(t *testing.T) { return session.Cache{} } - viper.Set("app.c4ghPrivateKeyPath", "../../dev_utils/c4gh.sec.pem") - viper.Set("app.c4ghPassphrase", "oaagCP1YgAZeEyl2eJAkHv9lkcWXWFgm") - var err error + viper.Set("app.c4ghPrivateKeyPath", privateKeyFilePath) + viper.Set("app.c4ghPassphrase", "password") config.Config.App.Crypt4GHPrivateKey, config.Config.App.Crypt4GHPublicKeyB64, err = config.GetC4GHKeys() assert.NoError(t, err, "Could not load c4gh keys") @@ -497,6 +512,9 @@ func TestDownload_Fail_NoPermissions(t *testing.T) { func TestDownload_Fail_GetFile(t *testing.T) { + privateKeyFilePath, err := GenerateTestC4ghKey(t) + assert.NoError(t, err) + // Save original to-be-mocked functions originalCheckFilePermission := database.CheckFilePermission originalGetCacheFromContext := middleware.GetCacheFromContext @@ -517,9 +535,8 @@ func TestDownload_Fail_GetFile(t *testing.T) { return nil, errors.New("database error") } - viper.Set("app.c4ghPrivateKeyPath", "../../dev_utils/c4gh.sec.pem") - viper.Set("app.c4ghPassphrase", "oaagCP1YgAZeEyl2eJAkHv9lkcWXWFgm") - var err error + viper.Set("app.c4ghPrivateKeyPath", privateKeyFilePath) + viper.Set("app.c4ghPassphrase", "password") config.Config.App.Crypt4GHPrivateKey, config.Config.App.Crypt4GHPublicKeyB64, err = config.GetC4GHKeys() assert.NoError(t, err, "Could not load c4gh keys") @@ -558,6 +575,9 @@ func TestDownload_Fail_GetFile(t *testing.T) { func TestDownload_Fail_OpenFile(t *testing.T) { + privateKeyFilePath, err := GenerateTestC4ghKey(t) + assert.NoError(t, err) + // Save original to-be-mocked functions originalCheckFilePermission := database.CheckFilePermission originalGetCacheFromContext := middleware.GetCacheFromContext @@ -585,9 +605,8 @@ func TestDownload_Fail_OpenFile(t *testing.T) { return fileDetails, nil } - viper.Set("app.c4ghPrivateKeyPath", "../../dev_utils/c4gh.sec.pem") - viper.Set("app.c4ghPassphrase", "oaagCP1YgAZeEyl2eJAkHv9lkcWXWFgm") - var err error + viper.Set("app.c4ghPrivateKeyPath", privateKeyFilePath) + viper.Set("app.c4ghPassphrase", "password") config.Config.App.Crypt4GHPrivateKey, config.Config.App.Crypt4GHPublicKeyB64, err = config.GetC4GHKeys() assert.NoError(t, err, "Could not load c4gh keys") @@ -725,6 +744,9 @@ func (f *fakeGRPC) ServeHTTP(w http.ResponseWriter, r *http.Request) { func TestDownload_Whole_Range_Encrypted(t *testing.T) { + privateKeyFilePath, err := GenerateTestC4ghKey(t) + assert.NoError(t, err) + // Save original to-be-mocked functions originalCheckFilePermission := database.CheckFilePermission originalGetCacheFromContext := middleware.GetCacheFromContext @@ -774,8 +796,8 @@ func TestDownload_Whole_Range_Encrypted(t *testing.T) { config.Config.Reencrypt.ClientKey = keyfile.Name() config.Config.Reencrypt.Timeout = 10 - viper.Set("app.c4ghPrivateKeyPath", "../../dev_utils/c4gh.sec.pem") - viper.Set("app.c4ghPassphrase", "oaagCP1YgAZeEyl2eJAkHv9lkcWXWFgm") + viper.Set("app.c4ghPrivateKeyPath", privateKeyFilePath) + viper.Set("app.c4ghPassphrase", "password") config.Config.App.Crypt4GHPrivateKey, config.Config.App.Crypt4GHPublicKeyB64, err = config.GetC4GHKeys() assert.NoError(t, err, "Could not load c4gh keys") @@ -930,3 +952,25 @@ func TestDownload_Whole_Range_Encrypted(t *testing.T) { viper.Set("app.c4ghPrivateKeyPath", "") viper.Set("app.c4ghPassphrase", "") } + +func GenerateTestC4ghKey(t *testing.T) (string, error) { + t.Helper() + // Generate a crypth4gh private key file + _, privateKey, err := keys.GenerateKeyPair() + if err != nil { + return "", err + } + + tempDir := t.TempDir() + privateKeyFile, err := os.Create(fmt.Sprintf("%s/c4fg.key", tempDir)) + if err != nil { + return "", err + } + + err = keys.WriteCrypt4GHX25519PrivateKey(privateKeyFile, privateKey, []byte("password")) + if err != nil { + return "", err + } + + return privateKeyFile.Name(), nil +} diff --git a/sda-download/internal/config/config_test.go b/sda-download/internal/config/config_test.go index 82a95e4a0..bb57340e9 100644 --- a/sda-download/internal/config/config_test.go +++ b/sda-download/internal/config/config_test.go @@ -4,6 +4,7 @@ import ( "bytes" "encoding/base64" "fmt" + "os" "testing" "time" @@ -64,6 +65,16 @@ func (suite *TestSuite) TestMissingRequiredConfVar() { func (suite *TestSuite) TestAppConfig() { + // Generate a crypth4gh private key file + _, privateKey, err := keys.GenerateKeyPair() + assert.NoError(suite.T(), err) + tempDir := suite.T().TempDir() + defer os.RemoveAll(tempDir) + privateKeyFile, err := os.Create(fmt.Sprintf("%s/c4fg.key", tempDir)) + assert.NoError(suite.T(), err) + err = keys.WriteCrypt4GHX25519PrivateKey(privateKeyFile, privateKey, []byte("password")) + assert.NoError(suite.T(), err) + // Test fail on missing middleware viper.Set("app.host", "test") viper.Set("app.port", 1234) @@ -75,7 +86,7 @@ func (suite *TestSuite) TestAppConfig() { viper.Set("app.middleware", "noexist") c := &Map{} - err := c.appConfig() + err = c.appConfig() assert.Error(suite.T(), err, "Error expected") viper.Reset() @@ -85,8 +96,8 @@ func (suite *TestSuite) TestAppConfig() { viper.Set("app.serverkey", "test") viper.Set("log.logLevel", "debug") viper.Set("db.sslmode", "disable") - viper.Set("app.c4ghPrivateKeyPath", "../../dev_utils/c4gh.sec.pem") - viper.Set("app.c4ghPassphrase", "oaagCP1YgAZeEyl2eJAkHv9lkcWXWFgm") + viper.Set("app.c4ghPrivateKeyPath", privateKeyFile.Name()) + viper.Set("app.c4ghPassphrase", "password") c = &Map{} err = c.appConfig() @@ -110,7 +121,7 @@ func (suite *TestSuite) TestAppConfig() { assert.ErrorContains(suite.T(), err, "no such file or directory") // Check false c4gh key - viper.Set("app.c4ghPrivateKeyPath", "../../dev_utils/c4gh.sec.pem") + viper.Set("app.c4ghPrivateKeyPath", privateKeyFile.Name()) viper.Set("app.c4ghPassphrase", "blablabla") err = c.appConfig() assert.ErrorContains(suite.T(), err, "chacha20poly1305: message authentication failed") From 0fc3ace79f01f64b451b1f49a26f27b545b5ce90 Mon Sep 17 00:00:00 2001 From: Alex Aperis Date: Mon, 9 Dec 2024 20:05:45 +0100 Subject: [PATCH 10/22] update s3notls test --- .../integration/setup/common/10_services.sh | 1 - .../tests/s3notls/52_check_endpoint.sh | 17 +++++++++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/sda-download/.github/integration/setup/common/10_services.sh b/sda-download/.github/integration/setup/common/10_services.sh index 52af643e8..1680ff917 100644 --- a/sda-download/.github/integration/setup/common/10_services.sh +++ b/sda-download/.github/integration/setup/common/10_services.sh @@ -1,5 +1,4 @@ #!/bin/bash -set -e # Build containers docker build -t neicnordic/sda-download:latest . || exit 1 diff --git a/sda-download/.github/integration/tests/s3notls/52_check_endpoint.sh b/sda-download/.github/integration/tests/s3notls/52_check_endpoint.sh index b4281921c..5699f0af0 100644 --- a/sda-download/.github/integration/tests/s3notls/52_check_endpoint.sh +++ b/sda-download/.github/integration/tests/s3notls/52_check_endpoint.sh @@ -75,8 +75,17 @@ export C4GH_PASSPHRASE crypt4gh decrypt -s c4gh.sec.pem -f dummy_data.c4gh && mv dummy_data old-file.txt +# first try downloading from download instance serving encrypted data, should fail curl -s -H "Authorization: Bearer $token" "http://localhost:8080/files/urn:neic:001-002" --output test-download.txt +if ! grep -q "downloading unencrypted data is not supported" "test-download.txt"; then + echo "wrong response when trying to download unencrypted data from encrypted endpoint" + exit 1 +fi + +# now try downloading from download instance serving unencrypted data +curl -s -H "Authorization: Bearer $token" "http://localhost:9080/files/urn:neic:001-002" --output test-download.txt + cmp --silent old-file.txt test-download.txt status=$? @@ -86,7 +95,8 @@ else echo "Files are different" fi -curl -s -H "Authorization: Bearer $token" "http://localhost:8080/files/urn:neic:001-002?startCoordinate=0&endCoordinate=2" --output test-part.txt +# downloading from download instance serving unencrypted data +curl -s -H "Authorization: Bearer $token" "http://localhost:9080/files/urn:neic:001-002?startCoordinate=0&endCoordinate=2" --output test-part.txt dd if=old-file.txt ibs=1 skip=0 count=2 > old-part.txt @@ -99,7 +109,8 @@ else exit 1 fi -curl -s -H "Authorization: Bearer $token" "http://localhost:8080/files/urn:neic:001-002?startCoordinate=7&endCoordinate=14" --output test-part2.txt +# downloading from download instance serving unencrypted data +curl -s -H "Authorization: Bearer $token" "http://localhost:9080/files/urn:neic:001-002?startCoordinate=7&endCoordinate=14" --output test-part2.txt dd if=old-file.txt ibs=1 skip=7 count=7 > old-part2.txt @@ -146,3 +157,5 @@ if [ "$check_dataset" != "https://doi.example/ty009.sfrrss/600.45asasga" ]; then fi echo "expected dataset found for token from untrusted source" + +echo "OK" \ No newline at end of file From 6588fa9b34cbdd58d3c074a53b624e627990c5f5 Mon Sep 17 00:00:00 2001 From: Alex Aperis Date: Mon, 9 Dec 2024 22:05:27 +0100 Subject: [PATCH 11/22] fix certifixer in compose --- sda-download/dev_utils/compose-sda.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/sda-download/dev_utils/compose-sda.yml b/sda-download/dev_utils/compose-sda.yml index 9b6b9f0d2..370de0a6c 100644 --- a/sda-download/dev_utils/compose-sda.yml +++ b/sda-download/dev_utils/compose-sda.yml @@ -7,7 +7,6 @@ services: - | cp /origcerts/* /certs chown -R nobody:nobody /certs/* - chmod -R 644 /certs/* chmod -R og-rw /certs/*-key.pem chown -R 70:70 /certs/db* ls -la /certs/ From b0541c37218ff0efe71f94f6a0328f39cda9d76c Mon Sep 17 00:00:00 2001 From: Alex Aperis Date: Wed, 11 Dec 2024 19:39:21 +0100 Subject: [PATCH 12/22] fail if passing pubkey to unencrypted /s3 --- sda-download/api/sda/sda.go | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/sda-download/api/sda/sda.go b/sda-download/api/sda/sda.go index 85dbcbdcd..c82c63d8d 100644 --- a/sda-download/api/sda/sda.go +++ b/sda-download/api/sda/sda.go @@ -207,12 +207,22 @@ func Files(c *gin.Context) { // Download serves file contents as bytes func Download(c *gin.Context) { + // This conditional should always be satisfied for /s3 since the c.Param is set to encrypted + // when Crypt4GHPublicKeyB64 is not set or empty, but this is not the case for calls to /files endpoint. + // So we need this check. + if c.Param("type") != "encrypted" && config.Config.App.Crypt4GHPublicKeyB64 == "" { c.String(http.StatusBadRequest, "downloading unencrypted data is not supported") return } + if c.Param("type") != "encrypted" && c.GetHeader("Client-Public-Key") != "" { + c.String(http.StatusBadRequest, "downloading encrypted data is not supported") + + return + } + // Get file ID from path fileID := c.Param("fileid") @@ -404,7 +414,7 @@ func Download(c *gin.Context) { fileStream = seekStream } default: - // Reencrypt header for use with our temporary key + // Reencrypt header for use with the loaded internal key newHeader, err := reencryptHeader(fileDetails.Header, config.Config.App.Crypt4GHPublicKeyB64) if err != nil { log.Errorf("Failed to reencrypt the file header, reason: %v", err) From abf40fd551ecd7153dc3c7efd53deb9e461a9d55 Mon Sep 17 00:00:00 2001 From: Alex Aperis Date: Wed, 11 Dec 2024 19:39:48 +0100 Subject: [PATCH 13/22] update sda testsuite and do a small refactoring --- sda-download/api/sda/sda_test.go | 61 +++++++++++++++----------------- 1 file changed, 28 insertions(+), 33 deletions(-) diff --git a/sda-download/api/sda/sda_test.go b/sda-download/api/sda/sda_test.go index a2c6415ee..55c142163 100644 --- a/sda-download/api/sda/sda_test.go +++ b/sda-download/api/sda/sda_test.go @@ -49,6 +49,7 @@ func TestDatasets(t *testing.T) { // Mock request and response holders w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) + gin.SetMode(gin.ReleaseMode) c.Set("datasets", session.Cache{Datasets: []string{"dataset1", "dataset2"}}) // Test the outcomes of the handler @@ -355,18 +356,10 @@ func TestDownload_Fail_UnencryptedDownloadNotAllowed(t *testing.T) { expectedStatusCode := 400 expectedBody := []byte("downloading unencrypted data is not supported") - if response.StatusCode != expectedStatusCode { - t.Errorf("TestDownload_Fail_UnencryptedDownloadNotAllowed failed, got %d expected %d", response.StatusCode, expectedStatusCode) - } - if !bytes.Equal(body, []byte(expectedBody)) { - // visual byte comparison in terminal (easier to find string differences) - t.Error(body) - t.Error([]byte(expectedBody)) - t.Errorf("TestDownload_Fail_UnencryptedDownloadNotAllowed failed, got %s expected %s", string(body), string(expectedBody)) - } + assert.Equal(t, expectedStatusCode, response.StatusCode, "Unexpected status code from download") + assert.Equal(t, expectedBody, body, "Unexpected body from download") // Test downloading of unencrypted file from the s3 endpoint, should fail - // encrypted w = httptest.NewRecorder() c, _ = gin.CreateTestContext(w) c.Request = &http.Request{Method: "GET", URL: &url.URL{Path: "/mocks3/somepath", RawQuery: "filename=somepath"}} @@ -378,15 +371,25 @@ func TestDownload_Fail_UnencryptedDownloadNotAllowed(t *testing.T) { expectedStatusCode = 400 expectedBody = []byte("downloading unencrypted data is not supported") - if response.StatusCode != expectedStatusCode { - t.Errorf("TestDownload_Fail_UnencryptedDownloadNotAllowed failed, got %d expected %d", response.StatusCode, expectedStatusCode) - } - if !bytes.Equal(body, []byte(expectedBody)) { - // visual byte comparison in terminal (easier to find string differences) - t.Error(body) - t.Error([]byte(expectedBody)) - t.Errorf("TestDownload_Fail_UnencryptedDownloadNotAllowed failed, got %s expected %s", string(body), string(expectedBody)) - } + assert.Equal(t, expectedStatusCode, response.StatusCode, "Unexpected status code from download") + assert.Equal(t, expectedBody, body, "Unexpected body from download") + + // Test downloading from unencrypted file serving /s3 when passing a c4gh pubkey, should fail + config.Config.App.Crypt4GHPublicKeyB64 = "somepubkeyBase64" + w = httptest.NewRecorder() + c, _ = gin.CreateTestContext(w) + c.Request = &http.Request{Method: "GET"} + c.Request.Header = http.Header{"Client-Public-Key": []string{"somepubkey"}} + + Download(c) + response = w.Result() + defer response.Body.Close() + body, _ = io.ReadAll(response.Body) + expectedStatusCode = 400 + expectedBody = []byte("downloading encrypted data is not supported") + + assert.Equal(t, expectedStatusCode, response.StatusCode, "Unexpected status code from download") + assert.Equal(t, expectedBody, body, "Unexpected body from download") // Return mock config to originals config.Config.App.Crypt4GHPublicKeyB64 = originalServeUnencryptedDataTrigger @@ -394,16 +397,6 @@ func TestDownload_Fail_UnencryptedDownloadNotAllowed(t *testing.T) { func TestDownload_Fail_FileNotFound(t *testing.T) { - // // Generate a crypth4gh private key file - // _, privateKey, err := keys.GenerateKeyPair() - // assert.NoError(t, err) - // tempDir := t.TempDir() - // defer os.RemoveAll(tempDir) - // privateKeyFile, err := os.Create(fmt.Sprintf("%s/c4fg.key", tempDir)) - // assert.NoError(t, err) - // err = keys.WriteCrypt4GHX25519PrivateKey(privateKeyFile, privateKey, []byte("password")) - // assert.NoError(t, err) - privateKeyFilePath, err := GenerateTestC4ghKey(t) assert.NoError(t, err) @@ -425,6 +418,8 @@ func TestDownload_Fail_FileNotFound(t *testing.T) { // Mock request and response holders w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) + c.Request = &http.Request{Method: "GET"} + c.Request.Header = http.Header{"Client-Public-Key": []string{""}} // Test the outcomes of the handler Download(c) @@ -481,6 +476,8 @@ func TestDownload_Fail_NoPermissions(t *testing.T) { // Mock request and response holders w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) + c.Request = &http.Request{Method: "GET"} + c.Request.Header = http.Header{"Client-Public-Key": []string{""}} // Test the outcomes of the handler Download(c) @@ -543,6 +540,8 @@ func TestDownload_Fail_GetFile(t *testing.T) { // Mock request and response holders w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) + c.Request = &http.Request{Method: "GET"} + c.Request.Header = http.Header{"Client-Public-Key": []string{""}} // Test the outcomes of the handler Download(c) @@ -801,10 +800,6 @@ func TestDownload_Whole_Range_Encrypted(t *testing.T) { config.Config.App.Crypt4GHPrivateKey, config.Config.App.Crypt4GHPublicKeyB64, err = config.GetC4GHKeys() assert.NoError(t, err, "Could not load c4gh keys") - // Set up crypt4gh keys - config.Config.App.Crypt4GHPrivateKey, config.Config.App.Crypt4GHPublicKeyB64, err = config.GetC4GHKeys() - assert.NoError(t, err, "Could not generate temporary keys") - // Make a file to hold the archive file datafile, err := os.CreateTemp(".", "datafile.") assert.NoError(t, err, "Could not create datafile for test") From 19d727ae29f5c4f64c095fc8da9a1c825a27dc4c Mon Sep 17 00:00:00 2001 From: Alex Aperis Date: Wed, 11 Dec 2024 19:41:59 +0100 Subject: [PATCH 14/22] add integration tests for s3 failure messages --- .../tests/common/80_check_reencrypt.sh | 15 --- .../tests/common/90_check_s3_errors.sh | 92 +++++++++++++++++++ 2 files changed, 92 insertions(+), 15 deletions(-) create mode 100644 sda-download/.github/integration/tests/common/90_check_s3_errors.sh diff --git a/sda-download/.github/integration/tests/common/80_check_reencrypt.sh b/sda-download/.github/integration/tests/common/80_check_reencrypt.sh index 3075132cb..f58c265ff 100644 --- a/sda-download/.github/integration/tests/common/80_check_reencrypt.sh +++ b/sda-download/.github/integration/tests/common/80_check_reencrypt.sh @@ -83,21 +83,6 @@ if ! grep -q "^THIS FILE IS JUST DUMMY DATA" part1.bam; then exit 1 fi -# try to download encrypted full file without sending a public key -resp=$(curl --cacert certs/ca.pem -H "Authorization: Bearer $token" "https://localhost:8443/s3/$dataset/$file" -s -o /dev/null -w "%{http_code}") - -if [ "$resp" -ne 400 ]; then - echo "Incorrect response with missing public key, expected 400 got $resp" - exit 1 -fi - -# try to download encrypted full file with a bad public key -resp=$(curl --cacert certs/ca.pem -H "Authorization: Bearer $token" -H "Client-Public-Key: YmFkIGtleQ==" "https://localhost:8443/s3/$dataset/$file" -s -o /dev/null -w "%{http_code}") - -if [ "$resp" -ne 500 ]; then - echo "Incorrect response with missing public key, expected 500 got $resp" -fi - # Clean up rm full1.bam full2.bam part1.bam* $reencryptedFile diff --git a/sda-download/.github/integration/tests/common/90_check_s3_errors.sh b/sda-download/.github/integration/tests/common/90_check_s3_errors.sh new file mode 100644 index 000000000..2c8918fab --- /dev/null +++ b/sda-download/.github/integration/tests/common/90_check_s3_errors.sh @@ -0,0 +1,92 @@ +#!/bin/bash + +if [ "$STORAGETYPE" = s3notls ]; then + exit 0 +fi + +cd dev_utils || exit 1 + +# Get a token, set up variables +token=$(curl -s --cacert certs/ca.pem "https://localhost:8000/tokens" | jq -r '.[0]') + +if [ -z "$token" ]; then + echo "Failed to obtain token" + exit 1 +fi + +dataset="https://doi.example/ty009.sfrrss/600.45asasga" +file="dummy_data" +clientkey=$(base64 -w0 client.pub.pem) +bad_token=token2=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJyZXF1ZXN0ZXJAZGVtby5vcmciLCJhdWQiOlsiYXVkMiIsImF1ZDMiXSwiYXpwIjoiYXpwIiwic2NvcGUiOiJvcGVuaWQiLCJpc3MiOiJodHRwczovL2RlbW8uZXhhbXBsZSIsImV4cCI6OTk5OTk5OTk5OSwiaWF0IjoxNTYxNjIxOTEzLCJqdGkiOiI2YWQ3YWE0Mi0zZTljLTQ4MzMtYmQxNi03NjVjYjgwYzIxMDIifQ.ncUyjNytxqS9bqLnsbjv6D839PnHVw-anQS4bFpAs20 + +# Test error codes and error messages returned to the user + +# try to download encrypted file without sending a public key +resp=$(curl -s --cacert certs/ca.pem -H "Authorization: Bearer $token" "https://localhost:8443/s3/$dataset/$file") +if ! echo "$resp" | grep -q "c4gh public key is missing from the header"; then + echo "Incorrect response, expected 'c4gh public key is missing from the header' got $resp" + exit 1 +fi + +resp=$(curl --cacert certs/ca.pem -H "Authorization: Bearer $token" "https://localhost:8443/s3/$dataset/$file" -s -o /dev/null -w "%{http_code}") +if [ "$resp" -ne 400 ]; then + echo "Incorrect response with missing public key, expected 400 got $resp" + exit 1 +fi + +# try to download encrypted file with a bad public key +resp=$(curl -s --cacert certs/ca.pem -H "Authorization: Bearer $token" -H "Client-Public-Key: YmFkIGtleQ==" "https://localhost:8443/s3/$dataset/$file") +if ! echo "$resp" | grep -q "file re-encryption error"; then + echo "Incorrect response, expected 'file re-encryption error' got $resp" + exit 1 +fi + +resp=$(curl --cacert certs/ca.pem -H "Authorization: Bearer $token" -H "Client-Public-Key: YmFkIGtleQ==" "https://localhost:8443/s3/$dataset/$file" -s -o /dev/null -w "%{http_code}") +if [ "$resp" -ne 500 ]; then + echo "Incorrect response with missing public key, expected 500 got $resp" +fi + +# try to download encrypted file from instance that serves unencrypted files + +resp=$(curl -s --cacert certs/ca.pem -H "Authorization: Bearer $token" -H "Client-Public-Key: $clientkey" "https://localhost:9443/s3/$dataset/$file") +if ! echo "$resp" | grep -q "downloading encrypted data is not supported"; then + echo "Incorrect response, expected 'downloading encrypted data is not supported' got $resp" + exit 1 +fi + +resp=$(curl --cacert certs/ca.pem -H "Authorization: Bearer $token" -H "Client-Public-Key: $clientkey" "https://localhost:9443/s3/$dataset/$file" -s -o /dev/null -w "%{http_code}") +if [ "$resp" -ne 400 ]; then + echo "Incorrect response, expected 400 got $resp" + exit 1 +fi + + +# try to download a file the user doesn't have access to + +resp=$(curl -s --cacert certs/ca.pem -H "Authorization: Bearer $bad_token" -H "Client-Public-Key: $clientkey" "https://localhost:8443/s3/$dataset/$file") +if ! echo "$resp" | grep -q "get visas failed"; then + echo "Incorrect response, expected 'get visas failed' got $resp" + exit 1 +fi + +resp=$(curl --cacert certs/ca.pem -H "Authorization: Bearer $bad_token" -H "Client-Public-Key: $clientkey" "https://localhost:8443/s3/$dataset/$file" -s -o /dev/null -w "%{http_code}") +if [ "$resp" -ne 401 ]; then + echo "Incorrect response, expected 401 got $resp" + exit 1 +fi + +# try to download a file that does not exist + +resp=$(curl -s --cacert certs/ca.pem -H "Authorization: Bearer $token" -H "Client-Public-Key: $clientkey" "https://localhost:9443/s3/$dataset/nonexistentfile") +if [ -n "$resp" ]; then + echo "Incorrect response, expected no error message, got $resp" + exit 1 +fi + +resp=$(curl --cacert certs/ca.pem -H "Authorization: Bearer $token" -H "Client-Public-Key: $clientkey" "https://localhost:9443/s3/$dataset/nonexistentfile" -s -o /dev/null -w "%{http_code}") +if [ "$resp" -ne 404 ]; then + echo "Incorrect response, expected 404 got $resp" + exit 1 +fi + +echo "OK" \ No newline at end of file From f2b0d0c606fdcf2dfcb9a127fc2b46531b2b1397 Mon Sep 17 00:00:00 2001 From: Alex Aperis Date: Wed, 11 Dec 2024 19:42:31 +0100 Subject: [PATCH 15/22] update api readme --- sda-download/api/api.md | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/sda-download/api/api.md b/sda-download/api/api.md index b2e2b923d..a7b247006 100644 --- a/sda-download/api/api.md +++ b/sda-download/api/api.md @@ -1,12 +1,9 @@ # API - The Download API service provides functionality for downloading files from the Archive. -It implements the [Data Out API](https://neic-sda.readthedocs.io/en/latest/dataout/#rest-api-endpoints). - -Further, it enables the endpoints `/s3` and `/s3-encrypted`, used for htsget. +It implements the [Data Out API](https://neic-sda.readthedocs.io/en/latest/dataout/#rest-api-endpoints). Further, it enables the endpoint `/s3` used for htsget and other services that need to interface with an s3-backend storage. -The response can be restricted to only contain a given range of a file, and the files can be returned encrypted or unencrypted. +The response can be restricted to only contain a given range of a file, and the files can be returned encrypted or unencrypted, depending on the configuration of the service. All endpoints require an `Authorization` header with an access token in the `Bearer` scheme. ``` @@ -25,10 +22,9 @@ The client can establish a session to bypass time-costly visa validations for fu - `/metadata/datasets/*dataset` - `/files/:fileid` -**[File download requests, for htsget](#file-download-requests)** +**[File download requests, for s3 endpoint](#file-download-requests)** - `/s3/*datasetid/*filepath` -- `/s3-encrypted/*datasetid/*filepath` ### Data out API #### Datasets @@ -100,10 +96,11 @@ Parts of a file can be requested with specific byte ranges using `startCoordinat ``` ### File download requests -These endpoints are designed for usage with [htsget](https://samtools.github.io/hts-specs/htsget.html). +This endpoint is designed for usage with [htsget](https://samtools.github.io/hts-specs/htsget.html) or other external applications that interface with s3-storage backends. + +The `/s3` endpoint accepts the parameters described below. Note that depending on the configuration of the download service, `/s3` may either serve only encrypted or decrypted files. -The `/s3` and `/s3-encrypted` endpoints accept the same parameters, described below. -Note that the download service may be configured to only allow encrypted file downloads. +By default, **Parameters**: From eb58f24d1810dd244bed07be435eaeb08e60bfa6 Mon Sep 17 00:00:00 2001 From: Alex Aperis Date: Fri, 13 Dec 2024 00:05:57 +0100 Subject: [PATCH 16/22] update chart - add serveDecrypted object variable - repurpose obsolete c4gh secret logic - update README --- charts/sda-svc/Chart.yaml | 2 +- charts/sda-svc/README.md | 4 ++- charts/sda-svc/templates/download-deploy.yaml | 29 ++++++++++++------- charts/sda-svc/values.yaml | 2 +- 4 files changed, 24 insertions(+), 13 deletions(-) diff --git a/charts/sda-svc/Chart.yaml b/charts/sda-svc/Chart.yaml index 472878aad..1630e2775 100644 --- a/charts/sda-svc/Chart.yaml +++ b/charts/sda-svc/Chart.yaml @@ -1,6 +1,6 @@ apiVersion: v2 name: sda-svc -version: 0.29.0 +version: 0.30.0 appVersion: v0.3.170 kubeVersion: '>= 1.26.0' description: Components for Sensitive Data Archive (SDA) installation diff --git a/charts/sda-svc/README.md b/charts/sda-svc/README.md index a184b3d40..1921bbf12 100644 --- a/charts/sda-svc/README.md +++ b/charts/sda-svc/README.md @@ -123,7 +123,9 @@ Parameter | Description | Default `global.doa.outbox.s3AccessKey` | Outbox S3 Access Key | `null` `global.doa.outbox.s3SecretKey` | Outbox S3 Secret key | `null` `global.download.enabled` | Deploy the download service | `true` -`global.download.serveUnencryptedData` | Whether the download service serves unencrypted data | `false` +`global.download.serveDecrypted` | If non-empty, the download service will serve decrypted data | `{}` +`global.download.serveDecrypted.c4ghKeyFile` | Transient private C4GH key | `""` +`global.download.serveDecrypted.secretName` | Secret holding the transient private C4GH key | `""` `global.download.sessionExpiration` | Session key expiration time in seconds | `28800` `global.download.trusted.configPath` | Path to the ISS config file | `$secrets/iss` `global.download.trusted.configFile` | Name of ISS config file | `iss.json` diff --git a/charts/sda-svc/templates/download-deploy.yaml b/charts/sda-svc/templates/download-deploy.yaml index e941df359..0b42e7249 100644 --- a/charts/sda-svc/templates/download-deploy.yaml +++ b/charts/sda-svc/templates/download-deploy.yaml @@ -140,9 +140,14 @@ spec: - name: LOG_LEVEL value: {{ .Values.global.log.level | quote }} {{- end }} - {{- if .Values.global.download.serveUnencryptedData }} - - name: APP_SERVEUNENCRYPTEDDATA - value: {{ .Values.global.download.serveUnencryptedData }} + {{- if .Values.global.download.serveDecrypted }} + - name: APP_C4GHPRIVATEKEYPATH + value: {{ template "c4ghPath" . }}/{{ .Values.global.download.serveDecrypted.c4ghKeyFile }} + - name: APP_C4GHPASSPHRASE + valueFrom: + secretKeyRef: + name: {{ required "A secret for the transient c4gh key is required" .Values.global.download.serveDecrypted.secretName }} + key: passphrase {{- end }} {{- if .Values.global.tls.enabled }} - name: APP_PORT @@ -223,9 +228,11 @@ spec: resources: {{ toYaml .Values.download.resources | trim | indent 10 }} volumeMounts: - {{- if not .Values.global.vaultSecrets }} - - name: c4gh + {{- if .Values.global.download.serveDecrypted }} + - name: c4gh-transient mountPath: {{ template "c4ghPath" . }} + {{- end }} + {{- if not .Values.global.vaultSecrets }} - name: iss mountPath: {{ template "trustedIssPath" . }} {{- end }} @@ -250,14 +257,16 @@ spec: secretName: {{ required "An certificate issuer or a TLS secret name is required for download" .Values.download.tls.secretName }} {{- end }} {{- end }} - {{- if not .Values.global.vaultSecrets }} - - name: c4gh + {{- if .Values.global.download.serveDecrypted }} + - name: c4gh-transient secret: defaultMode: 0440 - secretName: {{ required "A secret for the c4gh key is required" .Values.global.c4gh.secretName }} + secretName: {{ required "A secret for the transient c4gh key is required" .Values.global.download.serveDecrypted.secretName }} items: - - key: {{ .Values.global.c4gh.keyFile }} - path: {{ .Values.global.c4gh.keyFile }} + - key: {{ .Values.global.download.serveDecrypted.c4ghKeyFile }} + path: {{ .Values.global.download.serveDecrypted.c4ghKeyFile }} + {{- end }} + {{- if not .Values.global.vaultSecrets }} - name: iss secret: defaultMode: 0440 diff --git a/charts/sda-svc/values.yaml b/charts/sda-svc/values.yaml index a74bbe93b..a309d72d0 100644 --- a/charts/sda-svc/values.yaml +++ b/charts/sda-svc/values.yaml @@ -239,7 +239,7 @@ global: iss: - iss: "https://login.elixir-czech.org/oidc" jku: "https://login.elixir-czech.org/oidc/jwk" - serveUnencryptedData: false + serveDecrypted: {} oidc: provider: "https://login.elixir-czech.org/oidc/" From 05ee5b1ebd78bb99c1d440197671f1b0dcfcddf5 Mon Sep 17 00:00:00 2001 From: Alex Aperis Date: Fri, 13 Dec 2024 15:09:51 +0100 Subject: [PATCH 17/22] changing the passphrase breaks tests if they are run more than once --- .github/integration/scripts/charts/dependencies.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/integration/scripts/charts/dependencies.sh b/.github/integration/scripts/charts/dependencies.sh index f01365c51..fb717f4a1 100644 --- a/.github/integration/scripts/charts/dependencies.sh +++ b/.github/integration/scripts/charts/dependencies.sh @@ -31,7 +31,7 @@ else fi # secret for the crypt4gh keypair -C4GHPASSPHRASE="$(random-string)" +C4GHPASSPHRASE="ZB8ko3g2P9wEgagpgqCS7vJhJoKrSUfJ" export C4GHPASSPHRASE dir=$PWD if [ -n "$1" ]; then From c4c2a4949451a8edd93094e7b0587dd50078c0bc Mon Sep 17 00:00:00 2001 From: Alex Aperis Date: Fri, 13 Dec 2024 15:32:54 +0100 Subject: [PATCH 18/22] minimize time between api calls to increase success rate of date comparison --- .github/integration/tests/sda/11_api_test.sh | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/integration/tests/sda/11_api_test.sh b/.github/integration/tests/sda/11_api_test.sh index 5970e4ab8..c2226e501 100644 --- a/.github/integration/tests/sda/11_api_test.sh +++ b/.github/integration/tests/sda/11_api_test.sh @@ -2,7 +2,7 @@ set -e # Test the API files endpoint -token="$(curl http://oidc:8080/tokens | jq -r '.[0]')" +token="$(curl -s 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" @@ -52,6 +52,12 @@ if [ "$resp" != "200" ]; then 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 # list key hashes resp="$(curl -s -k -L -H "Authorization: Bearer $token" -X GET "http://api:8080/c4gh-keys/list" | jq '. | length')" @@ -66,11 +72,5 @@ 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" From 72c0e6b96faacd388d753c838c93e83a18e35e23 Mon Sep 17 00:00:00 2001 From: Alex Aperis Date: Sat, 14 Dec 2024 19:15:05 +0100 Subject: [PATCH 19/22] remove mention to serveDecrypted parent object --- charts/sda-svc/README.md | 1 - charts/sda-svc/templates/download-deploy.yaml | 6 +++--- charts/sda-svc/values.yaml | 4 +++- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/charts/sda-svc/README.md b/charts/sda-svc/README.md index 1921bbf12..ed0e3c9c0 100644 --- a/charts/sda-svc/README.md +++ b/charts/sda-svc/README.md @@ -123,7 +123,6 @@ Parameter | Description | Default `global.doa.outbox.s3AccessKey` | Outbox S3 Access Key | `null` `global.doa.outbox.s3SecretKey` | Outbox S3 Secret key | `null` `global.download.enabled` | Deploy the download service | `true` -`global.download.serveDecrypted` | If non-empty, the download service will serve decrypted data | `{}` `global.download.serveDecrypted.c4ghKeyFile` | Transient private C4GH key | `""` `global.download.serveDecrypted.secretName` | Secret holding the transient private C4GH key | `""` `global.download.sessionExpiration` | Session key expiration time in seconds | `28800` diff --git a/charts/sda-svc/templates/download-deploy.yaml b/charts/sda-svc/templates/download-deploy.yaml index 0b42e7249..733b9771d 100644 --- a/charts/sda-svc/templates/download-deploy.yaml +++ b/charts/sda-svc/templates/download-deploy.yaml @@ -140,7 +140,7 @@ spec: - name: LOG_LEVEL value: {{ .Values.global.log.level | quote }} {{- end }} - {{- if .Values.global.download.serveDecrypted }} + {{- if .Values.global.download.serveDecrypted.c4ghKeyFile }} - name: APP_C4GHPRIVATEKEYPATH value: {{ template "c4ghPath" . }}/{{ .Values.global.download.serveDecrypted.c4ghKeyFile }} - name: APP_C4GHPASSPHRASE @@ -228,7 +228,7 @@ spec: resources: {{ toYaml .Values.download.resources | trim | indent 10 }} volumeMounts: - {{- if .Values.global.download.serveDecrypted }} + {{- if .Values.global.download.serveDecrypted.c4ghKeyFile }} - name: c4gh-transient mountPath: {{ template "c4ghPath" . }} {{- end }} @@ -257,7 +257,7 @@ spec: secretName: {{ required "An certificate issuer or a TLS secret name is required for download" .Values.download.tls.secretName }} {{- end }} {{- end }} - {{- if .Values.global.download.serveDecrypted }} + {{- if .Values.global.download.serveDecrypted.c4ghKeyFile }} - name: c4gh-transient secret: defaultMode: 0440 diff --git a/charts/sda-svc/values.yaml b/charts/sda-svc/values.yaml index a309d72d0..6c2e83ff4 100644 --- a/charts/sda-svc/values.yaml +++ b/charts/sda-svc/values.yaml @@ -239,7 +239,9 @@ global: iss: - iss: "https://login.elixir-czech.org/oidc" jku: "https://login.elixir-czech.org/oidc/jwk" - serveDecrypted: {} + serveDecrypted: + c4ghKeyFile: "" + secretName: "" oidc: provider: "https://login.elixir-czech.org/oidc/" From 3a4f095e893d7e09985ed5c7f1207453a460f373 Mon Sep 17 00:00:00 2001 From: Alex Aperis Date: Sat, 14 Dec 2024 19:44:37 +0100 Subject: [PATCH 20/22] split variable names for more clarity --- charts/sda-svc/templates/download-deploy.yaml | 4 +- sda-download/api/sda/sda_test.go | 40 +++++++++---------- sda-download/dev_utils/compose-no-tls.yml | 4 +- sda-download/dev_utils/compose.yml | 4 +- sda-download/internal/config/config.go | 10 ++--- sda-download/internal/config/config_test.go | 10 ++--- 6 files changed, 36 insertions(+), 36 deletions(-) diff --git a/charts/sda-svc/templates/download-deploy.yaml b/charts/sda-svc/templates/download-deploy.yaml index 733b9771d..2de812ed6 100644 --- a/charts/sda-svc/templates/download-deploy.yaml +++ b/charts/sda-svc/templates/download-deploy.yaml @@ -141,9 +141,9 @@ spec: value: {{ .Values.global.log.level | quote }} {{- end }} {{- if .Values.global.download.serveDecrypted.c4ghKeyFile }} - - name: APP_C4GHPRIVATEKEYPATH + - name: APP_C4GH_PRIVATEKEYPATH value: {{ template "c4ghPath" . }}/{{ .Values.global.download.serveDecrypted.c4ghKeyFile }} - - name: APP_C4GHPASSPHRASE + - name: APP_C4GH_PASSPHRASE valueFrom: secretKeyRef: name: {{ required "A secret for the transient c4gh key is required" .Values.global.download.serveDecrypted.secretName }} diff --git a/sda-download/api/sda/sda_test.go b/sda-download/api/sda/sda_test.go index 55c142163..3733d0b61 100644 --- a/sda-download/api/sda/sda_test.go +++ b/sda-download/api/sda/sda_test.go @@ -410,8 +410,8 @@ func TestDownload_Fail_FileNotFound(t *testing.T) { return "", errors.New("file not found") } - viper.Set("app.c4ghPrivateKeyPath", privateKeyFilePath) - viper.Set("app.c4ghPassphrase", "password") + viper.Set("app.c4gh.privateKeyPath", privateKeyFilePath) + viper.Set("app.c4gh.passphrase", "password") config.Config.App.Crypt4GHPrivateKey, config.Config.App.Crypt4GHPublicKeyB64, err = config.GetC4GHKeys() assert.NoError(t, err, "Could not load c4gh keys") @@ -443,8 +443,8 @@ func TestDownload_Fail_FileNotFound(t *testing.T) { database.CheckFilePermission = originalCheckFilePermission config.Config.App.Crypt4GHPublicKeyB64 = originalServeUnencryptedDataTrigger config.Config.App.Crypt4GHPrivateKey = originalC4ghPrivateKeyFilepath - viper.Set("app.c4ghPrivateKeyPath", "") - viper.Set("app.c4ghPassphrase", "") + viper.Set("app.c4gh.privateKeyPath", "") + viper.Set("app.c4gh.passphrase", "") } @@ -468,8 +468,8 @@ func TestDownload_Fail_NoPermissions(t *testing.T) { return session.Cache{} } - viper.Set("app.c4ghPrivateKeyPath", privateKeyFilePath) - viper.Set("app.c4ghPassphrase", "password") + viper.Set("app.c4gh.privateKeyPath", privateKeyFilePath) + viper.Set("app.c4gh.passphrase", "password") config.Config.App.Crypt4GHPrivateKey, config.Config.App.Crypt4GHPublicKeyB64, err = config.GetC4GHKeys() assert.NoError(t, err, "Could not load c4gh keys") @@ -502,8 +502,8 @@ func TestDownload_Fail_NoPermissions(t *testing.T) { middleware.GetCacheFromContext = originalGetCacheFromContext config.Config.App.Crypt4GHPublicKeyB64 = originalServeUnencryptedDataTrigger config.Config.App.Crypt4GHPrivateKey = originalC4ghPrivateKeyFilepath - viper.Set("app.c4ghPrivateKeyPath", "") - viper.Set("app.c4ghPassphrase", "") + viper.Set("app.c4gh.privateKeyPath", "") + viper.Set("app.c4gh.passphrase", "") } @@ -532,8 +532,8 @@ func TestDownload_Fail_GetFile(t *testing.T) { return nil, errors.New("database error") } - viper.Set("app.c4ghPrivateKeyPath", privateKeyFilePath) - viper.Set("app.c4ghPassphrase", "password") + viper.Set("app.c4gh.privateKeyPath", privateKeyFilePath) + viper.Set("app.c4gh.passphrase", "password") config.Config.App.Crypt4GHPrivateKey, config.Config.App.Crypt4GHPublicKeyB64, err = config.GetC4GHKeys() assert.NoError(t, err, "Could not load c4gh keys") @@ -567,8 +567,8 @@ func TestDownload_Fail_GetFile(t *testing.T) { database.GetFile = originalGetFile config.Config.App.Crypt4GHPublicKeyB64 = originalServeUnencryptedDataTrigger config.Config.App.Crypt4GHPrivateKey = originalC4ghPrivateKeyFilepath - viper.Set("app.c4ghPrivateKeyPath", "") - viper.Set("app.c4ghPassphrase", "") + viper.Set("app.c4gh.privateKeyPath", "") + viper.Set("app.c4gh.passphrase", "") } @@ -604,8 +604,8 @@ func TestDownload_Fail_OpenFile(t *testing.T) { return fileDetails, nil } - viper.Set("app.c4ghPrivateKeyPath", privateKeyFilePath) - viper.Set("app.c4ghPassphrase", "password") + viper.Set("app.c4gh.privateKeyPath", privateKeyFilePath) + viper.Set("app.c4gh.passphrase", "password") config.Config.App.Crypt4GHPrivateKey, config.Config.App.Crypt4GHPublicKeyB64, err = config.GetC4GHKeys() assert.NoError(t, err, "Could not load c4gh keys") @@ -642,8 +642,8 @@ func TestDownload_Fail_OpenFile(t *testing.T) { database.GetFile = originalGetFile config.Config.App.Crypt4GHPublicKeyB64 = originalServeUnencryptedDataTrigger config.Config.App.Crypt4GHPrivateKey = originalC4ghPrivateKeyFilepath - viper.Set("app.c4ghPrivateKeyPath", "") - viper.Set("app.c4ghPassphrase", "") + viper.Set("app.c4gh.privateKeyPath", "") + viper.Set("app.c4gh.passphrase", "") } func Test_CalucalateCoords(t *testing.T) { @@ -795,8 +795,8 @@ func TestDownload_Whole_Range_Encrypted(t *testing.T) { config.Config.Reencrypt.ClientKey = keyfile.Name() config.Config.Reencrypt.Timeout = 10 - viper.Set("app.c4ghPrivateKeyPath", privateKeyFilePath) - viper.Set("app.c4ghPassphrase", "password") + viper.Set("app.c4gh.privateKeyPath", privateKeyFilePath) + viper.Set("app.c4gh.passphrase", "password") config.Config.App.Crypt4GHPrivateKey, config.Config.App.Crypt4GHPublicKeyB64, err = config.GetC4GHKeys() assert.NoError(t, err, "Could not load c4gh keys") @@ -944,8 +944,8 @@ func TestDownload_Whole_Range_Encrypted(t *testing.T) { database.GetFile = originalGetFile config.Config.App.Crypt4GHPublicKeyB64 = originalServeUnencryptedDataTrigger config.Config.App.Crypt4GHPrivateKey = originalC4ghPrivateKeyFilepath - viper.Set("app.c4ghPrivateKeyPath", "") - viper.Set("app.c4ghPassphrase", "") + viper.Set("app.c4gh.privateKeyPath", "") + viper.Set("app.c4gh.passphrase", "") } func GenerateTestC4ghKey(t *testing.T) (string, error) { diff --git a/sda-download/dev_utils/compose-no-tls.yml b/sda-download/dev_utils/compose-no-tls.yml index ac0434f2d..a858a793e 100644 --- a/sda-download/dev_utils/compose-no-tls.yml +++ b/sda-download/dev_utils/compose-no-tls.yml @@ -88,8 +88,8 @@ services: - ARCHIVE_URL=http://s3 - ARCHIVE_TYPE=s3 - DB_HOST=db - - APP_C4GHPRIVATEKEYPATH=/dev_utils/c4gh.sec.pem - - APP_C4GHPASSPHRASE=oaagCP1YgAZeEyl2eJAkHv9lkcWXWFgm + - APP_C4GH_PRIVATEKEYPATH=/dev_utils/c4gh.sec.pem + - APP_C4GH_PASSPHRASE=oaagCP1YgAZeEyl2eJAkHv9lkcWXWFgm image: neicnordic/sda-download:latest build: context: .. diff --git a/sda-download/dev_utils/compose.yml b/sda-download/dev_utils/compose.yml index f9f7ad954..d771dd663 100644 --- a/sda-download/dev_utils/compose.yml +++ b/sda-download/dev_utils/compose.yml @@ -121,8 +121,8 @@ services: condition: service_started env_file: ./env.download environment: - - APP_C4GHPRIVATEKEYPATH=/dev_utils/c4gh.sec.pem - - APP_C4GHPASSPHRASE=oaagCP1YgAZeEyl2eJAkHv9lkcWXWFgm + - APP_C4GH_PRIVATEKEYPATH=/dev_utils/c4gh.sec.pem + - APP_C4GH_PASSPHRASE=oaagCP1YgAZeEyl2eJAkHv9lkcWXWFgm image: neicnordic/sda-download:latest volumes: - ./config.yaml:/config.yaml diff --git a/sda-download/internal/config/config.go b/sda-download/internal/config/config.go index 160f263a7..cf724a01c 100644 --- a/sda-download/internal/config/config.go +++ b/sda-download/internal/config/config.go @@ -376,10 +376,10 @@ func (c *Map) appConfig() error { } var err error - if viper.IsSet("app.c4ghPrivateKeyPath") { + if viper.IsSet("app.c4gh.privateKeyPath") { - if !viper.IsSet("app.c4ghPassphrase") { - return errors.New("app.c4ghPassphrase is not set") + if !viper.IsSet("app.c4gh.passphrase") { + return errors.New("app.c4gh.passphrase is not set") } c.App.Crypt4GHPrivateKey, c.App.Crypt4GHPublicKeyB64, err = GetC4GHKeys() @@ -487,8 +487,8 @@ func constructWhitelist(obj []TrustedISS) *jwk.MapWhitelist { // GetC4GHKey reads and decrypts and returns the c4gh key func GetC4GHKeys() ([32]byte, string, error) { - keyPath := viper.GetString("app.c4ghPrivateKeyPath") - passphrase := viper.GetString("app.c4ghPassphrase") + keyPath := viper.GetString("app.c4gh.privateKeyPath") + passphrase := viper.GetString("app.c4gh.passphrase") // Make sure the key path and passphrase is valid keyFile, err := os.Open(keyPath) diff --git a/sda-download/internal/config/config_test.go b/sda-download/internal/config/config_test.go index bb57340e9..e4da7865b 100644 --- a/sda-download/internal/config/config_test.go +++ b/sda-download/internal/config/config_test.go @@ -96,8 +96,8 @@ func (suite *TestSuite) TestAppConfig() { viper.Set("app.serverkey", "test") viper.Set("log.logLevel", "debug") viper.Set("db.sslmode", "disable") - viper.Set("app.c4ghPrivateKeyPath", privateKeyFile.Name()) - viper.Set("app.c4ghPassphrase", "password") + viper.Set("app.c4gh.PrivateKeyPath", privateKeyFile.Name()) + viper.Set("app.c4gh.passphrase", "password") c = &Map{} err = c.appConfig() @@ -116,13 +116,13 @@ func (suite *TestSuite) TestAppConfig() { assert.Nilf(suite.T(), err, "Incorrect public c4gh key generated (bad key)") // Check false c4gh key - viper.Set("app.c4ghPrivateKeyPath", "some/nonexistent.key") + viper.Set("app.c4gh.privateKeyPath", "some/nonexistent.key") err = c.appConfig() assert.ErrorContains(suite.T(), err, "no such file or directory") // Check false c4gh key - viper.Set("app.c4ghPrivateKeyPath", privateKeyFile.Name()) - viper.Set("app.c4ghPassphrase", "blablabla") + viper.Set("app.c4gh.privateKeyPath", privateKeyFile.Name()) + viper.Set("app.c4gh.passphrase", "blablabla") err = c.appConfig() assert.ErrorContains(suite.T(), err, "chacha20poly1305: message authentication failed") } From 333d62c0e7f1835da54870b4d2e18a2e28d9d0cd Mon Sep 17 00:00:00 2001 From: Alex Aperis Date: Sat, 14 Dec 2024 19:58:11 +0100 Subject: [PATCH 21/22] add default config values --- sda-download/dev_utils/config.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sda-download/dev_utils/config.yaml b/sda-download/dev_utils/config.yaml index b60f62214..87c77f448 100644 --- a/sda-download/dev_utils/config.yaml +++ b/sda-download/dev_utils/config.yaml @@ -4,6 +4,9 @@ app: serverkey: "./dev_utils/certs/download-key.pem" port: "8443" middleware: "default" + c4gh: + passphrase: "" + privateKeyPath: "" log: level: "debug" From 40de21c1d22c9d4885fc8c44ba48418da9fd6ea2 Mon Sep 17 00:00:00 2001 From: Alex Aperis Date: Sat, 14 Dec 2024 20:48:12 +0100 Subject: [PATCH 22/22] skip volumes if c4gh key is read from vault config --- charts/sda-svc/templates/download-deploy.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/charts/sda-svc/templates/download-deploy.yaml b/charts/sda-svc/templates/download-deploy.yaml index 2de812ed6..bee64005c 100644 --- a/charts/sda-svc/templates/download-deploy.yaml +++ b/charts/sda-svc/templates/download-deploy.yaml @@ -228,11 +228,11 @@ spec: resources: {{ toYaml .Values.download.resources | trim | indent 10 }} volumeMounts: + {{- if not .Values.global.vaultSecrets }} {{- if .Values.global.download.serveDecrypted.c4ghKeyFile }} - name: c4gh-transient mountPath: {{ template "c4ghPath" . }} {{- end }} - {{- if not .Values.global.vaultSecrets }} - name: iss mountPath: {{ template "trustedIssPath" . }} {{- end }} @@ -257,6 +257,7 @@ spec: secretName: {{ required "An certificate issuer or a TLS secret name is required for download" .Values.download.tls.secretName }} {{- end }} {{- end }} + {{- if not .Values.global.vaultSecrets }} {{- if .Values.global.download.serveDecrypted.c4ghKeyFile }} - name: c4gh-transient secret: @@ -266,7 +267,6 @@ spec: - key: {{ .Values.global.download.serveDecrypted.c4ghKeyFile }} path: {{ .Values.global.download.serveDecrypted.c4ghKeyFile }} {{- end }} - {{- if not .Values.global.vaultSecrets }} - name: iss secret: defaultMode: 0440