diff --git a/.github/workflows/rock-build.yaml b/.github/workflows/rock-build.yaml index ab7f085d..ad98e637 100644 --- a/.github/workflows/rock-build.yaml +++ b/.github/workflows/rock-build.yaml @@ -13,7 +13,11 @@ jobs: id: rockcraft with: rockcraft-channel: edge - + + - name: Install pre-requisites + run: | + sudo apt-get update + sudo apt-get install -y openssl jq - name: Import the image to Docker registry run: | sudo rockcraft.skopeo --insecure-policy copy oci-archive:${{ steps.rockcraft.outputs.rock }} docker-daemon:notary:latest @@ -43,84 +47,26 @@ jobs: - name: Test if pebble notify fires correctly id: test_notify run : | + openssl genpkey -algorithm RSA -out private_key.pem -pkeyopt rsa_keygen_bits:2048 + openssl req -new -key private_key.pem -out request.csr -subj "/C=CA/ST=Quebec/L=Montreal/O=Test Company/OU=IT Department/CN=test.example.com" + openssl req -x509 -new -nodes -key private_key.pem -sha256 -days 365 -out ca_certificate.pem -subj "/C=CA/ST=Quebec/L=Montreal/O=Test CA/OU=CA Department/CN=Test CA" + openssl x509 -req -in request.csr -CA ca_certificate.pem -CAkey private_key.pem -CAcreateserial -out certificate.pem -days 365 -sha256 + CSR=$(cat request.csr | awk 'NF {sub(/\r/, ""); printf "%s\\n",$0;}') + CERTIFICATE=$(cat certificate.pem ca_certificate.pem | awk 'NF {sub(/\r/, ""); printf "%s\\n",$0;}') + curl -XPOST -k -d '{"username":"admin", "password": "Admin1234"}' https://localhost:3000/api/v1/accounts - export ADMIN_TOKEN=$(curl -XPOST -k -d '{"username":"admin", "password": "Admin1234"}' https://localhost:3000/login) - curl -XPOST -k -d '-----BEGIN CERTIFICATE REQUEST----- - MIICsTCCAZkCAQAwbDELMAkGA1UEBhMCQ0ExFDASBgNVBAgMC05vdmEgU2NvdGlh - MRAwDgYDVQQHDAdIYWxpZmF4MSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0 - eSBMdGQxEjAQBgNVBAMMCWFwcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEP - ADCCAQoCggEBAOhDSpNbeFiXMQzQcobExHqYMEGzqpX8N9+AR6/HPZWBybgx1hr3 - ejqsKornzpVph/dO9UC7O9aBlG071O9VQGHt3OU3rkZIk2009vYwLuSrAlJtnUne - p7KKn2lZGvh7jVyZE5RkS0X27vlT0soANsmcVq/82VneHrF/nbDcK6DOjQpS5o5l - EiNk2CIpYGUkw3WnQF4pBk8t4bNOl3nfpaAOfnmNuBX3mWyfPnaKMCENMpDqL9FR - V/O5bIPLmyH30OHUEJUkWOmFt9GFi+QfMoM0fR34KmRbDz79hZZb/yVPZZJl7l6i - FWXkNR3gxdEnwCZkTgWk5OqS9dCJOtsDE8ECAwEAAaAAMA0GCSqGSIb3DQEBCwUA - A4IBAQCqBX5WaNv/HjkzAyNXYuCToCb8GjmiMqL54t+1nEI1QTm6axQXivEbQT3x - GIh7uQYC06wHE23K6Znc1/G+o3y6lID07rvhBNal1qoXUiq6CsAqk+DXYdd8MEh5 - joerEedFqcW+WTUDcqddfIyDAGPqrM9j6/E+aFYyZjJ/xRuMf1zlWMljRiwj1NI9 - NxqjsYYQ3zxfUjv8gxXm0hN8Up1O9saoEF+zbuWNdiUWd6Ih3/3u5VBNSxgVOrDQ - CeXyyzkMx1pWTx0rWa7NSa+DMKVVzv46pck/9kLB4gPL8zqvIOMQsf74N0VcbVfd - 9jQR8mPXQYPUERl1ZhNrkzkyA0kd - -----END CERTIFICATE REQUEST-----' -H "Authorization: Bearer $ADMIN_TOKEN" 'https://localhost:3000/api/v1/certificate_requests' - curl -XPOST -k -d '-----BEGIN CERTIFICATE----- - MIIEVDCCAjwCFE8lmuBE85/RPw2M17Kzl93O+9IPMA0GCSqGSIb3DQEBCwUAMGEx - CzAJBgNVBAYTAlRSMQ4wDAYDVQQIDAVJem1pcjESMBAGA1UEBwwJTmFybGlkZXJl - MSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxCzAJBgNVBAMMAm1l - MB4XDTI0MDYyODEzMTAyMVoXDTI1MDYyODEzMTAyMVowbDELMAkGA1UEBhMCQ0Ex - FDASBgNVBAgMC05vdmEgU2NvdGlhMRAwDgYDVQQHDAdIYWxpZmF4MSEwHwYDVQQK - DBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxEjAQBgNVBAMMCWFwcGxlLmNvbTCC - ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOhDSpNbeFiXMQzQcobExHqY - MEGzqpX8N9+AR6/HPZWBybgx1hr3ejqsKornzpVph/dO9UC7O9aBlG071O9VQGHt - 3OU3rkZIk2009vYwLuSrAlJtnUnep7KKn2lZGvh7jVyZE5RkS0X27vlT0soANsmc - Vq/82VneHrF/nbDcK6DOjQpS5o5lEiNk2CIpYGUkw3WnQF4pBk8t4bNOl3nfpaAO - fnmNuBX3mWyfPnaKMCENMpDqL9FRV/O5bIPLmyH30OHUEJUkWOmFt9GFi+QfMoM0 - fR34KmRbDz79hZZb/yVPZZJl7l6iFWXkNR3gxdEnwCZkTgWk5OqS9dCJOtsDE8EC - AwEAATANBgkqhkiG9w0BAQsFAAOCAgEAOA+0C2Gjx+qWc/U8Bq7ayU8c/aKsegSb - nZ6tcxcFpfPvy7oLS+cD3LYnQodwmSXM/BXn5cHyXhkSJCzbxQX5d/dzSiSOtqLk - 51KQGTDElUMO8HPvPeb+YDVBNFqEJoN3PRRhSRwIm/pYd5cM3UmuD7lW1+NMfiVX - Vr4hWlt7nXh027VSslTPGQFnIRW3XbwpFsMguyt8CheKg2l+Q0ttiKMrzPmMPP/s - 8ZXvMhQqehoj+k3R7k37J9kzLM22YN+Ranns9OKbojQh9uGhoPGdgg5CcNt9/CTF - Ow9dE//5nXQe1OnbAmDc8+RxqJhcrjObV2zQcZS4QvzO3NW49tLEnBj4LrvDJIrU - saZhBJSlezPa2psd/vwXZ1e46e7fbdUVh9AtXa5Uq9RJ4q21hXlhgfv7UtvYQCmp - cEzIzvRuPs4bw8ZmAXSLm7EpxZmbStWjRRjolK8rbzXyzoRgksmAECh6GNGW0++V - 0uxHDvKHQh+B1+tPRr5sOAxSRHmKeDGE3EUO9Icyy0hsod1sGmyOJD22s5vi3ziM - v88ccwoaTDoh0sVma/eD1tm3wm38KtGWiAH8S5lmf9hOtzVndt86sT65Wp6An4ig - CJZJg3F9e0+V5dG4hkSzT+QW5AZlmzp/xAaLSbkaQ8WyXtknzWeo4LID+0SmYEwj - ccma2Ab7ZPU= - -----END CERTIFICATE----- - -----BEGIN CERTIFICATE----- - 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 - btZkbfdL5A== - -----END CERTIFICATE-----' -H "Authorization: Bearer $ADMIN_TOKEN" 'https://localhost:3000/api/v1/certificate_requests/1/certificate' + export ADMIN_TOKEN=$(curl -XPOST -k -d '{"username":"admin", "password": "Admin1234"}' https://localhost:3000/login | jq -r .result.token ) + + curl -k --location 'https://localhost:3000/api/v1/certificate_requests' \ + --header "Authorization: Bearer $ADMIN_TOKEN" \ + --header 'Content-Type: application/json' \ + --data "{\"csr\":\"${CSR}\"}" + + curl -k --location 'https://localhost:3000/api/v1/certificate_requests/1/certificate' \ + --header "Authorization: Bearer $ADMIN_TOKEN" \ + --header 'Content-Type: application/json' \ + --data "{\"certificate\":\"${COMBINED_CERTIFICATE}\"}" + docker exec notary /usr/bin/pebble notices docker exec notary /usr/bin/pebble notices | grep notary\\.com/certificate/update docker exec notary /usr/bin/pebble notice 3 diff --git a/.gitignore b/.gitignore index 1ca7378c..80097295 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,4 @@ *.db -*.pem *config.yaml .DS_Store diff --git a/cmd/notary/main_test.go b/cmd/notary/main_test.go deleted file mode 100644 index da893e0a..00000000 --- a/cmd/notary/main_test.go +++ /dev/null @@ -1,172 +0,0 @@ -package main - -import ( - "flag" - "io" - "log" - "os" - "os/exec" - "strings" - "testing" -) - -const ( - validCert = `-----BEGIN CERTIFICATE----- -MIIELjCCAxagAwIBAgICBnowDQYJKoZIhvcNAQELBQAwJzELMAkGA1UEBhMCVVMx -GDAWBgNVBAoTD0Nhbm9uaWNhbCwgSU5DLjAeFw0yNDA0MDUxMDAzMjhaFw0zNDA0 -MDUxMDAzMjhaMCcxCzAJBgNVBAYTAlVTMRgwFgYDVQQKEw9DYW5vbmljYWwsIElO -Qy4wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDAP98jcfNw40HbS1xR -6UpSQTp4AGldFWQZBOFaVzD+eh7sYM/BFdT0dZRHGjXxL77ewDbwdwAFJ5zuxo+u -8/VgKGRpK6KCnKailmVrdRDhA45airMRQN6QXurN4NZgXcCHJWGAQKA9XJzcwGJF -l5LxoFY58wCv0d1JP8fgmbcgIRQTCIvhrlgrJ5Acz9QP6BuaxEHKbYYvWyTWtAhi -HS/w51yEbh6959ceJGBDZPyEVd9sfGipvHrA73+33+XBluRcUuWV4dCecyP/m+8C -jTBmW5s8gS6JUDE8yl99qm7CnXTkNDqPXThrorcKRwcHrw3ZEOm5rUPLuyzGBx/C -DZUbY9bsvHJMHOHlbwiY+M2MFIO+3H6qyfPfcHs8NFkrZh/as+9hrEzSYcz+tGBi -NynkSmNPQi4yzT00ilKYgcBhPdDDlBbdhcmdeFA3XE880VkQdJgefsYpCgYRdILm -DDd6ZMfZsQOJjuRC8rQKLO+z1X5JhiOlkNxZaOkq9b9eu7230rxTFCGocn0l9oKw -0q8OIDOTb7UKdIaGq/y++uRxe0hhNoijN1OJvh+R3/KGuztu5Y8ejksIxKBrUqCg -bUDXmQ82xbdJ36qF+NHBqFqFaKhH1XuK6eAIfqgQam/u9HNZZw3mOdm9rvIZfwIT -F9gvSwm1bxzyIHL/zWOgyfzckQIDAQABo2QwYjAOBgNVHQ8BAf8EBAMCB4AwHQYD -VR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMBMA4GA1UdDgQHBAUBAgMEBjAhBgNV -HREEGjAYhwR/AAABhxAAAAAAAAAAAAAAAAAAAAABMA0GCSqGSIb3DQEBCwUAA4IB -AQB4UEu1/vTpEuuwoqgFpp8tEMewwBQ/CXPBN5seDnd/SUMXFrxk58f498qI3FQy -q98a+89jPWRGA5LY+DfIS82NYCwbKuvTzuJRoUpMPbebrhu7OQl7qQT6n8VOCy6x -IaRnPI0zEGbg2v340jMbB26FiyaFKyHEc24nnq3suZFmbslXzRE2Ebut+Qtft8he -0pSNQXtz5ULt0c8DTje7j+mRABzus45cj3HMDO4vcVRrHegdTE8YcZjwAFTKxqpg -W7GwJ5qPjnm6EMe8da55m8Q0hZchwGZreXNG7iCaw98pACBNgOOxh4LOhEZy25Bv -ayrvWnmPfg1u47sduuhHeUid ------END CERTIFICATE-----` - validPK = `-----BEGIN RSA PRIVATE KEY----- -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 ------END RSA PRIVATE KEY-----` - validConfig = `key_path: "./key_test.pem" -cert_path: "./cert_test.pem" -db_path: "./certs.db" -port: 8000 -pebble_notices: false` - invalidConfig = `hello: "world" -goodbye: "world"` - invalidDBConfig = `key_path: "./key_test.pem" -cert_path: "./cert_test.pem" -db_path: "/etc/hosts" -port: 8000 -pebble_notices: false` -) - -func TestMain(m *testing.M) { - cmd := exec.Command("go", "install", "./...") - if err := cmd.Run(); err != nil { - log.Fatalf("couldn't install the notary CLI: %v", err) - } - - testfolder, err := os.MkdirTemp("./", "configtest-") - if err != nil { - log.Fatalf("couldn't create temp directory: %v", err) - } - err = os.WriteFile(testfolder+"/cert_test.pem", []byte(validCert), 0o644) - if err != nil { - log.Fatalf("couldn't create temp testing file: %v", err) - } - err = os.WriteFile(testfolder+"/key_test.pem", []byte(validPK), 0o644) - if err != nil { - log.Fatalf("couldn't create temp testing file: %v", err) - } - if err := os.Chdir(testfolder); err != nil { - log.Fatalf("couldn't enter testing directory: %v", err) - } - - exitval := m.Run() - - if err := os.Chdir("../"); err != nil { - log.Fatalf("couldn't change back to parent directory: %v", err) - } - if err := os.RemoveAll(testfolder); err != nil { - log.Fatalf("couldn't remove temp testing directory: %v", err) - } - os.Exit(exitval) -} - -func TestNotaryFail(t *testing.T) { - oldArgs := os.Args - defer func() { os.Args = oldArgs }() - cases := []struct { - Name string - Args []string - ConfigYAML string - ExpectedOutput string - }{ - {"flags not set", []string{}, validConfig, "Providing a config file is required."}, - {"config file not valid", []string{"-config", "config.yaml"}, invalidConfig, "config file validation failed:"}, - {"database not connectable", []string{"-config", "config.yaml"}, invalidDBConfig, "Couldn't connect to database:"}, - } - for _, tc := range cases { - err := os.WriteFile("config.yaml", []byte(tc.ConfigYAML), 0o644) - if err != nil { - t.Errorf("Failed writing config file: %v", err) - } - flag.CommandLine = flag.NewFlagSet(tc.Name, flag.ExitOnError) - cmd := exec.Command("notary", tc.Args...) - stdout, _ := cmd.StdoutPipe() - - if err := cmd.Start(); err != nil { - t.Errorf("Failed running command: %v", err) - } - - slurp, _ := io.ReadAll(stdout) - - if err := cmd.Wait(); err == nil { - t.Errorf("Command did not fail: %s", tc.Name) - } - if !strings.Contains(string(slurp), tc.ExpectedOutput) { - t.Errorf("%s: Expected error not found: %s", tc.Name, slurp) - } - } -} diff --git a/internal/db/db.go b/internal/db/db.go index e3a7a098..fb5c0d99 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -55,15 +55,15 @@ type Database struct { // A CertificateRequest struct represents an entry in the database. // The object contains a Certificate Request, its matching Certificate if any, and the row ID. type CertificateRequest struct { - ID int `json:"id"` - CSR string `json:"csr"` - Certificate string `json:"certificate"` + ID int + CSR string + Certificate string } type User struct { - ID int `json:"id"` - Username string `json:"username"` - Password string `json:"password,omitempty"` - Permissions int `json:"permissions"` + ID int + Username string + Password string + Permissions int } var ErrIdNotFound = errors.New("id not found") diff --git a/internal/server/authorization_test.go b/internal/server/authorization_test.go index 7b34e926..164faa7e 100644 --- a/internal/server/authorization_test.go +++ b/internal/server/authorization_test.go @@ -1,168 +1,230 @@ package server_test import ( - "io" - "log" "net/http" - "net/http/httptest" - "regexp" "strings" "testing" - - "github.com/canonical/notary/internal/db" - "github.com/canonical/notary/internal/server" ) -func TestAuthorization(t *testing.T) { - testdb, err := db.NewDatabase(":memory:") +func TestAuthorizationNoAuth(t *testing.T) { + ts, _, err := setupServer() if err != nil { - log.Fatalf("couldn't create test sqlite db: %s", err) + t.Fatalf("couldn't create test server: %s", err) } - env := &server.HandlerConfig{} - env.DB = testdb - env.JWTSecret = []byte("secret") - ts := httptest.NewTLSServer(server.NewHandler(env)) defer ts.Close() - client := ts.Client() - var adminToken string - var nonAdminToken string - t.Run("prepare user accounts and tokens", prepareUserAccounts(ts.URL, client, &adminToken, &nonAdminToken)) - testCases := []struct { - desc string - method string - path string - data string - auth string - response string - status int + desc string + method string + path string }{ { - desc: "metrics reachable without auth", - method: "GET", - path: "/metrics", - data: "", - auth: "", - response: "# HELP certificate_requests Total number of certificate requests", - status: http.StatusOK, - }, - { - desc: "status reachable without auth", - method: "GET", - path: "/status", - data: "", - auth: "", - response: "", - status: http.StatusOK, - }, - { - desc: "missing endpoints produce 404", - method: "GET", - path: "/this/path/does/not/exist", - data: "", - auth: nonAdminToken, - response: "", - status: http.StatusNotFound, - }, - { - desc: "nonadmin can't see accounts", - method: "GET", - path: "/api/v1/accounts", - data: "", - auth: nonAdminToken, - response: "", - status: http.StatusForbidden, - }, - { - desc: "admin can see accounts", - method: "GET", - path: "/api/v1/accounts", - data: "", - auth: adminToken, - response: `[{"id":1,"username":"testadmin","permissions":1},{"id":2,"username":"testuser","permissions":0}]`, - status: http.StatusOK, + desc: "metrics reachable without auth", + method: "GET", + path: "/metrics", }, { - desc: "nonadmin can't delete admin account", - method: "DELETE", - path: "/api/v1/accounts/1", - data: "", - auth: nonAdminToken, - response: "", - status: http.StatusForbidden, + desc: "status reachable without auth", + method: "GET", + path: "/status", }, + } + for _, tC := range testCases { + t.Run(tC.desc, func(t *testing.T) { + req, err := http.NewRequest(tC.method, ts.URL+tC.path, nil) + if err != nil { + t.Fatal(err) + } + res, err := client.Do(req) + if err != nil { + t.Fatal(err) + } + if res.StatusCode != http.StatusOK { + t.Errorf("expected status code %d, got %d", http.StatusOK, res.StatusCode) + } + }) + } +} + +func TestAuthorizationNonAdminAuthorized(t *testing.T) { + ts, _, err := setupServer() + if err != nil { + t.Fatalf("couldn't create test server: %s", err) + } + defer ts.Close() + client := ts.Client() + var adminToken string + var nonAdminToken string + t.Run("prepare user accounts and tokens", prepareAccounts(ts.URL, client, &adminToken, &nonAdminToken)) + + testCases := []struct { + desc string + method string + path string + data string + status int + }{ { - desc: "user can't change admin password", - method: "POST", - path: "/api/v1/accounts/1/change_password", - data: `{"password":"Pwnd123!"}`, - auth: nonAdminToken, - response: "", - status: http.StatusForbidden, + desc: "user can change self password with /me", + method: "POST", + path: "/api/v1/accounts/me/change_password", + data: `{"password":"BetterPW1!"}`, + status: http.StatusCreated, }, { - desc: "user can change self password with /me", - method: "POST", - path: "/api/v1/accounts/me/change_password", - data: `{"password":"BetterPW1!"}`, - auth: nonAdminToken, - response: "", - status: http.StatusOK, + desc: "user can login with new password", + method: "POST", + path: "/login", + data: `{"username":"testuser","password":"BetterPW1!"}`, + status: http.StatusOK, }, + } + for _, tC := range testCases { + t.Run(tC.desc, func(t *testing.T) { + req, err := http.NewRequest(tC.method, ts.URL+tC.path, strings.NewReader(tC.data)) + if err != nil { + t.Fatal(err) + } + req.Header.Add("Authorization", "Bearer "+nonAdminToken) + res, err := client.Do(req) + if err != nil { + t.Fatal(err) + } + if res.StatusCode != tC.status { + t.Errorf("expected status code %d, got %d", tC.status, res.StatusCode) + } + }) + } +} + +func TestAuthorizationNonAdminUnauthorized(t *testing.T) { + ts, _, err := setupServer() + if err != nil { + t.Fatalf("couldn't create test server: %s", err) + } + defer ts.Close() + client := ts.Client() + var adminToken string + var nonAdminToken string + t.Run("prepare user accounts and tokens", prepareAccounts(ts.URL, client, &adminToken, &nonAdminToken)) + + testCases := []struct { + desc string + method string + path string + data string + status int + }{ { - desc: "user can login with new password", - method: "POST", - path: "/login", - data: `{"username":"testuser","password":"BetterPW1!"}`, - auth: nonAdminToken, - response: "", - status: http.StatusOK, + desc: "nonadmin can't see accounts", + method: "GET", + path: "/api/v1/accounts", + data: "", + status: http.StatusForbidden, }, { - desc: "admin can't delete itself", - method: "DELETE", - path: "/api/v1/accounts/1", - data: "", - auth: adminToken, - response: "error: deleting an Admin account is not allowed.", - status: http.StatusBadRequest, + desc: "nonadmin can't delete admin account", + method: "DELETE", + path: "/api/v1/accounts/1", + data: "", + status: http.StatusForbidden, }, { - desc: "admin can delete nonuser", - method: "DELETE", - path: "/api/v1/accounts/2", - data: "", - auth: adminToken, - response: "1", - status: http.StatusAccepted, + desc: "user can't change admin password", + method: "POST", + path: "/api/v1/accounts/1/change_password", + data: `{"password":"Pwnd123!"}`, + status: http.StatusForbidden, }, } for _, tC := range testCases { t.Run(tC.desc, func(t *testing.T) { req, err := http.NewRequest(tC.method, ts.URL+tC.path, strings.NewReader(tC.data)) - req.Header.Add("Authorization", "Bearer "+tC.auth) if err != nil { t.Fatal(err) } + req.Header.Add("Authorization", "Bearer "+nonAdminToken) res, err := client.Do(req) if err != nil { t.Fatal(err) } - resBody, err := io.ReadAll(res.Body) - res.Body.Close() + if res.StatusCode != tC.status { + t.Errorf("expected status code %d, got %d", tC.status, res.StatusCode) + } + }) + } +} + +func TestAuthorizationAdminAuthorized(t *testing.T) { + ts, _, err := setupServer() + if err != nil { + t.Fatalf("couldn't create test server: %s", err) + } + defer ts.Close() + client := ts.Client() + var adminToken string + var nonAdminToken string + t.Run("prepare user accounts and tokens", prepareAccounts(ts.URL, client, &adminToken, &nonAdminToken)) + + testCases := []struct { + desc string + method string + path string + status int + }{ + { + desc: "admin can see accounts", + method: "GET", + path: "/api/v1/accounts", + status: http.StatusOK, + }, + + { + desc: "admin can delete nonuser", + method: "DELETE", + path: "/api/v1/accounts/2", + status: http.StatusAccepted, + }, + } + for _, tC := range testCases { + t.Run(tC.desc, func(t *testing.T) { + req, err := http.NewRequest(tC.method, ts.URL+tC.path, strings.NewReader("")) if err != nil { t.Fatal(err) } - if res.StatusCode != tC.status || !strings.Contains(string(resBody), tC.response) { - t.Errorf("expected response did not match.\nExpected vs Received status code: %d vs %d\nExpected vs Received body: \n%s\nvs\n%s\n", tC.status, res.StatusCode, tC.response, string(resBody)) + req.Header.Add("Authorization", "Bearer "+adminToken) + res, err := client.Do(req) + if err != nil { + t.Fatal(err) } - if tC.desc == "Create no password user success" { - match, _ := regexp.MatchString(`"password":"[!-~]{16}"`, string(resBody)) - if !match { - t.Errorf("password does not match expected format or length: got %s", string(resBody)) - } + if res.StatusCode != tC.status { + t.Errorf("expected status code %d, got %d", tC.status, res.StatusCode) } }) } } + +func TestAuthorizationAdminUnAuthorized(t *testing.T) { + ts, _, err := setupServer() + if err != nil { + t.Fatalf("couldn't create test server: %s", err) + } + defer ts.Close() + client := ts.Client() + var adminToken string + var nonAdminToken string + t.Run("prepare user accounts and tokens", prepareAccounts(ts.URL, client, &adminToken, &nonAdminToken)) + + req, err := http.NewRequest("DELETE", ts.URL+"/api/v1/accounts/1", nil) + if err != nil { + t.Fatal(err) + } + req.Header.Add("Authorization", "Bearer "+nonAdminToken) + res, err := client.Do(req) + if err != nil { + t.Fatal(err) + } + if res.StatusCode != http.StatusForbidden { + t.Errorf("expected status code %d, got %d", http.StatusForbidden, res.StatusCode) + } +} diff --git a/internal/server/handlers_accounts.go b/internal/server/handlers_accounts.go new file mode 100644 index 00000000..9194325a --- /dev/null +++ b/internal/server/handlers_accounts.go @@ -0,0 +1,335 @@ +package server + +import ( + "crypto/rand" + "encoding/json" + "errors" + "log" + "math/big" + mrand "math/rand" + "net/http" + "regexp" + "strconv" + "strings" + + "github.com/canonical/notary/internal/db" +) + +type CreateAccountParams struct { + Username string `json:"username"` + Password string `json:"password"` +} + +type ChangeAccountParams struct { + Password string `json:"password"` +} + +type GetAccountResponse struct { + ID int `json:"id"` + Username string `json:"username"` + Permissions int `json:"permissions"` +} + +type CreateAccountResponse struct { + ID int `json:"id"` + Password string `json:"password"` +} + +type ChangeAccountResponse struct { + ID int `json:"id"` +} + +type DeleteAccountResponse struct { + ID int `json:"id"` +} + +func getRandomChars(charset string, length int) (string, error) { + result := make([]byte, length) + for i := range result { + n, err := rand.Int(rand.Reader, big.NewInt(int64(len(charset)))) + if err != nil { + return "", err + } + result[i] = charset[n.Int64()] + } + return string(result), nil +} + +// Generates a random 16 chars long password that contains uppercase and lowercase characters and numbers or symbols. +func generatePassword() (string, error) { + const ( + uppercaseSet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + lowercaseSet = "abcdefghijklmnopqrstuvwxyz" + numbersAndSymbolsSet = "0123456789*?@" + allCharsSet = uppercaseSet + lowercaseSet + numbersAndSymbolsSet + ) + uppercase, err := getRandomChars(uppercaseSet, 2) + if err != nil { + return "", err + } + lowercase, err := getRandomChars(lowercaseSet, 2) + if err != nil { + return "", err + } + numbersOrSymbols, err := getRandomChars(numbersAndSymbolsSet, 2) + if err != nil { + return "", err + } + allChars, err := getRandomChars(allCharsSet, 10) + if err != nil { + return "", err + } + res := []rune(uppercase + lowercase + numbersOrSymbols + allChars) + mrand.Shuffle(len(res), func(i, j int) { + res[i], res[j] = res[j], res[i] + }) + return string(res), nil +} + +func validatePassword(password string) bool { + if len(password) < 8 { + return false + } + hasCapital := regexp.MustCompile(`[A-Z]`).MatchString(password) + if !hasCapital { + return false + } + hasLower := regexp.MustCompile(`[a-z]`).MatchString(password) + if !hasLower { + return false + } + hasNumberOrSymbol := regexp.MustCompile(`[0-9!@#$%^&*()_+\-=\[\]{};':"|,.<>?~]`).MatchString(password) + + return hasNumberOrSymbol +} + +// ListAccounts returns all accounts from the database +func ListAccounts(env *HandlerConfig) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + accounts, err := env.DB.RetrieveAllUsers() + if err != nil { + log.Println(err) + writeError(w, http.StatusInternalServerError, "Internal Error") + return + } + accountsResponse := make([]GetAccountResponse, len(accounts)) + for i, account := range accounts { + accountsResponse[i] = GetAccountResponse{ + ID: account.ID, + Username: account.Username, + Permissions: account.Permissions, + } + } + w.WriteHeader(http.StatusOK) + err = writeJSON(w, accountsResponse) + if err != nil { + writeError(w, http.StatusInternalServerError, "internal error") + return + } + } +} + +// GetAccount receives an id as a path parameter, and +// returns the corresponding User Account +func GetAccount(env *HandlerConfig) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + id := r.PathValue("id") + var account db.User + var err error + if id == "me" { + claims, headerErr := getClaimsFromAuthorizationHeader(r.Header.Get("Authorization"), env.JWTSecret) + if headerErr != nil { + writeError(w, http.StatusUnauthorized, "Unauthorized") + } + account, err = env.DB.RetrieveUserByUsername(claims.Username) + } else { + account, err = env.DB.RetrieveUser(id) + } + if err != nil { + log.Println(err) + if errors.Is(err, db.ErrIdNotFound) { + writeError(w, http.StatusNotFound, "Not Found") + return + } + writeError(w, http.StatusInternalServerError, "Internal Error") + return + } + accountResponse := GetAccountResponse{ + ID: account.ID, + Username: account.Username, + Permissions: account.Permissions, + } + w.WriteHeader(http.StatusOK) + err = writeJSON(w, accountResponse) + if err != nil { + writeError(w, http.StatusInternalServerError, "internal error") + return + } + } +} + +// CreateAccount creates a new Account, and returns the id of the created row +func CreateAccount(env *HandlerConfig) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var createAccountParams CreateAccountParams + if err := json.NewDecoder(r.Body).Decode(&createAccountParams); err != nil { + writeError(w, http.StatusBadRequest, "Invalid JSON format") + return + } + if createAccountParams.Username == "" { + writeError(w, http.StatusBadRequest, "Username is required") + return + } + shouldGeneratePassword := createAccountParams.Password == "" + if shouldGeneratePassword { + generatedPassword, err := generatePassword() + if err != nil { + writeError(w, http.StatusInternalServerError, "Failed to generate password") + return + } + createAccountParams.Password = generatedPassword + } + if !validatePassword(createAccountParams.Password) { + writeError( + w, + http.StatusBadRequest, + "Password must have 8 or more characters, must include at least one capital letter, one lowercase letter, and either a number or a symbol.", + ) + return + } + numUsers, err := env.DB.NumUsers() + if err != nil { + writeError(w, http.StatusInternalServerError, "Failed to retrieve accounts: "+err.Error()) + return + } + + permission := UserPermission + if numUsers == 0 { + permission = AdminPermission + } + id, err := env.DB.CreateUser(createAccountParams.Username, createAccountParams.Password, permission) + if err != nil { + if strings.Contains(err.Error(), "UNIQUE constraint failed") { + writeError(w, http.StatusBadRequest, "account with given username already exists") + return + } + log.Println(err) + writeError(w, http.StatusInternalServerError, "Internal Error") + return + } + accountResponse := CreateAccountResponse{ + ID: int(id), + } + if shouldGeneratePassword { + accountResponse.Password = createAccountParams.Password + } + w.WriteHeader(http.StatusCreated) + err = writeJSON(w, accountResponse) + if err != nil { + writeError(w, http.StatusInternalServerError, "internal error") + return + } + } +} + +// DeleteAccount handler receives an id as a path parameter, +// deletes the corresponding User Account, and returns a http.StatusNoContent on success +func DeleteAccount(env *HandlerConfig) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + id := r.PathValue("id") + idInt, err := strconv.ParseInt(id, 10, 64) + if err != nil { + log.Println(err) + writeError(w, http.StatusInternalServerError, "Internal Error") + return + } + account, err := env.DB.RetrieveUser(id) + if err != nil { + if !errors.Is(err, db.ErrIdNotFound) { + log.Println(err) + writeError(w, http.StatusInternalServerError, "Internal Error") + return + } + } + if account.Permissions == 1 { + writeError(w, http.StatusBadRequest, "deleting an Admin account is not allowed.") + return + } + _, err = env.DB.DeleteUser(id) + if err != nil { + log.Println(err) + if errors.Is(err, db.ErrIdNotFound) { + writeError(w, http.StatusNotFound, "Not Found") + return + } + writeError(w, http.StatusInternalServerError, "Internal Error") + return + } + deleteAccountResponse := DeleteAccountResponse{ + ID: int(idInt), + } + w.WriteHeader(http.StatusAccepted) + err = writeJSON(w, deleteAccountResponse) + if err != nil { + writeError(w, http.StatusInternalServerError, "internal error") + return + } + } +} + +func ChangeAccountPassword(env *HandlerConfig) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + id := r.PathValue("id") + if id == "me" { + claims, err := getClaimsFromAuthorizationHeader(r.Header.Get("Authorization"), env.JWTSecret) + if err != nil { + log.Println(err) + writeError(w, http.StatusUnauthorized, "Unauthorized") + return + } + account, err := env.DB.RetrieveUserByUsername(claims.Username) + if err != nil { + log.Println(err) + writeError(w, http.StatusUnauthorized, "Unauthorized") + return + } + id = strconv.Itoa(account.ID) + } + var changeAccountParams ChangeAccountParams + if err := json.NewDecoder(r.Body).Decode(&changeAccountParams); err != nil { + writeError(w, http.StatusBadRequest, "Invalid JSON format") + return + } + if changeAccountParams.Password == "" { + writeError(w, http.StatusBadRequest, "Password is required") + return + } + if !validatePassword(changeAccountParams.Password) { + writeError( + w, + http.StatusBadRequest, + "Password must have 8 or more characters, must include at least one capital letter, one lowercase letter, and either a number or a symbol.", + ) + return + } + ret, err := env.DB.UpdateUser(id, changeAccountParams.Password) + if err != nil { + log.Println(err) + if errors.Is(err, db.ErrIdNotFound) { + writeError(w, http.StatusNotFound, "Not Found") + return + } + writeError(w, http.StatusInternalServerError, "Internal Error") + return + } + changeAccountResponse := ChangeAccountResponse{ + ID: int(ret), + } + w.WriteHeader(http.StatusCreated) + err = writeJSON(w, changeAccountResponse) + if err != nil { + writeError(w, http.StatusInternalServerError, "internal error") + return + } + } +} diff --git a/internal/server/handlers_accounts_test.go b/internal/server/handlers_accounts_test.go new file mode 100644 index 00000000..ef3dc223 --- /dev/null +++ b/internal/server/handlers_accounts_test.go @@ -0,0 +1,354 @@ +package server_test + +import ( + "encoding/json" + "net/http" + "strconv" + "strings" + "testing" +) + +type GetAccountResponseResult struct { + ID int `json:"id"` + Username string `json:"username"` + Permissions int `json:"permissions"` +} + +type GetAccountResponse struct { + Result GetAccountResponseResult `json:"result"` + Error string `json:"error,omitempty"` +} + +type CreateAccountParams struct { + Username string `json:"username"` + Password string `json:"password"` +} + +type CreateAccountResponseResult struct { + ID int `json:"id"` +} + +type CreateAccountResponse struct { + Result CreateAccountResponseResult `json:"result"` + Error string `json:"error,omitempty"` +} + +type ChangeAccountPasswordParams struct { + Password string `json:"password"` +} + +type ChangeAccountPasswordResponseResult struct { + ID int `json:"id"` +} + +type ChangeAccountPasswordResponse struct { + Result ChangeAccountPasswordResponseResult `json:"result"` + Error string `json:"error,omitempty"` +} + +type DeleteAccountResponseResult struct { + ID int `json:"id"` +} + +type DeleteAccountResponse struct { + Result DeleteAccountResponseResult `json:"result"` + Error string `json:"error,omitempty"` +} + +func getAccount(url string, client *http.Client, adminToken string, id int) (int, *GetAccountResponse, error) { + req, err := http.NewRequest("GET", url+"/api/v1/accounts/"+strconv.Itoa(id), nil) + if err != nil { + return 0, nil, err + } + req.Header.Set("Authorization", "Bearer "+adminToken) + res, err := client.Do(req) + if err != nil { + return 0, nil, err + } + defer res.Body.Close() + var accountResponse GetAccountResponse + if err := json.NewDecoder(res.Body).Decode(&accountResponse); err != nil { + return 0, nil, err + } + return res.StatusCode, &accountResponse, nil +} + +func createAccount(url string, client *http.Client, adminToken string, data *CreateAccountParams) (int, *CreateAccountResponse, error) { + body, err := json.Marshal(data) + if err != nil { + return 0, nil, err + } + req, err := http.NewRequest("POST", url+"/api/v1/accounts", strings.NewReader(string(body))) + if err != nil { + return 0, nil, err + } + req.Header.Set("Authorization", "Bearer "+adminToken) + res, err := client.Do(req) + if err != nil { + return 0, nil, err + } + defer res.Body.Close() + var createResponse CreateAccountResponse + if err := json.NewDecoder(res.Body).Decode(&createResponse); err != nil { + return 0, nil, err + } + return res.StatusCode, &createResponse, nil +} + +func changeAccountPassword(url string, client *http.Client, adminToken string, id int, data *ChangeAccountPasswordParams) (int, *ChangeAccountPasswordResponse, error) { + body, err := json.Marshal(data) + if err != nil { + return 0, nil, err + } + req, err := http.NewRequest("POST", url+"/api/v1/accounts/"+strconv.Itoa(id)+"/change_password", strings.NewReader(string(body))) + if err != nil { + return 0, nil, err + } + req.Header.Set("Authorization", "Bearer "+adminToken) + res, err := client.Do(req) + if err != nil { + return 0, nil, err + } + defer res.Body.Close() + var changeResponse ChangeAccountPasswordResponse + if err := json.NewDecoder(res.Body).Decode(&changeResponse); err != nil { + return 0, nil, err + } + return res.StatusCode, &changeResponse, nil +} + +func deleteAccount(url string, client *http.Client, adminToken string, id int) (int, *DeleteAccountResponse, error) { + req, err := http.NewRequest("DELETE", url+"/api/v1/accounts/"+strconv.Itoa(id), nil) + if err != nil { + return 0, nil, err + } + req.Header.Set("Authorization", "Bearer "+adminToken) + res, err := client.Do(req) + if err != nil { + return 0, nil, err + } + defer res.Body.Close() + var deleteResponse DeleteAccountResponse + if err := json.NewDecoder(res.Body).Decode(&deleteResponse); err != nil { + return 0, nil, err + } + return res.StatusCode, &deleteResponse, nil +} + +// This is an end-to-end test for the accounts handlers. +// The order of the tests is important, as some tests depend on +// the state of the server after previous tests. +func TestAccountsEndToEnd(t *testing.T) { + ts, _, err := setupServer() + if err != nil { + t.Fatalf("couldn't create test server: %s", err) + } + defer ts.Close() + client := ts.Client() + var adminToken string + var nonAdminToken string + t.Run("prepare accounts and tokens", prepareAccounts(ts.URL, client, &adminToken, &nonAdminToken)) + + t.Run("1. Get admin account - admin token", func(t *testing.T) { + statusCode, response, err := getAccount(ts.URL, client, adminToken, 1) + if err != nil { + t.Fatalf("couldn't get account: %s", err) + } + if statusCode != http.StatusOK { + t.Fatalf("expected status %d, got %d", http.StatusOK, statusCode) + } + if response.Error != "" { + t.Fatalf("expected error %q, got %q", "", response.Error) + } + if response.Result.ID != 1 { + t.Fatalf("expected ID 1, got %d", response.Result.ID) + } + if response.Result.Username != "testadmin" { + t.Fatalf("expected username testadmin, got %s", response.Result.Username) + } + if response.Result.Permissions != 1 { + t.Fatalf("expected permissions 1, got %d", response.Result.Permissions) + } + }) + + t.Run("2. Get admin account - non admin token", func(t *testing.T) { + statusCode, response, err := getAccount(ts.URL, client, nonAdminToken, 1) + if err != nil { + t.Fatalf("couldn't get account: %s", err) + } + if statusCode != http.StatusForbidden { + t.Fatalf("expected status %d, got %d", http.StatusForbidden, statusCode) + } + if response.Error != "forbidden: admin access required" { + t.Fatalf("expected error %q, got %q", "forbidden: admin access required", response.Error) + } + }) + + t.Run("3. Create account - no password", func(t *testing.T) { + createAccountParams := &CreateAccountParams{ + Username: "nopass", + Password: "", + } + statusCode, response, err := createAccount(ts.URL, client, adminToken, createAccountParams) + if err != nil { + t.Fatalf("couldn't create account: %s", err) + } + if statusCode != http.StatusCreated { + t.Fatalf("expected status %d, got %d", http.StatusCreated, statusCode) + } + if response.Error != "" { + t.Fatalf("expected error %q, got %q", "", response.Error) + } + if response.Result.ID != 3 { + t.Fatalf("expected ID 3, got %d", response.Result.ID) + } + }) + + t.Run("4. Get account", func(t *testing.T) { + statusCode, response, err := getAccount(ts.URL, client, adminToken, 3) + if err != nil { + t.Fatalf("couldn't get account: %s", err) + } + if statusCode != http.StatusOK { + t.Fatalf("expected status %d, got %d", http.StatusOK, statusCode) + } + if response.Error != "" { + t.Fatalf("expected error %q, got %q", "", response.Error) + } + if response.Result.ID != 3 { + t.Fatalf("expected ID 3, got %d", response.Result.ID) + } + if response.Result.Username != "nopass" { + t.Fatalf("expected username nopass, got %s", response.Result.Username) + } + if response.Result.Permissions != 0 { + t.Fatalf("expected permissions 0, got %d", response.Result.Permissions) + } + }) + + t.Run("5. Get account - id not found", func(t *testing.T) { + statusCode, response, err := getAccount(ts.URL, client, adminToken, 100) + if err != nil { + t.Fatalf("couldn't get account: %s", err) + } + if statusCode != http.StatusNotFound { + t.Fatalf("expected status %d, got %d", http.StatusNotFound, statusCode) + } + if response.Error != "Not Found" { + t.Fatalf("expected error %q, got %q", "Not Found", response.Error) + } + }) + + t.Run("6. Create account - no username", func(t *testing.T) { + createAccountParams := &CreateAccountParams{ + Username: "", + Password: "password", + } + statusCode, response, err := createAccount(ts.URL, client, adminToken, createAccountParams) + if err != nil { + t.Fatalf("couldn't create account: %s", err) + } + if statusCode != http.StatusBadRequest { + t.Fatalf("expected status %d, got %d", http.StatusBadRequest, statusCode) + } + if response.Error != "Username is required" { + t.Fatalf("expected error %q, got %q", "Username is required", response.Error) + } + }) + + t.Run("7. Change account password - success", func(t *testing.T) { + changeAccountPasswordParams := &ChangeAccountPasswordParams{ + Password: "newPassword1", + } + statusCode, response, err := changeAccountPassword(ts.URL, client, adminToken, 1, changeAccountPasswordParams) + if err != nil { + t.Fatalf("couldn't create account: %s", err) + } + if statusCode != http.StatusCreated { + t.Fatalf("expected status %d, got %d", http.StatusCreated, statusCode) + } + if response.Error != "" { + t.Fatalf("expected error %q, got %q", "", response.Error) + } + if response.Result.ID != 1 { + t.Fatalf("expected ID 1, got %d", response.Result.ID) + } + }) + + t.Run("8. Change account password - no user", func(t *testing.T) { + changeAccountPasswordParams := &ChangeAccountPasswordParams{ + Password: "newPassword1", + } + statusCode, response, err := changeAccountPassword(ts.URL, client, adminToken, 100, changeAccountPasswordParams) + if err != nil { + t.Fatalf("couldn't create account: %s", err) + } + if statusCode != http.StatusNotFound { + t.Fatalf("expected status %d, got %d", http.StatusNotFound, statusCode) + } + if response.Error != "Not Found" { + t.Fatalf("expected error %q, got %q", "Not Found", response.Error) + } + }) + + t.Run("9. Change account password - no password", func(t *testing.T) { + changeAccountPasswordParams := &ChangeAccountPasswordParams{ + Password: "", + } + statusCode, response, err := changeAccountPassword(ts.URL, client, adminToken, 1, changeAccountPasswordParams) + if err != nil { + t.Fatalf("couldn't create account: %s", err) + } + if statusCode != http.StatusBadRequest { + t.Fatalf("expected status %d, got %d", http.StatusBadRequest, statusCode) + } + if response.Error != "Password is required" { + t.Fatalf("expected error %q, got %q", "Password is required", response.Error) + } + }) + + t.Run("10. Change account password - bad password", func(t *testing.T) { + changeAccountPasswordParams := &ChangeAccountPasswordParams{ + Password: "password", + } + statusCode, response, err := changeAccountPassword(ts.URL, client, adminToken, 1, changeAccountPasswordParams) + if err != nil { + t.Fatalf("couldn't create account: %s", err) + } + if statusCode != http.StatusBadRequest { + t.Fatalf("expected status %d, got %d", http.StatusBadRequest, statusCode) + } + if response.Error != "Password must have 8 or more characters, must include at least one capital letter, one lowercase letter, and either a number or a symbol." { + t.Fatalf("expected error %q, got %q", "Password must have 8 or more characters, must include at least one capital letter, one lowercase letter, and either a number or a symbol.", response.Error) + } + }) + + t.Run("11. Delete account - success", func(t *testing.T) { + statusCode, response, err := deleteAccount(ts.URL, client, adminToken, 2) + if err != nil { + t.Fatalf("couldn't delete account: %s", err) + } + if statusCode != http.StatusAccepted { + t.Fatalf("expected status %d, got %d", http.StatusAccepted, statusCode) + } + if response.Error != "" { + t.Fatalf("expected error %q, got %q", "", response.Error) + } + if response.Result.ID != 2 { + t.Fatalf("expected ID 2, got %d", response.Result.ID) + } + }) + + t.Run("12. Delete account - no user", func(t *testing.T) { + statusCode, response, err := deleteAccount(ts.URL, client, adminToken, 100) + if err != nil { + t.Fatalf("couldn't delete account: %s", err) + } + if statusCode != http.StatusNotFound { + t.Fatalf("expected status %d, got %d", http.StatusNotFound, statusCode) + } + if response.Error != "Not Found" { + t.Fatalf("expected error %q, got %q", "Not Found", response.Error) + } + }) +} diff --git a/internal/server/handlers_certificate_requests.go b/internal/server/handlers_certificate_requests.go index a87cb084..b66d051a 100644 --- a/internal/server/handlers_certificate_requests.go +++ b/internal/server/handlers_certificate_requests.go @@ -3,7 +3,6 @@ package server import ( "encoding/json" "errors" - "io" "log" "net/http" "strconv" @@ -12,75 +11,131 @@ import ( "github.com/canonical/notary/internal/db" ) -// GetCertificateRequests returns all of the Certificate Requests -func GetCertificateRequests(env *HandlerConfig) http.HandlerFunc { +type CreateCertificateRequestParams struct { + CSR string `json:"csr"` +} + +type CreateCertificateParams struct { + Certificate string `json:"certificate"` +} + +type GetCertificateRequestResponse struct { + ID int `json:"id"` + CSR string `json:"csr"` + Certificate string `json:"certificate"` +} + +type CreateCertificateRequestResponse struct { + ID int `json:"id"` +} + +type DeleteCertificateRequestResponse struct { + ID int `json:"id"` +} + +type RejectCertificateRequestResponse struct { + ID int `json:"id"` +} + +type CreateCertificateResponse struct { + ID int `json:"id"` +} + +type DeleteCertificateResponse struct { + ID int `json:"id"` +} + +type RejectCertificateResponse struct { + ID int `json:"id"` +} + +// ListCertificateRequests returns all of the Certificate Requests +func ListCertificateRequests(env *HandlerConfig) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { certs, err := env.DB.RetrieveAllCSRs() if err != nil { - writeError(err.Error(), http.StatusInternalServerError, w) + log.Println(err) + writeError(w, http.StatusInternalServerError, "Internal Error") return } - body, err := json.Marshal(certs) + certificateRequestsResponse := make([]GetCertificateRequestResponse, len(certs)) + for i, cert := range certs { + certificateRequestsResponse[i] = GetCertificateRequestResponse{ + ID: cert.ID, + CSR: cert.CSR, + Certificate: cert.Certificate, + } + } + w.WriteHeader(http.StatusOK) + err = writeJSON(w, certificateRequestsResponse) if err != nil { - writeError(err.Error(), http.StatusInternalServerError, w) + writeError(w, http.StatusInternalServerError, "internal error") return } - if _, err := w.Write(body); err != nil { - writeError(err.Error(), http.StatusInternalServerError, w) - } } } -// PostCertificateRequest creates a new Certificate Request, and returns the id of the created row -func PostCertificateRequest(env *HandlerConfig) http.HandlerFunc { +// CreateCertificateRequest creates a new Certificate Request, and returns the id of the created row +func CreateCertificateRequest(env *HandlerConfig) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - csr, err := io.ReadAll(r.Body) - if err != nil { - writeError(err.Error(), http.StatusInternalServerError, w) + var createCertificateRequestParams CreateCertificateRequestParams + if err := json.NewDecoder(r.Body).Decode(&createCertificateRequestParams); err != nil { + writeError(w, http.StatusBadRequest, "Invalid JSON format") return } - id, err := env.DB.CreateCSR(string(csr)) + id, err := env.DB.CreateCSR(createCertificateRequestParams.CSR) if err != nil { if strings.Contains(err.Error(), "UNIQUE constraint failed") { - writeError("given csr already recorded", http.StatusBadRequest, w) + writeError(w, http.StatusBadRequest, "given csr already recorded") return } if strings.Contains(err.Error(), "csr validation failed") { - writeError(err.Error(), http.StatusBadRequest, w) + log.Println(err) + writeError(w, http.StatusBadRequest, "csr validation failed") return } - writeError(err.Error(), http.StatusInternalServerError, w) + log.Println(err) + writeError(w, http.StatusInternalServerError, "Internal Error") return } + certificateRequestResponse := CreateCertificateRequestResponse{ + ID: int(id), + } w.WriteHeader(http.StatusCreated) - if _, err := w.Write([]byte(strconv.FormatInt(id, 10))); err != nil { - writeError(err.Error(), http.StatusInternalServerError, w) + err = writeJSON(w, certificateRequestResponse) + if err != nil { + writeError(w, http.StatusInternalServerError, "internal error") + return } } } -// GetCertificateRequests receives an id as a path parameter, and +// GetCertificateRequest receives an id as a path parameter, and // returns the corresponding Certificate Request func GetCertificateRequest(env *HandlerConfig) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") cert, err := env.DB.RetrieveCSR(id) if err != nil { + log.Println(err) if errors.Is(err, db.ErrIdNotFound) { - writeError(err.Error(), http.StatusNotFound, w) + writeError(w, http.StatusNotFound, "Not Found") return } - writeError(err.Error(), http.StatusInternalServerError, w) + writeError(w, http.StatusInternalServerError, "Internal Error") return } - body, err := json.Marshal(cert) + certificateRequestResponse := GetCertificateRequestResponse{ + ID: cert.ID, + CSR: cert.CSR, + Certificate: cert.Certificate, + } + w.WriteHeader(http.StatusOK) + err = writeJSON(w, certificateRequestResponse) if err != nil { - writeError(err.Error(), http.StatusInternalServerError, w) + writeError(w, http.StatusInternalServerError, "internal error") return } - if _, err := w.Write(body); err != nil { - writeError(err.Error(), http.StatusInternalServerError, w) - } } } @@ -91,39 +146,46 @@ func DeleteCertificateRequest(env *HandlerConfig) http.HandlerFunc { id := r.PathValue("id") insertId, err := env.DB.DeleteCSR(id) if err != nil { + log.Println(err) if errors.Is(err, db.ErrIdNotFound) { - writeError(err.Error(), http.StatusNotFound, w) + writeError(w, http.StatusNotFound, "Not Found") return } - writeError(err.Error(), http.StatusInternalServerError, w) + writeError(w, http.StatusInternalServerError, "Internal Error") return } + certificateRequestResponse := DeleteCertificateRequestResponse{ + ID: int(insertId), + } w.WriteHeader(http.StatusAccepted) - if _, err := w.Write([]byte(strconv.FormatInt(insertId, 10))); err != nil { - writeError(err.Error(), http.StatusInternalServerError, w) + err = writeJSON(w, certificateRequestResponse) + if err != nil { + writeError(w, http.StatusInternalServerError, "internal error") + return } } } -// PostCertificate handler receives an id as a path parameter, +// CreateCertificate handler receives an id as a path parameter, // and attempts to add a given certificate to the corresponding certificate request -func PostCertificate(env *HandlerConfig) http.HandlerFunc { +func CreateCertificate(env *HandlerConfig) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - cert, err := io.ReadAll(r.Body) - if err != nil { - writeError(err.Error(), http.StatusBadRequest, w) + var createCertificateParams CreateCertificateParams + if err := json.NewDecoder(r.Body).Decode(&createCertificateParams); err != nil { + writeError(w, http.StatusBadRequest, "Invalid JSON format") return } id := r.PathValue("id") - insertId, err := env.DB.UpdateCSR(id, string(cert)) + insertId, err := env.DB.UpdateCSR(id, createCertificateParams.Certificate) if err != nil { + log.Println(err) if errors.Is(err, db.ErrIdNotFound) || err.Error() == "certificate does not match CSR" || strings.Contains(err.Error(), "cert validation failed") { - writeError(err.Error(), http.StatusBadRequest, w) + writeError(w, http.StatusBadRequest, "Bad Request") return } - writeError(err.Error(), http.StatusInternalServerError, w) + writeError(w, http.StatusInternalServerError, "Internal Error") return } insertIdStr := strconv.FormatInt(insertId, 10) @@ -133,9 +195,14 @@ func PostCertificate(env *HandlerConfig) http.HandlerFunc { log.Printf("pebble notify failed: %s. continuing silently.", err.Error()) } } + certificateResponse := CreateCertificateResponse{ + ID: int(insertId), + } w.WriteHeader(http.StatusCreated) - if _, err := w.Write([]byte(insertIdStr)); err != nil { - writeError(err.Error(), http.StatusInternalServerError, w) + err = writeJSON(w, certificateResponse) + if err != nil { + writeError(w, http.StatusInternalServerError, "internal error") + return } } } @@ -145,11 +212,12 @@ func RejectCertificate(env *HandlerConfig) http.HandlerFunc { id := r.PathValue("id") insertId, err := env.DB.UpdateCSR(id, "rejected") if err != nil { + log.Println(err) if errors.Is(err, db.ErrIdNotFound) { - writeError(err.Error(), http.StatusNotFound, w) + writeError(w, http.StatusNotFound, "Not Found") return } - writeError(err.Error(), http.StatusInternalServerError, w) + writeError(w, http.StatusInternalServerError, "Internal Error") return } insertIdStr := strconv.FormatInt(insertId, 10) @@ -159,9 +227,14 @@ func RejectCertificate(env *HandlerConfig) http.HandlerFunc { log.Printf("pebble notify failed: %s. continuing silently.", err.Error()) } } + certificateResponse := RejectCertificateResponse{ + ID: int(insertId), + } w.WriteHeader(http.StatusAccepted) - if _, err := w.Write([]byte(insertIdStr)); err != nil { - writeError(err.Error(), http.StatusInternalServerError, w) + err = writeJSON(w, certificateResponse) + if err != nil { + writeError(w, http.StatusInternalServerError, "internal error") + return } } } @@ -173,11 +246,12 @@ func DeleteCertificate(env *HandlerConfig) http.HandlerFunc { id := r.PathValue("id") insertId, err := env.DB.UpdateCSR(id, "") if err != nil { + log.Println(err) if errors.Is(err, db.ErrIdNotFound) { - writeError(err.Error(), http.StatusBadRequest, w) + writeError(w, http.StatusBadRequest, "Bad Request") return } - writeError(err.Error(), http.StatusInternalServerError, w) + writeError(w, http.StatusInternalServerError, "Internal Error") return } insertIdStr := strconv.FormatInt(insertId, 10) @@ -187,9 +261,14 @@ func DeleteCertificate(env *HandlerConfig) http.HandlerFunc { log.Printf("pebble notify failed: %s. continuing silently.", err.Error()) } } - w.WriteHeader(http.StatusAccepted) - if _, err := w.Write([]byte(insertIdStr)); err != nil { - writeError(err.Error(), http.StatusInternalServerError, w) + certificateResponse := DeleteCertificateResponse{ + ID: int(insertId), + } + w.WriteHeader(http.StatusOK) + err = writeJSON(w, certificateResponse) + if err != nil { + writeError(w, http.StatusInternalServerError, "internal error") + return } } } diff --git a/internal/server/handlers_certificate_requests_test.go b/internal/server/handlers_certificate_requests_test.go index 2a8a27f7..1313f453 100644 --- a/internal/server/handlers_certificate_requests_test.go +++ b/internal/server/handlers_certificate_requests_test.go @@ -1,400 +1,510 @@ package server_test import ( + "bytes" + "encoding/json" "fmt" - "io" - "log" "net/http" - "net/http/httptest" - "strings" + "os" + "path/filepath" + "strconv" "testing" - - "github.com/canonical/notary/internal/db" - "github.com/canonical/notary/internal/server" ) -const ( - AppleCSR = `-----BEGIN CERTIFICATE REQUEST----- -MIICsTCCAZkCAQAwbDELMAkGA1UEBhMCQ0ExFDASBgNVBAgMC05vdmEgU2NvdGlh -MRAwDgYDVQQHDAdIYWxpZmF4MSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0 -eSBMdGQxEjAQBgNVBAMMCWFwcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEP -ADCCAQoCggEBAOhDSpNbeFiXMQzQcobExHqYMEGzqpX8N9+AR6/HPZWBybgx1hr3 -ejqsKornzpVph/dO9UC7O9aBlG071O9VQGHt3OU3rkZIk2009vYwLuSrAlJtnUne -p7KKn2lZGvh7jVyZE5RkS0X27vlT0soANsmcVq/82VneHrF/nbDcK6DOjQpS5o5l -EiNk2CIpYGUkw3WnQF4pBk8t4bNOl3nfpaAOfnmNuBX3mWyfPnaKMCENMpDqL9FR -V/O5bIPLmyH30OHUEJUkWOmFt9GFi+QfMoM0fR34KmRbDz79hZZb/yVPZZJl7l6i -FWXkNR3gxdEnwCZkTgWk5OqS9dCJOtsDE8ECAwEAAaAAMA0GCSqGSIb3DQEBCwUA -A4IBAQCqBX5WaNv/HjkzAyNXYuCToCb8GjmiMqL54t+1nEI1QTm6axQXivEbQT3x -GIh7uQYC06wHE23K6Znc1/G+o3y6lID07rvhBNal1qoXUiq6CsAqk+DXYdd8MEh5 -joerEedFqcW+WTUDcqddfIyDAGPqrM9j6/E+aFYyZjJ/xRuMf1zlWMljRiwj1NI9 -NxqjsYYQ3zxfUjv8gxXm0hN8Up1O9saoEF+zbuWNdiUWd6Ih3/3u5VBNSxgVOrDQ -CeXyyzkMx1pWTx0rWa7NSa+DMKVVzv46pck/9kLB4gPL8zqvIOMQsf74N0VcbVfd -9jQR8mPXQYPUERl1ZhNrkzkyA0kd ------END CERTIFICATE REQUEST-----` - BananaCSR = `-----BEGIN CERTIFICATE REQUEST----- -MIICrjCCAZYCAQAwaTELMAkGA1UEBhMCVFIxDjAMBgNVBAgMBUl6bWlyMRIwEAYD -VQQHDAlOYXJsaWRlcmUxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0 -ZDETMBEGA1UEAwwKYmFuYW5hLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC -AQoCggEBAK+vJMxO1GTty09/E4M/RbTCPABleCuYc/uzj72KWaIvoDaanuJ4NBWM -2aUiepxWdMNTR6oe31gLq4agLYT309tXwCeBLQnOxvBFWONmBG1qo0fQkvT5kSoq -AO29D7hkQ0gVwg7EF3qOd0JgbDm/yvexKpYLVvWMQAngHwZRnd5vHGk6M3P7G4oG -mIj/CL2bF6va7GWODYHb+a7jI1nkcsrk+vapc+doVszcoJ+2ryoK6JndOSGjt9SD -uxulWZHQO32XC0btyub63pom4QxRtRXmb1mjM37XEwXJSsQO1HOnmc6ycqUK53p0 -jF8Qbs0m8y/p2NHFGTUfiyNYA3EdkjUCAwEAAaAAMA0GCSqGSIb3DQEBCwUAA4IB -AQA+hq8kS2Y1Y6D8qH97Mnnc6Ojm61Q5YJ4MghaTD+XXbueTCx4DfK7ujYzK3IEF -pH1AnSeJCsQeBdjT7p6nv5GcwqWXWztNKn9zibXiASK/yYKwqvQpjSjSeqGEh+Sa -9C9SHeaPhZrJRj0i3NkqmN8moWasF9onW6MNKBX0B+pvBB+igGPcjCIFIFGUUaky -upMXY9IG3LlWvlt+HTfuMZV+zSOZgD9oyqkh5K9XRKNq/mnNz/1llUCBZRmfeRBY -+sJ4M6MJRztiyX4/Fjb8UHQviH931rkiEGtG826IvWIyiRSnAeE8B/VzL0GlT9Zq -ge6lFRxB1FlDuU4Blef8FnOI ------END CERTIFICATE REQUEST-----` - StrawberryCSR = `-----BEGIN CERTIFICATE REQUEST----- -MIICrzCCAZcCAQAwajELMAkGA1UEBhMCSVQxDzANBgNVBAgMBlBhZG92YTEOMAwG -A1UEBwwFUGFkdWExITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEX -MBUGA1UEAwwOc3RyYXdiZXJyeS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw -ggEKAoIBAQDXXHpy+3LLRCImyEQitM9eUdgYkexLz2PcAf89tTpkpt3L1woJw0bv -+YR80UcR2Pg+7uUVm4XSKFvcdyWg8yADHIDDZkEmKFEbrOLUsWWTQEsCpFt5MU4u -6YnYXV0YflPXmRsJRd90NOen+wlM2ajK1gGTtLPdJ6axz15LdcT2uXXIvWhncjgL -CvVpd/x44AMxD/BPf/d27VO5hEjxR//DtcOmS/jA+Zf1+dyIAWs2LH+ctsaPLOcg -1rBiRrHtGL8wmPwgwK9b+QLiq9Ik+dx1Jl6BvC36LRk2CxTxfZ6e4UdYVhtnjMW2 -VEUAVg9LtowvXTexESUv6Mh4uQF6pW5ZAgMBAAGgADANBgkqhkiG9w0BAQsFAAOC -AQEAW40HaxjVSDNKeWJ8StWGfstdvk3dwqjsfLgmnBBZSLcGppYEnnRlJxhMJ9Ks -x2IYw7wJ55kOJ7V+SunKPPoY+7PwNDV9Llxp58vvE8CFnOc3WcL9pA2V5LbTXwtT -R7jID5GZjOv0bn3x1WXuKVW5tkYdT6sW14rfGut1T+r1kYls+JQ5ap+BzfMtThZz -38PCnEMmSo0/KmgUu5/LakPoy3JPaFB0bCgViZSWlxiSR44YZPsVaRL8E7Zt/qjJ -glRL/48q/tORtxv18/Girl6oiQholkADaH3j2gB3t/fCLp8guAVLWB9DzhwrqWwP -GFl9zB5HDoij2l0kHrb44TuonQ== ------END CERTIFICATE REQUEST-----` - BananaCert = `-----BEGIN CERTIFICATE----- -MIIEUTCCAjkCFE8lmuBE85/RPw2M17Kzl93O+9IIMA0GCSqGSIb3DQEBCwUAMGEx -CzAJBgNVBAYTAlRSMQ4wDAYDVQQIDAVJem1pcjESMBAGA1UEBwwJTmFybGlkZXJl -MSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxCzAJBgNVBAMMAm1l -MB4XDTI0MDYyODA4NDIyMFoXDTI1MDYyODA4NDIyMFowaTELMAkGA1UEBhMCVFIx -DjAMBgNVBAgMBUl6bWlyMRIwEAYDVQQHDAlOYXJsaWRlcmUxITAfBgNVBAoMGElu -dGVybmV0IFdpZGdpdHMgUHR5IEx0ZDETMBEGA1UEAwwKYmFuYW5hLmNvbTCCASIw -DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK+vJMxO1GTty09/E4M/RbTCPABl -eCuYc/uzj72KWaIvoDaanuJ4NBWM2aUiepxWdMNTR6oe31gLq4agLYT309tXwCeB -LQnOxvBFWONmBG1qo0fQkvT5kSoqAO29D7hkQ0gVwg7EF3qOd0JgbDm/yvexKpYL -VvWMQAngHwZRnd5vHGk6M3P7G4oGmIj/CL2bF6va7GWODYHb+a7jI1nkcsrk+vap -c+doVszcoJ+2ryoK6JndOSGjt9SDuxulWZHQO32XC0btyub63pom4QxRtRXmb1mj -M37XEwXJSsQO1HOnmc6ycqUK53p0jF8Qbs0m8y/p2NHFGTUfiyNYA3EdkjUCAwEA -ATANBgkqhkiG9w0BAQsFAAOCAgEAVZJZD0/ojZSOVIesZvrjLG0agSp0tsXY+hEt -I/knpYLvRcAd8b3Jx9gk+ug+FwDQ4IBIkTX18qhK2fgVUuMR/ubfpQeCMbp64N3Q -kmN/E1eu0bl6hhHAL7jEbi0DE3vAN9huQxAIu5pCyLvZIrPJtvuyj2jOpJBZwGoP -539lfEM++XALzI4qKQ6Z0a0rJZ4HoruKiYwEFZ7VkmRLD0uef6NMZRqa/Vx+o0uT -1TjH4AeDDmJmP/aHlHbpXkHQ9h9rfTa6Qbypo+T9pGDhd02O1tEqrHfiQyNWJxb0 -rbR+owT32iCfayzKKqhmAYSF2d9XKWEhulgxWDaXgvUbq4Y+fgfU2qMVz5uusTDh -a9Mp9dsYWySWEUcEa4v2w6FfaaVXE1S9ubm+HoIVtotuutL5fn86q19pAAePYjLQ -ybiETp5LU3chuYmMlCiDRNGHYhN5nvGcttqRdWIBe454RRPNo4iGVl13l6aG8rmI -xDfk5lIwObalbELv+mEIGI1j/j4//nJFXByxlLHm5/BF8rmvHDj1aPtPRw9DLgSX -ejhjjec1xnkBR+JF0g474hLdPjCnA0aqLQInZbjJJm5iXzyXBg1cy7KvIBy3ZkrR -Pp7ObjaWxjCT3O6nEH3w6Ozsyg2cHXQIdVXLvNnV1bxUbPnfhQosKGKgU6s+lcLM -SRhHB2k= ------END CERTIFICATE-----` - IssuerCert = `-----BEGIN CERTIFICATE----- -MIIFozCCA4ugAwIBAgIUDjtO3bEluUX3tzvrckATlycRVfwwDQYJKoZIhvcNAQEL -BQAwYTELMAkGA1UEBhMCVFIxDjAMBgNVBAgMBUl6bWlyMRIwEAYDVQQHDAlOYXJs -aWRlcmUxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDELMAkGA1UE -AwwCbWUwHhcNMjQwNjI4MDYwNTQ5WhcNMzQwNjI2MDYwNTQ5WjBhMQswCQYDVQQG -EwJUUjEOMAwGA1UECAwFSXptaXIxEjAQBgNVBAcMCU5hcmxpZGVyZTEhMB8GA1UE -CgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMQswCQYDVQQDDAJtZTCCAiIwDQYJ -KoZIhvcNAQEBBQADggIPADCCAgoCggIBAJU+5YaFlpn+bWvVri5L6EkmbAPuavsI -/KXY7ufRmc5qb08o1na9lLJ/7TuMD4K36Idnq20n1JohSlrdymBpNZ8O3m5fYYtk -hx5WADlBZsKnC5aZJIChEb4bYcOFLP+d3PooVsAKBxW0Q6TECviQcK7GxaxEZw0L -7FRhX2c9+CxbvRGP6OGVggXZxwkZik/JJ9aym+fltt9QvlxQVBq/GlFYZYC+H8jV -Z6RnUjugnWcTm9PAsQ6+EHEevAW+dWaDP+gr9AgKKz1EXbc1mVKAVOLHjb+Ue7RC -vFoar/YxYIszD58dOSB/GuAxn+JAjWbnOu7jeX3XeWlKOagUJF9L9TgMIUWdiuJG -8Uu/kK2MjyRFdT8opnPFAXrK7vSuMBzhRtswAlWc8xoZWeSQF+NpjU+swbg8ySYT -LfZxVB+s/ftxnGU3RM/RWdbZhb0DAuIBsFAGCbnj+Q61/cK4i58JVjUqzLk+XOwR -55LAyS0Y5pj9jDc5mqvS0z7ot7s2OBM1+o8e3KJgdMSXorYkv3toHMGEIUmPQZCX -JtRCjFNgnoWeLDc+oLiN6BlPx7bS4MDN9tMPCJwF6vnxFzLAzdRqY3D7uRS3chsx -7ClMR9MDsSxplC7tptXgv8UTzh1XZjWGCeZq0Gbe927Hmwy2q8k/BFwnR4PIVSiE -7YAZPb0CPmrfAgMBAAGjUzBRMB0GA1UdDgQWBBRgLXukRHTovOG6g9Z5eCaeh6Sx -aTAfBgNVHSMEGDAWgBRgLXukRHTovOG6g9Z5eCaeh6SxaTAPBgNVHRMBAf8EBTAD -AQH/MA0GCSqGSIb3DQEBCwUAA4ICAQA9TpgTrGmnyxKB2ne76LNQadiijVPpS6/U -OPFAX4EPJ0V5DhDreJjsZJC6Is2Q9+qsPpn/nlW7bvZUVHGodUKcE+TQWFiMtLvu -8ifzk8x1R46aqhTyxb7WBBFfvbvdmlEENKTmTS6A/C3nYgmkfk5N7x84iTowmsVl -Yzz9iRzxkqQ+mU3L2/Sp5nXPYWfzV9WXIJdxWcot7f4CJ79eVFu4D9hYfzcPQ9P9 -0qCBRbH/01D2E/3uTHhZPPmK2Tp1ao5SuGLppjMPX8VWVL5CMTXOj+1LF0nJJc/J -9MrqXwtlLyKGP6HX8qALbaXwcv7db6bF+aEsgWmIEB+0ecGk9IR3XQn7I379CO3v -J2oUCZ++lV9e2tcRehUprE1v8i+DFhPtS1iNjrO7KnDYkXimR5zI+3sGFI9/9wY0 -4PAV/roZFiEJHe5kA49vwIihJaDgy/SPIYgG/vhdj+WeIbi1ilEi12ou7VF0tyiE -j3eXaMAL8EAKxCUZbXcuwmK9qistAYXBFFEK9M08FwLH8HM4LoPjshMg3II9Ncs8 -p3to8U99/ZeFbJRzEUF9poZ7VwxBEcgfWD1RV0+gNLC3Au2yuc4C3anknOv7Db/r -jdzVA8yTI8cZ/RtRohp5H/s+j2tcdfB3Zt+wfS4nLxqN/kf7qv2VSdPbXyTyz/ft -btZkbfdL5A== ------END CERTIFICATE----- -` -) +type CertificateRequest struct { + ID int `json:"id"` + CSR string `json:"csr"` + Certificate string `json:"certificate"` +} -var ( - expectedGetAllCertsResponseBody1 = fmt.Sprintf("[{\"id\":1,\"csr\":\"%s\",\"certificate\":\"\"}]", trimmed(AppleCSR)) - expectedGetAllCertsResponseBody2 = fmt.Sprintf("[{\"id\":1,\"csr\":\"%s\",\"certificate\":\"\"},{\"id\":2,\"csr\":\"%s\",\"certificate\":\"\"}]", trimmed(AppleCSR), trimmed(BananaCSR)) - expectedGetAllCertsResponseBody3 = fmt.Sprintf("[{\"id\":2,\"csr\":\"%s\",\"certificate\":\"%s\\n%s\"},{\"id\":3,\"csr\":\"%s\",\"certificate\":\"\"},{\"id\":4,\"csr\":\"%s\",\"certificate\":\"rejected\"}]", trimmed(BananaCSR), trimmed(BananaCert), trimmed(IssuerCert), trimmed(StrawberryCSR), trimmed(AppleCSR)) - expectedGetAllCertsResponseBody4 = fmt.Sprintf("[{\"id\":2,\"csr\":\"%s\",\"certificate\":\"\"},{\"id\":3,\"csr\":\"%s\",\"certificate\":\"\"},{\"id\":4,\"csr\":\"%s\",\"certificate\":\"rejected\"}]", trimmed(BananaCSR), trimmed(StrawberryCSR), trimmed(AppleCSR)) - expectedGetCertReqResponseBody1 = fmt.Sprintf("{\"id\":2,\"csr\":\"%s\",\"certificate\":\"\"}", trimmed(BananaCSR)) - expectedGetCertReqResponseBody2 = fmt.Sprintf("{\"id\":4,\"csr\":\"%s\",\"certificate\":\"\"}", trimmed(AppleCSR)) - expectedGetCertReqResponseBody3 = fmt.Sprintf("{\"id\":2,\"csr\":\"%s\",\"certificate\":\"%s\\n%s\"}", trimmed(BananaCSR), trimmed(BananaCert), trimmed(IssuerCert)) - expectedGetCertReqResponseBody4 = fmt.Sprintf("{\"id\":2,\"csr\":\"%s\",\"certificate\":\"\"}", trimmed(BananaCSR)) -) +type GetCertificateRequestResponse struct { + Result CertificateRequest `json:"result"` + Error string `json:"error,omitempty"` +} + +type ListCertificateRequestsResponse struct { + Error string `json:"error,omitempty"` + Result []CertificateRequest `json:"result"` +} + +type CreateCertificateRequestResponse struct { + ID int `json:"id"` + Error string `json:"error,omitempty"` +} -func TestNotaryCertificatesHandlers(t *testing.T) { - testdb, err := db.NewDatabase(":memory:") +type CreateCertificateRequestParams struct { + CSR string `json:"csr"` +} + +type CreateCertificateParams struct { + Certificate string `json:"certificate"` +} + +type GetCertificateResponseResult struct { + Certificate string `json:"certificate"` +} + +type GetCertificateResponse struct { + Result GetCertificateResponseResult `json:"result"` + Error string `json:"error,omitempty"` +} + +type CreateCertificateResponse struct { + ID int `json:"id"` + Error string `json:"error,omitempty"` +} + +func listCertificateRequests(url string, client *http.Client, adminToken string) (int, *ListCertificateRequestsResponse, error) { + req, err := http.NewRequest("GET", url+"/api/v1/certificate_requests", nil) if err != nil { - log.Fatalf("couldn't create test sqlite db: %s", err) + return 0, nil, err } - env := &server.HandlerConfig{} - env.DB = testdb - ts := httptest.NewTLSServer(server.NewHandler(env)) - defer ts.Close() + req.Header.Set("Authorization", "Bearer "+adminToken) + res, err := client.Do(req) + if err != nil { + return 0, nil, err + } + var certificateRequestsResponse ListCertificateRequestsResponse + if err := json.NewDecoder(res.Body).Decode(&certificateRequestsResponse); err != nil { + return 0, nil, err + } + return res.StatusCode, &certificateRequestsResponse, nil +} + +func getCertificateRequest(url string, client *http.Client, adminToken string, id int) (int, *GetCertificateRequestResponse, error) { + req, err := http.NewRequest("GET", url+"/api/v1/certificate_requests/"+strconv.Itoa(id), nil) + if err != nil { + return 0, nil, err + } + req.Header.Set("Authorization", "Bearer "+adminToken) + res, err := client.Do(req) + if err != nil { + return 0, nil, err + } + var getCertificateRequestResponse GetCertificateRequestResponse + if err := json.NewDecoder(res.Body).Decode(&getCertificateRequestResponse); err != nil { + return 0, nil, err + } + return res.StatusCode, &getCertificateRequestResponse, nil +} + +func createCertificateRequest(url string, client *http.Client, adminToken string, certRequest CreateCertificateRequestParams) (int, *CreateCertificateRequestResponse, error) { + reqData, err := json.Marshal(certRequest) + if err != nil { + return 0, nil, err + } + req, err := http.NewRequest("POST", url+"/api/v1/certificate_requests", bytes.NewReader(reqData)) + if err != nil { + return 0, nil, err + } + req.Header.Set("Authorization", "Bearer "+adminToken) + req.Header.Set("Content-Type", "application/json") + res, err := client.Do(req) + if err != nil { + return 0, nil, err + } + var createCertificateRequestResponse CreateCertificateRequestResponse + if err := json.NewDecoder(res.Body).Decode(&createCertificateRequestResponse); err != nil { + return 0, nil, err + } + return res.StatusCode, &createCertificateRequestResponse, nil +} + +func deleteCertificateRequest(url string, client *http.Client, adminToken string, id int) (int, error) { + req, err := http.NewRequest("DELETE", url+"/api/v1/certificate_requests/"+strconv.Itoa(id), nil) + if err != nil { + return 0, err + } + req.Header.Set("Authorization", "Bearer "+adminToken) + res, err := client.Do(req) + if err != nil { + return 0, err + } + return res.StatusCode, nil +} +func createCertificate(url string, client *http.Client, adminToken string, cert CreateCertificateParams) (int, *CreateCertificateResponse, error) { + reqData, err := json.Marshal(cert) + if err != nil { + return 0, nil, err + } + req, err := http.NewRequest("POST", url+"/api/v1/certificate_requests/1/certificate", bytes.NewReader(reqData)) + if err != nil { + return 0, nil, err + } + req.Header.Set("Authorization", "Bearer "+adminToken) + req.Header.Set("Content-Type", "application/json") + res, err := client.Do(req) + if err != nil { + return 0, nil, err + } + var createCertificateResponse CreateCertificateResponse + if err := json.NewDecoder(res.Body).Decode(&createCertificateResponse); err != nil { + return 0, nil, err + } + return res.StatusCode, &createCertificateResponse, nil +} + +func rejectCertificate(url string, client *http.Client, adminToken string, id int) (int, error) { + req, err := http.NewRequest("POST", url+"/api/v1/certificate_requests/"+strconv.Itoa(id)+"/certificate/reject", nil) + if err != nil { + return 0, err + } + req.Header.Set("Authorization", "Bearer "+adminToken) + res, err := client.Do(req) + if err != nil { + return 0, err + } + return res.StatusCode, nil +} + +// This is an end-to-end test for the certificate requests endpoint. +// The order of the tests is important, as some tests depend on the +// state of the server after previous tests. +func TestCertificateRequestsEndToEnd(t *testing.T) { + ts, _, err := setupServer() + if err != nil { + t.Fatalf("couldn't create test server: %s", err) + } + defer ts.Close() client := ts.Client() var adminToken string var nonAdminToken string - t.Run("prepare user accounts and tokens", prepareUserAccounts(ts.URL, client, &adminToken, &nonAdminToken)) - - testCases := []struct { - desc string - method string - path string - data string - response string - status int - }{ - { - desc: "1: healthcheck success", - method: "GET", - path: "/status", - data: "", - response: "", - status: http.StatusOK, - }, - { - desc: "2: empty get csrs success", - method: "GET", - path: "/api/v1/certificate_requests", - data: "", - response: "null", - status: http.StatusOK, - }, - { - desc: "3: post csr1 fail", - method: "POST", - path: "/api/v1/certificate_requests", - data: "this is very clearly not a csr", - response: "error: csr validation failed: PEM Certificate Request string not found or malformed", - status: http.StatusBadRequest, - }, - { - desc: "4: post csr1 success", - method: "POST", - path: "/api/v1/certificate_requests", - data: AppleCSR, - response: "1", - status: http.StatusCreated, - }, - { - desc: "5: get csrs 1 success", - method: "GET", - path: "/api/v1/certificate_requests", - data: "", - response: expectedGetAllCertsResponseBody1, - status: http.StatusOK, - }, - { - desc: "6: post csr2 success", - method: "POST", - path: "/api/v1/certificate_requests", - data: BananaCSR, - response: "2", - status: http.StatusCreated, - }, - { - desc: "7: get csrs 2 success", - method: "GET", - path: "/api/v1/certificate_requests", - data: "", - response: expectedGetAllCertsResponseBody2, - status: http.StatusOK, - }, - { - desc: "8: post csr2 fail", - method: "POST", - path: "/api/v1/certificate_requests", - data: BananaCSR, - response: "error: given csr already recorded", - status: http.StatusBadRequest, - }, - { - desc: "9: post csr3 success", - method: "POST", - path: "/api/v1/certificate_requests", - data: StrawberryCSR, - response: "3", - status: http.StatusCreated, - }, - { - desc: "10: delete csr1 success", - method: "DELETE", - path: "/api/v1/certificate_requests/1", - data: "", - response: "1", - status: http.StatusAccepted, - }, - { - desc: "11: delete csr5 fail", - method: "DELETE", - path: "/api/v1/certificate_requests/5", - data: "", - response: "error: id not found", - status: http.StatusNotFound, - }, - { - desc: "12: get csr1 fail", - method: "GET", - path: "/api/v1/certificate_requests/1", - data: "", - response: "error: id not found", - status: http.StatusNotFound, - }, - { - desc: "13: get csr2 success", - method: "GET", - path: "/api/v1/certificate_requests/2", - data: "", - response: expectedGetCertReqResponseBody1, - status: http.StatusOK, - }, - { - desc: "14: post csr4 success", - method: "POST", - path: "/api/v1/certificate_requests", - data: AppleCSR, - response: "4", - status: http.StatusCreated, - }, - { - desc: "15: get csr4 success", - method: "GET", - path: "/api/v1/certificate_requests/4", - data: "", - response: expectedGetCertReqResponseBody2, - status: http.StatusOK, - }, - { - desc: "16: post cert2 fail 1", - method: "POST", - path: "/api/v1/certificate_requests/4/certificate", - data: BananaCert, - response: "error: cert validation failed: less than 2 certificate PEM strings were found", - status: http.StatusBadRequest, - }, - { - desc: "17: post cert2 fail 2", - method: "POST", - path: "/api/v1/certificate_requests/4/certificate", - data: "some random data that's clearly not a cert", - response: "error: cert validation failed: less than 2 certificate PEM strings were found", - status: http.StatusBadRequest, - }, - { - desc: "18: post cert2 success", - method: "POST", - path: "/api/v1/certificate_requests/2/certificate", - data: fmt.Sprintf("%s\n%s", BananaCert, IssuerCert), - response: "1", - status: http.StatusCreated, - }, - { - desc: "19: get csr2 success", - method: "GET", - path: "/api/v1/certificate_requests/2", - data: "", - response: expectedGetCertReqResponseBody3, - status: http.StatusOK, - }, - { - desc: "20: reject csr4 success", - method: "POST", - path: "/api/v1/certificate_requests/4/certificate/reject", - data: "", - response: "1", - status: http.StatusAccepted, - }, - { - desc: "21: get all csrs success", - method: "GET", - path: "/api/v1/certificate_requests", - data: "", - response: expectedGetAllCertsResponseBody3, - status: http.StatusOK, - }, - { - desc: "22: delete csr2 cert success", - method: "DELETE", - path: "/api/v1/certificate_requests/2/certificate", - data: "", - response: "1", - status: http.StatusAccepted, - }, - { - desc: "23: get csr2 success", - method: "GET", - path: "/api/v1/certificate_requests/2", - data: "", - response: expectedGetCertReqResponseBody4, - status: http.StatusOK, - }, - { - desc: "24: get csrs 3 success", - method: "GET", - path: "/api/v1/certificate_requests", - data: "", - response: expectedGetAllCertsResponseBody4, - status: http.StatusOK, - }, - { - desc: "25: healthcheck success", - method: "GET", - path: "/status", - data: "", - response: "", - status: http.StatusOK, - }, - { - desc: "26: metrics endpoint success", - method: "GET", - path: "/metrics", - data: "", - response: "", - status: http.StatusOK, - }, - } - for _, tC := range testCases { - t.Run(fmt.Sprintf("step %s", tC.desc), func(t *testing.T) { - req, err := http.NewRequest(tC.method, ts.URL+tC.path, strings.NewReader(tC.data)) - req.Header.Set("Authorization", "Bearer "+adminToken) - if err != nil { - t.Fatal(err) - } - res, err := client.Do(req) - if err != nil { - t.Fatal(err) - } - resBody, err := io.ReadAll(res.Body) - res.Body.Close() - if err != nil { - t.Fatal(err) - } - if res.StatusCode != tC.status || !strings.Contains(string(resBody), tC.response) { - t.Errorf("expected response did not match.\nExpected vs Received status code: %d vs %d\nExpected vs Received body: \n%s\nvs\n%s\n", tC.status, res.StatusCode, tC.response, string(resBody)) - } - }) - } + t.Run("prepare user accounts and tokens", prepareAccounts(ts.URL, client, &adminToken, &nonAdminToken)) + + t.Run("1. List certificate requests - no requests yet", func(t *testing.T) { + statusCode, listCertRequestsResponse, err := listCertificateRequests(ts.URL, client, adminToken) + if err != nil { + t.Fatal(err) + } + if statusCode != http.StatusOK { + t.Fatalf("expected status %d, got %d", http.StatusOK, statusCode) + } + if listCertRequestsResponse.Error != "" { + t.Fatalf("expected no error, got %s", listCertRequestsResponse.Error) + } + if len(listCertRequestsResponse.Result) != 0 { + t.Fatalf("expected no certificate requests, got %d", len(listCertRequestsResponse.Result)) + } + }) + + t.Run("2. Create certificate request", func(t *testing.T) { + csr1Path := filepath.Join("testdata", "csr1.pem") + csr1, err := os.ReadFile(csr1Path) + if err != nil { + t.Fatalf("cannot read file: %s", err) + } + createCertificateRequestRequest := CreateCertificateRequestParams{ + CSR: string(csr1), + } + statusCode, createCertResponse, err := createCertificateRequest(ts.URL, client, adminToken, createCertificateRequestRequest) + if err != nil { + t.Fatal(err) + } + if statusCode != http.StatusCreated { + t.Fatalf("expected status %d, got %d", http.StatusCreated, statusCode) + } + if createCertResponse.Error != "" { + t.Fatalf("expected no error, got %s", createCertResponse.Error) + } + }) + + t.Run("3. List certificate requests - 1 Certificate", func(t *testing.T) { + statusCode, listCertRequestsResponse, err := listCertificateRequests(ts.URL, client, adminToken) + if err != nil { + t.Fatal(err) + } + if statusCode != http.StatusOK { + t.Fatalf("expected status %d, got %d", http.StatusOK, statusCode) + } + if listCertRequestsResponse.Error != "" { + t.Fatalf("expected no error, got %s", listCertRequestsResponse.Error) + } + if len(listCertRequestsResponse.Result) != 1 { + t.Fatalf("expected 1 certificate request, got %d", len(listCertRequestsResponse.Result)) + } + }) + + t.Run("4. Get certificate request", func(t *testing.T) { + statusCode, getCertRequestResponse, err := getCertificateRequest(ts.URL, client, adminToken, 1) + if err != nil { + t.Fatal(err) + } + if statusCode != http.StatusOK { + t.Fatalf("expected status %d, got %d", http.StatusOK, statusCode) + } + if getCertRequestResponse.Error != "" { + t.Fatalf("expected no error, got %s", getCertRequestResponse.Error) + } + if getCertRequestResponse.Result.ID != 1 { + t.Fatalf("expected ID 1, got %d", getCertRequestResponse.Result.ID) + } + if getCertRequestResponse.Result.CSR == "" { + t.Fatalf("expected CSR, got empty string") + } + if getCertRequestResponse.Result.Certificate != "" { + t.Fatalf("expected no certificate, got %s", getCertRequestResponse.Result.Certificate) + } + }) + + t.Run("5. Create identical certificate request", func(t *testing.T) { + csr1Path := filepath.Join("testdata", "csr1.pem") + csr1, err := os.ReadFile(csr1Path) + if err != nil { + t.Fatalf("cannot read file: %s", err) + } + createCertificateRequestRequest := CreateCertificateRequestParams{ + CSR: string(csr1), + } + statusCode, createCertResponse, err := createCertificateRequest(ts.URL, client, adminToken, createCertificateRequestRequest) + if err != nil { + t.Fatal(err) + } + if statusCode != http.StatusBadRequest { + t.Fatalf("expected status %d, got %d", http.StatusBadRequest, statusCode) + } + if createCertResponse.Error != "given csr already recorded" { + t.Fatalf("expected error, got %s", createCertResponse.Error) + } + }) + + t.Run("6. List certificate requests - 1 Certificate", func(t *testing.T) { + statusCode, listCertRequestsResponse, err := listCertificateRequests(ts.URL, client, adminToken) + if err != nil { + t.Fatal(err) + } + if statusCode != http.StatusOK { + t.Fatalf("expected status %d, got %d", http.StatusOK, statusCode) + } + if listCertRequestsResponse.Error != "" { + t.Fatalf("expected no error, got %s", listCertRequestsResponse.Error) + } + if len(listCertRequestsResponse.Result) != 1 { + t.Fatalf("expected 2 certificate requests, got %d", len(listCertRequestsResponse.Result)) + } + }) + + t.Run("7. Create another certificate request", func(t *testing.T) { + csr2Path := filepath.Join("testdata", "csr2.pem") + csr2, err := os.ReadFile(csr2Path) + if err != nil { + t.Fatalf("cannot read file: %s", err) + } + createCertificateRequestRequest := CreateCertificateRequestParams{ + CSR: string(csr2), + } + statusCode, createCertResponse, err := createCertificateRequest(ts.URL, client, adminToken, createCertificateRequestRequest) + if err != nil { + t.Fatal(err) + } + if statusCode != http.StatusCreated { + t.Fatalf("expected status %d, got %d", http.StatusCreated, statusCode) + } + if createCertResponse.Error != "" { + t.Fatalf("expected no error, got %s", createCertResponse.Error) + } + }) + + t.Run("8. List certificate requests - 2 Certificates", func(t *testing.T) { + statusCode, listCertRequestsResponse, err := listCertificateRequests(ts.URL, client, adminToken) + if err != nil { + t.Fatal(err) + } + if statusCode != http.StatusOK { + t.Fatalf("expected status %d, got %d", http.StatusOK, statusCode) + } + if listCertRequestsResponse.Error != "" { + t.Fatalf("expected no error, got %s", listCertRequestsResponse.Error) + } + if len(listCertRequestsResponse.Result) != 2 { + t.Fatalf("expected 2 certificate requests, got %d", len(listCertRequestsResponse.Result)) + } + }) + + t.Run("9. Get certificate request 2", func(t *testing.T) { + statusCode, getCertRequestResponse, err := getCertificateRequest(ts.URL, client, adminToken, 2) + if err != nil { + t.Fatal(err) + } + if statusCode != http.StatusOK { + t.Fatalf("expected status %d, got %d", http.StatusOK, statusCode) + } + if getCertRequestResponse.Error != "" { + t.Fatalf("expected no error, got %s", getCertRequestResponse.Error) + } + if getCertRequestResponse.Result.ID != 2 { + t.Fatalf("expected ID 2, got %d", getCertRequestResponse.Result.ID) + } + if getCertRequestResponse.Result.CSR == "" { + t.Fatalf("expected CSR, got empty string") + } + if getCertRequestResponse.Result.Certificate != "" { + t.Fatalf("expected no certificate, got %s", getCertRequestResponse.Result.Certificate) + } + }) + + t.Run("10. Delete certificate request 1", func(t *testing.T) { + statusCode, err := deleteCertificateRequest(ts.URL, client, adminToken, 1) + if err != nil { + t.Fatal(err) + } + if statusCode != http.StatusAccepted { + t.Fatalf("expected status %d, got %d", http.StatusAccepted, statusCode) + } + }) + + t.Run("11. List certificate requests - 1 Certificate", func(t *testing.T) { + statusCode, listCertRequestsResponse, err := listCertificateRequests(ts.URL, client, adminToken) + if err != nil { + t.Fatal(err) + } + if statusCode != http.StatusOK { + t.Fatalf("expected status %d, got %d", http.StatusOK, statusCode) + } + if listCertRequestsResponse.Error != "" { + t.Fatalf("expected no error, got %s", listCertRequestsResponse.Error) + } + if len(listCertRequestsResponse.Result) != 1 { + t.Fatalf("expected 1 certificate request, got %d", len(listCertRequestsResponse.Result)) + } + }) + + t.Run("12. Delete certificate request 2", func(t *testing.T) { + statusCode, err := deleteCertificateRequest(ts.URL, client, adminToken, 2) + if err != nil { + t.Fatal(err) + } + if statusCode != http.StatusAccepted { + t.Fatalf("expected status %d, got %d", http.StatusAccepted, statusCode) + } + }) } -// trimmed removes all whitespace and newlines from a given string -func trimmed(s string) string { - return strings.ReplaceAll(strings.TrimSpace(s), "\n", "\\n") +// This is an end-to-end test for the certificates endpoint. +// The order of the tests is important, as some tests depend on the +// state of the server after previous tests. +func TestCertificatesEndToEnd(t *testing.T) { + ts, _, err := setupServer() + if err != nil { + t.Fatalf("couldn't create test server: %s", err) + } + defer ts.Close() + client := ts.Client() + + var adminToken string + var nonAdminToken string + t.Run("prepare user accounts and tokens", prepareAccounts(ts.URL, client, &adminToken, &nonAdminToken)) + t.Run("1. Create certificate request", func(t *testing.T) { + csr1Path := filepath.Join("testdata", "csr2.pem") + csr2, err := os.ReadFile(csr1Path) + if err != nil { + t.Fatalf("cannot read file: %s", err) + } + createCertificateRequestRequest := CreateCertificateRequestParams{ + CSR: string(csr2), + } + statusCode, createCertResponse, err := createCertificateRequest(ts.URL, client, adminToken, createCertificateRequestRequest) + if err != nil { + t.Fatal(err) + } + if statusCode != http.StatusCreated { + t.Fatalf("expected status %d, got %d", http.StatusCreated, statusCode) + } + if createCertResponse.Error != "" { + t.Fatalf("expected no error, got %s", createCertResponse.Error) + } + }) + + t.Run("2. Create Certificate", func(t *testing.T) { + certPath := filepath.Join("testdata", "csr2_cert.pem") + cert, err := os.ReadFile(certPath) + if err != nil { + t.Fatalf("cannot read file: %s", err) + } + issuerCertPath := filepath.Join("testdata", "issuer_cert.pem") + issuerCert, err := os.ReadFile(issuerCertPath) + if err != nil { + t.Fatalf("cannot read file: %s", err) + } + createCertificateRequest := CreateCertificateParams{ + Certificate: fmt.Sprintf("%s\n%s", cert, issuerCert), + } + statusCode, createCertResponse, err := createCertificate(ts.URL, client, adminToken, createCertificateRequest) + if err != nil { + t.Fatal(err) + } + if statusCode != http.StatusCreated { + t.Fatalf("expected status %d, got %d", http.StatusCreated, statusCode) + } + if createCertResponse.Error != "" { + t.Fatalf("expected no error, got %s", createCertResponse.Error) + } + }) + + t.Run("3. Get Certificate", func(t *testing.T) { + statusCode, getCertResponse, err := getCertificateRequest(ts.URL, client, adminToken, 1) + if err != nil { + t.Fatal(err) + } + if statusCode != http.StatusOK { + t.Fatalf("expected status %d, got %d", http.StatusOK, statusCode) + } + if getCertResponse.Error != "" { + t.Fatalf("expected no error, got %s", getCertResponse.Error) + } + if getCertResponse.Result.Certificate == "" { + t.Fatalf("expected certificate, got empty string") + } + }) + + t.Run("4. Reject Certificate", func(t *testing.T) { + statusCode, err := rejectCertificate(ts.URL, client, adminToken, 1) + if err != nil { + t.Fatal(err) + } + if statusCode != http.StatusAccepted { + t.Fatalf("expected status %d, got %d", http.StatusAccepted, statusCode) + } + }) + + t.Run("5. Get Certificate", func(t *testing.T) { + statusCode, getCertResponse, err := getCertificateRequest(ts.URL, client, adminToken, 1) + if err != nil { + t.Fatal(err) + } + if statusCode != http.StatusOK { + t.Fatalf("expected status %d, got %d", http.StatusOK, statusCode) + } + if getCertResponse.Error != "" { + t.Fatalf("expected no error, got %s", getCertResponse.Error) + } + if getCertResponse.Result.Certificate != "rejected" { + t.Fatalf("expected `rejected` certificate, got %s", getCertResponse.Result.Certificate) + } + }) + + t.Run("6. Delete Certificate", func(t *testing.T) { + statusCode, err := deleteCertificateRequest(ts.URL, client, adminToken, 1) + if err != nil { + t.Fatal(err) + } + if statusCode != http.StatusAccepted { + t.Fatalf("expected status %d, got %d", http.StatusAccepted, statusCode) + } + }) + + t.Run("7. Get Certificate", func(t *testing.T) { + statusCode, getCertResponse, err := getCertificateRequest(ts.URL, client, adminToken, 1) + if err != nil { + t.Fatal(err) + } + if statusCode != http.StatusNotFound { + t.Fatalf("expected status %d, got %d", http.StatusNotFound, statusCode) + } + + if getCertResponse.Error != "Not Found" { + t.Fatalf("expected error `Not Found`, got %s", getCertResponse.Error) + } + }) } diff --git a/internal/server/handlers_health.go b/internal/server/handlers_health.go deleted file mode 100644 index b56030df..00000000 --- a/internal/server/handlers_health.go +++ /dev/null @@ -1,27 +0,0 @@ -package server - -import ( - "encoding/json" - "net/http" -) - -// the health check endpoint returns a http.StatusOK alongside info about the server -// initialized means the first user has been created -func HealthCheck(env *HandlerConfig) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - users, err := env.DB.RetrieveAllUsers() - if err != nil { - writeError("couldn't generate status", http.StatusInternalServerError, w) - return - } - response, err := json.Marshal(map[string]any{ - "initialized": len(users) > 0, - }) - if err != nil { - writeError("couldn't generate status", http.StatusInternalServerError, w) - return - } - w.Write(response) //nolint:errcheck - w.WriteHeader(http.StatusOK) //nolint:errcheck - } -} diff --git a/internal/server/handlers_helpers_test.go b/internal/server/handlers_helpers_test.go index 78aec0a6..cb309c1a 100644 --- a/internal/server/handlers_helpers_test.go +++ b/internal/server/handlers_helpers_test.go @@ -2,96 +2,77 @@ package server_test import ( - "io" "net/http" - "strings" + "net/http/httptest" "testing" + + "github.com/canonical/notary/internal/db" + "github.com/canonical/notary/internal/server" ) -func prepareUserAccounts(url string, client *http.Client, adminToken, nonAdminToken *string) func(*testing.T) { +func setupServer() (*httptest.Server, *server.HandlerConfig, error) { + testdb, err := db.NewDatabase(":memory:") + if err != nil { + return nil, nil, err + } + config := &server.HandlerConfig{ + DB: testdb, + } + ts := httptest.NewTLSServer(server.NewHandler(config)) + return ts, config, nil +} + +func prepareAccounts(url string, client *http.Client, adminToken, nonAdminToken *string) func(*testing.T) { return func(t *testing.T) { - req, err := http.NewRequest("POST", url+"/api/v1/accounts", strings.NewReader(adminUser)) - if err != nil { - t.Fatal(err) - } - res, err := client.Do(req) - if err != nil { - t.Fatal(err) - } - _, err = io.ReadAll(res.Body) - res.Body.Close() - if err != nil { - t.Fatal(err) - } - if res.StatusCode != http.StatusCreated { - t.Fatalf("creating the first request should succeed when unauthorized. status code received: %d", res.StatusCode) - } - req, err = http.NewRequest("POST", url+"/api/v1/accounts", strings.NewReader(validUser)) - if err != nil { - t.Fatal(err) - } - res, err = client.Do(req) - if err != nil { - t.Fatal(err) + adminAccountParams := &CreateAccountParams{ + Username: "testadmin", + Password: "Admin123", } - _, err = io.ReadAll(res.Body) - res.Body.Close() + statusCode, _, err := createAccount(url, client, "", adminAccountParams) if err != nil { - t.Fatal(err) - } - if res.StatusCode != http.StatusUnauthorized { - t.Fatalf("the second request should have been rejected. status code received: %d", res.StatusCode) + t.Fatalf("couldn't create admin account: %s", err) } - req, err = http.NewRequest("POST", url+"/login", strings.NewReader(adminUser)) - if err != nil { - t.Fatal(err) + if statusCode != http.StatusCreated { + t.Fatalf("expected status %d, got %d", http.StatusCreated, statusCode) } - res, err = client.Do(req) - if err != nil { - t.Fatal(err) + adminLoginParams := &LoginParams{ + Username: adminAccountParams.Username, + Password: adminAccountParams.Password, } - resBody, err := io.ReadAll(res.Body) - res.Body.Close() + statusCode, loginResponse, err := login(url, client, adminLoginParams) if err != nil { - t.Fatal(err) + t.Fatalf("couldn't login admin account: %s", err) } - if res.StatusCode != http.StatusOK { - t.Fatalf("the admin login request should have succeeded. status code received: %d", res.StatusCode) + if statusCode != http.StatusOK { + t.Fatalf("expected status %d, got %d", http.StatusOK, statusCode) } - *adminToken = string(resBody) - req, err = http.NewRequest("POST", url+"/api/v1/accounts", strings.NewReader(validUser)) - req.Header.Set("Authorization", "Bearer "+*adminToken) - if err != nil { - t.Fatal(err) - } - res, err = client.Do(req) - if err != nil { - t.Fatal(err) + + *adminToken = string(loginResponse.Result.Token) + + nonAdminAccount := &CreateAccountParams{ + Username: "testuser", + Password: "userPass!", } - _, err = io.ReadAll(res.Body) - res.Body.Close() + statusCode, _, err = createAccount(url, client, *adminToken, nonAdminAccount) if err != nil { - t.Fatal(err) + t.Fatalf("couldn't create non-admin account: %s", err) } - if res.StatusCode != http.StatusCreated { - t.Fatalf("creating the second request should have succeeded when given the admin auth header. status code received: %d", res.StatusCode) - } - req, err = http.NewRequest("POST", url+"/login", strings.NewReader(validUser)) - if err != nil { - t.Fatal(err) + if statusCode != http.StatusCreated { + t.Fatalf("expected status %d, got %d", http.StatusCreated, statusCode) } - res, err = client.Do(req) - if err != nil { - t.Fatal(err) + + nonAdminLoginParams := &LoginParams{ + Username: nonAdminAccount.Username, + Password: nonAdminAccount.Password, } - resBody, err = io.ReadAll(res.Body) - res.Body.Close() + statusCode, loginResponse, err = login(url, client, nonAdminLoginParams) if err != nil { - t.Fatal(err) + t.Fatalf("couldn't login non-admin account: %s", err) } - if res.StatusCode != http.StatusOK { - t.Errorf("the admin login request should have succeeded. status code received: %d", res.StatusCode) + if statusCode != http.StatusOK { + t.Fatalf("expected status %d, got %d", http.StatusOK, statusCode) } - *nonAdminToken = string(resBody) + + *nonAdminToken = string(loginResponse.Result.Token) } } diff --git a/internal/server/handlers_login.go b/internal/server/handlers_login.go index 2771c39a..da0e22f4 100644 --- a/internal/server/handlers_login.go +++ b/internal/server/handlers_login.go @@ -3,6 +3,7 @@ package server import ( "encoding/json" "errors" + "log" "net/http" "time" @@ -18,6 +19,15 @@ type jwtNotaryClaims struct { jwt.StandardClaims } +type LoginParams struct { + Username string `json:"username"` + Password string `json:"password"` +} + +type LoginResponse struct { + Token string `json:"token"` +} + // Helper function to generate a JWT func generateJWT(id int, username string, jwtSecret []byte, permissions int) (string, error) { token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwtNotaryClaims{ @@ -38,41 +48,47 @@ func generateJWT(id int, username string, jwtSecret []byte, permissions int) (st func Login(env *HandlerConfig) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - var userRequest db.User - if err := json.NewDecoder(r.Body).Decode(&userRequest); err != nil { - writeError("Invalid JSON format", http.StatusBadRequest, w) + var loginParams LoginParams + if err := json.NewDecoder(r.Body).Decode(&loginParams); err != nil { + writeError(w, http.StatusBadRequest, "Invalid JSON format") return } - if userRequest.Username == "" { - writeError("Username is required", http.StatusBadRequest, w) + if loginParams.Username == "" { + writeError(w, http.StatusBadRequest, "Username is required") return } - if userRequest.Password == "" { - writeError("Password is required", http.StatusBadRequest, w) + if loginParams.Password == "" { + writeError(w, http.StatusBadRequest, "Password is required") return } - userAccount, err := env.DB.RetrieveUserByUsername(userRequest.Username) + userAccount, err := env.DB.RetrieveUserByUsername(loginParams.Username) if err != nil { - status := http.StatusInternalServerError + log.Println(err) if errors.Is(err, db.ErrIdNotFound) { - writeError("The username or password is incorrect. Try again.", http.StatusUnauthorized, w) + writeError(w, http.StatusUnauthorized, "The username or password is incorrect. Try again.") return } - writeError(err.Error(), status, w) + writeError(w, http.StatusInternalServerError, "Internal Error") return } - if err := bcrypt.CompareHashAndPassword([]byte(userAccount.Password), []byte(userRequest.Password)); err != nil { - writeError("The username or password is incorrect. Try again.", http.StatusUnauthorized, w) + if err := bcrypt.CompareHashAndPassword([]byte(userAccount.Password), []byte(loginParams.Password)); err != nil { + writeError(w, http.StatusUnauthorized, "The username or password is incorrect. Try again.") return } jwt, err := generateJWT(userAccount.ID, userAccount.Username, env.JWTSecret, userAccount.Permissions) if err != nil { - writeError(err.Error(), http.StatusInternalServerError, w) + log.Println(err) + writeError(w, http.StatusInternalServerError, "Internal Error") return } w.WriteHeader(http.StatusOK) - if _, err := w.Write([]byte(jwt)); err != nil { - writeError(err.Error(), http.StatusInternalServerError, w) + loginResponse := LoginResponse{ + Token: jwt, + } + err = writeJSON(w, loginResponse) + if err != nil { + writeError(w, http.StatusInternalServerError, "internal error") + return } } } diff --git a/internal/server/handlers_login_test.go b/internal/server/handlers_login_test.go index 8bbe9de8..408cb2c6 100644 --- a/internal/server/handlers_login_test.go +++ b/internal/server/handlers_login_test.go @@ -1,129 +1,172 @@ package server_test import ( + "encoding/json" "fmt" - "io" - "log" "net/http" - "net/http/httptest" "strings" "testing" - "github.com/canonical/notary/internal/db" - "github.com/canonical/notary/internal/server" "github.com/golang-jwt/jwt" ) -func TestLogin(t *testing.T) { - testdb, err := db.NewDatabase(":memory:") +type LoginParams struct { + Username string `json:"username"` + Password string `json:"password"` +} + +type LoginResponseResult struct { + Token string `json:"token"` +} + +type LoginResponse struct { + Result LoginResponseResult `json:"result"` + Error string `json:"error,omitempty"` +} + +func login(url string, client *http.Client, data *LoginParams) (int, *LoginResponse, error) { + body, err := json.Marshal(data) if err != nil { - log.Fatalf("couldn't create test sqlite db: %s", err) + return 0, nil, err } - env := &server.HandlerConfig{} - env.DB = testdb - env.JWTSecret = []byte("secret") - ts := httptest.NewTLSServer(server.NewHandler(env)) - defer ts.Close() + req, err := http.NewRequest("POST", url+"/login", strings.NewReader(string(body))) + if err != nil { + return 0, nil, err + } + res, err := client.Do(req) + if err != nil { + return 0, nil, err + } + defer res.Body.Close() + var loginResponse LoginResponse + if err := json.NewDecoder(res.Body).Decode(&loginResponse); err != nil { + return 0, nil, err + } + return res.StatusCode, &loginResponse, nil +} +func TestLoginEndToEnd(t *testing.T) { + ts, config, err := setupServer() + if err != nil { + t.Fatalf("couldn't create test server: %s", err) + } + defer ts.Close() client := ts.Client() - testCases := []struct { - desc string - method string - path string - data string - response string - status int - }{ - { - desc: "Create admin user", - method: "POST", - path: "/api/v1/accounts", - data: adminUser, - response: "{\"id\":1}", - status: http.StatusCreated, - }, - { - desc: "Login success", - method: "POST", - path: "/login", - data: adminUser, - response: "", - status: http.StatusOK, - }, - { - desc: "Login failure missing username", - method: "POST", - path: "/login", - data: invalidUser, - response: "Username is required", - status: http.StatusBadRequest, - }, - { - desc: "Login failure missing password", - method: "POST", - path: "/login", - data: noPasswordUser, - response: "Password is required", - status: http.StatusBadRequest, - }, - { - desc: "Login failure invalid password", - method: "POST", - path: "/login", - data: adminUserWrongPass, - response: "error: The username or password is incorrect. Try again.", - status: http.StatusUnauthorized, - }, - { - desc: "Login failure invalid username", - method: "POST", - path: "/login", - data: notExistingUser, - response: "error: The username or password is incorrect. Try again.", - status: http.StatusUnauthorized, - }, - } - for _, tC := range testCases { - t.Run(tC.desc, func(t *testing.T) { - req, err := http.NewRequest(tC.method, ts.URL+tC.path, strings.NewReader(tC.data)) - if err != nil { - t.Fatal(err) - } - res, err := client.Do(req) - if err != nil { - t.Fatal(err) - } - resBody, err := io.ReadAll(res.Body) - res.Body.Close() - if err != nil { - t.Fatal(err) - } - if res.StatusCode != tC.status || !strings.Contains(string(resBody), tC.response) { - t.Errorf("expected response did not match.\nExpected vs Received status code: %d vs %d\nExpected vs Received body: \n%s\nvs\n%s\n", tC.status, res.StatusCode, tC.response, string(resBody)) - } - if tC.desc == "Login success" && res.StatusCode == http.StatusOK { - token, parseErr := jwt.Parse(string(resBody), func(token *jwt.Token) (interface{}, error) { - if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { - return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"]) - } - return []byte(env.JWTSecret), nil - }) - if parseErr != nil { - t.Errorf("Error parsing JWT: %v", parseErr) - return - } + t.Run("Create admin user", func(t *testing.T) { + adminUser := &CreateAccountParams{ + Username: "testadmin", + Password: "Admin123", + } + statusCode, _, err := createAccount(ts.URL, client, "", adminUser) + if err != nil { + t.Fatalf("couldn't create admin user: %s", err) + } + if statusCode != http.StatusCreated { + t.Fatalf("expected status %d, got %d", http.StatusCreated, statusCode) + } + }) - if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid { - if claims["username"] != "testadmin" { - t.Errorf("Username found in JWT does not match expected value.") - } else if int(claims["permissions"].(float64)) != 1 { - t.Errorf("Permissions found in JWT does not match expected value.") - } - } else { - t.Errorf("Invalid JWT token or JWT claims are not readable") - } + t.Run("Login success", func(t *testing.T) { + adminUser := &LoginParams{ + Username: "testadmin", + Password: "Admin123", + } + statusCode, loginResponse, err := login(ts.URL, client, adminUser) + if err != nil { + t.Fatalf("couldn't login admin user: %s", err) + } + if statusCode != http.StatusOK { + t.Fatalf("expected status %d, got %d", http.StatusOK, statusCode) + } + if loginResponse.Result.Token == "" { + t.Fatalf("expected token, got empty string") + } + token, err := jwt.Parse(loginResponse.Result.Token, func(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"]) } + return []byte(config.JWTSecret), nil }) - } + if err != nil { + t.Fatalf("couldn't parse token: %s", err) + } + if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid { + if claims["username"] != "testadmin" { + t.Fatalf("expected username %q, got %q", "testadmin", claims["username"]) + } + } else { + t.Fatalf("invalid token or claims") + } + }) + + t.Run("Login failure missing username", func(t *testing.T) { + invalidUser := &LoginParams{ + Username: "", + Password: "Admin123", + } + statusCode, loginResponse, err := login(ts.URL, client, invalidUser) + if err != nil { + t.Fatalf("couldn't login admin user: %s", err) + } + if statusCode != http.StatusBadRequest { + t.Fatalf("expected status %d, got %d", http.StatusBadRequest, statusCode) + } + if loginResponse.Error != "Username is required" { + t.Fatalf("expected error %q, got %q", "Username is required", loginResponse.Error) + } + }) + + t.Run("Login failure missing password", func(t *testing.T) { + invalidUser := &LoginParams{ + Username: "testadmin", + Password: "", + } + statusCode, loginResponse, err := login(ts.URL, client, invalidUser) + if err != nil { + t.Fatalf("couldn't login admin user: %s", err) + } + if statusCode != http.StatusBadRequest { + t.Fatalf("expected status %d, got %d", http.StatusBadRequest, statusCode) + } + if loginResponse.Error != "Password is required" { + t.Fatalf("expected error %q, got %q", "Password is required", loginResponse.Error) + } + }) + + t.Run("Login failure invalid password", func(t *testing.T) { + invalidUser := &LoginParams{ + Username: "testadmin", + Password: "a-wrong-password", + } + statusCode, loginResponse, err := login(ts.URL, client, invalidUser) + if err != nil { + t.Fatalf("couldn't login admin user: %s", err) + } + if statusCode != http.StatusUnauthorized { + t.Fatalf("expected status %d, got %d", http.StatusUnauthorized, statusCode) + } + + if loginResponse.Error != "The username or password is incorrect. Try again." { + t.Fatalf("expected error %q, got %q", "The username or password is incorrect. Try again.", loginResponse.Error) + } + }) + + t.Run("Login failure invalid username", func(t *testing.T) { + invalidUser := &LoginParams{ + Username: "not-existing-user", + Password: "Admin123", + } + statusCode, loginResponse, err := login(ts.URL, client, invalidUser) + if err != nil { + t.Fatalf("couldn't login admin user: %s", err) + } + if statusCode != http.StatusUnauthorized { + t.Fatalf("expected status %d, got %d", http.StatusUnauthorized, statusCode) + } + + if loginResponse.Error != "The username or password is incorrect. Try again." { + t.Fatalf("expected error %q, got %q", "The username or password is incorrect. Try again.", loginResponse.Error) + } + }) } diff --git a/internal/server/handlers_status.go b/internal/server/handlers_status.go new file mode 100644 index 00000000..3ed3aef0 --- /dev/null +++ b/internal/server/handlers_status.go @@ -0,0 +1,30 @@ +package server + +import ( + "net/http" +) + +type StatusResponse struct { + Initialized bool `json:"initialized"` +} + +// the GET status endpoint returns a http.StatusOK alongside info about the server +// initialized means the first user has been created +func GetStatus(env *HandlerConfig) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + numUsers, err := env.DB.NumUsers() + if err != nil { + writeError(w, http.StatusInternalServerError, "couldn't generate status") + return + } + statusResponse := StatusResponse{ + Initialized: numUsers > 0, + } + w.WriteHeader(http.StatusOK) + err = writeJSON(w, statusResponse) + if err != nil { + writeError(w, http.StatusInternalServerError, "internal error") + return + } + } +} diff --git a/internal/server/handlers_status_test.go b/internal/server/handlers_status_test.go new file mode 100644 index 00000000..93b6f1f9 --- /dev/null +++ b/internal/server/handlers_status_test.go @@ -0,0 +1,85 @@ +package server_test + +import ( + "encoding/json" + "net/http" + "testing" +) + +type GetStatusResponseResult struct { + Initialized bool `json:"initialized"` +} + +type GetStatusResponse struct { + Error string `json:"error,omitempty"` + Result GetStatusResponseResult `json:"result"` +} + +func getStatus(url string, client *http.Client, adminToken string) (int, *GetStatusResponse, error) { + req, err := http.NewRequest("GET", url+"/status", nil) + if err != nil { + return 0, nil, err + } + req.Header.Set("Authorization", "Bearer "+adminToken) + res, err := client.Do(req) + if err != nil { + return 0, nil, err + } + defer res.Body.Close() + var statusResponse GetStatusResponse + if err := json.NewDecoder(res.Body).Decode(&statusResponse); err != nil { + return 0, nil, err + } + return res.StatusCode, &statusResponse, nil +} + +func TestStatus(t *testing.T) { + ts, _, err := setupServer() + if err != nil { + t.Fatalf("couldn't create test server: %s", err) + } + defer ts.Close() + client := ts.Client() + + t.Run("status not initialized", func(t *testing.T) { + statusCode, statusResponse, err := getStatus(ts.URL, client, "") + if err != nil { + t.Fatalf("couldn't get status: %s", err) + } + + if statusCode != http.StatusOK { + t.Fatalf("expected status %d, got %d", http.StatusOK, statusCode) + } + + if statusResponse.Error != "" { + t.Fatalf("expected error %q, got %q", "", statusResponse.Error) + } + + if statusResponse.Result.Initialized { + t.Fatalf("expected initialized to be false") + } + }) + + var adminToken string + var nonAdminToken string + t.Run("prepare accounts and tokens", prepareAccounts(ts.URL, client, &adminToken, &nonAdminToken)) + + t.Run("status initialized", func(t *testing.T) { + statusCode, statusResponse, err := getStatus(ts.URL, client, adminToken) + if err != nil { + t.Fatalf("couldn't get status: %s", err) + } + + if statusCode != http.StatusOK { + t.Fatalf("expected status %d, got %d", http.StatusOK, statusCode) + } + + if statusResponse.Error != "" { + t.Fatalf("expected error %q, got %q", "", statusResponse.Error) + } + + if !statusResponse.Result.Initialized { + t.Fatalf("expected initialized to be true") + } + }) +} diff --git a/internal/server/handlers_users.go b/internal/server/handlers_users.go deleted file mode 100644 index f1746a1d..00000000 --- a/internal/server/handlers_users.go +++ /dev/null @@ -1,275 +0,0 @@ -package server - -import ( - "crypto/rand" - "encoding/json" - "errors" - "math/big" - mrand "math/rand" - "net/http" - "regexp" - "strconv" - "strings" - - "github.com/canonical/notary/internal/db" -) - -func getRandomChars(charset string, length int) (string, error) { - result := make([]byte, length) - for i := range result { - n, err := rand.Int(rand.Reader, big.NewInt(int64(len(charset)))) - if err != nil { - return "", err - } - result[i] = charset[n.Int64()] - } - return string(result), nil -} - -// Generates a random 16 chars long password that contains uppercase and lowercase characters and numbers or symbols. -func generatePassword() (string, error) { - const ( - uppercaseSet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" - lowercaseSet = "abcdefghijklmnopqrstuvwxyz" - numbersAndSymbolsSet = "0123456789*?@" - allCharsSet = uppercaseSet + lowercaseSet + numbersAndSymbolsSet - ) - uppercase, err := getRandomChars(uppercaseSet, 2) - if err != nil { - return "", err - } - lowercase, err := getRandomChars(lowercaseSet, 2) - if err != nil { - return "", err - } - numbersOrSymbols, err := getRandomChars(numbersAndSymbolsSet, 2) - if err != nil { - return "", err - } - allChars, err := getRandomChars(allCharsSet, 10) - if err != nil { - return "", err - } - res := []rune(uppercase + lowercase + numbersOrSymbols + allChars) - mrand.Shuffle(len(res), func(i, j int) { - res[i], res[j] = res[j], res[i] - }) - return string(res), nil -} - -func validatePassword(password string) bool { - if len(password) < 8 { - return false - } - hasCapital := regexp.MustCompile(`[A-Z]`).MatchString(password) - if !hasCapital { - return false - } - hasLower := regexp.MustCompile(`[a-z]`).MatchString(password) - if !hasLower { - return false - } - hasNumberOrSymbol := regexp.MustCompile(`[0-9!@#$%^&*()_+\-=\[\]{};':"|,.<>?~]`).MatchString(password) - - return hasNumberOrSymbol -} - -// GetUserAccounts returns all users from the database -func GetUserAccounts(env *HandlerConfig) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - users, err := env.DB.RetrieveAllUsers() - if err != nil { - writeError(err.Error(), http.StatusInternalServerError, w) - return - } - for i := range users { - users[i].Password = "" - } - body, err := json.Marshal(users) - if err != nil { - writeError(err.Error(), http.StatusInternalServerError, w) - return - } - if _, err := w.Write(body); err != nil { - writeError(err.Error(), http.StatusInternalServerError, w) - } - } -} - -// GetUserAccount receives an id as a path parameter, and -// returns the corresponding User Account -func GetUserAccount(env *HandlerConfig) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - id := r.PathValue("id") - var userAccount db.User - var err error - if id == "me" { - claims, headerErr := getClaimsFromAuthorizationHeader(r.Header.Get("Authorization"), env.JWTSecret) - if headerErr != nil { - writeError(headerErr.Error(), http.StatusUnauthorized, w) - } - userAccount, err = env.DB.RetrieveUserByUsername(claims.Username) - } else { - userAccount, err = env.DB.RetrieveUser(id) - } - if err != nil { - if errors.Is(err, db.ErrIdNotFound) { - writeError(err.Error(), http.StatusNotFound, w) - return - } - writeError(err.Error(), http.StatusInternalServerError, w) - return - } - userAccount.Password = "" - body, err := json.Marshal(userAccount) - if err != nil { - writeError(err.Error(), http.StatusInternalServerError, w) - return - } - if _, err := w.Write(body); err != nil { - writeError(err.Error(), http.StatusInternalServerError, w) - } - } -} - -// PostUserAccount creates a new User Account, and returns the id of the created row -func PostUserAccount(env *HandlerConfig) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - var user db.User - if err := json.NewDecoder(r.Body).Decode(&user); err != nil { - writeError("Invalid JSON format", http.StatusBadRequest, w) - return - } - if user.Username == "" { - writeError("Username is required", http.StatusBadRequest, w) - return - } - shouldGeneratePassword := user.Password == "" - if shouldGeneratePassword { - generatedPassword, err := generatePassword() - if err != nil { - writeError("Failed to generate password", http.StatusInternalServerError, w) - return - } - user.Password = generatedPassword - } - if !validatePassword(user.Password) { - writeError( - "Password must have 8 or more characters, must include at least one capital letter, one lowercase letter, and either a number or a symbol.", - http.StatusBadRequest, - w, - ) - return - } - users, err := env.DB.RetrieveAllUsers() - if err != nil { - writeError("Failed to retrieve users: "+err.Error(), http.StatusInternalServerError, w) - return - } - - permission := UserPermission - if len(users) == 0 { - permission = AdminPermission - } - id, err := env.DB.CreateUser(user.Username, user.Password, permission) - if err != nil { - if strings.Contains(err.Error(), "UNIQUE constraint failed") { - writeError("user with given username already exists", http.StatusBadRequest, w) - return - } - writeError(err.Error(), http.StatusInternalServerError, w) - return - } - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusCreated) - response, err := json.Marshal(map[string]any{"id": id}) - if shouldGeneratePassword { - response, err = json.Marshal(map[string]any{"id": id, "password": user.Password}) - } - if err != nil { - writeError("Error marshaling response", http.StatusInternalServerError, w) - } - if _, err := w.Write(response); err != nil { - writeError(err.Error(), http.StatusInternalServerError, w) - } - } -} - -// DeleteUserAccount handler receives an id as a path parameter, -// deletes the corresponding User Account, and returns a http.StatusNoContent on success -func DeleteUserAccount(env *HandlerConfig) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - id := r.PathValue("id") - user, err := env.DB.RetrieveUser(id) - if err != nil { - if !errors.Is(err, db.ErrIdNotFound) { - writeError(err.Error(), http.StatusInternalServerError, w) - return - } - } - if user.Permissions == 1 { - writeError("deleting an Admin account is not allowed.", http.StatusBadRequest, w) - return - } - insertId, err := env.DB.DeleteUser(id) - if err != nil { - if errors.Is(err, db.ErrIdNotFound) { - writeError(err.Error(), http.StatusNotFound, w) - return - } - writeError(err.Error(), http.StatusInternalServerError, w) - return - } - w.WriteHeader(http.StatusAccepted) - if _, err := w.Write([]byte(strconv.FormatInt(insertId, 10))); err != nil { - writeError(err.Error(), http.StatusInternalServerError, w) - } - } -} - -func ChangeUserAccountPassword(env *HandlerConfig) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - id := r.PathValue("id") - if id == "me" { - claims, err := getClaimsFromAuthorizationHeader(r.Header.Get("Authorization"), env.JWTSecret) - if err != nil { - writeError(err.Error(), http.StatusUnauthorized, w) - } - userAccount, err := env.DB.RetrieveUserByUsername(claims.Username) - if err != nil { - writeError(err.Error(), http.StatusUnauthorized, w) - } - id = strconv.Itoa(userAccount.ID) - } - var user db.User - if err := json.NewDecoder(r.Body).Decode(&user); err != nil { - writeError("Invalid JSON format", http.StatusBadRequest, w) - return - } - if user.Password == "" { - writeError("Password is required", http.StatusBadRequest, w) - return - } - if !validatePassword(user.Password) { - writeError( - "Password must have 8 or more characters, must include at least one capital letter, one lowercase letter, and either a number or a symbol.", - http.StatusBadRequest, - w, - ) - return - } - ret, err := env.DB.UpdateUser(id, user.Password) - if err != nil { - if errors.Is(err, db.ErrIdNotFound) { - writeError(err.Error(), http.StatusNotFound, w) - return - } - writeError(err.Error(), http.StatusInternalServerError, w) - return - } - w.WriteHeader(http.StatusOK) - if _, err := w.Write([]byte(strconv.FormatInt(ret, 10))); err != nil { - writeError(err.Error(), http.StatusInternalServerError, w) - } - } -} diff --git a/internal/server/handlers_users_test.go b/internal/server/handlers_users_test.go deleted file mode 100644 index b16d65eb..00000000 --- a/internal/server/handlers_users_test.go +++ /dev/null @@ -1,189 +0,0 @@ -package server_test - -import ( - "io" - "log" - "net/http" - "net/http/httptest" - "regexp" - "strings" - "testing" - - "github.com/canonical/notary/internal/db" - "github.com/canonical/notary/internal/server" -) - -const ( - adminUser = `{"username": "testadmin", "password": "Admin123"}` - validUser = `{"username": "testuser", "password": "userPass!"}` - invalidUser = `{"username": "", "password": ""}` - noPasswordUser = `{"username": "nopass"}` - adminUserNewPassword = `{"id": 1, "password": "newPassword1"}` - userNewInvalidPassword = `{"id": 1, "password": "password"}` - userMissingPassword = `{"id": 1}` - adminUserWrongPass = `{"username": "testadmin", "password": "wrongpass"}` - notExistingUser = `{"username": "not_existing", "password": "user"}` -) - -func TestNotaryUsersHandlers(t *testing.T) { - testdb, err := db.NewDatabase(":memory:") - if err != nil { - log.Fatalf("couldn't create test sqlite db: %s", err) - } - env := &server.HandlerConfig{} - env.DB = testdb - ts := httptest.NewTLSServer(server.NewHandler(env)) - defer ts.Close() - - client := ts.Client() - - var adminToken string - var nonAdminToken string - t.Run("prepare user accounts and tokens", prepareUserAccounts(ts.URL, client, &adminToken, &nonAdminToken)) - - testCases := []struct { - desc string - method string - path string - data string - auth string - response string - status int - }{ - { - desc: "Retrieve admin user success", - method: "GET", - path: "/api/v1/accounts/1", - data: "", - auth: adminToken, - response: "{\"id\":1,\"username\":\"testadmin\",\"permissions\":1}", - status: http.StatusOK, - }, - { - desc: "Retrieve admin user fail", - method: "GET", - path: "/api/v1/accounts/1", - data: "", - auth: nonAdminToken, - response: "error: forbidden", - status: http.StatusForbidden, - }, - { - desc: "Create no password user success", - method: "POST", - path: "/api/v1/accounts", - data: noPasswordUser, - auth: adminToken, - response: "{\"id\":3,\"password\":", - status: http.StatusCreated, - }, - { - desc: "Retrieve normal user success", - method: "GET", - path: "/api/v1/accounts/2", - data: "", - auth: adminToken, - response: "{\"id\":2,\"username\":\"testuser\",\"permissions\":0}", - status: http.StatusOK, - }, - { - desc: "Retrieve user failure", - method: "GET", - path: "/api/v1/accounts/300", - data: "", - auth: adminToken, - response: "error: id not found", - status: http.StatusNotFound, - }, - { - desc: "Create user failure", - method: "POST", - path: "/api/v1/accounts", - data: invalidUser, - auth: adminToken, - response: "error: Username is required", - status: http.StatusBadRequest, - }, - { - desc: "Change password success", - method: "POST", - path: "/api/v1/accounts/1/change_password", - data: adminUserNewPassword, - auth: adminToken, - response: "1", - status: http.StatusOK, - }, - { - desc: "Change password failure no user", - method: "POST", - path: "/api/v1/accounts/100/change_password", - data: adminUserNewPassword, - auth: adminToken, - response: "id not found", - status: http.StatusNotFound, - }, - { - desc: "Change password failure missing password", - method: "POST", - path: "/api/v1/accounts/1/change_password", - data: userMissingPassword, - auth: adminToken, - response: "Password is required", - status: http.StatusBadRequest, - }, - { - desc: "Change password failure bad password", - method: "POST", - path: "/api/v1/accounts/1/change_password", - data: userNewInvalidPassword, - auth: adminToken, - response: "Password must have 8 or more characters, must include at least one capital letter, one lowercase letter, and either a number or a symbol.", - status: http.StatusBadRequest, - }, - { - desc: "Delete user success", - method: "DELETE", - path: "/api/v1/accounts/2", - data: invalidUser, - auth: adminToken, - response: "1", - status: http.StatusAccepted, - }, - { - desc: "Delete user failure", - method: "DELETE", - path: "/api/v1/accounts/2", - data: invalidUser, - auth: adminToken, - response: "error: id not found", - status: http.StatusNotFound, - }, - } - for _, tC := range testCases { - t.Run(tC.desc, func(t *testing.T) { - req, err := http.NewRequest(tC.method, ts.URL+tC.path, strings.NewReader(tC.data)) - req.Header.Add("Authorization", "Bearer "+tC.auth) - if err != nil { - t.Fatal(err) - } - res, err := client.Do(req) - if err != nil { - t.Fatal(err) - } - resBody, err := io.ReadAll(res.Body) - res.Body.Close() - if err != nil { - t.Fatal(err) - } - if res.StatusCode != tC.status || !strings.Contains(string(resBody), tC.response) { - t.Errorf("expected response did not match.\nExpected vs Received status code: %d vs %d\nExpected vs Received body: \n%s\nvs\n%s\n", tC.status, res.StatusCode, tC.response, string(resBody)) - } - if tC.desc == "Create no password user success" { - match, _ := regexp.MatchString(`"password":"[!-~]{16}"`, string(resBody)) - if !match { - t.Errorf("password does not match expected format or length: got %s", string(resBody)) - } - } - }) - } -} diff --git a/internal/server/middleware.go b/internal/server/middleware.go index 14cd4468..fb681e00 100644 --- a/internal/server/middleware.go +++ b/internal/server/middleware.go @@ -101,12 +101,13 @@ func adminOnly(jwtSecret []byte, handler func(http.ResponseWriter, *http.Request return func(w http.ResponseWriter, r *http.Request) { claims, err := getClaimsFromAuthorizationHeader(r.Header.Get("Authorization"), jwtSecret) if err != nil { - writeError(fmt.Sprintf("auth failed: %s", err.Error()), http.StatusUnauthorized, w) + log.Println(err) + writeError(w, http.StatusUnauthorized, "Unauthorized") return } if claims.Permissions != AdminPermission { - writeError("forbidden: admin access required", http.StatusForbidden, w) + writeError(w, http.StatusForbidden, "forbidden: admin access required") return } @@ -119,12 +120,12 @@ func adminOrUser(jwtSecret []byte, handler func(http.ResponseWriter, *http.Reque return func(w http.ResponseWriter, r *http.Request) { claims, err := getClaimsFromAuthorizationHeader(r.Header.Get("Authorization"), jwtSecret) if err != nil { - writeError(fmt.Sprintf("auth failed: %s", err.Error()), http.StatusUnauthorized, w) + writeError(w, http.StatusUnauthorized, "Unauthorized") return } if claims.Permissions != AdminPermission && claims.Permissions != UserPermission { - writeError("forbidden: admin or user access required", http.StatusForbidden, w) + writeError(w, http.StatusForbidden, "forbidden: admin or user access required") return } @@ -137,13 +138,14 @@ func adminOrMe(jwtSecret []byte, handler func(http.ResponseWriter, *http.Request return func(w http.ResponseWriter, r *http.Request) { claims, err := getClaimsFromAuthorizationHeader(r.Header.Get("Authorization"), jwtSecret) if err != nil { - writeError(fmt.Sprintf("auth failed: %s", err.Error()), http.StatusUnauthorized, w) + log.Println(err) + writeError(w, http.StatusUnauthorized, "Unauthorized") return } if claims.Permissions != AdminPermission { if r.PathValue("id") != "me" && strconv.Itoa(claims.ID) != r.PathValue("id") { - writeError("forbidden: admin access required", http.StatusForbidden, w) + writeError(w, http.StatusForbidden, "forbidden: admin access required") return } } @@ -157,19 +159,21 @@ func adminOrFirstUser(jwtSecret []byte, db *db.Database, handler func(http.Respo return func(w http.ResponseWriter, r *http.Request) { numUsers, err := db.NumUsers() if err != nil { - writeError(fmt.Sprintf("failed to get number of users: %s", err.Error()), http.StatusInternalServerError, w) + log.Println(err) + writeError(w, http.StatusInternalServerError, "Internal Error") return } if numUsers > 0 { claims, err := getClaimsFromAuthorizationHeader(r.Header.Get("Authorization"), jwtSecret) if err != nil { - writeError(fmt.Sprintf("auth failed: %s", err.Error()), http.StatusUnauthorized, w) + log.Println(err) + writeError(w, http.StatusUnauthorized, "Unauthorized") return } if claims.Permissions != AdminPermission && numUsers > 0 { - writeError("forbidden: admin access required", http.StatusForbidden, w) + writeError(w, http.StatusForbidden, "forbidden: admin access required") return } } diff --git a/internal/server/response.go b/internal/server/response.go index 4c784b0a..0b9678cd 100644 --- a/internal/server/response.go +++ b/internal/server/response.go @@ -1,17 +1,44 @@ package server import ( - "fmt" + "encoding/json" "log" "net/http" ) +// writeJSON is a helper function that writes a JSON response to the http.ResponseWriter +func writeJSON(w http.ResponseWriter, v any) error { + type response struct { + Result any `json:"result,omitempty"` + } + resp := response{Result: v} + respBytes, err := json.Marshal(&resp) + if err != nil { + return err + } + w.Header().Set("Content-Type", "application/json") + if _, err := w.Write(respBytes); err != nil { + return err + } + return nil +} + // writeError is a helper function that logs any error and writes it back as an http response -func writeError(msg string, status int, w http.ResponseWriter) { - errMsg := fmt.Sprintf("error: %s", msg) - log.Println(errMsg) +func writeError(w http.ResponseWriter, status int, message string) { + type errorResponse struct { + Error string `json:"error"` + } + log.Println(message) + resp := errorResponse{Error: message} + respBytes, err := json.Marshal(&resp) + if err != nil { + log.Printf("Error marshalling error response: %v", err) + w.WriteHeader(http.StatusInternalServerError) + return + } w.WriteHeader(status) - if _, err := w.Write([]byte(errMsg)); err != nil { - log.Printf("error writing response: %s", err.Error()) + _, err = w.Write(respBytes) + if err != nil { + log.Printf("Error writing error response: %v", err) } } diff --git a/internal/server/router.go b/internal/server/router.go index d94a4879..362a69df 100644 --- a/internal/server/router.go +++ b/internal/server/router.go @@ -11,19 +11,19 @@ import ( // then builds and returns it for a server to consume func NewHandler(config *HandlerConfig) http.Handler { apiV1Router := http.NewServeMux() - apiV1Router.HandleFunc("GET /certificate_requests", adminOrUser(config.JWTSecret, GetCertificateRequests(config))) - apiV1Router.HandleFunc("POST /certificate_requests", adminOrUser(config.JWTSecret, PostCertificateRequest(config))) + apiV1Router.HandleFunc("GET /certificate_requests", adminOrUser(config.JWTSecret, ListCertificateRequests(config))) + apiV1Router.HandleFunc("POST /certificate_requests", adminOrUser(config.JWTSecret, CreateCertificateRequest(config))) apiV1Router.HandleFunc("GET /certificate_requests/{id}", adminOrUser(config.JWTSecret, GetCertificateRequest(config))) apiV1Router.HandleFunc("DELETE /certificate_requests/{id}", adminOrUser(config.JWTSecret, DeleteCertificateRequest(config))) - apiV1Router.HandleFunc("POST /certificate_requests/{id}/certificate", adminOrUser(config.JWTSecret, PostCertificate(config))) + apiV1Router.HandleFunc("POST /certificate_requests/{id}/certificate", adminOrUser(config.JWTSecret, CreateCertificate(config))) apiV1Router.HandleFunc("POST /certificate_requests/{id}/certificate/reject", adminOrUser(config.JWTSecret, RejectCertificate(config))) apiV1Router.HandleFunc("DELETE /certificate_requests/{id}/certificate", adminOrUser(config.JWTSecret, DeleteCertificate(config))) - apiV1Router.HandleFunc("GET /accounts", adminOnly(config.JWTSecret, GetUserAccounts(config))) - apiV1Router.HandleFunc("POST /accounts", adminOrFirstUser(config.JWTSecret, config.DB, PostUserAccount(config))) - apiV1Router.HandleFunc("GET /accounts/{id}", adminOrMe(config.JWTSecret, GetUserAccount(config))) - apiV1Router.HandleFunc("DELETE /accounts/{id}", adminOnly(config.JWTSecret, DeleteUserAccount(config))) - apiV1Router.HandleFunc("POST /accounts/{id}/change_password", adminOrMe(config.JWTSecret, ChangeUserAccountPassword(config))) + apiV1Router.HandleFunc("GET /accounts", adminOnly(config.JWTSecret, ListAccounts(config))) + apiV1Router.HandleFunc("POST /accounts", adminOrFirstUser(config.JWTSecret, config.DB, CreateAccount(config))) + apiV1Router.HandleFunc("GET /accounts/{id}", adminOrMe(config.JWTSecret, GetAccount(config))) + apiV1Router.HandleFunc("DELETE /accounts/{id}", adminOnly(config.JWTSecret, DeleteAccount(config))) + apiV1Router.HandleFunc("POST /accounts/{id}/change_password", adminOrMe(config.JWTSecret, ChangeAccountPassword(config))) m := metrics.NewMetricsSubsystem(config.DB) frontendHandler := newFrontendFileServer() @@ -40,7 +40,7 @@ func NewHandler(config *HandlerConfig) http.Handler { router := http.NewServeMux() router.HandleFunc("POST /login", Login(config)) - router.HandleFunc("/status", HealthCheck(config)) + router.HandleFunc("GET /status", GetStatus(config)) router.Handle("/metrics", m.Handler) router.Handle("/api/v1/", http.StripPrefix("/api/v1", apiMiddlewareStack(apiV1Router))) router.Handle("/", metricsMiddlewareStack(frontendHandler)) diff --git a/internal/server/server_test.go b/internal/server/server_test.go index 664d4650..17d20575 100644 --- a/internal/server/server_test.go +++ b/internal/server/server_test.go @@ -1,122 +1,25 @@ package server_test import ( - "log" "os" + "path/filepath" "testing" "github.com/canonical/notary/internal/server" ) -const ( - validCert = `-----BEGIN CERTIFICATE----- -MIIELjCCAxagAwIBAgICBnowDQYJKoZIhvcNAQELBQAwJzELMAkGA1UEBhMCVVMx -GDAWBgNVBAoTD0Nhbm9uaWNhbCwgSU5DLjAeFw0yNDA0MDUxMDAzMjhaFw0zNDA0 -MDUxMDAzMjhaMCcxCzAJBgNVBAYTAlVTMRgwFgYDVQQKEw9DYW5vbmljYWwsIElO -Qy4wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDAP98jcfNw40HbS1xR -6UpSQTp4AGldFWQZBOFaVzD+eh7sYM/BFdT0dZRHGjXxL77ewDbwdwAFJ5zuxo+u -8/VgKGRpK6KCnKailmVrdRDhA45airMRQN6QXurN4NZgXcCHJWGAQKA9XJzcwGJF -l5LxoFY58wCv0d1JP8fgmbcgIRQTCIvhrlgrJ5Acz9QP6BuaxEHKbYYvWyTWtAhi -HS/w51yEbh6959ceJGBDZPyEVd9sfGipvHrA73+33+XBluRcUuWV4dCecyP/m+8C -jTBmW5s8gS6JUDE8yl99qm7CnXTkNDqPXThrorcKRwcHrw3ZEOm5rUPLuyzGBx/C -DZUbY9bsvHJMHOHlbwiY+M2MFIO+3H6qyfPfcHs8NFkrZh/as+9hrEzSYcz+tGBi -NynkSmNPQi4yzT00ilKYgcBhPdDDlBbdhcmdeFA3XE880VkQdJgefsYpCgYRdILm -DDd6ZMfZsQOJjuRC8rQKLO+z1X5JhiOlkNxZaOkq9b9eu7230rxTFCGocn0l9oKw -0q8OIDOTb7UKdIaGq/y++uRxe0hhNoijN1OJvh+R3/KGuztu5Y8ejksIxKBrUqCg -bUDXmQ82xbdJ36qF+NHBqFqFaKhH1XuK6eAIfqgQam/u9HNZZw3mOdm9rvIZfwIT -F9gvSwm1bxzyIHL/zWOgyfzckQIDAQABo2QwYjAOBgNVHQ8BAf8EBAMCB4AwHQYD -VR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMBMA4GA1UdDgQHBAUBAgMEBjAhBgNV -HREEGjAYhwR/AAABhxAAAAAAAAAAAAAAAAAAAAABMA0GCSqGSIb3DQEBCwUAA4IB -AQB4UEu1/vTpEuuwoqgFpp8tEMewwBQ/CXPBN5seDnd/SUMXFrxk58f498qI3FQy -q98a+89jPWRGA5LY+DfIS82NYCwbKuvTzuJRoUpMPbebrhu7OQl7qQT6n8VOCy6x -IaRnPI0zEGbg2v340jMbB26FiyaFKyHEc24nnq3suZFmbslXzRE2Ebut+Qtft8he -0pSNQXtz5ULt0c8DTje7j+mRABzus45cj3HMDO4vcVRrHegdTE8YcZjwAFTKxqpg -W7GwJ5qPjnm6EMe8da55m8Q0hZchwGZreXNG7iCaw98pACBNgOOxh4LOhEZy25Bv -ayrvWnmPfg1u47sduuhHeUid ------END CERTIFICATE-----` - validPK = `-----BEGIN RSA PRIVATE KEY----- -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 ------END RSA PRIVATE KEY-----` -) - -func TestMain(m *testing.M) { - testfolder, err := os.MkdirTemp("./", "configtest-") +func TestNewSuccess(t *testing.T) { + certPath := filepath.Join("testdata", "cert.pem") + cert, err := os.ReadFile(certPath) if err != nil { - log.Fatalf("couldn't create temp directory: %s", err) + t.Fatalf("cannot read file: %s", err) } - err = os.WriteFile(testfolder+"/cert_test.pem", []byte(validCert), 0o644) + keyPath := filepath.Join("testdata", "key.pem") + key, err := os.ReadFile(keyPath) if err != nil { - log.Fatalf("couldn't create temp testing file: %s", err) + t.Fatalf("cannot read file: %s", err) } - err = os.WriteFile(testfolder+"/key_test.pem", []byte(validPK), 0o644) - if err != nil { - log.Fatalf("couldn't create temp testing file: %s", err) - } - if err := os.Chdir(testfolder); err != nil { - log.Fatalf("couldn't enter testing directory: %s", err) - } - - exitval := m.Run() - - if err := os.Chdir("../"); err != nil { - log.Fatalf("couldn't change back to parent directory: %s", err) - } - if err := os.RemoveAll(testfolder); err != nil { - log.Fatalf("couldn't remove temp testing directory: %s", err) - } - os.Exit(exitval) -} - -func TestNewSuccess(t *testing.T) { - s, err := server.New(8000, []byte(validCert), []byte(validPK), "certs.db", false) + s, err := server.New(8000, []byte(cert), []byte(key), "certs.db", false) if err != nil { t.Errorf("Error occured: %s", err) } @@ -126,7 +29,12 @@ func TestNewSuccess(t *testing.T) { } func TestInvalidKeyFailure(t *testing.T) { - _, err := server.New(8000, []byte(validCert), []byte{}, "certs.db", false) + certPath := filepath.Join("testdata", "cert.pem") + cert, err := os.ReadFile(certPath) + if err != nil { + t.Fatalf("cannot read file: %s", err) + } + _, err = server.New(8000, []byte(cert), []byte{}, "certs.db", false) if err == nil { t.Errorf("No error was thrown for invalid key") } diff --git a/internal/server/testdata/cert.pem b/internal/server/testdata/cert.pem new file mode 100644 index 00000000..c51d2fb5 --- /dev/null +++ b/internal/server/testdata/cert.pem @@ -0,0 +1,25 @@ +-----BEGIN CERTIFICATE----- +MIIELjCCAxagAwIBAgICBnowDQYJKoZIhvcNAQELBQAwJzELMAkGA1UEBhMCVVMx +GDAWBgNVBAoTD0Nhbm9uaWNhbCwgSU5DLjAeFw0yNDA0MDUxMDAzMjhaFw0zNDA0 +MDUxMDAzMjhaMCcxCzAJBgNVBAYTAlVTMRgwFgYDVQQKEw9DYW5vbmljYWwsIElO +Qy4wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDAP98jcfNw40HbS1xR +6UpSQTp4AGldFWQZBOFaVzD+eh7sYM/BFdT0dZRHGjXxL77ewDbwdwAFJ5zuxo+u +8/VgKGRpK6KCnKailmVrdRDhA45airMRQN6QXurN4NZgXcCHJWGAQKA9XJzcwGJF +l5LxoFY58wCv0d1JP8fgmbcgIRQTCIvhrlgrJ5Acz9QP6BuaxEHKbYYvWyTWtAhi +HS/w51yEbh6959ceJGBDZPyEVd9sfGipvHrA73+33+XBluRcUuWV4dCecyP/m+8C +jTBmW5s8gS6JUDE8yl99qm7CnXTkNDqPXThrorcKRwcHrw3ZEOm5rUPLuyzGBx/C +DZUbY9bsvHJMHOHlbwiY+M2MFIO+3H6qyfPfcHs8NFkrZh/as+9hrEzSYcz+tGBi +NynkSmNPQi4yzT00ilKYgcBhPdDDlBbdhcmdeFA3XE880VkQdJgefsYpCgYRdILm +DDd6ZMfZsQOJjuRC8rQKLO+z1X5JhiOlkNxZaOkq9b9eu7230rxTFCGocn0l9oKw +0q8OIDOTb7UKdIaGq/y++uRxe0hhNoijN1OJvh+R3/KGuztu5Y8ejksIxKBrUqCg +bUDXmQ82xbdJ36qF+NHBqFqFaKhH1XuK6eAIfqgQam/u9HNZZw3mOdm9rvIZfwIT +F9gvSwm1bxzyIHL/zWOgyfzckQIDAQABo2QwYjAOBgNVHQ8BAf8EBAMCB4AwHQYD +VR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMBMA4GA1UdDgQHBAUBAgMEBjAhBgNV +HREEGjAYhwR/AAABhxAAAAAAAAAAAAAAAAAAAAABMA0GCSqGSIb3DQEBCwUAA4IB +AQB4UEu1/vTpEuuwoqgFpp8tEMewwBQ/CXPBN5seDnd/SUMXFrxk58f498qI3FQy +q98a+89jPWRGA5LY+DfIS82NYCwbKuvTzuJRoUpMPbebrhu7OQl7qQT6n8VOCy6x +IaRnPI0zEGbg2v340jMbB26FiyaFKyHEc24nnq3suZFmbslXzRE2Ebut+Qtft8he +0pSNQXtz5ULt0c8DTje7j+mRABzus45cj3HMDO4vcVRrHegdTE8YcZjwAFTKxqpg +W7GwJ5qPjnm6EMe8da55m8Q0hZchwGZreXNG7iCaw98pACBNgOOxh4LOhEZy25Bv +ayrvWnmPfg1u47sduuhHeUid +-----END CERTIFICATE----- \ No newline at end of file diff --git a/internal/server/testdata/csr1.pem b/internal/server/testdata/csr1.pem new file mode 100644 index 00000000..76cca2dc --- /dev/null +++ b/internal/server/testdata/csr1.pem @@ -0,0 +1,17 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIICsTCCAZkCAQAwbDELMAkGA1UEBhMCQ0ExFDASBgNVBAgMC05vdmEgU2NvdGlh +MRAwDgYDVQQHDAdIYWxpZmF4MSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0 +eSBMdGQxEjAQBgNVBAMMCWFwcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBAOhDSpNbeFiXMQzQcobExHqYMEGzqpX8N9+AR6/HPZWBybgx1hr3 +ejqsKornzpVph/dO9UC7O9aBlG071O9VQGHt3OU3rkZIk2009vYwLuSrAlJtnUne +p7KKn2lZGvh7jVyZE5RkS0X27vlT0soANsmcVq/82VneHrF/nbDcK6DOjQpS5o5l +EiNk2CIpYGUkw3WnQF4pBk8t4bNOl3nfpaAOfnmNuBX3mWyfPnaKMCENMpDqL9FR +V/O5bIPLmyH30OHUEJUkWOmFt9GFi+QfMoM0fR34KmRbDz79hZZb/yVPZZJl7l6i +FWXkNR3gxdEnwCZkTgWk5OqS9dCJOtsDE8ECAwEAAaAAMA0GCSqGSIb3DQEBCwUA +A4IBAQCqBX5WaNv/HjkzAyNXYuCToCb8GjmiMqL54t+1nEI1QTm6axQXivEbQT3x +GIh7uQYC06wHE23K6Znc1/G+o3y6lID07rvhBNal1qoXUiq6CsAqk+DXYdd8MEh5 +joerEedFqcW+WTUDcqddfIyDAGPqrM9j6/E+aFYyZjJ/xRuMf1zlWMljRiwj1NI9 +NxqjsYYQ3zxfUjv8gxXm0hN8Up1O9saoEF+zbuWNdiUWd6Ih3/3u5VBNSxgVOrDQ +CeXyyzkMx1pWTx0rWa7NSa+DMKVVzv46pck/9kLB4gPL8zqvIOMQsf74N0VcbVfd +9jQR8mPXQYPUERl1ZhNrkzkyA0kd +-----END CERTIFICATE REQUEST----- \ No newline at end of file diff --git a/internal/server/testdata/csr2.pem b/internal/server/testdata/csr2.pem new file mode 100644 index 00000000..730b799e --- /dev/null +++ b/internal/server/testdata/csr2.pem @@ -0,0 +1,17 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIICrjCCAZYCAQAwaTELMAkGA1UEBhMCVFIxDjAMBgNVBAgMBUl6bWlyMRIwEAYD +VQQHDAlOYXJsaWRlcmUxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0 +ZDETMBEGA1UEAwwKYmFuYW5hLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC +AQoCggEBAK+vJMxO1GTty09/E4M/RbTCPABleCuYc/uzj72KWaIvoDaanuJ4NBWM +2aUiepxWdMNTR6oe31gLq4agLYT309tXwCeBLQnOxvBFWONmBG1qo0fQkvT5kSoq +AO29D7hkQ0gVwg7EF3qOd0JgbDm/yvexKpYLVvWMQAngHwZRnd5vHGk6M3P7G4oG +mIj/CL2bF6va7GWODYHb+a7jI1nkcsrk+vapc+doVszcoJ+2ryoK6JndOSGjt9SD +uxulWZHQO32XC0btyub63pom4QxRtRXmb1mjM37XEwXJSsQO1HOnmc6ycqUK53p0 +jF8Qbs0m8y/p2NHFGTUfiyNYA3EdkjUCAwEAAaAAMA0GCSqGSIb3DQEBCwUAA4IB +AQA+hq8kS2Y1Y6D8qH97Mnnc6Ojm61Q5YJ4MghaTD+XXbueTCx4DfK7ujYzK3IEF +pH1AnSeJCsQeBdjT7p6nv5GcwqWXWztNKn9zibXiASK/yYKwqvQpjSjSeqGEh+Sa +9C9SHeaPhZrJRj0i3NkqmN8moWasF9onW6MNKBX0B+pvBB+igGPcjCIFIFGUUaky +upMXY9IG3LlWvlt+HTfuMZV+zSOZgD9oyqkh5K9XRKNq/mnNz/1llUCBZRmfeRBY ++sJ4M6MJRztiyX4/Fjb8UHQviH931rkiEGtG826IvWIyiRSnAeE8B/VzL0GlT9Zq +ge6lFRxB1FlDuU4Blef8FnOI +-----END CERTIFICATE REQUEST----- \ No newline at end of file diff --git a/internal/server/testdata/csr2_cert.pem b/internal/server/testdata/csr2_cert.pem new file mode 100644 index 00000000..524bd7f3 --- /dev/null +++ b/internal/server/testdata/csr2_cert.pem @@ -0,0 +1,26 @@ +-----BEGIN CERTIFICATE----- +MIIEUTCCAjkCFE8lmuBE85/RPw2M17Kzl93O+9IIMA0GCSqGSIb3DQEBCwUAMGEx +CzAJBgNVBAYTAlRSMQ4wDAYDVQQIDAVJem1pcjESMBAGA1UEBwwJTmFybGlkZXJl +MSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxCzAJBgNVBAMMAm1l +MB4XDTI0MDYyODA4NDIyMFoXDTI1MDYyODA4NDIyMFowaTELMAkGA1UEBhMCVFIx +DjAMBgNVBAgMBUl6bWlyMRIwEAYDVQQHDAlOYXJsaWRlcmUxITAfBgNVBAoMGElu +dGVybmV0IFdpZGdpdHMgUHR5IEx0ZDETMBEGA1UEAwwKYmFuYW5hLmNvbTCCASIw +DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK+vJMxO1GTty09/E4M/RbTCPABl +eCuYc/uzj72KWaIvoDaanuJ4NBWM2aUiepxWdMNTR6oe31gLq4agLYT309tXwCeB +LQnOxvBFWONmBG1qo0fQkvT5kSoqAO29D7hkQ0gVwg7EF3qOd0JgbDm/yvexKpYL +VvWMQAngHwZRnd5vHGk6M3P7G4oGmIj/CL2bF6va7GWODYHb+a7jI1nkcsrk+vap +c+doVszcoJ+2ryoK6JndOSGjt9SDuxulWZHQO32XC0btyub63pom4QxRtRXmb1mj +M37XEwXJSsQO1HOnmc6ycqUK53p0jF8Qbs0m8y/p2NHFGTUfiyNYA3EdkjUCAwEA +ATANBgkqhkiG9w0BAQsFAAOCAgEAVZJZD0/ojZSOVIesZvrjLG0agSp0tsXY+hEt +I/knpYLvRcAd8b3Jx9gk+ug+FwDQ4IBIkTX18qhK2fgVUuMR/ubfpQeCMbp64N3Q +kmN/E1eu0bl6hhHAL7jEbi0DE3vAN9huQxAIu5pCyLvZIrPJtvuyj2jOpJBZwGoP +539lfEM++XALzI4qKQ6Z0a0rJZ4HoruKiYwEFZ7VkmRLD0uef6NMZRqa/Vx+o0uT +1TjH4AeDDmJmP/aHlHbpXkHQ9h9rfTa6Qbypo+T9pGDhd02O1tEqrHfiQyNWJxb0 +rbR+owT32iCfayzKKqhmAYSF2d9XKWEhulgxWDaXgvUbq4Y+fgfU2qMVz5uusTDh +a9Mp9dsYWySWEUcEa4v2w6FfaaVXE1S9ubm+HoIVtotuutL5fn86q19pAAePYjLQ +ybiETp5LU3chuYmMlCiDRNGHYhN5nvGcttqRdWIBe454RRPNo4iGVl13l6aG8rmI +xDfk5lIwObalbELv+mEIGI1j/j4//nJFXByxlLHm5/BF8rmvHDj1aPtPRw9DLgSX +ejhjjec1xnkBR+JF0g474hLdPjCnA0aqLQInZbjJJm5iXzyXBg1cy7KvIBy3ZkrR +Pp7ObjaWxjCT3O6nEH3w6Ozsyg2cHXQIdVXLvNnV1bxUbPnfhQosKGKgU6s+lcLM +SRhHB2k= +-----END CERTIFICATE----- \ No newline at end of file diff --git a/internal/server/testdata/issuer_cert.pem b/internal/server/testdata/issuer_cert.pem new file mode 100644 index 00000000..470829f5 --- /dev/null +++ b/internal/server/testdata/issuer_cert.pem @@ -0,0 +1,33 @@ +-----BEGIN CERTIFICATE----- +MIIFozCCA4ugAwIBAgIUDjtO3bEluUX3tzvrckATlycRVfwwDQYJKoZIhvcNAQEL +BQAwYTELMAkGA1UEBhMCVFIxDjAMBgNVBAgMBUl6bWlyMRIwEAYDVQQHDAlOYXJs +aWRlcmUxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDELMAkGA1UE +AwwCbWUwHhcNMjQwNjI4MDYwNTQ5WhcNMzQwNjI2MDYwNTQ5WjBhMQswCQYDVQQG +EwJUUjEOMAwGA1UECAwFSXptaXIxEjAQBgNVBAcMCU5hcmxpZGVyZTEhMB8GA1UE +CgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMQswCQYDVQQDDAJtZTCCAiIwDQYJ +KoZIhvcNAQEBBQADggIPADCCAgoCggIBAJU+5YaFlpn+bWvVri5L6EkmbAPuavsI +/KXY7ufRmc5qb08o1na9lLJ/7TuMD4K36Idnq20n1JohSlrdymBpNZ8O3m5fYYtk +hx5WADlBZsKnC5aZJIChEb4bYcOFLP+d3PooVsAKBxW0Q6TECviQcK7GxaxEZw0L +7FRhX2c9+CxbvRGP6OGVggXZxwkZik/JJ9aym+fltt9QvlxQVBq/GlFYZYC+H8jV +Z6RnUjugnWcTm9PAsQ6+EHEevAW+dWaDP+gr9AgKKz1EXbc1mVKAVOLHjb+Ue7RC +vFoar/YxYIszD58dOSB/GuAxn+JAjWbnOu7jeX3XeWlKOagUJF9L9TgMIUWdiuJG +8Uu/kK2MjyRFdT8opnPFAXrK7vSuMBzhRtswAlWc8xoZWeSQF+NpjU+swbg8ySYT +LfZxVB+s/ftxnGU3RM/RWdbZhb0DAuIBsFAGCbnj+Q61/cK4i58JVjUqzLk+XOwR +55LAyS0Y5pj9jDc5mqvS0z7ot7s2OBM1+o8e3KJgdMSXorYkv3toHMGEIUmPQZCX +JtRCjFNgnoWeLDc+oLiN6BlPx7bS4MDN9tMPCJwF6vnxFzLAzdRqY3D7uRS3chsx +7ClMR9MDsSxplC7tptXgv8UTzh1XZjWGCeZq0Gbe927Hmwy2q8k/BFwnR4PIVSiE +7YAZPb0CPmrfAgMBAAGjUzBRMB0GA1UdDgQWBBRgLXukRHTovOG6g9Z5eCaeh6Sx +aTAfBgNVHSMEGDAWgBRgLXukRHTovOG6g9Z5eCaeh6SxaTAPBgNVHRMBAf8EBTAD +AQH/MA0GCSqGSIb3DQEBCwUAA4ICAQA9TpgTrGmnyxKB2ne76LNQadiijVPpS6/U +OPFAX4EPJ0V5DhDreJjsZJC6Is2Q9+qsPpn/nlW7bvZUVHGodUKcE+TQWFiMtLvu +8ifzk8x1R46aqhTyxb7WBBFfvbvdmlEENKTmTS6A/C3nYgmkfk5N7x84iTowmsVl +Yzz9iRzxkqQ+mU3L2/Sp5nXPYWfzV9WXIJdxWcot7f4CJ79eVFu4D9hYfzcPQ9P9 +0qCBRbH/01D2E/3uTHhZPPmK2Tp1ao5SuGLppjMPX8VWVL5CMTXOj+1LF0nJJc/J +9MrqXwtlLyKGP6HX8qALbaXwcv7db6bF+aEsgWmIEB+0ecGk9IR3XQn7I379CO3v +J2oUCZ++lV9e2tcRehUprE1v8i+DFhPtS1iNjrO7KnDYkXimR5zI+3sGFI9/9wY0 +4PAV/roZFiEJHe5kA49vwIihJaDgy/SPIYgG/vhdj+WeIbi1ilEi12ou7VF0tyiE +j3eXaMAL8EAKxCUZbXcuwmK9qistAYXBFFEK9M08FwLH8HM4LoPjshMg3II9Ncs8 +p3to8U99/ZeFbJRzEUF9poZ7VwxBEcgfWD1RV0+gNLC3Au2yuc4C3anknOv7Db/r +jdzVA8yTI8cZ/RtRohp5H/s+j2tcdfB3Zt+wfS4nLxqN/kf7qv2VSdPbXyTyz/ft +btZkbfdL5A== +-----END CERTIFICATE----- \ No newline at end of file diff --git a/internal/server/testdata/key.pem b/internal/server/testdata/key.pem new file mode 100644 index 00000000..5cf5dbc5 --- /dev/null +++ b/internal/server/testdata/key.pem @@ -0,0 +1,51 @@ +-----BEGIN RSA PRIVATE KEY----- +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 +-----END RSA PRIVATE KEY----- \ No newline at end of file diff --git a/ui/src/app/initialize/page.tsx b/ui/src/app/initialize/page.tsx index 4076be90..e6db2af0 100644 --- a/ui/src/app/initialize/page.tsx +++ b/ui/src/app/initialize/page.tsx @@ -23,9 +23,9 @@ export default function Initialize() { router.push("/login") } const loginMutation = useMutation(login, { - onSuccess: (e) => { + onSuccess: (result) => { setErrorText("") - setCookie('user_token', e, { + setCookie('user_token', result?.token, { sameSite: true, secure: true, expires: new Date(new Date().getTime() + 60 * 60 * 1000), diff --git a/ui/src/app/login/page.tsx b/ui/src/app/login/page.tsx index 0f739d1a..84bc81a0 100644 --- a/ui/src/app/login/page.tsx +++ b/ui/src/app/login/page.tsx @@ -21,9 +21,9 @@ export default function LoginPage() { router.push("/initialize") } const mutation = useMutation(login, { - onSuccess: (e) => { + onSuccess: (result) => { setErrorText("") - setCookie('user_token', e, { + setCookie('user_token', result?.token, { sameSite: true, secure: true, expires: new Date(new Date().getTime() + 60 * 60 * 1000), diff --git a/ui/src/app/queries.ts b/ui/src/app/queries.ts index bc82f1cf..8c1d8967 100644 --- a/ui/src/app/queries.ts +++ b/ui/src/app/queries.ts @@ -10,57 +10,66 @@ export type RequiredCSRParams = { export async function getStatus() { const response = await fetch("/status") + const respData = await response.json(); if (!response.ok) { - throw new Error(`${response.status}: ${HTTPStatus(response.status)}`) + throw new Error(`${response.status}: ${HTTPStatus(response.status)}. ${respData.error}`) } - return response.json() + return respData.result } export async function getCertificateRequests(params: { authToken: string }): Promise { const response = await fetch("/api/v1/certificate_requests", { headers: { "Authorization": "Bearer " + params.authToken } }) + const respData = await response.json(); if (!response.ok) { - throw new Error(`${response.status}: ${HTTPStatus(response.status)}`) + throw new Error(`${response.status}: ${HTTPStatus(response.status)}. ${respData.error}`) } - return response.json() + return respData.result } export async function postCSR(params: { authToken: string, csr: string }) { if (!params.csr) { throw new Error('CSR not provided') } + const reqParams = { + "csr": params.csr.trim() + } const response = await fetch("/api/v1/certificate_requests", { method: 'post', headers: { 'Content-Type': 'text/plain', 'Authorization': "Bearer " + params.authToken }, - body: params.csr.trim() + body: JSON.stringify(reqParams) }) + const respData = await response.json(); if (!response.ok) { - const responseText = await response.text() - throw new Error(`${response.status}: ${HTTPStatus(response.status)}. ${responseText}`) + throw new Error(`${response.status}: ${HTTPStatus(response.status)}. ${respData.error}`) } - return response.json() + return respData.result } export async function postCertToID(params: RequiredCSRParams) { if (!params.cert) { throw new Error('Certificate not provided') } + const reqParams = { + "cert": params.cert.trim() + } const response = await fetch("/api/v1/certificate_requests/" + params.id + "/certificate", { method: 'post', headers: { 'Content-Type': 'text/plain', 'Authorization': "Bearer " + params.authToken }, - body: params.cert.trim() + body: JSON.stringify(reqParams) }) + const respData = await response.json(); if (!response.ok) { - throw new Error(`${response.status}: ${HTTPStatus(response.status)}`) + throw new Error(`${response.status}: ${HTTPStatus(response.status)}. ${respData.error}`) } - return response.json() + return respData.result } export async function deleteCSR(params: RequiredCSRParams) { @@ -70,10 +79,11 @@ export async function deleteCSR(params: RequiredCSRParams) { 'Authorization': "Bearer " + params.authToken } }) + const respData = await response.json(); if (!response.ok) { - throw new Error(`${response.status}: ${HTTPStatus(response.status)}`) + throw new Error(`${response.status}: ${HTTPStatus(response.status)}. ${respData.error}`) } - return response.json() + return respData.result } export async function rejectCSR(params: RequiredCSRParams) { @@ -83,10 +93,11 @@ export async function rejectCSR(params: RequiredCSRParams) { 'Authorization': "Bearer " + params.authToken } }) + const respData = await response.json(); if (!response.ok) { - throw new Error(`${response.status}: ${HTTPStatus(response.status)}`) + throw new Error(`${response.status}: ${HTTPStatus(response.status)}. ${respData.error}`) } - return response.json() + return respData.result } export async function revokeCertificate(params: RequiredCSRParams) { @@ -96,10 +107,11 @@ export async function revokeCertificate(params: RequiredCSRParams) { 'Authorization': 'Bearer ' + params.authToken } }) + const respData = await response.json(); if (!response.ok) { - throw new Error(`${response.status}: ${HTTPStatus(response.status)}`) + throw new Error(`${response.status}: ${HTTPStatus(response.status)}. ${respData.error}`) } - return response.json() + return respData.result } export async function login(userForm: { username: string, password: string }) { @@ -108,11 +120,11 @@ export async function login(userForm: { username: string, password: string }) { body: JSON.stringify({ "username": userForm.username, "password": userForm.password }) }) - const responseText = await response.text() + const respData = await response.json(); if (!response.ok) { - throw new Error(`${response.status}: ${HTTPStatus(response.status)}. ${responseText}`) + throw new Error(`${response.status}: ${HTTPStatus(response.status)}. ${respData.error}`) } - return responseText + return respData.result } export async function changeSelfPassword(changePasswordForm: { authToken: string, password: string }) { @@ -123,11 +135,11 @@ export async function changeSelfPassword(changePasswordForm: { authToken: string }, body: JSON.stringify({ "password": changePasswordForm.password }) }) - const responseText = await response.text() + const respData = await response.json(); if (!response.ok) { - throw new Error(`${response.status}: ${HTTPStatus(response.status)}. ${responseText}`) + throw new Error(`${response.status}: ${HTTPStatus(response.status)}. ${respData.error}`) } - return responseText + return respData.result } export async function changePassword(changePasswordForm: { authToken: string, id: string, password: string }) { @@ -138,21 +150,22 @@ export async function changePassword(changePasswordForm: { authToken: string, id }, body: JSON.stringify({ "password": changePasswordForm.password }) }) - const responseText = await response.text() + const respData = await response.json(); if (!response.ok) { - throw new Error(`${response.status}: ${HTTPStatus(response.status)}. ${responseText}`) + throw new Error(`${response.status}: ${HTTPStatus(response.status)}. ${respData.error}`) } - return responseText + return respData.result } -export async function getUsers(params: { authToken: string }): Promise { +export async function ListUsers(params: { authToken: string }): Promise { const response = await fetch("/api/v1/accounts", { headers: { "Authorization": "Bearer " + params.authToken } }) + const respData = await response.json(); if (!response.ok) { - throw new Error(`${response.status}: ${HTTPStatus(response.status)}`) + throw new Error(`${response.status}: ${HTTPStatus(response.status)}. ${respData.error}`) } - return response.json() + return respData.result } export async function deleteUser(params: { authToken: string, id: string }) { @@ -162,10 +175,11 @@ export async function deleteUser(params: { authToken: string, id: string }) { 'Authorization': "Bearer " + params.authToken } }) + const respData = await response.json(); if (!response.ok) { - throw new Error(`${response.status}: ${HTTPStatus(response.status)}`) + throw new Error(`${response.status}: ${HTTPStatus(response.status)}. ${respData.error}`) } - return response.json() + return respData.result } export async function postFirstUser(userForm: { username: string, password: string }) { @@ -173,11 +187,11 @@ export async function postFirstUser(userForm: { username: string, password: stri method: "POST", body: JSON.stringify({ "username": userForm.username, "password": userForm.password }) }) - const responseText = await response.text() + const respData = await response.json(); if (!response.ok) { - throw new Error(`${response.status}: ${HTTPStatus(response.status)}. ${responseText}`) + throw new Error(`${response.status}: ${HTTPStatus(response.status)}. ${respData.error}`) } - return responseText + return respData.result } export async function postUser(userForm: { authToken: string, username: string, password: string }) { @@ -190,9 +204,9 @@ export async function postUser(userForm: { authToken: string, username: string, 'Authorization': "Bearer " + userForm.authToken } }) - const responseText = await response.text() + const respData = await response.json(); if (!response.ok) { - throw new Error(`${response.status}: ${HTTPStatus(response.status)}. ${responseText}`) + throw new Error(`${response.status}: ${HTTPStatus(response.status)}. ${respData.error}`) } - return responseText + return respData.result } \ No newline at end of file diff --git a/ui/src/app/users/page.tsx b/ui/src/app/users/page.tsx index 2780cb00..f7043fb8 100644 --- a/ui/src/app/users/page.tsx +++ b/ui/src/app/users/page.tsx @@ -1,7 +1,7 @@ "use client" import { useQuery } from "react-query" -import { getUsers } from "../queries" +import { ListUsers } from "../queries" import { UserEntry } from "../types" import { useCookies } from "react-cookie" import { useRouter } from "next/navigation" @@ -17,7 +17,7 @@ export default function Users() { } const query = useQuery({ queryKey: ['users', cookies.user_token], - queryFn: () => getUsers({ authToken: cookies.user_token }), + queryFn: () => ListUsers({ authToken: cookies.user_token }), retry: (failureCount, error): boolean => { if (error.message.includes("401")) { return false