From 770fab0545a7f5be11f8fa492ce5398c4b64c5fe Mon Sep 17 00:00:00 2001 From: dhgreene Date: Wed, 13 Nov 2019 10:37:49 -0500 Subject: [PATCH] BCDA-2212 Maintenance: Removing SSAS code and artifacts (#420) --- .travis.yml | 1 - Dockerfiles/Dockerfile.ssas | 10 +- Dockerfiles/Dockerfile.ssas-migrate | 9 + Gopkg.lock | 9 - Gopkg.toml | 4 - Makefile | 38 +- docker-compose.ssas-migrate.yml | 7 + docker-compose.test.yml | 35 - docker-compose.yml | 8 +- ops/build_and_package.sh | 15 - shared_files/ssas/admin_test_signing_key.pem | 27 - .../ssas/bad_base64_test_private_key.pem | 27 - shared_files/ssas/good_test_private_key.pem | 27 - .../ssas/not_rsa_test_private_key.pem | 8 - shared_files/ssas/public_test_signing_key.pem | 27 - .../ssas/too_small_test_private_key.pem | 15 - shared_files/ssas/unit_test_private_key.pem | 27 - ssas/README.md | 137 -- ssas/blacklist.go | 79 - ssas/blacklist_test.go | 70 - ssas/cfg/envv.go | 30 - ssas/cfg/envv_test.go | 62 - ssas/connection.go | 48 - ssas/connection_test.go | 65 - ssas/groups.go | 282 --- ssas/groups_test.go | 218 --- ssas/hash.go | 94 - ssas/hash_test.go | 48 - ssas/logger.go | 155 -- ssas/logger_test.go | 21 - ssas/okta/okta.go | 142 -- ssas/okta/okta_test.go | 58 - ssas/rsakeys.go | 165 -- ssas/rsakeys_test.go | 100 -- ssas/service/admin/api.go | 232 --- ssas/service/admin/api_test.go | 458 ----- ssas/service/admin/middleware.go | 42 - ssas/service/admin/router.go | 48 - ssas/service/admin/router_test.go | 178 -- ssas/service/logging.go | 107 -- ssas/service/main/main.go | 242 --- ssas/service/main/main_test.go | 122 -- ssas/service/public/api.go | 535 ------ ssas/service/public/api_test.go | 270 --- ssas/service/public/mfaprovider.go | 116 -- ssas/service/public/mfaprovider_test.go | 81 - ssas/service/public/middleware.go | 145 -- ssas/service/public/middleware_test.go | 270 --- ssas/service/public/mockmfa.go | 152 -- ssas/service/public/mockmfa_test.go | 186 -- ssas/service/public/oktalive_test.go | 215 --- ssas/service/public/oktamfa.go | 565 ------ ssas/service/public/oktamfa_test.go | 784 --------- ssas/service/public/router.go | 48 - ssas/service/public/router_test.go | 142 -- ssas/service/public/tokens.go | 144 -- ssas/service/public/tokens_test.go | 106 -- ssas/service/server.go | 273 --- ssas/service/server_test.go | 114 -- ssas/service/tokenblacklist.go | 137 -- ssas/service/tokenblacklist_test.go | 273 --- ssas/systems.go | 570 ------- ssas/systems_test.go | 707 -------- ssas/testutils.go | 55 - .../BCDA-SSAS.postman_collection.json | 453 ----- .../postman_test/SSAS.postman_collection.json | 1072 ------------ .../SSAS_Smoke_Test.postman_collection.json | 1513 ----------------- .../manual-SSAS.postman_collection.json | 421 ----- .../ssas-local.postman_environment.json | 54 - test/smoke_test/bulk_data_requests.sh | 11 + test/smoke_test/smoke_test.sh | 41 +- test/smoke_test/ssas_test.sh | 29 - unit_test.sh | 12 - unit_test_ssas.sh | 40 - .../patrickmn/go-cache/CONTRIBUTORS | 9 - vendor/github.com/patrickmn/go-cache/LICENSE | 19 - .../github.com/patrickmn/go-cache/README.md | 83 - vendor/github.com/patrickmn/go-cache/cache.go | 1161 ------------- .../github.com/patrickmn/go-cache/sharded.go | 192 --- 79 files changed, 75 insertions(+), 14420 deletions(-) create mode 100644 Dockerfiles/Dockerfile.ssas-migrate create mode 100644 docker-compose.ssas-migrate.yml delete mode 100644 shared_files/ssas/admin_test_signing_key.pem delete mode 100644 shared_files/ssas/bad_base64_test_private_key.pem delete mode 100644 shared_files/ssas/good_test_private_key.pem delete mode 100644 shared_files/ssas/not_rsa_test_private_key.pem delete mode 100644 shared_files/ssas/public_test_signing_key.pem delete mode 100644 shared_files/ssas/too_small_test_private_key.pem delete mode 100644 shared_files/ssas/unit_test_private_key.pem delete mode 100644 ssas/README.md delete mode 100644 ssas/blacklist.go delete mode 100644 ssas/blacklist_test.go delete mode 100644 ssas/cfg/envv.go delete mode 100644 ssas/cfg/envv_test.go delete mode 100644 ssas/connection.go delete mode 100644 ssas/connection_test.go delete mode 100644 ssas/groups.go delete mode 100644 ssas/groups_test.go delete mode 100644 ssas/hash.go delete mode 100644 ssas/hash_test.go delete mode 100644 ssas/logger.go delete mode 100644 ssas/logger_test.go delete mode 100644 ssas/okta/okta.go delete mode 100644 ssas/okta/okta_test.go delete mode 100644 ssas/rsakeys.go delete mode 100644 ssas/rsakeys_test.go delete mode 100644 ssas/service/admin/api.go delete mode 100644 ssas/service/admin/api_test.go delete mode 100644 ssas/service/admin/middleware.go delete mode 100644 ssas/service/admin/router.go delete mode 100644 ssas/service/admin/router_test.go delete mode 100644 ssas/service/logging.go delete mode 100644 ssas/service/main/main.go delete mode 100644 ssas/service/main/main_test.go delete mode 100644 ssas/service/public/api.go delete mode 100644 ssas/service/public/api_test.go delete mode 100644 ssas/service/public/mfaprovider.go delete mode 100644 ssas/service/public/mfaprovider_test.go delete mode 100644 ssas/service/public/middleware.go delete mode 100644 ssas/service/public/middleware_test.go delete mode 100644 ssas/service/public/mockmfa.go delete mode 100644 ssas/service/public/mockmfa_test.go delete mode 100644 ssas/service/public/oktalive_test.go delete mode 100644 ssas/service/public/oktamfa.go delete mode 100644 ssas/service/public/oktamfa_test.go delete mode 100644 ssas/service/public/router.go delete mode 100644 ssas/service/public/router_test.go delete mode 100644 ssas/service/public/tokens.go delete mode 100644 ssas/service/public/tokens_test.go delete mode 100644 ssas/service/server.go delete mode 100644 ssas/service/server_test.go delete mode 100644 ssas/service/tokenblacklist.go delete mode 100644 ssas/service/tokenblacklist_test.go delete mode 100644 ssas/systems.go delete mode 100644 ssas/systems_test.go delete mode 100644 ssas/testutils.go delete mode 100644 test/postman_test/BCDA-SSAS.postman_collection.json delete mode 100644 test/postman_test/SSAS.postman_collection.json delete mode 100644 test/postman_test/SSAS_Smoke_Test.postman_collection.json delete mode 100644 test/postman_test/manual-SSAS.postman_collection.json delete mode 100644 test/postman_test/ssas-local.postman_environment.json create mode 100755 test/smoke_test/bulk_data_requests.sh delete mode 100755 test/smoke_test/ssas_test.sh delete mode 100755 unit_test_ssas.sh delete mode 100644 vendor/github.com/patrickmn/go-cache/CONTRIBUTORS delete mode 100644 vendor/github.com/patrickmn/go-cache/LICENSE delete mode 100644 vendor/github.com/patrickmn/go-cache/README.md delete mode 100644 vendor/github.com/patrickmn/go-cache/cache.go delete mode 100644 vendor/github.com/patrickmn/go-cache/sharded.go diff --git a/.travis.yml b/.travis.yml index cd1bee300..e146a0873 100644 --- a/.travis.yml +++ b/.travis.yml @@ -45,4 +45,3 @@ before_script: script: - make docker-bootstrap - make test - - make test-ssas diff --git a/Dockerfiles/Dockerfile.ssas b/Dockerfiles/Dockerfile.ssas index 8b1b5d831..2f2fa6971 100644 --- a/Dockerfiles/Dockerfile.ssas +++ b/Dockerfiles/Dockerfile.ssas @@ -11,11 +11,11 @@ RUN go get -u github.com/derekparker/delve/cmd/dlv RUN go get github.com/BurntSushi/toml RUN go get github.com/howeyc/fsnotify RUN go get github.com/mattn/go-colorable +RUN go get github.com/CMSgov/bcda-ssas-app/ssas/service/main -WORKDIR /go/src/github.com/CMSgov/bcda-app/ssas -COPY . . +WORKDIR /go/src/github.com/CMSgov/bcda-app/ +COPY vendor/github.com/pressly/fresh vendor/github.com/pressly/fresh RUN go install ./vendor/github.com/pressly/fresh -RUN dep ensure -WORKDIR /go/src/github.com/CMSgov/bcda-app/ssas -CMD ["fresh", "-o", "ssas-service", "-p", "./service/main", "-r", "--migrate-and-start"] +WORKDIR /go/src/github.com/CMSgov/bcda-ssas-app/ssas +CMD ["fresh", "-o", "ssas-service", "-p", "./service/main", "-r", "--start"] diff --git a/Dockerfiles/Dockerfile.ssas-migrate b/Dockerfiles/Dockerfile.ssas-migrate new file mode 100644 index 000000000..9a47cb1e1 --- /dev/null +++ b/Dockerfiles/Dockerfile.ssas-migrate @@ -0,0 +1,9 @@ +FROM migrate/migrate + +RUN apk update upgrade && \ + apk add git + +RUN mkdir -p /go/src/github.com/CMSgov + +WORKDIR /go/src/github.com/CMSgov +RUN git clone https://github.com/CMSgov/bcda-ssas-app.git diff --git a/Gopkg.lock b/Gopkg.lock index 60e2f694d..cbdb27669 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -191,14 +191,6 @@ revision = "46d73e6be8b4faeee70850d0df829e4fe00d6819" version = "v2.1.0" -[[projects]] - branch = "master" - digest = "1:192ae5ee10b592d53a75b5513986dc4d322652d06bdb7eebcb768b842a309213" - name = "github.com/patrickmn/go-cache" - packages = ["."] - pruneopts = "UT" - revision = "5633e0862627c011927fa39556acae8b1f1df58a" - [[projects]] digest = "1:8aa28c7fb16a7b9afb08568f21f06aa869f18e3637f6f024413e45c2e211072b" name = "github.com/pborman/uuid" @@ -388,7 +380,6 @@ "github.com/jinzhu/gorm/dialects/postgres", "github.com/lib/pq", "github.com/newrelic/go-agent", - "github.com/patrickmn/go-cache", "github.com/pborman/uuid", "github.com/pkg/errors", "github.com/sirupsen/logrus", diff --git a/Gopkg.toml b/Gopkg.toml index c74e87a46..a0fb40d3f 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -52,7 +52,3 @@ [[constraint]] name = "github.com/aws/aws-sdk-go" version = "1.16.1" - -[[constraint]] - branch = "master" - name = "github.com/patrickmn/go-cache" diff --git a/Makefile b/Makefile index f9602f1b0..bafe630bd 100644 --- a/Makefile +++ b/Makefile @@ -13,17 +13,13 @@ package: -v ${PWD}:/go/src/github.com/CMSgov/bcda-app packaging $(version) lint: - docker-compose -f docker-compose.test.yml run --rm tests golangci-lint run --deadline=3m --skip-dirs=ssas - docker-compose -f docker-compose.test.yml run --rm tests gosec -exclude-dir=ssas ./... + docker-compose -f docker-compose.test.yml run --rm tests golangci-lint run --deadline=3m + docker-compose -f docker-compose.test.yml run --rm tests gosec ./... -lint-ssas: - docker-compose -f docker-compose.test.yml run --rm tests golangci-lint run ./ssas/... - docker-compose -f docker-compose.test.yml run --rm tests gosec ./ssas/... - -# The following vars are available to tests needing SSAS admin credentials; currently they are used in smoke-test-ssas, postman-ssas, and unit-test-ssas +# The following vars are available to tests needing SSAS admin credentials; currently they are used in smoke-test # Note that these variables should only be used for smoke tests, must be set before the api starts, and cannot be changed after the api starts SSAS_ADMIN_CLIENT_ID ?= 31e029ef-0e97-47f8-873c-0e8b7e7f99bf -SSAS_ADMIN_CLIENT_SECRET := $(shell docker-compose run --rm ssas sh -c 'tmp/ssas-service --reset-secret --client-id=$(SSAS_ADMIN_CLIENT_ID)'|tail -n1) +SSAS_ADMIN_CLIENT_SECRET := $(shell docker-compose run --rm ssas sh -c 'main --reset-secret --client-id=$(SSAS_ADMIN_CLIENT_ID)'|tail -n1) # # The following vars are used by both smoke-test and postman to pass credentials for obtaining an access token. @@ -45,11 +41,7 @@ clientTemp := $(shell docker-compose run --rm api sh -c 'tmp/bcda reset-client-c CLIENT_ID ?= $(shell echo $(clientTemp) |awk '{print $$1}') CLIENT_SECRET ?= $(shell echo $(clientTemp) |awk '{print $$2}') smoke-test: - BCDA_SSAS_CLIENT_ID=$(SSAS_ADMIN_CLIENT_ID) BCDA_SSAS_SECRET=$(SSAS_ADMIN_CLIENT_SECRET) CLIENT_ID=$(CLIENT_ID) CLIENT_SECRET=$(CLIENT_SECRET) docker-compose -f docker-compose.test.yml run --rm -w /go/src/github.com/CMSgov/bcda-app/test/smoke_test tests sh smoke_test.sh - -smoke-test-ssas: - docker-compose -f docker-compose.test.yml run --rm postman_test test/postman_test/SSAS_Smoke_Test.postman_collection.json -e test/postman_test/ssas-local.postman_environment.json --global-var "token=$(token)" --global-var adminClientId=$(SSAS_ADMIN_CLIENT_ID) --global-var adminClientSecret=$(SSAS_ADMIN_CLIENT_SECRET) - BCDA_SSAS_CLIENT_ID=$(SSAS_ADMIN_CLIENT_ID) BCDA_SSAS_SECRET=$(SSAS_ADMIN_CLIENT_SECRET) test/smoke_test/ssas_test.sh + BCDA_SSAS_CLIENT_ID=$(SSAS_ADMIN_CLIENT_ID) BCDA_SSAS_SECRET=$(SSAS_ADMIN_CLIENT_SECRET) test/smoke_test/smoke_test.sh postman: # This target should be executed by passing in an argument for the environment (dev/test/sbx) @@ -58,17 +50,10 @@ postman: # For example: make postman env=test token= docker-compose -f docker-compose.test.yml run --rm postman_test test/postman_test/BCDA_Tests_Sequential.postman_collection.json -e test/postman_test/$(env).postman_environment.json --global-var "token=$(token)" --global-var clientId=$(CLIENT_ID) --global-var clientSecret=$(CLIENT_SECRET) -postman-ssas: - docker-compose -f docker-compose.test.yml run --rm postman_test test/postman_test/SSAS.postman_collection.json -e test/postman_test/ssas-local.postman_environment.json --global-var adminClientId=$(SSAS_ADMIN_CLIENT_ID) --global-var adminClientSecret=$(SSAS_ADMIN_CLIENT_SECRET) - unit-test: docker-compose up -d db docker-compose -f docker-compose.test.yml run --rm tests bash unit_test.sh -unit-test-ssas: - docker-compose up -d db - docker-compose -f docker-compose.test.yml run --rm tests bash unit_test_ssas.sh - performance-test: docker-compose -f docker-compose.test.yml run --rm -w /go/src/github.com/CMSgov/bcda-app/test/performance_test tests sh performance_test.sh @@ -78,12 +63,6 @@ test: $(MAKE) postman env=local $(MAKE) smoke-test -test-ssas: - $(MAKE) lint-ssas - $(MAKE) unit-test-ssas - $(MAKE) postman-ssas - $(MAKE) smoke-test-ssas - load-fixtures: docker-compose up -d db echo "Wait for database to be ready..." @@ -109,9 +88,8 @@ load-synthetic-suppression-data: docker-compose run api sh -c 'tmp/bcda import-suppression-directory --directory=../shared_files/synthetic1800MedicareFiles' load-fixtures-ssas: - docker-compose up -d db - docker-compose run ssas sh -c 'tmp/ssas-service --migrate' - docker-compose run ssas sh -c 'tmp/ssas-service --add-fixture-data' + docker-compose -f docker-compose.ssas-migrate.yml run --rm ssas-migrate -database "postgres://postgres:toor@db:5432/bcda?sslmode=disable" -path /go/src/github.com/CMSgov/bcda-ssas-app/db/migrations up + docker-compose run ssas sh -c 'main --add-fixture-data' docker-build: docker-compose build --force-rm @@ -141,4 +119,4 @@ debug-worker: @-bash -c "trap 'docker-compose stop' EXIT; \ docker-compose -f docker-compose.yml -f docker-compose.debug.yml run --no-deps -T --rm -v $(shell pwd):/go/src/github.com/CMSgov/bcda-app worker dlv debug" -.PHONY: docker-build docker-bootstrap load-fixtures load-synthetic-cclf-data load-synthetic-suppression-data test debug-api debug-worker api-shell worker-shell package release smoke-test postman unit-test performance-test lint +.PHONY: api-shell debug-api debug-worker docker-bootstrap docker-build lint load-fixtures load-fixtures-ssas load-synthetic-cclf-data load-synthetic-suppression-data package performance-test postman release smoke-test test unit-test worker-shell diff --git a/docker-compose.ssas-migrate.yml b/docker-compose.ssas-migrate.yml new file mode 100644 index 000000000..c188caa1b --- /dev/null +++ b/docker-compose.ssas-migrate.yml @@ -0,0 +1,7 @@ +version: '3' + +services: + ssas-migrate: + build: + context: . + dockerfile: Dockerfiles/Dockerfile.ssas-migrate diff --git a/docker-compose.test.yml b/docker-compose.test.yml index 316452b92..a3df4cd1b 100644 --- a/docker-compose.test.yml +++ b/docker-compose.test.yml @@ -26,13 +26,8 @@ services: - OKTA_CLIENT_ORGURL=https://cms-sandbox.oktapreview.com - OKTA_EMAIL=shawn@bcda.aco-group.us - OKTA_CLIENT_TOKEN=${OKTA_CLIENT_TOKEN} - - OKTA_CA_CERT_FINGERPRINT=${OKTA_CA_CERT_FINGERPRINT} - BCDA_AUTH_PROVIDER=${BCDA_AUTH_PROVIDER} - OKTA_OAUTH_SERVER_ID=${OKTA_OAUTH_SERVER_ID} - - OKTA_MFA_EMAIL=${OKTA_MFA_EMAIL} - - OKTA_MFA_USER_ID=${OKTA_MFA_USER_ID} - - OKTA_MFA_USER_PASSWORD=${OKTA_MFA_USER_PASSWORD} - - OKTA_MFA_SMS_FACTOR_ID=${OKTA_MFA_SMS_FACTOR_ID} - CLIENT_ID - CLIENT_SECRET - BB_HASH_PEPPER=6E6F747468657265616C706570706572 @@ -43,24 +38,6 @@ services: - CCLF_IMPORT_STATUS_RECORDS_INTERVAL=10 - BCDA_SSAS_CLIENT_ID=fake-client-id - BCDA_SSAS_SECRET=fake-secret - - SSAS_ADMIN_SIGNING_KEY_PATH=../../../shared_files/ssas/admin_test_signing_key.pem - - SSAS_PUBLIC_SIGNING_KEY_PATH=../../../shared_files/ssas/public_test_signing_key.pem - - SSAS_PUBLIC_PORT=:3003 - - SSAS_ADMIN_PORT=:3004 - - SSAS_HTTP_TO_HTTPS_PORT=:3005 - - SSAS_READ_TIMEOUT=10 - - SSAS_WRITE_TIMEOUT=20 - - SSAS_IDLE_TIMEOUT=120 - - SSAS_HASH_ITERATIONS=130000 - - SSAS_HASH_KEY_LENGTH=64 - - SSAS_HASH_SALT_SIZE=32 - - SSAS_DEFAULT_SYSTEM_SCOPE=bcda-api - - SSAS_MFA_CHALLENGE_REQUEST_MILLISECONDS=0 - - SSAS_MFA_TOKEN_TIMEOUT_MINUTES=60 - - SSAS_MFA_PROVIDER=${SSAS_MFA_PROVIDER} - - SSAS_TOKEN_BLACKLIST_CACHE_CLEANUP_MINUTES=15 - - SSAS_TOKEN_BLACKLIST_CACHE_TIMEOUT_MINUTES=1440 - - SSAS_TOKEN_BLACKLIST_CACHE_REFRESH_MINUTES=5 - SSAS_URL=http://ssas:3004 - SSAS_PUBLIC_URL=http://ssas:3003 - ENABLE_ENCRYPTION=false @@ -72,15 +49,3 @@ services: dockerfile: Dockerfiles/Dockerfile.postman_test volumes: - .:/go/src/github.com/CMSgov/bcda-app - postman_test_ssas: - build: - context: . - dockerfile: Dockerfiles/Dockerfile.postman_test - volumes: - - .:/go/src/github.com/CMSgov/bcda-app - smoke_test_ssas: - build: - context: . - dockerfile: Dockerfiles/Dockerfile.postman_test - volumes: - - .:/go/src/github.com/CMSgov/bcda-app diff --git a/docker-compose.yml b/docker-compose.yml index 90c01d1f0..d1dd0920f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -121,10 +121,6 @@ services: - BCDA_AUTH_PROVIDER=${BCDA_AUTH_PROVIDER} - OKTA_OAUTH_SERVER_ID=${OKTA_OAUTH_SERVER_ID} - OKTA_CA_CERT_FINGERPRINT=${OKTA_CA_CERT_FINGERPRINT} - - OKTA_MFA_EMAIL=${OKTA_MFA_EMAIL} - - OKTA_MFA_USER_ID=${OKTA_MFA_USER_ID} - - OKTA_MFA_USER_PASSWORD=${OKTA_MFA_USER_PASSWORD} - - OKTA_MFA_SMS_FACTOR_ID=${OKTA_MFA_SMS_FACTOR_ID} - BCDA_SSAS_CLIENT_ID=${BCDA_SSAS_CLIENT_ID} - BCDA_SSAS_SECRET=${BCDA_SSAS_SECRET} - SSAS_ADMIN_SIGNING_KEY_PATH=../shared_files/ssas/admin_test_signing_key.pem @@ -150,4 +146,6 @@ services: ports: - "3003:3003" - "3004:3004" - - "3005:3005" \ No newline at end of file + - "3005:3005" + depends_on: + - db \ No newline at end of file diff --git a/ops/build_and_package.sh b/ops/build_and_package.sh index a780d25a8..c30f8462e 100755 --- a/ops/build_and_package.sh +++ b/ops/build_and_package.sh @@ -47,12 +47,6 @@ echo "Building bcdaworker..." go build echo "Packaging bcdaworker binary into RPM..." fpm -v $VERSION -s dir -t rpm -n bcdaworker bcdaworker=/usr/local/bin/bcdaworker -cd ../ssas -go clean -echo "Building ssas..." -go build -o ssas ./service/main -echo "Packaging ssas binary into RPM..." -fpm -v $VERSION -s dir -t rpm -n ssas ssas=/usr/local/bin/ssas #Sign RPMs echo "Importing GPG Key files" @@ -77,12 +71,3 @@ echo "allow-loopback-pinentry" > ~/.gnupg/gpg-agent.conf echo "Signing bcdaworker RPM" echo $WORKER_RPM echo $BCDA_GPG_RPM_PASSPHRASE | gpg --batch --yes --passphrase-fd 0 --pinentry-mode loopback --sign $WORKER_RPM - -cd ../ssas -SSAS_RPM="ssas-*.rpm" -echo "%_signature gpg %_gpg_path $PWD %_gpg_name $GPG_RPM_USER %_gpgbin /usr/bin/gpg" > $PWD/.rpmmacros -echo "allow-loopback-pinentry" > ~/.gnupg/gpg-agent.conf - -echo "Signing ssas RPM" -echo $SSAS_RPM -echo $BCDA_GPG_RPM_PASSPHRASE | gpg --batch --yes --passphrase-fd 0 --pinentry-mode loopback --sign $SSAS_RPM diff --git a/shared_files/ssas/admin_test_signing_key.pem b/shared_files/ssas/admin_test_signing_key.pem deleted file mode 100644 index 0721b65ba..000000000 --- a/shared_files/ssas/admin_test_signing_key.pem +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEogIBAAKCAQEAuGgagnDDtAYLOAU7o4X5V/UgQNyswdqBDHSItpBxix+AZ8UY -oXXE6fchA50/cM4p0NWAjwQ484hF9uXCXY8Tt7x19dKcViniVB9ewYdRJl/eQ71c -R6oYjbdj2RDGoaDiyd+rkCQf7zeI5ktt4SFAFapufVmI+e6Hn2wVnUsKOps5Aav8 -cnF3tVmpAaFegFSFlJ1B3WQBnHYRNvP0xYks+ysfrkr+iPIUTmSk0uyruZWNgDf2 -te+jbbptlxpcZg9lWA3fOjymiOX73TTHiqbiYPyLDvGw28FOwu1c8w0Lda6wmZie -d86o0rknhdmSN/IUNN5w6p78ugMoCww+Bn/8XwIDAQABAoIBABw7C7vmaX944WTN -IEkzbG2zwCFN50CyI8l1WayhRlCnHPBF8zRcpC2xQKOeAiVR2oL2/HxoiBN4TEW0 -/OF30uvw9RoSCQ8D0HtSZh44igrW6F70cVkjSCkB505GzDHYQH/pNwmVDjKADPw8 -lfC+N2JZuhqEh3zDsI2ObWysHT/FWrLGxQGOPcwYqHMvPMNbkgF5fvoXwFwcn1f5 -T1b+LC3UC0Vd/Fig0YXyCPbWznjcWZcU+Zti9vD4VvooBVW8sN3kZmTOtxcxfTLM -/xb4vrmtRKmJfvjjTv0B6IyzI3o/zrlszi13uXK3McFoLa09B0Md7ymFTnR/YBXS -gUhLidkCgYEA8nOgdTrtshlpZZwo3GO2iaDHNsJYBBQ+BvM3shtgMLedOEvbR4Y3 -2s5b1cDxwiWVWGH7Ld3iimxvCNCq3rDLJfM0yFyOTQxT/eyLn+A7tRJZLp1J/9gi -gBonIGddBa20DEnIe2FqdiijIX6D3JOZG5pJgkFUx4DBq5MTPePyT8sCgYEAwrYe -UDjV0lvBMr0TDwu+H/RBz4LgpyokgkaTlImDJ2K0EqyhOQyL/HJlDQAS6jLxeS7p -DLTtRWd6ZSUs+/3vV1ag/5CRzMt50YkCt9s1hmXH97ZqhGCRNeNcfB2RwZ5bBAP9 -+TwOIqEWjB/U51YMDcCyhgJnk8PCh73YagfXyz0CgYBnGqDb+alnmcLmgRgnUQgp -UwQk11TSt0EBd6Dxzw6C6TKk5C1mJz/NfwAy0JB+/bibE1/by4YxU5eMaiCf/xMF -Gn4Rzrp9LYbybwuZe6QohpsCZcU4VdOmInkNIKfAaHQu3ZmyTmUVxoZJEiJFRUdR -I7Wq/Nlu1eSGcE84fJ1pLQKBgDWlyhpeXoOUJlodgEfP+3WAbjWHoPBOCzsdyQHP -FaTfbDANAmreix9mQXNghtWibafvBeUrYIiT50RBBvDzWWOeCcQAiDt+ALV745TW -wBukpYEZ8KVCmh/X4h6MYyGOyRMFKo/mPRrLeZPoHVgT/EQ5yLZlqTYsZMfTxfII -8SsdAoGAHM1xXEAM2BhlV1lEdq0g74AM2tOjn3ad0C5Fffifv26IRorIbWhnDEYM -57w6lmTkHo16ITieTjELlebtuL8fHuyfULFVej8gWxKv8IWqMqIjn3rbhQtR0Jou -tPWGHXwT8KAg/A0WommdCX/lra/clwSr+NvldSyH6ct8x7kY/fA= ------END RSA PRIVATE KEY----- diff --git a/shared_files/ssas/bad_base64_test_private_key.pem b/shared_files/ssas/bad_base64_test_private_key.pem deleted file mode 100644 index d06494fc4..000000000 --- a/shared_files/ssas/bad_base64_test_private_key.pem +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEpAIBAAKCAQEAxmn7Rm4QRP3rQbbtfpau0yqkcwPv4mw4NsCySYITQ4H59e1a -L3PNdWUDdO7yxSEbLPx9Vc3j7AgJaMQ6BnilNzSdRwtBD9pN8WdGxzomHidqiDSw -J5MC+yjkdk6MWCDwRk7Pc+qqi6dPcjqsdUAj9J86YRb9gox0XG0FcmGKLGRb8tYO -mzCgq6PW/E96TwwWaNrD3drfL8Pkr9LEUpEzd1c0jVtVO6SRGAi1oz07mSJFo4V9 -Ren3vC5mSRMFJnyzb3zQlTYi0xQF7/nWisdyKtilIAooLWluAnNJpuAE9JAjXpmu -MyF4X4oNXyrZ2cr8NIhBRJje17LrgzpDJZnP5QIDAQABAoIBAQCeQpasnMnZXfey -WOiiWipkQNUe8Hr8zXkhIx6SR3B6Hkh0gre2tdWh18qkEJwP5NI6VcLbF/i+ocJv -kGUAGGcf25W/vxhMPsKA1PG9o4IX9zSgI/aF+lscXoesXgbPnKZKTj5mpT0XW4Kh -CG/arih/zCnSm3XmFYYS2trHW0nNEW/rPwhHbr+E5j7mZzUme4WkdqNaGqG0aDQF -tg6pFLRAX/zLuOZXNkELJ3uDuksHE+WP+yBghFMw0khTHqcpuvQ70QOnHLt7qEIO -teEqKcjbI1Kk9YDVHk1gIbqP3+gPjhu3H+xpm+/N/cnLlmwtjIHciiSnOgfGGsYj -8WL8UMNZAoGBAOuLn=9+3xDfF7hKhEzTpLGpJyPEkW3AcBShJ/KhCfGDssGteKbQ -QBq9ap64C4phg/U2sDJ617vlYfrL4Y6IkxIVjl/RkdaTxdplY4dZxSlwcB0A7DRV -zb6KGcORijkioU5IzR72fLryDrVzP6XFU+a17v2teod35fE2SLR0Fl3rAoGBANek -506v/p4GTrMz9XLsrDiTQEoTyLKxjiIf1Zk2QPg+zSe5A67QmiogPU5to4fRnQ6s -ycWjXtkdPlMnC6Ap1j8+0GI1e10QB0JMlASdliq7lTInSPEYwYJxRctNpsccjATO -1F43rQxwy1sqptk00C2S/av+Uf4qM8eyzKMgVIVvAoGARDrlBTXi6QUEuZC/J4yD -uSY2duO5OPhk5g7IiVT5AnqNgAx+uBWW3CgSQne9oBAvUVDOKVE8PMltYGC+rbMS -JyLnYwop3KZhoanM8uAmJKLVVxF1WpOgTZljbSszhulpIGwmPtnXt692Y0lHHpXS -f/ojiIg//g3VJdI7rUoTUJECgYBL5LyHhAcvZHbkOOAkf0kpbCGPMKFMypKETgHl -tyNseuXHGiVCrCXlt4z8Ajgwf8Qvuv4UMaga72DU8QP1bWP6xEegmMP+/7oeSkc5 -zKBiD7y1dwAD4juQhf8TSxPsNY7NzmENe7jKjRP01PD9tsmhkH74vjvrIL0yhinh -K2qzvwKBgQDZmCoB6Z9U0FLk1arZF9GDPmZCuTAIQHhz1GzvzKdk7XVegfIlUeuy -PHbPGhbllI/0a6N0sLuX1cqMQ01TiSLey6p+3nKK19Z6I0fYHJPeczVga/CuP7F3 -nBrEUv4BlLeiUoFHFwmJZgwGgWP/COPrkF1f157g3C0PaTUd16Z8jw== ------END RSA PRIVATE KEY----- diff --git a/shared_files/ssas/good_test_private_key.pem b/shared_files/ssas/good_test_private_key.pem deleted file mode 100644 index 65a9eecf4..000000000 --- a/shared_files/ssas/good_test_private_key.pem +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEpAIBAAKCAQEAxmn7Rm4QRP3rQbbtfpau0yqkcwPv4mw4NsCySYITQ4H59e1a -L3PNdWUDdO7yxSEbLPx9Vc3j7AgJaMQ6BnilNzSdRwtBD9pN8WdGxzomHidqiDSw -J5MC+yjkdk6MWCDwRk7Pc+qqi6dPcjqsdUAj9J86YRb9gox0XG0FcmGKLGRb8tYO -mzCgq6PW/E96TwwWaNrD3drfL8Pkr9LEUpEzd1c0jVtVO6SRGAi1oz07mSJFo4V9 -Ren3vC5mSRMFJnyzb3zQlTYi0xQF7/nWisdyKtilIAooLWluAnNJpuAE9JAjXpmu -MyF4X4oNXyrZ2cr8NIhBRJje17LrgzpDJZnP5QIDAQABAoIBAQCeQpasnMnZXfey -WOiiWipkQNUe8Hr8zXkhIx6SR3B6Hkh0gre2tdWh18qkEJwP5NI6VcLbF/i+ocJv -kGUAGGcf25W/vxhMPsKA1PG9o4IX9zSgI/aF+lscXoesXgbPnKZKTj5mpT0XW4Kh -CG/arih/zCnSm3XmFYYS2trHW0nNEW/rPwhHbr+E5j7mZzUme4WkdqNaGqG0aDQF -tg6pFLRAX/zLuOZXNkELJ3uDuksHE+WP+yBghFMw0khTHqcpuvQ70QOnHLt7qEIO -teEqKcjbI1Kk9YDVHk1gIbqP3+gPjhu3H+xpm+/N/cnLlmwtjIHciiSnOgfGGsYj -8WL8UMNZAoGBAOuLnS7b+DfF7hKhEzTpLGpJyPEkW3AcBShJ/KhCfGDssGteKbQc -QBq9ap64C4phg/U2sDJ617vlYfrL4Y6IkxIVjl/RkdaTxdplY4dZxSlwcB0A7DRV -zb6KGcORijkioU5IzR72fLryDrVzP6XFU+a17v2teod35fE2SLR0Fl3rAoGBANek -506v/p4GTrMz9XLsrDiTQEoTyLKxjiIf1Zk2QPg+zSe5A67QmiogPU5to4fRnQ6s -ycWjXtkdPlMnC6Ap1j8+0GI1e10QB0JMlASdliq7lTInSPEYwYJxRctNpsccjATO -1F43rQxwy1sqptk00C2S/av+Uf4qM8eyzKMgVIVvAoGARDrlBTXi6QUEuZC/J4yD -uSY2duO5OPhk5g7IiVT5AnqNgAx+uBWW3CgSQne9oBAvUVDOKVE8PMltYGC+rbMS -JyLnYwop3KZhoanM8uAmJKLVVxF1WpOgTZljbSszhulpIGwmPtnXt692Y0lHHpXS -f/ojiIg//g3VJdI7rUoTUJECgYBL5LyHhAcvZHbkOOAkf0kpbCGPMKFMypKETgHl -tyNseuXHGiVCrCXlt4z8Ajgwf8Qvuv4UMaga72DU8QP1bWP6xEegmMP+/7oeSkc5 -zKBiD7y1dwAD4juQhf8TSxPsNY7NzmENe7jKjRP01PD9tsmhkH74vjvrIL0yhinh -K2qzvwKBgQDZmCoB6Z9U0FLk1arZF9GDPmZCuTAIQHhz1GzvzKdk7XVegfIlUeuy -PHbPGhbllI/0a6N0sLuX1cqMQ01TiSLey6p+3nKK19Z6I0fYHJPeczVga/CuP7F3 -nBrEUv4BlLeiUoFHFwmJZgwGgWP/COPrkF1f157g3C0PaTUd16Z8jw== ------END RSA PRIVATE KEY----- diff --git a/shared_files/ssas/not_rsa_test_private_key.pem b/shared_files/ssas/not_rsa_test_private_key.pem deleted file mode 100644 index 429c7bba3..000000000 --- a/shared_files/ssas/not_rsa_test_private_key.pem +++ /dev/null @@ -1,8 +0,0 @@ ------BEGIN EC PARAMETERS----- -BggqhkjOPQMBBw== ------END EC PARAMETERS----- ------BEGIN EC PRIVATE KEY----- -MHcCAQEEIPtDqd3v2AqdJOYXOZvYPPJp238GzrBEURs8mGJ2R53SoAoGCCqGSM49 -AwEHoUQDQgAEbBqSgGJWwdsT2YusUltF40OaRQFuZagtDwVKqUSpFFCWR9N/QYMv -DfqP4X6ED81xOVrbrkafb4LLdlj0HsbGiA== ------END EC PRIVATE KEY----- diff --git a/shared_files/ssas/public_test_signing_key.pem b/shared_files/ssas/public_test_signing_key.pem deleted file mode 100644 index fe1cb94a8..000000000 --- a/shared_files/ssas/public_test_signing_key.pem +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEpAIBAAKCAQEAx10/H+MobMJ7LMyaWOU4XKcHai0CSUULI0tWSow6G8CTPYPK -W9L3RU02vkOywdpJ8skkSqDPUmyBi5+Mp2idA7oHXCyY6mV2p0q/QZWvODm8hnP9 -F5eIt4GbyVzH1/Md4rwlIsZEZ/C98wTpRJyLtORbXjteRloxLvL9teVwYF2wWHCM -jQqUbEagOcM5BbFRMt9QvQFbDWGAbRJSCUXfswzcTU3ZD+hFkudBahDn58Sl8T3F -GrAb7TEtdfMRH9gC8jOKO26ol0wNrACZYUYPUVOiDUGP0q5zxSysm+AIOV3u74Zf -R/ShQRwjrAvzRmIcQrjGfZPbT6xmn15Wcp3nRQIDAQABAoIBAQCezVTZ5oytzWBm -N/f+NV/m1ZlfZsi6akfL7lem+/nRX10pk8/dwrb6Od4QQkaiiWl7/eJtm5hpFEtA -V2+nbfocHNN+BXwswXN5IF4mNMAkrkDQbJW+dBMP8SqRg9kf1+UHVUzTXVDh5m63 -pELXR0c1aOyqq+mVaoRg3Gdhu4f/CVnF0iDnIu6ieU8NivUuQOUzoYFLdlmgKPKS -oqbPd+9ARgMYo2oPIWChkfK/fcJVWHZmvorZudknRRKEYQ26pFLNLdj3v2lkAJyT -s61mCSD5fVVH/+CovcUXvvhxZUOY7k9mWkD2ovVyyCSTEfhVizilzxic0NWAtBMi -oATNDL3FAoGBAOfv02msvDUc4JCJ6mpjDbRD8YMmClveLV9fW5S05uPR7cO4ABke -Elw4pDtf3YGVirKDvnJi4+z5y2IqJbY5Gj5FPCAK1qW247hmKCpZ5YVYTaoM5dDp -4HAdyiaBmxVID194EGM+DNxMnUxV4G7/uWCTNuMDvdNeXvx5mM3pC/0vAoGBANwM -TXummjU1OMVYEQMzFETSX/AFN39weN+uaOHuk5H1VEwG9av2n+VYqQicvNQPlnwE -FStvpwRfzsnSnQa7UlSq2EOpXvLKJ0tXQM77BsM6YGN6sOx/i2yxjOcXPrsYfWBr -gv48dxxjd/RMVb4TfmB/Li0nLZbUq5/4PONZG03LAoGAd5yhBNCGR0XbMe9OKwtm -V97qQF5v3SzZbWP6ENiyci8jVVohAtMVWOYFHHG2BEwguStkHg2NyfqQvtFJnY4Z -UJ/YABZW2CNXkRNuB1lRGtGNS/NW2cSjcG6MgAs69WCyPOPoX6Xyb/I69NEc62GK -Mpn5Jl4ZmVYD2mTDPv2+pxUCgYEAgckH0kx7W7KeX1cIAbkY1Va3mxuYliPCRzvZ -RJiwlT/7jjP0po551I2sdRXtEa539YF68vmRqrTPhJ4iW5wUfTefApldFRpCft9h -rDLG1FMUEtiEjZjUpTE7h/lf2H4jRMFkq4sCPc41K/PyBn/84/FfTOZ0ryeUam/B -id4+im0CgYBWNojHUhucKK1z+LCZHy5pcvC4GGAX4gGOjZNvAXayicx/IZPYrzfk -Xm02FAag9/6ZHIetBAStHVlwSApXd74FlCdeqWPpN6aY4MbIlA4hDm1PGzm/Esho -gQyVVpMFC4AdUlq5wZmXEGq3chOILurZS3B5BbICQJCDan/6a3YVPQ== ------END RSA PRIVATE KEY----- diff --git a/shared_files/ssas/too_small_test_private_key.pem b/shared_files/ssas/too_small_test_private_key.pem deleted file mode 100644 index a061641a8..000000000 --- a/shared_files/ssas/too_small_test_private_key.pem +++ /dev/null @@ -1,15 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIICXQIBAAKBgQDK6E9PzG8R/y0u0M0IYfb07ywk/AbJXKPxWdLFPiia1OHYSfu3 -G467Jfa12jvI4WT0cbWSTRWem5HpoBP3/uil65l7aykuAIdec5EMnf089EAV47at -wqnk7B4/MYvoIHA4EeRhnZ9gzZXzuCsVSNVy1sJmQt+apR1HNzoSgZs+mwIDAQAB -AoGBAIu8/ZIJSpzsTeOl7O5O+VKpvI8j+lCGumkm4R7xtekbnwXjvbTXB68fcA7B -YtbvUyV9gfxllfQLU6u/kMEEDpep5EJjFyKYky8rs6HI0JVpG75JuDOLQGeOrFNK -2LPic635FNVrIQKKnoyARMz3jYs9EOEj3W5jHL1z5X6JaIaxAkEA//7oHcsFXASg -e+83ImKvLBQZfmGdiLwZy/T5OoEqimJgZ5b6lLH0jxd8wbqCoT/dkQpESl13+4ov -/rzHRaW2VwJBAMrpLSc4fLQKuVhkVGWd8cUIyfalZ9+Ee4ev7T+QbOmVGDJqFq+A -EY0LhV1pxVOFuyWAa9DhaYZ4SBkstCNtZ10CQQDGa2CkfxEXYqq1hRP5/f2Cr82W -zLibHBjuomFu/GDpxBivEjIFgO1q36yeSB8qNuNYoVmPPmVaPaC31MCr9iafAkAo -dDimWyKxmnm9X7Nb1xN+nvP1EqEU9QrT0IVSaO7t2uXKF1CSiMv2/NcH+rB2qHDZ -VzMnn+k0AAMP0dPQRF2ZAkAJYNZ7mZI+5aeImiM6oZ6Pd2LSfhKDRt1foXZmPaKD -4vToVJundNiUgs3CIzM5jm0JfGL6ozbriAQY69Zx+ZiL ------END RSA PRIVATE KEY----- diff --git a/shared_files/ssas/unit_test_private_key.pem b/shared_files/ssas/unit_test_private_key.pem deleted file mode 100644 index 2cda4932c..000000000 --- a/shared_files/ssas/unit_test_private_key.pem +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEpQIBAAKCAQEAz4lpBOIbOb94ls9NJ8B0G27AfJqt0gvgzQOaxNTBhX3i4LMj -0Pzx9HCZOukn8jv6DL1D6gNT+8qZhBEo6N+deuEGKz3Y+L2kkOPee4XAu54v1TwQ -BIp/EdtQ1h4020+brfX0P6MWWJbeg8mSMtgF/X+1TUkvVvlbehO6EETqeoOsM/wt -gbqwNO75va3l4kE1JHEl3AIBmPmibw4EQIItG0WiPmEBi9nsYuP6wSGFi2inUL0n -BaNDtwxrVlO+0XQITrSdOpyvNjTkGN4gdUvgJoa6qtTnTQgQiP5p5KnircYAOjew -QJIZ6ZvNevBEOfBwJq6QGLVTLatFVyBJ33LxVwIDAQABAoIBAH8f3IRxO9wTvPoj -4U8IRBbJsH6jAPY48GqkWplW+XObuO2KzdWmG09v6Wx8hUuMEY7cIE2n3DnP0Fll -aXm7/+rVC8JRECbNg5B9BDsFoxlJvGMRd35Ql6WIgeumdRohmWrlzcdtTrLVN1fi -kPsz9/df0t6Uhbrw42fTzZVJoZeJFTjQRAvl4PENoZxkNdIHPl2rkeuas7KQjchw -O9vFI0/QU76Vz5v6WQQZVFtAAibHMtURfDxsSSvdiCQcZbxKrZF2kaOTkNHTdmvg -u0GXWI9omuLxmFirUvhT16T7LxRWbIWsiipMMcuKgyldnY2wDqRl4iQOMDVbuCgR -r7vovgECgYEA+Ees6qF7KAmzBmWA7m86cbXHTovtacCoG6ipuzn2YRCSSvvgAjQI -abwvS7weaotZrA2siOHqdQVsw1qoEIiZcXNmYk6Y7USu7bp9ZiCusqFDpnRbUrCd -Fzy1xnnbh4JlyUYVdYBK+ucKeeLnqbZNEB969aByOzqty8CYk4/as5ECgYEA1f1q -hiPVIckwm6JrFyRGoiNm6QVP4Or2nPXznSBAgLXJQVRmlR2YpXRJZLXoshE+Dy6h -fu5N8Yl7aLZciu0FlY4KW9BUpDexyKmHwsjQBwLbWRz9O5Kzi/9o4X+0afecNFNf -27cN59zw8H7Tmr+L7lXXBJ9WqBlHQquWjyVMkmcCgYEAv1sG0+PXtvkayRBMefiy -U9eloE1Kk1pQdtjc0JeQ5CjQiAhvE2OlJFFNJpL74mQ6ndgAJZPxj3W56SsjI3MU -yJMH0zb+uMhaBpHYenEwFC8ko9NEW4wR10oMU8exwlRnPOTOPzy9DXoq4dxXbr85 -z/ZjX5Lk1++W6dYsAnc6OXECgYEAy5LxIzm1ihXHS1hhfruBAsChJ29pRXTiNgJ5 -xvImywulsaPkj1l/nW+aXtf7zmM+4dyfwIxe1DjRkZVjRrskQ9nEGwJ/c1aUqGw3 -fmPiG7lpCUbd1i3C552MnnIKJYFtNg5XNEPaU8lJ4dEV57LwIUXCb2BSZUrfExr/ -+aAnT2MCgYEAhb8o7/jdQkM80G+t223G2QOshCbeRSODG8/WyvH2zn95HQ02xxPs -ZaxKnqYzyYbV7osNT+HicpIO4rcO5OiVkd75ORhGDT/bUn93kyMw1X6m3gzhr31V -G5aYIuwMg7/3b/qJoS2PnqAXCryJdeTGbCt7UvVGZAB983wk6iiUzGk= ------END RSA PRIVATE KEY----- diff --git a/ssas/README.md b/ssas/README.md deleted file mode 100644 index 7a1961c97..000000000 --- a/ssas/README.md +++ /dev/null @@ -1,137 +0,0 @@ -# System-to-System Authentication Service (SSAS) - -The SSAS can be run as a standalone web service or embedded as a library. - -# Code Organization - -The outline below shows the physical directory structure of the code, with package names highlighted. The service package contains a standalone http service that presents the authorization library via two http servers, one for admin tasks and one for authorization tasks. - -Imports always go up the directory tree from leaves; that is, parents do not import from their children. Children may import from their siblings. In short, the `ssas` and `okta` packages must not import from packages in the service directory. - -- **ssas** - - **cfg** - - _configuration management; cfg should not import from ssas packages_ - - **okta** - - **service** - - **admin** - - _contains the REST API for managing the service implementation_ - - **main** - - _cli for running servers and some admin tasks_ - - **public** - - _contains the rest API for authorization services_ - -# Configuration - -Required values must be present in the docker-compose.*.yml files. Some values are primarily for the use of the ACO API, and are only used by SSAS for testing purposes. Some values are only used by the ACO API; they are listed for reference. - -Very long keys have been split across two rows for formatting purposes. - -Some variables below have a note indicating their name should be changed. These changes serve to make the names consistent with established naming patterns and/or to clarify their purpose. They should be made after we complete the initial deployments to AWS envs so that we don't have to change all of our existing deployment checklists in a short timeframe. - -| Key | Required | SSAS | BCDA | Purpose | -| -------------------- |:--------:|:----:|:---:| ------- | -| BCDA_AUTH_PROVIDER | Yes | | X | Tells ACO API which auth provider to use | -| BCDA_CA_FILE | Yes | | X | Tells ACO API the certificate file with which to validate its TLS connection to SSAS. When setting vars for AWS envs, you must include a var for the key material | -| BCDA_SSAS_CLIENT_ID | Yes | | X | Tells ACO API the client_id to use with the SSAS REST API. | -| BCDA_SSAS_SECRET | Yes | | X | Tells ACO API the secret to use with the SSAS REST API. | -| SSAS_USE_TLS | Yes | | X | Should be renamed to BCDA_SSAS_USE_TLS | -| SSAS_URL | Yes | | X | The url of the SSAS admin server. Should be renamed to BCDA_SSAS_URL | -| SSAS_PUBLIC_URL | Yes | | X | The url of the SSAS public server (auth endpoints). Should be renamed to BCDA_SSAS_URL_PUBLIC | -| DATABASE_URL | Yes | X | | Provides the database url | -| DEBUG | Depends | X | | Flag to indicate that the system is running in a development environments. Generally not used outside of docker. | | | -| HTTP_ONLY | Depends | X | | Flag to operation of the system. By default, the servers will use https. When HTTP_ONLY is present **and** set to true, they will use http. Generally not used outside of docker. | -| OKTA_CLIENT_ORGURL | Yes | X | | Sets the URL for contacting Okta (will vary between production/non-production environments). | -| OKTA_CLIENT_TOKEN | Yes | X | | A token providing limited admin-level API rights to Okta. | -| OKTA_CA_CERT_FINGERPRINT | Yes | X | | SHA1 fingerprint for the CA certificate signing the Okta TLS cert. If the fingerprint does not match the CA certificate presented when we visit Okta, the HTTPS connection is terminated | -| OKTA_MFA_EMAIL | No | X | | The email address (Okta account identifier) for the account to test in the Okta sandbox. Required only if running the live Okta MFA tests. | -| OKTA_MFA_USER_ID | No | X | | The user ID for the account to test in the Okta sandbox. Required only if running the live Okta MFA tests. | -| OKTA_MFA_USER_PASSWORD| No | X | | The password for the account to test in the Okta sandbox. Required only if running the live Okta MFA tests. | -| OKTA_MFA_SMS_FACTOR_ID | No | X | | The SMS MFA factor ID enrolled for the account to test in the Okta sandbox. Required only if running the live Okta MFA tests. | -| SSAS_DEFAULT_SYSTEM_SCOPE | Yes | X | | Used to set the scope on systems that do not specify their scope. Must be set or runtime failures will occur. | -| SSAS_HASH_ITERATIONS | Yes | X | | Controls how many iterations our secure hashing mechanism performs. Service will panic if this key does not have a value. | -| SSAS_HASH_KEY_LENGTH | Yes | X | | Controls the key length used by our secure hashing mechanism. Service will panic if this key does not have a value. | -| SSAS_HASH_SALT_SIZE | Yes | X | | Controls salt size used by our secure hashing mechanism performs. Service will panic if this key does not have a value. | -| SSAS_MFA_PROVIDER | No | X | | Switches between mock Okta MFA calls and live calls. Defaults to "Mock". | -| SSAS_MFA_CHALLENGE_
REQUEST_MILLISECONDS | No | X | | Minimum execution time for RequestFactorChallenge(). If not present, defaults to 1500. In production, this should always be set longer than the longest expected execution time. (Actual execution time is logged.)| -| SSAS_MFA_TOKEN_
TIMEOUT_MINUTES | No | X | | Token lifetime for self-registration (MFA tokens and Registration tokens). Defaults to 60 (minutes). | -| SSAS_READ_TIMEOUT | No | X | | Sets the read timeout on server requests | -| SSAS_WRITE_TIMEOUT | No | X | | Sets the write timeout on server responses | -| SSAS_IDLE_TIMEOUT | No | X | | Sets how long the server will keep idle connections open | -| SSAS_LOG | No | X | | Directs all ssas logging to a named file | -| SSAS_ADMIN_PORT
SSAS_PUBLIC_PORT
SSAS_HTTP_TO_HTTPS_PORT | No | X | X | These values are not yet used by code. Intended to allow changing port assignments. If used, will affect BCDA SSAS URL vars. | -| SSAS_ADMIN_SIGNING_KEY_PATH | Yes | X | | Provides the location of the admin server signing key. When setting vars for AWS envs, you must include a var for the key material. | -| SSAS_PUBLIC_SIGNING_KEY_PATH | Yes | X | | Provides the location of the public server signing key. When setting vars for AWS envs, you must include a var for the key material. | -| SSAS_TOKEN_BLACKLIST_CACHE_
CLEANUP_MINUTES | No | X | | Tunes the frequency that expired entries are cleared from the token blacklist cache. Defaults to 15 minutes. | -| SSAS_TOKEN_BLACKLIST_CACHE_
TIMEOUT_MINUTES | No | X | | Sets the lifetime of token blacklist cache entries. Defaults to 24 hours. | -| SSAS_TOKEN_BLACKLIST_CACHE_
REFRESH_MINUTES | No | X | | Configures the number of minutes between times the token blacklist cache is refreshed from the database. | -| BCDA_TLS_CERT | Depends | X | | The cert used when the SSAS service is running in secure mode. When setting vars for AWS envs, you must include a var for the cert material. This var should be renamed to SSAS_TLS_CERT. | -| BCDA_TLS_KEY | Depends | X | | The private key used when the SSAS service is running in secure mode. When setting vars for AWS envs, you must include a var for the key material. This var should be renamed to SSAS_TLS_KEY. | - -# Build - -Build all the code and containers with `make docker-bootstrap`. Alternatively, `docker-compose up ssas` will build and run the SSAS by itself. Note that SSAS needs the db container to be running as well. - -## Bootstrapping CLI - -SSAS currently has a simple CLI intended to make bootstrapping tasks and manual testing easier to accomplish. The CLI will only run one command at a time; commands do not chain. - -The sequence of commands needed to bootstrap the SSAS into a new environment is as follows: - -1. migrate, which will build or update the tables -1. add-fixture-data, which adds the admin group and seeds minimal data for smoke Testing -1. new-admin-system, which adds an admin system and returns its client_id -1. reset-secret, which replaces the secret associated with a client_id and returns that new secret -1. start, which starts the servers and the token blacklist cache - -You will need the admin client_id and secret to use the service's admin endpoints. - -Note that to initialize our docker container, we use migrate-and-start, which combines the first three of the steps above with some conditional logic to make sure we're running in a development environment. This command should most likely not be used elsewhere. - -# Test - -The SSAS can be tested by running `make test-ssas` or `make unit-test-ssas`. You can also use the repo-wide commands `make test` and `make unit-test`, which will run tests against the entire repo, including the SSAS code. Some tests are designed to be only run as needed, and are excluded from `make` by a build tag. To include -one of these test suites, follow the instructions at the top of the test file. - -# Integration Testing - -To run postman tests locally: - -Build and startup the required containers. Building with docker-compose up first will significantly improve the performance of the following steps. - -``` -docker-compose up -docker-compose stop -docker-compose up -d db -docker-compose up ssas -``` - -Seed the database with a minimal group: - -``` -docker run --rm --network bcda-app_default -it postgres psql -h bcda-app_db_1 -U postgres bcda - insert into groups(group_id) values ('T0000'); -``` - -point your browser at one of the following ports, or use the postman test collection in tests. - -- public server: 3003 -- admin server: 3004 -- forwarding server: 3005 - - -# Goland IDE - -To run a test suite inside of Goland IDE, edit its configuration from the `Run` menu and add values for all necessary -environmental variables. It is also possible to run individual tests, but that may require configurations for each test. - -# Docker Fun - -``` -docker run --rm --network bcda-app_default -it postgres pg_dump -s -h bcda-app_db_1 -U postgres bcda > schema.sql -``` -``` -docker-compose run --rm ssas sh -c 'tmp/ssas-service --reset-secret --client-id=[client_id]' -``` -``` -docker-compose run --rm ssas sh -c 'tmp/ssas-service --new-admin-system --system-name=[entity name]' -``` diff --git a/ssas/blacklist.go b/ssas/blacklist.go deleted file mode 100644 index f22e22ce7..000000000 --- a/ssas/blacklist.go +++ /dev/null @@ -1,79 +0,0 @@ -package ssas - -import ( -"fmt" - "github.com/pborman/uuid" - "log" - "time" - - "github.com/jinzhu/gorm" -) - -// InitializeBlacklistModels will call gorm.DB.AutoMigrate() for BlacklistEntries{} -func InitializeBlacklistModels() *gorm.DB { - log.Println("Initialize blacklist models") - db := GetGORMDbConnection() - defer Close(db) - - db.AutoMigrate( - &BlacklistEntry{}, - ) - - return db -} - -type BlacklistEntry struct { - gorm.Model - Key string `gorm:"not null" json:"key"` - EntryDate int64 `gorm:"not null" json:"entry_date"` - CacheExpiration int64 `gorm:"not null" json:"cache_expiration"` -} - -func CreateBlacklistEntry(key string, entryDate time.Time, cacheExpiration time.Time) (entry BlacklistEntry, err error) { - event := Event{Op: "CreateBlacklistEntry", TrackingID: key, TokenID: key} - OperationStarted(event) - - if key == "" { - err = fmt.Errorf("key cannot be blank") - event.Help = err.Error() - OperationFailed(event) - return - } - - be := BlacklistEntry{ - Key: key, - EntryDate: entryDate.Unix(), - CacheExpiration: cacheExpiration.UnixNano(), - } - - db := GetGORMDbConnection() - defer Close(db) - err = db.Save(&be).Error - if err != nil { - event.Help = err.Error() - OperationFailed(event) - return - } - - OperationSucceeded(event) - entry = be - return -} - -func GetUnexpiredBlacklistEntries() (entries []BlacklistEntry, err error) { - trackingID := uuid.NewRandom().String() - event := Event{Op: "GetBlacklistEntries", TrackingID: trackingID} - OperationStarted(event) - - db := GetGORMDbConnection() - defer Close(db) - err = db.Order("entry_date, cache_expiration").Where("cache_expiration > ?", time.Now().UnixNano()).Find(&entries).Error - if err != nil { - event.Help = err.Error() - OperationFailed(event) - return - } - - OperationSucceeded(event) - return -} \ No newline at end of file diff --git a/ssas/blacklist_test.go b/ssas/blacklist_test.go deleted file mode 100644 index 96ad75446..000000000 --- a/ssas/blacklist_test.go +++ /dev/null @@ -1,70 +0,0 @@ -package ssas - -import ( - "github.com/jinzhu/gorm" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/stretchr/testify/suite" - "testing" - "time" -) - -type CacheEntriesTestSuite struct { - suite.Suite - db *gorm.DB -} - -func (s *CacheEntriesTestSuite) SetupSuite() { - s.db = GetGORMDbConnection() - InitializeBlacklistModels() -} - -func (s *CacheEntriesTestSuite) TearDownSuite() { - Close(s.db) -} - -func (s *CacheEntriesTestSuite) TestGetUnexpiredCacheEntries() { - entries, err := GetUnexpiredBlacklistEntries() - require.Nil(s.T(), err) - origEntries := len(entries) - - entryDate := time.Now().Add(time.Minute*-5).UnixNano() - expiration := time.Now().Add(time.Minute*5).UnixNano() - e1 := BlacklistEntry{Key: "key1", EntryDate: entryDate, CacheExpiration: expiration} - e2 := BlacklistEntry{Key: "key2", EntryDate: entryDate, CacheExpiration: expiration} - - if err = s.db.Save(&e1).Error; err != nil { - assert.FailNow(s.T(), err.Error()) - } - if err = s.db.Save(&e2).Error; err != nil { - assert.FailNow(s.T(), err.Error()) - } - - entries, err = GetUnexpiredBlacklistEntries() - assert.Nil(s.T(), err) - assert.True(s.T(),len(entries) == origEntries + 2) - - err = s.db.Unscoped().Delete(&e1).Error - assert.Nil(s.T(), err) - err = s.db.Unscoped().Delete(&e2).Error - assert.Nil(s.T(), err) -} - -func (s *CacheEntriesTestSuite) TestCreateBlacklistEntryEmptyKey() { - entryDate := time.Now().Add(time.Minute*-5) - expiration := time.Now().Add(time.Minute*5) - - _, err := CreateBlacklistEntry("", entryDate, expiration) - assert.NotNil(s.T(), err) - - e, err := CreateBlacklistEntry("another_key", entryDate, expiration) - assert.Nil(s.T(), err) - assert.Equal(s.T(), "another_key", e.Key) - - err = s.db.Unscoped().Delete(&e).Error - assert.Nil(s.T(), err) -} - -func TestCacheEntriesTestSuite(t *testing.T) { - suite.Run(t, new(CacheEntriesTestSuite)) -} diff --git a/ssas/cfg/envv.go b/ssas/cfg/envv.go deleted file mode 100644 index e6a1fb7c8..000000000 --- a/ssas/cfg/envv.go +++ /dev/null @@ -1,30 +0,0 @@ -package cfg - -import ( - "os" - "strconv" - - "github.com/sirupsen/logrus" -) - -// FromEnv always returns a string that is either a non-empty value from the environment variable named by key or -// the string otherwise -func FromEnv(key, otherwise string) string { - s := os.Getenv(key) - if s == "" { - logrus.Infof(`No %s value; using %s instead.`, key, otherwise) - return otherwise - } - return s -} - -func GetEnvInt(varName string, defaultVal int) int { - v := os.Getenv(varName) - if v != "" { - i, err := strconv.Atoi(v) - if err == nil { - return i - } - } - return defaultVal -} diff --git a/ssas/cfg/envv_test.go b/ssas/cfg/envv_test.go deleted file mode 100644 index d7c369c34..000000000 --- a/ssas/cfg/envv_test.go +++ /dev/null @@ -1,62 +0,0 @@ -package cfg - -import ( -"github.com/stretchr/testify/assert" -"github.com/stretchr/testify/suite" -"os" -"testing" -) - -type EnvvTestSuite struct { -suite.Suite -} - -var origTestKey string - -func (s *EnvvTestSuite) SetupTest() { -origTestKey = os.Getenv("TEST_KEY") -} - -func (s *EnvvTestSuite) TearDownTest() { -os.Setenv("TEST_KEY", origTestKey) -} - -func (s *EnvvTestSuite) TestFromEnvUnset() { -os.Unsetenv("TEST_KEY") -val := FromEnv("TEST_KEY", "test_val") -assert.Equal(s.T(), "test_val", val) -} - -func (s *EnvvTestSuite) TestFromEnvSet() { -os.Setenv("TEST_KEY", "set_val") -val := FromEnv("TEST_KEY", "test_val") -assert.Equal(s.T(), "set_val", val) -} - -func (s *EnvvTestSuite) TestGetEnvIntUnset() { -os.Unsetenv("TEST_KEY") -i := GetEnvInt("TEST_KEY", 33) -assert.Equal(s.T(), 33, i) -} - -func (s *EnvvTestSuite) TestGetEnvIntSet() { -os.Setenv("TEST_KEY", "55") -i := GetEnvInt("TEST_KEY", 33) -assert.Equal(s.T(), 55, i) -} - -func (s *EnvvTestSuite) TestGetEnvIntFloat() { -os.Setenv("TEST_KEY", "5.5") -i := GetEnvInt("TEST_KEY", 33) -assert.Equal(s.T(), 33, i) -} - -func (s *EnvvTestSuite) TestGetEnvIntString() { -os.Setenv("TEST_KEY", "Not an int value") -i := GetEnvInt("TEST_KEY", 33) -assert.Equal(s.T(), 33, i) -} - -func TestEnvvTestSuite(t *testing.T) { -suite.Run(t, new(EnvvTestSuite)) -} \ No newline at end of file diff --git a/ssas/connection.go b/ssas/connection.go deleted file mode 100644 index a59e85256..000000000 --- a/ssas/connection.go +++ /dev/null @@ -1,48 +0,0 @@ -package ssas - -import ( - "database/sql" - "log" - "os" - "runtime" - - "github.com/jinzhu/gorm" - _ "github.com/jinzhu/gorm/dialects/postgres" - _ "github.com/lib/pq" -) - -// Variable substitution to support testing. -var LogFatal = log.Fatal - -func GetDbConnection() *sql.DB { - databaseURL := os.Getenv("DATABASE_URL") - db, err := sql.Open("postgres", databaseURL) - if err != nil { - LogFatal(err) - } - pingErr := db.Ping() - if pingErr != nil { - LogFatal(pingErr) - } - return db -} - -func GetGORMDbConnection() *gorm.DB { - databaseURL := os.Getenv("DATABASE_URL") - db, err := gorm.Open("postgres", databaseURL) - if err != nil { - LogFatal(err) - } - pingErr := db.DB().Ping() - if pingErr != nil { - LogFatal(pingErr) - } - return db -} - -func Close(db *gorm.DB) { - if err := db.Close(); err != nil { - _, file, line, _ := runtime.Caller(1) - Logger.Infof("failed to close db connection at %s#%d because %s", file, line, err) - } -} diff --git a/ssas/connection_test.go b/ssas/connection_test.go deleted file mode 100644 index e63ed524d..000000000 --- a/ssas/connection_test.go +++ /dev/null @@ -1,65 +0,0 @@ -package ssas - -import ( - "database/sql" - "fmt" - "os" - "testing" - - "github.com/jinzhu/gorm" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/suite" -) - -type ConnectionTestSuite struct { - suite.Suite - db *sql.DB - gormdb *gorm.DB -} - -func (suite *ConnectionTestSuite) TestDbConnections() { - - // after this test, replace the original log.Fatal() function - origLogFatal := LogFatal - defer func() { LogFatal = origLogFatal }() - - // create the mock version of log.Fatal() - LogFatal = func(args ...interface{}) { - fmt.Println("FATAL (NO-OP)") - } - - // get the real database URL - actualDatabaseURL := os.Getenv("DATABASE_URL") - - // set the database URL to a bogus value to test negative scenarios - os.Setenv("DATABASE_URL", "fake_db_url") - - // attempt to open DB connection swith the bogus DB string - suite.db = GetDbConnection() - suite.gormdb = GetGORMDbConnection() - - // asert that Ping returns an error - assert.NotNil(suite.T(), suite.db.Ping(), fmt.Sprint("Database should fail to connect (negative scenario)")) - assert.NotNil(suite.T(), suite.gormdb.DB().Ping(), fmt.Sprint("Gorm database should fail to connect (negative scenario)")) - - // close DBs to reset the test - _ = suite.db.Close() - _ = suite.gormdb.Close() - - // set the database URL back to the real value to test the positive scenarios - os.Setenv("DATABASE_URL", actualDatabaseURL) - - suite.db = GetDbConnection() - defer suite.db.Close() - - suite.gormdb = GetGORMDbConnection() - defer suite.gormdb.Close() - - // assert that Ping() does not return an error - assert.Nil(suite.T(), suite.db.Ping(), fmt.Sprint("Error connecting to sql database")) - assert.Nil(suite.T(), suite.gormdb.DB().Ping(), fmt.Sprint("Error connecting to gorm database ")) -} - -func TestConnectionTestSuite(t *testing.T) { - suite.Run(t, new(ConnectionTestSuite)) -} diff --git a/ssas/groups.go b/ssas/groups.go deleted file mode 100644 index 7ab89c466..000000000 --- a/ssas/groups.go +++ /dev/null @@ -1,282 +0,0 @@ -package ssas - -import ( - "database/sql/driver" - "encoding/json" - "errors" - "fmt" - "log" - "strconv" - - "github.com/jinzhu/gorm" -) - -// InitializeGroupModels creates and updates the schema for groups -func InitializeGroupModels() *gorm.DB { - log.Println("Initialize group models") - db := GetGORMDbConnection() - defer Close(db) - - db.AutoMigrate( - &Group{}, - ) - - return db -} - -type Group struct { - gorm.Model - GroupID string `gorm:"unique;not null" json:"group_id"` - XData string `gorm:"type:text" json:"xdata"` - Data GroupData `gorm:"type:jsonb" json:"data"` -} - -func CreateGroup(gd GroupData) (Group, error) { - event := Event{Op: "CreateGroup", TrackingID: gd.GroupID} - OperationStarted(event) - - if gd.GroupID == "" { - err := fmt.Errorf("group_id cannot be blank") - event.Help = err.Error() - OperationFailed(event) - return Group{}, err - } - - xd := gd.XData - if xd != "" { - if s, err := strconv.Unquote(xd); err == nil { - xd = s - } - } - g := Group{ - GroupID: gd.GroupID, - XData: xd, - Data: gd, - } - - db := GetGORMDbConnection() - defer Close(db) - err := db.Save(&g).Error - if err != nil { - event.Help = err.Error() - OperationFailed(event) - return Group{}, err - } - - OperationSucceeded(event) - return g, nil -} - -func ListGroups(trackingID string) ([]Group, error) { - event := Event{Op: "ListGroups", TrackingID: trackingID} - OperationStarted(event) - - groups := []Group{} - db := GetGORMDbConnection() - defer Close(db) - err := db.Find(&groups).Error - if err != nil { - event.Help = err.Error() - OperationFailed(event) - return []Group{}, err - } - - OperationSucceeded(event) - return groups, nil -} - -func UpdateGroup(id string, gd GroupData) (Group, error) { - event := Event{Op: "UpdateGroup", TrackingID: id} - OperationStarted(event) - - db := GetGORMDbConnection() - defer Close(db) - - g, err := GetGroupByID(id) - if err != nil { - errString := fmt.Sprintf("record not found for id=%s", id) - event.Help = errString + ": " + err.Error() - err := fmt.Errorf(errString) - OperationFailed(event) - return Group{}, err - } - - gd.GroupID = g.Data.GroupID - gd.Name = g.Data.Name - - g.Data = gd - err = db.Save(&g).Error - if err != nil { - event.Help = err.Error() - OperationFailed(event) - return Group{}, err - } - - OperationSucceeded(event) - return g, nil -} - -func DeleteGroup(id string) error { - event := Event{Op: "DeleteGroup", TrackingID: id} - OperationStarted(event) - - db := GetGORMDbConnection() - defer Close(db) - g, err := GetGroupByID(id) - if err != nil { - event.Help = err.Error() - OperationFailed(event) - return err - } - - err = cascadeDeleteGroup(g) - if err != nil { - event.Help = err.Error() - OperationFailed(event) - return err - } - - OperationSucceeded(event) - return nil -} - -// GetAuthorizedGroupsForOktaID returns a slice of GroupID's representing all groups this Okta user has rights to manage -// TODO: this is the slowest and most memory intensive way possible to implement this. Refactor! -func GetAuthorizedGroupsForOktaID(oktaID string) ([]string, error) { - db := GetGORMDbConnection() - defer Close(db) - - var ( - result []string - ) - - groups := []Group{} - err := db.Select("*").Find(&groups).Error - if err != nil { - return result, err - } - - for _, group := range groups { - for _, user := range group.Data.Users { - if user == oktaID { - result = append(result, group.GroupID) - } - } - } - - return result, nil -} - -func cascadeDeleteGroup(group Group) error { - var ( - system System - encryptionKey EncryptionKey - secret Secret - systemIds []int - db = GetGORMDbConnection() - ) - defer Close(db) - - err := db.Table("systems").Where("group_id = ?", group.GroupID).Pluck("ID", &systemIds).Error - if err != nil { - return fmt.Errorf("unable to find associated systems: %s", err.Error()) - } - - err = db.Where("system_id IN (?)", systemIds).Delete(&encryptionKey).Error - if err != nil { - return fmt.Errorf("unable to delete encryption keys: %s", err.Error()) - } - - err = db.Where("system_id IN (?)", systemIds).Delete(&secret).Error - if err != nil { - return fmt.Errorf("unable to delete secrets: %s", err.Error()) - } - - err = db.Where("id IN (?)", systemIds).Delete(&system).Error - if err != nil { - return fmt.Errorf("unable to delete systems: %s", err.Error()) - } - - err = db.Delete(&group).Error - if err != nil { - return fmt.Errorf("unable to delete group: %s", err.Error()) - } - - return nil -} - -type GroupData struct { - GroupID string `json:"group_id"` - Name string `json:"name"` - XData string `json:"xdata"` - Users []string `json:"users,omitempty"` - Scopes []string `json:"scopes,omitempty"` - Systems []System `gorm:"-" json:"systems,omitempty"` - Resources []Resource `json:"resources,omitempty"` -} - -// Value implements the driver.Value interface for GroupData. -func (gd GroupData) Value() (driver.Value, error) { - systems, _ := GetSystemsByGroupID(gd.GroupID) - - gd.Systems = systems - - return json.Marshal(gd) -} - -// Make the GroupData struct implement the sql.Scanner interface -func (gd *GroupData) Scan(value interface{}) error { - b, ok := value.([]byte) - if !ok { - return errors.New("type assertion to []byte failed") - } - - if err := json.Unmarshal(b, &gd); err != nil { - return err - } - systems, _ := GetSystemsByGroupID(gd.GroupID) - - gd.Systems = systems - - return nil -} - -type Resource struct { - ID string `json:"id"` - Name string `json:"name"` - Scopes []string `json:"scopes"` -} - -func GetGroupByGroupID(groupID string) (Group, error) { - var ( - db = GetGORMDbConnection() - group Group - err error - ) - defer Close(db) - - if db.Find(&group, "group_id = ?", groupID).RecordNotFound() { - err = fmt.Errorf("no Group record found for groupID %s", groupID) - } - - return group, err -} - -// GetGroupByID returns the group associated with the provided ID -func GetGroupByID(id string) (Group, error) { - var ( - db = GetGORMDbConnection() - group Group - err error - ) - defer Close(db) - - if _, err = strconv.ParseUint(id, 10, 64); err != nil { - return Group{}, fmt.Errorf("invalid input %s; %s", id, err) - } - // must use the explicit where clause here because the id argument is a string - if err = db.Find(&group, "id = ?", id).Error; err != nil { - err = fmt.Errorf("no Group record found with ID %s", id) - } - return group, err -} diff --git a/ssas/groups_test.go b/ssas/groups_test.go deleted file mode 100644 index 03ab53b7d..000000000 --- a/ssas/groups_test.go +++ /dev/null @@ -1,218 +0,0 @@ -package ssas - -import ( - "encoding/json" - "fmt" - "testing" - - "github.com/jinzhu/gorm" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/stretchr/testify/suite" -) - -const SampleGroup string = `{ - "group_id":"%s", - "name": "ACO Corp Systems", - "resources": [ - { - "id": "xxx", - "name": "BCDA API", - "scopes": [ - "bcda-api" - ] - }, - { - "id": "eft", - "name": "EFT CCLF", - "scopes": [ - "eft-app:download", - "eft-data:read" - ] - } - ], - "scopes": [ - "user-admin", - "system-admin" - ], - "users": [ - "00uiqolo7fEFSfif70h7", - "l0vckYyfyow4TZ0zOKek", - "HqtEi2khroEZkH4sdIzj" - ], - "systems": [], - "xdata": %s -}` - -const SampleXdata string = `"{\"cms_ids\":[\"T67890\",\"T54321\"]}"` - -type GroupsTestSuite struct { - suite.Suite - db *gorm.DB -} - -func (s *GroupsTestSuite) SetupSuite() { - s.db = GetGORMDbConnection() - InitializeGroupModels() - InitializeSystemModels() -} - -func (s *GroupsTestSuite) TearDownSuite() { - Close(s.db) -} - -func (s *GroupsTestSuite) AfterTest() { -} - -func (s *GroupsTestSuite) TestCreateGroup() { - gid := RandomHexID() - gd := GroupData{} - err := json.Unmarshal([]byte(fmt.Sprintf(SampleGroup, gid, SampleXdata)), &gd) - assert.Nil(s.T(), err) - g, err := CreateGroup(gd) - - require.Nil(s.T(), err) - require.NotNil(s.T(), g) - assert.NotZero(s.T(), g.ID) - assert.Equal(s.T(), gid, g.GroupID) - assert.Equal(s.T(), gid, g.Data.GroupID) - assert.Equal(s.T(), 3, len(g.Data.Users)) - assert.NotEmpty(s.T(), g.XData) - assert.NotEmpty(s.T(), g.Data.XData) - assert.Equal(s.T(), g.Data.XData, g.XData) - - dbGroup := Group{} - db := GetGORMDbConnection() - defer Close(db) - if db.Where("id = ?", g.ID).Find(&dbGroup).RecordNotFound() { - assert.FailNow(s.T(), fmt.Sprintf("record not found for id=%d", g.ID)) - } - assert.Equal(s.T(), gid, dbGroup.GroupID) - assert.Equal(s.T(), gid, dbGroup.Data.GroupID) - assert.Equal(s.T(), g.XData, dbGroup.XData) - assert.Equal(s.T(), g.Data, dbGroup.Data) - assert.Equal(s.T(), dbGroup.Data.XData, dbGroup.XData) - - err = CleanDatabase(g) - assert.Nil(s.T(), err) - gd.GroupID = "" - _, err = CreateGroup(gd) - assert.EqualError(s.T(), err, "group_id cannot be blank") -} - -func (s *GroupsTestSuite) TestListGroups() { - var startingCount int - GetGORMDbConnection().Table("groups").Count(&startingCount) - groupBytes := []byte(fmt.Sprintf(SampleGroup, RandomHexID(), SampleXdata)) - gd := GroupData{} - err := json.Unmarshal(groupBytes, &gd) - require.Nil(s.T(), err) - g1, err := CreateGroup(gd) - require.Nil(s.T(), err) - - - gd.GroupID = RandomHexID() - gd.Name = "some-fake-name" - g2, err := CreateGroup(gd) - assert.Nil(s.T(), err) - - groups, err := ListGroups("test-list-groups") - assert.Nil(s.T(), err) - assert.Len(s.T(), groups, 2+startingCount) - - err = CleanDatabase(g1) - assert.Nil(s.T(), err) - err = CleanDatabase(g2) - assert.Nil(s.T(), err) - - groups, err = ListGroups("test-list-groups") - assert.Nil(s.T(), err) - assert.Len(s.T(), groups, startingCount) -} - -func (s *GroupsTestSuite) TestUpdateGroup() { - gid1 := RandomHexID() - groupBytes := []byte(fmt.Sprintf(SampleGroup, gid1, SampleXdata)) - gd := GroupData{} - err := json.Unmarshal(groupBytes, &gd) - assert.Nil(s.T(), err) - orig := Group{} - orig.Data = gd - err = s.db.Save(&orig).Error - require.Nil(s.T(), err) - - gd.Scopes = []string{"aScope", "anotherScope"} - gd.GroupID = RandomHexID() - gd.Name = "aNewGroupName" - changed, err := UpdateGroup(fmt.Sprint(orig.ID), gd) - assert.Nil(s.T(), err) - - assert.Nil(s.T(), err) - assert.Equal(s.T(), []string{"aScope", "anotherScope"}, changed.Data.Scopes) - assert.NotEqual(s.T(), "aNewGroupID", changed.Data.GroupID) - assert.NotEqual(s.T(), "aNewGroupName", changed.Data.Name) - err = CleanDatabase(orig) - assert.Nil(s.T(), err) -} - -func (s *GroupsTestSuite) TestDeleteGroup() { - gid := fmt.Sprintf("delete-group-%s", RandomHexID()) - group := Group{GroupID: gid} - err := s.db.Create(&group).Error - require.Nil(s.T(), err, "unexpected error") - - system := System{GroupID: group.GroupID, ClientID: "groups-test-delete-client-id"} - err = s.db.Create(&system).Error - require.Nil(s.T(), err, "unexpected error") - - keyStr := "publickey" - encrKey := EncryptionKey{ - SystemID: system.ID, - Body: keyStr, - } - err = s.db.Create(&encrKey).Error - require.Nil(s.T(), err, "unexpected error") - - err = DeleteGroup(fmt.Sprint(group.ID)) - assert.Nil(s.T(), err) - err = CleanDatabase(group) - assert.Nil(s.T(), err) -} - -func (s *GroupsTestSuite) TestGetAuthorizedGroupsForOktaID() { - group1bytes := []byte(`{"group_id":"T0001","users":["abcdef","qrstuv"],"scopes":[],"resources":[],"systems":[],"name":""}`) - group2bytes := []byte(`{"group_id":"T0002","users":["abcdef","qrstuv"],"scopes":[],"resources":[],"systems":[],"name":""}`) - group3bytes := []byte(`{"group_id":"T0003","users":["qrstuv"],"scopes":[],"resources":[],"systems":[],"name":""}`) - - g1 := GroupData{} - err := json.Unmarshal(group1bytes, &g1) - assert.Nil(s.T(), err) - group1, _ := CreateGroup(g1) - - g2 := GroupData{} - err = json.Unmarshal(group2bytes, &g2) - assert.Nil(s.T(), err) - group2, _ := CreateGroup(g2) - - g3 := GroupData{} - err = json.Unmarshal(group3bytes, &g3) - assert.Nil(s.T(), err) - group3, _ := CreateGroup(g3) - - defer s.db.Unscoped().Delete(&group1) - defer s.db.Unscoped().Delete(&group2) - defer s.db.Unscoped().Delete(&group3) - - authorizedGroups, err := GetAuthorizedGroupsForOktaID("abcdef") - if err != nil { - s.FailNow(err.Error()) - } - if len(authorizedGroups) != 2 { - s.FailNow("oktaID should be authorized for exactly two groups") - } - assert.Equal(s.T(), "T0001", authorizedGroups[0]) -} - -func TestGroupsTestSuite(t *testing.T) { - suite.Run(t, new(GroupsTestSuite)) -} diff --git a/ssas/hash.go b/ssas/hash.go deleted file mode 100644 index 2381d4bb8..000000000 --- a/ssas/hash.go +++ /dev/null @@ -1,94 +0,0 @@ -package ssas - -import ( - "crypto/rand" - "crypto/sha512" - "encoding/base64" - "errors" - "fmt" - "os" - "strings" - "time" - - "golang.org/x/crypto/pbkdf2" - - "github.com/CMSgov/bcda-app/ssas/cfg" -) - -var ( - hashIter int - hashKeyLen int - saltSize int -) - -// Hash is a cryptographically hashed string -type Hash string - -// The time for hash comparison should be about 1s. Increase hashIter if this is significantly faster in production. -// Note that changing hashIter or hashKeyLen will result in invalidating existing stored hashes (e.g. credentials). -func init() { - if (os.Getenv("DEBUG") == "true") { - hashIter = cfg.GetEnvInt("SSAS_HASH_ITERATIONS", 130000) - hashKeyLen = cfg.GetEnvInt("SSAS_HASH_KEY_LENGTH", 64) - saltSize = cfg.GetEnvInt("SSAS_HASH_SALT_SIZE", 32) - } else { - hashIter = cfg.GetEnvInt("SSAS_HASH_ITERATIONS", 0) - hashKeyLen = cfg.GetEnvInt("SSAS_HASH_KEY_LENGTH", 0) - saltSize = cfg.GetEnvInt("SSAS_HASH_SALT_SIZE", 0) - } - - if hashIter == 0 || hashKeyLen == 0 || saltSize == 0 { - // ServiceHalted(Event{Help:"SSAS_HASH_ITERATIONS, SSAS_HASH_KEY_LENGTH and SSAS_HASH_SALT_SIZE environment values must be set"}) - panic("SSAS_HASH_ITERATIONS, SSAS_HASH_KEY_LENGTH and SSAS_HASH_SALT_SIZE environment values must be set") - } -} - -// NewHash creates a Hash value from a source string -// The HashValue consists of the salt and hash separated by a colon ( : ) -// If the source of randomness fails it returns an error. -func NewHash(source string) (Hash, error) { - if source == "" { - return Hash(""), errors.New("empty string provided to hash function") - } - - salt := make([]byte, saltSize) - _, err := rand.Read(salt) - if err != nil { - return Hash(""), err - } - - start := time.Now() - h := pbkdf2.Key([]byte(source), salt, hashIter, hashKeyLen, sha512.New) - hashCreationTime := time.Since(start) - hashEvent := Event{Elapsed: hashCreationTime} - SecureHashTime(hashEvent) - - return Hash(fmt.Sprintf("%s:%s", base64.StdEncoding.EncodeToString(salt), base64.StdEncoding.EncodeToString(h))), nil -} - -// IsHashOf accepts an unhashed string, which it first hashes and then compares to itself -func (h Hash) IsHashOf(source string) bool { - // Avoid comparing with an empty source so that a hash of an empty string is never successful - if source == "" { - return false - } - - hashAndPass := strings.Split(h.String(), ":") - if len(hashAndPass) != 2 { - return false - } - - hash := hashAndPass[1] - salt, err := base64.StdEncoding.DecodeString(hashAndPass[0]) - if err != nil { - return false - } - - sourceHash := pbkdf2.Key([]byte(source), salt, hashIter, hashKeyLen, sha512.New) - return hash == base64.StdEncoding.EncodeToString(sourceHash) -} - -func (h Hash) String() string { - return string(h) -} - diff --git a/ssas/hash_test.go b/ssas/hash_test.go deleted file mode 100644 index 879490e9c..000000000 --- a/ssas/hash_test.go +++ /dev/null @@ -1,48 +0,0 @@ -package ssas - -import ( - "github.com/pborman/uuid" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/suite" - "testing" -) - -type HashTestSuite struct { - suite.Suite -} - -func (s *HashTestSuite) TestHashComparable() { - uuidString := uuid.NewRandom().String() - hash, err := NewHash(uuidString) - assert.Nil(s.T(), err) - assert.True(s.T(), hash.IsHashOf(uuidString)) - assert.False(s.T(), hash.IsHashOf(uuid.NewRandom().String())) -} - -func (s *HashTestSuite) TestHashUnique() { - uuidString := uuid.NewRandom().String() - hash1, _ := NewHash(uuidString) - hash2, _ := NewHash(uuidString) - assert.NotEqual(s.T(), hash1.String(), hash2.String()) -} - -func (s *HashTestSuite) TestHashCompatibility() { - uuidString := "96c5a0cd-b284-47ac-be6e-f33b14dc4697" - hash := Hash("YMkApwNDTca4xlM/ROE4ZsiPLrWhjBGbJWue5RghICs=:S/xW9ehijAxxBtsMrDH+R6MYc/l4Sr3Y2SNkPJizy7WW0yaw7FFoAQ1R95WdWnrbPWaM6U0St5U6fp8Bge5pIA==") - assert.True(s.T(), hash.IsHashOf(uuidString), "Possible change in hashing parameters or algorithm. Known input/output does not match. Merging this code will result in invalidating credentials.") -} - -func (s *HashTestSuite) TestHashEmpty() { - hash, err := NewHash("") - assert.NotNil(s.T(), err) - assert.False(s.T(), hash.IsHashOf("")) -} - -func (s *HashTestSuite) TestHashInvalid() { - hash := Hash("INVALID_NUMBER_OF_SEGMENTS:d3H4fX/uEk1jOW2gYrFezyuJoSv4ay2x3gH5C25KpWM=:kVqFm1he5S4R1/10oIkVNFot40VB3wTa+DXTp4TrwvyXHkQO7Dxjjo/OqwemiYP8p3UQ8r/HkmTQrSS99UXzaQ==") - assert.False(s.T(), hash.IsHashOf("96c5a0cd-b284-47ac-be6e-f33b14dc4697")) -} - -func TestHashTestSuite(t *testing.T) { - suite.Run(t, new(HashTestSuite)) -} diff --git a/ssas/logger.go b/ssas/logger.go deleted file mode 100644 index 6e00e3fdd..000000000 --- a/ssas/logger.go +++ /dev/null @@ -1,155 +0,0 @@ -package ssas - -import ( - "os" - "time" - - "github.com/sirupsen/logrus" -) - -// Logger provides a structured logger for this service -var Logger *logrus.Logger - -// Event contains the superset of fields that may be included in Logger statements -type Event struct { - UserID string - ClientID string - Elapsed time.Duration - Help string - Op string - TokenID string - TrackingID string -} - -func init() { - Logger = logrus.New() - Logger.Formatter = &logrus.JSONFormatter{} - Logger.Formatter.(*logrus.JSONFormatter).TimestampFormat = time.RFC3339Nano - - filePath, success := os.LookupEnv("SSAS_LOG") - if success { - /* #nosec -- 0640 permissions required for Splunk ingestion */ - file, err := os.OpenFile(filePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0640) - - if err == nil { - Logger.SetOutput(file) - } else { - Logger.Info("Failed to open SSAS log file; using default stderr") - } - } else { - Logger.Info("No SSAS log location provided; using default stderr") - } -} - -func mergeNonEmpty(data Event) *logrus.Entry { - var entry = logrus.NewEntry(Logger) - - if data.UserID != "" { - entry = entry.WithField("userID", data.UserID) - } - if data.ClientID != "" { - entry = entry.WithField("clientID", data.ClientID) - } - if data.TrackingID != "" { - entry = entry.WithField("trackingID", data.TrackingID) - } - if data.Elapsed != 0 { - entry = entry.WithField("elapsed", data.Elapsed) - } - if data.Op != "" { - entry = entry.WithField("op", data.Op) - } - if data.TokenID != "" { - entry = entry.WithField("tokenID", data.TokenID) - } - - return entry -} - -/* - The following logging functions should be passed an Event{} with at least the Op and TrackingID set, and - other general messages put in the Help field. Successive logs for the same event should use the same - randomly generated TrackingID. - */ - - -// OperationStarted should be called at the beginning of all logged events -func OperationStarted(data Event) { - mergeNonEmpty(data).WithField("Event", "OperationStarted").Print(data.Help) -} - - -// OperationSucceeded should be called after an event's success, and should always be preceded by -// a call to OperationStarted -func OperationSucceeded(data Event) { - mergeNonEmpty(data).WithField("Event", "OperationSucceeded").Print(data.Help) -} - - -// OperationCalled will log the caller of an operation. The caller should use the same -// randomly generated TrackingID as used in the operation for OperationStarted, OperationSucceeded, etc. -func OperationCalled(data Event) { - mergeNonEmpty(data).WithField("Event", "OperationCalled").Print(data.Help) -} - - -// OperationFailed should be called after an event's failure, and should always be preceded by -// a call to OperationStarted -func OperationFailed(data Event) { - mergeNonEmpty(data).WithField("Event", "OperationFailed").Print(data.Help) -} - -// TokenMintingFailure is emitted when a token can't be created. Usually, this is due to a -// issue with the signing key. -func TokenMintingFailure(data Event) { - mergeNonEmpty(data).WithField("Event", "TokenMintingFailure").Print(data.Help) -} - -// AccessTokenIssued should be called to log the successful creation of every access token -func AccessTokenIssued(data Event) { - mergeNonEmpty(data).WithField("Event", "AccessTokenIssued").Print(data.Help) -} - -// TokenBlacklisted records that a token with a specific key is invalidated -func TokenBlacklisted(data Event) { - mergeNonEmpty(data).WithField("Event", "TokenBlacklisted").Print(data.Help) -} - -// BlacklistedTokenPresented logs an attempt to verify a blacklisted token -func BlacklistedTokenPresented(data Event) { - mergeNonEmpty(data).WithField("Event", "BlacklistedTokenPresented").Print(data.Help) -} - -// CacheSyncFailure is called when an in-memory cache cannot be refreshed from the database -func CacheSyncFailure(data Event) { - mergeNonEmpty(data).WithField("Event", "CacheSyncFailure").Print(data.Help) -} - -// AuthorizationFailure should be called by middleware to record token or credential issues -func AuthorizationFailure(data Event) { - mergeNonEmpty(data).WithField("Event", "AuthorizationFailure").Print(data.Help) -} - -// SecureHashTime should be called with the time taken to create a hash, logs of which can be used -// to approximate the security provided by the hash -func SecureHashTime(data Event) { - mergeNonEmpty(data).WithField("Event", "SecureHashTime").Print(data.Help) -} - - -// SecretCreated should be called every time a system's secret is generated -func SecretCreated(data Event) { - mergeNonEmpty(data).WithField("Event", "SecretCreated").Print(data.Help) -} - - -// ServiceHalted should be called to log an unexpected stop to the service -func ServiceHalted(data Event) { - mergeNonEmpty(data).WithField("Event", "ServiceHalted").Print(data.Help) -} - - -// ServiceStarted should be called to log the starting of the service -func ServiceStarted(data Event) { - mergeNonEmpty(data).WithField("Event", "ServiceStarted").Print(data.Help) -} \ No newline at end of file diff --git a/ssas/logger_test.go b/ssas/logger_test.go deleted file mode 100644 index 495490422..000000000 --- a/ssas/logger_test.go +++ /dev/null @@ -1,21 +0,0 @@ -package ssas - -import ( - "testing" - - "github.com/sirupsen/logrus" - "github.com/sirupsen/logrus/hooks/test" - "github.com/stretchr/testify/assert" -) - -func TestOperationLogging(t*testing.T){ - testLogger := test.NewLocal(Logger) - OperationStarted(Event{Op: "TestOperation", Help: "A little more to the right"}) - - assert.Equal(t, 1, len(testLogger.Entries)) - assert.Equal(t, logrus.InfoLevel, testLogger.LastEntry().Level) - assert.Equal(t, "A little more to the right", testLogger.LastEntry().Message) - - testLogger.Reset() - assert.Nil(t, testLogger.LastEntry()) -} \ No newline at end of file diff --git a/ssas/okta/okta.go b/ssas/okta/okta.go deleted file mode 100644 index 5b20ceb37..000000000 --- a/ssas/okta/okta.go +++ /dev/null @@ -1,142 +0,0 @@ -package okta - -import ( - "bytes" - // #nosec: using SHA1 to match browser fingerprinting - "crypto/sha1" - "crypto/tls" - "encoding/hex" - "encoding/json" - "errors" - "fmt" - "net" - "net/http" - "os" - "time" - - "github.com/CMSgov/bcda-app/ssas" -) - -var OktaBaseUrl string -var OktaAuthString string -var OktaCACertFingerprint []byte - -type OktaError struct { - ErrorCode string `json:"errorCode"` - ErrorSummary string `json:"errorSummary"` -} - -type Dialer func(network, addr string) (net.Conn, error) - -func init() { - err := config() - if err != nil { - initEvent := ssas.Event{Op: "OktaInitialization", Help: "unable to complete Okta config: " + err.Error()} - ssas.OperationFailed(initEvent) - } -} - -// separate from init for testing -func config() error { - OktaBaseUrl = os.Getenv("OKTA_CLIENT_ORGURL") - oktaToken := os.Getenv("OKTA_CLIENT_TOKEN") - - at := oktaToken - if at != "" { - at = "[Redacted]" - } - OktaAuthString = fmt.Sprintf("SSWS %s", oktaToken) - OktaBaseUrl = os.Getenv("OKTA_CLIENT_ORGURL") - fingerprintString := os.Getenv("OKTA_CA_CERT_FINGERPRINT") - - if OktaBaseUrl == "" || oktaToken == "" || fingerprintString == "" { - return fmt.Errorf(fmt.Sprintf("missing env vars: OKTA_CLIENT_ORGURL=%s, OKTA_CA_CERT_FINGERPRINT=%s, OKTA_CLIENT_TOKEN=%s", - OktaBaseUrl, fingerprintString, at)) - } - - var err error - OktaCACertFingerprint, err = hex.DecodeString(fingerprintString) - if err != nil { - return fmt.Errorf("unable to parse OKTA_CA_CERT_FINGERPRINT: " + err.Error()) - } - - return nil -} - -/* - Client returns an http.Client set with appropriate defaults, including an extra layer of certificate validation -*/ -func Client() *http.Client { - client := http.Client{Timeout: time.Second * 10} - client.Transport = &http.Transport{ - DialTLS: makeDialer(OktaCACertFingerprint), - } - return &client -} - -/* - AddRequestHeaders sets common headers needed for all Okta requests -*/ -func AddRequestHeaders(req *http.Request) { - req.Header.Set("Accept", "application/json") - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", OktaAuthString) -} - -func ParseOktaError(body []byte) (OktaError, error) { - oktaError := OktaError{} - if err := json.Unmarshal(body, &oktaError); err != nil { - return oktaError, errors.New("unexpected response format; not a standard Okta error") - } - return oktaError, nil -} - -type RoundTripFunc func(req *http.Request) *http.Response - -/* - RoundTrip allows control of an http.Client's response for testing purposes. This code is taken - from https://hassansin.github.io/Unit-Testing-http-client-in-Go -*/ -func (f RoundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) { - return f(req), nil -} - -/* - NewTestClient returns *http.Client with Transport replaced to avoid making real calls -*/ -func NewTestClient(fn RoundTripFunc) *http.Client { - return &http.Client{ - Transport: RoundTripFunc(fn), - } -} - -// Modified from https://medium.com/@zmanian/server-public-key-pinning-in-go-7a57bbe39438 -func makeDialer(fingerprint []byte) Dialer { - return func(network, addr string) (net.Conn, error) { - var errMessage string - c, err := tls.Dial(network, addr, &tls.Config{}) - if err != nil { - return c, err - } - connstate := c.ConnectionState() - keyPinValid := false - for _, peercert := range connstate.PeerCertificates { - // #nosec: using SHA1 to match browser fingerprinting - hash := sha1.Sum(peercert.Raw) - - // We're not pinning the certificate itself, just the CA that issued it - if peercert.IsCA { - if !bytes.Equal(hash[0:], fingerprint) { - errMessage = fmt.Sprintf("pinned CA key changed; issuer of presented key: %s, DNSNames: %s, IsCA: %t, Subject: %s, fingerprint: %#v, stored fingerprint: %#v", - peercert.Issuer, peercert.DNSNames, peercert.IsCA, peercert.Subject, hash, OktaCACertFingerprint) - } else { - keyPinValid = true - } - } - } - if !keyPinValid { - return nil, fmt.Errorf(errMessage) - } - return c, nil - } -} diff --git a/ssas/okta/okta_test.go b/ssas/okta/okta_test.go deleted file mode 100644 index 379e9b003..000000000 --- a/ssas/okta/okta_test.go +++ /dev/null @@ -1,58 +0,0 @@ -// +build okta - -// To enable this test suite: -// Run "go test -tags=okta -v" from the ssas/okta directory -package okta - -import ( - "os" - "regexp" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/stretchr/testify/suite" -) - -type OTestSuite struct { - suite.Suite -} - -func (s *OTestSuite) TestConfig() { - originalOktaBaseUrl := os.Getenv("OKTA_CLIENT_ORGURL") - originalOktaToken := os.Getenv("OKTA_CLIENT_TOKEN") - originalOktaCACertFingerprint := os.Getenv("OKTA_CA_CERT_FINGERPRINT") - - os.Unsetenv("OKTA_CLIENT_ORGURL") - os.Unsetenv("OKTA_CLIENT_TOKEN") - os.Unsetenv("OKTA_CA_CERT_FINGERPRINT") - - err := config() - require.NotNil(s.T(), err) - assert.Regexp(s.T(), regexp.MustCompile("(OKTA_[A-Z_]*=, ){2}(OKTA_CLIENT_TOKEN=)"), err) - - os.Setenv("OKTA_CLIENT_TOKEN", originalOktaToken) - - err = config() - assert.NotNil(s.T(), err) - assert.Regexp(s.T(), regexp.MustCompile("(OKTA_[A-Z_]*=, ){2}(OKTA_CLIENT_TOKEN=\\[Redacted\\])"), err) - - os.Setenv("OKTA_CLIENT_ORGURL", originalOktaBaseUrl) - os.Setenv("OKTA_CA_CERT_FINGERPRINT", originalOktaCACertFingerprint) - os.Setenv("OKTA_CLIENT_TOKEN", originalOktaToken) - - err = config() - assert.Nil(s.T(), err) -} - -func (s *OTestSuite) TestParseOktaErrorSuccess() { - oktaResponse := []byte(`{"errorCode":"E0000011","errorSummary":"Invalid token provided","errorLink":"E0000011","errorId":"oae3iIXhkQVQ2izGNwhnR47JQ","errorCauses":[]}`) - oktaError, err := ParseOktaError(oktaResponse) - assert.Nil(s.T(), err) - assert.NotNil(s.T(), oktaError) - assert.Equal(s.T(), "Invalid token provided", oktaError.ErrorSummary) -} - -func TestOTestSuite(t *testing.T) { - suite.Run(t, new(OTestSuite)) -} diff --git a/ssas/rsakeys.go b/ssas/rsakeys.go deleted file mode 100644 index 6e600bbbd..000000000 --- a/ssas/rsakeys.go +++ /dev/null @@ -1,165 +0,0 @@ -package ssas - -import ( - "bytes" - "crypto/rand" - "crypto/rsa" - "crypto/x509" - "encoding/base64" - "encoding/json" - "encoding/pem" - "errors" - "fmt" - "io/ioutil" - "math/big" -) - -const RSAKEYMINBITS = 2048 - -// ReadPEMFile reads the contents of the file found at pemPath, returning those contents as a string -// or an empty string with a non-nil error -func ReadPEMFile(pemPath string) ([]byte, error) { - // This function only reads PEM files, and PEM files should never be large enough to require buffering - /* #nosec -- Potential file inclusion via variable */ - pemData, err := ioutil.ReadFile(pemPath) - if err != nil { - return nil, err - } - return pemData, nil -} - -// ReadPrivateKey reads a PEM-formatted private key and returns a pointer to an rsa.PublicKey type -// and an error. The key must have a length of at least 2048 bits, and it must be an rsa key. It must -// also be the first and only key in the file. -func ReadPrivateKey(privateKey []byte) (*rsa.PrivateKey, error) { - if len(privateKey) == 0 { - return nil, fmt.Errorf("empty or nil privateKey argument") - } - block, rest := pem.Decode(privateKey) - if block == nil { - return nil, fmt.Errorf("unable to decode private key '%s'", string(rest)) - } - - rsaPriv, err := x509.ParsePKCS1PrivateKey(block.Bytes) - if err != nil { - return nil, fmt.Errorf("unable to parse RSA private key: %s", err.Error()) - } - - if rsaPriv.Size() < RSAKEYMINBITS/8 { - return nil, fmt.Errorf("insecure key length (%d bytes)", rsaPriv.Size()) - } - - return rsaPriv, nil -} - -// ReadPublicKey reads a string containing a PEM-formatted public key and returns a pointer to an rsa.PublicKey type -// or an error. The key must have a length of at least 2048 bits, and it must be an rsa key. -func ReadPublicKey(publicKey string) (*rsa.PublicKey, error) { - block, _ := pem.Decode([]byte(publicKey)) - if block == nil { - return nil, fmt.Errorf("not able to decode PEM-formatted public key") - } - - publicKeyImported, err := x509.ParsePKIXPublicKey(block.Bytes) - if err != nil { - return nil, fmt.Errorf("unable to parse public key: %s", err.Error()) - } - - rsaPub, ok := publicKeyImported.(*rsa.PublicKey) - if !ok { - return nil, fmt.Errorf("not able to cast key as *rsa.PublicKey") - } - - if rsaPub.Size() < RSAKEYMINBITS/8 { - return nil, fmt.Errorf("insecure key length (%d bytes)", rsaPub.Size()) - } - - return rsaPub, nil -} - -// ConvertJWKToPEM extracts the (hopefully single) public key contained in a jwks. -// Modified from source at: https://play.golang.org/p/mLpOxS-5Fy -func ConvertJWKToPEM(jwks string) (string, error) { - j := map[string]string{} - err := json.Unmarshal([]byte(jwks), &j) - if err != nil { - return "", errors.New("unable to parse JSON for jwk: " + err.Error()) - } - - if j["kty"] != "RSA" { - return "", errors.New("invalid key type: " + j["kty"] + "; only 'RSA' accepted") - } - - if j["use"] != "" && j["use"] != "enc" { - return "", errors.New("invalid use type: " + j["use"] + "; only 'enc' accepted") - } - - nb, err := base64.RawURLEncoding.DecodeString(j["n"]) - if err != nil { - return "", errors.New("base64 error in key n value: " + err.Error()) - } - nv := new(big.Int).SetBytes(nb) - - eb, err := base64.RawURLEncoding.DecodeString(j["e"]) - if err != nil { - return "", errors.New("base64 error in key exponent: " + err.Error()) - } - - bigE := new(big.Int).SetBytes(eb) - if !bigE.IsInt64() { - return "", errors.New("key exponent too large: " + bigE.String()) - } - ev := int(bigE.Int64()) - - pk := &rsa.PublicKey{ - N: nv, - E: ev, - } - - der, err := x509.MarshalPKIXPublicKey(pk) - if err != nil { - return "", errors.New("unable to marshal public key: " + err.Error()) - } - - block := &pem.Block{ - Type: "RSA PUBLIC KEY", - Bytes: der, - } - - var out bytes.Buffer - err = pem.Encode(&out, block) - if err != nil { - return "", errors.New("unable to encode key in PEM format: " + err.Error()) - } - - return out.String(), nil -} - -func ConvertPublicKeyToPEMString(pk *rsa.PublicKey) (string, error) { - der, err := x509.MarshalPKIXPublicKey(pk) - if err != nil { - return "", errors.New("unable to marshal public key: " + err.Error()) - } - - block := &pem.Block{ - Type: "RSA PUBLIC KEY", - Bytes: der, - } - - var out bytes.Buffer - err = pem.Encode(&out, block) - if err != nil { - return "", errors.New("unable to encode key in PEM format: " + err.Error()) - } - - return out.String(), nil -} - -func GenerateTestKeys(bitSize int) (*rsa.PrivateKey, rsa.PublicKey, error) { - reader := rand.Reader - privateKey, err := rsa.GenerateKey(reader, bitSize) - if err != nil { - return nil, rsa.PublicKey{}, err - } - return privateKey, privateKey.PublicKey, nil -} diff --git a/ssas/rsakeys_test.go b/ssas/rsakeys_test.go deleted file mode 100644 index 3ac11d09a..000000000 --- a/ssas/rsakeys_test.go +++ /dev/null @@ -1,100 +0,0 @@ -package ssas - -import ( - "crypto/rsa" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/suite" -) - -const badBase64KeyPath = "../shared_files/ssas/bad_base64_test_private_key.pem" -const notRSAKeyPath = "../shared_files/ssas/not_rsa_test_private_key.pem" -const tooSmallKeyPath = "../shared_files/ssas/too_small_test_private_key.pem" -const goodTestKeyPath = "../shared_files/ssas/good_test_private_key.pem" - -type RSAKeysTestSuite struct { - suite.Suite -} - -func (s *RSAKeysTestSuite) TestReadPrivateKey_EmptyKey() { - pemData, err := ReadPEMFile("") - assert.NotNil(s.T(), err) - assert.Contains(s.T(), err.Error(), "no such file or directory", "expected os.open error") - _, err = ReadPrivateKey(pemData) - assert.NotNil(s.T(), err) - assert.Contains(s.T(), err.Error(), "empty or nil") -} - -func (s *RSAKeysTestSuite) TestReadPrivateKey_BadKeys() { - assertT := assert.New(s.T()) - var tests = []struct { - path string - errMsg string - }{ - {badBase64KeyPath, "decode"}, - {notRSAKeyPath, "parse RSA"}, - {tooSmallKeyPath, "insecure key length"}, - } - for _, test := range tests { - // filePath := os.Getenv(test.path) - pemData, err := ReadPEMFile(test.path) - assertT.Nil(err) - _, err = ReadPrivateKey(pemData) - assertT.NotNil(err) - assertT.Contains(err.Error(), test.errMsg) - } -} - -func (s *RSAKeysTestSuite) TestReadPrivateKey_GoodKeys() { - // filePath := os.Getenv(goodTestKeyPath) - pemData, err := ReadPEMFile(goodTestKeyPath) - assert.Nil(s.T(), err) - privateKey, err := ReadPrivateKey(pemData) - assert.Nil(s.T(), err) - assert.IsType(s.T(), &rsa.PrivateKey{}, privateKey) -} - -func (s *RSAKeysTestSuite) TestConvertJWKToPEMValid() { - var jwk1 = `{"alg":"RS256","e":"AQAB","n":"ok6rvXu95337IxsDXrKzlIqw_I_zPDG8JyEw2CTOtNMoDi1QzpXQVMGj2snNEmvNYaCTmFf51I-EDgeFLLexr40jzBXlg72quV4aw4yiNuxkigW0gMA92OmaT2jMRIdDZM8mVokoxyPfLub2YnXHFq0XuUUgkX_TlutVhgGbyPN0M12teYZtMYo2AUzIRggONhHvnibHP0CPWDjCwSfp3On1Recn4DPxbn3DuGslF2myalmCtkujNcrhHLhwYPP-yZFb8e0XSNTcQvXaQxAqmnWH6NXcOtaeWMQe43PNTAyNinhndgI8ozG3Hz-1NzHssDH_yk6UYFSszhDbWAzyqw","kid":"wyMwK4A6CL9Qw11uofVeyQ119XyX-xykymkkXygZ5OM","kty":"RSA","use":"enc"}` - var jwk2 = `{"e":"AAEAAQ","n":"ok6rvXu95337IxsDXrKzlIqw_I_zPDG8JyEw2CTOtNMoDi1QzpXQVMGj2snNEmvNYaCTmFf51I-EDgeFLLexr40jzBXlg72quV4aw4yiNuxkigW0gMA92OmaT2jMRIdDZM8mVokoxyPfLub2YnXHFq0XuUUgkX_TlutVhgGbyPN0M12teYZtMYo2AUzIRggONhHvnibHP0CPWDjCwSfp3On1Recn4DPxbn3DuGslF2myalmCtkujNcrhHLhwYPP-yZFb8e0XSNTcQvXaQxAqmnWH6NXcOtaeWMQe43PNTAyNinhndgI8ozG3Hz-1NzHssDH_yk6UYFSszhDbWAzyqw","kty":"RSA"}` - - pem1, err := ConvertJWKToPEM(jwk1) - assert.Nil(s.T(), err) - assert.NotEmpty(s.T(), pem1) - pub1, err := ReadPublicKey(pem1) - assert.Nil(s.T(), err) - assert.NotNil(s.T(), pub1) - - pem2, err := ConvertJWKToPEM(jwk2) - assert.Nil(s.T(), err) - assert.NotEmpty(s.T(), pem2) - pub2, err := ReadPublicKey(pem2) - assert.Nil(s.T(), err) - assert.NotNil(s.T(), pub2) -} - -func (s *RSAKeysTestSuite) TestConvertJWKToPEMInvalid() { - jwkForSig := `{"alg":"RS256","e":"AQAB","n":"ok6rvXu95337IxsDXrKzlIqw_I_zPDG8JyEw2CTOtNMoDi1QzpXQVMGj2snNEmvNYaCTmFf51I-EDgeFLLexr40jzBXlg72quV4aw4yiNuxkigW0gMA92OmaT2jMRIdDZM8mVokoxyPfLub2YnXHFq0XuUUgkX_TlutVhgGbyPN0M12teYZtMYo2AUzIRggONhHvnibHP0CPWDjCwSfp3On1Recn4DPxbn3DuGslF2myalmCtkujNcrhHLhwYPP-yZFb8e0XSNTcQvXaQxAqmnWH6NXcOtaeWMQe43PNTAyNinhndgI8ozG3Hz-1NzHssDH_yk6UYFSszhDbWAzyqw","kty":"RSA","use":"sig"}` - jwkECKeyType := `{"kty":"EC","crv":"P-256","x":"MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D4","y":"4Etl6SRW2YiLUrN5vfvVHuhp7x8PxltmWWlbbM4IFyM","use":"enc","kid":"1"}` - jwkCorruptKey := `{"alg":"RS256","e":"AQAB","n":"ok6rvXu95337IxsDXrKzlIqw_I_zPDG8JyEw2CTOtNMoDi1QzpXQVMGj2snNEmvNYaCTmFf51I-EDgeFLLexr40jzBXlg72quV4aw4yiNuxkigW0gMA92OmaT2jMRIdDZM8mVokoxyPfLub2YnXHFq0XuUUgkX_TlutVhgGbyPN0M12teYZtMYo2AUzIRggONhHvnibHP0CPWDjCwSfp3On1Recn4DPxbn3DuGslF2myalmCtkujNcrhHLhwYPP-yZFb8e0XSNTcQvXaQxAqmnWH6NXcOtaeWMQe43PNTAyNinhndgI8ozG3Hz-1NzHssDH_yk6UYFSszhDbW","kty":"RSA","use":"enc"}` - - pem1, err := ConvertJWKToPEM(jwkForSig) - assert.NotNil(s.T(), err) - assert.Contains(s.T(), err.Error(), "use type") - assert.Empty(s.T(), pem1) - - pem2, err := ConvertJWKToPEM(jwkECKeyType) - assert.NotNil(s.T(), err) - assert.Contains(s.T(), err.Error(), "key type") - assert.Empty(s.T(), pem2) - - pem3, err := ConvertJWKToPEM(jwkCorruptKey) - assert.NotNil(s.T(), err) - assert.Contains(s.T(), err.Error(), "error in key n value") - assert.Empty(s.T(), pem3) -} - -func TestRSAKeysTestSuite(t *testing.T) { - suite.Run(t, new(RSAKeysTestSuite)) -} diff --git a/ssas/service/admin/api.go b/ssas/service/admin/api.go deleted file mode 100644 index 9cf7906eb..000000000 --- a/ssas/service/admin/api.go +++ /dev/null @@ -1,232 +0,0 @@ -package admin - -import ( - "encoding/json" - "fmt" - "net/http" - "strings" - - "github.com/go-chi/chi" - "github.com/pborman/uuid" - - "github.com/CMSgov/bcda-app/ssas" - "github.com/CMSgov/bcda-app/ssas/service" -) - -func createGroup(w http.ResponseWriter, r *http.Request) { - gd := ssas.GroupData{} - err := json.NewDecoder(r.Body).Decode(&gd) - if err != nil { - http.Error(w, "Failed to create group due to invalid request body", http.StatusBadRequest) - return - } - - ssas.OperationCalled(ssas.Event{Op: "CreateGroup", TrackingID: gd.GroupID, Help: "calling from admin.createGroup()"}) - g, err := ssas.CreateGroup(gd) - if err != nil { - http.Error(w, fmt.Sprintf("Failed to create group. Error: %s", err), http.StatusBadRequest) - return - } - - groupJSON, err := json.Marshal(g) - if err != nil { - ssas.OperationFailed(ssas.Event{Op: "admin.createGroup", TrackingID: gd.GroupID, Help: err.Error()}) - http.Error(w, "Internal error", http.StatusInternalServerError) - return - } - - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusCreated) - _, err = w.Write(groupJSON) - if err != nil { - ssas.OperationFailed(ssas.Event{Op: "admin.createGroup", TrackingID: gd.GroupID, Help: err.Error()}) - http.Error(w, "Internal error", http.StatusInternalServerError) - } -} - -func listGroups(w http.ResponseWriter, r *http.Request) { - trackingID := uuid.NewRandom().String() - - ssas.OperationCalled(ssas.Event{Op: "ListGroups", TrackingID: trackingID, Help: "calling from admin.listGroups()"}) - groups, err := ssas.ListGroups(trackingID) - if err != nil { - ssas.OperationFailed(ssas.Event{Op: "admin.listGroups", TrackingID: trackingID, Help: err.Error()}) - http.Error(w, fmt.Sprintf("Failed to list groups. Error: %s", err), http.StatusBadRequest) - return - } - - groupsJSON, err := json.Marshal(groups) - if err != nil { - ssas.OperationFailed(ssas.Event{Op: "admin.listGroups", TrackingID: trackingID, Help: err.Error()}) - http.Error(w, "Internal error", http.StatusInternalServerError) - return - } - - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - _, err = w.Write(groupsJSON) - if err != nil { - ssas.OperationFailed(ssas.Event{Op: "admin.listGroups", TrackingID: trackingID, Help: err.Error()}) - http.Error(w, "Internal error", http.StatusInternalServerError) - } -} - -func updateGroup(w http.ResponseWriter, r *http.Request) { - id := chi.URLParam(r, "id") - - gd := ssas.GroupData{} - err := json.NewDecoder(r.Body).Decode(&gd) - if err != nil { - http.Error(w, "Failed to update group due to invalid request body", http.StatusBadRequest) - return - } - - ssas.OperationCalled(ssas.Event{Op: "UpdateGroup", TrackingID: id, Help: "calling from admin.updateGroup()"}) - g, err := ssas.UpdateGroup(id, gd) - if err != nil { - http.Error(w, fmt.Sprintf("Failed to update group. Error: %s", err), http.StatusBadRequest) - return - } - - groupJSON, err := json.Marshal(g) - if err != nil { - ssas.OperationFailed(ssas.Event{Op: "admin.updateGroup", TrackingID: id, Help: err.Error()}) - http.Error(w, "Internal error", http.StatusInternalServerError) - return - } - - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - _, err = w.Write(groupJSON) - if err != nil { - ssas.OperationFailed(ssas.Event{Op: "admin.updateGroup", TrackingID: id, Help: err.Error()}) - http.Error(w, "Internal error", http.StatusInternalServerError) - } -} - -func deleteGroup(w http.ResponseWriter, r *http.Request) { - id := chi.URLParam(r, "id") - - ssas.OperationCalled(ssas.Event{Op: "DeleteGroup", TrackingID: id, Help: "calling from admin.deleteGroup()"}) - err := ssas.DeleteGroup(id) - if err != nil { - ssas.OperationFailed(ssas.Event{Op: "admin.deleteGroup", TrackingID: id, Help: err.Error()}) - http.Error(w, fmt.Sprintf("Failed to delete group. Error: %s", err), http.StatusBadRequest) - return - } - - w.WriteHeader(http.StatusOK) -} - -func createSystem(w http.ResponseWriter, r *http.Request) { - type system struct { - ClientName string `json:"client_name"` - GroupID string `json:"group_id"` - Scope string `json:"scope"` - PublicKey string `json:"public_key"` - TrackingID string `json:"tracking_id"` - } - - sys := system{} - if err := json.NewDecoder(r.Body).Decode(&sys); err != nil { - http.Error(w, "Could not create system due to invalid request body", http.StatusBadRequest) - return - } - - ssas.OperationCalled(ssas.Event{Op: "RegisterClient", TrackingID: sys.TrackingID, Help: "calling from admin.createSystem()"}) - creds, err := ssas.RegisterSystem(sys.ClientName, sys.GroupID, sys.Scope, sys.PublicKey, sys.TrackingID) - if err != nil { - http.Error(w, fmt.Sprintf("Could not create system. Error: %s", err), http.StatusBadRequest) - return - } - - credsJSON, err := json.Marshal(creds) - if err != nil { - http.Error(w, "Internal error", http.StatusInternalServerError) - return - } - - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusCreated) - _, err = w.Write(credsJSON) - if err != nil { - http.Error(w, "Internal error", http.StatusInternalServerError) - } -} - -func resetCredentials(w http.ResponseWriter, r *http.Request) { - systemID := chi.URLParam(r, "systemID") - - system, err := ssas.GetSystemByID(systemID) - if err != nil { - http.Error(w, "Not found", http.StatusNotFound) - return - } - - trackingID := uuid.NewRandom().String() - ssas.OperationCalled(ssas.Event{Op: "ResetSecret", TrackingID: trackingID, Help: "calling from admin.resetCredentials()"}) - credentials, err := system.ResetSecret(trackingID) - if err != nil { - http.Error(w, "Internal error", http.StatusInternalServerError) - return - } - - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusCreated) - fmt.Fprintf(w, `{ "client_id": "%s", "client_secret": "%s" }`, credentials.ClientID, credentials.ClientSecret) -} - -func getPublicKey(w http.ResponseWriter, r *http.Request) { - systemID := chi.URLParam(r, "systemID") - - system, err := ssas.GetSystemByID(systemID) - if err != nil { - http.Error(w, "Not found", http.StatusNotFound) - return - } - - trackingID := uuid.NewRandom().String() - ssas.OperationCalled(ssas.Event{Op: "GetEncryptionKey", TrackingID: trackingID, Help: "calling from admin.getPublicKey()"}) - key, err := system.GetEncryptionKey(trackingID) - if err != nil { - http.Error(w, "Internal error", http.StatusInternalServerError) - return - } - - w.Header().Set("Content-Type", "application/json") - keyStr := strings.Replace(key.Body, "\n", "\\n", -1) - fmt.Fprintf(w, `{ "client_id": "%s", "public_key": "%s" }`, system.ClientID, keyStr) -} - -func deactivateSystemCredentials(w http.ResponseWriter, r *http.Request) { - systemID := chi.URLParam(r, "systemID") - - system, err := ssas.GetSystemByID(systemID) - if err != nil { - http.Error(w, "Not found", http.StatusNotFound) - return - } - err = system.DeactivateSecrets() - - if err != nil { - http.Error(w, "Internal error", http.StatusInternalServerError) - return - } - - w.WriteHeader(http.StatusOK) -} - -func revokeToken(w http.ResponseWriter, r *http.Request) { - tokenID := chi.URLParam(r, "tokenID") - - event := ssas.Event{Op: "TokenBlacklist", TokenID: tokenID} - ssas.OperationCalled(event) - - if err := service.TokenBlacklist.BlacklistToken(tokenID, service.TokenCacheLifetime); err != nil { - event.Help = err.Error() - ssas.OperationFailed(event) - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - } - - w.WriteHeader(http.StatusOK) -} diff --git a/ssas/service/admin/api_test.go b/ssas/service/admin/api_test.go deleted file mode 100644 index 4f27b9299..000000000 --- a/ssas/service/admin/api_test.go +++ /dev/null @@ -1,458 +0,0 @@ -package admin - -import ( - "context" - "encoding/json" - "fmt" - "github.com/CMSgov/bcda-app/ssas/service" - "net/http" - "net/http/httptest" - "strconv" - "strings" - "testing" - - "github.com/CMSgov/bcda-app/ssas" - "github.com/go-chi/chi" - "github.com/jinzhu/gorm" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/suite" -) - -const SampleGroup string = `{ - "group_id":"%s", - "name": "ACO Corp Systems", - "resources": [ - { - "id": "xxx", - "name": "BCDA API", - "scopes": [ - "bcda-api" - ] - }, - { - "id": "eft", - "name": "EFT CCLF", - "scopes": [ - "eft-app:download", - "eft-data:read" - ] - } - ], - "scopes": [ - "user-admin", - "system-admin" - ], - "users": [ - "00uiqolo7fEFSfif70h7", - "l0vckYyfyow4TZ0zOKek", - "HqtEi2khroEZkH4sdIzj" - ], - "xdata": %s -}` - -const SampleXdata string = `"{\"cms_ids\":[\"T67890\",\"T54321\"]}"` - -type APITestSuite struct { - suite.Suite - db *gorm.DB -} - -func (s *APITestSuite) SetupSuite() { - ssas.InitializeGroupModels() - ssas.InitializeSystemModels() - s.db = ssas.GetGORMDbConnection() - service.StartBlacklist() -} - -func (s *APITestSuite) TearDownSuite() { - ssas.Close(s.db) -} - -func (s *APITestSuite) TestCreateGroup() { - gid := ssas.RandomBase64(16) - testInput := fmt.Sprintf(SampleGroup, gid, SampleXdata) - req := httptest.NewRequest("POST", "/group", strings.NewReader(testInput)) - handler := http.HandlerFunc(createGroup) - rr := httptest.NewRecorder() - handler.ServeHTTP(rr, req) - assert.Equal(s.T(), http.StatusCreated, rr.Result().StatusCode) - assert.Equal(s.T(), "application/json", rr.Result().Header.Get("Content-Type")) - g := ssas.Group{} - if s.db.Where("group_id = ?", gid).Find(&g).RecordNotFound() { - assert.FailNow(s.T(), fmt.Sprintf("record not found for group_id=%s", gid)) - } - - // Duplicate request fails - rr = httptest.NewRecorder() - handler.ServeHTTP(rr, req) - assert.Equal(s.T(), http.StatusBadRequest, rr.Result().StatusCode) - - err := ssas.CleanDatabase(g) - assert.Nil(s.T(), err) -} - -func (s *APITestSuite) TestCreateGroupFailure() { - gid := ssas.RandomBase64(16) - testInput := fmt.Sprintf(SampleGroup, gid, SampleXdata) - req := httptest.NewRequest("POST", "/group", strings.NewReader(testInput)) - handler := http.HandlerFunc(createGroup) - rr := httptest.NewRecorder() - handler.ServeHTTP(rr, req) - assert.Equal(s.T(), http.StatusCreated, rr.Result().StatusCode) - assert.Equal(s.T(), "application/json", rr.Result().Header.Get("Content-Type")) - g := ssas.Group{} - if s.db.Where("group_id = ?", gid).Find(&g).RecordNotFound() { - assert.FailNow(s.T(), fmt.Sprintf("record not found for group_id=%s", gid)) - } - err := ssas.CleanDatabase(g) - assert.Nil(s.T(), err) -} - -func (s *APITestSuite) TestListGroups() { - var startingCount int - ssas.GetGORMDbConnection().Table("groups").Count(&startingCount) - - gid := ssas.RandomBase64(16) - testInput1 := fmt.Sprintf(SampleGroup, gid, SampleXdata) - gd := ssas.GroupData{} - err := json.Unmarshal([]byte(testInput1), &gd) - assert.Nil(s.T(), err) - g1, err := ssas.CreateGroup(gd) - assert.Nil(s.T(), err) - - gd.GroupID = ssas.RandomHexID() - gd.Name = "another-fake-name" - g2, err := ssas.CreateGroup(gd) - assert.Nil(s.T(), err) - - req := httptest.NewRequest("GET", "/group", nil) - handler := http.HandlerFunc(listGroups) - rr := httptest.NewRecorder() - handler.ServeHTTP(rr, req) - assert.Equal(s.T(), http.StatusOK, rr.Result().StatusCode) - assert.Equal(s.T(), "application/json", rr.Result().Header.Get("Content-Type")) - groups := []ssas.Group{} - err = json.Unmarshal(rr.Body.Bytes(), &groups) - assert.Nil(s.T(), err) - assert.True(s.T(), len(groups) == 2+startingCount) - - err = ssas.CleanDatabase(g1) - assert.Nil(s.T(), err) - err = ssas.CleanDatabase(g2) - assert.Nil(s.T(), err) -} - -func (s *APITestSuite) TestUpdateGroup() { - gid := ssas.RandomBase64(16) - testInput := fmt.Sprintf(SampleGroup, gid, SampleXdata) - gd := ssas.GroupData{} - err := json.Unmarshal([]byte(testInput), &gd) - assert.Nil(s.T(), err) - g, err := ssas.CreateGroup(gd) - assert.Nil(s.T(), err) - - url := fmt.Sprintf("/group/%v", g.ID) - req := httptest.NewRequest("PUT", url, strings.NewReader(testInput)) - rctx := chi.NewRouteContext() - rctx.URLParams.Add("id", fmt.Sprint(g.ID)) - req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx)) - handler := http.HandlerFunc(updateGroup) - rr := httptest.NewRecorder() - handler.ServeHTTP(rr, req) - assert.Equal(s.T(), http.StatusOK, rr.Result().StatusCode) - assert.Equal(s.T(), "application/json", rr.Result().Header.Get("Content-Type")) - err = ssas.CleanDatabase(g) - assert.Nil(s.T(), err) -} - - -func (s *APITestSuite) TestUpdateGroupBadGroupID() { - gid := ssas.RandomBase64(16) - testInput := fmt.Sprintf(SampleGroup, gid, SampleXdata) - gd := ssas.GroupData{} - err := json.Unmarshal([]byte(testInput), &gd) - assert.Nil(s.T(), err) - - // No group exists - url := fmt.Sprintf("/group/%v", gid) - req := httptest.NewRequest("PUT", url, strings.NewReader(testInput)) - rctx := chi.NewRouteContext() - req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx)) - handler := http.HandlerFunc(updateGroup) - rr := httptest.NewRecorder() - handler.ServeHTTP(rr, req) - assert.Equal(s.T(), http.StatusBadRequest, rr.Result().StatusCode) -} - -func (s *APITestSuite) TestRevokeToken() { - tokenID := "abc-123-def-456" - - url := fmt.Sprintf("/token/%s", tokenID) - req := httptest.NewRequest("DELETE", url, nil) - rctx := chi.NewRouteContext() - rctx.URLParams.Add("tokenID", tokenID) - req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx)) - handler := http.HandlerFunc(revokeToken) - rr := httptest.NewRecorder() - handler.ServeHTTP(rr, req) - assert.Equal(s.T(), http.StatusOK, rr.Result().StatusCode) - - assert.True(s.T(), service.TokenBlacklist.IsTokenBlacklisted(tokenID)) - assert.False(s.T(), service.TokenBlacklist.IsTokenBlacklisted("this_key_should_not_exist")) -} - -func (s *APITestSuite) TestDeleteGroup() { - gid := ssas.RandomHexID() - testInput := fmt.Sprintf(SampleGroup, gid, SampleXdata) - groupBytes := []byte(testInput) - gd := ssas.GroupData{} - err := json.Unmarshal(groupBytes, &gd) - assert.Nil(s.T(), err) - g, err := ssas.CreateGroup(gd) - assert.Nil(s.T(), err) - - url := fmt.Sprintf("/group/%v", g.ID) - req := httptest.NewRequest("DELETE", url, nil) - rctx := chi.NewRouteContext() - rctx.URLParams.Add("id", fmt.Sprint(g.ID)) - req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx)) - handler := http.HandlerFunc(deleteGroup) - rr := httptest.NewRecorder() - handler.ServeHTTP(rr, req) - assert.Equal(s.T(), http.StatusOK, rr.Result().StatusCode) - deleted := s.db.Find(&ssas.Group{}, g.ID).RecordNotFound() - assert.True(s.T(), deleted) - err = ssas.CleanDatabase(g) - assert.Nil(s.T(), err) -} - -func (s *APITestSuite) TestCreateSystem() { - group := ssas.Group{GroupID: "test-group-id"} - err := s.db.Save(&group).Error - if err != nil { - s.FailNow("Error creating test data", err.Error()) - } - - req := httptest.NewRequest("POST", "/auth/system", strings.NewReader(`{"client_name": "Test Client", "group_id": "test-group-id", "scope": "bcda-api", "public_key": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArhxobShmNifzW3xznB+L\nI8+hgaePpSGIFCtFz2IXGU6EMLdeufhADaGPLft9xjwdN1ts276iXQiaChKPA2CK\n/CBpuKcnU3LhU8JEi7u/db7J4lJlh6evjdKVKlMuhPcljnIKAiGcWln3zwYrFCeL\ncN0aTOt4xnQpm8OqHawJ18y0WhsWT+hf1DeBDWvdfRuAPlfuVtl3KkrNYn1yqCgQ\nlT6v/WyzptJhSR1jxdR7XLOhDGTZUzlHXh2bM7sav2n1+sLsuCkzTJqWZ8K7k7cI\nXK354CNpCdyRYUAUvr4rORIAUmcIFjaR3J4y/Dh2JIyDToOHg7vjpCtNnNoS+ON2\nHwIDAQAB\n-----END PUBLIC KEY-----", "tracking_id": "T00000"}`)) - handler := http.HandlerFunc(createSystem) - rr := httptest.NewRecorder() - handler.ServeHTTP(rr, req) - assert.Equal(s.T(), http.StatusCreated, rr.Result().StatusCode) - assert.Equal(s.T(), "application/json", rr.Result().Header.Get("Content-Type")) - var result map[string]interface{} - _ = json.Unmarshal(rr.Body.Bytes(), &result) - assert.NotEmpty(s.T(), result["client_id"]) - assert.NotEmpty(s.T(), result["client_secret"]) - assert.Equal(s.T(), "Test Client", result["client_name"]) - - err = ssas.CleanDatabase(group) - assert.Nil(s.T(), err) -} - -func (s *APITestSuite) TestCreateSystem_InvalidRequest() { - req := httptest.NewRequest("POST", "/auth/system", strings.NewReader("{ badJSON }")) - handler := http.HandlerFunc(createSystem) - rr := httptest.NewRecorder() - handler.ServeHTTP(rr, req) - assert.Equal(s.T(), http.StatusBadRequest, rr.Result().StatusCode) -} - -func (s *APITestSuite) TestCreateSystem_MissingRequiredParam() { - req := httptest.NewRequest("POST", "/auth/system", strings.NewReader(`{"group_id": "T00001", "client_name": "Test Client 1", "scope": "bcda-api"}`)) - handler := http.HandlerFunc(createSystem) - rr := httptest.NewRecorder() - handler.ServeHTTP(rr, req) - assert.Equal(s.T(), http.StatusBadRequest, rr.Result().StatusCode) -} - -func (s *APITestSuite) TestResetCredentials() { - group := ssas.Group{GroupID: "test-reset-creds-group"} - s.db.Create(&group) - system := ssas.System{GroupID: group.GroupID, ClientID: "test-reset-creds-client"} - s.db.Create(&system) - secret := ssas.Secret{Hash: "test-reset-creds-hash", SystemID: system.ID} - s.db.Create(&secret) - - systemID := strconv.FormatUint(uint64(system.ID), 10) - req := httptest.NewRequest("PUT", "/system/"+systemID+"/credentials", nil) - rctx := chi.NewRouteContext() - rctx.URLParams.Add("systemID", systemID) - req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx)) - handler := http.HandlerFunc(resetCredentials) - rr := httptest.NewRecorder() - handler.ServeHTTP(rr, req) - - assert.Equal(s.T(), http.StatusCreated, rr.Result().StatusCode) - assert.Equal(s.T(), "application/json", rr.Result().Header.Get("Content-Type")) - var result map[string]string - _ = json.Unmarshal(rr.Body.Bytes(), &result) - newSecret := result["client_secret"] - assert.NotEmpty(s.T(), newSecret) - assert.NotEqual(s.T(), secret.Hash, newSecret) - - _ = ssas.CleanDatabase(group) -} - -func (s *APITestSuite) TestResetCredentials_InvalidSystemID() { - systemID := "999" - req := httptest.NewRequest("PUT", "/system/"+systemID+"/credentials", nil) - rctx := chi.NewRouteContext() - rctx.URLParams.Add("systemID", systemID) - req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx)) - handler := http.HandlerFunc(resetCredentials) - rr := httptest.NewRecorder() - handler.ServeHTTP(rr, req) - - assert.Equal(s.T(), http.StatusNotFound, rr.Result().StatusCode) -} - -func (s *APITestSuite) TestGetPublicKeyBadSystemID() { - systemID := strconv.FormatUint(uint64(9999), 10) - req := httptest.NewRequest("GET", "/system/"+systemID+"/key", nil) - rctx := chi.NewRouteContext() - rctx.URLParams.Add("systemID", systemID) - req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx)) - handler := http.HandlerFunc(getPublicKey) - rr := httptest.NewRecorder() - handler.ServeHTTP(rr, req) - - assert.Equal(s.T(), http.StatusNotFound, rr.Result().StatusCode) -} - -func (s *APITestSuite) TestGetPublicKey() { - group := ssas.Group{GroupID: "api-test-get-public-key-group"} - err := s.db.Create(&group).Error - if err != nil { - s.FailNow(err.Error()) - } - - system := ssas.System{GroupID: group.GroupID, ClientID: "api-test-get-public-key-client"} - err = s.db.Create(&system).Error - if err != nil { - s.FailNow(err.Error()) - } - - key1Str := "publickey1" - encrKey1 := ssas.EncryptionKey{ - SystemID: system.ID, - Body: key1Str, - } - err = s.db.Create(&encrKey1).Error - if err != nil { - s.FailNow(err.Error()) - } - - systemID := strconv.FormatUint(uint64(system.ID), 10) - req := httptest.NewRequest("GET", "/system/"+systemID+"/key", nil) - rctx := chi.NewRouteContext() - rctx.URLParams.Add("systemID", systemID) - req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx)) - handler := http.HandlerFunc(getPublicKey) - rr := httptest.NewRecorder() - handler.ServeHTTP(rr, req) - - assert.Equal(s.T(), http.StatusOK, rr.Result().StatusCode) - assert.Equal(s.T(), "application/json", rr.Result().Header.Get("Content-Type")) - var result map[string]string - err = json.Unmarshal(rr.Body.Bytes(), &result) - if err != nil { - s.FailNow(err.Error()) - } - - assert.Equal(s.T(), system.ClientID, result["client_id"]) - resPublicKey := result["public_key"] - assert.NotEmpty(s.T(), resPublicKey) - assert.Equal(s.T(), key1Str, resPublicKey) - - err = ssas.CleanDatabase(group) - assert.Nil(s.T(), err) -} - -func (s *APITestSuite) TestGetPublicKey_Rotation() { - group := ssas.Group{GroupID: "api-test-get-public-key-group"} - err := s.db.Create(&group).Error - if err != nil { - s.FailNow(err.Error()) - } - - system := ssas.System{GroupID: group.GroupID, ClientID: "api-test-get-public-key-client"} - err = s.db.Create(&system).Error - if err != nil { - s.FailNow(err.Error()) - } - - key1, _ := ssas.GeneratePublicKey(2048) - err = system.SavePublicKey(strings.NewReader(key1)) - if err != nil { - s.FailNow(err.Error()) - } - - key2, _ := ssas.GeneratePublicKey(2048) - err = system.SavePublicKey(strings.NewReader(key2)) - if err != nil { - s.FailNow(err.Error()) - } - - systemID := strconv.FormatUint(uint64(system.ID), 10) - req := httptest.NewRequest("GET", "/system/"+systemID+"/key", nil) - rctx := chi.NewRouteContext() - rctx.URLParams.Add("systemID", systemID) - req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx)) - handler := http.HandlerFunc(getPublicKey) - rr := httptest.NewRecorder() - handler.ServeHTTP(rr, req) - - assert.Equal(s.T(), http.StatusOK, rr.Result().StatusCode) - assert.Equal(s.T(), "application/json", rr.Result().Header.Get("Content-Type")) - var result map[string]string - err = json.Unmarshal(rr.Body.Bytes(), &result) - if err != nil { - s.FailNow(err.Error()) - } - - assert.Equal(s.T(), system.ClientID, result["client_id"]) - resPublicKey := result["public_key"] - assert.NotEmpty(s.T(), resPublicKey) - assert.Equal(s.T(), key2, resPublicKey) - - err = ssas.CleanDatabase(group) - assert.Nil(s.T(), err) -} - -func (s *APITestSuite) TestDeactivateSystemCredentialsNotFound() { - systemID := strconv.FormatUint(uint64(9999), 10) - req := httptest.NewRequest("DELETE", "/system/"+systemID+"/credentials", nil) - rctx := chi.NewRouteContext() - rctx.URLParams.Add("systemID", systemID) - req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx)) - handler := http.HandlerFunc(deactivateSystemCredentials) - rr := httptest.NewRecorder() - handler.ServeHTTP(rr, req) - _ = Server() - assert.Equal(s.T(), http.StatusNotFound, rr.Result().StatusCode) -} - -func (s *APITestSuite) TestDeactivateSystemCredentials() { - group := ssas.Group{GroupID: "test-deactivate-creds-group"} - s.db.Create(&group) - system := ssas.System{GroupID: group.GroupID, ClientID: "test-deactivate-creds-client"} - s.db.Create(&system) - secret := ssas.Secret{Hash: "test-deactivate-creds-hash", SystemID: system.ID} - s.db.Create(&secret) - - systemID := strconv.FormatUint(uint64(system.ID), 10) - req := httptest.NewRequest("DELETE", "/system/"+systemID+"/credentials", nil) - rctx := chi.NewRouteContext() - rctx.URLParams.Add("systemID", systemID) - req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx)) - handler := http.HandlerFunc(deactivateSystemCredentials) - rr := httptest.NewRecorder() - handler.ServeHTTP(rr, req) - assert.Equal(s.T(), http.StatusOK, rr.Result().StatusCode) - - _ = ssas.CleanDatabase(group) -} - -func TestAPITestSuite(t *testing.T) { - suite.Run(t, new(APITestSuite)) -} diff --git a/ssas/service/admin/middleware.go b/ssas/service/admin/middleware.go deleted file mode 100644 index fc9ae4d34..000000000 --- a/ssas/service/admin/middleware.go +++ /dev/null @@ -1,42 +0,0 @@ -package admin - -import ( - "fmt" - "net/http" - - "github.com/CMSgov/bcda-app/ssas" -) - -func requireBasicAuth(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - clientID, secret, ok := r.BasicAuth() - if !ok { - http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) - return - } - - system, err := ssas.GetSystemByClientID(clientID) - if err != nil { - jsonError(w, http.StatusText(http.StatusUnauthorized), "invalid client id") - return - } - - savedSecret, err := system.GetSecret() - if err != nil || !ssas.Hash(savedSecret).IsHashOf(secret) { - jsonError(w, http.StatusText(http.StatusUnauthorized), "invalid client secret") - return - } - - next.ServeHTTP(w, r) - }) -} - -func jsonError(w http.ResponseWriter, error string, description string) { - ssas.Logger.Printf("%s; %s", description, error) - w.WriteHeader(http.StatusBadRequest) - body := []byte(fmt.Sprintf(`{"error":"%s","error_description":"%s"}`, error, description)) - _, err := w.Write(body) - if err != nil { - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - } -} diff --git a/ssas/service/admin/router.go b/ssas/service/admin/router.go deleted file mode 100644 index 7aea9d05c..000000000 --- a/ssas/service/admin/router.go +++ /dev/null @@ -1,48 +0,0 @@ -package admin - -import ( - "fmt" - "os" - "time" - - "github.com/go-chi/chi" - - "github.com/CMSgov/bcda-app/ssas/service" -) - -var version = "latest" -var infoMap map[string][]string -var adminSigningKeyPath string -var server *service.Server - -func init() { - infoMap = make(map[string][]string) - adminSigningKeyPath = os.Getenv("SSAS_ADMIN_SIGNING_KEY_PATH") -} - -// Server creates an SSAS admin server -func Server() *service.Server { - unsafeMode := os.Getenv("HTTP_ONLY") == "true" - server = service.NewServer("admin", ":3004", version, infoMap, routes(), unsafeMode, adminSigningKeyPath, 20*time.Minute) - if server != nil { - r, _ := server.ListRoutes() - infoMap["banner"] = []string{fmt.Sprintf("%s server running on port %s", "admin", ":3004")} - infoMap["routes"] = r - } - return server -} - -func routes() *chi.Mux { - r := chi.NewRouter() - r.Use(service.NewAPILogger(), service.ConnectionClose, requireBasicAuth) - r.Post("/group", createGroup) - r.Get("/group", listGroups) - r.Put("/group/{id}", updateGroup) - r.Delete("/group/{id}", deleteGroup) - r.Post("/system", createSystem) - r.Put("/system/{systemID}/credentials", resetCredentials) - r.Get("/system/{systemID}/key", getPublicKey) - r.Delete("/system/{systemID}/credentials", deactivateSystemCredentials) - r.Delete("/token/{tokenID}", revokeToken) - return r -} diff --git a/ssas/service/admin/router_test.go b/ssas/service/admin/router_test.go deleted file mode 100644 index 843c260e2..000000000 --- a/ssas/service/admin/router_test.go +++ /dev/null @@ -1,178 +0,0 @@ -package admin - -import ( - "encoding/base64" - "net/http" - "net/http/httptest" - "strconv" - "testing" - - "github.com/CMSgov/bcda-app/ssas" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/suite" -) - -type RouterTestSuite struct { - suite.Suite - router http.Handler - basicAuth string - badAuth string - group ssas.Group -} - -func (s *RouterTestSuite) SetupSuite() { - id := "31e029ef-0e97-47f8-873c-0e8b7e7f99bf" - system, err := ssas.GetSystemByClientID(id) - if err != nil { - s.FailNow(err.Error()) - } - - creds, err := system.ResetSecret(id) - if err != nil { - s.FailNow(err.Error()) - } - - basicAuth := id + ":" + creds.ClientSecret - s.basicAuth = base64.StdEncoding.EncodeToString([]byte(basicAuth)) - - badAuth := id + ":This_is_not_the_secret" - s.badAuth = base64.StdEncoding.EncodeToString([]byte(badAuth)) -} - -func (s *RouterTestSuite) SetupTest() { - s.router = routes() -} - -func (s *RouterTestSuite) TearDownSuite() { - db := ssas.GetGORMDbConnection() - defer ssas.Close(db) - - _ = ssas.CleanDatabase(s.group) -} - -func (s *RouterTestSuite) TestUnauthorized() { - req := httptest.NewRequest("GET", "/group", nil) - basicAuth := base64.StdEncoding.EncodeToString([]byte("bad:creds")) - req.Header.Add("Authorization", "Basic "+basicAuth) - rr := httptest.NewRecorder() - s.router.ServeHTTP(rr, req) - res := rr.Result() - assert.Equal(s.T(), http.StatusBadRequest, res.StatusCode) -} - -func (s *RouterTestSuite) TestNonBasicAuth() { - req := httptest.NewRequest("GET", "/group", nil) - req.Header.Add("Authorization", "This is not a base64-encoded username/password pair!") - rr := httptest.NewRecorder() - s.router.ServeHTTP(rr, req) - res := rr.Result() - assert.Equal(s.T(), http.StatusBadRequest, res.StatusCode) -} - -func (s *RouterTestSuite) TestBadSecret() { - req := httptest.NewRequest("GET", "/group", nil) - req.Header.Add("Authorization", "Basic "+s.badAuth) - rr := httptest.NewRecorder() - s.router.ServeHTTP(rr, req) - res := rr.Result() - assert.Equal(s.T(), http.StatusBadRequest, res.StatusCode) -} - -func (s *RouterTestSuite) TestRevokeToken() { - req := httptest.NewRequest("DELETE", "/token/abc-123", nil) - req.Header.Add("Authorization", "Basic "+s.basicAuth) - rr := httptest.NewRecorder() - s.router.ServeHTTP(rr, req) - res := rr.Result() - assert.Equal(s.T(), http.StatusOK, res.StatusCode) -} - -func (s *RouterTestSuite) TestPostGroup() { - req := httptest.NewRequest("POST", "/group", nil) - req.Header.Add("Authorization", "Basic "+s.basicAuth) - rr := httptest.NewRecorder() - s.router.ServeHTTP(rr, req) - res := rr.Result() - assert.Equal(s.T(), http.StatusBadRequest, res.StatusCode) -} - -func (s *RouterTestSuite) TestGetGroup() { - req := httptest.NewRequest("GET", "/group", nil) - req.Header.Add("Authorization", "Basic "+s.basicAuth) - rr := httptest.NewRecorder() - s.router.ServeHTTP(rr, req) - res := rr.Result() - assert.Equal(s.T(), http.StatusOK, res.StatusCode) -} - -func (s *RouterTestSuite) TestPutGroup() { - req := httptest.NewRequest("PUT", "/group/1", nil) - req.Header.Add("Authorization", "Basic "+s.basicAuth) - rr := httptest.NewRecorder() - s.router.ServeHTTP(rr, req) - res := rr.Result() - assert.Equal(s.T(), http.StatusBadRequest, res.StatusCode) -} - -func (s *RouterTestSuite) TestDeleteGroup() { - req := httptest.NewRequest("DELETE", "/group/101", nil) - req.Header.Add("Authorization", "Basic "+s.basicAuth) - rr := httptest.NewRecorder() - s.router.ServeHTTP(rr, req) - res := rr.Result() - assert.Equal(s.T(), http.StatusBadRequest, res.StatusCode) -} - -func (s *RouterTestSuite) TestPostSystem() { - req := httptest.NewRequest("POST", "/system", nil) - req.Header.Add("Authorization", "Basic "+s.basicAuth) - rr := httptest.NewRecorder() - s.router.ServeHTTP(rr, req) - res := rr.Result() - assert.Equal(s.T(), http.StatusBadRequest, res.StatusCode) -} - -func (s *RouterTestSuite) TestDeactivateSystemCredentials() { - db := ssas.GetGORMDbConnection() - defer db.Close() - group := ssas.Group{GroupID: "delete-system-credentials-test-group"} - db.Create(&group) - system := ssas.System{GroupID: group.GroupID, ClientID: "delete-system-credentials-test-system"} - db.Create(&system) - systemID := strconv.FormatUint(uint64(system.ID), 10) - - req := httptest.NewRequest("DELETE", "/system/"+systemID+"/credentials", nil) - req.Header.Add("Authorization", "Basic "+s.basicAuth) - rr := httptest.NewRecorder() - s.router.ServeHTTP(rr, req) - res := rr.Result() - assert.Equal(s.T(), http.StatusOK, res.StatusCode) - - err := ssas.CleanDatabase(group) - assert.Nil(s.T(), err) -} - -func (s *RouterTestSuite) TestPutSystemCredentials() { - db := ssas.GetGORMDbConnection() - defer db.Close() - group := ssas.Group{GroupID: "put-system-credentials-test-group"} - db.Create(&group) - system := ssas.System{GroupID: group.GroupID, ClientID: "put-system-credentials-test-system"} - db.Create(&system) - systemID := strconv.FormatUint(uint64(system.ID), 10) - - req := httptest.NewRequest("PUT", "/system/"+systemID+"/credentials", nil) - req.Header.Add("Authorization", "Basic "+s.basicAuth) - rr := httptest.NewRecorder() - s.router.ServeHTTP(rr, req) - res := rr.Result() - assert.Equal(s.T(), http.StatusCreated, res.StatusCode) - - err := ssas.CleanDatabase(group) - assert.Nil(s.T(), err) -} - -func TestRouterTestSuite(t *testing.T) { - suite.Run(t, new(RouterTestSuite)) -} diff --git a/ssas/service/logging.go b/ssas/service/logging.go deleted file mode 100644 index fac6ad774..000000000 --- a/ssas/service/logging.go +++ /dev/null @@ -1,107 +0,0 @@ -package service - -import ( - "fmt" - "net/http" - "regexp" - "strings" - "time" - - "github.com/go-chi/chi/middleware" - "github.com/sirupsen/logrus" - - "github.com/CMSgov/bcda-app/ssas" -) - -// https://github.com/go-chi/chi/blob/master/_examples/logging/main.go - -func NewAPILogger() func(next http.Handler) http.Handler { - return middleware.RequestLogger(&APILogger{ssas.Logger}) -} - -type APILogger struct { - Logger *logrus.Logger -} - -func (l *APILogger) NewLogEntry(r *http.Request) middleware.LogEntry { - entry := &APILoggerEntry{Logger: logrus.NewEntry(l.Logger)} - logFields := logrus.Fields{} - - logFields["ts"] = time.Now() // .UTC().Format(time.RFC1123) - - if reqID := middleware.GetReqID(r.Context()); reqID != "" { - logFields["req_id"] = reqID - } - - scheme := "http" - // if servicemux.IsHTTPS(r) { - // scheme = "https" - // } - logFields["http_scheme"] = scheme - logFields["http_proto"] = r.Proto - logFields["http_method"] = r.Method - - logFields["remote_addr"] = r.RemoteAddr - logFields["user_agent"] = r.UserAgent() - - logFields["uri"] = fmt.Sprintf("%s://%s%s", scheme, r.Host, Redact(r.RequestURI)) - - if rd, ok := r.Context().Value("rd").(ssas.AuthRegData); ok { - logFields["group_id"] = rd.GroupID - logFields["okta_id"] = rd.OktaID - } - - entry.Logger = entry.Logger.WithFields(logFields) - - entry.Logger.Infoln("request started") - - return entry -} - -type APILoggerEntry struct { - Logger logrus.FieldLogger -} - -func (l *APILoggerEntry) Write(status, bytes int, elapsed time.Duration) { - l.Logger = l.Logger.WithFields(logrus.Fields{ - "resp_status": status, "resp_bytes_length": bytes, - "resp_elapsed_ms": float64(elapsed.Nanoseconds()) / 1000000.0, - }) - - l.Logger.Infoln("request complete") -} - -func (l *APILoggerEntry) Panic(v interface{}, stack []byte) { - l.Logger = l.Logger.WithFields(logrus.Fields{ - "stack": string(stack), - "panic": fmt.Sprintf("%+v", v), - }) -} - -func Redact(uri string) string { - re := regexp.MustCompile(`Bearer%20([^&]+)(?:&|$)`) - submatches := re.FindAllStringSubmatch(uri, -1) - for _, match := range submatches { - uri = strings.Replace(uri, match[1], "", 1) - } - return uri -} - -func GetLogEntry(r *http.Request) logrus.FieldLogger { - entry := middleware.GetLogEntry(r).(*APILoggerEntry) - return entry.Logger -} - -func LogEntrySetField(r *http.Request, key string, value interface{}) { - if entry, ok := r.Context().Value(middleware.LogEntryCtxKey).(*APILoggerEntry); ok { - entry.Logger = entry.Logger.WithField(key, value) - } -} - -func LogEntrySetFields(r *http.Request, fields map[string]interface{}) { - if entry, ok := r.Context().Value(middleware.LogEntryCtxKey).(*APILoggerEntry); ok { - entry.Logger = entry.Logger.WithFields(fields) - } -} - - diff --git a/ssas/service/main/main.go b/ssas/service/main/main.go deleted file mode 100644 index 61a0286f0..000000000 --- a/ssas/service/main/main.go +++ /dev/null @@ -1,242 +0,0 @@ -package main - -import ( - "flag" - "fmt" - "io" - "net/http" - "os" - "strconv" - "time" - - "github.com/go-chi/chi" - "github.com/jinzhu/gorm" - - "github.com/CMSgov/bcda-app/ssas" - "github.com/CMSgov/bcda-app/ssas/service" - "github.com/CMSgov/bcda-app/ssas/service/admin" - "github.com/CMSgov/bcda-app/ssas/service/public" -) - -var doMigrate bool -var doAddFixtureData bool -var doResetSecret bool -var doNewAdminSystem bool -var doMigrateAndStart bool -var doStart bool -var clientID string -var systemName string -var output io.Writer - -func init() { - output = os.Stdout - - const usageMigrate = "unconditionally migrate the db" - flag.BoolVar(&doMigrate, "migrate", false, usageMigrate) - - const usageAddFixtureData = "unconditionally add fixture data" - flag.BoolVar(&doAddFixtureData, "add-fixture-data", false, usageAddFixtureData) - - const usageResetSecret = "reset system secret for the given client_id; requires client-id flag with argument" - flag.BoolVar(&doResetSecret, "reset-secret", false, usageResetSecret) - flag.StringVar(&clientID, "client-id", "", "a system's client id") - - const usageNewAdminSystem = "add a new admin system to the service; requires system-name flag with argument" - flag.BoolVar(&doNewAdminSystem, "new-admin-system", false, usageNewAdminSystem) - flag.StringVar(&systemName, "system-name", "", "the system's name (e.g., 'BCDA Admin')") - - // we need this all-in-one command to start using fresh in the docker container - const usageMigrateAndStart = "start the service; if DEBUG=true, will also migrate the db" - flag.BoolVar(&doMigrateAndStart, "migrate-and-start", false, usageMigrateAndStart) - - const usageStart = "start the service" - flag.BoolVar(&doStart, "start", false, usageStart) -} - -// We provide some simple commands for bootstrapping the system into place. Commands cannot be combined. -func main() { - ssas.Logger.Info("Home of the System-to-System Authentication Service") - - flag.Parse() - if doMigrate { - ssas.InitializeGroupModels() - ssas.InitializeSystemModels() - ssas.InitializeBlacklistModels() - return - } - if doAddFixtureData { - addFixtureData() - return - } - if doResetSecret && clientID != "" { - resetSecret(clientID) - return - } - if doNewAdminSystem && systemName != "" { - newAdminSystem(systemName) - return - } - if doMigrateAndStart { - if os.Getenv("DEBUG") == "true" { - ssas.InitializeGroupModels() - ssas.InitializeSystemModels() - ssas.InitializeBlacklistModels() - addFixtureData() - } - start() - return - } - if doStart { - start() - return - } -} - -func start() { - ssas.Logger.Infof("%s", "Starting ssas...") - - ps := public.Server() - if ps == nil { - ssas.Logger.Error("unable to create public server") - os.Exit(-1) - } - ps.LogRoutes() - ps.Serve() - - as := admin.Server() - if as == nil { - ssas.Logger.Error("unable to create admin server") - os.Exit(-1) - } - as.LogRoutes() - as.Serve() - - service.StartBlacklist() - - // Accepts and redirects HTTP requests to HTTPS. Not sure we should do this. - forwarder := &http.Server{ - Handler: newForwardingRouter(), - Addr: ":3005", - ReadTimeout: 5 * time.Second, - WriteTimeout: 5 * time.Second, - } - ssas.Logger.Fatal(forwarder.ListenAndServe()) -} - -func newForwardingRouter() http.Handler { - r := chi.NewRouter() - r.Use(service.NewAPILogger(), service.ConnectionClose) - r.Get("/*", http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - // TODO only forward requests for paths in our own host or resource server - url := "https://" + req.Host + req.URL.String() - ssas.Logger.Infof("forwarding from %s to %s", req.Host+req.URL.String(), url) - http.Redirect(w, req, url, http.StatusMovedPermanently) - })) - return r -} - -func addFixtureData() { - db := ssas.GetGORMDbConnection() - defer ssas.Close(db) - - if err := db.Save(&ssas.Group{GroupID: "admin"}).Error; err != nil { - fmt.Println(err) - } - // group for cms_id A9994; client_id 0c527d2e-2e8a-4808-b11d-0fa06baf8254 - if err := db.Save(&ssas.Group{GroupID: "0c527d2e-2e8a-4808-b11d-0fa06baf8254", Data: ssas.GroupData{GroupID: "0c527d2e-2e8a-4808-b11d-0fa06baf8254"}, XData: `"{\"cms_ids\":[\"A9994\"]}"`}).Error; err != nil { - fmt.Println(err) - } - makeSystem(db, "admin", "31e029ef-0e97-47f8-873c-0e8b7e7f99bf", - "BCDA API Admin", "bcda-admin", - "nbZ5oAnTlzyzeep46bL4qDGGuidXuYxs3xknVWBKjTI=:9s/Tnqvs8M7GN6VjGkLhCgjmS59r6TaVguos8dKV9lGqC1gVG8ywZVEpDMkdwOaj8GoNe4TU3jS+OZsK3kTfEQ==", - ) - makeSystem(db, "0c527d2e-2e8a-4808-b11d-0fa06baf8254", - "0c527d2e-2e8a-4808-b11d-0fa06baf8254", "ACO Dev", "bcda-api", - "h5hF9cm0Wmm+ClnoF0+Dq5JCQFmDVtzAsaquigoYcTk=:mptcWsBLNYFylRT1q95brbfKiaQkUt8oZXml0EMXobghbVVewZeG40EfNqe10u1+RftftEMvzSCB/oG17MRpVA==") -} - -func makeSystem(db *gorm.DB, groupID, clientID, clientName, scope, hash string) { - pem := `-----BEGIN PUBLIC KEY----- - MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArhxobShmNifzW3xznB+L - I8+hgaePpSGIFCtFz2IXGU6EMLdeufhADaGPLft9xjwdN1ts276iXQiaChKPA2CK - /CBpuKcnU3LhU8JEi7u/db7J4lJlh6evjdKVKlMuhPcljnIKAiGcWln3zwYrFCeL - cN0aTOt4xnQpm8OqHawJ18y0WhsWT+hf1DeBDWvdfRuAPlfuVtl3KkrNYn1yqCgQ - lT6v/WyzptJhSR1jxdR7XLOhDGTZUzlHXh2bM7sav2n1+sLsuCkzTJqWZ8K7k7cI - XK354CNpCdyRYUAUvr4rORIAUmcIFjaR3J4y/Dh2JIyDToOHg7vjpCtNnNoS+ON2 - HwIDAQAB - -----END PUBLIC KEY-----` - system := ssas.System{GroupID: groupID, ClientID: clientID, ClientName: clientName, APIScope: scope} - if err := db.Save(&system).Error; err != nil { - ssas.Logger.Warn(err) - } - - encryptionKey := ssas.EncryptionKey{ - Body: pem, - SystemID: system.ID, - } - if err := db.Save(&encryptionKey).Error; err != nil { - ssas.Logger.Warn(err) - } - - secret := ssas.Secret{ - Hash: hash, - SystemID: system.ID, - } - if err := db.Save(&secret).Error; err != nil { - ssas.Logger.Warn(err) - } -} - -func resetSecret(clientID string) { - var ( - err error - s ssas.System - c ssas.Credentials - ) - if s, err = ssas.GetSystemByClientID(clientID); err != nil { - ssas.Logger.Warn(err) - } - ssas.OperationCalled(ssas.Event{Op: "ResetSecret", TrackingID: cliTrackingID(), Help: "calling from main.resetSecret()"}) - if c, err = s.ResetSecret(clientID); err != nil { - ssas.Logger.Warn(err) - } else { - _, _ = fmt.Fprintf(output, "%s\n", c.ClientSecret) - } -} - -func newAdminSystem(name string) { - var ( - err error - pk string - c ssas.Credentials - u uint64 - ) - if pk, err = ssas.GeneratePublicKey(2048); err != nil { - ssas.Logger.Errorf("no public key; %s", err) - return - } - - trackingID := cliTrackingID() - ssas.OperationCalled(ssas.Event{Op: "RegisterSystem", TrackingID: trackingID, Help: "calling from main.newAdminSystem()"}) - if c, err = ssas.RegisterSystem(name, "admin", "bcda-api", pk, trackingID); err != nil { - ssas.Logger.Error(err) - return - } - - if u, err = strconv.ParseUint(c.SystemID, 10, 64); err != nil { - ssas.Logger.Errorf("invalid systemID %d; %s", u, err) - return - } - - db := ssas.GetGORMDbConnection() - defer db.Close() - if err = db.Model(&ssas.System{}).Where("id = ?", uint(u)).Update("api_scope", "bcda-admin").Error; err != nil { - ssas.Logger.Warnf("bcda-admin scope not set for new system %s", c.SystemID) - } else { - _, _ = fmt.Fprintf(output, "%s\n", c.ClientID) - } -} - -func cliTrackingID() string { - return fmt.Sprintf("cli-command-%d", time.Now().Unix()) -} diff --git a/ssas/service/main/main_test.go b/ssas/service/main/main_test.go deleted file mode 100644 index 65bab13bd..000000000 --- a/ssas/service/main/main_test.go +++ /dev/null @@ -1,122 +0,0 @@ -package main - -import ( - "bytes" - "io" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/stretchr/testify/suite" - - "github.com/CMSgov/bcda-app/ssas" -) - -type MainTestSuite struct { - suite.Suite -} - -func (s *MainTestSuite) SetupSuite() { - ssas.InitializeGroupModels() - ssas.InitializeSystemModels() -} - -func (s *MainTestSuite) TestResetSecret() { - fixtureClientID := "0c527d2e-2e8a-4808-b11d-0fa06baf8254" - output := captureOutput(func() { resetSecret(fixtureClientID) }) - assert.NotEqual(s.T(), "", output) -} - -func (s *MainTestSuite) TestResetCredentialsBadClientID() { - badClientID := "This client does not exist" - output := captureOutput(func() {resetSecret(badClientID)}) - assert.Equal(s.T(), "", output) -} - -func (s *MainTestSuite) TestMainResetCredentials() { - doResetSecret = true - clientID = "0c527d2e-2e8a-4808-b11d-0fa06baf8254" - - output := captureOutput(func() {main()}) - assert.NotEqual(s.T(), output, "") - - doResetSecret = false - clientID = "" -} - - -func (s *MainTestSuite) TestNewAdminSystem() { - output := captureOutput(func() { newAdminSystem("Main Test System") }) - assert.NotEqual(s.T(), "", output) -} - -func (s *MainTestSuite) TestMainLog() { - var str bytes.Buffer - ssas.Logger.SetOutput(&str) - main() - output := str.String() - assert.Contains(s.T(), output, "Home of") -} - -func (s *MainTestSuite) TestFixtureData() { - q := `select distinct g.id as gid, g.group_id, s.id as sid, s.client_name, ek.id as ekid, s.id as scrtid - from groups g - join systems s on g.group_id = s.group_id - join encryption_keys ek on ek.system_id = s.id - join secrets sc on sc.system_id = s.id - where g.group_id in ('admin', '0c527d2e-2e8a-4808-b11d-0fa06baf8254');` - // if you run the query above against the db, you will see a result like this: - // gid | group_id | sid | client_name | ekid | scrtid - // -----+--------------------------------------+-----+----------------+------+-------- - // 15 | admin | 13 | BCDA API Admin | 13 | 13 - // 16 | 0c527d2e-2e8a-4808-b11d-0fa06baf8254 | 14 | ACO Dev | 14 | 14 - // (2 rows) - // - // only complete fixture data will be included in the result - - type result struct { - GID uint `json:"gid"` - GroupID string `json:"group_id"` - SID uint `json:"sid"` - ClientName string `json:"client_name"` - EKID uint `json:"ekid"` - ScrtID uint `json:"scrtid"` - } - rows, err := ssas.GetGORMDbConnection().Raw(q).Rows() - require.Nil(s.T(), err, "error checking fixture data") - defer rows.Close() - - foundAdmin := false - foundConsumer := false - for rows.Next() { - var r result - err := rows.Scan(&r.GID, &r.GroupID, &r.SID, &r.ClientName, &r.EKID, &r.ScrtID) - require.Nil(s.T(), err, "error scanning data") - switch r.GroupID { - case "admin": - foundAdmin = true - case "0c527d2e-2e8a-4808-b11d-0fa06baf8254": - foundConsumer = true - } - } - - assert.True(s.T(), foundAdmin) - assert.True(s.T(), foundConsumer) -} - -func TestMainTestSuite(t *testing.T) { - suite.Run(t, new(MainTestSuite)) -} - -func captureOutput(f func()) string { - var ( - buf bytes.Buffer - outOrig io.Writer - ) - - outOrig = output - output = &buf - f() - output = outOrig - return buf.String() -} diff --git a/ssas/service/public/api.go b/ssas/service/public/api.go deleted file mode 100644 index 44d66ecb9..000000000 --- a/ssas/service/public/api.go +++ /dev/null @@ -1,535 +0,0 @@ -/* - Package public (ssas/service/api/public) contains API functions, middleware, and a router designed to: - 1. Be accessible to the public - 2. Offer system self-registration and self-management -*/ -package public - -import ( - "encoding/json" - "errors" - "fmt" - "io/ioutil" - "net/http" - "strconv" - - "github.com/go-chi/render" - "github.com/pborman/uuid" - - "github.com/CMSgov/bcda-app/ssas" - "github.com/CMSgov/bcda-app/ssas/service" -) - -type Key struct { - E string `json:"e"` - N string `json:"n"` - KTY string `json:"kty"` - Use string `json:"use,omitempty"` -} - -type JWKS struct { - Keys []Key `json:"keys"` -} - -type RegistrationRequest struct { - ClientID string `json:"client_id"` - ClientName string `json:"client_name"` - Scope string `json:"scope,omitempty"` - JSONWebKeys JWKS `json:"jwks"` -} - -type ResetRequest struct { - ClientID string `json:"client_id"` -} - -type MFARequest struct { - LoginID string `json:"login_id"` - FactorType string `json:"factor_type"` - Passcode *string `json:"passcode,omitempty"` - Transaction *string `json:"transaction,omitempty"` -} - -type PasswordRequest struct { - LoginID string `json:"login_id"` - Password string `json:"password"` -} - -/* - VerifyPassword is mounted at POST /authn and responds with the account status for a verified username/password - combination. -*/ -func VerifyPassword(w http.ResponseWriter, r *http.Request) { - var ( - err error - trackingID string - passReq PasswordRequest - ) - - setHeaders(w) - - bodyStr, err := ioutil.ReadAll(r.Body) - if err != nil { - jsonError(w, "invalid_client_metadata", "Request body cannot be read") - return - } - - err = json.Unmarshal(bodyStr, &passReq) - if err != nil { - service.LogEntrySetField(r, "bodyStr", "") - jsonError(w, "invalid_client_metadata", "Request body cannot be parsed") - return - } - - trackingID = uuid.NewRandom().String() - event := ssas.Event{Op: "VerifyOktaPassword", TrackingID: trackingID, Help: "calling from public.VerifyPassword()"} - ssas.OperationCalled(event) - passwordResponse, oktaId, err := GetProvider().VerifyPassword(passReq.LoginID, passReq.Password, trackingID) - if err != nil { - jsonError(w, "invalid_client_metadata", err.Error()) - return - } - - if passwordResponse.Success { - _, passwordResponse.Token, err = MintMFAToken(oktaId) - } - if err != nil { - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - event.Help = "failure generating JSON: " + err.Error() - ssas.OperationFailed(event) - return - } - - body, err := json.Marshal(passwordResponse) - if err != nil { - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - event.Help = "failure generating JSON: " + err.Error() - ssas.OperationFailed(event) - return - } - - _, err = w.Write(body) - if err != nil { - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - event.Help = "failure writing response body: " + err.Error() - ssas.OperationFailed(event) - return - } -} - -/* - RequestMultifactorChallenge is mounted at POST /authn/challenge and sends a multi-factor authentication request - using the specified factor. - - Valid factor types include: - "Google TOTP" (Google Authenticator) - "Okta TOTP" (Okta Verify app time-based token) - "Push" (Okta Verify app push) - "SMS" - "Call" - "Email" - - In the case of the Push factor, a transaction ID is returned to use with the polling endpoint: - POST /authn/verify/transactions/{transaction_id} -*/ -func RequestMultifactorChallenge(w http.ResponseWriter, r *http.Request) { - var ( - err error - trackingID string - mfaReq MFARequest - ) - - setHeaders(w) - - bodyStr, err := ioutil.ReadAll(r.Body) - if err != nil { - jsonError(w, "invalid_client_metadata", "Request body cannot be read") - return - } - - err = json.Unmarshal(bodyStr, &mfaReq) - if err != nil { - service.LogEntrySetField(r, "bodyStr", bodyStr) - jsonError(w, "invalid_client_metadata", "Request body cannot be parsed") - return - } - - trackingID = uuid.NewRandom().String() - event := ssas.Event{Op: "RequestOktaFactorChallenge", TrackingID: trackingID, Help: "calling from public.RequestMultifactorChallenge()"} - ssas.OperationCalled(event) - factorResponse, err := GetProvider().RequestFactorChallenge(mfaReq.LoginID, mfaReq.FactorType, trackingID) - if err != nil { - jsonError(w, "invalid_client_metadata", err.Error()) - return - } - - body, err := json.Marshal(factorResponse) - if err != nil { - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - event.Help = "failure generating JSON: " + err.Error() - ssas.OperationFailed(event) - return - } - - _, err = w.Write(body) - if err != nil { - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - event.Help = "failure writing response body: " + err.Error() - ssas.OperationFailed(event) - return - } -} - -/* - VerifyMultifactorResponse is mounted at POST /authn/verify and tests a multi-factor authentication passcode - for the specified factor, and should be used for all factor types except Push. -*/ -func VerifyMultifactorResponse(w http.ResponseWriter, r *http.Request) { - var ( - err error - trackingID string - mfaReq MFARequest - body []byte - ts string - groupIDs []string - ) - - setHeaders(w) - - bodyStr, err := ioutil.ReadAll(r.Body) - if err != nil { - jsonError(w, "invalid_client_metadata", "Request body cannot be read") - return - } - - err = json.Unmarshal(bodyStr, &mfaReq) - if err != nil { - service.LogEntrySetField(r, "bodyStr", bodyStr) - jsonError(w, "invalid_client_metadata", "Request body cannot be parsed") - return - } - - if mfaReq.Passcode == nil { - service.LogEntrySetField(r, "bodyStr", bodyStr) - jsonError(w, "invalid_client_metadata", "Request body missing passcode") - return - } - - trackingID = uuid.NewRandom().String() - event := ssas.Event{Op: "VerifyOktaFactorResponse", TrackingID: trackingID, Help: "calling from public.VerifyMultifactorResponse()"} - ssas.OperationCalled(event) - success, oktaID, groupIDs := GetProvider().VerifyFactorChallenge(mfaReq.LoginID, mfaReq.FactorType, *mfaReq.Passcode, trackingID) - - if !success { - event.Help = "passcode rejected" - ssas.OperationFailed(event) - - _, err = w.Write([]byte(`{"factor_result":"failure"}`)) - if err != nil { - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - event.Help = "failure writing response body: " + err.Error() - ssas.OperationFailed(event) - return - } - } - - if empty(groupIDs) { - http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) - event.Help = "no authorized groups" - ssas.OperationFailed(event) - return - } - - gIdsBytes, err := json.Marshal(groupIDs) - if err != nil { - http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) - event.Help = "no authorized groups: " + err.Error() - ssas.OperationFailed(event) - return - } - - event.Help = "passcode accepted" - ssas.OperationSucceeded(event) - if _, ts, err = MintRegistrationToken(oktaID, groupIDs); err != nil { - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - event.Help = "failure creating registration token: " + err.Error() - ssas.OperationFailed(event) - return - } - body = []byte(fmt.Sprintf(`{"factor_result":"success","registration_token":"%s", "available_groups":%s}`, ts, string(gIdsBytes))) - _, err = w.Write(body) - if err != nil { - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - event.Help = "failure writing response body: " + err.Error() - ssas.OperationFailed(event) - return - } -} - -/* - ResetSecret is mounted at POST /reset and allows the authenticated manager of a system to rotate their secret. -*/ -func ResetSecret(w http.ResponseWriter, r *http.Request) { - var ( - rd ssas.AuthRegData - err error - trackingID string - req ResetRequest - sys ssas.System - bodyStr []byte - credentials ssas.Credentials - event ssas.Event - ) - setHeaders(w) - - if rd, err = readRegData(r); err != nil || rd.GroupID == "" { - service.GetLogEntry(r).Println("missing or invalid GroupID") - http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) - return - } - - if bodyStr, err = ioutil.ReadAll(r.Body); err != nil { - jsonError(w, "invalid_client_metadata", "Request body cannot be read") - return - } - - if err = json.Unmarshal(bodyStr, &req); err != nil { - service.LogEntrySetField(r, "bodyStr", bodyStr) - jsonError(w, "invalid_client_metadata", "Request body cannot be parsed") - return - } - - if sys, err = ssas.GetSystemByClientID(req.ClientID); err != nil { - jsonError(w, "invalid_client_metadata", "Client not found") - return - } - - if !contains(rd.AllowedGroupIDs, rd.GroupID) || sys.GroupID != rd.GroupID { - jsonError(w, "invalid_client_metadata", "Invalid group") - return - } - - event = ssas.Event{Op: "ResetSecret", TrackingID: uuid.NewRandom().String(), Help: "calling from public.ResetSecret()"} - ssas.OperationCalled(event) - if credentials, err = sys.ResetSecret(trackingID); err != nil { - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return - } - - body := []byte(fmt.Sprintf(`{"client_id": "%s","client_secret":"%s","client_secret_expires_at":"%d","client_name":"%s"}`, - credentials.ClientID, credentials.ClientSecret, credentials.ExpiresAt.Unix(), credentials.ClientName)) - if _, err = w.Write(body); err != nil { - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - event.Help = "failure writing response body: " + err.Error() - ssas.OperationFailed(event) - return - } -} - -/* - RegisterSystem is mounted at POST /auth/register and allows for self-registration. It requires that a - registration token containing one or more group ids be presented and parsed by middleware, with the - GroupID[s] placed in the context key "rd". -*/ -func RegisterSystem(w http.ResponseWriter, r *http.Request) { - var ( - rd ssas.AuthRegData - err error - reg RegistrationRequest - publicKeyBytes []byte - trackingID string - ) - - setHeaders(w) - - if rd, err = readRegData(r); err != nil || rd.GroupID == "" { - service.GetLogEntry(r).Println("missing or invalid GroupID") - // Specified in RFC 7592 https://tools.ietf.org/html/rfc7592#page-6 - http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) - return - } - - bodyStr, err := ioutil.ReadAll(r.Body) - if err != nil { - // Response types and format specified in RFC 7591 https://tools.ietf.org/html/rfc7591#section-3.2.2 - jsonError(w, "invalid_client_metadata", "Request body cannot be read") - return - } - - err = json.Unmarshal(bodyStr, ®) - if err != nil { - service.LogEntrySetField(r, "bodyStr", bodyStr) - jsonError(w, "invalid_client_metadata", "Request body cannot be parsed") - return - } - - if reg.JSONWebKeys.Keys == nil || len(reg.JSONWebKeys.Keys) > 1 { - jsonError(w, "invalid_client_metadata", "Exactly one JWK must be presented") - return - } - - publicKeyBytes, err = json.Marshal(reg.JSONWebKeys.Keys[0]) - if err != nil { - jsonError(w, "invalid_client_metadata", "Unable to read JWK") - return - } - - publicKeyPEM, err := ssas.ConvertJWKToPEM(string(publicKeyBytes)) - if err != nil { - jsonError(w, "invalid_client_metadata", "Unable to process JWK") - return - } - - // Log the source of the call for this operation. Remaining logging will be in ssas.RegisterSystem() below. - trackingID = uuid.NewRandom().String() - event := ssas.Event{Op: "RegisterClient", TrackingID: trackingID, Help: "calling from public.RegisterSystem()"} - ssas.OperationCalled(event) - credentials, err := ssas.RegisterSystem(reg.ClientName, rd.GroupID, reg.Scope, publicKeyPEM, trackingID) - if err != nil { - jsonError(w, "invalid_client_metadata", err.Error()) - return - } - - body := []byte(fmt.Sprintf(`{"client_id": "%s","client_secret":"%s","client_secret_expires_at":"%d","client_name":"%s"}`, - credentials.ClientID, credentials.ClientSecret, credentials.ExpiresAt.Unix(), credentials.ClientName)) - // https://tools.ietf.org/html/rfc7591#section-3.2 dictates 201, not 200 - w.WriteHeader(http.StatusCreated) - _, err = w.Write(body) - if err != nil { - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - event.Help = "failure writing response body: " + err.Error() - ssas.OperationFailed(event) - return - } -} - -func readRegData(r *http.Request) (data ssas.AuthRegData, err error) { - var ok bool - data, ok = r.Context().Value("rd").(ssas.AuthRegData) - if !ok { - err = errors.New("no registration data in context") - } - return -} - -func jsonError(w http.ResponseWriter, error string, description string) { - ssas.Logger.Printf("%s; %s", description, error) - w.WriteHeader(http.StatusBadRequest) - body := []byte(fmt.Sprintf(`{"error":"%s","error_description":"%s"}`, error, description)) - _, err := w.Write(body) - if err != nil { - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - } -} - -func setHeaders(w http.ResponseWriter) { - w.Header().Set("Content-Type", "application/json") - w.Header().Set("Cache-Control", "no-store") - w.Header().Set("Pragma", "no-cache") -} - -type TokenResponse struct { - AccessToken string `json:"access_token"` - TokenType string `json:"token_type"` - ExpiresIn string `json:"expires_in"` -} - -func token(w http.ResponseWriter, r *http.Request) { - clientID, secret, ok := r.BasicAuth() - if !ok { - http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) - return - } - - system, err := ssas.GetSystemByClientID(clientID) - if err != nil { - jsonError(w, http.StatusText(http.StatusUnauthorized), "invalid client id") - return - } - - savedSecret, err := system.GetSecret() - if err != nil || !ssas.Hash(savedSecret).IsHashOf(secret) { - jsonError(w, http.StatusText(http.StatusUnauthorized), "invalid client secret") - return - } - - trackingID := uuid.NewRandom().String() - data, err := ssas.XDataFor(system) - ssas.Logger.Infof("public.api.token: XDataFor(%d) returned '%s'", system.ID, data) - if err != nil { - jsonError(w, http.StatusText(http.StatusUnauthorized), "no group for system") - return - } - - event := ssas.Event{Op: "Token", TrackingID: trackingID, Help: "calling from public.token()"} - ssas.OperationCalled(event) - token, ts, err := MintAccessToken(fmt.Sprintf("%d", system.ID), system.ClientID, data) - if err != nil { - event.Help = "failure minting token: " + err.Error() - ssas.OperationFailed(event) - http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) - return - } - - // https://tools.ietf.org/html/rfc6749#section-5.1 - // expires_in is duration in seconds - expiresIn := token.Claims.(*service.CommonClaims).ExpiresAt - token.Claims.(*service.CommonClaims).IssuedAt - m := TokenResponse{AccessToken: ts, TokenType: "bearer", ExpiresIn: strconv.FormatInt(expiresIn, 10)} - w.Header().Set("Content-Type", "application/json") - w.Header().Set("Cache-Control", "no-store") - w.Header().Set("Pragma", "no-cache") - ssas.AccessTokenIssued(event) - ssas.OperationSucceeded(event) - render.JSON(w, r, m) -} - -func introspect(w http.ResponseWriter, r *http.Request) { - clientID, secret, ok := r.BasicAuth() - - if !ok { - jsonError(w, http.StatusText(http.StatusUnauthorized), "invalid auth header") - return - } - - if clientID == "" || secret == "" { - msg := fmt.Sprint("empty value in clientID and/or secret") - jsonError(w, http.StatusText(http.StatusUnauthorized), msg) - return - } - - system, err := ssas.GetSystemByClientID(clientID) - if err != nil { - jsonError(w, http.StatusText(http.StatusUnauthorized), fmt.Sprintf("invalid client id; %s", err)) - return - } - - savedSecret, err := system.GetSecret() - if err != nil { - jsonError(w, http.StatusText(http.StatusUnauthorized), fmt.Sprintf("can't get secret; %s", err)) - return - } - - if !ssas.Hash(savedSecret).IsHashOf(secret) { - jsonError(w, http.StatusText(http.StatusUnauthorized), "invalid client secret") - return - } - - defer r.Body.Close() - - var reqV map[string]string - if err = json.NewDecoder(r.Body).Decode(&reqV); err != nil { - jsonError(w, http.StatusText(http.StatusBadRequest), "invalid body") - return - } - var answer = make(map[string]bool) - answer["active"] = true - if err = tokenValidity(reqV["token"], "AccessToken"); err != nil { - ssas.Logger.Infof("token failed tokenValidity") - answer["active"] = false - } - - w.Header().Set("Content-Type", "application/json") - w.Header().Set("Cache-Control", "no-store") - w.Header().Set("Pragma", "no-cache") - - render.JSON(w, r, answer) -} diff --git a/ssas/service/public/api_test.go b/ssas/service/public/api_test.go deleted file mode 100644 index eec5e9d08..000000000 --- a/ssas/service/public/api_test.go +++ /dev/null @@ -1,270 +0,0 @@ -package public - -import ( - "context" - "encoding/json" - "fmt" - "net/http" - "net/http/httptest" - "strings" - "testing" - - "github.com/go-chi/chi" - "github.com/pborman/uuid" - "github.com/stretchr/testify/require" - - "github.com/CMSgov/bcda-app/ssas" - "github.com/CMSgov/bcda-app/ssas/service" - - "github.com/jinzhu/gorm" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/suite" -) - -type APITestSuite struct { - suite.Suite - rr *httptest.ResponseRecorder - db *gorm.DB -} - -func (s *APITestSuite) SetupSuite() { - ssas.InitializeGroupModels() - ssas.InitializeSystemModels() - s.db = ssas.GetGORMDbConnection() - _ = Server() - service.StartBlacklist() -} - -func (s *APITestSuite) SetupTest() { - s.db = ssas.GetGORMDbConnection() - s.rr = httptest.NewRecorder() -} - -func (s *APITestSuite) TearDownSuite() { - ssas.Close(s.db) -} - -func (s *APITestSuite) TestAuthRegisterEmpty() { - regBody := strings.NewReader("") - - req, err := http.NewRequest("GET", "/auth/register", regBody) - assert.Nil(s.T(), err) - - req = addRegDataContext(req, "T12123", []string{"T12123"}) - http.HandlerFunc(RegisterSystem).ServeHTTP(s.rr, req) - assert.Equal(s.T(), http.StatusBadRequest, s.rr.Code) -} - -func (s *APITestSuite) TestAuthRegisterBadJSON() { - regBody := strings.NewReader("asdflkjghjkl") - - req, err := http.NewRequest("GET", "/auth/register", regBody) - assert.Nil(s.T(), err) - - req = addRegDataContext(req, "T12123", []string{"T12123"}) - http.HandlerFunc(RegisterSystem).ServeHTTP(s.rr, req) - assert.Equal(s.T(), http.StatusBadRequest, s.rr.Code) -} - -func (s *APITestSuite) TestAuthRegisterSuccess() { - groupID := "T12123" - group := ssas.Group{GroupID: groupID} - err := s.db.Create(&group).Error - if err != nil { - s.FailNow(err.Error()) - } - - regBody := strings.NewReader(fmt.Sprintf(`{"client_id":"my_client_id","client_name":"my_client_name","scope":"%s","jwks":{"keys":[{"e":"AAEAAQ","n":"ok6rvXu95337IxsDXrKzlIqw_I_zPDG8JyEw2CTOtNMoDi1QzpXQVMGj2snNEmvNYaCTmFf51I-EDgeFLLexr40jzBXlg72quV4aw4yiNuxkigW0gMA92OmaT2jMRIdDZM8mVokoxyPfLub2YnXHFq0XuUUgkX_TlutVhgGbyPN0M12teYZtMYo2AUzIRggONhHvnibHP0CPWDjCwSfp3On1Recn4DPxbn3DuGslF2myalmCtkujNcrhHLhwYPP-yZFb8e0XSNTcQvXaQxAqmnWH6NXcOtaeWMQe43PNTAyNinhndgI8ozG3Hz-1NzHssDH_yk6UYFSszhDbWAzyqw","kty":"RSA"}]}}`, - ssas.DefaultScope)) - - req, err := http.NewRequest("GET", "/auth/register", regBody) - assert.Nil(s.T(), err) - - req = addRegDataContext(req, "T12123", []string{"T12123"}) - http.HandlerFunc(RegisterSystem).ServeHTTP(s.rr, req) - assert.Equal(s.T(), http.StatusCreated, s.rr.Code) - - j := map[string]string{} - err = json.Unmarshal(s.rr.Body.Bytes(), &j) - assert.Nil(s.T(), err) - assert.Equal(s.T(), "my_client_name", j["client_name"]) - - err = ssas.CleanDatabase(group) - assert.Nil(s.T(), err) -} - -func (s *APITestSuite) TestResetSecretNoSystem() { - groupID := "T23234" - group := ssas.Group{GroupID: groupID} - if err := s.db.Create(&group).Error; err != nil { - s.FailNow("unable to create group: " + err.Error()) - } - - body := strings.NewReader(`{"client_id":"abcd1234"}`) - req, err := http.NewRequest("PUT", "/reset", body) - assert.Nil(s.T(), err) - - req = addRegDataContext(req, groupID, []string{groupID}) - http.HandlerFunc(ResetSecret).ServeHTTP(s.rr, req) - assert.Equal(s.T(), http.StatusBadRequest, s.rr.Code) - assert.Contains(s.T(), s.rr.Body.String(), "not found") - - err = ssas.CleanDatabase(group) - assert.Nil(s.T(), err) -} - -func (s *APITestSuite) TestResetSecretEmpty() { - groupID := "T23234" - - body := strings.NewReader("") - req, err := http.NewRequest("PUT", "/reset", body) - assert.Nil(s.T(), err) - - req = addRegDataContext(req, groupID, []string{groupID}) - http.HandlerFunc(ResetSecret).ServeHTTP(s.rr, req) - assert.Equal(s.T(), http.StatusBadRequest, s.rr.Code) -} - -func (s *APITestSuite) TestResetSecretBadJSON() { - groupID := "T23234" - - body := strings.NewReader(`abcdefg`) - req, err := http.NewRequest("PUT", "/reset", body) - assert.Nil(s.T(), err) - - req = addRegDataContext(req, groupID, []string{groupID}) - http.HandlerFunc(ResetSecret).ServeHTTP(s.rr, req) - assert.Equal(s.T(), http.StatusBadRequest, s.rr.Code) -} - -func (s *APITestSuite) TestResetSecretSuccess() { - groupID := "T23234" - group := ssas.Group{GroupID: groupID} - if err := s.db.Create(&group).Error; err != nil { - s.FailNow("unable to create group: " + err.Error()) - } - system := ssas.System{GroupID: group.GroupID, ClientID: "abcd1234"} - if err := s.db.Create(&system).Error; err != nil { - s.FailNow("unable to create system: " + err.Error()) - } - - hashedSecret := ssas.Hash("no_secret_at_all") - secret := ssas.Secret{Hash: hashedSecret.String(), SystemID: system.ID} - if err := s.db.Create(&secret).Error; err != nil { - s.FailNow("unable to create secret: " + err.Error()) - } - - body := strings.NewReader(`{"client_id":"abcd1234"}`) - req, err := http.NewRequest("PUT", "/reset", body) - assert.Nil(s.T(), err) - - req = addRegDataContext(req, groupID, []string{groupID}) - http.HandlerFunc(ResetSecret).ServeHTTP(s.rr, req) - assert.Equal(s.T(), http.StatusOK, s.rr.Code) - - newSecret := ssas.Secret{} - if err = s.db.Where("system_id = ?", system.ID).First(&newSecret).Error; err != nil { - s.FailNow("unable to find secret: " + err.Error()) - } - hash := ssas.Hash(newSecret.Hash) - - j := map[string]string{} - err = json.Unmarshal(s.rr.Body.Bytes(), &j) - assert.Nil(s.T(), err) - assert.True(s.T(), hash.IsHashOf(j["client_secret"])) - - err = ssas.CleanDatabase(group) - assert.Nil(s.T(), err) -} - -func addRegDataContext(req *http.Request, groupID string, groupIDs []string) *http.Request { - rctx := chi.NewRouteContext() - req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx)) - rd := ssas.AuthRegData{GroupID: groupID, AllowedGroupIDs: groupIDs} - req = req.WithContext(context.WithValue(req.Context(), "rd", rd)) - return req -} - -func (s *APITestSuite) TestTokenSuccess() { - groupID := ssas.RandomHexID()[0:4] - group := ssas.Group{GroupID: groupID, XData: "x_data"} - err := s.db.Create(&group).Error - require.Nil(s.T(), err) - - _, pubKey, err := ssas.GenerateTestKeys(2048) - require.Nil(s.T(), err) - - pemString, err := ssas.ConvertPublicKeyToPEMString(&pubKey) - require.Nil(s.T(), err) - - creds, err := ssas.RegisterSystem("Token Test", groupID, ssas.DefaultScope, pemString, uuid.NewRandom().String()) - assert.Nil(s.T(), err) - assert.Equal(s.T(), "Token Test", creds.ClientName) - assert.NotNil(s.T(), creds.ClientSecret) - - // now for the actual test - req := httptest.NewRequest("POST", "/token", nil) - req.SetBasicAuth(creds.ClientID, creds.ClientSecret) - req.Header.Add("Accept", "application/json") - handler := http.HandlerFunc(token) - handler.ServeHTTP(s.rr, req) - assert.Equal(s.T(), http.StatusOK, s.rr.Code) - t := TokenResponse{} - assert.NoError(s.T(), json.NewDecoder(s.rr.Body).Decode(&t)) - assert.NotEmpty(s.T(), t) - assert.NotEmpty(s.T(), t.AccessToken) - - err = ssas.CleanDatabase(group) - assert.Nil(s.T(), err) -} - -func (s *APITestSuite) TestIntrospectSuccess() { - groupID := ssas.RandomHexID()[0:4] - - group := ssas.Group{GroupID: groupID, XData: "x_data"} - err := s.db.Create(&group).Error - require.Nil(s.T(), err) - - _, pubKey, err := ssas.GenerateTestKeys(2048) - require.Nil(s.T(), err) - pemString, err := ssas.ConvertPublicKeyToPEMString(&pubKey) - require.Nil(s.T(), err) - - creds, err := ssas.RegisterSystem("Introspect Test", groupID, ssas.DefaultScope, pemString, uuid.NewRandom().String()) - assert.Nil(s.T(), err) - assert.Equal(s.T(), "Introspect Test", creds.ClientName) - assert.NotNil(s.T(), creds.ClientSecret) - - req := httptest.NewRequest("POST", "/token", nil) - req.SetBasicAuth(creds.ClientID, creds.ClientSecret) - req.Header.Add("Accept", "application/json") - handler := http.HandlerFunc(token) - handler.ServeHTTP(s.rr, req) - assert.Equal(s.T(), http.StatusOK, s.rr.Code) - t := TokenResponse{} - assert.NoError(s.T(), json.NewDecoder(s.rr.Body).Decode(&t)) - assert.NotEmpty(s.T(), t) - assert.NotEmpty(s.T(), t.AccessToken) - - // the actual test - body := strings.NewReader(fmt.Sprintf(`{"token":"%s"}`, t.AccessToken)) - req = httptest.NewRequest("POST", "/introspect", body) - req.SetBasicAuth(creds.ClientID, creds.ClientSecret) - req.Header.Add("Content-Type", "application/json") - req.Header.Add("Accept", "application/json") - handler = http.HandlerFunc(introspect) - handler.ServeHTTP(s.rr, req) - assert.Equal(s.T(), http.StatusOK, s.rr.Code) - - var v map[string]bool - assert.NoError(s.T(), json.NewDecoder(s.rr.Body).Decode(&v)) - assert.NotEmpty(s.T(), v) - assert.True(s.T(), v["active"]) - - err = ssas.CleanDatabase(group) - assert.Nil(s.T(), err) -} - -func TestAPITestSuite(t *testing.T) { - suite.Run(t, new(APITestSuite)) -} diff --git a/ssas/service/public/mfaprovider.go b/ssas/service/public/mfaprovider.go deleted file mode 100644 index 8b57b0645..000000000 --- a/ssas/service/public/mfaprovider.go +++ /dev/null @@ -1,116 +0,0 @@ -package public - -import ( - "fmt" - "os" - "strings" - "time" - - "github.com/CMSgov/bcda-app/ssas" - "github.com/CMSgov/bcda-app/ssas/okta" -) - -const ( - Mock = "mock" - Live = "live" -) - -var providerName = Mock - -func init() { - SetProvider(strings.ToLower(os.Getenv(`SSAS_MFA_PROVIDER`))) -} - -func SetProvider(name string) { - n := strings.ToLower(name) - if name != "" { - switch n { - case Live: - providerName = n - case Mock: - providerName = n - default: - providerEvent := ssas.Event{Op: "SetProvider", Help: fmt.Sprintf(`Unknown providerName %s; using %s`, n, providerName)} - ssas.ServiceStarted(providerEvent) - } - } - providerEvent := ssas.Event{Op: "SetProvider", Help: fmt.Sprintf(`MFA is made possible by %s`, providerName)} - ssas.ServiceStarted(providerEvent) -} - -func GetProviderName() string { - return providerName -} - -func GetProvider() MFAProvider { - switch providerName { - case Live: - return NewOktaMFA(okta.Client()) - case Mock: - fallthrough - default: - return &MockMFAPlugin{} - } -} - -// PasswordReturn defines the return type of VerifyPassword -type PasswordReturn struct { - Success bool `json:"success"` - Message string `json:"message"` - Token string `json:"token,omitempty"` -} - -// FactorReturn defines the return type of RequestFactorChallenge -type FactorReturn struct { - Action string `json:"action"` - Transaction *Transaction `json:"transaction,omitempty"` -} - -// Transaction defines the extra information provided in a response to RequestFactorChallenge for Push factors -type Transaction struct { - TransactionID string `json:"transaction_id"` - ExpiresAt time.Time `json:"expires_at"` -} - -func ValidFactorType(factorType string) bool { - switch strings.ToLower(factorType) { - case "google totp": - fallthrough - case "okta totp": - fallthrough - case "push": - fallthrough - case "sms": - fallthrough - case "call": - fallthrough - case "email": - return true - default: - return false - } -} - -// Provider defines operations performed through an Okta MFA provider. This indirection allows for a mock provider -// to use during CI/CD integration testing -type MFAProvider interface { - - // VerifyPassword checks username/password validity, and returns information about the status of the account. Most - // importantly for the MFA workflow, it indicates whether a successfully verified account is cleared to continue - // MFA authentication, or whether a condition exists such as an expired password or no actively enrolled - // MFA factors. - VerifyPassword(userIdentifier string, password string, trackingId string) (*PasswordReturn, string, error) - - // RequestFactorChallenge sends an MFA challenge request for the MFA factor type registered to the specified user, - // if both user and factor exist. For instance, for the SMS factor type, an SMS message would be sent with a - // passcode. Responses for successful and failed attempts should not vary. - RequestFactorChallenge(userIdentifier string, factorType string, trackingId string) (*FactorReturn, error) - - // VerifyFactorChallenge tests an MFA passcode for validity. This function should be used for all factor types - // except Push. - VerifyFactorChallenge(userIdentifier string, factorType string, passcode string, trackingId string) (bool, string, []string) - - // VerifyFactorTransaction reports the status of a Push factor's transaction. Possible non-error states include success, - // rejection, waiting, and timeout. - VerifyFactorTransaction(userIdentifier string, factorType string, transactionId string, trackingId string) (string, error) -} diff --git a/ssas/service/public/mfaprovider_test.go b/ssas/service/public/mfaprovider_test.go deleted file mode 100644 index e27b2002b..000000000 --- a/ssas/service/public/mfaprovider_test.go +++ /dev/null @@ -1,81 +0,0 @@ -package public - -import ( - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/suite" - "testing" -) - -var origProvider string - -type MFAProviderTestSuite struct { - suite.Suite -} - -func (s *MFAProviderTestSuite) SetupTest() { - origProvider = providerName -} - -func (s *MFAProviderTestSuite) TearDownTest() { - providerName = origProvider -} - -func (s *MFAProviderTestSuite) TestSetProvider() { - SetProvider("") - assert.Equal(s.T(), Mock, providerName) - - SetProvider("invalid_provider") - assert.Equal(s.T(), Mock, providerName) - - SetProvider("Mock") - assert.Equal(s.T(), Mock, providerName) - - SetProvider("mock") - assert.Equal(s.T(), Mock, providerName) - - SetProvider("Live") - assert.Equal(s.T(), Live, providerName) - - SetProvider("live") - assert.Equal(s.T(), Live, providerName) -} - -func (s *MFAProviderTestSuite) TestGetProviderName() { - providerName = "live" - assert.Equal(s.T(), Live, GetProviderName()) - - providerName = "mock" - assert.Equal(s.T(), Mock, GetProviderName()) -} - -func (s *MFAProviderTestSuite) TestGetProvider() { - providerName = "live" - assert.NotEqual(s.T(), &MockMFAPlugin{}, GetProvider()) - - providerName = "mock" - assert.Equal(s.T(), &MockMFAPlugin{}, GetProvider()) - - providerName = "invalid_provider" - assert.Equal(s.T(), &MockMFAPlugin{}, GetProvider()) -} - -func (s *MFAProviderTestSuite) TestValidFactorType() { - assert.Equal(s.T(), true, ValidFactorType("google totp")) - assert.Equal(s.T(), true, ValidFactorType("Google TOTP")) - assert.Equal(s.T(), true, ValidFactorType("okta totp")) - assert.Equal(s.T(), true, ValidFactorType("Okta TOTP")) - assert.Equal(s.T(), true, ValidFactorType("push")) - assert.Equal(s.T(), true, ValidFactorType("Push")) - assert.Equal(s.T(), true, ValidFactorType("sms")) - assert.Equal(s.T(), true, ValidFactorType("SMS")) - assert.Equal(s.T(), true, ValidFactorType("call")) - assert.Equal(s.T(), true, ValidFactorType("Call")) - assert.Equal(s.T(), true, ValidFactorType("email")) - assert.Equal(s.T(), true, ValidFactorType("Email")) - assert.Equal(s.T(), false, ValidFactorType("Any other factor type")) -} - - -func TestMFAProviderTestSuite(t *testing.T) { - suite.Run(t, new(MFAProviderTestSuite)) -} diff --git a/ssas/service/public/middleware.go b/ssas/service/public/middleware.go deleted file mode 100644 index 3c7e353ea..000000000 --- a/ssas/service/public/middleware.go +++ /dev/null @@ -1,145 +0,0 @@ -package public - -import ( - "context" - "fmt" - "github.com/CMSgov/bcda-app/ssas" - "github.com/CMSgov/bcda-app/ssas/service" - "net/http" - "regexp" -) - - -func readGroupID(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - var ( - rd ssas.AuthRegData - err error - ) - if rd, err = readRegData(r); err != nil { - service.GetLogEntry(r).Println("no data from token about allowed groups") - respond(w, http.StatusUnauthorized) - return - } - - if rd.GroupID = r.Header.Get("x-group-id"); rd.GroupID == "" { - service.GetLogEntry(r).Println("missing header x-group-id") - respond(w, http.StatusUnauthorized) - return - } - - if !contains(rd.AllowedGroupIDs, rd.GroupID) { - service.GetLogEntry(r).Println("group specified in x-group-id not in token's allowed groups") - respond(w, http.StatusUnauthorized) - return - } - - ctx := context.WithValue(r.Context(), "rd", rd) - service.LogEntrySetField(r, "rd", rd) - next.ServeHTTP(w, r.WithContext(ctx)) - }) -} - -// Puts the decoded token, identity, and authorization values into the request context. Decoded values have been -// verified to be tokens signed by our server and to have not expired. Additional authorization -// occurs in requireRegTokenAuth() or requireMFATokenAuth(). -func parseToken(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - event := ssas.Event{Op: "ParseToken"} - authHeader := r.Header.Get("Authorization") - if authHeader == "" { - event.Help = "no authorization header found" - ssas.AuthorizationFailure(event) - next.ServeHTTP(w, r) - return - } - - authRegexp := regexp.MustCompile(`^Bearer (\S+)$`) - authSubmatches := authRegexp.FindStringSubmatch(authHeader) - if len(authSubmatches) < 2 { - event.Help = "invalid Authorization header value" - ssas.AuthorizationFailure(event) - next.ServeHTTP(w, r) - return - } - - tokenString := authSubmatches[1] - token, err := server.VerifyToken(tokenString) - if err != nil { - event.Help = fmt.Sprintf("unable to decode authorization header value; %s", err) - ssas.AuthorizationFailure(event) - next.ServeHTTP(w, r) - return - } - - var rd ssas.AuthRegData - if rd, err = readRegData(r); err != nil { - rd = ssas.AuthRegData{} - } - - if claims, ok := token.Claims.(*service.CommonClaims); ok && token.Valid { - rd.AllowedGroupIDs = claims.GroupIDs - rd.OktaID = claims.OktaID - } - ctx := context.WithValue(r.Context(), "ts", tokenString) - ctx = context.WithValue(ctx, "rd", rd) - service.LogEntrySetField(r, "rd", rd) - next.ServeHTTP(w, r.WithContext(ctx)) - }) -} - -func requireRegTokenAuth(next http.Handler) http.Handler { - return tokenAuth(next, "RegistrationToken") -} - -func requireMFATokenAuth(next http.Handler) http.Handler { - return tokenAuth(next, "MFAToken") -} - -func tokenAuth(next http.Handler, tokenType string) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - var ( - ts string - ok bool - ) - event := ssas.Event{Op: "TokenAuth"} - - tsObj := r.Context().Value("ts") - if tsObj == nil { - event.Help = "no token string found" - ssas.AuthorizationFailure(event) - respond(w, http.StatusUnauthorized) - return - } - ts, ok = tsObj.(string) - if !ok { - event.Help = "token string invalid" - ssas.AuthorizationFailure(event) - respond(w, http.StatusUnauthorized) - return - } - - err := tokenValidity(ts, tokenType) - if err != nil { - event.Help = "token invalid" - ssas.AuthorizationFailure(event) - respond(w, http.StatusUnauthorized) - return - } - - next.ServeHTTP(w, r) - }) -} - -func respond(w http.ResponseWriter, status int) { - http.Error(w, http.StatusText(status), status) -} - -func contains(list []string, target string) bool { - for _, item := range list { - if item == target { - return true - } - } - return false -} diff --git a/ssas/service/public/middleware_test.go b/ssas/service/public/middleware_test.go deleted file mode 100644 index 1c337f92e..000000000 --- a/ssas/service/public/middleware_test.go +++ /dev/null @@ -1,270 +0,0 @@ -package public - -import ( - "context" - "github.com/CMSgov/bcda-app/ssas/service" - "github.com/go-chi/chi" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/suite" - "log" - "net/http" - "net/http/httptest" - "testing" -) - -var mockHandler http.HandlerFunc = func(w http.ResponseWriter, r *http.Request) {} - -type PublicMiddlewareTestSuite struct { - suite.Suite - server *httptest.Server - rr *httptest.ResponseRecorder -} - -func (s *PublicMiddlewareTestSuite) CreateRouter(handler ...func(http.Handler) http.Handler) http.Handler { - router := chi.NewRouter() - router.With(handler...).Get("/", func(w http.ResponseWriter, r *http.Request) { - _, err := w.Write([]byte("Test router")) - if err != nil { - log.Fatal(err) - } - }) - - return router -} - -func (s *PublicMiddlewareTestSuite) SetupTest() { - s.rr = httptest.NewRecorder() -} - -func (s *PublicMiddlewareTestSuite) TestRequireTokenAuthWithInvalidSignature() { - badToken := "eyJhbGciOiJFUzM4NCIsInR5cCI6IkpXVCIsImtpZCI6ImlUcVhYSTB6YkFuSkNLRGFvYmZoa00xZi02ck1TcFRmeVpNUnBfMnRLSTgifQ.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.cJOP_w-hBqnyTsBm3T6lOE5WpcHaAkLuQGAs1QO-lg2eWs8yyGW8p9WagGjxgvx7h9X72H7pXmXqej3GdlVbFmhuzj45A9SXDOAHZ7bJXwM1VidcPi7ZcrsMSCtP1hiN" - - testForToken := - func (next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - token := r.Context().Value("token") - assert.Nil(s.T(), token) - _, err := readRegData(r) - assert.NotNil(s.T(), err) - }) - } - s.server = httptest.NewServer(s.CreateRouter(parseToken, testForToken)) - client := s.server.Client() - - // Valid token should return a 200 response - req, err := http.NewRequest("GET", s.server.URL, nil) - if err != nil { - assert.FailNow(s.T(), err.Error()) - } - - req.Header.Add("Authorization", "Bearer " + badToken) - - resp, err := client.Do(req) - if err != nil { - assert.FailNow(s.T(), err.Error()) - } - assert.Equal(s.T(), http.StatusOK, resp.StatusCode) -} - -func (s *PublicMiddlewareTestSuite) TestParseTokenEmptyToken() { - testForToken := - func (next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - token := r.Context().Value("token") - assert.Nil(s.T(), token) - _, err := readRegData(r) - assert.NotNil(s.T(), err) - }) - } - s.server = httptest.NewServer(s.CreateRouter(parseToken, testForToken)) - client := s.server.Client() - - // Valid token should return a 200 response - req, err := http.NewRequest("GET", s.server.URL, nil) - if err != nil { - log.Fatal(err) - } - - req.Header.Add("Authorization", "Bearer ") - - _, err = client.Do(req) - if err != nil { - log.Fatal(err) - } -} - -func (s *PublicMiddlewareTestSuite) TestParseTokenValidToken() { - oktaID := "fake_okta_id" - groupIDs := []string{"T0001", "T0002"} - testForToken := - func (next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - ts := r.Context().Value("ts") - assert.NotNil(s.T(), ts) - rd, err := readRegData(r) - if err != nil { - assert.FailNow(s.T(), err.Error()) - } - assert.NotNil(s.T(), rd) - assert.Equal(s.T(), oktaID, rd.OktaID) - assert.Equal(s.T(), groupIDs, rd.AllowedGroupIDs) - }) - } - s.server = httptest.NewServer(s.CreateRouter(parseToken, testForToken)) - client := s.server.Client() - - _, ts, _ := MintRegistrationToken(oktaID, groupIDs) - - req, err := http.NewRequest("GET", s.server.URL, nil) - if err != nil { - assert.FailNow(s.T(), err.Error()) - } - - req.Header.Add("Authorization", "Bearer " + ts) - - res, err := client.Do(req) - if err != nil { - assert.FailNow(s.T(), err.Error()) - } - assert.Equal(s.T(), http.StatusOK, res.StatusCode) -} - -func (s *PublicMiddlewareTestSuite) TestRequireRegTokenAuthValidToken() { - s.server = httptest.NewServer(s.CreateRouter(requireRegTokenAuth)) - - // Valid token should return a 200 response - req, err := http.NewRequest("GET", s.server.URL, nil) - if err != nil { - assert.FailNow(s.T(), err.Error()) - } - - handler := requireRegTokenAuth(mockHandler) - - groupIDs := []string{"A0001", "A0002"} - token, ts, err := MintRegistrationToken("fake_okta_id", groupIDs) - assert.Nil(s.T(), err) - assert.NotNil(s.T(), token) - assert.NotNil(s.T(), ts) - - ctx := req.Context() - ctx = context.WithValue(ctx, "ts", ts) - req = req.WithContext(ctx) - - handler.ServeHTTP(s.rr, req) - if err != nil { - assert.FailNow(s.T(), err.Error()) - } - assert.Equal(s.T(), http.StatusOK, s.rr.Code) -} - -func (s *PublicMiddlewareTestSuite) TestRequireRegTokenAuthRevoked() { - s.server = httptest.NewServer(s.CreateRouter(requireMFATokenAuth)) - - // Valid token should return a 200 response - req, err := http.NewRequest("GET", s.server.URL, nil) - if err != nil { - assert.FailNow(s.T(), err.Error()) - } - - handler := requireMFATokenAuth(mockHandler) - - groupIDs := []string{"A0001", "A0002"} - token, ts, err := MintRegistrationToken("fake_okta_id", groupIDs) - assert.Nil(s.T(), err) - - claims := token.Claims.(*service.CommonClaims) - err = service.TokenBlacklist.BlacklistToken(claims.Id, service.TokenCacheLifetime) - assert.Nil(s.T(), err) - assert.True(s.T(), service.TokenBlacklist.IsTokenBlacklisted(claims.Id)) - - assert.NotNil(s.T(), token) - - ctx := req.Context() - ctx = context.WithValue(ctx, "ts", ts) - req = req.WithContext(ctx) - - handler.ServeHTTP(s.rr, req) - if err != nil { - assert.FailNow(s.T(), err.Error()) - } - assert.Equal(s.T(), http.StatusUnauthorized, s.rr.Code) -} - - -func (s *PublicMiddlewareTestSuite) TestRequireRegTokenAuthEmptyToken() { - s.server = httptest.NewServer(s.CreateRouter(requireMFATokenAuth)) - client := s.server.Client() - - // Valid token should return a 200 response - req, err := http.NewRequest("GET", s.server.URL, nil) - if err != nil { - assert.FailNow(s.T(), err.Error()) - } - - ctx := context.WithValue(context.Background(), "ts", nil) - - resp, err := client.Do(req.WithContext(ctx)) - if err != nil { - assert.FailNow(s.T(), err.Error()) - } - assert.Equal(s.T(), http.StatusUnauthorized, resp.StatusCode) -} - -func (s *PublicMiddlewareTestSuite) TestRequireMFATokenAuthValidToken() { - s.server = httptest.NewServer(s.CreateRouter(requireMFATokenAuth)) - - // Valid token should return a 200 response - req, err := http.NewRequest("GET", s.server.URL, nil) - if err != nil { - assert.FailNow(s.T(), err.Error()) - } - - handler := requireMFATokenAuth(mockHandler) - token, ts, err := MintMFAToken("fake_okta_id") - assert.Nil(s.T(), err) - assert.NotNil(s.T(), token) - assert.NotNil(s.T(), ts) - - ctx := req.Context() - ctx = context.WithValue(ctx, "ts", ts) - req = req.WithContext(ctx) - - handler.ServeHTTP(s.rr, req) - if err != nil { - assert.FailNow(s.T(), err.Error()) - } - assert.Equal(s.T(), http.StatusOK, s.rr.Code) -} - - -func (s *PublicMiddlewareTestSuite) TestRequireMFATokenAuthEmptyToken() { - s.server = httptest.NewServer(s.CreateRouter(requireMFATokenAuth)) - client := s.server.Client() - - // Valid token should return a 200 response - req, err := http.NewRequest("GET", s.server.URL, nil) - if err != nil { - assert.FailNow(s.T(), err.Error()) - } - - ctx := context.WithValue(context.Background(), "ts", nil) - - resp, err := client.Do(req.WithContext(ctx)) - if err != nil { - assert.FailNow(s.T(), err.Error()) - } - assert.Equal(s.T(), http.StatusUnauthorized, resp.StatusCode) -} - -func (s *PublicMiddlewareTestSuite) TestContains() { - list := []string{"abc", "def", "hij", "hij"} - assert.True(s.T(), contains(list, "abc")) - assert.True(s.T(), contains(list, "def")) - assert.True(s.T(), contains(list, "hij")) - assert.False(s.T(), contains(list, "lmn")) -} - -func TestPublicMiddlewareTestSuite(t *testing.T) { - suite.Run(t, new(PublicMiddlewareTestSuite)) -} - diff --git a/ssas/service/public/mockmfa.go b/ssas/service/public/mockmfa.go deleted file mode 100644 index 7be239df9..000000000 --- a/ssas/service/public/mockmfa.go +++ /dev/null @@ -1,152 +0,0 @@ -package public - -import ( - "errors" - "strings" - "time" - - "github.com/CMSgov/bcda-app/ssas" -) - -type MockMFAPlugin struct{} - -/* - VerifyPassword checks a username/password combination for validity, and if successful, returns a value representing - the state of the account. It mocks responses according to the following chart: - - userIdentifier response error - -------------- -------- ----- - success@test.com true none - locked_out@test.com false none - mfa_enroll@test.com false none - expired@test.com false none - bad_password@test.com false none - error@test.com (none) (non-nil error) - (all others) true none -*/ -func (m *MockMFAPlugin) VerifyPassword(userIdentifier string, password string, trackingId string) (passwordReturn *PasswordReturn, oktaId string, err error) { - r := PasswordReturn{Success: false, Message: ""} - oktaId = "fake_okta_id" - verifyEvent := ssas.Event{Op: "VerifyOktaPassword", TrackingID: trackingId} - ssas.OperationStarted(verifyEvent) - - switch strings.ToLower(userIdentifier) { - case "error@test.com": - err = errors.New("mocking error") - verifyEvent.Help = "mocking error" - passwordReturn = nil - ssas.OperationFailed(verifyEvent) - return - case "locked_out@test.com": - r.Message = "account locked out" - case "mfa_enroll@test.com": - r.Message = "account needs to enroll MFA factor" - case "expired@test.com": - r.Message = "password expired" - case "bad_password@test.com": - r.Message = "authentication failed" - case "success@test.com": - fallthrough - default: - r.Success = true - r.Message = "proceed to MFA verification" - passwordReturn = &r - ssas.OperationSucceeded(verifyEvent) - return - } - - passwordReturn = &r - ssas.OperationFailed(verifyEvent) - return -} - -/* - VerifyFactorChallenge tests an MFA passcode for validity. This function should be used for all factor types - except Push. It mocks responses with valid factor types according to the following chart: - - userIdentifier response error - -------------- -------- ----- - success@test.com true none - failure@test.com false none - error@test.com false (non-nil error) - (all others) false none -*/ -func (m *MockMFAPlugin) VerifyFactorChallenge(userIdentifier string, factorType string, passcode string, trackingId string) (success bool, oktaUserID string, groupIDs []string) { - success = false - verifyEvent := ssas.Event{Op: "VerifyOktaFactorChallenge", TrackingID: trackingId} - ssas.OperationStarted(verifyEvent) - - if !ValidFactorType(factorType) { - verifyEvent.Help = "invalid factor type: " + factorType - ssas.OperationFailed(verifyEvent) - return - } - - switch strings.ToLower(userIdentifier) { - case "error@test.com": - verifyEvent.Help = "mocking error" - case "failure@test.com": // noop - case "success@test.com": - fallthrough - default: - oktaUserID = "fake_okta_id" - groupIDs = []string{"T0001", "T0002"} - ssas.OperationSucceeded(verifyEvent) - success = true - return - } - - ssas.OperationFailed(verifyEvent) - return -} - -/* - VerifyFactorTransaction reports the status of a Push factor's transaction. Possible non-error states include success, - rejection, waiting, and timeout. -*/ -func (m *MockMFAPlugin) VerifyFactorTransaction(userIdentifier string, factorType string, transactionId string, trackingId string) (string, error) { - return "", errors.New("function VerifyFactorTransaction() not yet implemented in MockMFAPlugin") -} - -/* - RequestFactorChallenge is to be called from the /authn/request endpoint. It mocks responses with - valid factor types according to the following chart: - - userIdentifier response error - -------------- -------- ----- - success@test.com request_sent none - transaction@test.com request_sent, transaction none - error@test.com (none) (non-nil error) - (all others) request_sent none -*/ -func (m *MockMFAPlugin) RequestFactorChallenge(userIdentifier string, factorType string, trackingId string) (factorReturn *FactorReturn, err error) { - requestEvent := ssas.Event{Op: "RequestOktaFactorChallenge", TrackingID: trackingId} - ssas.OperationStarted(requestEvent) - - if !ValidFactorType(factorType) { - factorReturn = &FactorReturn{Action: "invalid_request"} - requestEvent.Help = "invalid factor type: " + factorType - ssas.OperationFailed(requestEvent) - return - } - - factorReturn = &FactorReturn{Action: "request_sent"} - - switch strings.ToLower(userIdentifier) { - case "error@test.com": - err = errors.New("mocking error") - requestEvent.Help = "mocking error" - ssas.OperationFailed(requestEvent) - return - case "transaction@test.com": - transactionId, _ := generateOktaTransactionId() - transactionExpires := time.Now().Add(time.Minute * 5) - factorReturn = &FactorReturn{Action: "request_sent", Transaction: &Transaction{TransactionID: transactionId, ExpiresAt: transactionExpires}} - case "success@test.com": - fallthrough - default: - } - - ssas.OperationSucceeded(requestEvent) - return -} diff --git a/ssas/service/public/mockmfa_test.go b/ssas/service/public/mockmfa_test.go deleted file mode 100644 index 310a015c6..000000000 --- a/ssas/service/public/mockmfa_test.go +++ /dev/null @@ -1,186 +0,0 @@ -package public - -import ( - "testing" - - "github.com/pborman/uuid" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/suite" -) - -type MockMFATestSuite struct { - suite.Suite - o MFAProvider -} - -func (s *MockMFATestSuite) TestConfig() { - s.o = &MockMFAPlugin{} -} - -func (s *MockMFATestSuite) TestVerifyPasswordSuccess() { - trackingId := uuid.NewRandom().String() - userId := "success@test.com" - - passwordReturn, oktaId, err := s.o.VerifyPassword(userId, "any_password_will_do", trackingId) - assert.Nil(s.T(), err) - if passwordReturn == nil || oktaId == "" { - s.FailNow("we expect no errors from the mocked VerifyPassword() for this user ID") - } - assert.True(s.T(), passwordReturn.Success) - assert.NotEqual(s.T(), passwordReturn.Message, "") -} - -func (s *MockMFATestSuite) TestVerifyPasswordFailure() { - trackingId := uuid.NewRandom().String() - userId := "locked_out@test.com" - - passwordReturn, oktaId, err := s.o.VerifyPassword(userId, "any_password_will_do", trackingId) - assert.Nil(s.T(), err) - if passwordReturn == nil || oktaId == "" { - s.FailNow("we expect a passwordReturn struct from the mocked VerifyPassword() for this user ID") - } - assert.False(s.T(), passwordReturn.Success) -} - -func (s *MockMFATestSuite) TestVerifyPasswordError() { - trackingId := uuid.NewRandom().String() - userId := "error@test.com" - - passwordReturn, oktaId, err := s.o.VerifyPassword(userId, "any_password_will_do", trackingId) - assert.NotNil(s.T(), err) - if passwordReturn != nil || oktaId == "" { - s.FailNow("we expect no passwordReturn from the mocked VerifyPassword() when an error is raised") - } -} - -func (s *MockMFATestSuite) TestRequestFactorChallengeSuccess() { - trackingId := uuid.NewRandom().String() - userId := "success@test.com" - factorType := "SMS" - - factorReturn, err := s.o.RequestFactorChallenge(userId, factorType, trackingId) - assert.Nil(s.T(), err) - if factorReturn == nil { - s.FailNow("we expect no errors from the mocked RequestFactorChallenge() for this user ID") - } - assert.Equal(s.T(), factorReturn.Action, "request_sent") - assert.Nil(s.T(), factorReturn.Transaction) -} - -func (s *MockMFATestSuite) TestRequestFactorChallengeTransaction() { - trackingId := uuid.NewRandom().String() - userId := "transaction@test.com" - factorType := "SMS" - - factorReturn, err := s.o.RequestFactorChallenge(userId, factorType, trackingId) - assert.Nil(s.T(), err) - if factorReturn == nil { - s.FailNow("we expect no errors from the mocked RequestFactorChallenge() for this user ID") - } - assert.Equal(s.T(), factorReturn.Action, "request_sent") - if factorReturn.Transaction == nil { - s.FailNow("we expect a Transaction from the mocked RequestFactorChallenge() for this user ID") - } - assert.NotNil(s.T(), factorReturn.Transaction.TransactionID) -} - -func (s *MockMFATestSuite) TestRequestFactorChallengeError() { - trackingId := uuid.NewRandom().String() - userId := "error@test.com" - factorType := "SMS" - - factorReturn, err := s.o.RequestFactorChallenge(userId, factorType, trackingId) - if factorReturn == nil { - s.FailNow("despite the error, we always expect a factorReturn from the mocked RequestFactorChallenge()") - } - assert.Equal(s.T(), factorReturn.Action, "request_sent") - assert.NotNil(s.T(), err) -} - -func (s *MockMFATestSuite) TestRequestFactorChallengeRandomUserID() { - trackingId := uuid.NewRandom().String() - userId := "asdf@test.com" - factorType := "SMS" - - factorReturn, err := s.o.RequestFactorChallenge(userId, factorType, trackingId) - assert.Nil(s.T(), err) - if factorReturn == nil { - s.FailNow("we expect no errors from the mocked RequestFactorChallenge() for this user ID") - } - assert.Equal(s.T(), factorReturn.Action, "request_sent") - assert.Nil(s.T(), factorReturn.Transaction) -} - -func (s *MockMFATestSuite) TestRequestFactorChallengeBadFactor() { - trackingId := uuid.NewRandom().String() - userId := "success@test.com" - factorType := "Unknown factor type" - - factorReturn, err := s.o.RequestFactorChallenge(userId, factorType, trackingId) - assert.Nil(s.T(), err) - if factorReturn == nil { - s.FailNow("despite the error, we always expect a factorReturn from the mocked RequestFactorChallenge()") - } - assert.Equal(s.T(), factorReturn.Action, "invalid_request") - assert.Nil(s.T(), factorReturn.Transaction) -} - -func (s *MockMFATestSuite) TestVerifyFactorChallengeSuccess() { - trackingId := uuid.NewRandom().String() - userId := "success@test.com" - factorType := "SMS" - passcode := "mock doesn't care what this is" - - success, oktaID, groupIDs := s.o.VerifyFactorChallenge(userId, factorType, passcode, trackingId) - assert.True(s.T(), success) - assert.NotEqual(s.T(), "", oktaID) - assert.False(s.T(), empty(groupIDs)) -} - -func (s *MockMFATestSuite) TestVerifyFactorChallengeFailure() { - trackingId := uuid.NewRandom().String() - userId := "failure@test.com" - factorType := "SMS" - passcode := "mock doesn't care what this is" - - success, _, _ := s.o.VerifyFactorChallenge(userId, factorType, passcode, trackingId) - assert.False(s.T(), success) -} - -func (s *MockMFATestSuite) TestVerifyFactorChallengeError() { - trackingId := uuid.NewRandom().String() - userId := "error@test.com" - factorType := "SMS" - passcode := "mock doesn't care what this is" - - success, oktaID, groupIDs := s.o.VerifyFactorChallenge(userId, factorType, passcode, trackingId) - assert.False(s.T(), success) - assert.Equal(s.T(), "", oktaID) - assert.True(s.T(), empty(groupIDs)) -} - -func (s *MockMFATestSuite) TestVerifyFactorChallengeRandomUserID() { - trackingId := uuid.NewRandom().String() - userId := "asdf@test.com" - factorType := "SMS" - passcode := "mock doesn't care what this is" - - success, oktaID, groupIDs := s.o.VerifyFactorChallenge(userId, factorType, passcode, trackingId) - assert.True(s.T(), success) - assert.NotEqual(s.T(), "", oktaID) - assert.False(s.T(), empty(groupIDs)) -} - -func (s *MockMFATestSuite) TestVerifyFactorChallengeBadFactor() { - trackingId := uuid.NewRandom().String() - userId := "success@test.com" - factorType := "Unknown factor type" - passcode := "mock doesn't care what this is" - - success, _, _ := s.o.VerifyFactorChallenge(userId, factorType, passcode, trackingId) - assert.False(s.T(), success) -} - -func TestMockMFATestSuite(t *testing.T) { - suite.Run(t, new(MockMFATestSuite)) -} diff --git a/ssas/service/public/oktalive_test.go b/ssas/service/public/oktalive_test.go deleted file mode 100644 index 00f253155..000000000 --- a/ssas/service/public/oktalive_test.go +++ /dev/null @@ -1,215 +0,0 @@ -// +build okta - -// To enable this test suite, either: - -// - Run from your IDE after setting the env vars below -// OR -// - Run "DATABASE_URL=postgresql://postgres:toor@127.0.0.1:5432/bcda?sslmode=disable go test -tags=okta -v" from the ssas/service/public directory - -package public - -import ( - "encoding/hex" - "fmt" - "os" - "testing" - - "github.com/pborman/uuid" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/suite" - - "github.com/CMSgov/bcda-app/ssas/okta" -) - -type OktaLiveTestSuite struct { - suite.Suite - oc *OktaMFAPlugin - email string - userId string - password string - smsFactorId string -} - -func (s *OktaLiveTestSuite) SetupSuite() { - s.email = os.Getenv("OKTA_MFA_EMAIL") - s.userId = os.Getenv("OKTA_MFA_USER_ID") - s.password = os.Getenv("OKTA_MFA_USER_PASSWORD") - s.smsFactorId = os.Getenv("OKTA_MFA_SMS_FACTOR_ID") - - if s.email == "" || s.userId == "" || s.password == "" || s.smsFactorId == "" { - s.FailNow(fmt.Sprintf("Cannot run live Okta tests without env vars set: OKTA_MFA_EMAIL=%s; OKTA_MFA_USER_ID=%s; OKTA_MFA_USER_PASSWORD=%s; OKTA_MFA_SMS_FACTOR_ID=%s", - s.email, s.userId, s.password, s.smsFactorId)) - } -} - -func (s *OktaLiveTestSuite) SetupTest() { - s.oc = NewOktaMFA(okta.Client()) -} - -func (s *OktaLiveTestSuite) TestPostPasswordSuccess() { - trackingId := uuid.NewRandom().String() - - passwordRequest, err := s.oc.postPassword(s.email, s.password, trackingId) - if err != nil || passwordRequest == nil { - s.FailNow("password result not parsed: " + err.Error()) - } - assert.Equal(s.T(), "MFA_REQUIRED", passwordRequest.Status) -} - -func (s *OktaLiveTestSuite) TestPostPasswordFailure() { - trackingId := uuid.NewRandom().String() - - passwordRequest, err := s.oc.postPassword(s.userId, "bad_password", trackingId) - if err != nil || passwordRequest == nil { - s.FailNow("password result not parsed: " + err.Error()) - } - assert.Equal(s.T(), "AUTHENTICATION_FAILED", passwordRequest.Status) -} - -func (s *OktaLiveTestSuite) TestPostPasswordBadUserId() { - trackingId := uuid.NewRandom().String() - - passwordRequest, err := s.oc.postPassword("bad_user_id", s.password, trackingId) - if err != nil || passwordRequest == nil { - s.FailNow("password result not parsed: " + err.Error()) - } - assert.Equal(s.T(), "AUTHENTICATION_FAILED", passwordRequest.Status) -} - -func (s *OktaLiveTestSuite) TestPostFactorChallengeSuccess() { - trackingId := uuid.NewRandom().String() - factor := Factor{Id: s.smsFactorId, Type: "sms"} - - factorVerification, err := s.oc.postFactorChallenge(s.userId, factor, trackingId) - if err != nil || factorVerification == nil { - s.FailNow("factor result not parsed: " + err.Error()) - } - assert.Equal(s.T(), "CHALLENGE", factorVerification.Result) - - // A second SMS request within 30 seconds will fail. This second test case must - // follow in the same unit test as the successful test case. - factorVerification, err = s.oc.postFactorChallenge(s.userId, factor, trackingId) - if err == nil || factorVerification != nil { - s.FailNow("second request should not be successful") - } -} - -func (s *OktaLiveTestSuite) TestPostFactorChallengeInvalidFactor() { - trackingId := uuid.NewRandom().String() - factor := Factor{Id: "abcdefg1234567", Type: "sms"} - - factorVerification, err := s.oc.postFactorChallenge(s.userId, factor, trackingId) - if err == nil || factorVerification != nil { - s.FailNow("invalid factor should not be successful") - } -} - -func (s *OktaLiveTestSuite) TestPostFactorChallengeInvalidUser() { - trackingId := uuid.NewRandom().String() - userId := "abcdefg1234567" - factor := Factor{Id: s.smsFactorId, Type: "sms"} - - factorVerification, err := s.oc.postFactorChallenge(userId, factor, trackingId) - if err == nil || factorVerification != nil { - s.FailNow("invalid factor should not be successful") - } -} - -func (s *OktaLiveTestSuite) TestPinnedKeyNotMatched() { - originalOktaCACertFingerprint := okta.OktaCACertFingerprint - okta.OktaCACertFingerprint, _ = hex.DecodeString("00112233aabbcc") - s.oc = NewOktaMFA(okta.Client()) - - trackingId := uuid.NewRandom().String() - factor := Factor{Id: s.smsFactorId, Type: "sms"} - - factorVerification, err := s.oc.postFactorChallenge(s.userId, factor, trackingId) - if err == nil || factorVerification != nil { - s.FailNow("certificate signed by unexpected CA should abort TLS handshake") - } - okta.OktaCACertFingerprint = originalOktaCACertFingerprint -} - -func (s *OktaLiveTestSuite) TestGetUserSuccess() { - trackingId := uuid.NewRandom().String() - - foundUserId, err := s.oc.getUser(s.email, trackingId) - assert.Nil(s.T(), err) - assert.Equal(s.T(), s.userId, foundUserId) -} - -func (s *OktaLiveTestSuite) TestGetUserBadAuth() { - originalOktaAuthString := okta.OktaAuthString - okta.OktaAuthString = "SSWS 00112233aabbcc" - - trackingId := uuid.NewRandom().String() - - foundUserId, err := s.oc.getUser(s.email, trackingId) - if err == nil || foundUserId != "" { - s.FailNow("bad Okta token should not be successful") - } - assert.Contains(s.T(), err.Error(), "error received") - okta.OktaAuthString = originalOktaAuthString -} - -func (s *OktaLiveTestSuite) TestGetUserTooManyFound() { - trackingId := uuid.NewRandom().String() - searchString := "bcda" - - foundUserId, err := s.oc.getUser(searchString, trackingId) - if err == nil || foundUserId != "" { - s.FailNow("user search string with multiple matches should not be successful") - } - assert.Contains(s.T(), err.Error(), "multiple users") -} - -func (s *OktaLiveTestSuite) TestGetUserNoneFound() { - trackingId := uuid.NewRandom().String() - searchString := "a1b2c3d4" - - foundUserId, err := s.oc.getUser(searchString, trackingId) - if err == nil || foundUserId != "" { - s.FailNow("user search string with no matches should not be successful") - } - assert.Contains(s.T(), err.Error(), "not found") -} - -func (s *OktaLiveTestSuite) TestGetUserFactorSuccess() { - trackingId := uuid.NewRandom().String() - factorType := "SMS" - - factor, err := s.oc.getUserFactor(s.userId, factorType, trackingId) - assert.Nil(s.T(), err) - if factor == nil { - s.FailNow("getUserFactor() should successfully return a factor") - } - assert.Equal(s.T(), s.smsFactorId, factor.Id) - assert.Equal(s.T(), "sms", factor.Type) -} - -func (s *OktaLiveTestSuite) TestGetUserBadUser() { - trackingId := uuid.NewRandom().String() - userId := "abc123" - factorType := "SMS" - - factor, err := s.oc.getUserFactor(userId, factorType, trackingId) - if err == nil || factor != nil { - s.FailNow("getUserFactor() should fail with a bad user ID") - } - assert.Contains(s.T(), err.Error(), "error received") -} - -func (s *OktaLiveTestSuite) TestGetUserFactorNotFound() { - trackingId := uuid.NewRandom().String() - factorType := "Push" - - factor, err := s.oc.getUserFactor(s.userId, factorType, trackingId) - if err == nil || factor != nil { - s.FailNow("getUserFactor() should fail with factor type to registered to specified user") - } - assert.Contains(s.T(), err.Error(), "active factor") -} - -func TestOktaLiveTestSuite(t *testing.T) { - suite.Run(t, new(OktaLiveTestSuite)) -} diff --git a/ssas/service/public/oktamfa.go b/ssas/service/public/oktamfa.go deleted file mode 100644 index 943641114..000000000 --- a/ssas/service/public/oktamfa.go +++ /dev/null @@ -1,565 +0,0 @@ -package public - -import ( - "crypto/rand" - "encoding/json" - "errors" - "fmt" - "io" - "io/ioutil" - "math/big" - "net/http" - "regexp" - "strings" - "time" - - "github.com/CMSgov/bcda-app/ssas" - "github.com/CMSgov/bcda-app/ssas/cfg" - "github.com/CMSgov/bcda-app/ssas/okta" -) - -type OktaUser struct { - Id string `json:"id"` - Status string `json:"status,omitempty"` - Profile UserProfile `json:"profile"` -} - -type UserProfile struct { - LOA string `json:"LOA,omitempty"` -} - -type Factor struct { - Id string `json:"id"` - Type string `json:"factorType"` - Provider string `json:"provider"` - Status string `json:"status"` -} - -type Embedded struct { - User OktaUser `json:"user"` -} - -type PasswordResponse struct { - Status string `json:"status"` - Embedded Embedded `json:"_embedded,omitempty"` -} - -type FactorResponse struct { - Result string `json:"factorResult"` - ExpiresAt time.Time `json:"expiresAt,omitempty"` - Links OktaLinks `json:"_links,omitempty"` -} - -type OktaLinks struct { - Cancel Link `json:"cancel,omitempty"` - Poll Link `json:"poll,omitempty"` -} - -type Allow struct { - Verbs []string `json:"allow"` -} - -type Link struct { - Href string `json:"href"` - Hints Allow `json:"hints"` -} - -type OktaMFAPlugin struct { - Client *http.Client -} - -var RequestFactorChallengeDuration time.Duration - -func init() { - factorChallengeMilliseconds := cfg.GetEnvInt("SSAS_MFA_CHALLENGE_REQUEST_MILLISECONDS", 1500) - RequestFactorChallengeDuration = time.Millisecond * time.Duration(factorChallengeMilliseconds) -} - -func NewOktaMFA(client *http.Client) *OktaMFAPlugin { - if nil == client { - client = okta.Client() - } - - return &OktaMFAPlugin{Client: client} -} - -// VerifyPassword tests a username/password for validity. This function should be used before calling MFA functions. -func (o *OktaMFAPlugin) VerifyPassword(userIdentifier string, password string, trackingId string) (passwordReturn *PasswordReturn, oktaId string, err error) { - passwordEvent := ssas.Event{Op: "VerifyOktaPassword", TrackingID: trackingId} - ssas.OperationStarted(passwordEvent) - - // Look up Okta ID for logging purposes - oktaUserID, err := o.getUser(userIdentifier, trackingId) - if err != nil { - passwordEvent.Help = "matching user not found: " + err.Error() - ssas.OperationFailed(passwordEvent) - err = errors.New(passwordEvent.Help) - return - } - passwordEvent.UserID = oktaUserID - - passwordResponse, err := o.postPassword(userIdentifier, password, trackingId) - if err != nil { - passwordEvent.Help = "error validating factor passcode: " + err.Error() - ssas.OperationFailed(passwordEvent) - err = errors.New(passwordEvent.Help) - return - } - - success := false - message := "" - switch passwordResponse.Status { - case "PASSWORD_EXPIRED": - message = "password expired" - case "MFA_ENROLL": - message = "account needs to enroll MFA factor" - case "MFA_ENROLL_ACTIVATE": - message = "account needs to activate MFA factor" - case "AUTHENTICATION_FAILED": - message = "authentication request failed" - case "MFA_REQUIRED": - fallthrough - case "SUCCESS": - message = "proceed to MFA verification" - success = true - default: - message = "unknown password verification response" - passwordEvent.Help = message - ssas.OperationFailed(passwordEvent) - err = errors.New(message) - return - } - - // Will be "" in some cases - oktaId = passwordResponse.Embedded.User.Id - - passwordReturn = &PasswordReturn{Success: success, Message: message} - passwordEvent.Help = message - ssas.OperationSucceeded(passwordEvent) - return -} - -// VerifyFactorChallenge tests an MFA passcode for validity. This function should be used for all factor types -// except Push. -func (o *OktaMFAPlugin) VerifyFactorChallenge(userIdentifier string, factorType string, passcode string, trackingId string) (success bool, oktaID string, groupIDs []string) { - startTime := time.Now() - success = false - requestEvent := ssas.Event{Op: "VerifyOktaFactorChallenge", TrackingID: trackingId} - ssas.OperationStarted(requestEvent) - - if !ValidFactorType(factorType) { - requestEvent.Help = "invalid factor type: " + factorType - ssas.OperationFailed(requestEvent) - wait(startTime, RequestFactorChallengeDuration) - return - } - - oktaID, err := o.getUser(userIdentifier, trackingId) - if err != nil { - requestEvent.Help = "matching user not found: " + err.Error() - ssas.OperationFailed(requestEvent) - wait(startTime, RequestFactorChallengeDuration) - return - } - - requestEvent.UserID = oktaID - oktaFactor, err := o.getUserFactor(oktaID, factorType, trackingId) - if err != nil { - requestEvent.Help = "matching factor not found: " + err.Error() - ssas.OperationFailed(requestEvent) - wait(startTime, RequestFactorChallengeDuration) - return - } - - factorRequest, err := o.postFactorResponse(oktaID, *oktaFactor, passcode, trackingId) - if err != nil { - requestEvent.Help = "error validating factor passcode: " + err.Error() - ssas.OperationFailed(requestEvent) - wait(startTime, RequestFactorChallengeDuration) - return - } - - success = factorRequest.Result == "SUCCESS" - - if !success { - requestEvent.Help = "passcode not accepted" - ssas.OperationFailed(requestEvent) - wait(startTime, RequestFactorChallengeDuration) - return - } - - groupIDs, err = ssas.GetAuthorizedGroupsForOktaID(oktaID) - if err != nil { - requestEvent.Help = "failure getting authorized groups: " + err.Error() - ssas.OperationFailed(requestEvent) - wait(startTime, RequestFactorChallengeDuration) - return - } - - requestEvent.Help = fmt.Sprintf("okta.VerifyFactorChallenge() execution seconds: %f", time.Since(startTime).Seconds()) - ssas.OperationSucceeded(requestEvent) - wait(startTime, RequestFactorChallengeDuration) - return -} - -// VerifyFactorTransaction reports the status of a Push factor's transaction. Possible non-error states include success, -// rejection, waiting, and timeout. -func (o *OktaMFAPlugin) VerifyFactorTransaction(userIdentifier string, factorType string, transactionId string, trackingId string) (string, error) { - return "", errors.New("function VerifyFactorTransaction() not yet implemented in OktaMFAPlugin") -} - -// RequestFactorChallenge is to be called from the /authn/request endpoint. It looks up the Okta user ID and factor ID, calls okta.postFactorChallenge(), -// and filters the information returned to minimize information leakage. -// -// Valid factor types include: -// "Google TOTP" (Google Authenticator) -// "Okta TOTP" (Okta Verify app time-based token) -// "Push" (Okta Verify app push) -// "SMS" -// "Call" -// "Email" -func (o *OktaMFAPlugin) RequestFactorChallenge(userIdentifier string, factorType string, trackingId string) (factorReturn *FactorReturn, err error) { - startTime := time.Now() - requestEvent := ssas.Event{Op: "RequestOktaFactorChallenge", TrackingID: trackingId} - ssas.OperationStarted(requestEvent) - - if !ValidFactorType(factorType) { - factorReturn = &FactorReturn{Action: "invalid_request"} - requestEvent.Help = "invalid factor type: " + factorType - ssas.OperationFailed(requestEvent) - wait(startTime, RequestFactorChallengeDuration) - return - } - - oktaUserID, err := o.getUser(userIdentifier, trackingId) - if err != nil { - factorReturn = formatFactorReturn(factorType, factorReturn) - requestEvent.Help = "matching user not found: " + err.Error() - ssas.OperationFailed(requestEvent) - wait(startTime, RequestFactorChallengeDuration) - return - } - - requestEvent.UserID = oktaUserID - oktaFactor, err := o.getUserFactor(oktaUserID, factorType, trackingId) - if err != nil { - factorReturn = formatFactorReturn(factorType, factorReturn) - requestEvent.Help = "matching factor not found: " + err.Error() - ssas.OperationFailed(requestEvent) - wait(startTime, RequestFactorChallengeDuration) - return - } - - factorRequest, err := o.postFactorChallenge(oktaUserID, *oktaFactor, trackingId) - if err != nil { - factorReturn = formatFactorReturn(factorType, factorReturn) - requestEvent.Help = "error requesting challenge for factor: " + err.Error() - ssas.OperationFailed(requestEvent) - wait(startTime, RequestFactorChallengeDuration) - return - } - - if factorRequest.Links.Poll.Href != "" { - factorReturn = &FactorReturn{Action: "request_sent"} - factorReturn.Transaction = &Transaction{} - factorReturn.Transaction.TransactionID = parsePushTransaction(factorRequest.Links.Poll.Href) - factorReturn.Transaction.ExpiresAt = factorRequest.ExpiresAt - } - - factorReturn = formatFactorReturn(factorType, factorReturn) - requestEvent.Help = fmt.Sprintf("okta.RequestFactorChallenge() execution seconds: %f", time.Since(startTime).Seconds()) - ssas.OperationSucceeded(requestEvent) - wait(startTime, RequestFactorChallengeDuration) - return -} - -// formatFactorReturn generates dummy return values if needed -func formatFactorReturn(factorType string, factorReturn *FactorReturn) *FactorReturn { - if factorReturn == nil || factorReturn.Action == "" { - factorReturn = &FactorReturn{Action: "request_sent"} - } - - if strings.ToLower(factorType) == "push" { - if factorReturn.Transaction == nil || factorReturn.Transaction.TransactionID == "" { - factorReturn.Transaction = &Transaction{} - transactionID, err := generateOktaTransactionId() - if err != nil { - return &FactorReturn{Action: "aborted"} - } - factorReturn.Transaction.TransactionID = transactionID - } - - if factorReturn.Transaction.ExpiresAt.Before(time.Now()) { - factorReturn.Transaction.ExpiresAt = time.Now().Add(time.Minute * 5) - } - } else { - factorReturn.Transaction = nil - } - return factorReturn -} - -// wait() provides fixed-time execution for functions that could leak information based on how quickly they return -func wait(startTime time.Time, targetDuration time.Duration) { - elapsed := time.Since(startTime) - time.Sleep(targetDuration - elapsed) -} - -func (o *OktaMFAPlugin) postPassword(oktaUserId string, password string, trackingId string) (passwordResponse *PasswordResponse, err error) { - requestEvent := ssas.Event{Op: "PostOktaPassword", TrackingID: trackingId} - ssas.OperationStarted(requestEvent) - - passwordUrl := fmt.Sprintf("%s/api/v1/authn", okta.OktaBaseUrl) - requestBody := strings.NewReader(fmt.Sprintf(`{"username":"%s","password":"%s"}`, oktaUserId, password)) - - resp, body, err := o.oktaRequest("POST", passwordUrl, requestBody) - - if resp != nil && resp.StatusCode == 401 { - requestEvent.Help = "authentication failure: " + string(body) - ssas.OperationFailed(requestEvent) - p := PasswordResponse{Status: "AUTHENTICATION_FAILED"} - return &p, nil - } - - if err != nil { - requestEvent.Help = err.Error() - ssas.OperationFailed(requestEvent) - return nil, err - } - - var p PasswordResponse - if err = json.Unmarshal(body, &p); err != nil { - requestEvent.Help = fmt.Sprintf("unexpected response format; response: %s", string(body)) - ssas.OperationFailed(requestEvent) - return nil, errors.New(requestEvent.Help) - } - - ssas.OperationSucceeded(requestEvent) - return &p, nil -} - -// getUser searches for Okta users using the provided search string. Only return results if exactly one active user -// of LOA=3 is found. -func (o *OktaMFAPlugin) getUser(searchString string, trackingId string) (oktaId string, err error) { - userEvent := ssas.Event{Op: "FindOktaUser", TrackingID: trackingId} - ssas.OperationStarted(userEvent) - - userUrl := fmt.Sprintf("%s/api/v1/users/?q=%s", okta.OktaBaseUrl, searchString) - - _, body, err := o.oktaRequest("GET", userUrl, nil) - if err != nil { - userEvent.Help = err.Error() - ssas.OperationFailed(userEvent) - return "", err - } - - var users []OktaUser - if err = json.Unmarshal(body, &users); err != nil { - userEvent.Help = fmt.Sprintf("unexpected response format; response: %s", string(body)) - ssas.OperationFailed(userEvent) - return "", errors.New(userEvent.Help) - } - - var userCountMessage string - switch { - case len(users) == 0: - userCountMessage = "user not found" - case len(users) > 1: - userCountMessage = "multiple users found" - } - - if len(users) != 1 { - userEvent.Help = fmt.Sprintf("error finding user: %s", userCountMessage) - ssas.OperationFailed(userEvent) - return "", errors.New(userEvent.Help) - } - - user := users[0] - if user.Status != "ACTIVE" { - userEvent.Help = "user not active" - ssas.OperationFailed(userEvent) - return "", errors.New(userEvent.Help) - } - - if user.Profile.LOA != "3" { - userEvent.Help = "user not certified LOA 3" - ssas.OperationFailed(userEvent) - return "", errors.New(userEvent.Help) - } - - return user.Id, nil -} - -// getUserFactor looks for the active Okta factor of the specified type enrolled for a given user. -// -// Valid factor types include: -// "Google TOTP" (Google Authenticator) -// "Okta TOTP" (Okta Verify app time-based token) -// "Push" (Okta Verify app push) -// "SMS" -// "Call" -// "Email" -func (o *OktaMFAPlugin) getUserFactor(oktaUserId string, factorType string, trackingId string) (factor *Factor, err error) { - factorEvent := ssas.Event{Op: "FindOktaUserFactors", UserID: oktaUserId, TrackingID: trackingId} - ssas.OperationStarted(factorEvent) - - factorUrl := fmt.Sprintf("%s/api/v1/users/%s/factors", okta.OktaBaseUrl, oktaUserId) - - _, body, err := o.oktaRequest("GET", factorUrl, nil) - if err != nil { - factorEvent.Help = err.Error() - ssas.OperationFailed(factorEvent) - return factor, err - } - - var factors []Factor - if err = json.Unmarshal(body, &factors); err != nil { - factorEvent.Help = fmt.Sprintf("unexpected response format; response: %s", string(body)) - ssas.OperationFailed(factorEvent) - return factor, errors.New(factorEvent.Help) - } - - for _, f := range factors { - if f.Status != "ACTIVE" { - continue - } - - t := strings.ToLower(factorType) - - switch { - case t == "google totp" && f.Type == "token:software:totp" && f.Provider == "GOOGLE": - ssas.OperationSucceeded(factorEvent) - return &f, nil - case t == "okta totp" && f.Type == "token:software:totp" && f.Provider == "OKTA": - ssas.OperationSucceeded(factorEvent) - return &f, nil - case t == string(f.Type): - ssas.OperationSucceeded(factorEvent) - return &f, nil - default: - continue - } - } - - factorEvent.Help = fmt.Sprintf("no active factor of requested type %s found", factorType) - ssas.OperationFailed(factorEvent) - return factor, errors.New(factorEvent.Help) -} - -func (o *OktaMFAPlugin) postFactorChallenge(oktaUserId string, oktaFactor Factor, trackingId string) (factorRequest *FactorResponse, err error) { - requestEvent := ssas.Event{Op: "PostOktaFactorChallenge", UserID: oktaUserId, TrackingID: trackingId} - ssas.OperationStarted(requestEvent) - - requestUrl := fmt.Sprintf("%s/api/v1/users/%s/factors/%s/verify", okta.OktaBaseUrl, oktaUserId, oktaFactor.Id) - _, body, err := o.oktaRequest("POST", requestUrl, nil) - if err != nil { - requestEvent.Help = err.Error() - ssas.OperationFailed(requestEvent) - return factorRequest, err - } - - f := FactorResponse{} - if err = json.Unmarshal(body, &f); err != nil { - requestEvent.Help = fmt.Sprintf("unexpected response format; response: %s", string(body)) - ssas.OperationFailed(requestEvent) - return factorRequest, errors.New(requestEvent.Help) - } - - ssas.OperationSucceeded(requestEvent) - return &f, nil -} - -func (o *OktaMFAPlugin) postFactorResponse(oktaUserId string, oktaFactor Factor, passcode string, trackingId string) (factorRequest *FactorResponse, err error) { - requestEvent := ssas.Event{Op: "PostOktaFactorResponse", UserID: oktaUserId, TrackingID: trackingId} - ssas.OperationStarted(requestEvent) - - requestUrl := fmt.Sprintf("%s/api/v1/users/%s/factors/%s/verify", okta.OktaBaseUrl, oktaUserId, oktaFactor.Id) - requestBody := strings.NewReader(fmt.Sprintf(`{"passCode":"%s"}`, passcode)) - _, body, err := o.oktaRequest("POST", requestUrl, requestBody) - if err != nil { - requestEvent.Help = err.Error() - ssas.OperationFailed(requestEvent) - return factorRequest, err - } - - f := FactorResponse{} - if err = json.Unmarshal(body, &f); err != nil { - requestEvent.Help = fmt.Sprintf("unexpected response format; response: %s", string(body)) - ssas.OperationFailed(requestEvent) - return factorRequest, errors.New(requestEvent.Help) - } - - ssas.OperationSucceeded(requestEvent) - return &f, nil -} - -// oktaRequest consolidates the common steps of requesting and parsing Okta queries -func (o *OktaMFAPlugin) oktaRequest(verb, url string, requestBody io.Reader, ) (resp *http.Response, body []byte, err error) { - req, err := http.NewRequest(verb, url, requestBody) - if err != nil { - err = errors.New("unable to create request: " + err.Error()) - return - } - - okta.AddRequestHeaders(req) - resp, err = o.Client.Do(req) - if err != nil { - err = errors.New("request error: " + err.Error()) - return - } - - body, err = ioutil.ReadAll(resp.Body) - if err != nil { - err = fmt.Errorf(fmt.Sprintf("unexpected status code %d; unable to read response body", resp.StatusCode)) - return - } - - if resp.StatusCode >= 400 { - oktaError, e := okta.ParseOktaError(body) - if e == nil { - err = fmt.Errorf(fmt.Sprintf("error received, HTTP response code %d, Okta error %s: %s", - resp.StatusCode, oktaError.ErrorCode, oktaError.ErrorSummary)) - return - } - } - - if resp.StatusCode != 200 && resp.StatusCode != 201 { - err = fmt.Errorf(fmt.Sprintf("unexpected status code %d; response: %s", resp.StatusCode, string(body))) - return - } - - return -} - -// parsePushTransaction returns the Okta transaction ID for a Push factor request -func parsePushTransaction(url string) string { - re := regexp.MustCompile(`/transactions/(.*)$`) - matches := re.FindSubmatch([]byte(url)) - if len(matches) > 1 { - return string(matches[1]) - } - - return "" -} - -func generateOktaTransactionId() (string, error) { - randomPart, err := randomCharacters(22) - if err != nil { - return "", errors.New("unable to generate random characters") - } - - return "v2mst." + randomPart, nil -} - -func randomCharacters(length int) (string, error) { - chars := []byte("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_") - randomBytes := make([]byte, length) - for i := 0; i < length; i++ { - bign, err := rand.Int(rand.Reader, big.NewInt(int64(len(chars)))) - if err != nil { - return "", errors.New("unable to generate random number") - } - n := bign.Int64() - randomBytes[i] = chars[n] - } - return string(randomBytes), nil -} diff --git a/ssas/service/public/oktamfa_test.go b/ssas/service/public/oktamfa_test.go deleted file mode 100644 index f0d64d090..000000000 --- a/ssas/service/public/oktamfa_test.go +++ /dev/null @@ -1,784 +0,0 @@ -package public - -import ( - "bytes" - "encoding/json" - "fmt" - "github.com/CMSgov/bcda-app/ssas/okta" - "github.com/pborman/uuid" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/stretchr/testify/suite" - "io/ioutil" - "net/http" - "regexp" - "strings" - "testing" - "time" -) - -type OTestSuite struct { - suite.Suite -} - -type TestResponse struct { - GiveAnswer func(*http.Request) bool - Response *http.Response -} - -func (s *OTestSuite) TestPostFactorResponseInvalidToken() { - trackingId := uuid.NewRandom().String() - userId := "abc123" - passcode := "not_a_passcode" - factor := Factor{Id: "123abc", Type: "call"} - client := okta.NewTestClient(func(req *http.Request) *http.Response { - assert.Equal(s.T(), req.URL.String(), okta.OktaBaseUrl + "/api/v1/users/" + userId + "/factors/" + factor.Id + "/verify") - b, err := ioutil.ReadAll(req.Body) - assert.Nil(s.T(), err) - assert.Equal(s.T(), fmt.Sprintf(`{"passCode":"%s"}`, passcode), string(b)) - return testHttpResponse(401, `{"errorCode":"E0000011","errorSummary":"Invalid token provided","errorLink":"E0000011","errorId":"oaeBRew9Pp3Rq-6jNarZEpmAg","errorCauses":[]}`) - }) - - o := NewOktaMFA(client) - factorResponse, err := o.postFactorResponse(userId, factor, passcode, trackingId) - assert.NotNil(s.T(), err) - assert.Nil(s.T(), factorResponse) -} - -func (s *OTestSuite) TestPostFactorResponseInvalidPasscode() { - trackingId := uuid.NewRandom().String() - userId := "abc123" - passcode := "not_a_passcode" - factor := Factor{Id: "123abc", Type: "call"} - client := okta.NewTestClient(func(req *http.Request) *http.Response { - assert.Equal(s.T(), req.URL.String(), okta.OktaBaseUrl + "/api/v1/users/" + userId + "/factors/" + factor.Id + "/verify") - b, err := ioutil.ReadAll(req.Body) - assert.Nil(s.T(), err) - assert.Equal(s.T(), fmt.Sprintf(`{"passCode":"%s"}`, passcode), string(b)) - return testHttpResponse(403, `{"errorCode":"E0000068","errorSummary":"Invalid Passcode/Answer","errorLink":"E0000068","errorId":"oaeZAX9ava1RYS8lWNksxtqeg","errorCauses":[{"errorSummary":"Your token doesn't match our records. Please try again."}]}`) - }) - - o := NewOktaMFA(client) - factorResponse, err := o.postFactorResponse(userId, factor, passcode, trackingId) - assert.NotNil(s.T(), err) - assert.Nil(s.T(), factorResponse) -} - -func (s *OTestSuite) TestPostFactorResponseSuccess() { - trackingId := uuid.NewRandom().String() - userId := "abc123" - passcode := "not_a_passcode" - factor := Factor{Id: "123abc", Type: "call"} - client := okta.NewTestClient(func(req *http.Request) *http.Response { - assert.Equal(s.T(), req.URL.String(), okta.OktaBaseUrl + "/api/v1/users/" + userId + "/factors/" + factor.Id + "/verify") - b, err := ioutil.ReadAll(req.Body) - assert.Nil(s.T(), err) - assert.Equal(s.T(), fmt.Sprintf(`{"passCode":"%s"}`, passcode), string(b)) - return testHttpResponse(200, `{"factorResult":"SUCCESS"}`) - }) - - o := NewOktaMFA(client) - factorResponse, err := o.postFactorResponse(userId, factor, passcode, trackingId) - if err != nil || factorResponse == nil { - assert.FailNow(s.T(), "no response object returned") - } - assert.Equal(s.T(), "SUCCESS", factorResponse.Result) -} - -func (s *OTestSuite) TestPostFactorChallengeSuccess() { - trackingId := uuid.NewRandom().String() - userId := "abc123" - factor := Factor{Id: "123abc", Type: "call"} - client := okta.NewTestClient(func(req *http.Request) *http.Response { - assert.Equal(s.T(), req.URL.String(), okta.OktaBaseUrl + "/api/v1/users/" + userId + "/factors/" + factor.Id + "/verify") - return testHttpResponse(200, `{"factorResult":"CHALLENGE","_links":{"verify":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123/factors/123abc/verify","hints":{"allow":["POST"]}},"factor":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123/factors/123abc","hints":{"allow":["GET","DELETE"]}}}}`) - }) - - o := NewOktaMFA(client) - factorVerification, err := o.postFactorChallenge(userId, factor, trackingId) - if err != nil || factorVerification == nil { - s.FailNow("factor result not parsed") - } - assert.Equal(s.T(), "CHALLENGE", factorVerification.Result) -} - -func (s *OTestSuite) TestPostFactorChallengePushSuccess() { - trackingId := uuid.NewRandom().String() - userId := "abc123" - factor := Factor{Id: "123mno", Type: "push"} - client := okta.NewTestClient(func(req *http.Request) *http.Response { - assert.Equal(s.T(), req.URL.String(), okta.OktaBaseUrl + "/api/v1/users/" + userId + "/factors/" + factor.Id + "/verify") - return testHttpResponse(200, `{"factorResult":"WAITING","profile":{"credentialId":"bcda_user1@cms.gov","deviceType":"SmartPhone_IPhone","keys":[{"kty":"PKIX","use":"sig","kid":"default","x5c":["MIIBI..."]}],"name":"User’s iPhone","platform":"IOS","version":"12.1.2"},"expiresAt":"2019-07-12T14:21:30.000Z","_links":{"cancel":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123/factors/123mno/transactions/v2mst.WmiSGGkvQc6P-QUQ5Qy0jg","hints":{"allow":["DELETE"]}},"poll":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123/factors/123mno/transactions/v2mst.WmiSGGkvQc6P-QUQ5Qy0jg","hints":{"allow":["GET"]}}}}`) - }) - - o := NewOktaMFA(client) - factorVerification, err := o.postFactorChallenge(userId, factor, trackingId) - if err != nil || factorVerification == nil { - s.FailNow("factor result not parsed") - } - expectedTime, err := time.Parse("2006-01-02T15:04:05.999Z", "2019-07-12T14:21:30.000Z") - assert.Nil(s.T(), err) - assert.Equal(s.T(), expectedTime, factorVerification.ExpiresAt) - assert.Equal(s.T(), "WAITING", factorVerification.Result) - assert.Equal(s.T(), "https://cms-sandbox.oktapreview.com/api/v1/users/abc123/factors/123mno/transactions/v2mst.WmiSGGkvQc6P-QUQ5Qy0jg", factorVerification.Links.Poll.Href) -} - -func (s *OTestSuite) TestPostFactorChallengeFactorNotFound() { - trackingId := uuid.NewRandom().String() - userId := "abc123" - factor := Factor{Id: "nonexistent_factor", Type: "call"} - client := okta.NewTestClient(func(req *http.Request) *http.Response { - assert.Equal(s.T(), req.URL.String(), okta.OktaBaseUrl + "/api/v1/users/" + userId + "/factors/" + factor.Id + "/verify") - return testHttpResponse(404, `{"errorCode":"E0000007","errorSummary":"Not found: Resource not found: nonexistent_factor (UserFactor)","errorLink":"E0000007","errorId":"oaeTd-sjkYlSuKXMVPzEb4okw","errorCauses":[]}`) - }) - - o := NewOktaMFA(client) - factorVerification, err := o.postFactorChallenge(userId, factor, trackingId) - assert.Empty(s.T(), factorVerification) - if err == nil { - s.FailNow("postFactorChallenge() should fail on invalid factor ID") - } - assert.Contains(s.T(), err.Error(), "Resource not found") -} - -func (s *OTestSuite) TestPostPasswordSuccess() { - trackingId := uuid.NewRandom().String() - userId := "abc123" - password, err := randomCharacters(16) - require.Nil(s.T(), err) - client := okta.NewTestClient(func(req *http.Request) *http.Response { - assert.Equal(s.T(), req.URL.String(), okta.OktaBaseUrl + "/api/v1/authn") - b, err := ioutil.ReadAll(req.Body) - assert.Nil(s.T(), err) - assert.Equal(s.T(), fmt.Sprintf(`{"username":"%s","password":"%s"}`, userId, password), string(b)) - return testHttpResponse(200, `{"stateToken":"00zABCabc123-000","expiresAt":"2019-09-16T19:28:58.000Z","status":"MFA_REQUIRED","_embedded":{"user":{"id":"abc123","passwordChanged":"2019-07-23T20:03:32.000Z","profile":{"login":"bcda_user2@cms.gov","firstName":"ACO","lastName":"User2","locale":"en","timeZone":"America/Los_Angeles"}},"factors":[{"id":"123ghi","factorType":"sms","provider":"OKTA","vendorName":"OKTA","profile":{"phoneNumber":"+1 XXX-XXX-7922"},"_links":{"verify":{"href":"https://cms-sandbox.oktapreview.com/api/v1/authn/factors/123ghi/verify","hints":{"allow":["POST"]}}}}],"policy":{"allowRememberDevice":true,"rememberDeviceLifetimeInMinutes":30,"rememberDeviceByDefault":false,"factorsPolicyInfo":{}}},"_links":{"cancel":{"href":"https://cms-sandbox.oktapreview.com/api/v1/authn/cancel","hints":{"allow":["POST"]}}}}`) - }) - - o := NewOktaMFA(client) - passwordResponse, err := o.postPassword(userId, password, trackingId) - if err != nil || passwordResponse == nil { - assert.FailNow(s.T(), "no response object returned") - } - assert.Equal(s.T(), "MFA_REQUIRED", passwordResponse.Status) -} - -func (s *OTestSuite) TestPostPasswordFailure() { - trackingId := uuid.NewRandom().String() - userId := "abc123" - password, err := randomCharacters(16) - require.Nil(s.T(), err) - client := okta.NewTestClient(func(req *http.Request) *http.Response { - assert.Equal(s.T(), req.URL.String(), okta.OktaBaseUrl + "/api/v1/authn") - b, err := ioutil.ReadAll(req.Body) - assert.Nil(s.T(), err) - assert.Equal(s.T(), fmt.Sprintf(`{"username":"%s","password":"%s"}`, userId, password), string(b)) - return testHttpResponse(401, `{"errorCode":"E0000004","errorSummary":"Authentication failed","errorLink":"E0000004","errorId":"oae_JnAectySpajMuJA3dqs7g","errorCauses":[]}`) - }) - - o := NewOktaMFA(client) - passwordResponse, err := o.postPassword(userId, password, trackingId) - if err != nil || passwordResponse == nil { - assert.FailNow(s.T(), "no response object returned") - } - assert.Equal(s.T(), "AUTHENTICATION_FAILED", passwordResponse.Status) -} - -func (s *OTestSuite) TestPostPasswordBadRequest() { - trackingId := uuid.NewRandom().String() - userId := "abc123" - password := "" - client := okta.NewTestClient(func(req *http.Request) *http.Response { - assert.Equal(s.T(), req.URL.String(), okta.OktaBaseUrl + "/api/v1/authn") - b, err := ioutil.ReadAll(req.Body) - assert.Nil(s.T(), err) - assert.Equal(s.T(), fmt.Sprintf(`{"username":"%s","password":""}`, userId), string(b)) - return testHttpResponse(400, `{"errorCode":"E0000001","errorSummary":"Api validation failed: authRequest","errorLink":"E0000001","errorId":"oaebM8KyzEVS5uMiImx4sHzGg","errorCauses":[{"errorSummary":"The 'username' and 'password' attributes are required in this context."}]}`) - }) - - o := NewOktaMFA(client) - passwordResponse, err := o.postPassword(userId, password, trackingId) - assert.NotNil(s.T(), err) - assert.Nil(s.T(), passwordResponse) -} - -func (s *OTestSuite) TestGetUserHeaders() { - trackingId := uuid.NewRandom().String() - client := okta.NewTestClient(func(req *http.Request) *http.Response { - assert.Contains(s.T(), req.Header.Get("Authorization"), "SSWS") - assert.Equal(s.T(), req.Header.Get("Content-Type"), "application/json") - assert.Equal(s.T(), req.Header.Get("Accept"), "application/json") - return testHttpResponse(200, `[]`) - }) - - o := NewOktaMFA(client) - _, _ = o.getUser("nonexistent_user", trackingId) -} - -func (s *OTestSuite) TestGetUserSuccess() { - trackingId := uuid.NewRandom().String() - expectedUserId := "abc123" - searchString := "a_user@cms.gov" - client := okta.NewTestClient(func(req *http.Request) *http.Response { - assert.Equal(s.T(), req.URL.String(), okta.OktaBaseUrl + "/api/v1/users/?q=" + searchString) - return testHttpResponse(200, `[{"id":"abc123","status":"ACTIVE","created":"2018-12-05T19:48:17.000Z","activated":"2018-12-05T19:48:17.000Z","statusChanged":"2019-06-04T12:52:49.000Z","lastLogin":"2019-06-06T18:45:35.000Z","lastUpdated":"2019-06-04T12:52:49.000Z","passwordChanged":"2019-06-04T12:52:49.000Z","profile":{"firstName":"Test","lastName":"User","mobilePhone":null,"addressType":"Select Type...","secondEmail":"a_user@cms.gov","login":"a_user@cms.gov","email":"a_user@cms.gov","LOA":"3"},"credentials":{"password":{},"emails":[{"value":"a_user@cms.gov","status":"VERIFIED","type":"PRIMARY"},{"value":"a_user@cms.gov","status":"VERIFIED","type":"SECONDARY"}],"provider":{"type":"OKTA","name":"OKTA"}},"_links":{"self":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123"}}}]`) - }) - - o := NewOktaMFA(client) - foundUserId, err := o.getUser(searchString, trackingId) - assert.Nil(s.T(), err) - assert.Equal(s.T(), expectedUserId, foundUserId) -} - -func (s *OTestSuite) TestGetUserBadStatusCode() { - trackingId := uuid.NewRandom().String() - client := okta.NewTestClient(func(req *http.Request) *http.Response { - return testHttpResponse(404, "") - }) - - o := NewOktaMFA(client) - foundUserId, err := o.getUser("user_irrelevant", trackingId) - if err == nil { - s.FailNow("getUser() should fail unless status code = 200") - } - assert.Contains(s.T(), err.Error(), "status code") - assert.Equal(s.T(), "", foundUserId) -} - -func (s *OTestSuite) TestGetUserNotLOA3() { - trackingId := uuid.NewRandom().String() - searchString := "a_user@cms.gov" - client := okta.NewTestClient(func(req *http.Request) *http.Response { - return testHttpResponse(200, `[{"id":"abc123","status":"ACTIVE","created":"2018-12-05T19:48:17.000Z","activated":"2018-12-05T19:48:17.000Z","statusChanged":"2019-06-04T12:52:49.000Z","lastLogin":"2019-06-06T18:45:35.000Z","lastUpdated":"2019-06-04T12:52:49.000Z","passwordChanged":"2019-06-04T12:52:49.000Z","profile":{"firstName":"Test","lastName":"User","mobilePhone":null,"addressType":"Select Type...","secondEmail":"a_user@cms.gov","login":"a_user@cms.gov","email":"a_user@cms.gov"},"credentials":{"password":{},"emails":[{"value":"a_user@cms.gov","status":"VERIFIED","type":"PRIMARY"},{"value":"a_user@cms.gov","status":"VERIFIED","type":"SECONDARY"}],"provider":{"type":"OKTA","name":"OKTA"}},"_links":{"self":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123"}}}]`) - }) - - o := NewOktaMFA(client) - foundUserId, err := o.getUser(searchString, trackingId) - if err == nil { - s.FailNow("getUser() should fail unless LOA=3") - } - assert.Contains(s.T(), err.Error(), "LOA") - assert.Equal(s.T(), "", foundUserId) -} - -func (s *OTestSuite) TestGetUserNotActive() { - trackingId := uuid.NewRandom().String() - searchString := "a_user@cms.gov" - client := okta.NewTestClient(func(req *http.Request) *http.Response { - return testHttpResponse(200, `[{"id":"abc123","status":"STAGED","created":"2018-12-05T19:48:17.000Z","activated":"2018-12-05T19:48:17.000Z","statusChanged":"2019-06-04T12:52:49.000Z","lastLogin":"2019-06-06T18:45:35.000Z","lastUpdated":"2019-06-04T12:52:49.000Z","passwordChanged":"2019-06-04T12:52:49.000Z","profile":{"firstName":"Test","lastName":"User","mobilePhone":null,"addressType":"Select Type...","secondEmail":"a_user@cms.gov","login":"a_user@cms.gov","email":"a_user@cms.gov","LOA":"3"},"credentials":{"password":{},"emails":[{"value":"a_user@cms.gov","status":"VERIFIED","type":"PRIMARY"},{"value":"a_user@cms.gov","status":"VERIFIED","type":"SECONDARY"}],"provider":{"type":"OKTA","name":"OKTA"}},"_links":{"self":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123"}}}]`) - }) - - o := NewOktaMFA(client) - foundUserId, err := o.getUser(searchString, trackingId) - if err == nil { - s.FailNow("getUser() should fail unless status=ACTIVE") - } - assert.Contains(s.T(), err.Error(), "active") - assert.Equal(s.T(), "", foundUserId) -} - -func (s *OTestSuite) TestGetUserMultipleUsers() { - trackingId := uuid.NewRandom().String() - searchString := "a_user" - client := okta.NewTestClient(func(req *http.Request) *http.Response { - return testHttpResponse(200, `[{"id":"def456","status":"ACTIVE","created":"2019-06-04T13:21:06.000Z","activated":"2019-06-04T13:21:07.000Z","statusChanged":"2019-06-04T13:21:07.000Z","lastLogin":null,"lastUpdated":"2019-06-04T13:21:07.000Z","passwordChanged":"2019-06-04T13:21:07.000Z","profile":{"firstName":"Test2","lastName":"User","mobilePhone":null,"secondEmail":"","login":"bcda_user1","email":"bcda_user1@cms.gov"},"credentials":{"password":{},"emails":[{"value":"bcda_user1@cms.gov","status":"VERIFIED","type":"PRIMARY"},{"value":"","status":"VERIFIED","type":"SECONDARY"}],"provider":{"type":"OKTA","name":"OKTA"}},"_links":{"self":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/def456"}}},{"id":"ghi789","status":"STAGED","created":"2019-06-04T13:22:55.000Z","activated":null,"statusChanged":null,"lastLogin":null,"lastUpdated":"2019-06-04T16:34:21.000Z","passwordChanged":null,"profile":{"firstName":"Test3","lastName":"User","aco_ids":["A0000","A0001"],"mobilePhone":null,"addressType":"Select Type...","secondEmail":null,"login":"bcda_user3@cms.gov","email":"bcda_user3@cms.gov","LOA":"Select Level..."},"credentials":{"emails":[{"value":"bcda_user3@cms.gov","status":"VERIFIED","type":"PRIMARY"}],"provider":{"type":"OKTA","name":"OKTA"}},"_links":{"self":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/ghi789"}}}]`) - }) - - o := NewOktaMFA(client) - foundUserId, err := o.getUser(searchString, trackingId) - if err == nil { - s.FailNow("getUser() should fail unless a single user matches the search string") - } - assert.Contains(s.T(), err.Error(), "multiple") - assert.Equal(s.T(), "", foundUserId) -} - -func (s *OTestSuite) TestGetUserNoUsers() { - trackingId := uuid.NewRandom().String() - searchString := "no_match_expected" - client := okta.NewTestClient(func(req *http.Request) *http.Response { - return testHttpResponse(200, `[]`) - }) - - o := NewOktaMFA(client) - foundUserId, err := o.getUser(searchString, trackingId) - if err == nil { - s.FailNow("getUser() should fail unless a user matches the search string") - } - assert.Contains(s.T(), err.Error(), "not found") - assert.Equal(s.T(), "", foundUserId) -} - -func (s *OTestSuite) TestGetUserBadToken() { - trackingId := uuid.NewRandom().String() - searchString := "no_match_expected" - client := okta.NewTestClient(func(req *http.Request) *http.Response { - return testHttpResponse(401, `{"errorCode":"E0000011","errorSummary":"Invalid token provided","errorLink":"E0000011","errorId":"oae3iIXhkQVQ2izGNwhnR47JQ","errorCauses":[]}`) - }) - - o := NewOktaMFA(client) - foundUserId, err := o.getUser(searchString, trackingId) - assert.NotNil(s.T(), err) - assert.Contains(s.T(), err.Error(), "Invalid token provided") - assert.Empty(s.T(), foundUserId) -} - -func (s *OTestSuite) TestGetUserFactorSuccess() { - trackingId := uuid.NewRandom().String() - userId := "abc123" - factorType := "SMS" - client := okta.NewTestClient(func(req *http.Request) *http.Response { - assert.Equal(s.T(), req.URL.String(), okta.OktaBaseUrl + "/api/v1/users/" + userId + "/factors") - return testHttpResponse(200, `[{"id":"123abc","factorType":"call","provider":"OKTA","vendorName":"OKTA","status":"ACTIVE","created":"2019-06-05T14:13:57.000Z","lastUpdated":"2019-06-05T14:13:57.000Z","profile":{"phoneNumber":"+15555555555"},"_links":{"self":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123/factors/123abc","hints":{"allow":["GET","DELETE"]}},"verify":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123/factors/123abc/verify","hints":{"allow":["POST"]}},"user":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123","hints":{"allow":["GET"]}}}},{"id":"123def","factorType":"email","provider":"OKTA","vendorName":"OKTA","status":"ACTIVE","profile":{"email":"a_user@cms.gov"},"_links":{"self":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123/factors/123def","hints":{"allow":["GET","DELETE"]}},"verify":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123/factors/123def/verify","hints":{"allow":["POST"]}},"user":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123","hints":{"allow":["GET"]}}}},{"id":"123ghi","factorType":"sms","provider":"OKTA","vendorName":"OKTA","status":"ACTIVE","created":"2019-06-05T14:10:19.000Z","lastUpdated":"2019-06-05T14:10:19.000Z","profile":{"phoneNumber":"+15555555555"},"_links":{"self":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123/factors/123ghi","hints":{"allow":["GET","DELETE"]}},"verify":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123/factors/123ghi/verify","hints":{"allow":["POST"]}},"user":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123","hints":{"allow":["GET"]}}}},{"id":"123jkl","factorType":"token:software:totp","provider":"GOOGLE","vendorName":"GOOGLE","status":"ACTIVE","created":"2018-12-05T20:38:23.000Z","lastUpdated":"2018-12-05T20:38:47.000Z","profile":{"credentialId":"a_user@cms.gov"},"_links":{"self":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123/factors/123jkl","hints":{"allow":["GET","DELETE"]}},"verify":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123/factors/123jkl/verify","hints":{"allow":["POST"]}},"user":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123","hints":{"allow":["GET"]}}}},{"id":"123mno","factorType":"push","provider":"OKTA","vendorName":"OKTA","status":"ACTIVE","created":"2019-01-03T18:18:52.000Z","lastUpdated":"2019-01-03T18:19:04.000Z","profile":{"credentialId":"a_user@cms.gov","deviceType":"SmartPhone_IPhone","keys":[{"kty":"PKIX","use":"sig","kid":"default","x5c":["MIIBI..."]}],"name":"A User’s iPhone","platform":"IOS","version":"12.1.2"},"_links":{"self":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123/factors/123mno","hints":{"allow":["GET","DELETE"]}},"verify":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123/factors/123mno/verify","hints":{"allow":["POST"]}},"user":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123","hints":{"allow":["GET"]}}}},{"id":"123pqr","factorType":"token:software:totp","provider":"OKTA","vendorName":"OKTA","status":"ACTIVE","created":"2019-01-03T18:18:52.000Z","lastUpdated":"2019-01-03T18:19:04.000Z","profile":{"credentialId":"a_user@cms.gov"},"_links":{"self":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123/factors/123pqr","hints":{"allow":["GET","DELETE"]}},"verify":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123/factors/123pqr/verify","hints":{"allow":["POST"]}},"user":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123","hints":{"allow":["GET"]}}}}]`) - }) - - o := NewOktaMFA(client) - factor, err := o.getUserFactor(userId, factorType, trackingId) - assert.Nil(s.T(), err) - if factor == nil { - s.FailNow("getUserFactor() should successfully return a factor") - } - assert.Equal(s.T(), "123ghi", factor.Id) - assert.Equal(s.T(), "sms", factor.Type) -} - -func (s *OTestSuite) TestGetUserFactorAllTypes() { - trackingId := uuid.NewRandom().String() - userId := "abc123" - client := okta.NewTestClient(func(req *http.Request) *http.Response { - return testHttpResponse(200, `[{"id":"123abc","factorType":"call","provider":"OKTA","vendorName":"OKTA","status":"ACTIVE","created":"2019-06-05T14:13:57.000Z","lastUpdated":"2019-06-05T14:13:57.000Z","profile":{"phoneNumber":"+15555555555"},"_links":{"self":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123/factors/123abc","hints":{"allow":["GET","DELETE"]}},"verify":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123/factors/123abc/verify","hints":{"allow":["POST"]}},"user":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123","hints":{"allow":["GET"]}}}},{"id":"123def","factorType":"email","provider":"OKTA","vendorName":"OKTA","status":"ACTIVE","profile":{"email":"a_user@cms.gov"},"_links":{"self":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123/factors/123def","hints":{"allow":["GET","DELETE"]}},"verify":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123/factors/123def/verify","hints":{"allow":["POST"]}},"user":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123","hints":{"allow":["GET"]}}}},{"id":"123ghi","factorType":"sms","provider":"OKTA","vendorName":"OKTA","status":"ACTIVE","created":"2019-06-05T14:10:19.000Z","lastUpdated":"2019-06-05T14:10:19.000Z","profile":{"phoneNumber":"+15555555555"},"_links":{"self":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123/factors/123ghi","hints":{"allow":["GET","DELETE"]}},"verify":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123/factors/123ghi/verify","hints":{"allow":["POST"]}},"user":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123","hints":{"allow":["GET"]}}}},{"id":"123jkl","factorType":"token:software:totp","provider":"GOOGLE","vendorName":"GOOGLE","status":"ACTIVE","created":"2018-12-05T20:38:23.000Z","lastUpdated":"2018-12-05T20:38:47.000Z","profile":{"credentialId":"a_user@cms.gov"},"_links":{"self":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123/factors/123jkl","hints":{"allow":["GET","DELETE"]}},"verify":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123/factors/123jkl/verify","hints":{"allow":["POST"]}},"user":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123","hints":{"allow":["GET"]}}}},{"id":"123mno","factorType":"push","provider":"OKTA","vendorName":"OKTA","status":"ACTIVE","created":"2019-01-03T18:18:52.000Z","lastUpdated":"2019-01-03T18:19:04.000Z","profile":{"credentialId":"a_user@cms.gov","deviceType":"SmartPhone_IPhone","keys":[{"kty":"PKIX","use":"sig","kid":"default","x5c":["MIIBI..."]}],"name":"A User’s iPhone","platform":"IOS","version":"12.1.2"},"_links":{"self":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123/factors/123mno","hints":{"allow":["GET","DELETE"]}},"verify":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123/factors/123mno/verify","hints":{"allow":["POST"]}},"user":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123","hints":{"allow":["GET"]}}}},{"id":"123pqr","factorType":"token:software:totp","provider":"OKTA","vendorName":"OKTA","status":"ACTIVE","created":"2019-01-03T18:18:52.000Z","lastUpdated":"2019-01-03T18:19:04.000Z","profile":{"credentialId":"a_user@cms.gov"},"_links":{"self":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123/factors/123pqr","hints":{"allow":["GET","DELETE"]}},"verify":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123/factors/123pqr/verify","hints":{"allow":["POST"]}},"user":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123","hints":{"allow":["GET"]}}}}]`) - }) - o := NewOktaMFA(client) - - factor, err := o.getUserFactor(userId, "Call", trackingId) - assert.Nil(s.T(), err) - if factor == nil { - s.FailNow("getUserFactor() should successfully return a factor") - } - assert.Equal(s.T(), "123abc", factor.Id) - assert.Equal(s.T(), "call", factor.Type) - - factor, err = o.getUserFactor(userId, "Email", trackingId) - assert.Nil(s.T(), err) - if factor == nil { - s.FailNow("getUserFactor() should successfully return a factor") - } - assert.Equal(s.T(), "123def", factor.Id) - assert.Equal(s.T(), "email", factor.Type) - - factor, err = o.getUserFactor(userId, "Google TOTP", trackingId) - assert.Nil(s.T(), err) - if factor == nil { - s.FailNow("getUserFactor() should successfully return a factor") - } - assert.Equal(s.T(), "123jkl", factor.Id) - assert.Equal(s.T(), "token:software:totp", factor.Type) - assert.Equal(s.T(), "GOOGLE", factor.Provider) - - factor, err = o.getUserFactor(userId, "OKTA TOTP", trackingId) - assert.Nil(s.T(), err) - if factor == nil { - s.FailNow("getUserFactor() should successfully return a factor") - } - assert.Equal(s.T(), "123pqr", factor.Id) - assert.Equal(s.T(), "token:software:totp", factor.Type) - assert.Equal(s.T(), "OKTA", factor.Provider) - - factor, err = o.getUserFactor(userId, "Push", trackingId) - assert.Nil(s.T(), err) - if factor == nil { - s.FailNow("getUserFactor() should successfully return a factor") - } - assert.Equal(s.T(), "123mno", factor.Id) - assert.Equal(s.T(), "push", factor.Type) -} - -func (s *OTestSuite) TestGetUserFactorInactive() { - trackingId := uuid.NewRandom().String() - userId := "abc123" - factorType := "Call" - client := okta.NewTestClient(func(req *http.Request) *http.Response { - assert.Equal(s.T(), req.URL.String(), okta.OktaBaseUrl + "/api/v1/users/" + userId + "/factors") - return testHttpResponse(200, `[{"id":"123abc","factorType":"call","provider":"OKTA","vendorName":"OKTA","status":"PENDING","created":"2019-06-05T14:13:57.000Z","lastUpdated":"2019-06-05T14:13:57.000Z","profile":{"phoneNumber":"+15555555555"},"_links":{"self":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123/factors/123abc","hints":{"allow":["GET","DELETE"]}},"verify":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123/factors/123abc/verify","hints":{"allow":["POST"]}},"user":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123","hints":{"allow":["GET"]}}}},{"id":"123def","factorType":"email","provider":"OKTA","vendorName":"OKTA","status":"ACTIVE","profile":{"email":"a_user@cms.gov"},"_links":{"self":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123/factors/123def","hints":{"allow":["GET","DELETE"]}},"verify":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123/factors/123def/verify","hints":{"allow":["POST"]}},"user":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123","hints":{"allow":["GET"]}}}},{"id":"123ghi","factorType":"sms","provider":"OKTA","vendorName":"OKTA","status":"ACTIVE","created":"2019-06-05T14:10:19.000Z","lastUpdated":"2019-06-05T14:10:19.000Z","profile":{"phoneNumber":"+15555555555"},"_links":{"self":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123/factors/123ghi","hints":{"allow":["GET","DELETE"]}},"verify":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123/factors/123ghi/verify","hints":{"allow":["POST"]}},"user":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123","hints":{"allow":["GET"]}}}},{"id":"123jkl","factorType":"token:software:totp","provider":"GOOGLE","vendorName":"GOOGLE","status":"ACTIVE","created":"2018-12-05T20:38:23.000Z","lastUpdated":"2018-12-05T20:38:47.000Z","profile":{"credentialId":"a_user@cms.gov"},"_links":{"self":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123/factors/123jkl","hints":{"allow":["GET","DELETE"]}},"verify":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123/factors/123jkl/verify","hints":{"allow":["POST"]}},"user":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123","hints":{"allow":["GET"]}}}},{"id":"123mno","factorType":"push","provider":"OKTA","vendorName":"OKTA","status":"ACTIVE","created":"2019-01-03T18:18:52.000Z","lastUpdated":"2019-01-03T18:19:04.000Z","profile":{"credentialId":"a_user@cms.gov","deviceType":"SmartPhone_IPhone","keys":[{"kty":"PKIX","use":"sig","kid":"default","x5c":["MIIBI..."]}],"name":"A User’s iPhone","platform":"IOS","version":"12.1.2"},"_links":{"self":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123/factors/123mno","hints":{"allow":["GET","DELETE"]}},"verify":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123/factors/123mno/verify","hints":{"allow":["POST"]}},"user":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123","hints":{"allow":["GET"]}}}},{"id":"123pqr","factorType":"token:software:totp","provider":"OKTA","vendorName":"OKTA","status":"ACTIVE","created":"2019-01-03T18:18:52.000Z","lastUpdated":"2019-01-03T18:19:04.000Z","profile":{"credentialId":"a_user@cms.gov"},"_links":{"self":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123/factors/123pqr","hints":{"allow":["GET","DELETE"]}},"verify":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123/factors/123pqr/verify","hints":{"allow":["POST"]}},"user":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123","hints":{"allow":["GET"]}}}}]`) - }) - - o := NewOktaMFA(client) - factor, err := o.getUserFactor(userId, factorType, trackingId) - assert.NotNil(s.T(), err) - assert.Nil(s.T(), factor) -} - -func (s *OTestSuite) TestGetUserFactorNotFound() { - trackingId := uuid.NewRandom().String() - userId := "abc123" - factorType := "Call" - client := okta.NewTestClient(func(req *http.Request) *http.Response { - assert.Equal(s.T(), req.URL.String(), okta.OktaBaseUrl + "/api/v1/users/" + userId + "/factors") - return testHttpResponse(200, `[{"id":"123def","factorType":"email","provider":"OKTA","vendorName":"OKTA","status":"ACTIVE","profile":{"email":"a_user@cms.gov"},"_links":{"self":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123/factors/123def","hints":{"allow":["GET","DELETE"]}},"verify":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123/factors/123def/verify","hints":{"allow":["POST"]}},"user":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123","hints":{"allow":["GET"]}}}},{"id":"123jkl","factorType":"token:software:totp","provider":"GOOGLE","vendorName":"GOOGLE","status":"ACTIVE","created":"2018-12-05T20:38:23.000Z","lastUpdated":"2018-12-05T20:38:47.000Z","profile":{"credentialId":"a_user@cms.gov"},"_links":{"self":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123/factors/123jkl","hints":{"allow":["GET","DELETE"]}},"verify":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123/factors/123jkl/verify","hints":{"allow":["POST"]}},"user":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123","hints":{"allow":["GET"]}}}},{"id":"123mno","factorType":"push","provider":"OKTA","vendorName":"OKTA","status":"ACTIVE","created":"2019-01-03T18:18:52.000Z","lastUpdated":"2019-01-03T18:19:04.000Z","profile":{"credentialId":"a_user@cms.gov","deviceType":"SmartPhone_IPhone","keys":[{"kty":"PKIX","use":"sig","kid":"default","x5c":["MIIBI..."]}],"name":"A User’s iPhone","platform":"IOS","version":"12.1.2"},"_links":{"self":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123/factors/123mno","hints":{"allow":["GET","DELETE"]}},"verify":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123/factors/123mno/verify","hints":{"allow":["POST"]}},"user":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123","hints":{"allow":["GET"]}}}},{"id":"123pqr","factorType":"token:software:totp","provider":"OKTA","vendorName":"OKTA","status":"ACTIVE","created":"2019-01-03T18:18:52.000Z","lastUpdated":"2019-01-03T18:19:04.000Z","profile":{"credentialId":"a_user@cms.gov"},"_links":{"self":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123/factors/123pqr","hints":{"allow":["GET","DELETE"]}},"verify":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123/factors/123pqr/verify","hints":{"allow":["POST"]}},"user":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123","hints":{"allow":["GET"]}}}}]`) - }) - - o := NewOktaMFA(client) - factor, err := o.getUserFactor(userId, factorType, trackingId) - assert.NotNil(s.T(), err) - assert.Nil(s.T(), factor) -} - -func (s *OTestSuite) TestGetUserFactorBadToken() { - trackingId := uuid.NewRandom().String() - userId := "abc123" - factorType := "Call" - client := okta.NewTestClient(func(req *http.Request) *http.Response { - return testHttpResponse(401, `{"errorCode":"E0000011","errorSummary":"Invalid token provided","errorLink":"E0000011","errorId":"oae3iIXhkQVQ2izGNwhnR47JQ","errorCauses":[]}`) - }) - - o := NewOktaMFA(client) - factor, err := o.getUserFactor(userId, factorType, trackingId) - assert.NotNil(s.T(), err) - assert.Contains(s.T(), err.Error(), "Invalid token provided") - assert.Nil(s.T(), factor) -} - -func (s *OTestSuite) TestGenerateOktaTransactionId() { - transactionId, err := generateOktaTransactionId() - assert.Nil(s.T(), err) - assert.True(s.T(), strings.HasPrefix(transactionId, "v2mst.")) - assert.Equal(s.T(), 28, len(transactionId)) -} - -func (s *OTestSuite) TestParsePushTransactionMatch() { - testUrl := "https://cms-sandbox.oktapreview.com/api/v1/users/abc123/factors/123mno/transactions/v2mst.WmiSGGkvQc6P-QUQ5Qy0jg" - transactionId := parsePushTransaction(testUrl) - assert.Equal(s.T(), "v2mst.WmiSGGkvQc6P-QUQ5Qy0jg", transactionId) -} - -func (s *OTestSuite) TestParsePushTransactionNoMatch() { - testUrl := "https://cms-sandbox.oktapreview.com/api/v1/users/abc123/factors/123mno" - transactionId := parsePushTransaction(testUrl) - assert.Equal(s.T(), "", transactionId) -} - -func (s *OTestSuite) TestFormatFactorReturnFailedSMSRequest() { - result := formatFactorReturn("SMS", &FactorReturn{}) - assert.NotEmpty(s.T(), result) - assert.Equal(s.T(), "request_sent", result.Action) - assert.Empty(s.T(), result.Transaction) -} - -func (s *OTestSuite) TestFormatFactorReturnFailedPushRequest() { - result := formatFactorReturn("Push", &FactorReturn{}) - assert.NotEmpty(s.T(), result) - assert.Equal(s.T(), "request_sent", result.Action) - assert.NotEmpty(s.T(), result.Transaction) - assert.NotEqual(s.T(), "", result.Transaction.TransactionID) -} - -func (s *OTestSuite) TestFormatFactorReturnSucceededSMSRequest() { - factorReturn := FactorReturn{Action: "request_sent"} - assert.NotEmpty(s.T(), factorReturn) - assert.Equal(s.T(), "request_sent", factorReturn.Action) - assert.Nil(s.T(), factorReturn.Transaction) -} - -func (s *OTestSuite) TestFormatFactorReturnSucceededPushRequest() { - transaction := Transaction{TransactionID: "any_id"} - f := FactorReturn{Action: "request_sent", Transaction: &transaction} - factorReturn := formatFactorReturn("Push", &f) - assert.NotEmpty(s.T(), factorReturn) - assert.Equal(s.T(), "request_sent", factorReturn.Action) - assert.Equal(s.T(), "any_id", factorReturn.Transaction.TransactionID) - assert.True(s.T(), factorReturn.Transaction.ExpiresAt.After(time.Now())) -} - -func (s *OTestSuite) TestVerifyFactorChallengeInvalidFactor() { - trackingId := uuid.NewRandom().String() - o := NewOktaMFA(nil) - success, oktaID, groupIDs := o.VerifyFactorChallenge("bcda_user@cms.gov", "badFactor", "badPasscode", trackingId) - assert.False(s.T(), success) - assert.Equal(s.T(), "", oktaID) - assert.Len(s.T(), groupIDs, 0) -} - -func (s *OTestSuite) TestVerifyFactorChallengeUserNotFound() { - trackingId := uuid.NewRandom().String() - responses := []TestResponse{ - newTestResponse(isGetUser(), 200, `[]`), - } - client := okta.NewTestClient(testHttpResponses(responses)) - o := NewOktaMFA(client) - success, oktaID, groupIDs := o.VerifyFactorChallenge("bcda_user@cms.gov", "sms", "badPasscode", trackingId) - assert.False(s.T(), success) - assert.Equal(s.T(), "", oktaID) - assert.Len(s.T(), groupIDs, 0) -} - -func (s *OTestSuite) TestVerifyFactorChallengeFactorNotFound() { - trackingId := uuid.NewRandom().String() - responses := []TestResponse{ - newTestResponse(isGetUser(), 200, `[{"id":"abc123","status":"ACTIVE","created":"2018-12-05T19:48:17.000Z","activated":"2018-12-05T19:48:17.000Z","statusChanged":"2019-06-04T12:52:49.000Z","lastLogin":"2019-06-06T18:45:35.000Z","lastUpdated":"2019-06-04T12:52:49.000Z","passwordChanged":"2019-06-04T12:52:49.000Z","profile":{"firstName":"Test","lastName":"User","mobilePhone":null,"addressType":"Select Type...","secondEmail":"bcda_user@cms.gov","login":"a_user@cms.gov","email":"a_user@cms.gov","LOA":"3"},"credentials":{"password":{},"emails":[{"value":"a_user@cms.gov","status":"VERIFIED","type":"PRIMARY"},{"value":"a_user@cms.gov","status":"VERIFIED","type":"SECONDARY"}],"provider":{"type":"OKTA","name":"OKTA"}},"_links":{"self":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123"}}}]`), - newTestResponse(isGetFactor(), 200, `[]`), - } - client := okta.NewTestClient(testHttpResponses(responses)) - o := NewOktaMFA(client) - success, oktaID, groupIDs := o.VerifyFactorChallenge("bcda_user@cms.gov", "sms", "badPasscode", trackingId) - assert.False(s.T(), success) - assert.Equal(s.T(), "abc123", oktaID) - assert.Len(s.T(), groupIDs, 0) -} - -func (s *OTestSuite) TestVerifyFactorChallengePasscodeError() { - trackingId := uuid.NewRandom().String() - responses := []TestResponse{ - newTestResponse(isGetUser(), 200, `[{"id":"abc123","status":"ACTIVE","created":"2018-12-05T19:48:17.000Z","activated":"2018-12-05T19:48:17.000Z","statusChanged":"2019-06-04T12:52:49.000Z","lastLogin":"2019-06-06T18:45:35.000Z","lastUpdated":"2019-06-04T12:52:49.000Z","passwordChanged":"2019-06-04T12:52:49.000Z","profile":{"firstName":"Test","lastName":"User","mobilePhone":null,"addressType":"Select Type...","secondEmail":"bcda_user@cms.gov","login":"a_user@cms.gov","email":"a_user@cms.gov","LOA":"3"},"credentials":{"password":{},"emails":[{"value":"a_user@cms.gov","status":"VERIFIED","type":"PRIMARY"},{"value":"a_user@cms.gov","status":"VERIFIED","type":"SECONDARY"}],"provider":{"type":"OKTA","name":"OKTA"}},"_links":{"self":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123"}}}]`), - newTestResponse(isGetFactor(), 200, `[{"id":"123abc","factorType":"call","provider":"OKTA","vendorName":"OKTA","status":"ACTIVE","created":"2019-06-05T14:13:57.000Z","lastUpdated":"2019-06-05T14:13:57.000Z","profile":{"phoneNumber":"+15555555555"},"_links":{"self":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123/factors/123abc","hints":{"allow":["GET","DELETE"]}},"verify":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123/factors/123abc/verify","hints":{"allow":["POST"]}},"user":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123","hints":{"allow":["GET"]}}}}]`), - newTestResponse(isPostFactorChallenge(), 400, `{"errorCode":"E0000001","errorSummary":"Api validation failed: authRequest","errorLink":"E0000001","errorId":"oaebM8KyzEVS5uMiImx4sHzGg","errorCauses":[{"errorSummary":"The 'username' and 'password' attributes are required in this context."}]}`), - } - client := okta.NewTestClient(testHttpResponses(responses)) - o := NewOktaMFA(client) - success, oktaID, groupIDs := o.VerifyFactorChallenge("bcda_user@cms.gov", "call", "badPasscode", trackingId) - assert.False(s.T(), success) - assert.Equal(s.T(), "abc123", oktaID) - assert.Len(s.T(), groupIDs, 0) -} - -func (s *OTestSuite) TestVerifyFactorChallengeInvalidPasscode() { - trackingId := uuid.NewRandom().String() - responses := []TestResponse{ - newTestResponse(isGetUser(), 200, `[{"id":"abc123","status":"ACTIVE","created":"2018-12-05T19:48:17.000Z","activated":"2018-12-05T19:48:17.000Z","statusChanged":"2019-06-04T12:52:49.000Z","lastLogin":"2019-06-06T18:45:35.000Z","lastUpdated":"2019-06-04T12:52:49.000Z","passwordChanged":"2019-06-04T12:52:49.000Z","profile":{"firstName":"Test","lastName":"User","mobilePhone":null,"addressType":"Select Type...","secondEmail":"bcda_user@cms.gov","login":"a_user@cms.gov","email":"a_user@cms.gov","LOA":"3"},"credentials":{"password":{},"emails":[{"value":"a_user@cms.gov","status":"VERIFIED","type":"PRIMARY"},{"value":"a_user@cms.gov","status":"VERIFIED","type":"SECONDARY"}],"provider":{"type":"OKTA","name":"OKTA"}},"_links":{"self":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123"}}}]`), - newTestResponse(isGetFactor(), 200, `[{"id":"123abc","factorType":"call","provider":"OKTA","vendorName":"OKTA","status":"ACTIVE","created":"2019-06-05T14:13:57.000Z","lastUpdated":"2019-06-05T14:13:57.000Z","profile":{"phoneNumber":"+15555555555"},"_links":{"self":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123/factors/123abc","hints":{"allow":["GET","DELETE"]}},"verify":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123/factors/123abc/verify","hints":{"allow":["POST"]}},"user":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123","hints":{"allow":["GET"]}}}}]`), - newTestResponse(isPostFactorChallenge(), 403, `{"errorCode":"E0000068","errorSummary":"Invalid Passcode/Answer","errorLink":"E0000068","errorId":"oaeZAX9ava1RYS8lWNksxtqeg","errorCauses":[{"errorSummary":"Your token doesn't match our records. Please try again."}]}`), - } - client := okta.NewTestClient(testHttpResponses(responses)) - o := NewOktaMFA(client) - success, oktaID, groupIDs := o.VerifyFactorChallenge("bcda_user@cms.gov", "call", "badPasscode", trackingId) - assert.False(s.T(), success) - assert.Equal(s.T(), "abc123", oktaID) - assert.Len(s.T(), groupIDs, 0) -} - -func (s *OTestSuite) TestVerifyFactorChallengeSuccess() { - trackingId := uuid.NewRandom().String() - responses := []TestResponse{ - newTestResponse(isGetUser(), 200, `[{"id":"abc123","status":"ACTIVE","created":"2018-12-05T19:48:17.000Z","activated":"2018-12-05T19:48:17.000Z","statusChanged":"2019-06-04T12:52:49.000Z","lastLogin":"2019-06-06T18:45:35.000Z","lastUpdated":"2019-06-04T12:52:49.000Z","passwordChanged":"2019-06-04T12:52:49.000Z","profile":{"firstName":"Test","lastName":"User","mobilePhone":null,"addressType":"Select Type...","secondEmail":"bcda_user@cms.gov","login":"a_user@cms.gov","email":"a_user@cms.gov","LOA":"3"},"credentials":{"password":{},"emails":[{"value":"a_user@cms.gov","status":"VERIFIED","type":"PRIMARY"},{"value":"a_user@cms.gov","status":"VERIFIED","type":"SECONDARY"}],"provider":{"type":"OKTA","name":"OKTA"}},"_links":{"self":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123"}}}]`), - newTestResponse(isGetFactor(), 200, `[{"id":"123abc","factorType":"call","provider":"OKTA","vendorName":"OKTA","status":"ACTIVE","created":"2019-06-05T14:13:57.000Z","lastUpdated":"2019-06-05T14:13:57.000Z","profile":{"phoneNumber":"+15555555555"},"_links":{"self":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123/factors/123abc","hints":{"allow":["GET","DELETE"]}},"verify":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123/factors/123abc/verify","hints":{"allow":["POST"]}},"user":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123","hints":{"allow":["GET"]}}}}]`), - newTestResponse(isPostFactorChallenge(), 200, `{"factorResult":"SUCCESS"}`), - } - client := okta.NewTestClient(testHttpResponses(responses)) - o := NewOktaMFA(client) - success, oktaID, groupIDs := o.VerifyFactorChallenge("bcda_user@cms.gov", "call", "badPasscode", trackingId) - assert.True(s.T(), success) - assert.Equal(s.T(), "abc123", oktaID) - assert.Len(s.T(), groupIDs, 0) -} - -func (s *OTestSuite) TestVerifyPasswordSuccess() { - trackingId := uuid.NewRandom().String() - responses := []TestResponse{ - newTestResponse(isGetUser(), 200, `[{"id":"abc123","status":"ACTIVE","created":"2018-12-05T19:48:17.000Z","activated":"2018-12-05T19:48:17.000Z","statusChanged":"2019-06-04T12:52:49.000Z","lastLogin":"2019-06-06T18:45:35.000Z","lastUpdated":"2019-06-04T12:52:49.000Z","passwordChanged":"2019-06-04T12:52:49.000Z","profile":{"firstName":"Test","lastName":"User","mobilePhone":null,"addressType":"Select Type...","secondEmail":"bcda_user@cms.gov","login":"a_user@cms.gov","email":"a_user@cms.gov","LOA":"3"},"credentials":{"password":{},"emails":[{"value":"a_user@cms.gov","status":"VERIFIED","type":"PRIMARY"},{"value":"a_user@cms.gov","status":"VERIFIED","type":"SECONDARY"}],"provider":{"type":"OKTA","name":"OKTA"}},"_links":{"self":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123"}}}]`), - newTestResponse(isPostPassword(), 200, `{"stateToken":"00zABCabc123-000","expiresAt":"2019-09-16T19:28:58.000Z","status":"MFA_REQUIRED","_embedded":{"user":{"id":"abc123","passwordChanged":"2019-07-23T20:03:32.000Z","profile":{"login":"bcda_user2@cms.gov","firstName":"ACO","lastName":"User2","locale":"en","timeZone":"America/Los_Angeles"}},"factors":[{"id":"123ghi","factorType":"sms","provider":"OKTA","vendorName":"OKTA","profile":{"phoneNumber":"+1 XXX-XXX-7922"},"_links":{"verify":{"href":"https://cms-sandbox.oktapreview.com/api/v1/authn/factors/123ghi/verify","hints":{"allow":["POST"]}}}}],"policy":{"allowRememberDevice":true,"rememberDeviceLifetimeInMinutes":30,"rememberDeviceByDefault":false,"factorsPolicyInfo":{}}},"_links":{"cancel":{"href":"https://cms-sandbox.oktapreview.com/api/v1/authn/cancel","hints":{"allow":["POST"]}}}}`), - } - client := okta.NewTestClient(testHttpResponses(responses)) - o := NewOktaMFA(client) - passwordReturn, oktaID, err := o.VerifyPassword("bcda_user@cms.gov", "not_a_password", trackingId) - if err != nil || passwordReturn == nil { - assert.FailNow(s.T(), "must have valid passwordReturn") - } - assert.True(s.T(), passwordReturn.Success) - assert.Equal(s.T(), "abc123", oktaID) -} - -func (s *OTestSuite) TestVerifyPasswordUserNotFound() { - trackingId := uuid.NewRandom().String() - responses := []TestResponse{ - newTestResponse(isGetUser(), 200, `[]`), - } - client := okta.NewTestClient(testHttpResponses(responses)) - o := NewOktaMFA(client) - passwordReturn, oktaID, err := o.VerifyPassword("bcda_user@cms.gov", "not_a_password", trackingId) - assert.NotNil(s.T(), err) - assert.Empty(s.T(), passwordReturn) - assert.Equal(s.T(), "", oktaID) -} - -func (s *OTestSuite) TestVerifyPasswordInvalidRequest() { - trackingId := uuid.NewRandom().String() - responses := []TestResponse{ - newTestResponse(isGetUser(), 200, `[{"id":"abc123","status":"ACTIVE","created":"2018-12-05T19:48:17.000Z","activated":"2018-12-05T19:48:17.000Z","statusChanged":"2019-06-04T12:52:49.000Z","lastLogin":"2019-06-06T18:45:35.000Z","lastUpdated":"2019-06-04T12:52:49.000Z","passwordChanged":"2019-06-04T12:52:49.000Z","profile":{"firstName":"Test","lastName":"User","mobilePhone":null,"addressType":"Select Type...","secondEmail":"bcda_user@cms.gov","login":"a_user@cms.gov","email":"a_user@cms.gov","LOA":"3"},"credentials":{"password":{},"emails":[{"value":"a_user@cms.gov","status":"VERIFIED","type":"PRIMARY"},{"value":"a_user@cms.gov","status":"VERIFIED","type":"SECONDARY"}],"provider":{"type":"OKTA","name":"OKTA"}},"_links":{"self":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123"}}}]`), - newTestResponse(isPostPassword(), 200, `{"errorCode":"E0000001","errorSummary":"Api validation failed: authRequest","errorLink":"E0000001","errorId":"oaebM8KyzEVS5uMiImx4sHzGg","errorCauses":[{"errorSummary":"The 'username' and 'password' attributes are required in this context."}]}`), - } - client := okta.NewTestClient(testHttpResponses(responses)) - o := NewOktaMFA(client) - passwordReturn, oktaID, err := o.VerifyPassword("bcda_user@cms.gov", "not_a_password", trackingId) - assert.NotNil(s.T(), err) - assert.Empty(s.T(), passwordReturn) - assert.Equal(s.T(), "", oktaID) -} - -func (s *OTestSuite) TestRequestFactorChallengeInvalidFactor() { - trackingId := uuid.NewRandom().String() - responses := []TestResponse{ - newTestResponse(isGetUser(), 200, `[{"id":"abc123","status":"ACTIVE","created":"2018-12-05T19:48:17.000Z","activated":"2018-12-05T19:48:17.000Z","statusChanged":"2019-06-04T12:52:49.000Z","lastLogin":"2019-06-06T18:45:35.000Z","lastUpdated":"2019-06-04T12:52:49.000Z","passwordChanged":"2019-06-04T12:52:49.000Z","profile":{"firstName":"Test","lastName":"User","mobilePhone":null,"addressType":"Select Type...","secondEmail":"bcda_user@cms.gov","login":"a_user@cms.gov","email":"a_user@cms.gov","LOA":"3"},"credentials":{"password":{},"emails":[{"value":"a_user@cms.gov","status":"VERIFIED","type":"PRIMARY"},{"value":"a_user@cms.gov","status":"VERIFIED","type":"SECONDARY"}],"provider":{"type":"OKTA","name":"OKTA"}},"_links":{"self":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123"}}}]`), - newTestResponse(isGetFactor(), 200, `[{"id":"123abc","factorType":"call","provider":"OKTA","vendorName":"OKTA","status":"ACTIVE","created":"2019-06-05T14:13:57.000Z","lastUpdated":"2019-06-05T14:13:57.000Z","profile":{"phoneNumber":"+15555555555"},"_links":{"self":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123/factors/123abc","hints":{"allow":["GET","DELETE"]}},"verify":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123/factors/123abc/verify","hints":{"allow":["POST"]}},"user":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123","hints":{"allow":["GET"]}}}}]`), - } - client := okta.NewTestClient(testHttpResponses(responses)) - o := NewOktaMFA(client) - factorReturn, err := o.RequestFactorChallenge("bcda_user@cms.gov", "badFactor", trackingId) - if err != nil { - s.FailNow("RequestFactorChallenge should return an error for an invalid factor") - } - assert.NotEmpty(s.T(), factorReturn) - assert.Equal(s.T(), "invalid_request", factorReturn.Action) -} - -func (s *OTestSuite) TestRequestFactorChallengeCallFactor() { - trackingId := uuid.NewRandom().String() - responses := []TestResponse{ - newTestResponse(isGetUser(), 200, `[{"id":"abc123","status":"ACTIVE","created":"2018-12-05T19:48:17.000Z","activated":"2018-12-05T19:48:17.000Z","statusChanged":"2019-06-04T12:52:49.000Z","lastLogin":"2019-06-06T18:45:35.000Z","lastUpdated":"2019-06-04T12:52:49.000Z","passwordChanged":"2019-06-04T12:52:49.000Z","profile":{"firstName":"Test","lastName":"User","mobilePhone":null,"addressType":"Select Type...","secondEmail":"bcda_user@cms.gov","login":"a_user@cms.gov","email":"a_user@cms.gov","LOA":"3"},"credentials":{"password":{},"emails":[{"value":"a_user@cms.gov","status":"VERIFIED","type":"PRIMARY"},{"value":"a_user@cms.gov","status":"VERIFIED","type":"SECONDARY"}],"provider":{"type":"OKTA","name":"OKTA"}},"_links":{"self":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123"}}}]`), - newTestResponse(isGetFactor(), 200, `[{"id":"123abc","factorType":"call","provider":"OKTA","vendorName":"OKTA","status":"ACTIVE","created":"2019-06-05T14:13:57.000Z","lastUpdated":"2019-06-05T14:13:57.000Z","profile":{"phoneNumber":"+15555555555"},"_links":{"self":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123/factors/123abc","hints":{"allow":["GET","DELETE"]}},"verify":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123/factors/123abc/verify","hints":{"allow":["POST"]}},"user":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123","hints":{"allow":["GET"]}}}}]`), - newTestResponse(isPostFactorChallenge(), 200, `{"factorResult":"CHALLENGE","_links":{"verify":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123/factors/123abc/verify","hints":{"allow":["POST"]}},"factor":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123/factors/123abc","hints":{"allow":["GET","DELETE"]}}}}`), - } - client := okta.NewTestClient(testHttpResponses(responses)) - o := NewOktaMFA(client) - factorReturn, err := o.RequestFactorChallenge("bcda_user@cms.gov", "Call", trackingId) - if err != nil { - s.FailNow("RequestFactorChallenge should not return an error for this combination of valid responses and valid factor") - } - assert.NotEmpty(s.T(), factorReturn) - assert.Equal(s.T(), "request_sent", factorReturn.Action) - assert.Empty(s.T(), factorReturn.Transaction) - - responseBody, err := json.Marshal(factorReturn) - if err != nil { - s.FailNow("RequestFactorChallenge should always be able to get valid JSON from the response") - } - assert.NotContains(s.T(), string(responseBody), "transaction") -} - -func (s *OTestSuite) TestRequestFactorChallengePushFactor() { - trackingId := uuid.NewRandom().String() - responses := []TestResponse{ - newTestResponse(isGetUser(), 200, `[{"id":"abc123","status":"ACTIVE","created":"2018-12-05T19:48:17.000Z","activated":"2018-12-05T19:48:17.000Z","statusChanged":"2019-06-04T12:52:49.000Z","lastLogin":"2019-06-06T18:45:35.000Z","lastUpdated":"2019-06-04T12:52:49.000Z","passwordChanged":"2019-06-04T12:52:49.000Z","profile":{"firstName":"Test","lastName":"User","mobilePhone":null,"addressType":"Select Type...","secondEmail":"bcda_user@cms.gov","login":"a_user@cms.gov","email":"a_user@cms.gov","LOA":"3"},"credentials":{"password":{},"emails":[{"value":"a_user@cms.gov","status":"VERIFIED","type":"PRIMARY"},{"value":"a_user@cms.gov","status":"VERIFIED","type":"SECONDARY"}],"provider":{"type":"OKTA","name":"OKTA"}},"_links":{"self":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123"}}}]`), - newTestResponse(isGetFactor(), 200, `[{"id":"123mno","factorType":"push","provider":"OKTA","vendorName":"OKTA","status":"ACTIVE","created":"2019-01-03T18:18:52.000Z","lastUpdated":"2019-01-03T18:19:04.000Z","profile":{"credentialId":"a_user@cms.gov","deviceType":"SmartPhone_IPhone","keys":[{"kty":"PKIX","use":"sig","kid":"default","x5c":["MIIBI..."]}],"name":"A User’s iPhone","platform":"IOS","version":"12.1.2"},"_links":{"self":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123/factors/123mno","hints":{"allow":["GET","DELETE"]}},"verify":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123/factors/123mno/verify","hints":{"allow":["POST"]}},"user":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123","hints":{"allow":["GET"]}}}}]`), - newTestResponse(isPostFactorChallenge(), 200, `{"factorResult":"WAITING","profile":{"credentialId":"bcda_user1@cms.gov","deviceType":"SmartPhone_IPhone","keys":[{"kty":"PKIX","use":"sig","kid":"default","x5c":["MIIBI..."]}],"name":"User’s iPhone","platform":"IOS","version":"12.1.2"},"expiresAt":"2019-07-12T14:21:30.000Z","_links":{"cancel":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123/factors/123mno/transactions/v2mst.WmiSGGkvQc6P-QUQ5Qy0jg","hints":{"allow":["DELETE"]}},"poll":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123/factors/123mno/transactions/v2mst.WmiSGGkvQc6P-QUQ5Qy0jg","hints":{"allow":["GET"]}}}}`), - } - client := okta.NewTestClient(testHttpResponses(responses)) - o := NewOktaMFA(client) - factorReturn, err := o.RequestFactorChallenge("bcda_user@cms.gov", "Push", trackingId) - if err != nil { - s.FailNow("RequestFactorChallenge should not return an error for this combination of valid responses and valid factor") - } - assert.NotEmpty(s.T(), factorReturn) - assert.Equal(s.T(), "request_sent", factorReturn.Action) - assert.Equal(s.T(), "v2mst.WmiSGGkvQc6P-QUQ5Qy0jg", factorReturn.Transaction.TransactionID) - - responseBody, err := json.Marshal(factorReturn) - if err != nil { - s.FailNow("RequestFactorChallenge should always be able to get valid JSON from the response") - } - assert.Contains(s.T(), string(responseBody), "transaction") -} - -func (s *OTestSuite) TestRequestFactorChallengeInvalidPasscode() { - trackingId := uuid.NewRandom().String() - responses := []TestResponse{ - newTestResponse(isGetUser(), 200, `[{"id":"abc123","status":"ACTIVE","created":"2018-12-05T19:48:17.000Z","activated":"2018-12-05T19:48:17.000Z","statusChanged":"2019-06-04T12:52:49.000Z","lastLogin":"2019-06-06T18:45:35.000Z","lastUpdated":"2019-06-04T12:52:49.000Z","passwordChanged":"2019-06-04T12:52:49.000Z","profile":{"firstName":"Test","lastName":"User","mobilePhone":null,"addressType":"Select Type...","secondEmail":"bcda_user@cms.gov","login":"a_user@cms.gov","email":"a_user@cms.gov","LOA":"3"},"credentials":{"password":{},"emails":[{"value":"a_user@cms.gov","status":"VERIFIED","type":"PRIMARY"},{"value":"a_user@cms.gov","status":"VERIFIED","type":"SECONDARY"}],"provider":{"type":"OKTA","name":"OKTA"}},"_links":{"self":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123"}}}]`), - newTestResponse(isGetFactor(), 200, `[{"id":"123mno","factorType":"push","provider":"OKTA","vendorName":"OKTA","status":"ACTIVE","created":"2019-01-03T18:18:52.000Z","lastUpdated":"2019-01-03T18:19:04.000Z","profile":{"credentialId":"a_user@cms.gov","deviceType":"SmartPhone_IPhone","keys":[{"kty":"PKIX","use":"sig","kid":"default","x5c":["MIIBI..."]}],"name":"A User’s iPhone","platform":"IOS","version":"12.1.2"},"_links":{"self":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123/factors/123mno","hints":{"allow":["GET","DELETE"]}},"verify":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123/factors/123mno/verify","hints":{"allow":["POST"]}},"user":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123","hints":{"allow":["GET"]}}}}]`), - newTestResponse(isPostFactorChallenge(), 403, `{"errorCode":"E0000068","errorSummary":"Invalid Passcode/Answer","errorLink":"E0000068","errorId":"oaeZAX9ava1RYS8lWNksxtqeg","errorCauses":[{"errorSummary":"Your token doesn't match our records. Please try again."}]}`), - } - client := okta.NewTestClient(testHttpResponses(responses)) - o := NewOktaMFA(client) - factorReturn, err := o.RequestFactorChallenge("bcda_user@cms.gov", "Push", trackingId) - assert.NotNil(s.T(), err) - assert.Equal(s.T(), "request_sent", factorReturn.Action) -} - -func (s *OTestSuite) TestRequestFactorChallengeFactorNotFound() { - trackingId := uuid.NewRandom().String() - responses := []TestResponse{ - newTestResponse(isGetUser(), 200, `[{"id":"abc123","status":"ACTIVE","created":"2018-12-05T19:48:17.000Z","activated":"2018-12-05T19:48:17.000Z","statusChanged":"2019-06-04T12:52:49.000Z","lastLogin":"2019-06-06T18:45:35.000Z","lastUpdated":"2019-06-04T12:52:49.000Z","passwordChanged":"2019-06-04T12:52:49.000Z","profile":{"firstName":"Test","lastName":"User","mobilePhone":null,"addressType":"Select Type...","secondEmail":"bcda_user@cms.gov","login":"a_user@cms.gov","email":"a_user@cms.gov","LOA":"3"},"credentials":{"password":{},"emails":[{"value":"a_user@cms.gov","status":"VERIFIED","type":"PRIMARY"},{"value":"a_user@cms.gov","status":"VERIFIED","type":"SECONDARY"}],"provider":{"type":"OKTA","name":"OKTA"}},"_links":{"self":{"href":"https://cms-sandbox.oktapreview.com/api/v1/users/abc123"}}}]`), - newTestResponse(isGetFactor(), 200, `[]`), - } - client := okta.NewTestClient(testHttpResponses(responses)) - o := NewOktaMFA(client) - factorReturn, err := o.RequestFactorChallenge("bcda_user@cms.gov", "Push", trackingId) - assert.NotNil(s.T(), err) - assert.Contains(s.T(), err.Error(), "no active factor of requested type") - assert.Equal(s.T(), "request_sent", factorReturn.Action) -} - -func (s *OTestSuite) TestRequestFactorChallengeUserNotFound() { - trackingId := uuid.NewRandom().String() - responses := []TestResponse{ - newTestResponse(isGetUser(), 200, `[]`), - } - client := okta.NewTestClient(testHttpResponses(responses)) - o := NewOktaMFA(client) - factorReturn, err := o.RequestFactorChallenge("bcda_user@cms.gov", "Push", trackingId) - assert.NotNil(s.T(), err) - assert.Contains(s.T(), err.Error(), "user not found") - assert.Equal(s.T(), "request_sent", factorReturn.Action) -} - -func (s *OTestSuite) TestVerifyFactorTransaction() { - o := NewOktaMFA(nil) - _, err := o.VerifyFactorTransaction("", "", "", "") - if err == nil { - assert.FailNow(s.T(), "error should be thrown") - } - assert.Contains(s.T(), err.Error(), "not yet implemented") -} - -func TestOTestSuite(t *testing.T) { - suite.Run(t, new(OTestSuite)) -} - -func isMatch(regex string, comparison string) bool { - re := regexp.MustCompile(regex) - return re.Match([]byte(comparison)) -} - -func testHttpResponse(statusCode int, body string) *http.Response { - return &http.Response{ - StatusCode: statusCode, - Body: ioutil.NopCloser(bytes.NewBufferString(body)), - Header: make(http.Header), - } -} - -func testHttpResponses(responses []TestResponse) func(*http.Request) *http.Response { - return func(req *http.Request) *http.Response { - for _, resp := range responses { - if resp.GiveAnswer(req) { - return resp.Response - } - } - return testHttpResponse(404, "Test request not found: " + req.URL.String()) - } -} - -func newTestResponse(testFunc func(*http.Request) bool, code int, body string) TestResponse { - response := testHttpResponse(code, body) - return TestResponse{GiveAnswer: testFunc, Response: response} -} - -func isPostFactorChallenge() func(*http.Request) bool { - return func(req *http.Request) bool { - return req.Method == "POST" && isMatch(`\/api\/v1\/users\/.*\/factors\/.*\/verify`, req.URL.String()) - } -} - -func isGetUser() func(*http.Request) bool { - return func(req *http.Request) bool { - return req.Method == "GET" && isMatch(`\/api\/v1\/users\/\?q=`, req.URL.String()) - } -} - -func isGetFactor() func(*http.Request) bool { - return func(req *http.Request) bool { - return req.Method == "GET" && isMatch(`\/api\/v1\/users\/.*\/factors`, req.URL.String()) - } -} - -func isPostPassword() func(*http.Request) bool { - return func(req *http.Request) bool { - return req.Method == "POST" && isMatch(`\/api\/v1\/authn`, req.URL.String()) - } -} diff --git a/ssas/service/public/router.go b/ssas/service/public/router.go deleted file mode 100644 index e55693258..000000000 --- a/ssas/service/public/router.go +++ /dev/null @@ -1,48 +0,0 @@ -package public - -import ( - "fmt" - "os" - "time" - - "github.com/go-chi/chi" - - "github.com/CMSgov/bcda-app/ssas" - "github.com/CMSgov/bcda-app/ssas/service" -) - -var version = "latest" -var infoMap map[string][]string -var publicSigningKeyPath string -var server *service.Server - -func init() { - infoMap = make(map[string][]string) - publicSigningKeyPath = os.Getenv("SSAS_PUBLIC_SIGNING_KEY_PATH") - ssas.Logger.Info("public signing key sourced from ", publicSigningKeyPath) -} - -func Server() (*service.Server) { - unsafeMode := os.Getenv("HTTP_ONLY") == "true" - server = service.NewServer("public", ":3003", version, infoMap, routes(), unsafeMode, publicSigningKeyPath, 20 * time.Minute) - if server != nil { - r, _ := server.ListRoutes() - infoMap["banner"] = []string{fmt.Sprintf("%s server running on port %s", "public", ":3003")} - infoMap["routes"] = r - } - return server -} - -func routes() *chi.Mux { - router := chi.NewRouter() - router.Use(service.NewAPILogger(), service.ConnectionClose) - router.Post("/token", token) - router.Post("/introspect", introspect) - router.Post("/authn", VerifyPassword) - router.With(parseToken, requireMFATokenAuth).Post("/authn/challenge", RequestMultifactorChallenge) - router.With(parseToken, requireMFATokenAuth).Post("/authn/verify", VerifyMultifactorResponse) - router.With(parseToken, requireRegTokenAuth, readGroupID).Post("/register", RegisterSystem) - router.With(parseToken, requireRegTokenAuth, readGroupID).Post("/reset", ResetSecret) - - return router -} diff --git a/ssas/service/public/router_test.go b/ssas/service/public/router_test.go deleted file mode 100644 index 9638166b2..000000000 --- a/ssas/service/public/router_test.go +++ /dev/null @@ -1,142 +0,0 @@ -package public - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "net/http" - "net/http/httptest" - "os" - "strings" - "testing" - - "github.com/jinzhu/gorm" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/suite" - - "github.com/CMSgov/bcda-app/ssas" -) - -type PublicRouterTestSuite struct { - suite.Suite - publicRouter http.Handler - rr *httptest.ResponseRecorder - db *gorm.DB - group ssas.Group - system ssas.System -} - -func (s *PublicRouterTestSuite) SetupSuite() { - os.Setenv("DEBUG", "true") - s.publicRouter = routes() - ssas.InitializeGroupModels() - ssas.InitializeSystemModels() - s.db = ssas.GetGORMDbConnection() - s.rr = httptest.NewRecorder() - groupBytes := []byte(`{"group_id":"T1234","users":["fake_okta_id","abcdefg"]}`) - gd := ssas.GroupData{} - err := json.Unmarshal(groupBytes, &gd) - assert.Nil(s.T(), err) - s.group, err = ssas.CreateGroup(gd) - if err != nil { - s.FailNow("unable to create group: " + err.Error()) - } - s.system = ssas.System{GroupID: s.group.GroupID, ClientID: "abcd1234"} - if err := s.db.Create(&s.system).Error; err != nil { - s.FailNow("unable to create system: " + err.Error()) - } -} - -func (s *PublicRouterTestSuite) TearDownSuite() { - err := ssas.CleanDatabase(s.group) - assert.Nil(s.T(), err) - ssas.Close(s.db) -} - -func (s *PublicRouterTestSuite) reqPublicRoute(verb string, route string, body io.Reader, token string) *http.Response { - req := httptest.NewRequest(strings.ToUpper(verb), route, body) - req.Header.Add("x-group-id", s.group.GroupID) - if token != "" { - req.Header.Add("Authorization", "Bearer "+token) - } - rr := httptest.NewRecorder() - s.publicRouter.ServeHTTP(rr, req) - return rr.Result() -} - -func (s *PublicRouterTestSuite) TestTokenRoute() { - res := s.reqPublicRoute("POST", "/token", nil, "") - assert.Equal(s.T(), http.StatusBadRequest, res.StatusCode) -} - -func (s *PublicRouterTestSuite) TestRegisterRoute() { - groupIDs := []string{"T1234", "T0001"} - _, ts, _ := MintRegistrationToken("test_okta_id", groupIDs) - rb := strings.NewReader(`{"client_id":"evil_twin","client_name":"my evil twin","scope":"bcda-api","jwks":{"keys":[{"e":"AAEAAQ","n":"ok6rvXu95337IxsDXrKzlIqw_I_zPDG8JyEw2CTOtNMoDi1QzpXQVMGj2snNEmvNYaCTmFf51I-EDgeFLLexr40jzBXlg72quV4aw4yiNuxkigW0gMA92OmaT2jMRIdDZM8mVokoxyPfLub2YnXHFq0XuUUgkX_TlutVhgGbyPN0M12teYZtMYo2AUzIRggONhHvnibHP0CPWDjCwSfp3On1Recn4DPxbn3DuGslF2myalmCtkujNcrhHLhwYPP-yZFb8e0XSNTcQvXaQxAqmnWH6NXcOtaeWMQe43PNTAyNinhndgI8ozG3Hz-1NzHssDH_yk6UYFSszhDbWAzyqw","kty":"RSA"}]}}`) - res := s.reqPublicRoute("POST", "/register", rb, ts) - assert.Equal(s.T(), http.StatusCreated, res.StatusCode) -} - -func (s *PublicRouterTestSuite) TestRegisterRouteNoToken() { - rb := strings.NewReader(`{"client_id":"evil_twin","client_name":"my evil twin","scope":"bcda-api","jwks":{"keys":[{"e":"AAEAAQ","n":"ok6rvXu95337IxsDXrKzlIqw_I_zPDG8JyEw2CTOtNMoDi1QzpXQVMGj2snNEmvNYaCTmFf51I-EDgeFLLexr40jzBXlg72quV4aw4yiNuxkigW0gMA92OmaT2jMRIdDZM8mVokoxyPfLub2YnXHFq0XuUUgkX_TlutVhgGbyPN0M12teYZtMYo2AUzIRggONhHvnibHP0CPWDjCwSfp3On1Recn4DPxbn3DuGslF2myalmCtkujNcrhHLhwYPP-yZFb8e0XSNTcQvXaQxAqmnWH6NXcOtaeWMQe43PNTAyNinhndgI8ozG3Hz-1NzHssDH_yk6UYFSszhDbWAzyqw","kty":"RSA"}]}}`) - res := s.reqPublicRoute("POST", "/register", rb, "") - assert.Equal(s.T(), http.StatusUnauthorized, res.StatusCode) -} - -func (s *PublicRouterTestSuite) TestResetRoute() { - groupIDs := []string{"T1234", "T0001"} - _, ts, _ := MintRegistrationToken("test_okta_id", groupIDs) - rb := strings.NewReader(fmt.Sprintf(`{"client_id":"%s"}`, s.system.ClientID)) - res := s.reqPublicRoute("POST", "/reset", rb, ts) - assert.Equal(s.T(), http.StatusOK, res.StatusCode) -} - -func (s *PublicRouterTestSuite) TestAuthnChallengeRoute() { - _, ts, _ := MintMFAToken("fake_okta_id") - rb := strings.NewReader(`{"login_id":"success@test.com","factor_type":"SMS"}`) - res := s.reqPublicRoute("POST", "/authn/challenge", rb, ts) - assert.Equal(s.T(), http.StatusOK, res.StatusCode) - buf := new(bytes.Buffer) - _, err := buf.ReadFrom(res.Body) - if err != nil { - s.FailNow(err.Error()) - } - body := buf.String() - assert.Contains(s.T(), body, "request_sent") -} - -func (s *PublicRouterTestSuite) TestAuthnChallengeRouteNoToken() { - rb := strings.NewReader(`{"login_id":"success@test.com","factor_type":"SMS"}`) - res := s.reqPublicRoute("POST", "/authn/challenge", rb, "") - assert.Equal(s.T(), http.StatusUnauthorized, res.StatusCode) -} - -func (s *PublicRouterTestSuite) TestResetRouteNoToken() { - rb := strings.NewReader(fmt.Sprintf(`{"client_id":"%s"}`, s.system.ClientID)) - res := s.reqPublicRoute("POST", "/reset", rb, "") - assert.Equal(s.T(), http.StatusUnauthorized, res.StatusCode) -} - -func (s *PublicRouterTestSuite) TestAuthnRoute() { - rb := strings.NewReader(`{"login_id":"success@test.com","password":"abcdefg"}`) - res := s.reqPublicRoute("POST", "/authn", rb, "") - assert.Equal(s.T(), http.StatusOK, res.StatusCode) -} - -func (s *PublicRouterTestSuite) TestAuthnVerifyRoute() { - _, ts, _ := MintMFAToken("fake_okta_id") - rb := strings.NewReader(`{"login_id":"success@test.com","factor_type":"SMS","passcode":"123456"}`) - res := s.reqPublicRoute("POST", "/authn/verify", rb, ts) - assert.Equal(s.T(), http.StatusOK, res.StatusCode) -} - -func (s *PublicRouterTestSuite) TestAuthnVerifyRouteNoToken() { - rb := strings.NewReader(`{"login_id":"success@test.com","factor_type":"SMS","passcode":"123456"}`) - res := s.reqPublicRoute("POST", "/authn/verify", rb, "") - assert.Equal(s.T(), http.StatusUnauthorized, res.StatusCode) -} - -func TestPublicRouterTestSuite(t *testing.T) { - suite.Run(t, new(PublicRouterTestSuite)) -} diff --git a/ssas/service/public/tokens.go b/ssas/service/public/tokens.go deleted file mode 100644 index e51a69bf2..000000000 --- a/ssas/service/public/tokens.go +++ /dev/null @@ -1,144 +0,0 @@ -package public - -import ( - "fmt" - "time" - - "github.com/dgrijalva/jwt-go" - - "github.com/CMSgov/bcda-app/ssas" - "github.com/CMSgov/bcda-app/ssas/cfg" - "github.com/CMSgov/bcda-app/ssas/service" -) - -var selfRegistrationTokenDuration time.Duration - -func init() { - minutes := cfg.GetEnvInt("SSAS_MFA_TOKEN_TIMEOUT_MINUTES", 60) - selfRegistrationTokenDuration = time.Duration(int64(time.Minute) * int64(minutes)) -} - -// MintMFAToken generates a tokenstring for MFA endpoints -func MintMFAToken(oktaID string) (*jwt.Token, string, error) { - claims := service.CommonClaims{ - TokenType: "MFAToken", - OktaID: oktaID, - } - - if err := checkTokenClaims(&claims); err != nil { - return nil, "", err - } - - return server.MintTokenWithDuration(&claims, selfRegistrationTokenDuration) -} - -// MintRegistrationToken generates a tokenstring for system self-registration endpoints -func MintRegistrationToken(oktaID string, groupIDs []string) (*jwt.Token, string, error) { - claims := service.CommonClaims{ - TokenType: "RegistrationToken", - OktaID: oktaID, - GroupIDs: groupIDs, - } - - if err := checkTokenClaims(&claims); err != nil { - return nil, "", err - } - - return server.MintTokenWithDuration(&claims, selfRegistrationTokenDuration) -} - -// MintAccessToken generates a tokenstring that expires in server.tokenTTL time -func MintAccessToken(systemID, clientID string, data string) (*jwt.Token, string, error) { - claims := service.CommonClaims{ - TokenType: "AccessToken", - SystemID: systemID, - ClientID: clientID, - Data: data, - } - - if err := checkTokenClaims(&claims); err != nil { - return nil, "", err - } - - return server.MintToken(&claims) -} - -func empty(arr []string) bool { - empty := true - for _, item := range arr { - if item != "" { - empty = false - break - } - } - return empty -} - -func tokenValidity(tokenString string, requiredTokenType string) error { - tknEvent := ssas.Event{Op: "tokenValidity"} - ssas.OperationStarted(tknEvent) - t, err := server.VerifyToken(tokenString) - if err != nil { - tknEvent.Help = err.Error() - ssas.OperationFailed(tknEvent) - return err - } - - c := t.Claims.(*service.CommonClaims) - - err = checkAllClaims(c, requiredTokenType) - if err != nil { - tknEvent.Help = err.Error() - ssas.OperationFailed(tknEvent) - return err - } - - err = c.Valid() - if err != nil { - tknEvent.Help = err.Error() - ssas.OperationFailed(tknEvent) - return err - } - - if service.TokenBlacklist.IsTokenBlacklisted(c.Id) { - err = fmt.Errorf("token has been revoked") - tknEvent.Help = err.Error() - ssas.OperationFailed(tknEvent) - return err - } - - ssas.OperationSucceeded(tknEvent) - return nil -} - -func checkAllClaims(claims *service.CommonClaims, requiredTokenType string) error { - if err := server.CheckRequiredClaims(claims, requiredTokenType); err != nil { - return err - } - - if err := checkTokenClaims(claims); err != nil { - return err - } - return nil -} - -func checkTokenClaims(claims *service.CommonClaims) error { - switch claims.TokenType { - case "MFAToken": - if claims.OktaID == "" { - return fmt.Errorf("MFA token must have OktaID claim") - } - case "RegistrationToken": - if empty(claims.GroupIDs) { - return fmt.Errorf("registration token must have GroupIDs claim") - } - case "AccessToken": - if claims.Data == "" { - return fmt.Errorf("access token must have Data claim") - } - default: - return fmt.Errorf("missing token type claim") - } - - return nil -} \ No newline at end of file diff --git a/ssas/service/public/tokens_test.go b/ssas/service/public/tokens_test.go deleted file mode 100644 index da498af7c..000000000 --- a/ssas/service/public/tokens_test.go +++ /dev/null @@ -1,106 +0,0 @@ -package public - -import ( - "encoding/json" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/stretchr/testify/suite" - "os" - "strconv" - "testing" - - "github.com/CMSgov/bcda-app/ssas/service" -) - -type PublicTokenTestSuite struct { - suite.Suite - server *service.Server -} - -func (s *PublicTokenTestSuite) SetupSuite() { - info := make(map[string][]string) - info["public"] = []string{"token", "register"} - s.server = Server() - err := os.Setenv("DEBUG", "true") - assert.Nil(s.T(), err) -} - -func (s *PublicTokenTestSuite) TestMintMFAToken() { - token, ts, err := MintMFAToken("my_okta_id") - - assert.Nil(s.T(), err) - assert.NotNil(s.T(), token) - assert.NotNil(s.T(), ts) -} - -func (s *PublicTokenTestSuite) TestMintMFATokenMissingID() { - token, ts, err := MintMFAToken("") - - assert.NotNil(s.T(), err) - assert.Nil(s.T(), token) - assert.Equal(s.T(), "", ts) -} - -func (s *PublicTokenTestSuite) TestMintRegistrationToken() { - groupIDs := []string{"A0000", "A0001"} - token, ts, err := MintRegistrationToken("my_okta_id", groupIDs) - - assert.Nil(s.T(), err) - assert.NotNil(s.T(), token) - assert.NotNil(s.T(), ts) -} - -func (s *PublicTokenTestSuite) TestMintRegistrationTokenMissingID() { - groupIDs := []string{"", ""} - token, ts, err := MintRegistrationToken("my_okta_id", groupIDs) - - assert.NotNil(s.T(), err) - assert.Nil(s.T(), token) - assert.Equal(s.T(), "", ts) -} - -func (s *PublicTokenTestSuite) TestMintAccessToken() { - data := `"{\"cms_ids\":[\"T67890\",\"T54321\"]}"` - t, ts, err := MintAccessToken("2", "0c527d2e-2e8a-4808-b11d-0fa06baf8254", data) - - require.Nil(s.T(), err, ) - assert.NotEmpty(s.T(), ts, "missing token string value") - assert.NotNil(s.T(), t, "missing token value") - - claims := t.Claims.(*service.CommonClaims) - assert.NotNil(s.T(), claims.Data, "missing data claim") - type XData struct { - IDList []string `json:"cms_ids"` - } - - var xData XData - d, err := strconv.Unquote(claims.Data) - require.Nil(s.T(), err, "couldn't unquote ", d) - err = json.Unmarshal([]byte(d), &xData) - require.Nil(s.T(), err, "unexpected error in: ", d) - require.NotEmpty(s.T(), xData, "no data in data :(") - assert.Equal(s.T(), 2, len(xData.IDList)) - assert.Equal(s.T(), "T67890", xData.IDList[0]) - assert.Equal(s.T(), "T54321", xData.IDList[1]) -} - -func (s *PublicTokenTestSuite) TestCheckTokenClaimsMissingType() { - c := service.CommonClaims{} - err := checkTokenClaims(&c) - if err == nil { - assert.FailNow(s.T(), "must have error with missing token type") - } - assert.Contains(s.T(), err.Error(), "missing token type claim") -} - -func (s *PublicTokenTestSuite) TestEmpty() { - groupIDs := []string{"", ""} - assert.True(s.T(), empty(groupIDs)) - - groupIDs = []string{"", "asdf"} - assert.False(s.T(), empty(groupIDs)) -} - -func TestPublicTokenTestSuite(t *testing.T) { - suite.Run(t, new(PublicTokenTestSuite)) -} diff --git a/ssas/service/server.go b/ssas/service/server.go deleted file mode 100644 index 20aa2a107..000000000 --- a/ssas/service/server.go +++ /dev/null @@ -1,273 +0,0 @@ -package service - -import ( - "crypto/rsa" - "database/sql" - "fmt" - "log" - "net/http" - "os" - "time" - - "github.com/dgrijalva/jwt-go" - "github.com/go-chi/chi" - "github.com/go-chi/render" - "github.com/pborman/uuid" - - "github.com/CMSgov/bcda-app/ssas" - "github.com/CMSgov/bcda-app/ssas/cfg" -) - -// Server configures and provisions an SSAS server -type Server struct { - name string - // port server is running on; must have leading :, as in ":3000" - port string - // version of code running this server - version string - // info contains json metadata about server - info interface{} - // router associates handlers to server endpoints - router chi.Router - // notSecure flag; when true, not running in https mode // TODO set this from HTTP_ONLY envv - notSecure bool - tokenSigningKey *rsa.PrivateKey - tokenTTL time.Duration - server http.Server -} - -// NewServer correctly initializes an instance of the Server type. -func NewServer(name, port, version string, info interface{}, routes *chi.Mux, notSecure bool, signingKeyPath string, ttl time.Duration) *Server { - sk, err := getPrivateKey(signingKeyPath); - if err != nil { - msg := fmt.Sprintf("bad signing key; path %s; %v", signingKeyPath, err) - ssas.Logger.Error(msg) - return nil - } - - s := Server{} - s.name = name - s.port = port - s.version = version - s.info = info - s.router = s.newBaseRouter() - if routes != nil { - s.router.Mount("/", routes) - } - s.notSecure = notSecure - s.tokenSigningKey = sk - s.tokenTTL = ttl - s.server = http.Server{ - Handler: s.router, - Addr: s.port, - ReadTimeout: time.Duration(cfg.GetEnvInt("SSAS_READ_TIMEOUT", 10)) * time.Second, - WriteTimeout: time.Duration(cfg.GetEnvInt("SSAS_WRITE_TIMEOUT", 20)) * time.Second, - IdleTimeout: time.Duration(cfg.GetEnvInt("SSAS_IDLE_TIMEOUT", 120)) * time.Second, - } - - return &s -} - -func (s *Server) ListRoutes() ([]string, error) { - var routes []string - walker := func(method, route string, handler http.Handler, middlewares ...func(http.Handler) http.Handler) error { - routes = append(routes, fmt.Sprintf("%s %s", method, route)) - return nil - } - err := chi.Walk(s.router, walker) - return routes, err -} - -// LogRoutes reports the routes supported by this server to the active log. Code is based on an example -// from https://itnext.io/structuring-a-production-grade-rest-api-in-golang-c0229b3feedc -func (s *Server) LogRoutes() { - banner := fmt.Sprintf("Routes for %s at port %s: ", s.name, s.port) - routes, err := s.ListRoutes() - if err != nil { - ssas.Logger.Infof("%s routing error: %v", banner, err) - } - ssas.Logger.Infof("%s %v", banner, routes) -} - -// Serve starts the server listening for and responding to requests. -func (s *Server) Serve() { - if s.notSecure { - ssas.Logger.Infof("starting %s server running UNSAFE http only mode; do not do this in production environments", s.name) - go func() { log.Fatal(s.server.ListenAndServe()) }() - } else { - tlsCertPath := os.Getenv("BCDA_TLS_CERT") // borrowing for now; we need to get our own (for both servers?) - tlsKeyPath := os.Getenv("BCDA_TLS_KEY") - go func() { log.Fatal(s.server.ListenAndServeTLS(tlsCertPath, tlsKeyPath)) }() - } -} - -// Stops the server listening for and responding to requests. -func (s *Server) Stop() { - ssas.Logger.Infof("closing server %s; %+v", s.name, s.server.Close()) -} - -func (s *Server) newBaseRouter() *chi.Mux { - r := chi.NewRouter() - r.Use( - NewAPILogger(), - render.SetContentType(render.ContentTypeJSON), - ConnectionClose, - ) - r.Get("/_version", s.getVersion) - r.Get("/_health", s.getHealthCheck) - r.Get("/_info", s.getInfo) - return r -} - -func (s *Server) getInfo(w http.ResponseWriter, r *http.Request) { - render.JSON(w, r, s.info) -} - -func (s *Server) getVersion(w http.ResponseWriter, r *http.Request) { - respMap := make(map[string]string) - respMap["version"] = fmt.Sprintf("%v", s.version) - render.JSON(w, r, s.version) -} - -func (s *Server) getHealthCheck(w http.ResponseWriter, r *http.Request) { - m := make(map[string]string) - if doHealthCheck() { - m["database"] = "ok" - w.WriteHeader(http.StatusOK) - } else { - m["database"] = "error" - w.WriteHeader(http.StatusBadGateway) - } - render.JSON(w, r, m) -} - -// is this the right health check for a service? the db could be up but the service down -// is there any condition under which the server could be running but become invalid? -// is there any circumstance where the server could be partially disabled? (e.g., unable to sign tokens but still running) -// could less than 3 servers be running? -// since this ping will be run against all servers, isn't this excessive? -func doHealthCheck() bool { - db, err := sql.Open("postgres", os.Getenv("DATABASE_URL")) - if err != nil { - // TODO health check failed event - ssas.Logger.Error("health check: database connection error: ", err.Error()) - return false - } - - defer func() { - if err = db.Close(); err != nil { - ssas.Logger.Infof("failed to close db connection in ssas/service/server.go#doHealthCheck() because %s", err) - } - }() - - if err = db.Ping(); err != nil { - ssas.Logger.Error("health check: database ping error: ", err.Error()) - return false - } - - return true -} - -// This method gets the private key from the file system. Given that the server is completely unable to fulfill its -// purpose without a signing key, a server should be considered invalid if it this function returns an error. -func getPrivateKey(keyPath string) (*rsa.PrivateKey, error) { - keyData, err := ssas.ReadPEMFile(keyPath) - if err != nil { - return nil, err - } - return ssas.ReadPrivateKey(keyData) -} - -// NYI provides a convenience handler for endpoints that are not yet implemented -func NYI(w http.ResponseWriter, r *http.Request) { - response := make(map[string]string) - response["msg"] = "Not Yet Implemented" - render.JSON(w, r, response) -} - -// ConnectionClose provides a convenience handler for closing the http connection -func ConnectionClose(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Connection", "close") - next.ServeHTTP(w, r) - }) -} - -// CommonClaims contains the superset of claims that may be found in the token -type CommonClaims struct { - jwt.StandardClaims - // AccessToken, MFAToken, or RegistrationToken - TokenType string `json:"use,omitempty"` - // In an MFA token, presence of an OktaID is taken as proof of username/password authentication - OktaID string `json:"oid,omitempty"` - ClientID string `json:"cid,omitempty"` - SystemID string `json:"sys,omitempty"` - // In a registration token, GroupIDs contains a list of all groups this user is authorized to manage - GroupIDs []string `json:"gid,omitempty"` - Data string `json:"dat,omitempty"` - Scopes []string `json:"scp,omitempty"` - // deprecated - ACOID string `json:"aco,omitempty"` - // deprecated - UUID string `json:"id,omitempty"` -} - -// MintTokenWithDuration generates a tokenstring that expires after a specific duration from now. -// If duration is <= 0, the token will be expired upon creation -func (s *Server) MintTokenWithDuration(claims *CommonClaims, duration time.Duration) (*jwt.Token, string, error) { - return s.mintToken(claims, time.Now().Unix(), time.Now().Add(duration).Unix()) -} - -// MintToken generates a tokenstring that expires in tokenTTL time -func (s *Server) MintToken(claims *CommonClaims) (*jwt.Token, string, error) { - return s.mintToken(claims, time.Now().Unix(), time.Now().Add(s.tokenTTL).Unix()) -} - -func (s *Server) mintToken(claims *CommonClaims, issuedAt int64, expiresAt int64) (*jwt.Token, string, error) { - token := jwt.New(jwt.SigningMethodRS512) - tokenID := newTokenID() - claims.IssuedAt = issuedAt - claims.ExpiresAt = expiresAt - claims.Id = tokenID - claims.Issuer = "ssas" - token.Claims = claims - var signedString, err = token.SignedString(s.tokenSigningKey) - if err != nil { - ssas.TokenMintingFailure(ssas.Event{TokenID: tokenID}) - ssas.Logger.Errorf("token signing error %s", err) - return nil, "", err - } - // not emitting AccessTokenIssued here because it hasn't been given to anyone - return token, signedString, nil -} - -func newTokenID() string { - return uuid.NewRandom().String() -} - -func (s *Server) VerifyToken(tokenString string) (*jwt.Token, error) { - - keyFunc := func(token *jwt.Token) (interface{}, error) { - if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok { - return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) - } - return &s.tokenSigningKey.PublicKey, nil - } - - return jwt.ParseWithClaims(tokenString, &CommonClaims{}, keyFunc) -} - -func (s *Server) CheckRequiredClaims(claims *CommonClaims, requiredTokenType string) error { - if claims.ExpiresAt == 0 || - claims.IssuedAt == 0 || - claims.Issuer == "" || - claims.TokenType == "" { - return fmt.Errorf("missing one or more claims") - } - - if requiredTokenType != claims.TokenType { - return fmt.Errorf(fmt.Sprintf("wrong token type: %s; required type: %s", claims.TokenType, requiredTokenType)) - } - - return nil -} diff --git a/ssas/service/server_test.go b/ssas/service/server_test.go deleted file mode 100644 index 5ec1babe9..000000000 --- a/ssas/service/server_test.go +++ /dev/null @@ -1,114 +0,0 @@ -package service - -import ( - "io/ioutil" - "net/http" - "net/http/httptest" - "testing" - "time" - - "github.com/go-chi/chi" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/suite" -) - -const unitSigningKeyPath string = "../../shared_files/ssas/unit_test_private_key.pem" - -type ServerTestSuite struct { - suite.Suite - server *Server - info map[string][]string -} - -func (s *ServerTestSuite) SetupSuite() { - s.info = make(map[string][]string) - s.info["public"] = []string{"token", "register"} -} - -func (s *ServerTestSuite) SetupTest() { - s.server = NewServer("test-server", ":9999", "9.99.999", s.info, nil, true, unitSigningKeyPath, 37 * time.Minute) -} - -func (s *ServerTestSuite) TestNewServer() { - assert.NotNil(s.T(), s.server) - assert.NotNil(s.T(), s.server.tokenSigningKey) - assert.NotEmpty(s.T(), s.server.name) - assert.NotEmpty(s.T(), s.server.port) - assert.NotEmpty(s.T(), s.server.version) - assert.NotEmpty(s.T(), s.server.info) - assert.NotEmpty(s.T(), s.server.router) - assert.True(s.T(), s.server.notSecure) - assert.NotNil(s.T(), s.server.tokenSigningKey) - assert.NotZero(s.T(), s.server.tokenTTL) - - r := chi.NewRouter() - r.Get("/test", func(w http.ResponseWriter, r *http.Request) { - _, _ = w.Write([]byte("test")) - }) - ts := NewServer("test-server", ":9999", "9.99.999", s.info, r, true, unitSigningKeyPath, 37 * time.Minute) - assert.NotEmpty(s.T(), ts.router) - routes, err := ts.ListRoutes() - assert.Nil(s.T(), err) - assert.NotNil(s.T(), routes) - expected := []string{"GET /_health", "GET /_info", "GET /_version", "GET /*/test"} - assert.Equal(s.T(), expected, routes) -} - -// test Server() ? how???? - -func (s *ServerTestSuite) TestGetInfo() { - req := httptest.NewRequest("GET", "/_info", nil) - handler := http.HandlerFunc(s.server.getInfo) - rr := httptest.NewRecorder() - handler.ServeHTTP(rr, req) - - assert.Equal(s.T(), http.StatusOK, rr.Result().StatusCode) - b, _ := ioutil.ReadAll(rr.Result().Body) - assert.Contains(s.T(), string(b), `{"public":["token","register"]}`) -} - -func (s *ServerTestSuite) TestGetVersion() { - req := httptest.NewRequest("GET", "/_version", nil) - handler := http.HandlerFunc(s.server.getVersion) - rr := httptest.NewRecorder() - handler.ServeHTTP(rr, req) - - assert.Equal(s.T(), http.StatusOK, rr.Result().StatusCode) - b, _ := ioutil.ReadAll(rr.Result().Body) - assert.Contains(s.T(), string(b), "9.99.999") -} - -func (s *ServerTestSuite) TestGetHealthCheck() { - req := httptest.NewRequest("GET", "/_health", nil) - handler := http.HandlerFunc(s.server.getHealthCheck) - rr := httptest.NewRecorder() - handler.ServeHTTP(rr, req) - - assert.Equal(s.T(), http.StatusOK, rr.Result().StatusCode) - b, _ := ioutil.ReadAll(rr.Result().Body) - assert.Contains(s.T(), string(b), `{"database":"ok"}`) -} - -func (s *ServerTestSuite) TestNYI() { - req := httptest.NewRequest("GET", "/random_endpoint", nil) - handler := http.HandlerFunc(NYI) - rr := httptest.NewRecorder() - handler.ServeHTTP(rr, req) - - assert.Equal(s.T(), http.StatusOK, rr.Result().StatusCode) - b, _ := ioutil.ReadAll(rr.Result().Body) - assert.Contains(s.T(), string(b), "Not Yet Implemented") -} - -// test ConnectionClose() - -// MintToken(), MintTokenWithDuration() - -func (s *ServerTestSuite) TestNewServerWithBadSigningKey() { - ts := NewServer("test-server", ":9999", "9.99.999", s.info, nil, true, "", 37 * time.Minute) - assert.Nil(s.T(), ts) -} - -func TestServerTestSuite(t *testing.T) { - suite.Run(t, new(ServerTestSuite)) -} diff --git a/ssas/service/tokenblacklist.go b/ssas/service/tokenblacklist.go deleted file mode 100644 index dd5378b14..000000000 --- a/ssas/service/tokenblacklist.go +++ /dev/null @@ -1,137 +0,0 @@ -package service - -import ( - "fmt" - "github.com/CMSgov/bcda-app/ssas" - "github.com/CMSgov/bcda-app/ssas/cfg" - "github.com/patrickmn/go-cache" - "github.com/pborman/uuid" - "time" -) - -var ( - TokenBlacklist Blacklist - // This default cache timeout value should never be used, since individual cache elements have their own timeouts - defaultCacheTimeout = 24*time.Hour - cacheCleanupInterval time.Duration - TokenCacheLifetime time.Duration - cacheRefreshFreq time.Duration - cacheRefreshTicker *time.Ticker -) - -func init() { - cacheCleanupInterval = time.Duration(cfg.GetEnvInt("SSAS_TOKEN_BLACKLIST_CACHE_CLEANUP_MINUTES", 15)) * time.Minute - TokenCacheLifetime = time.Duration(cfg.GetEnvInt("SSAS_TOKEN_BLACKLIST_CACHE_TIMEOUT_MINUTES", 60*24)) * time.Minute - cacheRefreshFreq = time.Duration(cfg.GetEnvInt("SSAS_TOKEN_BLACKLIST_CACHE_REFRESH_MINUTES", 5)) * time.Minute -} - -// This function should only be called by main -func StartBlacklist() { - NewBlacklist(defaultCacheTimeout, cacheCleanupInterval) -} - -// NewBlacklist allows for easy Blacklist{} creation and manipulation during testing, and, outside a test suite, -// should not be called -func NewBlacklist(cacheTimeout time.Duration, cleanupInterval time.Duration) *Blacklist { - // In case a Blacklist timer has already been started: - stopCacheRefreshTicker() - - trackingID := uuid.NewRandom().String() - event := ssas.Event{Op: "InitBlacklist", TrackingID: trackingID} - ssas.OperationStarted(event) - - bl := Blacklist{ID: trackingID} - bl.c = cache.New(cacheTimeout, cleanupInterval) - - if err := bl.LoadFromDatabase(); err != nil { - event.Help = "unable to load blacklist from database: " + err.Error() - ssas.OperationFailed(event) - // Log this failure, but allow the cache to operate. It's conceivable the next cache refresh will work. - // Any alerts should be based on the error logged in BlackList.LoadFromDatabase(). - } else - { - ssas.OperationSucceeded(event) - } - - cacheRefreshTicker = bl.startCacheRefreshTicker(cacheRefreshFreq) - - TokenBlacklist = bl - return &bl -} - -type Blacklist struct { - c *cache.Cache - ID string -} - -// BlacklistToken invalidates the specified tokenID -func (t *Blacklist) BlacklistToken(tokenID string, blacklistExpiration time.Duration) error { - entryDate := time.Now() - expirationDate := entryDate.Add(blacklistExpiration) - if _, err := ssas.CreateBlacklistEntry(tokenID, entryDate, expirationDate); err != nil { - return fmt.Errorf(fmt.Sprintf("unable to blacklist token id %s: %s", tokenID, err.Error())) - } - - // Add to cache only after token is blacklisted in database - ssas.TokenBlacklisted(ssas.Event{Op: "TokenBlacklist", TrackingID: tokenID, TokenID: tokenID}) - t.c.Set(tokenID, entryDate.Unix(), blacklistExpiration) - - return nil -} - -// IsTokenBlacklisted tests whether this tokenID is in the blacklist cache. -// - Tokens should expire before blacklist entries, so a tokenID for a recently expired token may return "true." -// - This queries the cache only, so if a tokenID has been blacklisted on a different instance, it will return "false" -// until the cached blacklist is refreshed from the database. -func (t *Blacklist) IsTokenBlacklisted(tokenID string) bool { - bEvent := ssas.Event{Op: "TokenVerification", TrackingID: t.ID, TokenID: tokenID} - if _, found := t.c.Get(tokenID); found { - ssas.BlacklistedTokenPresented(bEvent) - return true - } - return false -} - -// LoadFromDatabase refreshes unexpired blacklist entries from the database -func (t *Blacklist) LoadFromDatabase() error { - var ( - entries []ssas.BlacklistEntry - err error - ) - - if entries, err = ssas.GetUnexpiredBlacklistEntries(); err != nil { - ssas.CacheSyncFailure(ssas.Event{Op: "BlacklistLoadFromDatabase", TrackingID: t.ID, Help: err.Error()}) - return err - } - - t.c.Flush() - - // If the key already exists in the cache, it will be updated. - for _, entry := range entries { - cacheDuration := time.Since(time.Unix(0, entry.CacheExpiration)) - t.c.Set(entry.Key, entry.EntryDate, cacheDuration) - } - return nil -} - -func (t *Blacklist) startCacheRefreshTicker(refreshFreq time.Duration) *time.Ticker { - event := ssas.Event{Op: "CacheRefreshTicker", TrackingID: t.ID} - ssas.ServiceStarted(event) - - ticker := time.NewTicker(refreshFreq) - - go func() { - for range ticker.C { - // Errors are logged in LoadFromDatabase() - _ = t.LoadFromDatabase() - } - }() - - return ticker -} - -func stopCacheRefreshTicker() { - if cacheRefreshTicker != nil { - cacheRefreshTicker.Stop() - } -} \ No newline at end of file diff --git a/ssas/service/tokenblacklist_test.go b/ssas/service/tokenblacklist_test.go deleted file mode 100644 index 361e187b4..000000000 --- a/ssas/service/tokenblacklist_test.go +++ /dev/null @@ -1,273 +0,0 @@ -package service - -import ( - "github.com/CMSgov/bcda-app/ssas" - "github.com/jinzhu/gorm" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/suite" - "math/rand" - "strconv" - "testing" - "time" -) - -// Using a constant for this makes the tests more readable; any arbitrary value longer than the test execution time -// should work -var ( - expiration = 90*time.Minute - timeExpired = time.Now().Add(time.Minute*-5) - timeNotExpired = time.Now().Add(time.Minute*5) -) - -type TokenCacheTestSuite struct { - suite.Suite - t *Blacklist - db *gorm.DB -} - -func (s *TokenCacheTestSuite) SetupSuite() { - ssas.InitializeBlacklistModels() - s.db = ssas.GetGORMDbConnection() - s.t = NewBlacklist(defaultCacheTimeout, cacheCleanupInterval) -} - -func (s *TokenCacheTestSuite) TearDownSuite() { - s.db.Close() -} - -func (s *TokenCacheTestSuite) TearDownTest() { - s.t.c.Flush() - err := s.db.Exec("DELETE FROM blacklist_entries;").Error - assert.Nil(s.T(), err) -} - -func (s *TokenCacheTestSuite) TestLoadFromDatabaseEmpty() { - key := "tokenID" - - var blackListEntries []ssas.BlacklistEntry - s.db.Unscoped().Find(&blackListEntries) - assert.Len(s.T(), blackListEntries, 0) - if err := s.t.LoadFromDatabase(); err != nil { - assert.FailNow(s.T(), err.Error()) - } - assert.Len(s.T(), s.t.c.Items(), 0) - - if err := s.t.BlacklistToken(key, expiration); err != nil { - assert.FailNow(s.T(), err.Error()) - } - - s.db.Unscoped().Find(&blackListEntries) - assert.Len(s.T(), blackListEntries, 1) - if err := s.t.LoadFromDatabase(); err != nil { - assert.FailNow(s.T(), err.Error()) - } - assert.Len(s.T(), s.t.c.Items(), 1) -} - -func (s *TokenCacheTestSuite) TestLoadFromDatabaseSomeExpired() { - expiredKey := "expiredKey" - notExpiredKey := "notExpiredKey" - var err error - entryDate := timeExpired.Unix() - expired := timeExpired.UnixNano() - notExpired := timeNotExpired.UnixNano() - entryExpired := ssas.BlacklistEntry{Key: expiredKey, EntryDate: entryDate, CacheExpiration: expired} - entryDuplicateExpired := ssas.BlacklistEntry{Key: notExpiredKey, EntryDate: entryDate, CacheExpiration: expired} - entryNotExpired := ssas.BlacklistEntry{Key: notExpiredKey, EntryDate: entryDate, CacheExpiration: notExpired} - - if err = s.db.Save(&entryExpired).Error; err != nil { - assert.FailNow(s.T(), err.Error()) - } - if err = s.db.Save(&entryDuplicateExpired).Error; err != nil { - assert.FailNow(s.T(), err.Error()) - } - - if err = s.t.LoadFromDatabase(); err != nil { - assert.FailNow(s.T(), err.Error()) - } - - assert.Len(s.T(), s.t.c.Items(), 0) - assert.False(s.T(), s.t.IsTokenBlacklisted(expiredKey)) - // This result changes after putting a new entry in the database that has not expired. - assert.False(s.T(), s.t.IsTokenBlacklisted(notExpiredKey)) - - if err = s.db.Save(&entryNotExpired).Error; err != nil { - assert.FailNow(s.T(), err.Error()) - } - if err = s.t.LoadFromDatabase(); err != nil { - assert.FailNow(s.T(), err.Error()) - } - - assert.Len(s.T(), s.t.c.Items(), 1) - assert.False(s.T(), s.t.IsTokenBlacklisted(expiredKey)) - // The second time we check, this key is blacklisted - assert.True(s.T(), s.t.IsTokenBlacklisted(notExpiredKey)) -} - -func (s *TokenCacheTestSuite) TestLoadFromDatabase() { - var err error - entryDate := timeExpired.Unix() - expiration := timeNotExpired.UnixNano() - e1 := ssas.BlacklistEntry{Key: "key1", EntryDate: entryDate, CacheExpiration: expiration} - e2 := ssas.BlacklistEntry{Key: "key2", EntryDate: entryDate, CacheExpiration: expiration} - - if err = s.db.Save(&e1).Error; err != nil { - assert.FailNow(s.T(), err.Error()) - } - if err = s.db.Save(&e2).Error; err != nil { - assert.FailNow(s.T(), err.Error()) - } - - if err = s.t.LoadFromDatabase(); err != nil { - assert.FailNow(s.T(), err.Error()) - } - - assert.Len(s.T(), s.t.c.Items(), 2) - assert.True(s.T(), s.t.IsTokenBlacklisted(e1.Key)) - assert.True(s.T(), s.t.IsTokenBlacklisted(e2.Key)) - - obj1, _, found := s.t.c.GetWithExpiration(e1.Key) - assert.True(s.T(), found) - insertedDate1, ok := obj1.(int64) - assert.True(s.T(), ok) - assert.Equal(s.T(), entryDate, insertedDate1) - - obj2, _, found := s.t.c.GetWithExpiration(e2.Key) - assert.True(s.T(), found) - insertedDate2, ok := obj2.(int64) - assert.True(s.T(), ok) - assert.Equal(s.T(), entryDate, insertedDate2) -} - -func (s *TokenCacheTestSuite) TestIsTokenBlacklistedTrue() { - key := strconv.Itoa(rand.Int()) - err := s.t.c.Add(key, "value does not matter", expiration) - if err != nil { - assert.FailNow(s.T(), "unable to set cache value: " + err.Error()) - } - assert.True(s.T(), s.t.IsTokenBlacklisted(key)) -} - -func (s *TokenCacheTestSuite) TestIsTokenBlacklistedExpired() { - minimalDuration := 1*time.Nanosecond - key := strconv.Itoa(rand.Int()) - err := s.t.c.Add(key, "value does not matter", minimalDuration) - if err != nil { - assert.FailNow(s.T(), "unable to set cache value: " + err.Error()) - } - time.Sleep(minimalDuration*5) - assert.False(s.T(), s.t.IsTokenBlacklisted(key)) -} - -func (s *TokenCacheTestSuite) TestIsTokenBlacklistedFalse() { - key := strconv.Itoa(rand.Int()) - assert.False(s.T(), s.t.IsTokenBlacklisted(key)) -} - -func (s *TokenCacheTestSuite) TestBlacklistToken() { - key := strconv.Itoa(rand.Int()) - if err := s.t.BlacklistToken(key, expiration); err != nil { - assert.FailNow(s.T(), err.Error()) - } - - _, found := s.t.c.Get(key) - assert.True(s.T(), found) - - entries, err := ssas.GetUnexpiredBlacklistEntries() - assert.Nil(s.T(), err) - assert.Len(s.T(), entries, 1) - assert.Equal(s.T(), key, entries[0].Key) -} - -func (s *TokenCacheTestSuite) TestStartCacheRefreshTicker() { - stopCacheRefreshTicker() - - var err error - entryDate := timeExpired.Unix() - expiration := timeNotExpired.UnixNano() - key1 := "key1" - key2 := "key2" - - e1 := ssas.BlacklistEntry{Key: key1, EntryDate: entryDate, CacheExpiration: expiration} - if err = s.db.Save(&e1).Error; err != nil { - assert.FailNow(s.T(), err.Error()) - } - - assert.False(s.T(), s.t.IsTokenBlacklisted(key1)) - assert.False(s.T(), s.t.IsTokenBlacklisted(key2)) - - ticker := s.t.startCacheRefreshTicker(time.Millisecond*250) - defer ticker.Stop() - - time.Sleep(time.Millisecond*350) - assert.True(s.T(), s.t.IsTokenBlacklisted(key1)) - assert.False(s.T(), s.t.IsTokenBlacklisted(key2)) - - e2 := ssas.BlacklistEntry{Key: key2, EntryDate: entryDate, CacheExpiration: expiration} - if err = s.db.Save(&e2).Error; err != nil { - assert.FailNow(s.T(), err.Error()) - } - - time.Sleep(time.Millisecond*250) - assert.True(s.T(), s.t.IsTokenBlacklisted(key1)) - assert.True(s.T(), s.t.IsTokenBlacklisted(key2)) -} - -func (s *TokenCacheTestSuite) TestBlacklistTokenKeyExists() { - key := strconv.Itoa(rand.Int()) - - // Place key in blacklist - if err := s.t.BlacklistToken(key, expiration); err != nil { - assert.FailNow(s.T(), err.Error()) - } - // Verify key exists in cache - obj1, found := s.t.c.Get(key) - assert.True(s.T(), found) - - // Verify key exists in database - entries1, err := ssas.GetUnexpiredBlacklistEntries() - assert.Nil(s.T(), err) - assert.Len(s.T(), entries1, 1) - assert.Equal(s.T(), key, entries1[0].Key) - assert.Equal(s.T(), obj1, entries1[0].EntryDate) - - // The value stored is the current time expressed as in Unix time. - // Wait to make sure the new blacklist entry has a different value - time.Sleep(2*time.Second) - - // Place key in cache a second time; the expiration will be different - if err := s.t.BlacklistToken(key, expiration); err != nil { - assert.FailNow(s.T(), err.Error()) - } - - // Verify retrieving key from cache gets new value (timestamp) - obj2, found := s.t.c.Get(key) - assert.True(s.T(), found) - assert.NotEqual(s.T(), obj1, obj2) - - // Verify both keys are in the database, and that they are in time order - entries2, err := ssas.GetUnexpiredBlacklistEntries() - assert.Nil(s.T(), err) - // 2 entries were added in this test; 1 was added in middleware_test - // depending on which order the tests are completed, sometimes there are 2 entries and sometimes there are 3 - assert.Len(s.T(), entries2, 2) - assert.Equal(s.T(), key, entries2[1].Key) - assert.Equal(s.T(), obj2, entries2[1].EntryDate) - - // Verify that the blacklisted object changed in both cache and database - assert.NotEqual(s.T(), obj1, obj2) - assert.NotEqual(s.T(), entries1[0].CacheExpiration, entries2[1].CacheExpiration) - - // Show that loading the cache from the database preserves the most recent entry, even if two - // objects with the same key are unexpired - err = s.t.LoadFromDatabase() - assert.Nil(s.T(), err) - obj3, found := s.t.c.Get(key) - assert.True(s.T(), found) - assert.Equal(s.T(), obj2, obj3) - assert.NotEqual(s.T(), obj1, obj3) -} - -func TestTokenCacheTestSuite(t *testing.T) { - suite.Run(t, new(TokenCacheTestSuite)) -} \ No newline at end of file diff --git a/ssas/systems.go b/ssas/systems.go deleted file mode 100644 index 57146ff4b..000000000 --- a/ssas/systems.go +++ /dev/null @@ -1,570 +0,0 @@ -package ssas - -import ( - "crypto/rand" - "crypto/rsa" - "crypto/x509" - "encoding/pem" - "errors" - "fmt" - "io" - "io/ioutil" - "log" - "os" - "strconv" - "time" - - "github.com/jinzhu/gorm" - "github.com/pborman/uuid" -) - -var DefaultScope string - -const CredentialExpiration = 90 * 24 * time.Hour - -func init() { - getEnvVars() -} - -func getEnvVars() { - DefaultScope = os.Getenv("SSAS_DEFAULT_SYSTEM_SCOPE") - - if DefaultScope == "" { - if os.Getenv("DEBUG") == "true" { - DefaultScope = "bcda-api" - return - } - ServiceHalted(Event{Help: "SSAS_DEFAULT_SYSTEM_SCOPE environment value must be set"}) - panic("SSAS_DEFAULT_SYSTEM_SCOPE environment value must be set") - } -} - -/* - InitializeSystemModels will call gorm.DB.AutoMigrate() for models associated with systems, and set up foreign key - relationships for those models if needed -*/ -func InitializeSystemModels() *gorm.DB { - log.Println("Initialize system models") - db := GetGORMDbConnection() - defer Close(db) - - db.AutoMigrate( - &System{}, - &EncryptionKey{}, - &Secret{}, - ) - - db.Model(&System{}).AddForeignKey("group_id", "groups(group_id)", "RESTRICT", "RESTRICT") - db.Model(&EncryptionKey{}).AddForeignKey("system_id", "systems(id)", "RESTRICT", "RESTRICT") - db.Model(&Secret{}).AddForeignKey("system_id", "systems(id)", "RESTRICT", "RESTRICT") - - return db -} - -type System struct { - gorm.Model - GroupID string `json:"group_id"` - ClientID string `json:"client_id" gorm:"unique_index:idx_client"` - SoftwareID string `json:"software_id"` - ClientName string `json:"client_name"` - APIScope string `json:"api_scope"` - EncryptionKeys []EncryptionKey `json:"encryption_keys,omitempty"` - Secrets []Secret `json:"secrets,omitempty"` -} - -type EncryptionKey struct { - gorm.Model - Body string `json:"body"` - System System `gorm:"foreignkey:SystemID;association_foreignkey:ID"` - SystemID uint `json:"system_id"` -} - -type Secret struct { - gorm.Model - Hash string `json:"hash"` - System System `gorm:"foreignkey:SystemID;association_foreignkey:ID"` - SystemID uint `json:"system_id"` -} - -type AuthRegData struct { - GroupID string - AllowedGroupIDs []string - OktaID string -} - -/* - SaveSecret should be provided with a secret hashed with ssas.NewHash(), which will - be saved to the secrets table and associated with the current system. -*/ -func (system *System) SaveSecret(hashedSecret string) error { - db := GetGORMDbConnection() - defer Close(db) - - secret := Secret{ - Hash: hashedSecret, - SystemID: system.ID, - } - - if err := system.DeactivateSecrets(); err != nil { - return err - } - - if err := db.Create(&secret).Error; err != nil { - return fmt.Errorf("could not save secret for clientID %s: %s", system.ClientID, err.Error()) - } - SecretCreated(Event{Op: "SaveSecret", TrackingID: uuid.NewRandom().String(), ClientID: system.ClientID}) - - return nil -} - -/* - GetSecret will retrieve the hashed secret associated with the current system. -*/ -func (system *System) GetSecret() (string, error) { - db := GetGORMDbConnection() - defer Close(db) - - secret := Secret{} - - err := db.Where("system_id = ?", system.ID).First(&secret).Error - if err != nil { - return "", fmt.Errorf("unable to get hashed secret for clientID %s: %s", system.ClientID, err.Error()) - } - - if secret.Hash == "" { - return "", fmt.Errorf("stored hash of secret for clientID %s is blank", system.ClientID) - } - - return secret.Hash, nil -} - -/* - DeactivateSecrets soft deletes secrets associated with the system. -*/ -func (system *System) DeactivateSecrets() error { - db := GetGORMDbConnection() - defer Close(db) - err := db.Where("system_id = ?", system.ID).Delete(&Secret{}).Error - if err != nil { - return fmt.Errorf("unable to soft delete previous secrets for clientID %s: %s", system.ClientID, err.Error()) - } - return nil -} - -/* - GetEncryptionKey retrieves the key associated with the current system. -*/ -func (system *System) GetEncryptionKey(trackingID string) (EncryptionKey, error) { - db := GetGORMDbConnection() - defer Close(db) - - getKeyEvent := Event{Op: "GetEncryptionKey", TrackingID: trackingID, ClientID: system.ClientID} - OperationStarted(getKeyEvent) - - var encryptionKey EncryptionKey - err := db.Where("system_id = ?", system.ID).Find(&encryptionKey).Error - if err != nil { - OperationFailed(getKeyEvent) - return encryptionKey, fmt.Errorf("cannot find key for clientID %s: %s", system.ClientID, err.Error()) - } - - OperationSucceeded(getKeyEvent) - return encryptionKey, nil -} - -/* - SavePublicKey should be provided with a public key in PEM format, which will be saved - to the encryption_keys table and associated with the current system. -*/ -func (system *System) SavePublicKey(publicKey io.Reader) error { - db := GetGORMDbConnection() - defer Close(db) - - k, err := ioutil.ReadAll(publicKey) - if err != nil { - return fmt.Errorf("cannot read public key for clientID %s: %s", system.ClientID, err.Error()) - } - - key, err := ReadPublicKey(string(k)) - if err != nil { - return fmt.Errorf("invalid public key for clientID %s: %s", system.ClientID, err.Error()) - } - if key == nil { - return fmt.Errorf("invalid public key for clientID %s", system.ClientID) - } - - encryptionKey := EncryptionKey{ - Body: string(k), - SystemID: system.ID, - } - - // Only one key should be valid per system. Soft delete the currently active key, if any. - err = db.Where("system_id = ?", system.ID).Delete(&EncryptionKey{}).Error - if err != nil { - return fmt.Errorf("unable to soft delete previous encryption keys for clientID %s: %s", system.ClientID, err.Error()) - } - - err = db.Create(&encryptionKey).Error - if err != nil { - return fmt.Errorf("could not save public key for clientID %s: %s", system.ClientID, err.Error()) - } - - return nil -} - -/* - RevokeSystemKeyPair soft deletes the active encryption key - for the specified system so that it can no longer be used -*/ -func (system *System) RevokeSystemKeyPair() error { - db := GetGORMDbConnection() - defer Close(db) - - var encryptionKey EncryptionKey - - err := db.Where("system_id = ?", system.ID).Find(&encryptionKey).Error - if err != nil { - return err - } - - err = db.Delete(&encryptionKey).Error - if err != nil { - return err - } - - return nil -} - -/* - GenerateSystemKeyPair creates a keypair for a system. The public key is saved to the database and the private key is returned. -*/ -func (system *System) GenerateSystemKeyPair() (string, error) { - db := GetGORMDbConnection() - defer Close(db) - - var key EncryptionKey - if !db.Where("system_id = ?", system.ID).Find(&key).RecordNotFound() { - return "", fmt.Errorf("encryption keypair already exists for system ID %d", system.ID) - } - - privateKey, err := rsa.GenerateKey(rand.Reader, 2048) - if err != nil { - return "", fmt.Errorf("could not create key for system ID %d: %s", system.ID, err.Error()) - } - - publicKeyPKIX, err := x509.MarshalPKIXPublicKey(&privateKey.PublicKey) - if err != nil { - return "", fmt.Errorf("could not marshal public key for system ID %d: %s", system.ID, err.Error()) - } - - publicKeyBytes := pem.EncodeToMemory(&pem.Block{ - Type: "RSA PUBLIC KEY", - Bytes: publicKeyPKIX, - }) - - encryptionKey := EncryptionKey{ - Body: string(publicKeyBytes), - SystemID: system.ID, - } - - err = db.Create(&encryptionKey).Error - if err != nil { - return "", fmt.Errorf("could not save key for system ID %d: %s", system.ID, err.Error()) - } - - privateKeyBytes := pem.EncodeToMemory( - &pem.Block{ - Type: "RSA PRIVATE KEY", - Bytes: x509.MarshalPKCS1PrivateKey(privateKey), - }, - ) - - return string(privateKeyBytes), nil -} - -type Credentials struct { - ClientID string `json:"client_id"` - ClientSecret string `json:"client_secret"` - SystemID string `json:"system_id"` - ClientName string `json:"client_name"` - ExpiresAt time.Time `json:"expires_at"` -} - -/* - RegisterSystem will save a new system and public key after verifying provided details for validity. It returns - a ssas.Credentials struct including the generated clientID and secret. -*/ -func RegisterSystem(clientName string, groupID string, scope string, publicKeyPEM string, trackingID string) (Credentials, error) { - db := GetGORMDbConnection() - defer Close(db) - - // A system is not valid without an active public key and a hashed secret. However, they are stored separately in the - // encryption_keys and secrets tables, requiring multiple INSERT statement. To ensure we do not get into an invalid state, - // wrap the two INSERT statements in a transaction. - tx := db.Begin() - defer func() { - if r := recover(); r != nil { - tx.Rollback() - } - }() - - creds := Credentials{} - clientID := uuid.NewRandom().String() - - // The caller of this function should have logged OperationCalled() with the same trackingID - regEvent := Event{Op: "RegisterClient", TrackingID: trackingID, ClientID: clientID} - OperationStarted(regEvent) - - if clientName == "" { - regEvent.Help = "clientName is required" - OperationFailed(regEvent) - return creds, errors.New(regEvent.Help) - } - - if scope == "" { - scope = DefaultScope - } else if scope != DefaultScope { - regEvent.Help = "scope must be: " + DefaultScope - OperationFailed(regEvent) - return creds, errors.New(regEvent.Help) - } - - _, err := ReadPublicKey(publicKeyPEM) - if err != nil { - regEvent.Help = "error in public key: " + err.Error() - OperationFailed(regEvent) - return creds, errors.New("error in public key") - } - - system := System{ - GroupID: groupID, - ClientID: clientID, - ClientName: clientName, - APIScope: scope, - } - - err = tx.Create(&system).Error - if err != nil { - regEvent.Help = fmt.Sprintf("could not save system for clientID %s, groupID %s: %s", clientID, groupID, err.Error()) - OperationFailed(regEvent) - // Returned errors are passed to API callers, and should include enough information to correct invalid submissions - // without revealing implementation details. CLI callers will be able to review logs for more information. - return creds, errors.New("internal system error") - } - - encryptionKey := EncryptionKey{ - Body: publicKeyPEM, - SystemID: system.ID, - } - - // While the createEncryptionKey method below _could_ be called here (and system.SaveSecret() below), - // we would lose the benefit of the transaction. - err = tx.Create(&encryptionKey).Error - if err != nil { - regEvent.Help = fmt.Sprintf("could not save public key for clientID %s, groupID %s: %s", clientID, groupID, err.Error()) - OperationFailed(regEvent) - return creds, errors.New("internal system error") - } - - clientSecret, err := GenerateSecret() - if err != nil { - regEvent.Help = fmt.Sprintf("cannot generate secret for clientID %s: %s", system.ClientID, err.Error()) - OperationFailed(regEvent) - return creds, errors.New("internal system error") - } - - hashedSecret, err := NewHash(clientSecret) - if err != nil { - regEvent.Help = fmt.Sprintf("cannot generate hash of secret for clientID %s: %s", system.ClientID, err.Error()) - OperationFailed(regEvent) - return creds, errors.New("internal system error") - } - - secret := Secret{ - Hash: hashedSecret.String(), - SystemID: system.ID, - } - - err = tx.Create(&secret).Error - if err != nil { - regEvent.Help = fmt.Sprintf("cannot save secret for clientID %s: %s", system.ClientID, err.Error()) - OperationFailed(regEvent) - return creds, errors.New("internal system error") - } - SecretCreated(regEvent) - - err = tx.Commit().Error - if err != nil { - regEvent.Help = fmt.Sprintf("could not commit transaction for new system with groupID %s: %s", groupID, err.Error()) - OperationFailed(regEvent) - return creds, errors.New("internal system error") - } - - creds.SystemID = fmt.Sprint(system.ID) - creds.ClientID = system.ClientID - creds.ClientSecret = clientSecret - creds.ClientName = system.ClientName - creds.ExpiresAt = time.Now().Add(CredentialExpiration) - - OperationSucceeded(regEvent) - return creds, nil -} - -// DataForSystem returns the group extra data associated with this system -func XDataFor(system System) (string, error) { - group, err := GetGroupByGroupID(system.GroupID) - if err != nil { - return "", fmt.Errorf("no group for system %d; %s", system.ID, err) - } - Logger.Info("group xdata '", group, "'") - // strconv.Unquote here? - return group.XData, nil -} - -// GetSystemsByGroupID returns the systems associated with the provided group_id -func GetSystemsByGroupID(groupId string) ([]System, error) { - var ( - db = GetGORMDbConnection() - systems []System - err error - ) - defer Close(db) - - if err = db.Where("group_id = ?", groupId).Find(&systems).Error; err != nil { - err = fmt.Errorf("no Systems found with group_id %s", groupId) - } - return systems, err -} - -// GetSystemByClientID returns the system associated with the provided clientID -func GetSystemByClientID(clientID string) (System, error) { - var ( - db = GetGORMDbConnection() - system System - err error - ) - defer Close(db) - - if db.Find(&system, "client_id = ?", clientID).RecordNotFound() { - err = fmt.Errorf("no System record found for client %s", clientID) - } - return system, err -} - -// GetSystemByID returns the system associated with the provided ID -func GetSystemByID(id string) (System, error) { - var ( - db = GetGORMDbConnection() - system System - err error - ) - defer Close(db) - - if _, err = strconv.ParseUint(id, 10, 64); err != nil { - return System{}, fmt.Errorf("invalid input %s; %s", id, err) - } - // must use the explicit where clause here because the id argument is a string - if err = db.Find(&system, "id = ?", id).Error; err != nil { - err = fmt.Errorf("no System record found with ID %s", id) - } - return system, err -} - -func GenerateSecret() (string, error) { - b := make([]byte, 40) - _, err := rand.Read(b) - if err != nil { - return "", err - } - - return fmt.Sprintf("%x", b), nil -} - -// ResetSecret creates a new secret for the current system. -func (system *System) ResetSecret(trackingID string) (Credentials, error) { - db := GetGORMDbConnection() - defer Close(db) - - creds := Credentials{} - - newSecretEvent := Event{Op: "ResetSecret", TrackingID: trackingID, ClientID: system.ClientID} - OperationStarted(newSecretEvent) - - secretString, err := GenerateSecret() - if err != nil { - newSecretEvent.Help = fmt.Sprintf("could not reset secret for clientID %s: %s", system.ClientID, err.Error()) - OperationFailed(newSecretEvent) - return creds, errors.New("internal system error") - } - - hashedSecret, err := NewHash(secretString) - if err != nil { - newSecretEvent.Help = fmt.Sprintf("could not reset secret for clientID %s: %s", system.ClientID, err.Error()) - OperationFailed(newSecretEvent) - return creds, errors.New("internal system error") - } - - hashedSecretString := hashedSecret.String() - if err = system.SaveSecret(hashedSecretString); err != nil { - newSecretEvent.Help = fmt.Sprintf("could not reset secret for clientID %s: %s", system.ClientID, err.Error()) - OperationFailed(newSecretEvent) - return creds, errors.New("internal system error") - } - - OperationSucceeded(newSecretEvent) - - creds.ClientID = system.ClientID - creds.ClientSecret = secretString - creds.ClientName = system.ClientName - creds.ExpiresAt = time.Now().Add(CredentialExpiration) - return creds, nil -} - -// CleanDatabase deletes the given group and associated systems, encryption keys, and secrets. -func CleanDatabase(group Group) error { - var ( - system System - encryptionKey EncryptionKey - secret Secret - systemIds []int - db = GetGORMDbConnection() - ) - defer Close(db) - - if group.ID == 0 { - return fmt.Errorf("invalid group.ID") - } - - foundGroup := Group{GroupID: group.GroupID} - err := db.Unscoped().Find(&foundGroup).Error - if err != nil { - return fmt.Errorf("unable to find group %s: %s", group.GroupID, err.Error()) - } - - err = db.Table("systems").Where("group_id = ?", group.GroupID).Pluck("ID", &systemIds).Error - if err != nil { - Logger.Errorf("unable to find associated systems: %s", err.Error()) - } else { - err = db.Unscoped().Where("system_id IN (?)", systemIds).Delete(&encryptionKey).Error - if err != nil { - Logger.Errorf("unable to delete encryption keys: %s", err.Error()) - } - - err = db.Unscoped().Where("system_id IN (?)", systemIds).Delete(&secret).Error - if err != nil { - Logger.Errorf("unable to delete secrets: %s", err.Error()) - } - - err = db.Unscoped().Where("id IN (?)", systemIds).Delete(&system).Error - if err != nil { - Logger.Errorf("unable to delete systems: %s", err.Error()) - } - } - - err = db.Unscoped().Delete(&group).Error - if err != nil { - return fmt.Errorf("unable to delete group: %s", err.Error()) - } - - return nil -} diff --git a/ssas/systems_test.go b/ssas/systems_test.go deleted file mode 100644 index 2e330fe7f..000000000 --- a/ssas/systems_test.go +++ /dev/null @@ -1,707 +0,0 @@ -package ssas - -import ( - "bytes" - "crypto/rand" - "crypto/rsa" - "crypto/x509" - "encoding/json" - "encoding/pem" - "fmt" - "os" - "strconv" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/jinzhu/gorm" - "github.com/pborman/uuid" - "github.com/stretchr/testify/suite" -) - -type SystemsTestSuite struct { - suite.Suite - db *gorm.DB -} - -func (s *SystemsTestSuite) SetupSuite() { - s.db = GetGORMDbConnection() - InitializeGroupModels() - InitializeSystemModels() -} - -func (s *SystemsTestSuite) TearDownSuite() { - Close(s.db) -} - -func (s *SystemsTestSuite) AfterTest() { -} - -func (s *SystemsTestSuite) TestRevokeSystemKeyPair() { - assert := s.Assert() - - group := Group{GroupID: "A00001"} - s.db.Save(&group) - system := System{GroupID: group.GroupID, ClientID: "test-revoke-system-key-pair-client"} - - err := system.RevokeSystemKeyPair() - assert.NotNil(err) - - s.db.Save(&system) - encryptionKey := EncryptionKey{SystemID: system.ID} - s.db.Save(&encryptionKey) - - err = system.RevokeSystemKeyPair() - assert.Nil(err) - assert.Empty(system.EncryptionKeys) - s.db.Unscoped().Find(&encryptionKey) - assert.NotNil(encryptionKey.DeletedAt) - - err = CleanDatabase(group) - assert.Nil(err) -} - -func (s *SystemsTestSuite) TestGenerateSystemKeyPair() { - assert := s.Assert() - - group := Group{GroupID: "abcdef123456"} - err := s.db.Create(&group).Error - if err != nil { - s.FailNow(err.Error()) - } - - system := System{GroupID: group.GroupID} - err = s.db.Create(&system).Error - if err != nil { - s.FailNow(err.Error()) - } - - privateKeyStr, err := system.GenerateSystemKeyPair() - assert.NoError(err) - assert.NotEmpty(privateKeyStr) - - privKeyBlock, _ := pem.Decode([]byte(privateKeyStr)) - if privKeyBlock == nil { - s.FailNow("unable to decode private key ", privateKeyStr) - } - privateKey, err := x509.ParsePKCS1PrivateKey(privKeyBlock.Bytes) - if err != nil { - s.FailNow(err.Error()) - } - - var pubEncrKey EncryptionKey - err = s.db.First(&pubEncrKey, "system_id = ?", system.ID).Error - if err != nil { - s.FailNow(err.Error()) - } - pubKeyBlock, _ := pem.Decode([]byte(pubEncrKey.Body)) - publicKey, err := x509.ParsePKIXPublicKey(pubKeyBlock.Bytes) - if err != nil { - s.FailNow(err.Error()) - } - assert.Equal(&privateKey.PublicKey, publicKey) - - err = CleanDatabase(group) - assert.Nil(err) -} - -func (s *SystemsTestSuite) TestGenerateSystemKeyPair_AlreadyExists() { - assert := s.Assert() - - group := Group{GroupID: "bcdefa234561"} - err := s.db.Create(&group).Error - if err != nil { - s.FailNow(err.Error()) - } - - system := System{GroupID: group.GroupID} - err = s.db.Create(&system).Error - if err != nil { - s.FailNow(err.Error()) - } - - encrKey := EncryptionKey{ - SystemID: system.ID, - } - err = s.db.Create(&encrKey).Error - if err != nil { - s.FailNow(err.Error()) - } - - privateKey, err := system.GenerateSystemKeyPair() - systemIDStr := strconv.FormatUint(uint64(system.ID), 10) - assert.EqualError(err, "encryption keypair already exists for system ID "+systemIDStr) - assert.Empty(privateKey) - - err = CleanDatabase(group) - assert.Nil(err) -} - -func (s *SystemsTestSuite) TestGetEncryptionKey() { - group := Group{GroupID: "test-get-encryption-key-group"} - err := s.db.Create(&group).Error - if err != nil { - s.FailNow(err.Error()) - } - - system := System{GroupID: group.GroupID} - err = s.db.Create(&system).Error - if err != nil { - s.FailNow(err.Error()) - } - - pubKey := `-----BEGIN RSA PUBLIC KEY----- -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsZYpl2VjUja8VgkgoQ9K -lgjvcjwaQZ7pLGrIA/BQcm+KnCIYOHaDH15eVDKQ+M2qE4FHRwLec/DTqlwg8TkT -IYjBnXgN1Sg18y+SkSYYklO4cxlvMO3V8gaot9amPmt4YbpgG7CyZ+BOUHuoGBTh -z2v9wLlK4zPAs3pLln3R/4NnGFKw2Eku2JVFTotQ03gSmSzesZixicw8LxgYKbNV -oyTpERFansw6BbCJe7AP90rmaxCx80NiewFq+7ncqMbCMcqeUuCwk8MjS6bjvpcC -htFCqeRi6AAUDRg0pcG8yoM+jo13Z5RJPOIf3ofohncfH5wr5Q7qiOCE5VH4I7cp -OwIDAQAB ------END RSA PUBLIC KEY-----` - - origKey := EncryptionKey{ - SystemID: system.ID, - Body: pubKey, - } - err = s.db.Create(&origKey).Error - if err != nil { - s.FailNow(err.Error()) - } - - key, err := system.GetEncryptionKey("") - assert.Nil(s.T(), err) - assert.Equal(s.T(), pubKey, key.Body) - - _ = CleanDatabase(group) -} - -func (s *SystemsTestSuite) TestSystemSavePublicKey() { - assert := s.Assert() - - clientID := uuid.NewRandom().String() - groupID := "T33333" - - // Setup Group and System - group := Group{GroupID: groupID} - err := s.db.Create(&group).Error - assert.Nil(err) - system := System{ClientID: clientID, GroupID: groupID} - err = s.db.Create(&system).Error - assert.Nil(err) - - // Setup key - keyPair, err := rsa.GenerateKey(rand.Reader, 2048) - assert.Nil(err, "error creating random test keypair") - publicKeyPKIX, err := x509.MarshalPKIXPublicKey(&keyPair.PublicKey) - assert.Nil(err, "unable to marshal public key") - publicKeyBytes := pem.EncodeToMemory(&pem.Block{ - Type: "RSA PUBLIC KEY", - Bytes: publicKeyPKIX, - }) - assert.NotNil(publicKeyBytes, "unexpectedly empty public key byte slice") - - // Save key - err = system.SavePublicKey(bytes.NewReader(publicKeyBytes)) - if err != nil { - assert.FailNow("error saving key: " + err.Error()) - } - - // Retrieve and verify - storedKey, err := system.GetEncryptionKey("") - assert.Nil(err) - assert.NotNil(storedKey) - assert.Equal(storedKey.Body, string(publicKeyBytes)) - - err = CleanDatabase(group) - assert.Nil(err) -} - -func (s *SystemsTestSuite) TestSystemSavePublicKeyInvalidKey() { - assert := s.Assert() - - clientID := uuid.NewRandom().String() - groupID := "T44444" - - // Setup Group and System - group := Group{GroupID: groupID} - err := s.db.Create(&group).Error - assert.Nil(err) - system := System{ClientID: clientID, GroupID: groupID} - err = s.db.Create(&system).Error - assert.Nil(err) - - emptyPEM := "-----BEGIN RSA PUBLIC KEY----- -----END RSA PUBLIC KEY-----" - invalidPEM := - `-----BEGIN RSA PUBLIC KEY----- -z2v9wLlK4zPAs3pLln3R/4NnGFKw2Eku2JVFTotQ03gSmSzesZixicw8LxgYKbNV -oyTpERFansw6BbCJe7AP90rmaxCx80NiewFq+7ncqMbCMcqeUuCwk8MjS6bjvpcC -htFCqeRi6AAUDRg0pcG8yoM+jo13Z5RJPOIf3ofohncfH5wr5Q7qiOCE5VH4I7cp -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsZYpl2VjUja8VgkgoQ9K -lgjvcjwaQZ7pLGrIA/BQcm+KnCIYOHaDH15eVDKQ+M2qE4FHRwLec/DTqlwg8TkT -IYjBnXgN1Sg18y+SkSYYklO4cxlvMO3V8gaot9amPmt4YbpgG7CyZ+BOUHuoGBTh -OwIDAQAB ------END RSA PUBLIC KEY-----` - keyPair, err := rsa.GenerateKey(rand.Reader, 512) - assert.Nil(err, "unable to generate key pair") - publicKeyPKIX, err := x509.MarshalPKIXPublicKey(&keyPair.PublicKey) - assert.Nil(err, "unable to marshal public key") - lowBitPubKey := pem.EncodeToMemory(&pem.Block{ - Type: "RSA PUBLIC KEY", - Bytes: publicKeyPKIX, - }) - assert.NotNil(lowBitPubKey, "unexpectedly empty public key byte slice") - - err = system.SavePublicKey(strings.NewReader("")) - assert.NotNil(err, "empty string should not be saved") - - err = system.SavePublicKey(strings.NewReader(emptyPEM)) - assert.NotNil(err, "empty PEM should not be saved") - - err = system.SavePublicKey(strings.NewReader(invalidPEM)) - assert.NotNil(err, "invalid PEM should not be saved") - - err = system.SavePublicKey(bytes.NewReader(lowBitPubKey)) - assert.NotNil(err, "insecure public key should not be saved") - - err = CleanDatabase(group) - assert.Nil(err) -} - -func (s *SystemsTestSuite) TestSystemPublicKeyEmpty() { - assert := s.Assert() - - clientID := uuid.NewRandom().String() - groupID := "T22222" - - // Setup Group and System - group := Group{GroupID: groupID} - err := s.db.Create(&group).Error - assert.Nil(err) - system := System{ClientID: clientID, GroupID: groupID} - err = s.db.Create(&system).Error - assert.Nil(err) - - emptyPEM := "-----BEGIN RSA PUBLIC KEY----- -----END RSA PUBLIC KEY-----" - validPEM, err := generatePublicKey(2048) - assert.Nil(err) - - err = system.SavePublicKey(strings.NewReader("")) - assert.EqualError(err, fmt.Sprintf("invalid public key for clientID %s: not able to decode PEM-formatted public key", clientID)) - k, err := system.GetEncryptionKey("") - assert.EqualError(err, fmt.Sprintf("cannot find key for clientID %s: record not found", clientID)) - assert.Empty(k, "Empty string does not yield empty encryption key!") - err = system.SavePublicKey(strings.NewReader(emptyPEM)) - assert.EqualError(err, fmt.Sprintf("invalid public key for clientID %s: not able to decode PEM-formatted public key", clientID)) - k, err = system.GetEncryptionKey("") - assert.EqualError(err, fmt.Sprintf("cannot find key for clientID %s: record not found", clientID)) - assert.Empty(k, "Empty PEM key does not yield empty encryption key!") - err = system.SavePublicKey(strings.NewReader(validPEM)) - assert.Nil(err) - k, err = system.GetEncryptionKey("") - assert.Nil(err) - assert.NotEmpty(k, "Valid PEM key yields empty public key!") - - err = CleanDatabase(group) - assert.Nil(err) -} - -func (s *SystemsTestSuite) TestEncryptionKeyModel() { - assert := s.Assert() - - group := Group{GroupID: "A00000"} - s.db.Save(&group) - - system := System{GroupID: "A00000"} - s.db.Save(&system) - - systemIDStr := strconv.FormatUint(uint64(system.ID), 10) - encryptionKeyBytes := []byte(`{"body": "this is a public key", "system_id": ` + systemIDStr + `}`) - encryptionKey := EncryptionKey{} - err := json.Unmarshal(encryptionKeyBytes, &encryptionKey) - assert.Nil(err) - - err = s.db.Save(&encryptionKey).Error - assert.Nil(err) - - err = CleanDatabase(group) - assert.Nil(err) -} - -func (s *SystemsTestSuite) TestGetSystemByClientIDSuccess() { - assert := s.Assert() - - group := Group{GroupID: "abcdef123456"} - err := s.db.Create(&group).Error - if err != nil { - s.FailNow(err.Error()) - } - - system := System{GroupID: group.GroupID, ClientID: "987654zyxwvu", ClientName: "Client with System"} - err = s.db.Create(&system).Error - if err != nil { - s.FailNow(err.Error()) - } - - sys, err := GetSystemByClientID(system.ClientID) - assert.Nil(err) - assert.NotEmpty(sys) - assert.Equal("Client with System", sys.ClientName) - - err = CleanDatabase(group) - assert.Nil(err) -} - -func (s *SystemsTestSuite) TestSystemClientGroupDuplicate() { - assert := s.Assert() - - group1 := Group{GroupID: "fabcde612345"} - err := s.db.Create(&group1).Error - if err != nil { - s.FailNow(err.Error()) - } - - group2 := Group{GroupID: "efabcd561234"} - err = s.db.Create(&group2).Error - if err != nil { - s.FailNow(err.Error()) - } - - system := System{GroupID: group1.GroupID, ClientID: "498765uzyxwv", ClientName: "First Client"} - err = s.db.Create(&system).Error - if err != nil { - s.FailNow(err.Error()) - } - - system = System{GroupID: group2.GroupID, ClientID: "498765uzyxwv", ClientName: "Duplicate Client"} - err = s.db.Create(&system).Error - assert.EqualError(err, "pq: duplicate key value violates unique constraint \"idx_client\"") - - sys, err := GetSystemByClientID(system.ClientID) - assert.Nil(err) - assert.NotEmpty(sys) - assert.Equal("First Client", sys.ClientName) - - err = CleanDatabase(group1) - assert.Nil(err) - - err = CleanDatabase(group2) - assert.Nil(err) -} - -func (s *SystemsTestSuite) TestRegisterSystemSuccess() { - assert := s.Assert() - - trackingID := uuid.NewRandom().String() - groupID := "T54321" - group := Group{GroupID: groupID} - err := s.db.Create(&group).Error - if err != nil { - s.FailNow(err.Error()) - } - - pubKey, err := generatePublicKey(2048) - assert.Nil(err) - - creds, err := RegisterSystem("Create System Test", groupID, DefaultScope, pubKey, trackingID) - assert.Nil(err) - assert.Equal("Create System Test", creds.ClientName) - assert.NotEqual("", creds.ClientSecret) - - err = CleanDatabase(group) - assert.Nil(err) -} - -func (s *SystemsTestSuite) TestRegisterSystemMissingData() { - assert := s.Assert() - - trackingID := uuid.NewRandom().String() - groupID := "T11223" - group := Group{GroupID: groupID} - err := s.db.Create(&group).Error - if err != nil { - s.FailNow(err.Error()) - } - - pubKey, err := generatePublicKey(2048) - assert.Nil(err) - - // No clientName - creds, err := RegisterSystem("", groupID, DefaultScope, pubKey, trackingID) - assert.EqualError(err, "clientName is required") - assert.Empty(creds) - - // No scope = success - creds, err = RegisterSystem("Register System Success2", groupID, "", pubKey, trackingID) - assert.Nil(err) - assert.NotEmpty(creds) - - // No scope = success - creds, err = RegisterSystem("Register System Failure", groupID, "badScope", pubKey, trackingID) - assert.NotNil(err) - assert.Empty(creds) - - err = CleanDatabase(group) - assert.Nil(err) -} - -func (s *SystemsTestSuite) TestRegisterSystemBadKey() { - assert := s.Assert() - - trackingID := uuid.NewRandom().String() - groupID := "T22334" - group := Group{GroupID: groupID} - err := s.db.Create(&group).Error - if err != nil { - s.FailNow(err.Error()) - } - - pubKey, err := generatePublicKey(1024) - assert.Nil(err) - - // Blank key - creds, err := RegisterSystem("Register System Failure", groupID, DefaultScope, "", trackingID) - assert.EqualError(err, "error in public key") - assert.Empty(creds) - - // Invalid key - creds, err = RegisterSystem("Register System Failure", groupID, DefaultScope, "NotAKey", trackingID) - assert.EqualError(err, "error in public key") - assert.Empty(creds) - - // Key length too low - creds, err = RegisterSystem("Register System Failure", groupID, DefaultScope, pubKey, trackingID) - assert.EqualError(err, "error in public key") - assert.Empty(creds) - - err = s.db.Unscoped().Delete(&group).Error - assert.Nil(err) -} - -func generatePublicKey(bits int) (string, error) { - return GeneratePublicKey(bits) -} - -func (s *SystemsTestSuite) TestSaveSecret() { - assert := s.Assert() - - group := Group{GroupID: "T21212"} - err := s.db.Create(&group).Error - if err != nil { - s.FailNow(err.Error()) - } - - system := System{GroupID: group.GroupID, ClientID: "test-save-secret-client"} - err = s.db.Create(&system).Error - if err != nil { - s.FailNow(err.Error()) - } - - // First secret should save - secret1, err := GenerateSecret() - - if err != nil { - s.FailNow("cannot generate random secret") - } - hashedSecret1, err := NewHash(secret1) - if err != nil { - s.FailNow("cannot hash random secret") - } - err = system.SaveSecret(hashedSecret1.String()) - if err != nil { - s.FailNow(err.Error()) - } - - // Second secret should cause first secret to be soft-deleted - secret2, err := GenerateSecret() - - if err != nil { - s.FailNow("cannot generate random secret") - } - hashedSecret2, err := NewHash(secret2) - if err != nil { - s.FailNow("cannot hash random secret") - } - err = system.SaveSecret(hashedSecret2.String()) - if err != nil { - s.FailNow(err.Error()) - } - - // Verify we now retrieve second secret - // Note that this also tests GetSecret() - savedHash, err := system.GetSecret() - if err != nil { - s.FailNow(err.Error()) - } - assert.True(Hash(savedHash).IsHashOf(secret2)) - - err = CleanDatabase(group) - assert.Nil(err) -} - -func (s *SystemsTestSuite) TestDeactivateSecrets() { - group := Group{GroupID: "test-deactivate-secrets-group"} - s.db.Create(&group) - system := System{GroupID: group.GroupID, ClientID: "test-deactivate-secrets-client"} - s.db.Create(&system) - secret := Secret{Hash: "test-deactivate-secrets-hash", SystemID: system.ID} - s.db.Create(&secret) - - var systemSecrets []Secret - s.db.Find(&systemSecrets, "system_id = ?", system.ID) - assert.NotEmpty(s.T(), systemSecrets) - - err := system.DeactivateSecrets() - assert.Nil(s.T(), err) - s.db.Find(&systemSecrets, "system_id = ?", system.ID) - assert.Empty(s.T(), systemSecrets) - - _ = CleanDatabase(group) -} - -func (s *SystemsTestSuite) TestResetSecret() { - group := Group{GroupID: "group-12345"} - s.db.Create(&group) - system := System{GroupID: group.GroupID, ClientID: "client-12345"} - s.db.Create(&system) - secret := Secret{Hash: "foo", SystemID: system.ID} - s.db.Create(&secret) - - secret1 := Secret{} - s.db.Where("system_id = ?", system.ID).First(&secret1) - assert.Equal(s.T(), secret1.Hash, secret.Hash) - - credentials, err := system.ResetSecret("tracking-id") - if err != nil { - s.FailNow("Error from ResetSecret()", err.Error()) - return - } - - assert.Nil(s.T(), err) - assert.NotEmpty(s.T(), credentials) - assert.NotEqual(s.T(), secret1.Hash, credentials.ClientSecret) - - _ = CleanDatabase(group) -} - -func (s *SystemsTestSuite) TestScopeEnvSuccess() { - key := "SSAS_DEFAULT_SYSTEM_SCOPE" - newScope := "my_scope" - oldScope := os.Getenv(key) - err := os.Setenv(key, newScope) - if err != nil { - s.FailNow(err.Error()) - } - getEnvVars() - - assert.Equal(s.T(), newScope, DefaultScope) - err = os.Setenv(key, oldScope) - assert.Nil(s.T(), err) -} - -func (s *SystemsTestSuite) TestScopeEnvFailure() { - scope := "" - err := os.Setenv("SSAS_DEFAULT_SYSTEM_SCOPE", scope) - if err != nil { - s.FailNow(err.Error()) - } - - assert.Panics(s.T(), func() { getEnvVars() }) -} - -func makeTestSystem(db *gorm.DB) (Group, System, error) { - groupID := "T" + RandomHexID()[:4] - group := Group{GroupID: groupID} - if err := db.Save(&group).Error; err != nil { - return Group{}, System{}, err - } - system := System{GroupID: group.GroupID, ClientID: "system-for-test-group-" + groupID} - if err := db.Save(&system).Error; err != nil { - return Group{}, System{}, err - } - return group, system, nil -} - -func (s *SystemsTestSuite) TestGetSystemByIDWithKnownSystem() { - g, system, err := makeTestSystem(s.db) - assert.Nil(s.T(), err, "unexpected error") - require.Nil(s.T(), err, "unexpected error ", err) - systemFromID, err := GetSystemByID(fmt.Sprint(system.ID)) - assert.Nil(s.T(), err, "unexpected error ", err) - assert.Equal(s.T(), system.ID, systemFromID.ID) - assert.Equal(s.T(), system.GroupID, systemFromID.GroupID) - _ = CleanDatabase(g) -} - -func (s *SystemsTestSuite) TestGetSystemByIDWithNonExistentID() { - // make sure there's at least one system - g, _, err := makeTestSystem(s.db) - assert.Nil(s.T(), err, "can't make test system") - var max uint - row := s.db.Table("systems").Select("MAX(id)").Row() - err = row.Scan(&max) - assert.Nil(s.T(), err, "no max id?") - _, err = GetSystemByID(fmt.Sprint(max + 1)) - require.NotEmpty(s.T(), err, "should not have found system for ID: ", max+1) - _ = CleanDatabase(g) -} - -func (s *SystemsTestSuite) TestGetSystemByIDWithEmptyID() { - _, err := GetSystemByID("") - require.NotNil(s.T(), err, "found system for empty id") -} - -func (s *SystemsTestSuite) TestGetSystemBySystemIDWithNonNumberID() { - _, err := GetSystemByID("i am not a number") - require.NotNil(s.T(), err, "found system for non-number id") -} - -func (s *SystemsTestSuite) TestGetSystemByClientIDWithEmptyID() { - _, err := GetSystemByClientID("") - require.NotNil(s.T(), err, "found system for empty id") -} - -func (s *SystemsTestSuite) TestGetSystemByClientIDWithNonNumberID() { - _, err := GetSystemByClientID("i am not a number") - require.NotNil(s.T(), err, "found system for non-number id") -} - -func (s *SystemsTestSuite) TestGetSystemsByGroupIDWithEmptyID() { - systems, _ := GetSystemsByGroupID("") - assert.Empty(s.T(), systems, "found system for empty group id") -} - -func (s *SystemsTestSuite) TestGetSystemsByGroupIDWithNonexistentID() { - // make sure there's at least one system - g, _, err := makeTestSystem(s.db) - assert.Nil(s.T(), err, "can't make test system") - randomGroupID := RandomHexID()[:4] - systems, _ := GetSystemsByGroupID(randomGroupID) - assert.Empty(s.T(), systems, "should not have found system for ID: " + randomGroupID) - _ = CleanDatabase(g) -} - -func (s *SystemsTestSuite) TestGetSystemsByGroupIDWithKnownSystem() { - g, system, err := makeTestSystem(s.db) - - require.Nil(s.T(), err, "unexpected error ", err) - systemsFromGroupID, err := GetSystemsByGroupID(g.GroupID) - assert.Nil(s.T(), err, "unexpected error ", err) - - assert.Len(s.T(), systemsFromGroupID, 1, "should find exactly one system") - - // Don't stop the test (so we can run CleanDatabase() at the end), but also don't bother with the next two - // assertions unless the previous one was true. - if len(systemsFromGroupID) == 1 { - assert.Equal(s.T(), system.ID, systemsFromGroupID[0].ID) - assert.Equal(s.T(), system.GroupID, systemsFromGroupID[0].GroupID) - } - - _ = CleanDatabase(g) -} - -func TestSystemsTestSuite(t *testing.T) { - suite.Run(t, new(SystemsTestSuite)) -} diff --git a/ssas/testutils.go b/ssas/testutils.go deleted file mode 100644 index 88169f07f..000000000 --- a/ssas/testutils.go +++ /dev/null @@ -1,55 +0,0 @@ -package ssas - -import ( - "crypto/rand" - "crypto/rsa" - "crypto/x509" - "encoding/base64" - "encoding/pem" - "fmt" -) - -func GeneratePublicKey(bits int) (string, error) { - keyPair, err := rsa.GenerateKey(rand.Reader, bits) - if err != nil { - return "", fmt.Errorf("unable to generate keyPair: %s", err.Error()) - } - - publicKeyPKIX, err := x509.MarshalPKIXPublicKey(&keyPair.PublicKey) - if err != nil { - return "", fmt.Errorf("unable to marshal public key: %s", err.Error()) - } - - publicKeyBytes := pem.EncodeToMemory(&pem.Block{ - Type: "RSA PUBLIC KEY", - Bytes: publicKeyPKIX, - }) - - return string(publicKeyBytes), nil -} - -func RandomHexID() string { - b, err := someRandomBytes(4) - if err != nil { - return "not_a_random_client_id" - } - return fmt.Sprintf("%x", b) -} - -func RandomBase64(n int) string { - b, err := someRandomBytes(20) - if err != nil { - return "not_a_random_base_64_string" - } - return base64.StdEncoding.EncodeToString(b) -} - -func someRandomBytes(n int) ([]byte, error) { - b := make([]byte, n) - _, err := rand.Read(b) - if err != nil { - return nil, err - } - return b, nil -} - diff --git a/test/postman_test/BCDA-SSAS.postman_collection.json b/test/postman_test/BCDA-SSAS.postman_collection.json deleted file mode 100644 index 4c9682f7e..000000000 --- a/test/postman_test/BCDA-SSAS.postman_collection.json +++ /dev/null @@ -1,453 +0,0 @@ -{ - "info": { - "_postman_id": "1013173b-e2e6-4cbf-867a-b333d06aa9cc", - "name": "BCDA-SSAS", - "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" - }, - "item": [ - { - "name": "Create group 1", - "event": [ - { - "listen": "test", - "script": { - "id": "0fd18536-7b7c-433f-bd30-c7b1ae57a92d", - "exec": [ - "pm.test(\"Status code is 201\", function () {", - " pm.response.to.have.status(201);", - "});", - "", - "pm.environment.set(\"group1ID\", pm.response.json().ID);" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "basic", - "basic": [ - { - "key": "password", - "value": "{{bcdaSSASClientSecret}}", - "type": "string" - }, - { - "key": "username", - "value": "{{bcdaSSASClientID}}", - "type": "string" - } - ] - }, - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\n\t\"group_id\": \"pm-bcda-ssas-group-1\",\n\t\"name\": \"PM BCDA-SSAS Group 1\",\n\t\"scopes\": [ \"bcda-api\" ],\n\t\"xdata\": \"{ \\\"cms_ids\\\": [\\\"A9994\\\"] }\"\n}" - }, - "url": { - "raw": "{{scheme}}://{{ssasAdminHost}}/group", - "protocol": "{{scheme}}", - "host": [ - "{{ssasAdminHost}}" - ], - "path": [ - "group" - ] - } - }, - "response": [] - }, - { - "name": "Create system 1", - "event": [ - { - "listen": "test", - "script": { - "id": "287532b5-8d18-48e6-abf6-6ed98a9b9440", - "exec": [ - "pm.test(\"Status code is 201\", function () {", - " pm.response.to.have.status(201);", - "});", - "", - "respJSON = pm.response.json()", - "pm.environment.set(\"system1ID\", respJSON.system_id)", - "pm.environment.set(\"system1ClientID\", respJSON.client_id)", - "pm.environment.set(\"system1ClientSecret\", respJSON.client_secret)" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "basic", - "basic": [ - { - "key": "password", - "value": "{{bcdaSSASClientSecret}}", - "type": "string" - }, - { - "key": "username", - "value": "{{bcdaSSASClientID}}", - "type": "string" - } - ] - }, - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\n\t\"group_id\": \"pm-bcda-ssas-group-1\",\n\t\"client_name\": \"PM BCDA-SSAS System 1\",\n\t\"scope\": \"bcda-api\",\n\t\"public_key\": \"-----BEGIN PUBLIC KEY-----\\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArhxobShmNifzW3xznB+L\\nI8+hgaePpSGIFCtFz2IXGU6EMLdeufhADaGPLft9xjwdN1ts276iXQiaChKPA2CK\\n/CBpuKcnU3LhU8JEi7u/db7J4lJlh6evjdKVKlMuhPcljnIKAiGcWln3zwYrFCeL\\ncN0aTOt4xnQpm8OqHawJ18y0WhsWT+hf1DeBDWvdfRuAPlfuVtl3KkrNYn1yqCgQ\\nlT6v/WyzptJhSR1jxdR7XLOhDGTZUzlHXh2bM7sav2n1+sLsuCkzTJqWZ8K7k7cI\\nXK354CNpCdyRYUAUvr4rORIAUmcIFjaR3J4y/Dh2JIyDToOHg7vjpCtNnNoS+ON2\\nHwIDAQAB\\n-----END PUBLIC KEY-----\",\n\t\"tracking_id\": \"pm-bcda-ssas-system-1\"\n}" - }, - "url": { - "raw": "{{scheme}}://{{ssasAdminHost}}/system", - "protocol": "{{scheme}}", - "host": [ - "{{ssasAdminHost}}" - ], - "path": [ - "system" - ] - } - }, - "response": [] - }, - { - "name": "Get system 1 token", - "event": [ - { - "listen": "test", - "script": { - "id": "00c5dc02-45e2-4aeb-bc9e-ed9efd19c1ef", - "exec": [ - "pm.test(\"Status code is 200\", function () {", - " pm.response.to.have.status(200);", - "});", - "", - "pm.environment.set(\"system1Token\", pm.response.json().access_token)" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "basic", - "basic": [ - { - "key": "password", - "value": "{{system1ClientSecret}}", - "type": "string" - }, - { - "key": "username", - "value": "{{system1ClientID}}", - "type": "string" - } - ] - }, - "method": "POST", - "header": [], - "url": { - "raw": "{{scheme}}://{{host}}/auth/token", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "path": [ - "auth", - "token" - ] - } - }, - "response": [] - }, - { - "name": "Create group 2", - "event": [ - { - "listen": "test", - "script": { - "id": "7b0299ce-ce18-4c45-b0d3-58ed9f190bad", - "exec": [ - "pm.test(\"Status code is 201\", function () {", - " pm.response.to.have.status(201);", - "});", - "", - "pm.environment.set(\"group2ID\", pm.response.json().ID);" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "basic", - "basic": [ - { - "key": "password", - "value": "{{bcdaSSASClientSecret}}", - "type": "string" - }, - { - "key": "username", - "value": "{{bcdaSSASClientID}}", - "type": "string" - } - ] - }, - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\n\t\"group_id\": \"pm-bcda-ssas-group-2\",\n\t\"name\": \"PM BCDA-SSAS Group 2\",\n\t\"scopes\": [ \"bcda-api\" ],\n\t\"xdata\": \"{ \\\"cms_ids\\\": [\\\"A9992\\\"] }\"\n}" - }, - "url": { - "raw": "{{scheme}}://{{ssasAdminHost}}/group", - "protocol": "{{scheme}}", - "host": [ - "{{ssasAdminHost}}" - ], - "path": [ - "group" - ] - } - }, - "response": [] - }, - { - "name": "Create system 2", - "event": [ - { - "listen": "test", - "script": { - "id": "876152a7-fd93-45cb-92e9-111c9c60ed64", - "exec": [ - "pm.test(\"Status code is 201\", function () {", - " pm.response.to.have.status(201);", - "});", - "", - "respJSON = pm.response.json()", - "pm.environment.set(\"system2ID\", respJSON.system_id)", - "pm.environment.set(\"system2ClientID\", respJSON.client_id)", - "pm.environment.set(\"system2ClientSecret\", respJSON.client_secret)" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "noauth" - }, - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\n\t\"group_id\": \"pm-bcda-ssas-group-2\",\n\t\"client_name\": \"PM BCDA-SSAS System 2\",\n\t\"scope\": \"bcda-api\",\n\t\"public_key\": \"-----BEGIN PUBLIC KEY-----\\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArhxobShmNifzW3xznB+L\\nI8+hgaePpSGIFCtFz2IXGU6EMLdeufhADaGPLft9xjwdN1ts276iXQiaChKPA2CK\\n/CBpuKcnU3LhU8JEi7u/db7J4lJlh6evjdKVKlMuhPcljnIKAiGcWln3zwYrFCeL\\ncN0aTOt4xnQpm8OqHawJ18y0WhsWT+hf1DeBDWvdfRuAPlfuVtl3KkrNYn1yqCgQ\\nlT6v/WyzptJhSR1jxdR7XLOhDGTZUzlHXh2bM7sav2n1+sLsuCkzTJqWZ8K7k7cI\\nXK354CNpCdyRYUAUvr4rORIAUmcIFjaR3J4y/Dh2JIyDToOHg7vjpCtNnNoS+ON2\\nHwIDAQAB\\n-----END PUBLIC KEY-----\",\n\t\"tracking_id\": \"pm-bcda-ssas-system-2\"\n}" - }, - "url": { - "raw": "{{scheme}}://{{ssasAdminHost}}/system", - "protocol": "{{scheme}}", - "host": [ - "{{ssasAdminHost}}" - ], - "path": [ - "system" - ] - } - }, - "response": [] - }, - { - "name": "Get system 2 token", - "event": [ - { - "listen": "test", - "script": { - "id": "eb9d23b7-dbb7-49d8-928e-d83c7ebe39e3", - "exec": [ - "pm.test(\"Status code is 200\", function () {", - " pm.response.to.have.status(200);", - "});", - "", - "pm.environment.set(\"system2Token\", pm.response.json().access_token)" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "basic", - "basic": [ - { - "key": "password", - "value": "{{system2ClientSecret}}", - "type": "string" - }, - { - "key": "username", - "value": "{{system2ClientID}}", - "type": "string" - } - ] - }, - "method": "POST", - "header": [], - "url": { - "raw": "{{scheme}}://{{host}}/auth/token", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "path": [ - "auth", - "token" - ] - } - }, - "response": [] - }, - { - "name": "Start Patient export as system 1", - "event": [ - { - "listen": "test", - "script": { - "id": "dc5452f0-5f37-4c72-9691-7e72038eac47", - "exec": [ - "pm.test(\"Status code is 202\", function() {", - " pm.response.to.have.status(202);", - "});", - "", - "pm.test(\"Has Content-Location header\", function() {", - " pm.response.to.have.header(\"Content-Location\");", - "});", - "", - "pm.environment.set(\"jobURL\", pm.response.headers.get(\"Content-Location\"));" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{system1Token}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/fhir+json", - "type": "text" - }, - { - "key": "Prefer", - "value": "respond-async", - "type": "text" - } - ], - "url": { - "raw": "{{scheme}}://{{host}}/api/v1/Patient/$export", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "path": [ - "api", - "v1", - "Patient", - "$export" - ] - } - }, - "response": [] - }, - { - "name": "Request system 1 job status as system 1", - "event": [ - { - "listen": "test", - "script": { - "id": "16131bfc-6797-4e06-9d6b-2554b7ab9333", - "exec": [ - "pm.test(\"Status code is 200 or 202\", function () {", - " pm.expect(pm.response.code).to.be.oneOf([200,202]);", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{system1Token}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [], - "url": { - "raw": "{{jobURL}}", - "host": [ - "{{jobURL}}" - ] - } - }, - "response": [] - }, - { - "name": "Request system 1 job status as system 2", - "event": [ - { - "listen": "test", - "script": { - "id": "16131bfc-6797-4e06-9d6b-2554b7ab9333", - "exec": [ - "pm.test(\"Status code is 401\", function () {", - " pm.response.to.have.status(401);", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{system2Token}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [], - "url": { - "raw": "{{jobURL}}", - "host": [ - "{{jobURL}}" - ] - } - }, - "response": [] - } - ] -} \ No newline at end of file diff --git a/test/postman_test/SSAS.postman_collection.json b/test/postman_test/SSAS.postman_collection.json deleted file mode 100644 index d92c3f3a2..000000000 --- a/test/postman_test/SSAS.postman_collection.json +++ /dev/null @@ -1,1072 +0,0 @@ -{ - "info": { - "_postman_id": "fb633ab8-9995-4ff3-9f28-88ff04e69e07", - "name": "SSAS", - "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" - }, - "item": [ - { - "name": "admin post group, 400", - "event": [ - { - "listen": "test", - "script": { - "id": "1678ec38-847b-4912-9d74-469a233d0200", - "exec": [ - "pm.test(\"response is 400\", function () {", - " pm.response.to.have.status(400);", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "name": "Content-Type", - "value": "application/json", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"no-group-id\": \"here\"\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{scheme}}://{{host}}:{{admin-port}}/group", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "port": "{{admin-port}}", - "path": [ - "group" - ] - } - }, - "response": [] - }, - { - "name": "admin post group, 201", - "event": [ - { - "listen": "test", - "script": { - "id": "20ed753f-bd64-4270-9993-305d4efc1372", - "exec": [ - "pm.test(\"response is created and returns json\", function () {", - " pm.response.to.have.status(201);", - " ", - " const schema = {", - " \"ID\": {\"type\": \"int\"},", - " \"CreatedAt\": {\"type\": \"string\"},", - " \"UpdatedAt\": {\"type\": \"string\"},", - " \"DeletedAt\": {\"type\": \"string\"},", - " \"group_id\": {\"type\": \"string\"},", - " \"data\": {\"type\": \"json\"}", - " };", - " var respJson = pm.response.json();", - " ", - " pm.test(\"schema is valid\", function() {", - " pm.expect(tv4.validate(respJson, schema)).to.be.true;", - " });", - " ", - " pm.environment.set(\"group-id\", respJson.ID);", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "name": "Content-Type", - "value": "application/json", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"group_id\": \"fake-id\",\n \"name\": \"fake-name\",\n \"users\": [\n \"00uiqolo7fEFSfif70h7\",\n \"l0vckYyfyow4TZ0zOKek\",\n \"HqtEi2khroEZkH4sdIzj\"\n ],\n \"scopes\": [\n \"user-admin\",\n \"system-admin\"\n ],\n \"resources\": [\n {\n \"id\": \"xxx\",\n \"name\": \"BCDA API\",\n \"scopes\": [\n \"bcda-api\"\n ]\n },\n {\n \"id\": \"eft\",\n \"name\": \"EFT CCLF\",\n \"scopes\": [\n \"eft-app:download\",\n \"eft-data:read\"\n ]\n }\n ]\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{scheme}}://{{host}}:{{admin-port}}/group", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "port": "{{admin-port}}", - "path": [ - "group" - ] - } - }, - "response": [] - }, - { - "name": "admin put group, 400", - "event": [ - { - "listen": "test", - "script": { - "id": "8dffcb16-25eb-44bc-8be3-fe19b23dc302", - "exec": [ - "pm.test(\"response is 400 and record not found\", function () {", - " pm.response.to.have.status(400);", - " pm.response.to.have.body(\"Failed to update group. Error: record not found for id=9999\\n\")", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "PUT", - "header": [ - { - "key": "Content-Type", - "name": "Content-Type", - "value": "application/json", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"id\": \"fake-id\",\n \"name\": \"fake-name\",\n \"users\": [\n \"00uiqolo7fEFSfif70h7\",\n \"l0vckYyfyow4TZ0zOKek\",\n \"HqtEi2khroEZkH4sdIzj\",\n \"new-user\"\n ],\n \"scopes\": [\n \"user-admin\",\n \"system-admin\",\n \"new-scope\"\n ],\n \"resources\": [\n {\n \"id\": \"xxx\",\n \"name\": \"BCDA API\",\n \"scopes\": [\n \"bcda-api\"\n ]\n },\n {\n \"id\": \"eft\",\n \"name\": \"EFT CCLF\",\n \"scopes\": [\n \"eft-app:download\",\n \"eft-data:read\"\n ]\n }\n ],\n \"system\": {\n \"client_id\": \"4tuhiOIFIwriIOH3zn\",\n \"software_id\": \"4NRB1-0XZABZI9E6-5SM3R\",\n \"client_name\": \"ACO System A\"\n }\n}" - }, - "url": { - "raw": "{{scheme}}://{{host}}:{{admin-port}}/group/9999", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "port": "{{admin-port}}", - "path": [ - "group", - "9999" - ] - } - }, - "response": [] - }, - { - "name": "admin put group, 200", - "event": [ - { - "listen": "test", - "script": { - "id": "8dffcb16-25eb-44bc-8be3-fe19b23dc302", - "exec": [ - "pm.test(\"response is 200 and response has updated group\", function () {", - " pm.response.to.have.status(200);", - " pm.response.to.have.jsonBody({", - " \"group_id\": \"a-fake-id\",", - " \"data\": {", - " \"id\": \"a-fake-id\",", - " \"name\": \"a-fake-name\",", - " \"users\": [", - " \"00uiqolo7fEFSfif70h7\",", - " \"l0vckYyfyow4TZ0zOKek\",", - " \"HqtEi2khroEZkH4sdIzj\",", - " \"new-user\"", - " ],", - " \"scopes\": [", - " \"user-admin\",", - " \"system-admin\",", - " \"new-scope\"", - " ],", - " \"system\": {", - " \"ID\": 0,", - " \"CreatedAt\": \"0001-01-01T00:00:00Z\",", - " \"UpdatedAt\": \"0001-01-01T00:00:00Z\",", - " \"DeletedAt\": null,", - " \"group_id\": \"\",", - " \"client_id\": \"4tuhiOIFIwriIOH3zn\",", - " \"software_id\": \"4NRB1-0XZABZI9E6-5SM3R\",", - " \"client_name\": \"ACO System A\",", - " \"api_scope\": \"\",", - " \"encryption_keys\": null,", - " \"secrets\": null", - " },", - " \"resources\": [", - " {", - " \"id\": \"xxx\",", - " \"name\": \"BCDA API\",", - " \"scopes\": [", - " \"bcda-api\"", - " ]", - " },", - " {", - " \"id\": \"eft\",", - " \"name\": \"EFT CCLF\",", - " \"scopes\": [", - " \"eft-app:download\",", - " \"eft-data:read\"", - " ]", - " }", - " ]", - " }", - "})", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "PUT", - "header": [ - { - "key": "Content-Type", - "name": "Content-Type", - "type": "text", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"group_id\": \"fake-id\",\n \"name\": \"fake-name\",\n \"users\": [\n \"00uiqolo7fEFSfif70h7\",\n \"l0vckYyfyow4TZ0zOKek\",\n \"HqtEi2khroEZkH4sdIzj\",\n \"new-user\"\n ],\n \"scopes\": [\n \"user-admin\",\n \"system-admin\",\n \"new-scope\"\n ],\n \"resources\": [\n {\n \"id\": \"xxx\",\n \"name\": \"BCDA API\",\n \"scopes\": [\n \"bcda-api\"\n ]\n },\n {\n \"id\": \"eft\",\n \"name\": \"EFT CCLF\",\n \"scopes\": [\n \"eft-app:download\",\n \"eft-data:read\"\n ]\n }\n ],\n \"system\": {\n \"client_id\": \"4tuhiOIFIwriIOH3zn\",\n \"software_id\": \"4NRB1-0XZABZI9E6-5SM3R\",\n \"client_name\": \"ACO System A\"\n }\n}" - }, - "url": { - "raw": "{{scheme}}://{{host}}:{{admin-port}}/group/{{group-id}}", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "port": "{{admin-port}}", - "path": [ - "group", - "{{group-id}}" - ] - } - }, - "response": [] - }, - { - "name": "admin get group, one group", - "event": [ - { - "listen": "test", - "script": { - "id": "f4740e30-67e2-4ec7-bfae-9d44f7aa84fc", - "exec": [ - "pm.test(\"response is 200 and group found\", function () {", - " pm.response.to.have.status(200);", - " pm.response.to.have.jsonBody({\"name\":\"a-fake-name\"});", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [], - "url": { - "raw": "{{scheme}}://{{host}}:{{admin-port}}/group", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "port": "{{admin-port}}", - "path": [ - "group" - ] - } - }, - "response": [] - }, - { - "name": "admin delete group, 400", - "event": [ - { - "listen": "test", - "script": { - "id": "95ce3470-8202-4e9f-b4f6-4fe36a09cb0d", - "exec": [ - "pm.test(\"response is 400 and record not found\", function () {", - " pm.response.to.have.status(400);", - " pm.response.to.have.body(\"Failed to delete group. Error: no Group record found with ID 9999\\n\")", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "DELETE", - "header": [], - "url": { - "raw": "{{scheme}}://{{host}}:{{admin-port}}/group/9999", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "port": "{{admin-port}}", - "path": [ - "group", - "9999" - ] - } - }, - "response": [] - }, - { - "name": "admin create system, 400", - "event": [ - { - "listen": "test", - "script": { - "id": "fc625fc2-e25e-4fd2-a110-fc1528839ca7", - "exec": [ - "pm.test(\"response is 400\", function () {", - " pm.response.to.have.status(400);", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "name": "Content-Type", - "type": "text", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\n\t\"not-gonna\": \"fly\"\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{scheme}}://{{host}}:{{admin-port}}/system", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "port": "{{admin-port}}", - "path": [ - "system" - ] - } - }, - "response": [] - }, - { - "name": "admin create system, 201", - "event": [ - { - "listen": "test", - "script": { - "id": "fc625fc2-e25e-4fd2-a110-fc1528839ca7", - "exec": [ - "pm.test(\"response is 201 and returns json\", function () {", - " pm.response.to.have.status(201);", - "", - " const schema = {", - " \"system_id\": {\"type\": \"string\"},", - " \"client_id\": {\"type\": \"string\"},", - " \"client_secret\": {\"type\": \"string\"},", - " \"client_name\": {\"type\": \"string\"},", - " \"expires_at\": {\"type\": \"string\"},", - " };", - " var respJson = pm.response.json();", - " ", - " pm.test(\"schema is valid\", function() {", - " pm.expect(tv4.validate(respJson, schema)).to.be.true;", - " });", - " ", - " pm.environment.set(\"system-id\", respJson.system_id);", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "name": "Content-Type", - "type": "text", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"client_name\": \"Test Client\",\n \"group_id\": \"fake-id\",\n \"scope\": \"bcda-api\",\n \"public_key\": \"-----BEGIN PUBLIC KEY-----\\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArhxobShmNifzW3xznB+L\\nI8+hgaePpSGIFCtFz2IXGU6EMLdeufhADaGPLft9xjwdN1ts276iXQiaChKPA2CK\\n/CBpuKcnU3LhU8JEi7u/db7J4lJlh6evjdKVKlMuhPcljnIKAiGcWln3zwYrFCeL\\ncN0aTOt4xnQpm8OqHawJ18y0WhsWT+hf1DeBDWvdfRuAPlfuVtl3KkrNYn1yqCgQ\\nlT6v/WyzptJhSR1jxdR7XLOhDGTZUzlHXh2bM7sav2n1+sLsuCkzTJqWZ8K7k7cI\\nXK354CNpCdyRYUAUvr4rORIAUmcIFjaR3J4y/Dh2JIyDToOHg7vjpCtNnNoS+ON2\\nHwIDAQAB\\n-----END PUBLIC KEY-----\",\n \"tracking_id\": \"T00000\"\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{scheme}}://{{host}}:{{admin-port}}/system", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "port": "{{admin-port}}", - "path": [ - "system" - ] - } - }, - "response": [] - }, - { - "name": "admin reset credentials, 404", - "event": [ - { - "listen": "test", - "script": { - "id": "fc625fc2-e25e-4fd2-a110-fc1528839ca7", - "exec": [ - "pm.test(\"response is 404\", function () {", - " pm.response.to.have.status(404);", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "PUT", - "header": [ - { - "key": "Content-Type", - "name": "Content-Type", - "type": "text", - "value": "application/json" - } - ], - "url": { - "raw": "{{scheme}}://{{host}}:{{admin-port}}/system/9999/credentials", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "port": "{{admin-port}}", - "path": [ - "system", - "9999", - "credentials" - ] - } - }, - "response": [] - }, - { - "name": "admin reset credentials, 201", - "event": [ - { - "listen": "test", - "script": { - "id": "fc625fc2-e25e-4fd2-a110-fc1528839ca7", - "exec": [ - "pm.test(\"response is 201 and returns json\", function () {", - " pm.response.to.have.status(201);", - "", - " const schema = {", - " \"client_id\": {\"type\": \"string\"},", - " \"client_secret\": {\"type\": \"string\"},", - " };", - " var respJson = pm.response.json();", - " ", - " pm.test(\"schema is valid\", function() {", - " pm.expect(tv4.validate(respJson, schema)).to.be.true;", - " });", - " ", - " pm.environment.set(\"client-id\", respJson.client_id);", - " pm.environment.set(\"client-secret\", respJson.client_secret);", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "PUT", - "header": [ - { - "key": "Content-Type", - "name": "Content-Type", - "value": "application/json", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{scheme}}://{{host}}:{{admin-port}}/system/{{system-id}}/credentials", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "port": "{{admin-port}}", - "path": [ - "system", - "{{system-id}}", - "credentials" - ] - } - }, - "response": [] - }, - { - "name": "admin get public key, 404", - "event": [ - { - "listen": "test", - "script": { - "id": "fc625fc2-e25e-4fd2-a110-fc1528839ca7", - "exec": [ - "pm.test(\"response is 404\", function () {", - " pm.response.to.have.status(404);", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Content-Type", - "name": "Content-Type", - "type": "text", - "value": "application/json" - } - ], - "url": { - "raw": "{{scheme}}://{{host}}:{{admin-port}}/system/9999/key", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "port": "{{admin-port}}", - "path": [ - "system", - "9999", - "key" - ] - } - }, - "response": [] - }, - { - "name": "admin get public key, 200", - "event": [ - { - "listen": "test", - "script": { - "id": "fc625fc2-e25e-4fd2-a110-fc1528839ca7", - "exec": [ - "pm.test(\"response is 200 and returns json\", function () {", - " pm.response.to.have.status(200);", - " ", - " const schema = {", - " \"client_id\": {\"type\": \"string\"},", - " \"public_key\": {\"type\": \"string\"},", - " };", - " var respJson = pm.response.json();", - " ", - " pm.test(\"schema is valid\", function() {", - " pm.expect(tv4.validate(respJson, schema)).to.be.true;", - " });", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Content-Type", - "name": "Content-Type", - "type": "text", - "value": "application/json" - } - ], - "url": { - "raw": "{{scheme}}://{{host}}:{{admin-port}}/system/{{system-id}}/key", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "port": "{{admin-port}}", - "path": [ - "system", - "{{system-id}}", - "key" - ] - } - }, - "response": [] - }, - { - "name": "admin delete credentials, 404", - "event": [ - { - "listen": "test", - "script": { - "id": "fc625fc2-e25e-4fd2-a110-fc1528839ca7", - "exec": [ - "pm.test(\"response is 404\", function () {", - " pm.response.to.have.status(404);", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "DELETE", - "header": [ - { - "key": "Content-Type", - "name": "Content-Type", - "type": "text", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{scheme}}://{{host}}:{{admin-port}}/system/9999/credentials", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "port": "{{admin-port}}", - "path": [ - "system", - "9999", - "credentials" - ] - } - }, - "response": [] - }, - { - "name": "admin delete credentials, 200", - "event": [ - { - "listen": "test", - "script": { - "id": "fc625fc2-e25e-4fd2-a110-fc1528839ca7", - "exec": [ - "pm.test(\"response is 200\", function () {", - " pm.response.to.have.status(200);", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "DELETE", - "header": [ - { - "key": "Content-Type", - "name": "Content-Type", - "type": "text", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{scheme}}://{{host}}:{{admin-port}}/system/{{system-id}}/credentials", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "port": "{{admin-port}}", - "path": [ - "system", - "{{system-id}}", - "credentials" - ] - } - }, - "response": [] - }, - { - "name": "admin delete group, 200", - "event": [ - { - "listen": "test", - "script": { - "id": "95ce3470-8202-4e9f-b4f6-4fe36a09cb0d", - "exec": [ - "pm.test(\"response is 200\", function () {", - " pm.response.to.have.status(200);", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "DELETE", - "header": [], - "url": { - "raw": "{{scheme}}://{{host}}:{{admin-port}}/group/{{group-id}}", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "port": "{{admin-port}}", - "path": [ - "group", - "{{group-id}}" - ] - } - }, - "response": [] - }, - { - "name": "admin get group, only fixture groups", - "event": [ - { - "listen": "test", - "script": { - "id": "fc625fc2-e25e-4fd2-a110-fc1528839ca7", - "exec": [ - "pm.test(\"response is 200 and at least 2 fixture groups in response\", function () {", - " pm.response.to.have.status(200);", - " var jsonData = pm.response.json();", - " pm.expect(jsonData[0].group_id).to.eql(\"admin\");", - " pm.expect(jsonData[1].group_id).to.eql(\"0c527d2e-2e8a-4808-b11d-0fa06baf8254\");", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [], - "url": { - "raw": "{{scheme}}://{{host}}:{{admin-port}}/group", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "port": "{{admin-port}}", - "path": [ - "group" - ] - } - }, - "response": [] - }, - { - "name": "admin delete token, success", - "event": [ - { - "listen": "test", - "script": { - "id": "95ce3470-8202-4e9f-b4f6-4fe36a09cb0d", - "exec": [ - "pm.test(\"response is 200\", function () {", - " pm.response.to.have.status(200);", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "DELETE", - "header": [], - "url": { - "raw": "{{scheme}}://{{host}}:{{admin-port}}/token/{{token_id}}", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "port": "{{admin-port}}", - "path": [ - "token", - "{{token_id}}" - ] - } - }, - "response": [] - }, - { - "name": "public authn/challenge", - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "", - "type": "string" - } - ] - }, - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\"login_id\":\"{{login_id}}\",\"factor_type\":\"{{factor_type}}\"}" - }, - "url": { - "raw": "{{scheme}}://{{host}}:{{public-port}}/authn/challenge", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "port": "{{public-port}}", - "path": [ - "authn", - "challenge" - ] - }, - "description": "Request that an MFA challenge passcode be sent, for example, via SMS" - }, - "response": [] - }, - { - "name": "public authn/verify", - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "", - "type": "string" - } - ] - }, - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\"login_id\":\"{{login_id}}\",\"factor_type\":\"{{factor_type}}\",\"passcode\":\"{{passcode}}\"}" - }, - "url": { - "raw": "{{scheme}}://{{host}}:{{public-port}}/authn/verify", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "port": "{{public-port}}", - "path": [ - "authn", - "verify" - ] - }, - "description": "Verify an MFA passcode" - }, - "response": [] - }, - { - "name": "public authn", - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\"login_id\":\"{{login_id}}\",\"password\":\"{{password}}\"}" - }, - "url": { - "raw": "{{scheme}}://{{host}}:{{public-port}}/authn", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "port": "{{public-port}}", - "path": [ - "authn" - ] - }, - "description": "Verify a username and password" - }, - "response": [] - }, - { - "name": "public register", - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "name": "Content-Type", - "value": "application/json", - "type": "text" - }, - { - "key": "x-group-id", - "value": "{{group_id}}", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"client_name\": \"My favorite name in all the world\",\n \"scope\": \"bcda-api\",\n \"jwks\": {\n \"keys\": [\n {\n \"e\": \"AAEAAQ\",\n \"n\": \"ok6rvXu95337IxsDXrKzlIqw_I_zPDG8JyEw2CTOtNMoDi1QzpXQVMGj2snNEmvNYaCTmFf51I-EDgeFLLexr40jzBXlg72quV4aw4yiNuxkigW0gMA92OmaT2jMRIdDZM8mVokoxyPfLub2YnXHFq0XuUUgkX_TlutVhgGbyPN0M12teYZtMYo2AUzIRggONhHvnibHP0CPWDjCwSfp3On1Recn4DPxbn3DuGslF2myalmCtkujNcrhHLhwYPP-yZFb8e0XSNTcQvXaQxAqmnWH6NXcOtaeWMQe43PNTAyNinhndgI8ozG3Hz-1NzHssDH_yk6UYFSszhDbWAzyqw\",\n \"kty\": \"RSA\"\n }\n ]\n }\n}" - }, - "url": { - "raw": "{{scheme}}://{{host}}:{{public-port}}/register", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "port": "{{public-port}}", - "path": [ - "register" - ] - } - }, - "response": [] - }, - { - "name": "public reset", - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "name": "Content-Type", - "type": "text", - "value": "application/json" - }, - { - "key": "x-group-id", - "type": "text", - "value": "T0001" - } - ], - "body": { - "mode": "raw", - "raw": "{\"client_id\":\"{{client_id}}\"}" - }, - "url": { - "raw": "{{scheme}}://{{host}}:{{public-port}}/reset", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "port": "{{public-port}}", - "path": [ - "reset" - ] - } - }, - "response": [] - } - ], - "auth": { - "type": "basic", - "basic": [ - { - "key": "password", - "value": "{{adminClientSecret}}", - "type": "string" - }, - { - "key": "username", - "value": "{{adminClientId}}", - "type": "string" - } - ] - }, - "event": [ - { - "listen": "prerequest", - "script": { - "id": "e3311938-8ba3-4d78-a5fc-82d922c68b48", - "type": "text/javascript", - "exec": [ - "" - ] - } - }, - { - "listen": "test", - "script": { - "id": "5bef9511-420b-4bd0-9f12-3a1ad54e2315", - "type": "text/javascript", - "exec": [ - "" - ] - } - } - ], - "protocolProfileBehavior": {} -} \ No newline at end of file diff --git a/test/postman_test/SSAS_Smoke_Test.postman_collection.json b/test/postman_test/SSAS_Smoke_Test.postman_collection.json deleted file mode 100644 index e8b27e52f..000000000 --- a/test/postman_test/SSAS_Smoke_Test.postman_collection.json +++ /dev/null @@ -1,1513 +0,0 @@ -{ - "info": { - "_postman_id": "dee5abdb-c9d0-476b-9fe7-f4907c710d11", - "name": "SSAS Smoke Test", - "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" - }, - "item": [ - { - "name": "admin info", - "event": [ - { - "listen": "test", - "script": { - "id": "b601d0c1-40e0-4b4a-a257-1095497e7cf8", - "exec": [ - "var schema = {", - " \"properties\": {", - " \"banner\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } },", - " \"routes\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } }", - " }", - "};", - "var Ajv = require('ajv');", - "var ajv = new Ajv({schemas: [schema]});", - "", - "pm.test(\"Response is 'ok'\", function () {", - " pm.response.to.have.status(200);", - "});", - "", - "pm.test(\"Body contains path of an endpoint\", function () {", - " pm.expect(pm.response.text()).to.include(\"/_info\");", - "});", - "", - "pm.test('Schema is valid', function() {", - " pm.expect(ajv.validate(schema, pm.response.text())).to.be.true;", - "});", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [], - "url": { - "raw": "{{scheme}}://{{host}}:{{admin-port}}/_info", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "port": "{{admin-port}}", - "path": [ - "_info" - ] - } - }, - "response": [] - }, - { - "name": "admin health", - "event": [ - { - "listen": "test", - "script": { - "id": "39f25f26-ea63-48a5-9bca-c9b8d2dd5f4d", - "exec": [ - "var schema = {", - " \"$id\": \"https://bcda.cms.gov/schemas/health.json\",", - " \"database\": { \"type\": \"string\" }", - "};", - "", - "var Ajv = require('ajv');", - "var ajv = new Ajv({schemas: [schema]});", - "", - "pm.test(\"Response is 'ok'\", function () {", - " pm.response.to.have.status(200);", - "});", - "", - "pm.test(\"Body contains 'ok'\", function () {", - " pm.expect(pm.response.text()).to.include(\"ok\");", - "});", - "", - "pm.test('Schema is valid', function() {", - " pm.expect(ajv.validate(schema, pm.response.text())).to.be.true;", - "});", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [], - "url": { - "raw": "{{scheme}}://{{host}}:{{public-port}}/_health", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "port": "{{public-port}}", - "path": [ - "_health" - ] - } - }, - "response": [] - }, - { - "name": "admin _version", - "event": [ - { - "listen": "test", - "script": { - "id": "478577ae-564e-4264-92d0-1aa90421cfd7", - "exec": [ - "var schema = {", - " \"properties\": {", - " \"version\": { \"type\": \"string\" },", - " },", - " \"additionalProperties\": false", - "};", - "var Ajv = require('ajv');", - "var ajv = new Ajv({schemas: [schema]});", - "", - "pm.test(\"Response is 'ok'\", function () {", - " pm.response.to.have.status(200);", - "});", - "", - "// Enable when actual version returned", - "//", - "// pm.test(\"Response is JSON\", function () {", - " // pm.response.to.have.jsonBody(\"version\")", - "// });", - "", - "//pm.test('Schema is valid', function() {", - "// pm.expect(ajv.validate(schema, pm.response.text())).to.be.true;", - "//});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [], - "url": { - "raw": "{{scheme}}://{{host}}:{{public-port}}/_version", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "port": "{{public-port}}", - "path": [ - "_version" - ] - } - }, - "response": [] - }, - { - "name": "public info", - "event": [ - { - "listen": "test", - "script": { - "id": "f0cf6a0b-e506-4d55-88ef-72678806fd9c", - "exec": [ - "var schema = {", - " \"properties\": {", - " \"banner\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } },", - " \"routes\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } }", - " }", - "};", - "var Ajv = require('ajv');", - "var ajv = new Ajv({schemas: [schema]});", - "", - "pm.test(\"Response is 'ok'\", function () {", - " pm.response.to.have.status(200);", - "});", - "", - "pm.test(\"Body contains path of an endpoint\", function () {", - " pm.expect(pm.response.text()).to.include(\"/_info\");", - "});", - "", - "pm.test('Schema is valid', function() {", - " pm.expect(ajv.validate(schema, pm.response.text())).to.be.true;", - "});", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [], - "url": { - "raw": "{{scheme}}://{{host}}:{{public-port}}/_info", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "port": "{{public-port}}", - "path": [ - "_info" - ] - } - }, - "response": [] - }, - { - "name": "public health", - "event": [ - { - "listen": "test", - "script": { - "id": "ffd17434-9290-4cb2-b1b0-c1ae84f65d0a", - "exec": [ - "var schema = {", - " \"$id\": \"https://bcda.cms.gov/schemas/health.json\",", - " \"database\": { \"type\": \"string\" }", - "};", - "", - "var Ajv = require('ajv');", - "var ajv = new Ajv({schemas: [schema]});", - "", - "pm.test(\"Response is 'ok'\", function () {", - " pm.response.to.have.status(200);", - "});", - "", - "pm.test(\"Body contains 'ok'\", function () {", - " pm.expect(pm.response.text()).to.include(\"ok\");", - "});", - "", - "pm.test('Schema is valid', function() {", - " pm.expect(ajv.validate(schema, pm.response.text())).to.be.true;", - "});", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [], - "url": { - "raw": "{{scheme}}://{{host}}:{{public-port}}/_health", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "port": "{{public-port}}", - "path": [ - "_health" - ] - } - }, - "response": [] - }, - { - "name": "public _version", - "event": [ - { - "listen": "test", - "script": { - "id": "34204d21-bf63-4a3f-a496-19bc2557fce9", - "exec": [ - "var schema = {", - " \"properties\": {", - " \"version\": { \"type\": \"string\" },", - " },", - " \"additionalProperties\": false", - "};", - "var Ajv = require('ajv');", - "var ajv = new Ajv({schemas: [schema]});", - "", - "pm.test(\"Response is 'ok'\", function () {", - " pm.response.to.have.status(200);", - "});", - "", - "// Enable when actual version returned", - "//", - "// pm.test(\"Response is JSON\", function () {", - " // pm.response.to.have.jsonBody(\"version\")", - "// });", - "", - "//pm.test('Schema is valid', function() {", - "// pm.expect(ajv.validate(schema, pm.response.text())).to.be.true;", - "//});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [], - "url": { - "raw": "{{scheme}}://{{host}}:{{public-port}}/_version", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "port": "{{public-port}}", - "path": [ - "_version" - ] - } - }, - "response": [] - }, - { - "name": "create group", - "event": [ - { - "listen": "test", - "script": { - "id": "20ed753f-bd64-4270-9993-305d4efc1372", - "exec": [ - "var schema = {", - " \"properties\": {", - " \"ID\": { \"type\": \"integer\" },", - " \"group_id\": { \"type\": \"string\" },", - " \"data\": { ", - " \"properties\": {", - " \"id\": { \"type\": \"string\" },", - " \"name\": { \"type\": \"string\" },", - " \"users\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } },", - " \"scopes\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } },", - " \"resources\": { ", - " \"type\": \"array\", \"items\": { ", - " \"properties\": {", - " \"id\": { \"type\": \"string\" },", - " \"name\": { \"type\": \"string\" },", - " \"scopes\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } }", - " }", - " }", - " },", - " \"system\": {", - " \"type\": \"array\", \"items\": {", - " \"properties\": {", - " \"client_id\": { \"type\": \"string\" },", - " \"software_id\": { \"type\": \"string\" },", - " \"client_name\": { \"type\": \"string\" }", - " }", - " }", - " }", - " }", - " }", - " }", - "};", - "var Ajv = require('ajv');", - "var ajv = new Ajv({schemas: [schema]});", - "", - "pm.test(\"Response is 'created'\", function () {", - " pm.response.to.have.status(201);", - "});", - "", - "pm.test(\"Group.group_id has expected value\", function () {", - " pm.response.to.have.jsonBody(\"group_id\", pm.environment.get(\"group.group_id\"))", - "});", - "", - "pm.test(\"Group.data is JSON\", function () {", - " pm.response.to.have.jsonBody(\"data\")", - "});", - "", - "pm.test('Schema is valid', function() {", - " pm.expect(ajv.validate(schema, pm.response.text())).to.be.true;", - "});", - "", - "var respJson = pm.response.json();", - "pm.environment.set(\"group.id\", respJson.ID);" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "id": "e6d558fb-7002-437a-b2cf-1bdcff29d62d", - "exec": [ - "const uuid = require('uuid')", - "pm.environment.set(\"group.group_id\", uuid());" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "basic", - "basic": [ - { - "key": "password", - "value": "{{adminClientSecret}}", - "type": "string" - }, - { - "key": "username", - "value": "{{adminClientId}}", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "name": "Content-Type", - "value": "application/json", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"group_id\": \"{{group.group_id}}\",\n \"name\": \"Smoke Test Group\",\n \"xdata\": \"{\\\"cms_ids\\\":[\\\"A9994\\\"]}\",\n \"resources\": [\n {\n \"id\": \"bcda\",\n \"name\": \"BCDA API\",\n \"scopes\": [\n \"bcda-api\"\n ]\n }\n ]\n}" - }, - "url": { - "raw": "{{scheme}}://{{host}}:{{admin-port}}/group", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "port": "{{admin-port}}", - "path": [ - "group" - ] - } - }, - "response": [] - }, - { - "name": "update group", - "event": [ - { - "listen": "test", - "script": { - "id": "8dffcb16-25eb-44bc-8be3-fe19b23dc302", - "exec": [ - "var schema = {", - " \"properties\": {", - " \"ID\": { \"type\": \"integer\" },", - " \"group_id\": { \"type\": \"string\" },", - " \"data\": { ", - " \"properties\": {", - " \"id\": { \"type\": \"string\" },", - " \"name\": { \"type\": \"string\" },", - " \"users\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } },", - " \"scopes\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } },", - " \"resources\": { ", - " \"type\": \"array\", \"items\": { ", - " \"properties\": {", - " \"id\": { \"type\": \"string\" },", - " \"name\": { \"type\": \"string\" },", - " \"scopes\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } }", - " }", - " }", - " },", - " \"system\": {", - " \"type\": \"array\", \"items\": {", - " \"properties\": {", - " \"client_id\": { \"type\": \"string\" },", - " \"software_id\": { \"type\": \"string\" },", - " \"client_name\": { \"type\": \"string\" }", - " }", - " }", - " }", - " }", - " }", - " }", - "};", - "var Ajv = require('ajv');", - "var ajv = new Ajv({schemas: [schema]});", - "", - "pm.test(\"Response is 'ok'\", function () {", - " pm.response.to.have.status(200);", - "});", - "", - "pm.test(\"Group.group_id has original value\", function () {", - " pm.response.to.have.jsonBody(\"group_id\", pm.environment.get(\"group.group_id\"))", - "});", - "", - "pm.test(\"Group.data is JSON\", function () {", - " pm.response.to.have.jsonBody(\"data\")", - "});", - "", - "pm.test('Schema is valid', function() {", - " pm.expect(ajv.validate(schema, pm.response.text())).to.be.true;", - "});", - "", - "var respObj = JSON.parse(pm.response.text());", - "pm.test(\"Scope is changed\", function() {", - " pm.expect(respObj.data.resources[0].scopes[0]).to.eql(\"new_scope\");", - "});", - "", - "pm.test(\"Group name has original value\", function() {", - " pm.expect(respObj.data.name).to.eql(\"Smoke Test Group\");", - "});", - "", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "basic", - "basic": [ - { - "key": "password", - "value": "{{adminClientSecret}}", - "type": "string" - }, - { - "key": "username", - "value": "{{adminClientId}}", - "type": "string" - } - ] - }, - "method": "PUT", - "header": [ - { - "key": "Content-Type", - "name": "Content-Type", - "type": "text", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"id\": \"this_id_is_ignored_and_should_be_omitted\",\n \"name\": \"this_name_is_ignored_and_should_be_omitted\",\n \"resources\": [\n {\n \"id\": \"bcda\",\n \"name\": \"BCDA API\",\n \"scopes\": [\n \"new_scope\"\n ]\n }\n ]\n}" - }, - "url": { - "raw": "{{scheme}}://{{host}}:{{admin-port}}/group/{{group.id}}", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "port": "{{admin-port}}", - "path": [ - "group", - "{{group.id}}" - ] - } - }, - "response": [] - }, - { - "name": "list group (present)", - "event": [ - { - "listen": "test", - "script": { - "id": "f4740e30-67e2-4ec7-bfae-9d44f7aa84fc", - "exec": [ - "var schema = {", - " \"properties\": {", - " \"ID\": { \"type\": \"integer\" },", - " \"group_id\": { \"type\": \"string\" },", - " \"data\": { ", - " \"properties\": {", - " \"id\": { \"type\": \"string\" },", - " \"name\": { \"type\": \"string\" },", - " \"users\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } },", - " \"scopes\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } },", - " \"resources\": { ", - " \"type\": \"array\", \"items\": { ", - " \"properties\": {", - " \"id\": { \"type\": \"string\" },", - " \"name\": { \"type\": \"string\" }, ", - " \"scopes\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } }", - " }", - " }", - " },", - " \"system\": {", - " \"type\": \"array\", \"items\": {", - " \"properties\": {", - " \"client_id\": { \"type\": \"string\" }, \"software_id\": { \"type\": \"string\" },", - " \"client_name\": { \"type\": \"string\" }", - " }", - " }", - " }", - " }", - " }", - " }", - "};", - "var Ajv = require('ajv');", - "var ajv = new Ajv({schemas: [schema]});", - "", - "pm.test(\"Response is 'ok'\", function () {", - " pm.response.to.have.status(200);", - "});", - "", - "pm.test('Schema is valid', function() {", - " pm.expect(ajv.validate(schema, pm.response.text())).to.be.true;", - "});", - "", - "pm.test(\"Response includes Group.group_id\", function() {", - " pm.expect(pm.response.text()).to.include(pm.environment.get(\"group.group_id\"));", - "});", - "", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "basic", - "basic": [ - { - "key": "password", - "value": "{{adminClientSecret}}", - "type": "string" - }, - { - "key": "username", - "value": "{{adminClientId}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [], - "url": { - "raw": "{{scheme}}://{{host}}:{{admin-port}}/group", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "port": "{{admin-port}}", - "path": [ - "group" - ] - } - }, - "response": [] - }, - { - "name": "create system", - "event": [ - { - "listen": "test", - "script": { - "id": "20ed753f-bd64-4270-9993-305d4efc1372", - "exec": [ - "var schema = {", - " \"properties\": {", - " \"client_id\": { \"type\": \"string\" },", - " \"client_secret\": { \"type\": \"string\" },", - " \"client_name\": { \"type\": \"string\" },", - " \"system_id\": { \"type\": \"string\" },", - " \"expires_at\": { \"type\": \"string\", \"format\": \"time\" }", - " }", - "};", - "var Ajv = require('ajv');", - "var ajv = new Ajv({schemas: [schema]});", - "", - "pm.test(\"Response is 'created'\", function () {", - " pm.response.to.have.status(201);", - "});", - "", - "pm.test(\"client_name has expected value\", function () {", - " pm.response.to.have.jsonBody(\"client_name\", \"System for group \" + pm.environment.get(\"group.group_id\"))", - "});", - "", - "pm.test('Schema is valid', function() {", - " pm.expect(ajv.validate(schema, pm.response.text())).to.be.true;", - "});", - "", - "var respJson = pm.response.json();", - "pm.environment.set(\"client_id\", respJson.client_id);", - "pm.environment.set(\"client_secret\", respJson.client_secret);", - "pm.environment.set(\"system_id\", respJson.system_id);" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "id": "6d53fcea-1dfa-4739-b2af-64eca365b31e", - "exec": [ - "const uuid = require('uuid')", - "pm.environment.set(\"tracking_id\", uuid());" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "basic", - "basic": [ - { - "key": "password", - "value": "{{adminClientSecret}}", - "type": "string" - }, - { - "key": "username", - "value": "{{adminClientId}}", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "name": "Content-Type", - "value": "application/json", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"client_name\": \"System for group {{group.group_id}}\",\n \"group_id\": \"{{group.group_id}}\",\n \"scope\": \"bcda-api\",\n \"public_key\": \"-----BEGIN PUBLIC KEY-----\\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArhxobShmNifzW3xznB+L\\nI8+hgaePpSGIFCtFz2IXGU6EMLdeufhADaGPLft9xjwdN1ts276iXQiaChKPA2CK\\n/CBpuKcnU3LhU8JEi7u/db7J4lJlh6evjdKVKlMuhPcljnIKAiGcWln3zwYrFCeL\\ncN0aTOt4xnQpm8OqHawJ18y0WhsWT+hf1DeBDWvdfRuAPlfuVtl3KkrNYn1yqCgQ\\nlT6v/WyzptJhSR1jxdR7XLOhDGTZUzlHXh2bM7sav2n1+sLsuCkzTJqWZ8K7k7cI\\nXK354CNpCdyRYUAUvr4rORIAUmcIFjaR3J4y/Dh2JIyDToOHg7vjpCtNnNoS+ON2\\nHwIDAQAB\\n-----END PUBLIC KEY-----\",\n \"tracking_id\": \"{{tracking_id}}\"\n}" - }, - "url": { - "raw": "{{scheme}}://{{host}}:{{admin-port}}/system", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "port": "{{admin-port}}", - "path": [ - "system" - ] - } - }, - "response": [] - }, - { - "name": "get system public key", - "event": [ - { - "listen": "test", - "script": { - "id": "20ed753f-bd64-4270-9993-305d4efc1372", - "exec": [ - "var schema = {", - " \"properties\": {", - " \"client_id\": { \"type\": \"string\" },", - " \"public_key\": { \"type\": \"string\" }", - " }", - "};", - "var Ajv = require('ajv');", - "var ajv = new Ajv({schemas: [schema]});", - "", - "pm.test(\"Response is 'ok'\", function () {", - " pm.response.to.have.status(200);", - "});", - "", - "pm.test('Schema is valid', function() {", - " pm.expect(ajv.validate(schema, pm.response.text())).to.be.true;", - "});", - "", - "pm.test(\"client_id has expected value\", function () {", - " pm.response.to.have.jsonBody(\"client_id\", pm.environment.get(\"client_id\"))", - "});", - "", - "pm.test(\"public_key has expected value\", function () {", - " pm.response.to.have.jsonBody(\"public_key\", \"-----BEGIN PUBLIC KEY-----\\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArhxobShmNifzW3xznB+L\\nI8+hgaePpSGIFCtFz2IXGU6EMLdeufhADaGPLft9xjwdN1ts276iXQiaChKPA2CK\\n/CBpuKcnU3LhU8JEi7u/db7J4lJlh6evjdKVKlMuhPcljnIKAiGcWln3zwYrFCeL\\ncN0aTOt4xnQpm8OqHawJ18y0WhsWT+hf1DeBDWvdfRuAPlfuVtl3KkrNYn1yqCgQ\\nlT6v/WyzptJhSR1jxdR7XLOhDGTZUzlHXh2bM7sav2n1+sLsuCkzTJqWZ8K7k7cI\\nXK354CNpCdyRYUAUvr4rORIAUmcIFjaR3J4y/Dh2JIyDToOHg7vjpCtNnNoS+ON2\\nHwIDAQAB\\n-----END PUBLIC KEY-----\")", - "});", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "basic", - "basic": [ - { - "key": "password", - "value": "{{adminClientSecret}}", - "type": "string" - }, - { - "key": "username", - "value": "{{adminClientId}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Content-Type", - "name": "Content-Type", - "type": "text", - "value": "application/json" - } - ], - "url": { - "raw": "{{scheme}}://{{host}}:{{admin-port}}/system/{{system_id}}/key", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "port": "{{admin-port}}", - "path": [ - "system", - "{{system_id}}", - "key" - ] - } - }, - "response": [] - }, - { - "name": "get system token", - "event": [ - { - "listen": "test", - "script": { - "id": "76825199-0e23-4120-a0a8-c83b67899915", - "exec": [ - "var schema = {", - " \"properties\": {", - " \"access_token\": { \"type\": \"string\" },", - " \"token_type\": { \"type\": \"string\" },", - " \"expires_in\": { \"type\": \"string\", \"format\": \"time\" }", - " }", - "};", - "var Ajv = require('ajv');", - "var ajv = new Ajv({schemas: [schema]});", - "", - "pm.test(\"Response is 'created'\", function () {", - " pm.response.to.have.status(200);", - "});", - "", - "pm.test(\"token_type has expected value\", function () {", - " pm.response.to.have.jsonBody(\"token_type\", \"bearer\")", - "});", - "", - "pm.test('Schema is valid', function() {", - " pm.expect(ajv.validate(schema, pm.response.text())).to.be.true;", - "});", - "", - "var respJson = pm.response.json();", - "pm.environment.set(\"token\", respJson.access_token);", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "basic", - "basic": [ - { - "key": "password", - "value": "{{client_secret}}", - "type": "string" - }, - { - "key": "username", - "value": "{{client_id}}", - "type": "string" - } - ] - }, - "method": "POST", - "header": [], - "url": { - "raw": "{{scheme}}://{{host}}:{{public-port}}/token", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "port": "{{public-port}}", - "path": [ - "token" - ] - }, - "description": "Verify a username and password" - }, - "response": [] - }, - { - "name": "token active", - "event": [ - { - "listen": "test", - "script": { - "id": "518dc44c-5f13-4d12-8239-037420892f5f", - "exec": [ - "var schema = {", - " \"properties\": {", - " \"active\": { \"type\": \"string\" }", - " }", - "};", - "var Ajv = require('ajv');", - "var ajv = new Ajv({schemas: [schema]});", - "", - "pm.test(\"Response is 'ok'\", function () {", - " pm.response.to.have.status(200);", - "});", - "", - "pm.test('Schema is valid', function() {", - " pm.expect(ajv.validate(schema, pm.response.text())).to.be.true;", - "});", - "", - "pm.test(\"Token is active\", function () {", - " pm.response.to.have.jsonBody(\"active\", true)", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "basic", - "basic": [ - { - "key": "username", - "value": "{{adminClientId}}", - "type": "string" - }, - { - "key": "password", - "value": "{{adminClientSecret}}", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "name": "Content-Type", - "value": "application/json", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "{\"token\":\"{{token}}\"}" - }, - "url": { - "raw": "{{scheme}}://{{host}}:{{public-port}}/introspect", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "port": "{{public-port}}", - "path": [ - "introspect" - ] - } - }, - "response": [] - }, - { - "name": "revoke token", - "event": [ - { - "listen": "test", - "script": { - "id": "95ce3470-8202-4e9f-b4f6-4fe36a09cb0d", - "exec": [ - "pm.test(\"Response is 200\", function () {", - " pm.response.to.have.status(200);", - "});" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "id": "2e3ad198-59dc-4ceb-8f05-0045b7e38c98", - "exec": [ - "var parts = pm.environment.get(\"token\").split('.'); // header, payload, signature", - "var t = JSON.parse(atob(parts[1]));", - "", - "", - "pm.test(\"Can parse token ID\", function () {", - " pm.expect(t.jti).to.not.eql(\"\");", - "});", - "", - "", - "pm.environment.set(\"token_id\", t.jti)" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "basic", - "basic": [ - { - "key": "password", - "value": "{{adminClientSecret}}", - "type": "string" - }, - { - "key": "username", - "value": "{{adminClientId}}", - "type": "string" - } - ] - }, - "method": "DELETE", - "header": [], - "url": { - "raw": "{{scheme}}://{{host}}:{{admin-port}}/token/{{token_id}}", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "port": "{{admin-port}}", - "path": [ - "token", - "{{token_id}}" - ] - } - }, - "response": [] - }, - { - "name": "token inactive", - "event": [ - { - "listen": "test", - "script": { - "id": "518dc44c-5f13-4d12-8239-037420892f5f", - "exec": [ - "var schema = {", - " \"properties\": {", - " \"active\": { \"type\": \"string\" }", - " }", - "};", - "var Ajv = require('ajv');", - "var ajv = new Ajv({schemas: [schema]});", - "", - "pm.test(\"Response is 'ok'\", function () {", - " pm.response.to.have.status(200);", - "});", - "", - "pm.test('Schema is valid', function() {", - " pm.expect(ajv.validate(schema, pm.response.text())).to.be.true;", - "});", - "", - "pm.test(\"Token is not active\", function () {", - " pm.response.to.have.jsonBody(\"active\", false)", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "basic", - "basic": [ - { - "key": "username", - "value": "{{adminClientId}}", - "type": "string" - }, - { - "key": "password", - "value": "{{adminClientSecret}}", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "name": "Content-Type", - "type": "text", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\"token\":\"{{token}}\"}" - }, - "url": { - "raw": "{{scheme}}://{{host}}:{{public-port}}/introspect", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "port": "{{public-port}}", - "path": [ - "introspect" - ] - } - }, - "response": [] - }, - { - "name": "reset system credentials", - "event": [ - { - "listen": "test", - "script": { - "id": "20ed753f-bd64-4270-9993-305d4efc1372", - "exec": [ - "var schema = {", - " \"properties\": {", - " \"client_id\": { \"type\": \"string\" },", - " \"secret\": { \"type\": \"string\" },", - " \"expires_in\": { \"type\": \"string\", \"format\": \"time\" }", - " }", - "};", - "var Ajv = require('ajv');", - "var ajv = new Ajv({schemas: [schema]});", - "", - "pm.test(\"Response is 'created'\", function () {", - " pm.response.to.have.status(201);", - "});", - "", - "pm.test('Schema is valid', function() {", - " pm.expect(ajv.validate(schema, pm.response.text())).to.be.true;", - "});", - "", - "// uncomment when client_id bug is fixed", - "//pm.test(\"client_id has expected value\", function () {", - "// pm.response.to.have.jsonBody(\"client_id\", pm.environment.get(\"client_id\"))", - "//});", - "", - "var respJson = pm.response.json();", - "", - "pm.test(\"client_secret is not blank\", function () {", - " pm.expect(respJson.client_secret).to.not.eql(\"\");", - "});", - "", - "// uncomment when client_id bug is fixed ", - "//pm.environment.set(\"client_id\", respJson.client_id); ", - "pm.environment.set(\"client_secret\", respJson.client_secret);", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "basic", - "basic": [ - { - "key": "password", - "value": "{{adminClientSecret}}", - "type": "string" - }, - { - "key": "username", - "value": "{{adminClientId}}", - "type": "string" - } - ] - }, - "method": "PUT", - "header": [ - { - "key": "Content-Type", - "name": "Content-Type", - "type": "text", - "value": "application/json" - } - ], - "url": { - "raw": "{{scheme}}://{{host}}:{{admin-port}}/system/{{system_id}}/credentials", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "port": "{{admin-port}}", - "path": [ - "system", - "{{system_id}}", - "credentials" - ] - } - }, - "response": [] - }, - { - "name": "get system token (new credentials)", - "event": [ - { - "listen": "test", - "script": { - "id": "76825199-0e23-4120-a0a8-c83b67899915", - "exec": [ - "var schema = {", - " \"properties\": {", - " \"access_token\": { \"type\": \"string\" },", - " \"token_type\": { \"type\": \"string\" },", - " \"expires_in\": { \"type\": \"string\", \"format\": \"time\" }", - " }", - "};", - "var Ajv = require('ajv');", - "var ajv = new Ajv({schemas: [schema]});", - "", - "pm.test(\"Response is 'created'\", function () {", - " pm.response.to.have.status(200);", - "});", - "", - "pm.test(\"token_type has expected value\", function () {", - " pm.response.to.have.jsonBody(\"token_type\", \"bearer\")", - "});", - "", - "pm.test('Schema is valid', function() {", - " pm.expect(ajv.validate(schema, pm.response.text())).to.be.true;", - "});", - "", - "var respJson = pm.response.json();", - "pm.environment.set(\"token\", respJson.access_token);", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "basic", - "basic": [ - { - "key": "password", - "value": "{{client_secret}}", - "type": "string" - }, - { - "key": "username", - "value": "{{client_id}}", - "type": "string" - } - ] - }, - "method": "POST", - "header": [], - "url": { - "raw": "{{scheme}}://{{host}}:{{public-port}}/token", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "port": "{{public-port}}", - "path": [ - "token" - ] - }, - "description": "Verify a username and password" - }, - "response": [] - }, - { - "name": "revoke system credentials", - "event": [ - { - "listen": "test", - "script": { - "id": "20ed753f-bd64-4270-9993-305d4efc1372", - "exec": [ - "pm.test(\"Response is 'ok'\", function () {", - " pm.response.to.have.status(200);", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "basic", - "basic": [ - { - "key": "password", - "value": "{{adminClientSecret}}", - "type": "string" - }, - { - "key": "username", - "value": "{{adminClientId}}", - "type": "string" - } - ] - }, - "method": "DELETE", - "header": [ - { - "key": "Content-Type", - "name": "Content-Type", - "type": "text", - "value": "application/json" - } - ], - "url": { - "raw": "{{scheme}}://{{host}}:{{admin-port}}/system/{{system_id}}/credentials", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "port": "{{admin-port}}", - "path": [ - "system", - "{{system_id}}", - "credentials" - ] - } - }, - "response": [] - }, - { - "name": "get system token denied", - "event": [ - { - "listen": "test", - "script": { - "id": "76825199-0e23-4120-a0a8-c83b67899915", - "exec": [ - "var schema = {", - " \"properties\": {", - " \"error\": { \"type\": \"string\" },", - " \"error_description\": { \"type\": \"string\" }", - " }", - "};", - "var Ajv = require('ajv');", - "var ajv = new Ajv({schemas: [schema]});", - "", - "pm.test(\"Response is 'bad request'\", function () {", - " pm.response.to.have.status(400);", - "});", - "", - "pm.test('Schema is valid', function() {", - " pm.expect(ajv.validate(schema, pm.response.text())).to.be.true;", - "});", - "", - "pm.test(\"Error has expected value\", function () {", - " pm.response.to.have.jsonBody(\"error\", \"Unauthorized\")", - "});", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "basic", - "basic": [ - { - "key": "password", - "value": "{{client_secret}}", - "type": "string" - }, - { - "key": "username", - "value": "{{client_id}}", - "type": "string" - } - ] - }, - "method": "POST", - "header": [], - "url": { - "raw": "{{scheme}}://{{host}}:{{public-port}}/token", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "port": "{{public-port}}", - "path": [ - "token" - ] - }, - "description": "Verify a username and password" - }, - "response": [] - }, - { - "name": "delete group and system", - "event": [ - { - "listen": "test", - "script": { - "id": "95ce3470-8202-4e9f-b4f6-4fe36a09cb0d", - "exec": [ - "pm.test(\"Response is 'ok'\", function () {", - " pm.response.to.have.status(200);", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "basic", - "basic": [ - { - "key": "password", - "value": "{{adminClientSecret}}", - "type": "string" - }, - { - "key": "username", - "value": "{{adminClientId}}", - "type": "string" - } - ] - }, - "method": "DELETE", - "header": [], - "url": { - "raw": "{{scheme}}://{{host}}:{{admin-port}}/group/{{group.id}}", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "port": "{{admin-port}}", - "path": [ - "group", - "{{group.id}}" - ] - } - }, - "response": [] - }, - { - "name": "list group (not present)", - "event": [ - { - "listen": "test", - "script": { - "id": "fc625fc2-e25e-4fd2-a110-fc1528839ca7", - "exec": [ - "pm.test(\"Response is 'ok'\", function () {", - " pm.response.to.have.status(200);", - "});", - "", - "pm.test(\"Response does not include Group.group_id\", function() {", - " pm.expect(pm.response.text()).to.not.include(pm.environment.get(\"group.group_id\"));", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "basic", - "basic": [ - { - "key": "password", - "value": "{{adminClientSecret}}", - "type": "string" - }, - { - "key": "username", - "value": "{{adminClientId}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [], - "url": { - "raw": "{{scheme}}://{{host}}:{{admin-port}}/group", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "port": "{{admin-port}}", - "path": [ - "group" - ] - } - }, - "response": [] - } - ], - "event": [ - { - "listen": "prerequest", - "script": { - "id": "70fab265-98e0-4061-9553-ff8d6d75ac9e", - "type": "text/javascript", - "exec": [ - "" - ] - } - }, - { - "listen": "test", - "script": { - "id": "4019121e-102f-45b0-bf5b-e1bf6a9c6595", - "type": "text/javascript", - "exec": [ - "" - ] - } - } - ] -} diff --git a/test/postman_test/manual-SSAS.postman_collection.json b/test/postman_test/manual-SSAS.postman_collection.json deleted file mode 100644 index 194a3f416..000000000 --- a/test/postman_test/manual-SSAS.postman_collection.json +++ /dev/null @@ -1,421 +0,0 @@ -{ - "info": { - "_postman_id": "46602b35-6ed4-4ebd-bbca-c426572f5c92", - "name": "manual-SSAS", - "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" - }, - "item": [ - { - "name": "admin info", - "request": { - "method": "GET", - "header": [], - "url": { - "raw": "localhost:3004/_info", - "host": [ - "localhost" - ], - "port": "3004", - "path": [ - "_info" - ] - } - }, - "response": [] - }, - { - "name": "admin health", - "request": { - "method": "GET", - "header": [], - "url": { - "raw": "localhost:3004/_health", - "host": [ - "localhost" - ], - "port": "3004", - "path": [ - "_health" - ] - } - }, - "response": [] - }, - { - "name": "admin _version", - "request": { - "method": "GET", - "header": [], - "url": { - "raw": "localhost:3004/_version", - "host": [ - "localhost" - ], - "port": "3004", - "path": [ - "_version" - ] - } - }, - "response": [] - }, - { - "name": "admin create group", - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{ \n\t\"id\":\"T0000\",\n\t\"name\":\"ACO Small\",\n\t\"users\":[ \n\t\t\"00uiqolo7fEFSfif70h7\",\n\t\t\"l0vckYyfyow4TZ0zOKek\",\n\t\t\"HqtEi2khroEZkH4sdIzj\"\n\t],\n\t\"resources\":[ \n\t\t{ \n\t\t\t\"id\":\"BCDA\",\n\t\t\t\"name\":\"BCDA API\",\n\t\t\t\"scopes\":[ \n\t\t\t\t\"bcda-api\"\n\t\t\t]\n\t\t}\n\t]\n}" - }, - "url": { - "raw": "http://localhost:3004/group", - "protocol": "http", - "host": [ - "localhost" - ], - "port": "3004", - "path": [ - "group" - ] - } - }, - "response": [] - }, - { - "name": "admin create bcda-admin group", - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{ \n\t\"id\":\"bcda-admin\",\n\t\"name\":\"BCDA Admin\",\n\t\"users\":[ \n\t\t\"00uiqolo7fEFSfif70h7\",\n\t\t\"l0vckYyfyow4TZ0zOKek\",\n\t\t\"HqtEi2khroEZkH4sdIzj\"\n\t],\n\t\"resources\":[ \n\t\t{ \n\t\t\t\"id\":\"BCDA\",\n\t\t\t\"name\":\"BCDA API\",\n\t\t\t\"scopes\":[ \n\t\t\t\t\"bcda-api\"\n\t\t\t]\n\t\t}\n\t]\n}" - }, - "url": { - "raw": "http://localhost:3004/group", - "protocol": "http", - "host": [ - "localhost" - ], - "port": "3004", - "path": [ - "group" - ] - } - }, - "response": [] - }, - { - "name": "admin register", - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "name": "Content-Type", - "value": "application/json", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"group_id\": \"T0000\",\n \"client_name\": \"my sister's evil twin\",\n \"scope\": \"bcda-api\",\n \"public_key\": {\n \"e\": \"AAEAAQ\",\n \"n\": \"ok6rvXu95337IxsDXrKzlIqw_I_zPDG8JyEw2CTOtNMoDi1QzpXQVMGj2snNEmvNYaCTmFf51I-EDgeFLLexr40jzBXlg72quV4aw4yiNuxkigW0gMA92OmaT2jMRIdDZM8mVokoxyPfLub2YnXHFq0XuUUgkX_TlutVhgGbyPN0M12teYZtMYo2AUzIRggONhHvnibHP0CPWDjCwSfp3On1Recn4DPxbn3DuGslF2myalmCtkujNcrhHLhwYPP-yZFb8e0XSNTcQvXaQxAqmnWH6NXcOtaeWMQe43PNTAyNinhndgI8ozG3Hz-1NzHssDH_yk6UYFSszhDbWAzyqw\",\n \"kty\": \"RSA\"\n },\n \"tracking_id\": \"777\"\n}" - }, - "url": { - "raw": "http://localhost:3004/system", - "protocol": "http", - "host": [ - "localhost" - ], - "port": "3004", - "path": [ - "system" - ] - }, - "description": "register a new system via POST /system" - }, - "response": [] - }, - { - "name": "admin register bcda system", - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "name": "Content-Type", - "value": "application/json", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"group_id\": \"T0000\",\n \"client_name\": \"my sister's evil twin\",\n \"scope\": \"bcda-api\",\n \"public_key\": {\n \"e\": \"AAEAAQ\",\n \"n\": \"ok6rvXu95337IxsDXrKzlIqw_I_zPDG8JyEw2CTOtNMoDi1QzpXQVMGj2snNEmvNYaCTmFf51I-EDgeFLLexr40jzBXlg72quV4aw4yiNuxkigW0gMA92OmaT2jMRIdDZM8mVokoxyPfLub2YnXHFq0XuUUgkX_TlutVhgGbyPN0M12teYZtMYo2AUzIRggONhHvnibHP0CPWDjCwSfp3On1Recn4DPxbn3DuGslF2myalmCtkujNcrhHLhwYPP-yZFb8e0XSNTcQvXaQxAqmnWH6NXcOtaeWMQe43PNTAyNinhndgI8ozG3Hz-1NzHssDH_yk6UYFSszhDbWAzyqw\",\n \"kty\": \"RSA\"\n },\n \"tracking_id\": \"777\"\n}" - }, - "url": { - "raw": "http://localhost:3004/system", - "protocol": "http", - "host": [ - "localhost" - ], - "port": "3004", - "path": [ - "system" - ] - }, - "description": "register a new system via POST /system" - }, - "response": [] - }, - { - "name": "public info", - "request": { - "method": "GET", - "header": [], - "url": { - "raw": "localhost:3003/_info", - "host": [ - "localhost" - ], - "port": "3003", - "path": [ - "_info" - ] - } - }, - "response": [] - }, - { - "name": "public health", - "request": { - "method": "GET", - "header": [], - "url": { - "raw": "localhost:3003/_health", - "host": [ - "localhost" - ], - "port": "3003", - "path": [ - "_health" - ] - } - }, - "response": [] - }, - { - "name": "public _version", - "request": { - "method": "GET", - "header": [], - "url": { - "raw": "localhost:3003/_version", - "host": [ - "localhost" - ], - "port": "3003", - "path": [ - "_version" - ] - } - }, - "response": [] - }, - { - "name": "public authn", - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\"login_id\":\"{{login_id}}\",\"password\":\"{{password}}\"}" - }, - "url": { - "raw": "http://localhost:3003/authn", - "protocol": "http", - "host": [ - "localhost" - ], - "port": "3003", - "path": [ - "authn" - ] - }, - "description": "Verify a username and password" - }, - "response": [] - }, - { - "name": "public authn/challenge", - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "", - "type": "string" - } - ] - }, - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\"login_id\":\"{{login_id}}\",\"factor_type\":\"{{factor_type}}\"}" - }, - "url": { - "raw": "http://localhost:3003/authn/challenge", - "protocol": "http", - "host": [ - "localhost" - ], - "port": "3003", - "path": [ - "authn", - "challenge" - ] - }, - "description": "Request that an MFA challenge passcode be sent, for example, via SMS" - }, - "response": [] - }, - { - "name": "public authn/verify", - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "", - "type": "string" - } - ] - }, - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\"login_id\":\"{{login_id}}\",\"factor_type\":\"{{factor_type}}\",\"passcode\":\"{{passcode}}\"}" - }, - "url": { - "raw": "http://localhost:3003/authn/verify", - "protocol": "http", - "host": [ - "localhost" - ], - "port": "3003", - "path": [ - "authn", - "verify" - ] - }, - "description": "Verify an MFA passcode" - }, - "response": [] - }, - { - "name": "public register", - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "name": "Content-Type", - "value": "application/json", - "type": "text" - }, - { - "key": "x-group-id", - "value": "{{group_id}}", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"client_name\": \"My favorite name in all the world\",\n \"scope\": \"bcda-api\",\n \"jwks\": {\n \"keys\": [\n {\n \"e\": \"AAEAAQ\",\n \"n\": \"ok6rvXu95337IxsDXrKzlIqw_I_zPDG8JyEw2CTOtNMoDi1QzpXQVMGj2snNEmvNYaCTmFf51I-EDgeFLLexr40jzBXlg72quV4aw4yiNuxkigW0gMA92OmaT2jMRIdDZM8mVokoxyPfLub2YnXHFq0XuUUgkX_TlutVhgGbyPN0M12teYZtMYo2AUzIRggONhHvnibHP0CPWDjCwSfp3On1Recn4DPxbn3DuGslF2myalmCtkujNcrhHLhwYPP-yZFb8e0XSNTcQvXaQxAqmnWH6NXcOtaeWMQe43PNTAyNinhndgI8ozG3Hz-1NzHssDH_yk6UYFSszhDbWAzyqw\",\n \"kty\": \"RSA\"\n }\n ]\n }\n}" - }, - "url": { - "raw": "http://localhost:3003/register", - "protocol": "http", - "host": [ - "localhost" - ], - "port": "3003", - "path": [ - "register" - ] - } - }, - "response": [] - }, - { - "name": "public reset", - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "name": "Content-Type", - "type": "text", - "value": "application/json" - }, - { - "key": "x-group-id", - "type": "text", - "value": "T0001" - } - ], - "body": { - "mode": "raw", - "raw": "{\"client_id\":\"{{client_id}}\"}" - }, - "url": { - "raw": "http://localhost:3003/reset", - "protocol": "http", - "host": [ - "localhost" - ], - "port": "3003", - "path": [ - "reset" - ] - } - }, - "response": [] - }, - { - "name": "public token ", - "request": { - "method": "GET", - "header": [], - "url": { - "raw": "" - } - }, - "response": [] - } - ] -} - diff --git a/test/postman_test/ssas-local.postman_environment.json b/test/postman_test/ssas-local.postman_environment.json deleted file mode 100644 index 2c5558ec9..000000000 --- a/test/postman_test/ssas-local.postman_environment.json +++ /dev/null @@ -1,54 +0,0 @@ -{ - "id": "116175dc-c290-4d89-b676-a76a2b550a51", - "name": "SSAS", - "values": [ - { - "key": "scheme", - "value": "http", - "enabled": true - }, - { - "key": "host", - "value": "ssas", - "enabled": true - }, - { - "key": "admin-port", - "value": "3004", - "enabled": true - }, - { - "key": "public-port", - "value": "3003", - "enabled": true - }, - { - "key": "factor_type", - "value": "SMS", - "enabled": true - }, - { - "key": "passcode", - "value": "123456", - "enabled": true - }, - { - "key": "login_id", - "value": "bcda_user2@cms.gov", - "enabled": true - }, - { - "key": "token_id", - "value": "any_token_id", - "enabled": true - }, - { - "key": "group_id", - "value": "T0001", - "enabled": true - } - ], - "_postman_variable_scope": "environment", - "_postman_exported_at": "2019-08-08T14:17:24.333Z", - "_postman_exported_using": "Postman/7.3.6" -} \ No newline at end of file diff --git a/test/smoke_test/bulk_data_requests.sh b/test/smoke_test/bulk_data_requests.sh new file mode 100755 index 000000000..977e0697b --- /dev/null +++ b/test/smoke_test/bulk_data_requests.sh @@ -0,0 +1,11 @@ +#!/bin/bash +# +# This script is intended to be run from within the Docker "smoke_test" container +# +set -e +echo "Running EOB" +go run bcda_client.go -host=api:3000 -clientID=$CLIENT_ID -clientSecret=$CLIENT_SECRET -endpoint=ExplanationOfBenefit +echo "Running Patient" +go run bcda_client.go -host=api:3000 -clientID=$CLIENT_ID -clientSecret=$CLIENT_SECRET -endpoint=Patient +echo "Running Coverage" +go run bcda_client.go -host=api:3000 -clientID=$CLIENT_ID -clientSecret=$CLIENT_SECRET -endpoint=Coverage \ No newline at end of file diff --git a/test/smoke_test/smoke_test.sh b/test/smoke_test/smoke_test.sh index 977e0697b..87824d03e 100755 --- a/test/smoke_test/smoke_test.sh +++ b/test/smoke_test/smoke_test.sh @@ -1,11 +1,34 @@ #!/bin/bash -# -# This script is intended to be run from within the Docker "smoke_test" container -# + set -e -echo "Running EOB" -go run bcda_client.go -host=api:3000 -clientID=$CLIENT_ID -clientSecret=$CLIENT_SECRET -endpoint=ExplanationOfBenefit -echo "Running Patient" -go run bcda_client.go -host=api:3000 -clientID=$CLIENT_ID -clientSecret=$CLIENT_SECRET -endpoint=Patient -echo "Running Coverage" -go run bcda_client.go -host=api:3000 -clientID=$CLIENT_ID -clientSecret=$CLIENT_SECRET -endpoint=Coverage \ No newline at end of file +function cleanup { + docker-compose exec db sh -c 'psql "postgres://postgres:toor@db:5432/bcda?sslmode=disable" -c "DELETE FROM jobs WHERE aco_id = '"'"$ACO_ID"'"'"' + docker-compose exec db sh -c 'psql "postgres://postgres:toor@db:5432/bcda?sslmode=disable" -c "DELETE FROM users WHERE aco_id = '"'"$ACO_ID"'"'"' + docker-compose exec db sh -c 'psql "postgres://postgres:toor@db:5432/bcda?sslmode=disable" -c "DELETE FROM acos WHERE cms_id = '"'"'A9996'"'"'"' + docker-compose exec db sh -c 'psql "postgres://postgres:toor@db:5432/bcda?sslmode=disable" -c "DELETE FROM encryption_keys WHERE system_id IN (SELECT id FROM systems WHERE group_id = '"'"'smoke-test-group'"'"')"' + docker-compose exec db sh -c 'psql "postgres://postgres:toor@db:5432/bcda?sslmode=disable" -c "DELETE FROM secrets WHERE system_id IN (SELECT id FROM systems WHERE group_id = '"'"'smoke-test-group'"'"')"' + docker-compose exec db sh -c 'psql "postgres://postgres:toor@db:5432/bcda?sslmode=disable" -c "DELETE FROM systems WHERE group_id = '"'"'smoke-test-group'"'"'"' + docker-compose exec db sh -c 'psql "postgres://postgres:toor@db:5432/bcda?sslmode=disable" -c "DELETE FROM groups WHERE group_id = '"'"'smoke-test-group'"'"'"' +} +trap cleanup EXIT + +docker-compose stop api +SSAS_URL="http://ssas:3004" SSAS_PUBLIC_URL="http://ssas:3003" BCDA_AUTH_PROVIDER=ssas BCDA_SSAS_CLIENT_ID=$BCDA_SSAS_CLIENT_ID BCDA_SSAS_SECRET=$BCDA_SSAS_SECRET SSAS_ADMIN_CLIENT_ID=$BCDA_SSAS_CLIENT_ID SSAS_ADMIN_CLIENT_SECRET=$BCDA_SSAS_SECRET DEBUG=true docker-compose up -d api ssas + +echo "waiting for API to rebuild with SSAS as auth provider" +sleep 30 + +ACO_ID=$(docker-compose exec api sh -c 'tmp/bcda create-aco --name "Smoke Test ACO" --cms-id A9996' | tail -n1 | tr -d '\r') +USER_ID=$(docker-compose exec api sh -c 'tmp/bcda create-user --name "SSAS Smoke Test User" --email ssassmoketest@example.com --aco-id '$ACO_ID | tail -n1) +SAVE_KEY_RESULT=$(docker-compose exec api sh -c 'tmp/bcda save-public-key --cms-id A9996 --key-file "../shared_files/ATO_public.pem"' | tail -n1) +GROUP_ID=$(docker-compose exec api sh -c 'tmp/bcda create-group --id "smoke-test-group" --name "Smoke Test Group" --aco-id A9996' | tail -n1 | tr -d '\r') +CREDS=($(docker-compose exec api sh -c 'tmp/bcda generate-client-credentials --cms-id A9996' | tail -n2 | tr -d '\r')) +CLIENT_ID=${CREDS[0]} +CLIENT_SECRET=${CREDS[1]} + +CLIENT_ID=$CLIENT_ID CLIENT_SECRET=$CLIENT_SECRET docker-compose -f docker-compose.test.yml run --rm -w /go/src/github.com/CMSgov/bcda-app/test/smoke_test tests sh bulk_data_requests.sh +docker-compose stop api ssas + +echo "waiting for API to rebuild with alpha as auth provider" +sleep 30 +docker-compose up -d api \ No newline at end of file diff --git a/test/smoke_test/ssas_test.sh b/test/smoke_test/ssas_test.sh deleted file mode 100755 index 872fdee92..000000000 --- a/test/smoke_test/ssas_test.sh +++ /dev/null @@ -1,29 +0,0 @@ -#!/bin/bash - -set -e -function cleanup { - docker-compose exec db sh -c 'psql "postgres://postgres:toor@db:5432/bcda?sslmode=disable" -c "DELETE FROM jobs WHERE aco_id = '"'"$ACO_ID"'"'"' - docker-compose exec db sh -c 'psql "postgres://postgres:toor@db:5432/bcda?sslmode=disable" -c "DELETE FROM users WHERE aco_id = '"'"$ACO_ID"'"'"' - docker-compose exec db sh -c 'psql "postgres://postgres:toor@db:5432/bcda?sslmode=disable" -c "DELETE FROM acos WHERE cms_id = '"'"'A9996'"'"'"' - docker-compose exec db sh -c 'psql "postgres://postgres:toor@db:5432/bcda?sslmode=disable" -c "DELETE FROM encryption_keys WHERE system_id IN (SELECT id FROM systems WHERE group_id = '"'"'smoke-test-group'"'"')"' - docker-compose exec db sh -c 'psql "postgres://postgres:toor@db:5432/bcda?sslmode=disable" -c "DELETE FROM secrets WHERE system_id IN (SELECT id FROM systems WHERE group_id = '"'"'smoke-test-group'"'"')"' - docker-compose exec db sh -c 'psql "postgres://postgres:toor@db:5432/bcda?sslmode=disable" -c "DELETE FROM systems WHERE group_id = '"'"'smoke-test-group'"'"'"' - docker-compose exec db sh -c 'psql "postgres://postgres:toor@db:5432/bcda?sslmode=disable" -c "DELETE FROM groups WHERE group_id = '"'"'smoke-test-group'"'"'"' -} -trap cleanup EXIT - -docker-compose stop api -SSAS_URL="http://ssas:3004" SSAS_PUBLIC_URL="http://ssas:3003" BCDA_AUTH_PROVIDER=ssas BCDA_SSAS_CLIENT_ID=$BCDA_SSAS_CLIENT_ID BCDA_SSAS_SECRET=$BCDA_SSAS_SECRET SSAS_ADMIN_CLIENT_ID=$BCDA_SSAS_CLIENT_ID SSAS_ADMIN_CLIENT_SECRET=$BCDA_SSAS_SECRET DEBUG=true docker-compose up -d api ssas - -echo "waiting for API to rebuild" -sleep 30 - -ACO_ID=$(docker-compose exec api sh -c 'tmp/bcda create-aco --name "Smoke Test ACO" --cms-id A9996' | tail -n1 | tr -d '\r') -USER_ID=$(docker-compose exec api sh -c 'tmp/bcda create-user --name "SSAS Smoke Test User" --email ssassmoketest@example.com --aco-id '$ACO_ID | tail -n1) -SAVE_KEY_RESULT=$(docker-compose exec api sh -c 'tmp/bcda save-public-key --cms-id A9996 --key-file "../shared_files/ATO_public.pem"' | tail -n1) -GROUP_ID=$(docker-compose exec api sh -c 'tmp/bcda create-group --id "smoke-test-group" --name "Smoke Test Group" --aco-id A9996' | tail -n1 | tr -d '\r') -CREDS=($(docker-compose exec api sh -c 'tmp/bcda generate-client-credentials --cms-id A9996' | tail -n2 | tr -d '\r')) -CLIENT_ID=${CREDS[0]} -CLIENT_SECRET=${CREDS[1]} - -CLIENT_ID=$CLIENT_ID CLIENT_SECRET=$CLIENT_SECRET docker-compose -f docker-compose.test.yml run --rm -w /go/src/github.com/CMSgov/bcda-app/test/smoke_test tests sh smoke_test.sh diff --git a/unit_test.sh b/unit_test.sh index e4e473fbc..048a8b375 100755 --- a/unit_test.sh +++ b/unit_test.sh @@ -36,18 +36,6 @@ DATABASE_URL=$TEST_DB_URL QUEUE_DATABASE_URL=$TEST_DB_URL go run github.com/CMSg echo "Successfully imported all CCLF Files" -echo "Migrating SSAS Database with GORM migration" - -DATABASE_URL=$TEST_DB_URL DEBUG=true go run github.com/CMSgov/bcda-app/ssas/service/main --migrate - -echo "SSAS Database migration complete" - -echo "Loading SSAS fixture data" - -DATABASE_URL=$TEST_DB_URL DEBUG=true go run github.com/CMSgov/bcda-app/ssas/service/main --add-fixture-data - -echo "Successfully loaded SSAS fixture data" - echo "Running unit tests and placing results/coverage in test_results/${timestamp} on host..." DATABASE_URL=$TEST_DB_URL QUEUE_DATABASE_URL=$TEST_DB_URL gotestsum --junitfile test_results/${timestamp}/junit.xml -- -race ./... -coverprofile test_results/${timestamp}/testcoverage.out 2>&1 | tee test_results/${timestamp}/testresults.out go tool cover -func test_results/${timestamp}/testcoverage.out > test_results/${timestamp}/testcov_byfunc.out diff --git a/unit_test_ssas.sh b/unit_test_ssas.sh deleted file mode 100755 index 89953ea45..000000000 --- a/unit_test_ssas.sh +++ /dev/null @@ -1,40 +0,0 @@ -#!/bin/bash -# -# This script is intended to be run from within the Docker "unit_test" container -# The docker-compose file brings forward the env vars: DB -# -set -e -set -o pipefail - -timestamp=`date +%Y-%m-%d_%H-%M-%S` -mkdir -p test_results/${timestamp} -mkdir -p test_results/latest - -echo "Setting up test DB (bcda_test)..." -DB_HOST_URL=${DB}?sslmode=disable -TEST_DB_URL=${DB}/bcda_test?sslmode=disable -echo "DB_HOST_URL is $DB_HOST_URL" -echo "TEST_DB_URL is $TEST_DB_URL" -usql $DB_HOST_URL -c 'drop database if exists bcda_test;' -usql $DB_HOST_URL -c 'create database bcda_test;' - -echo "Migrating SSAS Database with GORM migration" - -DATABASE_URL=$TEST_DB_URL DEBUG=true go run github.com/CMSgov/bcda-app/ssas/service/main --migrate - -echo "SSAS Database migration complete" - -echo "Loading SSAS fixture data" - -DATABASE_URL=$TEST_DB_URL DEBUG=true go run github.com/CMSgov/bcda-app/ssas/service/main --add-fixture-data - -echo "Successfully loaded SSAS fixture data" - -echo "Running SSAS unit tests and placing results/coverage in test_results/${timestamp} on host..." -DATABASE_URL=$TEST_DB_URL QUEUE_DATABASE_URL=$TEST_DB_URL gotestsum --debug --junitfile test_results/${timestamp}/junit.xml -- -race ./ssas/... -coverprofile test_results/${timestamp}/testcoverage.out 2>&1 | tee test_results/${timestamp}/testresults.out -go tool cover -func test_results/${timestamp}/testcoverage.out > test_results/${timestamp}/testcov_byfunc.out -echo TOTAL COVERAGE: $(tail -1 test_results/${timestamp}/testcov_byfunc.out | head -1) -go tool cover -html=test_results/${timestamp}/testcoverage.out -o test_results/${timestamp}/testcoverage.html -cp test_results/${timestamp}/* test_results/latest -echo "Cleaning up test DB (bcda_test)..." -usql $DB_HOST_URL -c 'drop database bcda_test;' diff --git a/vendor/github.com/patrickmn/go-cache/CONTRIBUTORS b/vendor/github.com/patrickmn/go-cache/CONTRIBUTORS deleted file mode 100644 index 2b16e9974..000000000 --- a/vendor/github.com/patrickmn/go-cache/CONTRIBUTORS +++ /dev/null @@ -1,9 +0,0 @@ -This is a list of people who have contributed code to go-cache. They, or their -employers, are the copyright holders of the contributed code. Contributed code -is subject to the license restrictions listed in LICENSE (as they were when the -code was contributed.) - -Dustin Sallings -Jason Mooberry -Sergey Shepelev -Alex Edwards diff --git a/vendor/github.com/patrickmn/go-cache/LICENSE b/vendor/github.com/patrickmn/go-cache/LICENSE deleted file mode 100644 index 30b9cade0..000000000 --- a/vendor/github.com/patrickmn/go-cache/LICENSE +++ /dev/null @@ -1,19 +0,0 @@ -Copyright (c) 2012-2018 Patrick Mylund Nielsen and the go-cache contributors - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. diff --git a/vendor/github.com/patrickmn/go-cache/README.md b/vendor/github.com/patrickmn/go-cache/README.md deleted file mode 100644 index c5789cc66..000000000 --- a/vendor/github.com/patrickmn/go-cache/README.md +++ /dev/null @@ -1,83 +0,0 @@ -# go-cache - -go-cache is an in-memory key:value store/cache similar to memcached that is -suitable for applications running on a single machine. Its major advantage is -that, being essentially a thread-safe `map[string]interface{}` with expiration -times, it doesn't need to serialize or transmit its contents over the network. - -Any object can be stored, for a given duration or forever, and the cache can be -safely used by multiple goroutines. - -Although go-cache isn't meant to be used as a persistent datastore, the entire -cache can be saved to and loaded from a file (using `c.Items()` to retrieve the -items map to serialize, and `NewFrom()` to create a cache from a deserialized -one) to recover from downtime quickly. (See the docs for `NewFrom()` for caveats.) - -### Installation - -`go get github.com/patrickmn/go-cache` - -### Usage - -```go -import ( - "fmt" - "github.com/patrickmn/go-cache" - "time" -) - -func main() { - // Create a cache with a default expiration time of 5 minutes, and which - // purges expired items every 10 minutes - c := cache.New(5*time.Minute, 10*time.Minute) - - // Set the value of the key "foo" to "bar", with the default expiration time - c.Set("foo", "bar", cache.DefaultExpiration) - - // Set the value of the key "baz" to 42, with no expiration time - // (the item won't be removed until it is re-set, or removed using - // c.Delete("baz") - c.Set("baz", 42, cache.NoExpiration) - - // Get the string associated with the key "foo" from the cache - foo, found := c.Get("foo") - if found { - fmt.Println(foo) - } - - // Since Go is statically typed, and cache values can be anything, type - // assertion is needed when values are being passed to functions that don't - // take arbitrary types, (i.e. interface{}). The simplest way to do this for - // values which will only be used once--e.g. for passing to another - // function--is: - foo, found := c.Get("foo") - if found { - MyFunction(foo.(string)) - } - - // This gets tedious if the value is used several times in the same function. - // You might do either of the following instead: - if x, found := c.Get("foo"); found { - foo := x.(string) - // ... - } - // or - var foo string - if x, found := c.Get("foo"); found { - foo = x.(string) - } - // ... - // foo can then be passed around freely as a string - - // Want performance? Store pointers! - c.Set("foo", &MyStruct, cache.DefaultExpiration) - if x, found := c.Get("foo"); found { - foo := x.(*MyStruct) - // ... - } -} -``` - -### Reference - -`godoc` or [http://godoc.org/github.com/patrickmn/go-cache](http://godoc.org/github.com/patrickmn/go-cache) diff --git a/vendor/github.com/patrickmn/go-cache/cache.go b/vendor/github.com/patrickmn/go-cache/cache.go deleted file mode 100644 index db88d2f2c..000000000 --- a/vendor/github.com/patrickmn/go-cache/cache.go +++ /dev/null @@ -1,1161 +0,0 @@ -package cache - -import ( - "encoding/gob" - "fmt" - "io" - "os" - "runtime" - "sync" - "time" -) - -type Item struct { - Object interface{} - Expiration int64 -} - -// Returns true if the item has expired. -func (item Item) Expired() bool { - if item.Expiration == 0 { - return false - } - return time.Now().UnixNano() > item.Expiration -} - -const ( - // For use with functions that take an expiration time. - NoExpiration time.Duration = -1 - // For use with functions that take an expiration time. Equivalent to - // passing in the same expiration duration as was given to New() or - // NewFrom() when the cache was created (e.g. 5 minutes.) - DefaultExpiration time.Duration = 0 -) - -type Cache struct { - *cache - // If this is confusing, see the comment at the bottom of New() -} - -type cache struct { - defaultExpiration time.Duration - items map[string]Item - mu sync.RWMutex - onEvicted func(string, interface{}) - janitor *janitor -} - -// Add an item to the cache, replacing any existing item. If the duration is 0 -// (DefaultExpiration), the cache's default expiration time is used. If it is -1 -// (NoExpiration), the item never expires. -func (c *cache) Set(k string, x interface{}, d time.Duration) { - // "Inlining" of set - var e int64 - if d == DefaultExpiration { - d = c.defaultExpiration - } - if d > 0 { - e = time.Now().Add(d).UnixNano() - } - c.mu.Lock() - c.items[k] = Item{ - Object: x, - Expiration: e, - } - // TODO: Calls to mu.Unlock are currently not deferred because defer - // adds ~200 ns (as of go1.) - c.mu.Unlock() -} - -func (c *cache) set(k string, x interface{}, d time.Duration) { - var e int64 - if d == DefaultExpiration { - d = c.defaultExpiration - } - if d > 0 { - e = time.Now().Add(d).UnixNano() - } - c.items[k] = Item{ - Object: x, - Expiration: e, - } -} - -// Add an item to the cache, replacing any existing item, using the default -// expiration. -func (c *cache) SetDefault(k string, x interface{}) { - c.Set(k, x, DefaultExpiration) -} - -// Add an item to the cache only if an item doesn't already exist for the given -// key, or if the existing item has expired. Returns an error otherwise. -func (c *cache) Add(k string, x interface{}, d time.Duration) error { - c.mu.Lock() - _, found := c.get(k) - if found { - c.mu.Unlock() - return fmt.Errorf("Item %s already exists", k) - } - c.set(k, x, d) - c.mu.Unlock() - return nil -} - -// Set a new value for the cache key only if it already exists, and the existing -// item hasn't expired. Returns an error otherwise. -func (c *cache) Replace(k string, x interface{}, d time.Duration) error { - c.mu.Lock() - _, found := c.get(k) - if !found { - c.mu.Unlock() - return fmt.Errorf("Item %s doesn't exist", k) - } - c.set(k, x, d) - c.mu.Unlock() - return nil -} - -// Get an item from the cache. Returns the item or nil, and a bool indicating -// whether the key was found. -func (c *cache) Get(k string) (interface{}, bool) { - c.mu.RLock() - // "Inlining" of get and Expired - item, found := c.items[k] - if !found { - c.mu.RUnlock() - return nil, false - } - if item.Expiration > 0 { - if time.Now().UnixNano() > item.Expiration { - c.mu.RUnlock() - return nil, false - } - } - c.mu.RUnlock() - return item.Object, true -} - -// GetWithExpiration returns an item and its expiration time from the cache. -// It returns the item or nil, the expiration time if one is set (if the item -// never expires a zero value for time.Time is returned), and a bool indicating -// whether the key was found. -func (c *cache) GetWithExpiration(k string) (interface{}, time.Time, bool) { - c.mu.RLock() - // "Inlining" of get and Expired - item, found := c.items[k] - if !found { - c.mu.RUnlock() - return nil, time.Time{}, false - } - - if item.Expiration > 0 { - if time.Now().UnixNano() > item.Expiration { - c.mu.RUnlock() - return nil, time.Time{}, false - } - - // Return the item and the expiration time - c.mu.RUnlock() - return item.Object, time.Unix(0, item.Expiration), true - } - - // If expiration <= 0 (i.e. no expiration time set) then return the item - // and a zeroed time.Time - c.mu.RUnlock() - return item.Object, time.Time{}, true -} - -func (c *cache) get(k string) (interface{}, bool) { - item, found := c.items[k] - if !found { - return nil, false - } - // "Inlining" of Expired - if item.Expiration > 0 { - if time.Now().UnixNano() > item.Expiration { - return nil, false - } - } - return item.Object, true -} - -// Increment an item of type int, int8, int16, int32, int64, uintptr, uint, -// uint8, uint32, or uint64, float32 or float64 by n. Returns an error if the -// item's value is not an integer, if it was not found, or if it is not -// possible to increment it by n. To retrieve the incremented value, use one -// of the specialized methods, e.g. IncrementInt64. -func (c *cache) Increment(k string, n int64) error { - c.mu.Lock() - v, found := c.items[k] - if !found || v.Expired() { - c.mu.Unlock() - return fmt.Errorf("Item %s not found", k) - } - switch v.Object.(type) { - case int: - v.Object = v.Object.(int) + int(n) - case int8: - v.Object = v.Object.(int8) + int8(n) - case int16: - v.Object = v.Object.(int16) + int16(n) - case int32: - v.Object = v.Object.(int32) + int32(n) - case int64: - v.Object = v.Object.(int64) + n - case uint: - v.Object = v.Object.(uint) + uint(n) - case uintptr: - v.Object = v.Object.(uintptr) + uintptr(n) - case uint8: - v.Object = v.Object.(uint8) + uint8(n) - case uint16: - v.Object = v.Object.(uint16) + uint16(n) - case uint32: - v.Object = v.Object.(uint32) + uint32(n) - case uint64: - v.Object = v.Object.(uint64) + uint64(n) - case float32: - v.Object = v.Object.(float32) + float32(n) - case float64: - v.Object = v.Object.(float64) + float64(n) - default: - c.mu.Unlock() - return fmt.Errorf("The value for %s is not an integer", k) - } - c.items[k] = v - c.mu.Unlock() - return nil -} - -// Increment an item of type float32 or float64 by n. Returns an error if the -// item's value is not floating point, if it was not found, or if it is not -// possible to increment it by n. Pass a negative number to decrement the -// value. To retrieve the incremented value, use one of the specialized methods, -// e.g. IncrementFloat64. -func (c *cache) IncrementFloat(k string, n float64) error { - c.mu.Lock() - v, found := c.items[k] - if !found || v.Expired() { - c.mu.Unlock() - return fmt.Errorf("Item %s not found", k) - } - switch v.Object.(type) { - case float32: - v.Object = v.Object.(float32) + float32(n) - case float64: - v.Object = v.Object.(float64) + n - default: - c.mu.Unlock() - return fmt.Errorf("The value for %s does not have type float32 or float64", k) - } - c.items[k] = v - c.mu.Unlock() - return nil -} - -// Increment an item of type int by n. Returns an error if the item's value is -// not an int, or if it was not found. If there is no error, the incremented -// value is returned. -func (c *cache) IncrementInt(k string, n int) (int, error) { - c.mu.Lock() - v, found := c.items[k] - if !found || v.Expired() { - c.mu.Unlock() - return 0, fmt.Errorf("Item %s not found", k) - } - rv, ok := v.Object.(int) - if !ok { - c.mu.Unlock() - return 0, fmt.Errorf("The value for %s is not an int", k) - } - nv := rv + n - v.Object = nv - c.items[k] = v - c.mu.Unlock() - return nv, nil -} - -// Increment an item of type int8 by n. Returns an error if the item's value is -// not an int8, or if it was not found. If there is no error, the incremented -// value is returned. -func (c *cache) IncrementInt8(k string, n int8) (int8, error) { - c.mu.Lock() - v, found := c.items[k] - if !found || v.Expired() { - c.mu.Unlock() - return 0, fmt.Errorf("Item %s not found", k) - } - rv, ok := v.Object.(int8) - if !ok { - c.mu.Unlock() - return 0, fmt.Errorf("The value for %s is not an int8", k) - } - nv := rv + n - v.Object = nv - c.items[k] = v - c.mu.Unlock() - return nv, nil -} - -// Increment an item of type int16 by n. Returns an error if the item's value is -// not an int16, or if it was not found. If there is no error, the incremented -// value is returned. -func (c *cache) IncrementInt16(k string, n int16) (int16, error) { - c.mu.Lock() - v, found := c.items[k] - if !found || v.Expired() { - c.mu.Unlock() - return 0, fmt.Errorf("Item %s not found", k) - } - rv, ok := v.Object.(int16) - if !ok { - c.mu.Unlock() - return 0, fmt.Errorf("The value for %s is not an int16", k) - } - nv := rv + n - v.Object = nv - c.items[k] = v - c.mu.Unlock() - return nv, nil -} - -// Increment an item of type int32 by n. Returns an error if the item's value is -// not an int32, or if it was not found. If there is no error, the incremented -// value is returned. -func (c *cache) IncrementInt32(k string, n int32) (int32, error) { - c.mu.Lock() - v, found := c.items[k] - if !found || v.Expired() { - c.mu.Unlock() - return 0, fmt.Errorf("Item %s not found", k) - } - rv, ok := v.Object.(int32) - if !ok { - c.mu.Unlock() - return 0, fmt.Errorf("The value for %s is not an int32", k) - } - nv := rv + n - v.Object = nv - c.items[k] = v - c.mu.Unlock() - return nv, nil -} - -// Increment an item of type int64 by n. Returns an error if the item's value is -// not an int64, or if it was not found. If there is no error, the incremented -// value is returned. -func (c *cache) IncrementInt64(k string, n int64) (int64, error) { - c.mu.Lock() - v, found := c.items[k] - if !found || v.Expired() { - c.mu.Unlock() - return 0, fmt.Errorf("Item %s not found", k) - } - rv, ok := v.Object.(int64) - if !ok { - c.mu.Unlock() - return 0, fmt.Errorf("The value for %s is not an int64", k) - } - nv := rv + n - v.Object = nv - c.items[k] = v - c.mu.Unlock() - return nv, nil -} - -// Increment an item of type uint by n. Returns an error if the item's value is -// not an uint, or if it was not found. If there is no error, the incremented -// value is returned. -func (c *cache) IncrementUint(k string, n uint) (uint, error) { - c.mu.Lock() - v, found := c.items[k] - if !found || v.Expired() { - c.mu.Unlock() - return 0, fmt.Errorf("Item %s not found", k) - } - rv, ok := v.Object.(uint) - if !ok { - c.mu.Unlock() - return 0, fmt.Errorf("The value for %s is not an uint", k) - } - nv := rv + n - v.Object = nv - c.items[k] = v - c.mu.Unlock() - return nv, nil -} - -// Increment an item of type uintptr by n. Returns an error if the item's value -// is not an uintptr, or if it was not found. If there is no error, the -// incremented value is returned. -func (c *cache) IncrementUintptr(k string, n uintptr) (uintptr, error) { - c.mu.Lock() - v, found := c.items[k] - if !found || v.Expired() { - c.mu.Unlock() - return 0, fmt.Errorf("Item %s not found", k) - } - rv, ok := v.Object.(uintptr) - if !ok { - c.mu.Unlock() - return 0, fmt.Errorf("The value for %s is not an uintptr", k) - } - nv := rv + n - v.Object = nv - c.items[k] = v - c.mu.Unlock() - return nv, nil -} - -// Increment an item of type uint8 by n. Returns an error if the item's value -// is not an uint8, or if it was not found. If there is no error, the -// incremented value is returned. -func (c *cache) IncrementUint8(k string, n uint8) (uint8, error) { - c.mu.Lock() - v, found := c.items[k] - if !found || v.Expired() { - c.mu.Unlock() - return 0, fmt.Errorf("Item %s not found", k) - } - rv, ok := v.Object.(uint8) - if !ok { - c.mu.Unlock() - return 0, fmt.Errorf("The value for %s is not an uint8", k) - } - nv := rv + n - v.Object = nv - c.items[k] = v - c.mu.Unlock() - return nv, nil -} - -// Increment an item of type uint16 by n. Returns an error if the item's value -// is not an uint16, or if it was not found. If there is no error, the -// incremented value is returned. -func (c *cache) IncrementUint16(k string, n uint16) (uint16, error) { - c.mu.Lock() - v, found := c.items[k] - if !found || v.Expired() { - c.mu.Unlock() - return 0, fmt.Errorf("Item %s not found", k) - } - rv, ok := v.Object.(uint16) - if !ok { - c.mu.Unlock() - return 0, fmt.Errorf("The value for %s is not an uint16", k) - } - nv := rv + n - v.Object = nv - c.items[k] = v - c.mu.Unlock() - return nv, nil -} - -// Increment an item of type uint32 by n. Returns an error if the item's value -// is not an uint32, or if it was not found. If there is no error, the -// incremented value is returned. -func (c *cache) IncrementUint32(k string, n uint32) (uint32, error) { - c.mu.Lock() - v, found := c.items[k] - if !found || v.Expired() { - c.mu.Unlock() - return 0, fmt.Errorf("Item %s not found", k) - } - rv, ok := v.Object.(uint32) - if !ok { - c.mu.Unlock() - return 0, fmt.Errorf("The value for %s is not an uint32", k) - } - nv := rv + n - v.Object = nv - c.items[k] = v - c.mu.Unlock() - return nv, nil -} - -// Increment an item of type uint64 by n. Returns an error if the item's value -// is not an uint64, or if it was not found. If there is no error, the -// incremented value is returned. -func (c *cache) IncrementUint64(k string, n uint64) (uint64, error) { - c.mu.Lock() - v, found := c.items[k] - if !found || v.Expired() { - c.mu.Unlock() - return 0, fmt.Errorf("Item %s not found", k) - } - rv, ok := v.Object.(uint64) - if !ok { - c.mu.Unlock() - return 0, fmt.Errorf("The value for %s is not an uint64", k) - } - nv := rv + n - v.Object = nv - c.items[k] = v - c.mu.Unlock() - return nv, nil -} - -// Increment an item of type float32 by n. Returns an error if the item's value -// is not an float32, or if it was not found. If there is no error, the -// incremented value is returned. -func (c *cache) IncrementFloat32(k string, n float32) (float32, error) { - c.mu.Lock() - v, found := c.items[k] - if !found || v.Expired() { - c.mu.Unlock() - return 0, fmt.Errorf("Item %s not found", k) - } - rv, ok := v.Object.(float32) - if !ok { - c.mu.Unlock() - return 0, fmt.Errorf("The value for %s is not an float32", k) - } - nv := rv + n - v.Object = nv - c.items[k] = v - c.mu.Unlock() - return nv, nil -} - -// Increment an item of type float64 by n. Returns an error if the item's value -// is not an float64, or if it was not found. If there is no error, the -// incremented value is returned. -func (c *cache) IncrementFloat64(k string, n float64) (float64, error) { - c.mu.Lock() - v, found := c.items[k] - if !found || v.Expired() { - c.mu.Unlock() - return 0, fmt.Errorf("Item %s not found", k) - } - rv, ok := v.Object.(float64) - if !ok { - c.mu.Unlock() - return 0, fmt.Errorf("The value for %s is not an float64", k) - } - nv := rv + n - v.Object = nv - c.items[k] = v - c.mu.Unlock() - return nv, nil -} - -// Decrement an item of type int, int8, int16, int32, int64, uintptr, uint, -// uint8, uint32, or uint64, float32 or float64 by n. Returns an error if the -// item's value is not an integer, if it was not found, or if it is not -// possible to decrement it by n. To retrieve the decremented value, use one -// of the specialized methods, e.g. DecrementInt64. -func (c *cache) Decrement(k string, n int64) error { - // TODO: Implement Increment and Decrement more cleanly. - // (Cannot do Increment(k, n*-1) for uints.) - c.mu.Lock() - v, found := c.items[k] - if !found || v.Expired() { - c.mu.Unlock() - return fmt.Errorf("Item not found") - } - switch v.Object.(type) { - case int: - v.Object = v.Object.(int) - int(n) - case int8: - v.Object = v.Object.(int8) - int8(n) - case int16: - v.Object = v.Object.(int16) - int16(n) - case int32: - v.Object = v.Object.(int32) - int32(n) - case int64: - v.Object = v.Object.(int64) - n - case uint: - v.Object = v.Object.(uint) - uint(n) - case uintptr: - v.Object = v.Object.(uintptr) - uintptr(n) - case uint8: - v.Object = v.Object.(uint8) - uint8(n) - case uint16: - v.Object = v.Object.(uint16) - uint16(n) - case uint32: - v.Object = v.Object.(uint32) - uint32(n) - case uint64: - v.Object = v.Object.(uint64) - uint64(n) - case float32: - v.Object = v.Object.(float32) - float32(n) - case float64: - v.Object = v.Object.(float64) - float64(n) - default: - c.mu.Unlock() - return fmt.Errorf("The value for %s is not an integer", k) - } - c.items[k] = v - c.mu.Unlock() - return nil -} - -// Decrement an item of type float32 or float64 by n. Returns an error if the -// item's value is not floating point, if it was not found, or if it is not -// possible to decrement it by n. Pass a negative number to decrement the -// value. To retrieve the decremented value, use one of the specialized methods, -// e.g. DecrementFloat64. -func (c *cache) DecrementFloat(k string, n float64) error { - c.mu.Lock() - v, found := c.items[k] - if !found || v.Expired() { - c.mu.Unlock() - return fmt.Errorf("Item %s not found", k) - } - switch v.Object.(type) { - case float32: - v.Object = v.Object.(float32) - float32(n) - case float64: - v.Object = v.Object.(float64) - n - default: - c.mu.Unlock() - return fmt.Errorf("The value for %s does not have type float32 or float64", k) - } - c.items[k] = v - c.mu.Unlock() - return nil -} - -// Decrement an item of type int by n. Returns an error if the item's value is -// not an int, or if it was not found. If there is no error, the decremented -// value is returned. -func (c *cache) DecrementInt(k string, n int) (int, error) { - c.mu.Lock() - v, found := c.items[k] - if !found || v.Expired() { - c.mu.Unlock() - return 0, fmt.Errorf("Item %s not found", k) - } - rv, ok := v.Object.(int) - if !ok { - c.mu.Unlock() - return 0, fmt.Errorf("The value for %s is not an int", k) - } - nv := rv - n - v.Object = nv - c.items[k] = v - c.mu.Unlock() - return nv, nil -} - -// Decrement an item of type int8 by n. Returns an error if the item's value is -// not an int8, or if it was not found. If there is no error, the decremented -// value is returned. -func (c *cache) DecrementInt8(k string, n int8) (int8, error) { - c.mu.Lock() - v, found := c.items[k] - if !found || v.Expired() { - c.mu.Unlock() - return 0, fmt.Errorf("Item %s not found", k) - } - rv, ok := v.Object.(int8) - if !ok { - c.mu.Unlock() - return 0, fmt.Errorf("The value for %s is not an int8", k) - } - nv := rv - n - v.Object = nv - c.items[k] = v - c.mu.Unlock() - return nv, nil -} - -// Decrement an item of type int16 by n. Returns an error if the item's value is -// not an int16, or if it was not found. If there is no error, the decremented -// value is returned. -func (c *cache) DecrementInt16(k string, n int16) (int16, error) { - c.mu.Lock() - v, found := c.items[k] - if !found || v.Expired() { - c.mu.Unlock() - return 0, fmt.Errorf("Item %s not found", k) - } - rv, ok := v.Object.(int16) - if !ok { - c.mu.Unlock() - return 0, fmt.Errorf("The value for %s is not an int16", k) - } - nv := rv - n - v.Object = nv - c.items[k] = v - c.mu.Unlock() - return nv, nil -} - -// Decrement an item of type int32 by n. Returns an error if the item's value is -// not an int32, or if it was not found. If there is no error, the decremented -// value is returned. -func (c *cache) DecrementInt32(k string, n int32) (int32, error) { - c.mu.Lock() - v, found := c.items[k] - if !found || v.Expired() { - c.mu.Unlock() - return 0, fmt.Errorf("Item %s not found", k) - } - rv, ok := v.Object.(int32) - if !ok { - c.mu.Unlock() - return 0, fmt.Errorf("The value for %s is not an int32", k) - } - nv := rv - n - v.Object = nv - c.items[k] = v - c.mu.Unlock() - return nv, nil -} - -// Decrement an item of type int64 by n. Returns an error if the item's value is -// not an int64, or if it was not found. If there is no error, the decremented -// value is returned. -func (c *cache) DecrementInt64(k string, n int64) (int64, error) { - c.mu.Lock() - v, found := c.items[k] - if !found || v.Expired() { - c.mu.Unlock() - return 0, fmt.Errorf("Item %s not found", k) - } - rv, ok := v.Object.(int64) - if !ok { - c.mu.Unlock() - return 0, fmt.Errorf("The value for %s is not an int64", k) - } - nv := rv - n - v.Object = nv - c.items[k] = v - c.mu.Unlock() - return nv, nil -} - -// Decrement an item of type uint by n. Returns an error if the item's value is -// not an uint, or if it was not found. If there is no error, the decremented -// value is returned. -func (c *cache) DecrementUint(k string, n uint) (uint, error) { - c.mu.Lock() - v, found := c.items[k] - if !found || v.Expired() { - c.mu.Unlock() - return 0, fmt.Errorf("Item %s not found", k) - } - rv, ok := v.Object.(uint) - if !ok { - c.mu.Unlock() - return 0, fmt.Errorf("The value for %s is not an uint", k) - } - nv := rv - n - v.Object = nv - c.items[k] = v - c.mu.Unlock() - return nv, nil -} - -// Decrement an item of type uintptr by n. Returns an error if the item's value -// is not an uintptr, or if it was not found. If there is no error, the -// decremented value is returned. -func (c *cache) DecrementUintptr(k string, n uintptr) (uintptr, error) { - c.mu.Lock() - v, found := c.items[k] - if !found || v.Expired() { - c.mu.Unlock() - return 0, fmt.Errorf("Item %s not found", k) - } - rv, ok := v.Object.(uintptr) - if !ok { - c.mu.Unlock() - return 0, fmt.Errorf("The value for %s is not an uintptr", k) - } - nv := rv - n - v.Object = nv - c.items[k] = v - c.mu.Unlock() - return nv, nil -} - -// Decrement an item of type uint8 by n. Returns an error if the item's value is -// not an uint8, or if it was not found. If there is no error, the decremented -// value is returned. -func (c *cache) DecrementUint8(k string, n uint8) (uint8, error) { - c.mu.Lock() - v, found := c.items[k] - if !found || v.Expired() { - c.mu.Unlock() - return 0, fmt.Errorf("Item %s not found", k) - } - rv, ok := v.Object.(uint8) - if !ok { - c.mu.Unlock() - return 0, fmt.Errorf("The value for %s is not an uint8", k) - } - nv := rv - n - v.Object = nv - c.items[k] = v - c.mu.Unlock() - return nv, nil -} - -// Decrement an item of type uint16 by n. Returns an error if the item's value -// is not an uint16, or if it was not found. If there is no error, the -// decremented value is returned. -func (c *cache) DecrementUint16(k string, n uint16) (uint16, error) { - c.mu.Lock() - v, found := c.items[k] - if !found || v.Expired() { - c.mu.Unlock() - return 0, fmt.Errorf("Item %s not found", k) - } - rv, ok := v.Object.(uint16) - if !ok { - c.mu.Unlock() - return 0, fmt.Errorf("The value for %s is not an uint16", k) - } - nv := rv - n - v.Object = nv - c.items[k] = v - c.mu.Unlock() - return nv, nil -} - -// Decrement an item of type uint32 by n. Returns an error if the item's value -// is not an uint32, or if it was not found. If there is no error, the -// decremented value is returned. -func (c *cache) DecrementUint32(k string, n uint32) (uint32, error) { - c.mu.Lock() - v, found := c.items[k] - if !found || v.Expired() { - c.mu.Unlock() - return 0, fmt.Errorf("Item %s not found", k) - } - rv, ok := v.Object.(uint32) - if !ok { - c.mu.Unlock() - return 0, fmt.Errorf("The value for %s is not an uint32", k) - } - nv := rv - n - v.Object = nv - c.items[k] = v - c.mu.Unlock() - return nv, nil -} - -// Decrement an item of type uint64 by n. Returns an error if the item's value -// is not an uint64, or if it was not found. If there is no error, the -// decremented value is returned. -func (c *cache) DecrementUint64(k string, n uint64) (uint64, error) { - c.mu.Lock() - v, found := c.items[k] - if !found || v.Expired() { - c.mu.Unlock() - return 0, fmt.Errorf("Item %s not found", k) - } - rv, ok := v.Object.(uint64) - if !ok { - c.mu.Unlock() - return 0, fmt.Errorf("The value for %s is not an uint64", k) - } - nv := rv - n - v.Object = nv - c.items[k] = v - c.mu.Unlock() - return nv, nil -} - -// Decrement an item of type float32 by n. Returns an error if the item's value -// is not an float32, or if it was not found. If there is no error, the -// decremented value is returned. -func (c *cache) DecrementFloat32(k string, n float32) (float32, error) { - c.mu.Lock() - v, found := c.items[k] - if !found || v.Expired() { - c.mu.Unlock() - return 0, fmt.Errorf("Item %s not found", k) - } - rv, ok := v.Object.(float32) - if !ok { - c.mu.Unlock() - return 0, fmt.Errorf("The value for %s is not an float32", k) - } - nv := rv - n - v.Object = nv - c.items[k] = v - c.mu.Unlock() - return nv, nil -} - -// Decrement an item of type float64 by n. Returns an error if the item's value -// is not an float64, or if it was not found. If there is no error, the -// decremented value is returned. -func (c *cache) DecrementFloat64(k string, n float64) (float64, error) { - c.mu.Lock() - v, found := c.items[k] - if !found || v.Expired() { - c.mu.Unlock() - return 0, fmt.Errorf("Item %s not found", k) - } - rv, ok := v.Object.(float64) - if !ok { - c.mu.Unlock() - return 0, fmt.Errorf("The value for %s is not an float64", k) - } - nv := rv - n - v.Object = nv - c.items[k] = v - c.mu.Unlock() - return nv, nil -} - -// Delete an item from the cache. Does nothing if the key is not in the cache. -func (c *cache) Delete(k string) { - c.mu.Lock() - v, evicted := c.delete(k) - c.mu.Unlock() - if evicted { - c.onEvicted(k, v) - } -} - -func (c *cache) delete(k string) (interface{}, bool) { - if c.onEvicted != nil { - if v, found := c.items[k]; found { - delete(c.items, k) - return v.Object, true - } - } - delete(c.items, k) - return nil, false -} - -type keyAndValue struct { - key string - value interface{} -} - -// Delete all expired items from the cache. -func (c *cache) DeleteExpired() { - var evictedItems []keyAndValue - now := time.Now().UnixNano() - c.mu.Lock() - for k, v := range c.items { - // "Inlining" of expired - if v.Expiration > 0 && now > v.Expiration { - ov, evicted := c.delete(k) - if evicted { - evictedItems = append(evictedItems, keyAndValue{k, ov}) - } - } - } - c.mu.Unlock() - for _, v := range evictedItems { - c.onEvicted(v.key, v.value) - } -} - -// Sets an (optional) function that is called with the key and value when an -// item is evicted from the cache. (Including when it is deleted manually, but -// not when it is overwritten.) Set to nil to disable. -func (c *cache) OnEvicted(f func(string, interface{})) { - c.mu.Lock() - c.onEvicted = f - c.mu.Unlock() -} - -// Write the cache's items (using Gob) to an io.Writer. -// -// NOTE: This method is deprecated in favor of c.Items() and NewFrom() (see the -// documentation for NewFrom().) -func (c *cache) Save(w io.Writer) (err error) { - enc := gob.NewEncoder(w) - defer func() { - if x := recover(); x != nil { - err = fmt.Errorf("Error registering item types with Gob library") - } - }() - c.mu.RLock() - defer c.mu.RUnlock() - for _, v := range c.items { - gob.Register(v.Object) - } - err = enc.Encode(&c.items) - return -} - -// Save the cache's items to the given filename, creating the file if it -// doesn't exist, and overwriting it if it does. -// -// NOTE: This method is deprecated in favor of c.Items() and NewFrom() (see the -// documentation for NewFrom().) -func (c *cache) SaveFile(fname string) error { - fp, err := os.Create(fname) - if err != nil { - return err - } - err = c.Save(fp) - if err != nil { - fp.Close() - return err - } - return fp.Close() -} - -// Add (Gob-serialized) cache items from an io.Reader, excluding any items with -// keys that already exist (and haven't expired) in the current cache. -// -// NOTE: This method is deprecated in favor of c.Items() and NewFrom() (see the -// documentation for NewFrom().) -func (c *cache) Load(r io.Reader) error { - dec := gob.NewDecoder(r) - items := map[string]Item{} - err := dec.Decode(&items) - if err == nil { - c.mu.Lock() - defer c.mu.Unlock() - for k, v := range items { - ov, found := c.items[k] - if !found || ov.Expired() { - c.items[k] = v - } - } - } - return err -} - -// Load and add cache items from the given filename, excluding any items with -// keys that already exist in the current cache. -// -// NOTE: This method is deprecated in favor of c.Items() and NewFrom() (see the -// documentation for NewFrom().) -func (c *cache) LoadFile(fname string) error { - fp, err := os.Open(fname) - if err != nil { - return err - } - err = c.Load(fp) - if err != nil { - fp.Close() - return err - } - return fp.Close() -} - -// Copies all unexpired items in the cache into a new map and returns it. -func (c *cache) Items() map[string]Item { - c.mu.RLock() - defer c.mu.RUnlock() - m := make(map[string]Item, len(c.items)) - now := time.Now().UnixNano() - for k, v := range c.items { - // "Inlining" of Expired - if v.Expiration > 0 { - if now > v.Expiration { - continue - } - } - m[k] = v - } - return m -} - -// Returns the number of items in the cache. This may include items that have -// expired, but have not yet been cleaned up. -func (c *cache) ItemCount() int { - c.mu.RLock() - n := len(c.items) - c.mu.RUnlock() - return n -} - -// Delete all items from the cache. -func (c *cache) Flush() { - c.mu.Lock() - c.items = map[string]Item{} - c.mu.Unlock() -} - -type janitor struct { - Interval time.Duration - stop chan bool -} - -func (j *janitor) Run(c *cache) { - ticker := time.NewTicker(j.Interval) - for { - select { - case <-ticker.C: - c.DeleteExpired() - case <-j.stop: - ticker.Stop() - return - } - } -} - -func stopJanitor(c *Cache) { - c.janitor.stop <- true -} - -func runJanitor(c *cache, ci time.Duration) { - j := &janitor{ - Interval: ci, - stop: make(chan bool), - } - c.janitor = j - go j.Run(c) -} - -func newCache(de time.Duration, m map[string]Item) *cache { - if de == 0 { - de = -1 - } - c := &cache{ - defaultExpiration: de, - items: m, - } - return c -} - -func newCacheWithJanitor(de time.Duration, ci time.Duration, m map[string]Item) *Cache { - c := newCache(de, m) - // This trick ensures that the janitor goroutine (which--granted it - // was enabled--is running DeleteExpired on c forever) does not keep - // the returned C object from being garbage collected. When it is - // garbage collected, the finalizer stops the janitor goroutine, after - // which c can be collected. - C := &Cache{c} - if ci > 0 { - runJanitor(c, ci) - runtime.SetFinalizer(C, stopJanitor) - } - return C -} - -// Return a new cache with a given default expiration duration and cleanup -// interval. If the expiration duration is less than one (or NoExpiration), -// the items in the cache never expire (by default), and must be deleted -// manually. If the cleanup interval is less than one, expired items are not -// deleted from the cache before calling c.DeleteExpired(). -func New(defaultExpiration, cleanupInterval time.Duration) *Cache { - items := make(map[string]Item) - return newCacheWithJanitor(defaultExpiration, cleanupInterval, items) -} - -// Return a new cache with a given default expiration duration and cleanup -// interval. If the expiration duration is less than one (or NoExpiration), -// the items in the cache never expire (by default), and must be deleted -// manually. If the cleanup interval is less than one, expired items are not -// deleted from the cache before calling c.DeleteExpired(). -// -// NewFrom() also accepts an items map which will serve as the underlying map -// for the cache. This is useful for starting from a deserialized cache -// (serialized using e.g. gob.Encode() on c.Items()), or passing in e.g. -// make(map[string]Item, 500) to improve startup performance when the cache -// is expected to reach a certain minimum size. -// -// Only the cache's methods synchronize access to this map, so it is not -// recommended to keep any references to the map around after creating a cache. -// If need be, the map can be accessed at a later point using c.Items() (subject -// to the same caveat.) -// -// Note regarding serialization: When using e.g. gob, make sure to -// gob.Register() the individual types stored in the cache before encoding a -// map retrieved with c.Items(), and to register those same types before -// decoding a blob containing an items map. -func NewFrom(defaultExpiration, cleanupInterval time.Duration, items map[string]Item) *Cache { - return newCacheWithJanitor(defaultExpiration, cleanupInterval, items) -} diff --git a/vendor/github.com/patrickmn/go-cache/sharded.go b/vendor/github.com/patrickmn/go-cache/sharded.go deleted file mode 100644 index bcc0538bc..000000000 --- a/vendor/github.com/patrickmn/go-cache/sharded.go +++ /dev/null @@ -1,192 +0,0 @@ -package cache - -import ( - "crypto/rand" - "math" - "math/big" - insecurerand "math/rand" - "os" - "runtime" - "time" -) - -// This is an experimental and unexported (for now) attempt at making a cache -// with better algorithmic complexity than the standard one, namely by -// preventing write locks of the entire cache when an item is added. As of the -// time of writing, the overhead of selecting buckets results in cache -// operations being about twice as slow as for the standard cache with small -// total cache sizes, and faster for larger ones. -// -// See cache_test.go for a few benchmarks. - -type unexportedShardedCache struct { - *shardedCache -} - -type shardedCache struct { - seed uint32 - m uint32 - cs []*cache - janitor *shardedJanitor -} - -// djb2 with better shuffling. 5x faster than FNV with the hash.Hash overhead. -func djb33(seed uint32, k string) uint32 { - var ( - l = uint32(len(k)) - d = 5381 + seed + l - i = uint32(0) - ) - // Why is all this 5x faster than a for loop? - if l >= 4 { - for i < l-4 { - d = (d * 33) ^ uint32(k[i]) - d = (d * 33) ^ uint32(k[i+1]) - d = (d * 33) ^ uint32(k[i+2]) - d = (d * 33) ^ uint32(k[i+3]) - i += 4 - } - } - switch l - i { - case 1: - case 2: - d = (d * 33) ^ uint32(k[i]) - case 3: - d = (d * 33) ^ uint32(k[i]) - d = (d * 33) ^ uint32(k[i+1]) - case 4: - d = (d * 33) ^ uint32(k[i]) - d = (d * 33) ^ uint32(k[i+1]) - d = (d * 33) ^ uint32(k[i+2]) - } - return d ^ (d >> 16) -} - -func (sc *shardedCache) bucket(k string) *cache { - return sc.cs[djb33(sc.seed, k)%sc.m] -} - -func (sc *shardedCache) Set(k string, x interface{}, d time.Duration) { - sc.bucket(k).Set(k, x, d) -} - -func (sc *shardedCache) Add(k string, x interface{}, d time.Duration) error { - return sc.bucket(k).Add(k, x, d) -} - -func (sc *shardedCache) Replace(k string, x interface{}, d time.Duration) error { - return sc.bucket(k).Replace(k, x, d) -} - -func (sc *shardedCache) Get(k string) (interface{}, bool) { - return sc.bucket(k).Get(k) -} - -func (sc *shardedCache) Increment(k string, n int64) error { - return sc.bucket(k).Increment(k, n) -} - -func (sc *shardedCache) IncrementFloat(k string, n float64) error { - return sc.bucket(k).IncrementFloat(k, n) -} - -func (sc *shardedCache) Decrement(k string, n int64) error { - return sc.bucket(k).Decrement(k, n) -} - -func (sc *shardedCache) Delete(k string) { - sc.bucket(k).Delete(k) -} - -func (sc *shardedCache) DeleteExpired() { - for _, v := range sc.cs { - v.DeleteExpired() - } -} - -// Returns the items in the cache. This may include items that have expired, -// but have not yet been cleaned up. If this is significant, the Expiration -// fields of the items should be checked. Note that explicit synchronization -// is needed to use a cache and its corresponding Items() return values at -// the same time, as the maps are shared. -func (sc *shardedCache) Items() []map[string]Item { - res := make([]map[string]Item, len(sc.cs)) - for i, v := range sc.cs { - res[i] = v.Items() - } - return res -} - -func (sc *shardedCache) Flush() { - for _, v := range sc.cs { - v.Flush() - } -} - -type shardedJanitor struct { - Interval time.Duration - stop chan bool -} - -func (j *shardedJanitor) Run(sc *shardedCache) { - j.stop = make(chan bool) - tick := time.Tick(j.Interval) - for { - select { - case <-tick: - sc.DeleteExpired() - case <-j.stop: - return - } - } -} - -func stopShardedJanitor(sc *unexportedShardedCache) { - sc.janitor.stop <- true -} - -func runShardedJanitor(sc *shardedCache, ci time.Duration) { - j := &shardedJanitor{ - Interval: ci, - } - sc.janitor = j - go j.Run(sc) -} - -func newShardedCache(n int, de time.Duration) *shardedCache { - max := big.NewInt(0).SetUint64(uint64(math.MaxUint32)) - rnd, err := rand.Int(rand.Reader, max) - var seed uint32 - if err != nil { - os.Stderr.Write([]byte("WARNING: go-cache's newShardedCache failed to read from the system CSPRNG (/dev/urandom or equivalent.) Your system's security may be compromised. Continuing with an insecure seed.\n")) - seed = insecurerand.Uint32() - } else { - seed = uint32(rnd.Uint64()) - } - sc := &shardedCache{ - seed: seed, - m: uint32(n), - cs: make([]*cache, n), - } - for i := 0; i < n; i++ { - c := &cache{ - defaultExpiration: de, - items: map[string]Item{}, - } - sc.cs[i] = c - } - return sc -} - -func unexportedNewSharded(defaultExpiration, cleanupInterval time.Duration, shards int) *unexportedShardedCache { - if defaultExpiration == 0 { - defaultExpiration = -1 - } - sc := newShardedCache(shards, defaultExpiration) - SC := &unexportedShardedCache{sc} - if cleanupInterval > 0 { - runShardedJanitor(sc, cleanupInterval) - runtime.SetFinalizer(SC, stopShardedJanitor) - } - return SC -}